Skip to content

Commit

Permalink
feature #784 Added a new feature to create custom menus (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the master branch (closes #784).

Discussion
----------

Added a new feature to create custom menus

**VERY IMPORTANT**: this article explains a feature that DOESN'T EXIST YET in EasyAdmin. Don't try to use this feature. It won't work!

---

Following our "DDD tradition" (*Documentation-Driven Development*), I've created a tutorial for an imaginary feature that allows to define custom menus. For all those developers that need this feature, please add your comments and try to reply to these questions

  * If this feature was real, would it solve your menu-related needs?
    * If yes, could we simplify it?
    * If no, what does it lack?
  * Do you know any good-practice related to menus that we could import from other projects (KnpMenuBundle, Sonata, ...) or technologies (Django, Rails, ...)?

Thanks!

Commits
-------

70de83a Added a new feature to create custom menus
  • Loading branch information
javiereguiluz committed Jan 17, 2016
2 parents b0c8e9a + 70de83a commit 6dfe83f
Show file tree
Hide file tree
Showing 162 changed files with 3,599 additions and 107 deletions.
86 changes: 83 additions & 3 deletions Configuration/DefaultConfigPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,94 @@
class DefaultConfigPass implements ConfigPassInterface
{
public function process(array $backendConfig)
{
$backendConfig = $this->processDefaultEntity($backendConfig);
$backendConfig = $this->processDefaultMenuItem($backendConfig);
$backendConfig = $this->processDefaultHomepage($backendConfig);

return $backendConfig;
}

/**
* Finds the default entity to display when the backend index is not
* defined explicitly.
*/
private function processDefaultEntity(array $backendConfig)
{
$entityNames = array_keys($backendConfig['entities']);
$firstEntityName = isset($entityNames[0]) ? $entityNames[0] : null;

// this option is used to redirect the homepage of the backend to the
// 'list' view of the first configured entity.
$backendConfig['default_entity_name'] = $firstEntityName;

return $backendConfig;
}

/**
* Finds the default menu item to display when browsing the backend index.
*/
private function processDefaultMenuItem(array $backendConfig)
{
$defaultMenuItem = $this->findDefaultMenuItem($backendConfig['design']['menu']);

if ('empty' === $defaultMenuItem['type']) {
throw new \RuntimeException(sprintf('The "menu" configuration sets "%s" as the default item, which is not possible because its type is "empty" and it cannot redirect to a valid URL.', $defaultMenuItem['label']));
}

$backendConfig['default_menu_item'] = $defaultMenuItem;

return $backendConfig;
}

/**
* Finds the first menu item whose 'default' option is 'true' (if any).
* It looks for the option both in the first level items and in the
* submenu items.
*/
private function findDefaultMenuItem(array $menuConfig)
{
foreach ($menuConfig as $itemConfig) {
if (true === $itemConfig['default']) {
return $itemConfig;
}

foreach ($itemConfig['children'] as $subitemConfig) {
if (true === $subitemConfig['default']) {
return $subitemConfig;
}
}
}
}

/**
* Processes the backend config to define the URL or the route/params to
* use as the default backend homepage when none is defined explicitly.
* (Note: we store the route/params instead of generating the URL because
* the 'router' service cannot be used inside a compiler pass).
*/
private function processDefaultHomepage(array $backendConfig)
{
$backendHomepage = array();

// if no menu item has been set as "default", use the "list"
// action of the first configured entity as the backend homepage
if (null === $menuItemConfig = $backendConfig['default_menu_item']) {
$backendHomepage['route'] = 'easyadmin';
$backendHomepage['params'] = array('action' => 'list', 'entity' => $backendConfig['default_entity_name']);
} else {
$routeParams = array('menuIndex' => $menuItemConfig['menu_index'], 'submenuIndex' => $menuItemConfig['submenu_index']);

if ('entity' === $menuItemConfig['type']) {
$backendHomepage['route'] = 'easyadmin';
$backendHomepage['params'] = array_merge(array('action' => 'list', 'entity' => $menuItemConfig['entity']), $routeParams, $menuItemConfig['params']);
} elseif ('route' === $menuItemConfig['type']) {
$backendHomepage['route'] = $menuItemConfig['route'];
$backendHomepage['params'] = array_merge($routeParams, $menuItemConfig['params']);
} elseif ('link' === $menuItemConfig['type']) {
$backendHomepage['url'] = $menuItemConfig['url'];
}
}

$backendConfig['homepage'] = $backendHomepage;

return $backendConfig;
}
}
168 changes: 168 additions & 0 deletions Configuration/MenuConfigPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

