PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/easycorp/easyadmin-bundle/src/Configuration/ActionConfigPass.php

https://bitbucket.org/sicomore/next-op-exercise
PHP | 358 lines | 191 code | 51 blank | 116 comment | 13 complexity | 4d521fd26ba320501d1ba9067de2a47b MD5 | raw file
Possible License(s): LGPL-2.0, Unlicense, Apache-2.0, BSD-3-Clause
  1. <?php
  2. /*
  3. * This file is part of the EasyAdminBundle.
  4. *
  5. * (c) Javier Eguiluz <javier.eguiluz@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace EasyCorp\Bundle\EasyAdminBundle\Configuration;
  11. /**
  12. * Merges all the actions that can be configured in the backend and normalizes
  13. * them to get the final action configuration for each entity view.
  14. *
  15. * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  16. */
  17. class ActionConfigPass implements ConfigPassInterface
  18. {
  19. private $views = array('edit', 'list', 'new', 'show');
  20. private $defaultActionConfig = array(
  21. // either the name of a controller method or an application route (it depends on the 'type' option)
  22. 'name' => null,
  23. // 'method' if the action is a controller method; 'route' if it's an application route
  24. 'type' => 'method',
  25. // action label (displayed as link or button) (if 'null', autogenerate it)
  26. 'label' => null,
  27. // the HTML title attribute of the action link (useful when action only displays its icon)
  28. 'title' => null,
  29. // the CSS class applied to the button/link displayed by the action
  30. 'css_class' => null,
  31. // the name of the FontAwesome icon to display next to the 'label' (doesn't include the 'fa-' prefix)
  32. 'icon' => null,
  33. // the value of the HTML 'target' attribute add to the links of the actions (e.g. '_blank')
  34. 'target' => '_self',
  35. );
  36. public function process(array $backendConfig)
  37. {
  38. $backendConfig = $this->processDisabledActions($backendConfig);
  39. $backendConfig = $this->normalizeActionsConfig($backendConfig);
  40. $backendConfig = $this->resolveActionInheritance($backendConfig);
  41. $backendConfig = $this->processActionsConfig($backendConfig);
  42. return $backendConfig;
  43. }
  44. private function processDisabledActions(array $backendConfig)
  45. {
  46. $actionsDisabledByBackend = $backendConfig['disabled_actions'];
  47. foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
  48. $actionsDisabledByEntity = isset($entityConfig['disabled_actions']) ? $entityConfig['disabled_actions'] : array();
  49. $disabledActions = array_unique(array_merge($actionsDisabledByBackend, $actionsDisabledByEntity));
  50. $backendConfig['entities'][$entityName]['disabled_actions'] = $disabledActions;
  51. }
  52. return $backendConfig;
  53. }
  54. /**
  55. * Transforms the different action configuration formats into a normalized
  56. * and expanded format. These are the two simple formats allowed:.
  57. *
  58. * # Config format #1: no custom option
  59. * easy_admin:
  60. * entities:
  61. * User:
  62. * list:
  63. * actions: ['search', 'show', 'grantAccess']
  64. *
  65. * # Config format #2: one or more actions define any of their options
  66. * easy_admin:
  67. * entities:
  68. * User:
  69. * list:
  70. * actions: ['search', { name: 'show', label: 'Show', 'icon': 'user' }, 'grantAccess']
  71. *
  72. * @param array $backendConfig
  73. *
  74. * @return array
  75. */
  76. private function normalizeActionsConfig(array $backendConfig)
  77. {
  78. // first, normalize actions defined globally for the entire backend
  79. foreach ($this->views as $view) {
  80. $actionsConfig = $backendConfig[$view]['actions'];
  81. $actionsConfig = $this->doNormalizeActionsConfig($actionsConfig, sprintf('the global "%s" view defined under "easy_admin" option', $view));
  82. $actionsConfig = $this->doNormalizeDefaultActionsConfig($actionsConfig, $view);
  83. $backendConfig[$view]['actions'] = $actionsConfig;
  84. }
  85. // second, normalize actions defined for each entity
  86. foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
  87. foreach ($this->views as $view) {
  88. $actionsConfig = $entityConfig[$view]['actions'];
  89. $actionsConfig = $this->doNormalizeActionsConfig($actionsConfig, sprintf('the "%s" view of the "%s" entity', $view, $entityName));
  90. $actionsConfig = $this->doNormalizeDefaultActionsConfig($actionsConfig, $view);
  91. $backendConfig['entities'][$entityName][$view]['actions'] = $actionsConfig;
  92. }
  93. }
  94. return $backendConfig;
  95. }
  96. private function doNormalizeActionsConfig(array $actionsConfig, $errorOrigin = '')
  97. {
  98. $normalizedConfig = array();
  99. foreach ($actionsConfig as $i => $actionConfig) {
  100. if (!is_string($actionConfig) && !is_array($actionConfig)) {
  101. throw new \RuntimeException(sprintf('One of the actions defined by %s contains an invalid value (action config can only be a YAML string or hash).', $errorOrigin));
  102. }
  103. // config format #1
  104. if (is_string($actionConfig)) {
  105. $actionConfig = array('name' => $actionConfig);
  106. }
  107. $actionConfig = array_merge($this->defaultActionConfig, $actionConfig);
  108. // 'name' is the only mandatory option for actions (it might
  109. // be missing when using the config format #2)
  110. if (!isset($actionConfig['name'])) {
  111. throw new \RuntimeException(sprintf('One of the actions defined by %s does not define its name, which is the only mandatory option for actions.', $errorOrigin));
  112. }
  113. $actionName = $actionConfig['name'];
  114. $normalizedConfig[$actionName] = $actionConfig;
  115. }
  116. return $normalizedConfig;
  117. }
  118. /**
  119. * If the user overrides the configuration of a default action, they usually
  120. * define just the options they want to change. For example:
  121. * actions: ['delete', 'list'] just to redefine the order
  122. * actions: [ { name: 'list', label: 'Listing' }] just to redefine the label.
  123. *
  124. * For that reason, this method merges the full configuration of the default
  125. * actions with the new action configuration. This means that you get the
  126. * default value for any option that you don't explicitly set (e.g. the icon
  127. * or the CSS class).
  128. *
  129. * @param array $actionsConfig
  130. * @param string $view
  131. *
  132. * @return array
  133. */
  134. private function doNormalizeDefaultActionsConfig(array $actionsConfig, $view)
  135. {
  136. $defaultActionsConfig = $this->getDefaultActionsConfig($view);
  137. foreach ($actionsConfig as $actionName => $actionConfig) {
  138. if (array_key_exists($actionName, $defaultActionsConfig)) {
  139. // remove null config options but maintain empty options (this allows to set an empty label for the action)
  140. $actionConfig = array_filter($actionConfig, function ($element) {
  141. return null !== $element;
  142. });
  143. $actionsConfig[$actionName] = array_merge($defaultActionsConfig[$actionName], $actionConfig);
  144. }
  145. }
  146. return $actionsConfig;
  147. }
  148. /**
  149. * Actions can be added/removed globally in the edit/list/new/show views of
  150. * the backend and locally in each of the configured entities. Local config always
  151. * wins over the global config (e.g. if backend removes 'delete' action in the
  152. * 'list' view but some action explicitly adds 'delete' in its 'list' view,
  153. * then that entity shows the 'delete' action and the others don't).
  154. */
  155. private function resolveActionInheritance(array $backendConfig)
  156. {
  157. foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
  158. foreach ($this->views as $view) {
  159. $defaultActions = $this->getDefaultActions($view);
  160. $backendActions = $backendConfig[$view]['actions'];
  161. $entityActions = $entityConfig[$view]['actions'];
  162. // filter actions removed in the global view configuration
  163. foreach ($backendActions as $backendAction) {
  164. if ('-' === $backendAction['name'][0]) {
  165. $actionName = substr($backendAction['name'], 1);
  166. unset($backendActions[$actionName], $backendActions['-'.$actionName]);
  167. // unless the entity explicitly adds this globally removed action, remove it from the
  168. // default actions config to avoid adding it to the entity later when merging everything
  169. if (!isset($entityActions[$actionName])) {
  170. unset($defaultActions[$actionName]);
  171. }
  172. }
  173. }
  174. // filter actions removed in the local entity configuration
  175. foreach ($entityActions as $entityAction) {
  176. if ('-' === $entityAction['name'][0]) {
  177. $actionName = substr($entityAction['name'], 1);
  178. unset($entityActions[$actionName], $entityActions['-'.$actionName], $defaultActions[$actionName]);
  179. }
  180. }
  181. $actionsConfig = array_merge($defaultActions, $backendActions, $entityActions);
  182. // reorder the actions to match the order set by the user in the
  183. // entity or in the global backend options
  184. if (!empty($entityActions)) {
  185. $actionsConfig = $this->reorderArrayItems($actionsConfig, array_keys($entityActions));
  186. } elseif (!empty($backendActions)) {
  187. $actionsConfig = $this->reorderArrayItems($actionsConfig, array_keys($backendActions));
  188. }
  189. $backendConfig['entities'][$entityName][$view]['actions'] = $actionsConfig;
  190. }
  191. }
  192. return $backendConfig;
  193. }
  194. private function processActionsConfig(array $backendConfig)
  195. {
  196. foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
  197. foreach ($this->views as $view) {
  198. foreach ($entityConfig[$view]['actions'] as $actionName => $actionConfig) {
  199. // 'name' value is used as the class method name or the Symfony route name
  200. // check that its value complies with the PHP method name rules
  201. if (!$this->isValidMethodName($actionName)) {
  202. throw new \InvalidArgumentException(sprintf('The name of the "%s" action defined in the "%s" view of the "%s" entity contains invalid characters (allowed: letters, numbers, underscores; the first character cannot be a number).', $actionName, $view, $entityName));
  203. }
  204. if (null === $actionConfig['label']) {
  205. $actionConfig['label'] = $this->humanizeString($actionName);
  206. }
  207. // Add default classes ("action-{actionName}") to each action configuration
  208. $actionConfig['css_class'] .= ' action-'.$actionName;
  209. $backendConfig['entities'][$entityName][$view]['actions'][$actionName] = $actionConfig;
  210. }
  211. }
  212. }
  213. return $backendConfig;
  214. }
  215. /**
  216. * Returns the default configuration for all the built-in actions of the
  217. * given view, including the actions which are not enabled by default for
  218. * that view (e.g. the 'show' action for the 'list' view).
  219. *
  220. * @param string $view
  221. *
  222. * @return array
  223. */
  224. private function getDefaultActionsConfig($view)
  225. {
  226. $actions = $this->doNormalizeActionsConfig(array(
  227. 'delete' => array('name' => 'delete', 'label' => 'action.delete', 'icon' => 'trash-o', 'css_class' => 'btn btn-default'),
  228. 'edit' => array('name' => 'edit', 'label' => 'action.edit', 'icon' => 'edit', 'css_class' => 'btn btn-primary'),
  229. 'new' => array('name' => 'new', 'label' => 'action.new', 'css_class' => 'btn btn-primary'),
  230. 'search' => array('name' => 'search', 'label' => 'action.search'),
  231. 'show' => array('name' => 'show', 'label' => 'action.show'),
  232. 'list' => array('name' => 'list', 'label' => 'action.list', 'css_class' => 'btn btn-secondary'),
  233. ));
  234. // minor tweaks for some action + view combinations
  235. if ('list' === $view) {
  236. $actions['delete']['icon'] = null;
  237. $actions['delete']['css_class'] = 'text-danger';
  238. $actions['edit']['icon'] = null;
  239. $actions['edit']['css_class'] = 'text-primary';
  240. $actions['list']['css_class'] = '';
  241. }
  242. return $actions;
  243. }
  244. /**
  245. * Returns the built-in actions defined by EasyAdmin for the given view.
  246. * This allows to provide some nice defaults for backends that don't
  247. * define their own actions.
  248. *
  249. * @param string $view
  250. *
  251. * @return array
  252. */
  253. private function getDefaultActions($view)
  254. {
  255. $defaultActions = array();
  256. $defaultActionsConfig = $this->getDefaultActionsConfig($view);
  257. // actions are displayed in the same order as defined in this array
  258. $actionsEnabledByView = array(
  259. 'edit' => array('delete', 'list'),
  260. 'list' => array('edit', 'delete', 'new', 'search'),
  261. 'new' => array('list'),
  262. 'show' => array('edit', 'delete', 'list'),
  263. );
  264. foreach ($actionsEnabledByView[$view] as $actionName) {
  265. $defaultActions[$actionName] = $defaultActionsConfig[$actionName];
  266. }
  267. return $defaultActions;
  268. }
  269. /**
  270. * Checks whether the given string is valid as a PHP method name.
  271. *
  272. * @param string $name
  273. *
  274. * @return bool
  275. */
  276. private function isValidMethodName($name)
  277. {
  278. return 0 !== preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $name);
  279. }
  280. /**
  281. * copied from Symfony\Component\Form\FormRenderer::humanize()
  282. * (author: Bernhard Schussek <bschussek@gmail.com>).
  283. *
  284. * @param string $content
  285. *
  286. * @return string
  287. */
  288. private function humanizeString($content)
  289. {
  290. return ucfirst(trim(mb_strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $content))));
  291. }
  292. private function reorderArrayItems(array $originalArray, array $newKeyOrder)
  293. {
  294. $newArray = array();
  295. foreach ($newKeyOrder as $key) {
  296. if (isset($originalArray[$key])) {
  297. $newArray[$key] = $originalArray[$key];
  298. }
  299. }
  300. $missingKeys = array_diff(array_keys($originalArray), array_keys($newArray));
  301. foreach ($missingKeys as $key) {
  302. $newArray[$key] = $originalArray[$key];
  303. }
  304. return $newArray;
  305. }
  306. }
  307. class_alias('EasyCorp\Bundle\EasyAdminBundle\Configuration\ActionConfigPass', 'JavierEguiluz\Bundle\EasyAdminBundle\Configuration\ActionConfigPass', false);