diff --git a/docs/reference/templates.rst b/docs/reference/templates.rst index 6b2da1023f..8ed2761b07 100644 --- a/docs/reference/templates.rst +++ b/docs/reference/templates.rst @@ -94,22 +94,42 @@ There are several other templates that can be customized, enabling you to fine-t * ``pager_results`` : renders the dropdown that lets you choose the number of elements per page on list views -Configuring templates ---------------------- +Configuring layouts and templates +--------------------------------- -The main goal of this template structure is to make it comfortable for you +The main goal of this layout/template structure is to make it comfortable for you to customize the ones you need. You can extend the ones you want in your own bundle, and -tell ``SonataAdminBundle`` to use your templates instead of the default ones. You can do so +tell ``SonataAdminBundle`` to use your layouts/templates instead of the default ones. You can do so in several ways. -You can specify your templates in the config file: +You can specify your layouts/templates in the config file: .. code-block:: yaml # config/packages/sonata_admin.yaml sonata_admin: - templates: + use_layouts: false + allow_layouts: [default] # [custom, default] - the first defined layout will be used as default + layouts: + default: + name: 'Sonata Admin - AdminLTE 2' + templates: + layout: '@SonataAdmin/standard_layout.html.twig' + ... + button_show: '@SonataAdmin/Button/show_button.html.twig' + form_theme: [] + filter_theme: [] + custom_layout: + name: 'Sonata Admin - AdminLTE 3' + templates: + layout: '@SonataAdmin/AdminLTE3/standard_layout.html.twig' + ... + button_show: '@SonataAdmin/AdminLTE3/Button/show_button.html.twig' + form_theme: [] + filter_theme: [] + + templates: # it is "default" layout layout: '@SonataAdmin/standard_layout.html.twig' ajax: '@SonataAdmin/ajax_layout.html.twig' list: '@SonataAdmin/CRUD/list.html.twig' @@ -167,12 +187,13 @@ can specify the templates to use in the ``Admin`` service definition: class: App\Admin\PostAdmin calls: - [setTemplate, ['edit', 'PostAdmin/edit.html.twig']] + - [setTemplate, ['edit', 'PostAdmin/edit.html.twig', 'custom_layout']] tags: - { name: sonata.admin, model_class: App\Entity\Post, manager_type: orm, group: 'Content', label: 'Post' } .. note:: - A ``setTemplates(array $templates)`` (notice the plural) method also + A ``setTemplates(array $templates, string $layout = 'default')`` (notice the plural) method also exists, that allows you to set multiple templates at once. Changes made using the ``setTemplate()`` and ``setTemplates()`` methods diff --git a/src/Controller/CRUDController.php b/src/Controller/CRUDController.php index 39496e8107..5c66493bfe 100644 --- a/src/Controller/CRUDController.php +++ b/src/Controller/CRUDController.php @@ -26,6 +26,8 @@ use Sonata\AdminBundle\Form\FormErrorIteratorToConstraintViolationList; use Sonata\AdminBundle\Model\AuditManagerInterface; use Sonata\AdminBundle\Request\AdminFetcherInterface; +use Sonata\AdminBundle\Templating\LayoutStorage\CookieLayoutStorage; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Templating\TemplateRegistryInterface; use Sonata\AdminBundle\Util\AdminAclUserManagerInterface; use Sonata\AdminBundle\Util\AdminObjectAclData; @@ -94,6 +96,7 @@ public static function getSubscribedServices(): array 'sonata.exporter.exporter' => '?'.ExporterInterface::class, 'sonata.admin.admin_exporter' => '?'.AdminExporter::class, 'sonata.admin.security.acl_user_manager' => '?'.AdminAclUserManagerInterface::class, + 'sonata.admin.layout_cookie_storage' => LayoutStorageInterface::class, 'controller_resolver' => 'controller_resolver', 'http_kernel' => HttpKernelInterface::class, @@ -127,7 +130,7 @@ public function listAction(Request $request): Response // set the theme for the current Admin Form $this->setFormTheme($formView, $this->admin->getFilterTheme()); - $template = $this->templateRegistry->getTemplate('list'); + $template = $this->getTemplate('list'); if ($this->container->has('sonata.admin.admin_exporter')) { $exporter = $this->container->get('sonata.admin.admin_exporter'); @@ -262,7 +265,7 @@ public function deleteAction(Request $request): Response return $this->redirectTo($request, $object); } - $template = $this->templateRegistry->getTemplate('delete'); + $template = $this->getTemplate('delete'); /** * @psalm-suppress DeprecatedMethod @@ -373,7 +376,7 @@ public function editAction(Request $request): Response // set the theme for the current Admin Form $this->setFormTheme($formView, $this->admin->getFormTheme()); - $template = $this->templateRegistry->getTemplate($templateKey); + $template = $this->getTemplate($templateKey); /** * @psalm-suppress DeprecatedMethod @@ -496,7 +499,7 @@ public function batchAction(Request $request): Response $formView = $datagrid->getForm()->createView(); $this->setFormTheme($formView, $this->admin->getFilterTheme()); - $template = $batchAction['template'] ?? $this->templateRegistry->getTemplate('batch_confirmation'); + $template = $batchAction['template'] ?? $this->getTemplate('batch_confirmation'); /** * @psalm-suppress DeprecatedMethod @@ -645,7 +648,7 @@ public function createAction(Request $request): Response // set the theme for the current Admin Form $this->setFormTheme($formView, $this->admin->getFormTheme()); - $template = $this->templateRegistry->getTemplate($templateKey); + $template = $this->getTemplate($templateKey); /** * @psalm-suppress DeprecatedMethod @@ -680,7 +683,7 @@ public function showAction(Request $request): Response $fields = $this->admin->getShow(); - $template = $this->templateRegistry->getTemplate('show'); + $template = $this->getTemplate('show'); /** * @psalm-suppress DeprecatedMethod @@ -721,7 +724,7 @@ public function historyAction(Request $request): Response $revisions = $reader->findRevisions($this->admin->getClass(), $objectId); - $template = $this->templateRegistry->getTemplate('history'); + $template = $this->getTemplate('history'); /** * @psalm-suppress DeprecatedMethod @@ -775,7 +778,7 @@ public function historyViewRevisionAction(Request $request, string $revision): R $this->admin->setSubject($object); - $template = $this->templateRegistry->getTemplate('show'); + $template = $this->getTemplate('show'); /** * @psalm-suppress DeprecatedMethod @@ -838,7 +841,7 @@ public function historyCompareRevisionsAction(Request $request, string $baseRevi $this->admin->setSubject($baseObject); - $template = $this->templateRegistry->getTemplate('show_compare'); + $template = $this->getTemplate('show_compare'); /** * @psalm-suppress DeprecatedMethod @@ -949,7 +952,7 @@ public function aclAction(Request $request): Response } } - $template = $this->templateRegistry->getTemplate('acl'); + $template = $this->getTemplate('acl'); /** * @psalm-suppress DeprecatedMethod @@ -997,9 +1000,9 @@ final public function setTwigGlobals(Request $request): void $this->setTwigGlobal('admin', $this->admin); if ($this->isXmlHttpRequest($request)) { - $baseTemplate = $this->templateRegistry->getTemplate('ajax'); + $baseTemplate = $this->getTemplate('ajax'); } else { - $baseTemplate = $this->templateRegistry->getTemplate('layout'); + $baseTemplate = $this->getTemplate('layout'); } $this->setTwigGlobal('base_template', $baseTemplate); @@ -1096,10 +1099,10 @@ protected function getBaseTemplate(): string \assert(null !== $request); if ($this->isXmlHttpRequest($request)) { - return $this->templateRegistry->getTemplate('ajax'); + return $this->getTemplate('ajax'); } - return $this->templateRegistry->getTemplate('layout'); + return $this->getTemplate('layout'); } /** @@ -1613,4 +1616,11 @@ private function equalsOrContains($haystack, object $needle): bool return false; } + + private function getTemplate(string $name): string + { + $cookieLayoutStorage = $this->container->get('sonata.admin.layout_cookie_storage'); + + return $this->templateRegistry->getTemplate($name, $cookieLayoutStorage->get()); + } } diff --git a/src/DependencyInjection/Admin/AbstractTaggedAdmin.php b/src/DependencyInjection/Admin/AbstractTaggedAdmin.php index dd0146551e..ac58a57425 100644 --- a/src/DependencyInjection/Admin/AbstractTaggedAdmin.php +++ b/src/DependencyInjection/Admin/AbstractTaggedAdmin.php @@ -524,13 +524,39 @@ final public function setTemplateRegistry(MutableTemplateRegistryInterface $temp $this->templateRegistry = $templateRegistry; } - final public function setTemplates(array $templates): void + final public function setTemplates(array $templates, /* string $layout = 'default' */): void { - $this->getTemplateRegistry()->setTemplates($templates); + if (\func_num_args() < 2) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $this->getTemplateRegistry()->setTemplates($templates, 'default'); + } + + $layout = \func_get_arg(1); + + // NEXT_MAJOR: Remove code before this comment + + $this->getTemplateRegistry()->setTemplates($templates, $layout); } - final public function setTemplate(string $name, string $template): void + final public function setTemplate(string $name, string $template, /* string $layout = 'default' */): void { - $this->getTemplateRegistry()->setTemplate($name, $template); + if (\func_num_args() < 2) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $this->getTemplateRegistry()->setTemplate($name, $template, 'default'); + } + + $layout = \func_get_arg(1); + + // NEXT_MAJOR: Remove code before this comment + + $this->getTemplateRegistry()->setTemplate($name, $template, $layout); } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 8c6b9d64b0..281b9d4385 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -123,6 +123,54 @@ * }, * search: bool, * show_mosaic_button: bool, + * use_layouts: bool, + * allow_layouts: list, + * layouts: array{ + * name: string, + * templates: array{ + * acl: string, + * action: string, + * action_create: string, + * add_block: string, + * ajax: string, + * base_list_field: string, + * batch: string, + * batch_confirmation: string, + * button_acl: string, + * button_create: string, + * button_edit: string, + * button_history: string, + * button_list: string, + * button_show: string, + * dashboard: string, + * delete: string, + * edit: string, + * filter: string, + * filter_theme: list, + * form_theme: list, + * history: string, + * history_revision_timestamp: string, + * inner_list_row: string, + * knp_menu_template: string, + * layout: string, + * list: string, + * list_block: string, + * outer_list_rows_list: string, + * outer_list_rows_mosaic: string, + * outer_list_rows_tree: string, + * pager_links: string, + * pager_results: string, + * preview: string, + * search: string, + * search_result_block: string, + * select: string, + * short_object_description: string, + * show: string, + * show_compare: string, + * tab_menu_template: string, + * user_block: string, + * }, + * }, * templates: array{ * acl: string, * action: string, @@ -560,6 +608,67 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() + ->booleanNode('use_layouts')->defaultFalse()->end() + ->arrayNode('allow_layouts') + ->prototype('scalar')->defaultValue(['default'])->end() + ->end() + ->arrayNode('layouts') + ->prototype('array') + ->children() + ->scalarNode('name')->cannotBeEmpty()->end() + ->arrayNode('templates') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('user_block')->defaultValue('@SonataAdmin/Core/user_block.html.twig')->cannotBeEmpty()->end() + ->scalarNode('add_block')->defaultValue('@SonataAdmin/Core/add_block.html.twig')->cannotBeEmpty()->end() + ->scalarNode('layout')->defaultValue('@SonataAdmin/standard_layout.html.twig')->cannotBeEmpty()->end() + ->scalarNode('ajax')->defaultValue('@SonataAdmin/ajax_layout.html.twig')->cannotBeEmpty()->end() + ->scalarNode('dashboard')->defaultValue('@SonataAdmin/Core/dashboard.html.twig')->cannotBeEmpty()->end() + ->scalarNode('search')->defaultValue('@SonataAdmin/Core/search.html.twig')->cannotBeEmpty()->end() + ->scalarNode('list')->defaultValue('@SonataAdmin/CRUD/list.html.twig')->cannotBeEmpty()->end() + ->scalarNode('filter')->defaultValue('@SonataAdmin/Form/filter_admin_fields.html.twig')->cannotBeEmpty()->end() + ->scalarNode('show')->defaultValue('@SonataAdmin/CRUD/show.html.twig')->cannotBeEmpty()->end() + ->scalarNode('show_compare')->defaultValue('@SonataAdmin/CRUD/show_compare.html.twig')->cannotBeEmpty()->end() + ->scalarNode('edit')->defaultValue('@SonataAdmin/CRUD/edit.html.twig')->cannotBeEmpty()->end() + ->scalarNode('preview')->defaultValue('@SonataAdmin/CRUD/preview.html.twig')->cannotBeEmpty()->end() + ->scalarNode('history')->defaultValue('@SonataAdmin/CRUD/history.html.twig')->cannotBeEmpty()->end() + ->scalarNode('acl')->defaultValue('@SonataAdmin/CRUD/acl.html.twig')->cannotBeEmpty()->end() + ->scalarNode('history_revision_timestamp')->defaultValue('@SonataAdmin/CRUD/history_revision_timestamp.html.twig')->cannotBeEmpty()->end() + ->scalarNode('action')->defaultValue('@SonataAdmin/CRUD/action.html.twig')->cannotBeEmpty()->end() + ->scalarNode('select')->defaultValue('@SonataAdmin/CRUD/list__select.html.twig')->cannotBeEmpty()->end() + ->scalarNode('list_block')->defaultValue('@SonataAdmin/Block/block_admin_list.html.twig')->cannotBeEmpty()->end() + ->scalarNode('search_result_block')->defaultValue('@SonataAdmin/Block/block_search_result.html.twig')->cannotBeEmpty()->end() + ->scalarNode('short_object_description')->defaultValue('@SonataAdmin/Helper/short-object-description.html.twig')->cannotBeEmpty()->end() + ->scalarNode('delete')->defaultValue('@SonataAdmin/CRUD/delete.html.twig')->cannotBeEmpty()->end() + ->scalarNode('batch')->defaultValue('@SonataAdmin/CRUD/list__batch.html.twig')->cannotBeEmpty()->end() + ->scalarNode('batch_confirmation')->defaultValue('@SonataAdmin/CRUD/batch_confirmation.html.twig')->cannotBeEmpty()->end() + ->scalarNode('inner_list_row')->defaultValue('@SonataAdmin/CRUD/list_inner_row.html.twig')->cannotBeEmpty()->end() + ->scalarNode('outer_list_rows_mosaic')->defaultValue('@SonataAdmin/CRUD/list_outer_rows_mosaic.html.twig')->cannotBeEmpty()->end() + ->scalarNode('outer_list_rows_list')->defaultValue('@SonataAdmin/CRUD/list_outer_rows_list.html.twig')->cannotBeEmpty()->end() + ->scalarNode('outer_list_rows_tree')->defaultValue('@SonataAdmin/CRUD/list_outer_rows_tree.html.twig')->cannotBeEmpty()->end() + ->scalarNode('base_list_field')->defaultValue('@SonataAdmin/CRUD/base_list_field.html.twig')->cannotBeEmpty()->end() + ->scalarNode('pager_links')->defaultValue('@SonataAdmin/Pager/links.html.twig')->cannotBeEmpty()->end() + ->scalarNode('pager_results')->defaultValue('@SonataAdmin/Pager/results.html.twig')->cannotBeEmpty()->end() + ->scalarNode('tab_menu_template')->defaultValue('@SonataAdmin/Core/tab_menu_template.html.twig')->cannotBeEmpty()->end() + ->scalarNode('knp_menu_template')->defaultValue('@SonataAdmin/Menu/sonata_menu.html.twig')->cannotBeEmpty()->end() + ->scalarNode('action_create')->defaultValue('@SonataAdmin/CRUD/dashboard__action_create.html.twig')->cannotBeEmpty()->end() + ->scalarNode('button_acl')->defaultValue('@SonataAdmin/Button/acl_button.html.twig')->cannotBeEmpty()->end() + ->scalarNode('button_create')->defaultValue('@SonataAdmin/Button/create_button.html.twig')->cannotBeEmpty()->end() + ->scalarNode('button_edit')->defaultValue('@SonataAdmin/Button/edit_button.html.twig')->cannotBeEmpty()->end() + ->scalarNode('button_history')->defaultValue('@SonataAdmin/Button/history_button.html.twig')->cannotBeEmpty()->end() + ->scalarNode('button_list')->defaultValue('@SonataAdmin/Button/list_button.html.twig')->cannotBeEmpty()->end() + ->scalarNode('button_show')->defaultValue('@SonataAdmin/Button/show_button.html.twig')->cannotBeEmpty()->end() + ->arrayNode('form_theme') + ->prototype('scalar')->end() + ->end() + ->arrayNode('filter_theme') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() ->arrayNode('templates') ->addDefaultsIfNotSet() ->children() diff --git a/src/DependencyInjection/SonataAdminExtension.php b/src/DependencyInjection/SonataAdminExtension.php index d4a15cac0c..bd5332fe5a 100644 --- a/src/DependencyInjection/SonataAdminExtension.php +++ b/src/DependencyInjection/SonataAdminExtension.php @@ -109,10 +109,13 @@ public function load(array $configs, ContainerBuilder $container): void if (false === $config['options']['lock_protection']) { $container->removeDefinition('sonata.admin.lock.extension'); } - + $defaultLayout = ['name' => 'AdminLTE2', 'templates' => $config['templates']]; $container->setParameter('sonata.admin.configuration.global_search.empty_boxes', $config['global_search']['empty_boxes']); $container->setParameter('sonata.admin.configuration.global_search.admin_route', $config['global_search']['admin_route']); $container->setParameter('sonata.admin.configuration.templates', $config['templates']); + $container->setParameter('sonata.admin.configuration.use_layouts', $config['use_layouts']); + $container->setParameter('sonata.admin.configuration.allow_layouts', $config['allow_layouts']); + $container->setParameter('sonata.admin.configuration.layouts', ['default' => $defaultLayout] + $config['layouts']); $container->setParameter('sonata.admin.configuration.default_admin_services', $config['default_admin_services']); $container->setParameter('sonata.admin.configuration.default_controller', $config['default_controller']); $container->setParameter('sonata.admin.configuration.dashboard_groups', $config['dashboard']['groups']); diff --git a/src/Resources/config/core.php b/src/Resources/config/core.php index 3ff2683220..dc34dbd89d 100644 --- a/src/Resources/config/core.php +++ b/src/Resources/config/core.php @@ -35,6 +35,8 @@ use Sonata\AdminBundle\Search\SearchHandler; use Sonata\AdminBundle\Search\SearchHandlerInterface; use Sonata\AdminBundle\SonataConfiguration; +use Sonata\AdminBundle\Templating\LayoutStorage\CookieLayoutStorage; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Templating\TemplateRegistry; use Sonata\AdminBundle\Translator\BCLabelTranslatorStrategy; use Sonata\AdminBundle\Translator\Extractor\AdminExtractor; @@ -153,6 +155,13 @@ param('sonata.admin.configuration.templates'), ]) + ->set('sonata.admin.layout_cookie_storage', CookieLayoutStorage::class) + ->args([ + service('request_stack'), + ]) + + ->alias(LayoutStorageInterface::class, 'sonata.admin.layout_cookie_storage') + ->set('sonata.admin.request.fetcher', AdminFetcher::class) ->args([ service('sonata.admin.pool'), diff --git a/src/Resources/config/twig.php b/src/Resources/config/twig.php index db366486cb..f8e36c0402 100644 --- a/src/Resources/config/twig.php +++ b/src/Resources/config/twig.php @@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Twig\BreadcrumbsRuntime; use Sonata\AdminBundle\Twig\CanonicalizeRuntime; use Sonata\AdminBundle\Twig\Extension\BreadcrumbsExtension; @@ -80,6 +81,8 @@ ->args([ service('sonata.admin.global_template_registry'), service('sonata.admin.pool'), + service(LayoutStorageInterface::class), + param('sonata.admin.configuration.use_layouts'), ]) // NEXT_MAJOR: Remove the `args()` call. diff --git a/src/Templating/AbstractTemplateRegistry.php b/src/Templating/AbstractTemplateRegistry.php index 04349a8f98..e1d06c04ba 100644 --- a/src/Templating/AbstractTemplateRegistry.php +++ b/src/Templating/AbstractTemplateRegistry.php @@ -16,34 +16,99 @@ abstract class AbstractTemplateRegistry implements TemplateRegistryInterface { /** - * @var array + * @deprecated since sonata-project/admin-bundle x.y, will be removed in 5.0. + * + * @var array 'name' => 'file_path.html.twig' */ protected $templates = []; + /** + * @var array> 'layout' => ['name' => 'file_path.html.twig'] + */ + protected array $layoutTemplates = []; + /** * @param string[] $templates */ public function __construct(array $templates = []) { $this->templates = $templates; + $this->layoutTemplates['default'] = $templates; } - final public function getTemplates(): array + final public function getTemplates(/* string $layout */): array { - return $this->templates; + if (\func_num_args() < 1) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $layout = 'default'; + } else { + $layout = func_get_arg(0); + } + + // Keep BC compatibility + if ('default' === $layout) { + return $this->templates; + } + + // NEXT_MAJOR: Remove code before this comment + + return $this->layoutTemplates[$layout] + $this->layoutTemplates['default']; } - final public function hasTemplate(string $name): bool + final public function hasTemplate(string $name /* ,string $layout */): bool { - return isset($this->templates[$name]); + if (\func_num_args() < 2) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $layout = 'default'; + } else { + $layout = func_get_arg(1); + } + + // Keep BC compatibility + if ('default' === $layout) { + return isset($this->templates[$name]); + } + + // NEXT_MAJOR: Remove code before this comment + + return isset($this->layoutTemplates[$layout][$name]) || isset($this->layoutTemplates['default'][$name]); } - final public function getTemplate(string $name): string + final public function getTemplate(string $name /* ,string $layout */): string { - if ($this->hasTemplate($name)) { - return $this->templates[$name]; + if (\func_num_args() < 2) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $layout = 'default'; + } else { + $layout = func_get_arg(1); + } + + // Keep BC compatibility + if ('default' === $layout) { + if ($this->hasTemplate($name)) { + return $this->templates[$name]; + } + + throw new \InvalidArgumentException(sprintf('Template named "%s" doesn\'t exist.', $name)); + } + + // NEXT_MAJOR: Remove code before this comment + if ($this->hasTemplate($name, $layout)) { + return $this->layoutTemplates[$layout][$name] ?? $this->layoutTemplates['default'][$name]; } - throw new \InvalidArgumentException(sprintf('Template named "%s" doesn\'t exist.', $name)); + throw new \InvalidArgumentException(sprintf('Template named "%s" for "%s" layout doesn\'t exist.', $name, $layout)); } } diff --git a/src/Templating/LayoutStorage/CookieLayoutStorage.php b/src/Templating/LayoutStorage/CookieLayoutStorage.php new file mode 100644 index 0000000000..b9a6f07fbe --- /dev/null +++ b/src/Templating/LayoutStorage/CookieLayoutStorage.php @@ -0,0 +1,24 @@ +requestStack->getCurrentRequest()?->cookies?->get(self::COOKIE_NAME, 'default') ?? 'default'; + } + + public function set(string $layout): void + { + $this->requestStack->getCurrentRequest()?->cookies?->set(self::COOKIE_NAME, $layout); + } +} diff --git a/src/Templating/LayoutStorage/LayoutStorageInterface.php b/src/Templating/LayoutStorage/LayoutStorageInterface.php new file mode 100644 index 0000000000..ad94254c79 --- /dev/null +++ b/src/Templating/LayoutStorage/LayoutStorageInterface.php @@ -0,0 +1,10 @@ +templates = $templates + $this->templates; + if (\func_num_args() < 2) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $layout = 'default'; + } else { + $layout = func_get_arg(1); + } + + // Keep BC compatibility + if ('default' === $layout) { + $this->templates = $templates + $this->templates; + } + // NEXT_MAJOR: Remove code before this comment + $this->layoutTemplates[$layout] = $templates + ($this->layoutTemplates[$layout] ?? []); } - public function setTemplate(string $name, string $template): void + public function setTemplate(string $name, string $template /* ,string $layout */): void { - $this->templates[$name] = $template; + if (\func_num_args() < 3) { + @trigger_error( + 'Not passing the "string $layout" argument explicitly is deprecated since sonata-project/admin-bundle x.y and will be required in 5.0.', + \E_USER_DEPRECATED + ); + + $layout = 'default'; + } else { + $layout = func_get_arg(2); + } + + // Keep BC compatibility + if ('default' === $layout) { + $this->templates[$name] = $template; + } + + // NEXT_MAJOR: Remove code before this comment + $this->layoutTemplates[$layout][$name] = $template; } } diff --git a/src/Templating/MutableTemplateRegistryAwareInterface.php b/src/Templating/MutableTemplateRegistryAwareInterface.php index 9a96d438df..f9ef208a5a 100644 --- a/src/Templating/MutableTemplateRegistryAwareInterface.php +++ b/src/Templating/MutableTemplateRegistryAwareInterface.php @@ -24,10 +24,10 @@ public function setTemplateRegistry(MutableTemplateRegistryInterface $templateRe public function hasTemplateRegistry(): bool; - public function setTemplate(string $name, string $template): void; + public function setTemplate(string $name, string $template /* ,string $layout = 'default' */): void; /** * @param array $templates */ - public function setTemplates(array $templates): void; + public function setTemplates(array $templates /* ,string $layout = 'default' */): void; } diff --git a/src/Templating/MutableTemplateRegistryInterface.php b/src/Templating/MutableTemplateRegistryInterface.php index 100d126ca5..6347b17c7e 100644 --- a/src/Templating/MutableTemplateRegistryInterface.php +++ b/src/Templating/MutableTemplateRegistryInterface.php @@ -21,7 +21,7 @@ interface MutableTemplateRegistryInterface extends TemplateRegistryInterface /** * @param array $templates 'name' => 'file_path.html.twig' */ - public function setTemplates(array $templates): void; + public function setTemplates(array $templates /* ,string $layout = 'default' */): void; - public function setTemplate(string $name, string $template): void; + public function setTemplate(string $name, string $template /* ,string $layout = 'default' */): void; } diff --git a/src/Templating/TemplateRegistryInterface.php b/src/Templating/TemplateRegistryInterface.php index f534ff52b0..6164976d58 100644 --- a/src/Templating/TemplateRegistryInterface.php +++ b/src/Templating/TemplateRegistryInterface.php @@ -77,9 +77,9 @@ interface TemplateRegistryInterface /** * @return array 'name' => 'file_path.html.twig' */ - public function getTemplates(): array; + public function getTemplates(/* string $layout */): array; - public function getTemplate(string $name): string; + public function getTemplate(string $name /* ,string $layout */): string; - public function hasTemplate(string $name): bool; + public function hasTemplate(string $name /* ,string $layout */): bool; } diff --git a/src/Twig/TemplateRegistryRuntime.php b/src/Twig/TemplateRegistryRuntime.php index 0065bbb784..c25e4e89b2 100644 --- a/src/Twig/TemplateRegistryRuntime.php +++ b/src/Twig/TemplateRegistryRuntime.php @@ -15,6 +15,7 @@ use Sonata\AdminBundle\Admin\Pool; use Sonata\AdminBundle\Exception\AdminCodeNotFoundException; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Templating\TemplateRegistryInterface; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; @@ -27,7 +28,8 @@ final class TemplateRegistryRuntime implements RuntimeExtensionInterface */ public function __construct( private TemplateRegistryInterface $globalTemplateRegistry, - private Pool $pool + private Pool $pool, + private LayoutStorageInterface $layoutStorage ) { } @@ -37,12 +39,12 @@ public function __construct( */ public function getAdminTemplate(string $name, string $adminCode): string { - return $this->getTemplateRegistry($adminCode)->getTemplate($name); + return $this->getTemplateRegistry($adminCode)->getTemplate($name, $this->layoutStorage->get()); } public function getGlobalTemplate(string $name): string { - return $this->globalTemplateRegistry->getTemplate($name); + return $this->globalTemplateRegistry->getTemplate($name, $this->layoutStorage->get()); } /** diff --git a/tests/App/AppKernel.php b/tests/App/AppKernel.php index 037d35a185..dc1eff2ee7 100644 --- a/tests/App/AppKernel.php +++ b/tests/App/AppKernel.php @@ -97,6 +97,7 @@ protected function configureContainer(ContainerBuilder $containerBuilder, Loader $securityConfig['enable_authenticator_manager'] = true; } + $containerBuilder->loadFromExtension('security', $securityConfig); $containerBuilder->loadFromExtension('security', $securityConfig); $containerBuilder->loadFromExtension('twig', [ diff --git a/tests/Controller/CRUDControllerTest.php b/tests/Controller/CRUDControllerTest.php index 96e33e2603..a9e8c4baac 100644 --- a/tests/Controller/CRUDControllerTest.php +++ b/tests/Controller/CRUDControllerTest.php @@ -31,6 +31,7 @@ use Sonata\AdminBundle\Model\ModelManagerInterface; use Sonata\AdminBundle\Request\AdminFetcherInterface; use Sonata\AdminBundle\Security\Handler\AclSecurityHandlerInterface; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface; use Sonata\AdminBundle\Tests\App\Controller\CustomModelManagerExceptionMessageController; use Sonata\AdminBundle\Tests\App\Controller\CustomModelManagerThrowableMessageController; @@ -110,6 +111,8 @@ final class CRUDControllerTest extends TestCase private AdminObjectAclManipulator $adminObjectAclManipulator; + private LayoutStorageInterface $layoutStorage; + /** * @var array */ @@ -190,6 +193,12 @@ protected function setUp(): void $this->adminObjectAclManipulator = new AdminObjectAclManipulator($this->formFactory, MaskBuilder::class); + $this->layoutStorage = $this->createMock(LayoutStorageInterface::class); + + $this->layoutStorage + ->method('get') + ->willReturn('default'); + $this->csrfProvider = $this->createMock(CsrfTokenManagerInterface::class); $this->csrfProvider @@ -217,6 +226,7 @@ protected function setUp(): void $this->container->set('sonata.admin.admin_exporter', $adminExporter); $this->container->set('sonata.admin.audit.manager', $this->auditManager); $this->container->set('sonata.admin.object.manipulator.acl.admin', $this->adminObjectAclManipulator); + $this->container->set('sonata.admin.layout_cookie_storage', $this->layoutStorage); $this->container->set('security.csrf.token_manager', $this->csrfProvider); $this->container->set('logger', $this->logger); $this->container->set('translator', $this->translator); @@ -237,20 +247,20 @@ protected function setUp(): void $this->parameterBag->set('kernel.debug', false); $this->templateRegistry->method('getTemplate')->willReturnMap([ - ['ajax', '@SonataAdmin/ajax_layout.html.twig'], - ['layout', '@SonataAdmin/standard_layout.html.twig'], - ['show', '@SonataAdmin/CRUD/show.html.twig'], - ['show_compare', '@SonataAdmin/CRUD/show_compare.html.twig'], - ['edit', '@SonataAdmin/CRUD/edit.html.twig'], - ['dashboard', '@SonataAdmin/Core/dashboard.html.twig'], - ['search', '@SonataAdmin/Core/search.html.twig'], - ['list', '@SonataAdmin/CRUD/list.html.twig'], - ['preview', '@SonataAdmin/CRUD/preview.html.twig'], - ['history', '@SonataAdmin/CRUD/history.html.twig'], - ['acl', '@SonataAdmin/CRUD/acl.html.twig'], - ['delete', '@SonataAdmin/CRUD/delete.html.twig'], - ['batch', '@SonataAdmin/CRUD/list__batch.html.twig'], - ['batch_confirmation', '@SonataAdmin/CRUD/batch_confirmation.html.twig'], + ['ajax', 'default', '@SonataAdmin/ajax_layout.html.twig'], + ['layout', 'default', '@SonataAdmin/standard_layout.html.twig'], + ['show', 'default', '@SonataAdmin/CRUD/show.html.twig'], + ['show_compare', 'default', '@SonataAdmin/CRUD/show_compare.html.twig'], + ['edit', 'default', '@SonataAdmin/CRUD/edit.html.twig'], + ['dashboard', 'default', '@SonataAdmin/Core/dashboard.html.twig'], + ['search', 'default', '@SonataAdmin/Core/search.html.twig'], + ['list', 'default', '@SonataAdmin/CRUD/list.html.twig'], + ['preview', 'default', '@SonataAdmin/CRUD/preview.html.twig'], + ['history', 'default', '@SonataAdmin/CRUD/history.html.twig'], + ['acl', 'default', '@SonataAdmin/CRUD/acl.html.twig'], + ['delete', 'default', '@SonataAdmin/CRUD/delete.html.twig'], + ['batch', 'default', '@SonataAdmin/CRUD/list__batch.html.twig'], + ['batch_confirmation', 'default', '@SonataAdmin/CRUD/batch_confirmation.html.twig'], ]); $this->admin->method('getIdParameter')->willReturn('id'); diff --git a/tests/EventListener/ConfigureCRUDControllerListenerTest.php b/tests/EventListener/ConfigureCRUDControllerListenerTest.php index 1a1bd9df19..7e230fc1f7 100644 --- a/tests/EventListener/ConfigureCRUDControllerListenerTest.php +++ b/tests/EventListener/ConfigureCRUDControllerListenerTest.php @@ -18,6 +18,7 @@ use Sonata\AdminBundle\Controller\CRUDController; use Sonata\AdminBundle\EventListener\ConfigureCRUDControllerListener; use Sonata\AdminBundle\Request\AdminFetcherInterface; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; @@ -45,6 +46,13 @@ public function testItConfiguresCRUDController(): void $adminFetcher = $this->createMock(AdminFetcherInterface::class); $container->set('sonata.admin.request.fetcher', $adminFetcher); + $layoutStorage = $this->createMock(LayoutStorageInterface::class); + + $layoutStorage + ->method('get') + ->willReturn('default'); + $container->set('sonata.admin.layout_cookie_storage', $layoutStorage); + $request = new Request([], [], [ '_sonata_admin' => 'admin.code', ]); diff --git a/tests/Twig/Extension/TemplateRegistryExtensionTest.php b/tests/Twig/Extension/TemplateRegistryExtensionTest.php index a5e8b8441b..b7925ea5be 100644 --- a/tests/Twig/Extension/TemplateRegistryExtensionTest.php +++ b/tests/Twig/Extension/TemplateRegistryExtensionTest.php @@ -17,6 +17,7 @@ use Sonata\AdminBundle\Admin\AdminInterface; use Sonata\AdminBundle\Admin\Pool; use Sonata\AdminBundle\Exception\AdminCodeNotFoundException; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface; use Sonata\AdminBundle\Templating\TemplateRegistryInterface; use Sonata\AdminBundle\Twig\Extension\TemplateRegistryExtension; @@ -45,13 +46,16 @@ protected function setUp(): void ->method('getTemplateRegistry') ->willReturn($adminTemplateRegistry); + $layoutStorage = $this->createStub(LayoutStorageInterface::class); $container = new Container(); $container->set('admin.post', $admin); $pool = new Pool($container, ['admin.post']); $this->extension = new TemplateRegistryExtension(new TemplateRegistryRuntime( $templateRegistry, - $pool + $pool, + $layoutStorage, + false )); } diff --git a/tests/Twig/TemplateRegistryRuntimeTest.php b/tests/Twig/TemplateRegistryRuntimeTest.php index dd69726fd0..02370baa90 100644 --- a/tests/Twig/TemplateRegistryRuntimeTest.php +++ b/tests/Twig/TemplateRegistryRuntimeTest.php @@ -17,6 +17,7 @@ use Sonata\AdminBundle\Admin\AdminInterface; use Sonata\AdminBundle\Admin\Pool; use Sonata\AdminBundle\Exception\AdminCodeNotFoundException; +use Sonata\AdminBundle\Templating\LayoutStorage\LayoutStorageInterface; use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface; use Sonata\AdminBundle\Templating\TemplateRegistryInterface; use Sonata\AdminBundle\Twig\TemplateRegistryRuntime; @@ -39,13 +40,16 @@ protected function setUp(): void ->method('getTemplateRegistry') ->willReturn($adminTemplateRegistry); + $layoutStorage = $this->createStub(LayoutStorageInterface::class); $container = new Container(); $container->set('admin.post', $admin); $pool = new Pool($container, ['admin.post']); $this->templateRegistryRuntime = new TemplateRegistryRuntime( $templateRegistry, - $pool + $pool, + $layoutStorage, + false ); }