/*
* This file is part of the EasyAdminBundle.
*
* (c) Javier Eguiluz <javier.eguiluz@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JavierEguiluz\Bundle\EasyAdminBundle\Configuration;

/**
* Processes the main menu configuration defined in the "design.menu"
* option or creates the default config for the menu if none is defined.
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class MenuConfigPass implements ConfigPassInterface
{
public function process(array $backendConfig)
{
// process 1st level menu items
$menuConfig = $backendConfig['design']['menu'];
$menuConfig = $this->normalizeMenuConfig($menuConfig, $backendConfig);
$menuConfig = $this->processMenuConfig($menuConfig, $backendConfig);

$backendConfig['design']['menu'] = $menuConfig;

// process 2nd level menu items (i.e. submenus)
foreach ($backendConfig['design']['menu'] as $i => $itemConfig) {
if (empty($itemConfig['children'])) {
continue;
}

$submenuConfig = $itemConfig['children'];
$submenuConfig = $this->normalizeMenuConfig($submenuConfig, $backendConfig, $i);
$submenuConfig = $this->processMenuConfig($submenuConfig, $backendConfig, $i);

$backendConfig['design']['menu'][$i]['children'] = $submenuConfig;
}

return $backendConfig;
}

/**
* Normalizes the different shortcut notations of the menu config to simplify
* further processing.
*
* @param array $menuConfig
* @param array $backendConfig
* @param int $parentItemIndex The index of the parent item for this menu item (allows to treat submenus differently)
*
* @return array
*/
private function normalizeMenuConfig(array $menuConfig, array $backendConfig, $parentItemIndex = -1)
{
// if the backend doesn't define the menu configuration: create a default
// menu configuration to display all its entities
if (empty($menuConfig)) {
foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
$menuConfig[] = array('entity' => $entityName, 'label' => $entityConfig['label']);
}
}

// replaces the short config syntax:
// design.menu: ['Product', 'User']
// by the expanded config syntax:
// design.menu: []{ entity: 'Product' }, { entity: 'User' }]
foreach ($menuConfig as $i => $itemConfig) {
if (is_string($itemConfig)) {
$itemConfig = array('entity' => $itemConfig);
}

$menuConfig[$i] = $itemConfig;
}

foreach ($menuConfig as $i => $itemConfig) {
// normalize icon configuration
if (!array_key_exists('icon', $itemConfig)) {
$itemConfig['icon'] = ($parentItemIndex > -1) ? 'fa-chevron-right' : 'fa-chevron-circle-right';
} elseif (empty($itemConfig['icon'])) {
$itemConfig['icon'] = null;
} else {
$itemConfig['icon'] = 'fa-'.$itemConfig['icon'];
}

// normalize submenu configuration (only for main menu items)
if (!isset($itemConfig['children']) && $parentItemIndex === -1) {
$itemConfig['children'] = array();
}

// normalize 'default' option, which sets the menu item used as the backend index
if (!array_key_exists('default', $itemConfig)) {
$itemConfig['default'] = false;
} else {
$itemConfig['default'] = (bool) $itemConfig['default'];
}

$menuConfig[$i] = $itemConfig;
}

return $menuConfig;
}

private function processMenuConfig(array $menuConfig, array $backendConfig, $parentItemIndex = -1)
{
foreach ($menuConfig as $i => $itemConfig) {
// these options are needed to find the active menu/submenu item in the template
$itemConfig['menu_index'] = ($parentItemIndex === -1) ? $i : $parentItemIndex;
$itemConfig['submenu_index'] = ($parentItemIndex === -1) ? -1 : $i;

// 1st level priority: if 'entity' is defined, link to the given entity
if (isset($itemConfig['entity'])) {
$itemConfig['type'] = 'entity';
$entityName = $itemConfig['entity'];

if (!array_key_exists($entityName, $backendConfig['entities'])) {
throw new \RuntimeException(sprintf('The "%s" entity included in the "menu" option is not managed by EasyAdmin. The menu can only include any of these entities: %s.', $entityName, implode(', ', array_keys($backendConfig['entities']))));
}

if (!isset($itemConfig['label'])) {
$itemConfig['label'] = $backendConfig['entities'][$entityName]['label'];
}

if (!isset($itemConfig['params'])) {
$itemConfig['params'] = array();
}
}

// 2nd level priority: if 'url' is defined, link to the given absolute/relative URL
elseif (isset($itemConfig['url'])) {
$itemConfig['type'] = 'link';

if (!isset($itemConfig['label'])) {
throw new \RuntimeException(sprintf('The configuration of the menu item with "url = %s" must define the "label" option.', $itemConfig['url']));
}
}

// 3rd level priority: if 'route' is defined, link to the path generated with the given route
elseif (isset($itemConfig['route'])) {
$itemConfig['type'] = 'route';

if (!isset($itemConfig['label'])) {
throw new \RuntimeException(sprintf('The configuration of the menu item with "route = %s" must define the "label" option.', $itemConfig['route']));
}

if (!isset($itemConfig['params'])) {
$itemConfig['params'] = array();
}
}

// 4th level priority: if 'label' is defined (and not the previous options), this is an empty element
elseif (isset($itemConfig['label'])) {
$itemConfig['type'] = 'empty';
}

else {
throw new \RuntimeException(sprintf('The configuration of the menu item in the position %d (being 0 the first item) must define at least one of these options: entity, url, route, label.', $i));
}

$menuConfig[$i] = $itemConfig;
}

return $menuConfig;
}
}
1 change: 1 addition & 0 deletions Configuration/TemplateConfigPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TemplateConfigPass implements ConfigPassInterface

