Skip to content

Commit

Permalink
feature #738 Refactored the way configuration is managed (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the master branch (closes #738).

Discussion
----------

Refactored the way configuration is managed

For end-users this pull request doesn't change anything. This is just a refactor. You don't have to change anything or care for anything. Everything will work as before, except a bit faster.

---

For people looking into the bundle internals: previously we processed the configuration in two steps (the first half of the config before the app started and the other half during runtime). Now we process the entire configuration before the app runs. We do this using a compiler pass. For that reason performance is better because nothing related to config is parsed during runtime.

The other important change is the introduction of `ConfigPass` classes. Instead of having one single class parsing the entire config, we now have smaller and dedicated class to parse the config in several passes.

---

Tests pass, except for PHP 5.3 and `COMPOSER_FLAGS=--prefer-lowest` which shows error messages like this one ([see full details](https://travis-ci.org/javiereguiluz/EasyAdminBundle/jobs/99936347)):

```
Template name "@EasyAdmin/default/list.html.twig" is not valid (format is
"bundle:section:template.format.engine"). (500 Internal Server Error)
```

Commits
-------

95729a6 Refactored the way configuration is managed
  • Loading branch information
javiereguiluz committed Jan 3, 2016
2 parents adce668 + 95729a6 commit 55559dc
Show file tree
Hide file tree
Showing 510 changed files with 110,634 additions and 27,744 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,57 @@
* file that was distributed with this source code.
*/

namespace JavierEguiluz\Bundle\EasyAdminBundle\Configuration\Normalizer;
namespace JavierEguiluz\Bundle\EasyAdminBundle\Configuration;

/**
* Merges all the actions that can be configured in the backend and normalizes
* them to get the final action configuration for each entity view.
*
* @author Javier Eguiluz <[email protected]>
*/
class ActionNormalizer implements NormalizerInterface
class ActionConfigPass implements ConfigPassInterface
{
private $defaultActionConfiguration = array(
'name' => null, // either the name of a controller method or an application route (it depends on the 'type' option)
'type' => 'method', // 'method' if the action is a controller method; 'route' if it's an application route
'label' => null, // action label (displayed as link or button) (if 'null', autogenerate it)
'css_class' => '', // the CSS class applied to the button/link displayed by the action
'icon' => null, // the name of the FontAwesome icon to display next to the 'label' (doesn't include the 'fa-' prefix)
private $defaultActionConfig = array(
// either the name of a controller method or an application route (it depends on the 'type' option)
'name' => null,
// 'method' if the action is a controller method; 'route' if it's an application route
'type' => 'method',
// action label (displayed as link or button) (if 'null', autogenerate it)
'label' => null,
// the CSS class applied to the button/link displayed by the action
'css_class' => '',
// the name of the FontAwesome icon to display next to the 'label' (doesn't include the 'fa-' prefix)
'icon' => null,
);

public function normalize(array $backendConfiguration)
public function process(array $backendConfig)
{
$entitiesConfiguration = array();

foreach ($backendConfiguration['entities'] as $entityName => $entityConfiguration) {
$entitiesConfig = array();
foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
// first, define the disabled actions
$actionsDisabledByBackend = $backendConfiguration['disabled_actions'];
$actionsDisabledByEntity = isset($entityConfiguration['disabled_actions']) ? $entityConfiguration['disabled_actions'] : array();
$actionsDisabledByBackend = $backendConfig['disabled_actions'];
$actionsDisabledByEntity = isset($entityConfig['disabled_actions']) ? $entityConfig['disabled_actions'] : array();
$disabledActions = array_unique(array_merge($actionsDisabledByBackend, $actionsDisabledByEntity));
$entityConfiguration['disabled_actions'] = $disabledActions;
$entityConfig['disabled_actions'] = $disabledActions;

// second, define the actions of each entity view
foreach (array('edit', 'list', 'new', 'show') as $view) {
$defaultActions = $this->getDefaultActions($view);
$backendActions = isset($backendConfiguration[$view]['actions']) ? $backendConfiguration[$view]['actions'] : array();
$backendActions = $this->normalizeActionsConfiguration($backendActions, $defaultActions);
$backendActions = isset($backendConfig[$view]['actions']) ? $backendConfig[$view]['actions'] : array();
$backendActions = $this->normalizeActionsConfig($backendActions, $defaultActions);

$defaultViewActions = array_replace($defaultActions, $backendActions);
$defaultViewActions = $this->filterRemovedActions($defaultViewActions);

$entityActions = isset($entityConfiguration[$view]['actions']) ? $entityConfiguration[$view]['actions'] : array();
$entityActions = $this->normalizeActionsConfiguration($entityActions, $defaultViewActions);
$entityActions = isset($entityConfig[$view]['actions']) ? $entityConfig[$view]['actions'] : array();
$entityActions = $this->normalizeActionsConfig($entityActions, $defaultViewActions);

$viewActions = array_replace($defaultViewActions, $entityActions);
$viewActions = $this->filterRemovedActions($viewActions);

// 'list' action is mandatory for all views
if (!array_key_exists('list', $viewActions)) {
$viewActions = array_merge($viewActions, $this->normalizeActionsConfiguration(array('list')));
$viewActions = array_merge($viewActions, $this->normalizeActionsConfig(array('list')));
}

if (isset($viewActions['delete'])) {
Expand All @@ -66,15 +70,15 @@ public function normalize(array $backendConfiguration)
}
}

$entityConfiguration[$view]['actions'] = $viewActions;
$entityConfig[$view]['actions'] = $viewActions;
}

$entitiesConfiguration[$entityName] = $entityConfiguration;
$entitiesConfig[$entityName] = $entityConfig;
}

$backendConfiguration['entities'] = $entitiesConfiguration;
$backendConfig['entities'] = $entitiesConfig;

return $backendConfiguration;
return $backendConfig;
}

/**
Expand All @@ -89,20 +93,20 @@ public function normalize(array $backendConfiguration)
private function getDefaultActions($view)
{
// basic configuration for default actions
$actions = $this->normalizeActionsConfiguration(array(
$actions = $this->normalizeActionsConfig(array(
array('name' => 'delete', 'label' => 'action.delete', 'icon' => 'trash'),
array('name' => 'edit', 'label' => 'action.edit', 'icon' => 'edit'),
array('name' => 'new', 'label' => 'action.new'),
array('name' => 'edit', 'label' => 'action.edit', 'icon' => 'edit'),
array('name' => 'new', 'label' => 'action.new'),
array('name' => 'search', 'label' => 'action.search'),
array('name' => 'show', 'label' => 'action.show'),
array('name' => 'list', 'label' => 'action.list'),
array('name' => 'show', 'label' => 'action.show'),
array('name' => 'list', 'label' => 'action.list'),
));

// define which actions are enabled for each view
$actionsPerView = array(
'edit' => array('delete' => $actions['delete'], 'list' => $actions['list']),
'list' => array('show' => $actions['show'], 'edit' => $actions['edit'], 'search' => $actions['search'], 'new' => $actions['new']),
'new' => array('list' => $actions['list']),
'new' => array('list' => $actions['list']),
'show' => array('delete' => $actions['delete'], 'list' => $actions['list'], 'edit' => $actions['edit']),
);

Expand All @@ -114,7 +118,7 @@ private function getDefaultActions($view)

/**
* Transforms the different action configuration formats into a normalized
* and expanded format. These are the two simple formats allowed:
* and expanded format. These are the two simple formats allowed:.
*
* # Config format #1: no custom option
* easy_admin:
Expand All @@ -130,32 +134,32 @@ private function getDefaultActions($view)
* list:
* actions: ['search', { name: 'show', label: 'Show', 'icon': 'user' }, 'grantAccess']
*
* @param array $actionsConfiguration
* @param array $defaultActionsConfiguration
* @param array $actionsConfig
* @param array $defaultActionsConfig
*
* @return array
*/
private function normalizeActionsConfiguration(array $actionsConfiguration, array $defaultActionsConfiguration = array())
private function normalizeActionsConfig(array $actionsConfig, array $defaultActionsConfig = array())
{
$configuration = array();

foreach ($actionsConfiguration as $action) {
foreach ($actionsConfig as $action) {
if (is_string($action)) {
// config format #1
$actionConfiguration = array('name' => $action);
$actionConfig = array('name' => $action);
} elseif (is_array($action)) {
// config format #2
$actionConfiguration = $action;
$actionConfig = $action;
} else {
throw new \RuntimeException('The values of the "actions" option can only be strings or arrays.');
}

// 'name' is the only mandatory option for actions
if (!isset($actionConfiguration['name'])) {
if (!isset($actionConfig['name'])) {
throw new \RuntimeException('When using the expanded configuration format for actions, you must define their "name" option.');
}

$actionName = $actionConfiguration['name'];
$actionName = $actionConfig['name'];

// 'name' value is used as the class method name or the Symfony route name
// check that its value complies with the PHP method name rules (the leading dash
Expand All @@ -164,43 +168,43 @@ private function normalizeActionsConfiguration(array $actionsConfiguration, arra
throw new \InvalidArgumentException(sprintf('The name of the "%s" action contains invalid characters (allowed: letters, numbers, underscores; the first character cannot be a number).', $actionName));
}

$normalizedConfiguration = array_replace($this->defaultActionConfiguration, $actionConfiguration);
$normalizedConfig = array_replace($this->defaultActionConfig, $actionConfig);

$actionName = $normalizedConfiguration['name'];
$actionName = $normalizedConfig['name'];

// use the special 'action.<action name>' label for the default actions
// only if the user hasn't defined a custom label (which can also be an empty string)
if (null === $normalizedConfiguration['label'] && in_array($actionName, array('delete', 'edit', 'new', 'search', 'show', 'list'))) {
$normalizedConfiguration['label'] = 'action.'.$actionName;
if (null === $normalizedConfig['label'] && in_array($actionName, array('delete', 'edit', 'new', 'search', 'show', 'list'))) {
$normalizedConfig['label'] = 'action.'.$actionName;
}

// the rest of actions without a custom label use their name as label
if (null === $normalizedConfiguration['label']) {
if (null === $normalizedConfig['label']) {
// copied from Symfony\Component\Form\FormRenderer::humanize() (author: Bernhard Schussek <[email protected]>)
$label = ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $actionName))));
$normalizedConfiguration['label'] = $label;
$normalizedConfig['label'] = $label;
}

if (count($defaultActionsConfiguration)) {
if (count($defaultActionsConfig)) {
// if the user defines an action with the same name of a default action,
// he/she is in fact overriding the default configuration of that action.
// for example: actions: ['delete', 'list']
// this condition ensures that when the user doesn't define the value for
// some option of the action (for example the icon or the label) that
// option is actually added with the right default value. Otherwise,
// those options would be 'null' and the template would show some issues
if (array_key_exists($actionName, $defaultActionsConfiguration)) {
if (array_key_exists($actionName, $defaultActionsConfig)) {
// remove null config options but maintain empty options (this allows to set an empty label for the action)
$normalizedConfiguration = array_filter($normalizedConfiguration, function ($element) { return null !== $element; });
$normalizedConfiguration = array_replace($defaultActionsConfiguration[$actionName], $normalizedConfiguration);
$normalizedConfig = array_filter($normalizedConfig, function ($element) { return null !== $element; });
$normalizedConfig = array_replace($defaultActionsConfig[$actionName], $normalizedConfig);
}
}

// Add default classes ("action-{actionName}") to each action configuration
$normalizedConfiguration['css_class'] .= ' action-'.$normalizedConfiguration['name'];
$normalizedConfiguration['css_class'] = ltrim($normalizedConfiguration['css_class']);
$normalizedConfig['css_class'] .= ' action-'.$normalizedConfig['name'];
$normalizedConfig['css_class'] = ltrim($normalizedConfig['css_class']);

$configuration[$actionName] = $normalizedConfiguration;
$configuration[$actionName] = $normalizedConfig;
}

return $configuration;
Expand All @@ -209,22 +213,22 @@ private function normalizeActionsConfiguration(array $actionsConfiguration, arra
/**
* Removes the actions marked as deleted from the given actions configuration.
*
* @param array $actionsConfiguration
* @param array $actionsConfig
*
* @return array
*/
private function filterRemovedActions(array $actionsConfiguration)
private function filterRemovedActions(array $actionsConfig)
{
// if the name of the action starts with a dash ('-'), remove it
$removedActions = array_filter($actionsConfiguration, function ($action) {
$removedActions = array_filter($actionsConfig, function ($action) {
return '-' === $action['name']{0};
});

if (empty($removedActions)) {
return $actionsConfiguration;
return $actionsConfig;
}

return array_filter($actionsConfiguration, function ($action) use ($removedActions) {
return array_filter($actionsConfig, function ($action) use ($removedActions) {
// e.g. '-search' action name removes both '-search' and 'search' (if exists)
return !array_key_exists($action['name'], $removedActions)
&& !array_key_exists('-'.$action['name'], $removedActions);
Expand Down
32 changes: 32 additions & 0 deletions Configuration/ConfigPassInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the EasyAdminBundle.
*
* (c) Javier Eguiluz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JavierEguiluz\Bundle\EasyAdminBundle\Configuration;

/**
* The interface that must be implemented by all the classes that normalize,
* parse, complete or manipulate in any way the original backend configuration
* in order to generate the final backend configuration. This allows the
* end-user to use shortcuts and syntactic sugar to define the backend configuration.
*
* @author Javier Eguiluz <[email protected]>
*
* @internal
*/
interface ConfigPassInterface
{
/**
* @param array $backendConfig
*
* @return array
*/
public function process(array $backendConfig);
}
Loading

0 comments on commit 55559dc

Please sign in to comment.