private $defaultBackendTemplates = array(
'layout' => '@EasyAdmin/default/layout.html.twig',
'menu' => '@EasyAdmin/default/menu.html.twig',
'edit' => '@EasyAdmin/default/edit.html.twig',
'list' => '@EasyAdmin/default/list.html.twig',
'new' => '@EasyAdmin/default/new.html.twig',
Expand Down
16 changes: 15 additions & 1 deletion Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function indexAction(Request $request)
$this->initialize($request);

if (null === $request->query->get('entity')) {
return $this->redirect($this->generateUrl('easyadmin', array('action' => 'list', 'entity' => $this->config['default_entity_name'])));
return $this->redirectToBackendHomepage();
}

$action = $request->query->get('action', 'list');
Expand Down Expand Up @@ -767,4 +767,18 @@ private function useLegacyFormComponent()
{
return false === class_exists('Symfony\\Component\\Form\\Util\\StringUtil');
}

/**
* Generates the backend homepage and redirects to it.
*/
private function redirectToBackendHomepage()
{
$homepageConfig = $this->config['homepage'];

$url = isset($homepageConfig['url'])
? $homepageConfig['url']
: $this->get('router')->generate($homepageConfig['route'], $homepageConfig['params']);

return $this->redirect($url);
}
}
2 changes: 2 additions & 0 deletions DependencyInjection/Compiler/EasyAdminConfigurationPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use JavierEguiluz\Bundle\EasyAdminBundle\Configuration\ActionConfigPass;
use JavierEguiluz\Bundle\EasyAdminBundle\Configuration\DefaultConfigPass;
use JavierEguiluz\Bundle\EasyAdminBundle\Configuration\MenuConfigPass;
use JavierEguiluz\Bundle\EasyAdminBundle\Configuration\MetadataConfigPass;
use JavierEguiluz\Bundle\EasyAdminBundle\Configuration\NormalizerConfigPass;
use JavierEguiluz\Bundle\EasyAdminBundle\Configuration\PropertyConfigPass;
Expand Down Expand Up @@ -45,6 +46,7 @@ public function process(ContainerBuilder $container)

$configPasses = array(
new NormalizerConfigPass(),
new MenuConfigPass(),
new ActionConfigPass(),
new MetadataConfigPass($container->get('doctrine')),
new PropertyConfigPass(),
Expand Down
8 changes: 8 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ private function addDesignSection(ArrayNodeDefinition $rootNode)
->info('The custom templates used to render each backend element.')
->children()
->scalarNode('layout')->info('Used to decorate the main templates (list, edit, new and show)')->end()
->scalarNode('menu')->info('Used to render the main menu')->end()
->scalarNode('edit')->info('Used to render the page where entities are edited')->end()
->scalarNode('list')->info('Used to render the listing page and the search results page')->end()
->scalarNode('new')->info('Used to render the page where new entities are created')->end()
Expand Down Expand Up @@ -342,6 +343,13 @@ private function addDesignSection(ArrayNodeDefinition $rootNode)
->scalarNode('label_undefined')->info('Used when any kind of error or exception happens when trying to access the value of the field to render')->end()
->end()
->end()

->arrayNode('menu')
->normalizeKeys(false)
->defaultValue(array())
->info('The items to display in the main menu.')
->prototype('variable')
->end()
->end()
->end()
->end()
Expand Down

0 comments on commit 6dfe83f

Please sign in to comment.