diff --git a/core/core.services.yml b/core/core.services.yml index 0fca40adeb73d30ac2b3587c5f6ca9c9ad0f5be2..6f8cce6566a6b506957794f4754e46437f5d442b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -293,13 +293,25 @@ services: - { name: route_filter } paramconverter_manager: class: Drupal\Core\ParamConverter\ParamConverterManager + calls: + - [setContainer, ['@service_container']] tags: - { name: route_enhancer } + paramconverter_subscriber: + class: Drupal\Core\EventSubscriber\ParamConverterSubscriber + tags: + - { name: event_subscriber } + arguments: ['@paramconverter_manager'] paramconverter.entity: class: Drupal\Core\ParamConverter\EntityConverter tags: - { name: paramconverter } arguments: ['@plugin.manager.entity'] + route_subscriber.entity: + class: Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber + tags: + - { name: event_subscriber } + arguments: ['@plugin.manager.entity'] reverse_proxy_subscriber: class: Drupal\Core\EventSubscriber\ReverseProxySubscriber tags: diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 68f5a97ba6da2b1e95f157c7791c7380bce1e1b2..a6cb9574da9bb537209484ab7c47530e92eb5aaa 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -55,7 +55,7 @@ public function register(ContainerBuilder $container) { // Add a compiler pass for registering event subscribers. $container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new RegisterAccessChecksPass()); - // Add a compiler pass for upcasting of entity route parameters. + // Add a compiler pass for upcasting route parameters. $container->addCompilerPass(new RegisterParamConvertersPass()); $container->addCompilerPass(new RegisterRouteEnhancersPass()); // Add a compiler pass for registering services needing destruction. diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php index 6fe1447bc588208f8a517839b28f6ba31af2d3f7..1948727b2b2d96161cb981a2fe39c8d90f8d0393 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php @@ -23,26 +23,14 @@ class RegisterParamConvertersPass implements CompilerPassInterface { * The container to process. */ public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('paramconverter_manager')) { return; } $manager = $container->getDefinition('paramconverter_manager'); - - $services = array(); foreach ($container->findTaggedServiceIds('paramconverter') as $id => $attributes) { $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; - - $services[$priority][] = new Reference($id); - } - - krsort($services); - - foreach ($services as $priority) { - foreach ($priority as $service) { - $manager->addMethodCall('addConverter', array($service)); - } + $manager->addMethodCall('addConverter', array($id, $priority)); } } } diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..53f9e3155f081934f533998e60cca4747c8e1d69 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Contains Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\Entity\EntityManager; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Drupal\Core\Routing\RoutingEvents; +use Drupal\Core\Routing\RouteBuildEvent; + +/** + * Registers the 'type' of route parameter names that match an entity type. + * + * @todo Matching on parameter *name* is not ideal, because it breaks + * encapsulation: parameter names are local to the controller and route, and + * controllers and routes can't be expected to know what all possible entity + * types might exist across all modules in order to pick names that don't + * conflict. Instead, the 'type' should be determined from introspecting what + * kind of PHP variable (e.g., a type hinted interface) the controller + * requires: https://drupal.org/node/2041907. + */ +class EntityRouteAlterSubscriber implements EventSubscriberInterface { + + /** + * Entity manager. + * + * @var \Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** + * Constructs a new EntityRouteAlterSubscriber. + * + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * The entity manager. + */ + public function __construct(EntityManager $entity_manager) { + $this->entityManager = $entity_manager; + } + + /** + * Applies parameter converters to route parameters. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The event to process. + */ + public function onRoutingRouteAlterSetType(RouteBuildEvent $event) { + $entity_types = array_keys($this->entityManager->getDefinitions()); + foreach ($event->getRouteCollection() as $route) { + $parameter_definitions = $route->getOption('parameters') ?: array(); + // For all route parameter names that match an entity type, add the 'type' + // to the parameter definition if it's not already explicitly provided. + foreach (array_intersect($route->compile()->getVariables(), $entity_types) as $parameter_name) { + if (!isset($parameter_definitions[$parameter_name])) { + $parameter_definitions[$parameter_name] = array(); + } + $parameter_definitions[$parameter_name] += array( + 'type' => 'entity:' . $parameter_name, + ); + } + if (!empty($parameter_definitions)) { + $route->setOption('parameters', $parameter_definitions); + } + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetType', 100); + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..f9a0a6933d32df737f935fb49e4a6a47f6ed45c9 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Contains Drupal\Core\EventSubscriber\ParamConverterSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\ParamConverter\ParamConverterManager; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Drupal\Core\Routing\RoutingEvents; +use Drupal\Core\Routing\RouteBuildEvent; + +/** + * Event subscriber for registering parameter converters with routes. + */ +class ParamConverterSubscriber implements EventSubscriberInterface { + + /** + * The parameter converter manager. + * + * @var \Drupal\Core\ParamConverter\ParamConverterManager + */ + protected $paramConverterManager; + + /** + * Constructs a new ParamConverterSubscriber. + * + * @param \Drupal\Core\ParamConverter\ParamConverterManager $param_converter_manager + * The parameter converter manager that will be responsible for upcasting + * request attributes. + */ + public function __construct(ParamConverterManager $param_converter_manager) { + $this->paramConverterManager = $param_converter_manager; + } + + /** + * Applies parameter converters to route parameters. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The event to process. + */ + public function onRoutingRouteAlterSetParameterConverters(RouteBuildEvent $event) { + $this->paramConverterManager->setRouteParameterConverters($event->getRouteCollection()); + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetParameterConverters', 10); + return $events; + } +} diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index 50b7d7cb7a4abdc39c8c6e6421afbfacab01c5b2..5d637950cb8dc050c292f71e218fb09174656e06 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -12,8 +12,7 @@ use Drupal\Core\Entity\EntityManager; /** - * This class allows the upcasting of entity ids to the respective entity - * object. + * Parameter converter for upcasting entity ids to full objects. */ class EntityConverter implements ParamConverterInterface { @@ -30,74 +29,29 @@ class EntityConverter implements ParamConverterInterface { * @param \Drupal\Core\Entity\EntityManager $entityManager * The entity manager. */ - public function __construct(EntityManager $entityManager) { - $this->entityManager = $entityManager; + public function __construct(EntityManager $entity_manager) { + $this->entityManager = $entity_manager; } /** - * Tries to upcast every variable to an entity type. - * - * If there is a type denoted in the route options it will try to upcast to - * it, if there is no definition in the options it will try to upcast to an - * entity type of that name. If the chosen enity type does not exists it will - * leave the variable untouched. - * If the entity type exist, but there is no entity with the given id it will - * convert the variable to NULL. - * - * Example: - * - * pattern: '/a/{user}/some/{foo}/and/{bar}/' - * options: - * converters: - * foo: 'node' - * - * The value for {user} will be converted to a user entity and the value - * for {foo} to a node entity, but it will not touch the value for {bar}. - * - * It will not process variables which are marked as converted. It will mark - * any variable it processes as converted. - * - * @param array &$variables - * Array of values to convert to their corresponding objects, if applicable. - * @param \Symfony\Component\Routing\Route $route - * The route object. - * @param array &$converted - * Array collecting the names of all variables which have been - * altered by a converter. + * {@inheritdoc} */ - public function process(array &$variables, Route $route, array &$converted) { - $variable_names = $route->compile()->getVariables(); - - $options = $route->getOptions(); - $configuredTypes = isset($options['converters']) ? $options['converters'] : array(); - - $entityTypes = array_keys($this->entityManager->getDefinitions()); - - foreach ($variable_names as $name) { - // Do not process this variable if it's already marked as converted. - if (in_array($name, $converted)) { - continue; - } - - // Obtain entity type to convert to from the route configuration or just - // use the variable name as default. - if (array_key_exists($name, $configuredTypes)) { - $type = $configuredTypes[$name]; - } - else { - $type = $name; - } - - if (in_array($type, $entityTypes)) { - $value = $variables[$name]; - - $storageController = $this->entityManager->getStorageController($type); - $entity = $storageController->load($value); - $variables[$name] = $entity; + public function convert($value, $definition, $name, array $defaults, Request $request) { + $entity_type = substr($definition['type'], strlen('entity:')); + if ($storage = $this->entityManager->getStorageController($entity_type)) { + return $storage->load($value); + } + } - // Mark this variable as converted. - $converted[] = $name; - } + /** + * {@inheritdoc} + */ + public function applies($definition, $name, Route $route) { + if (!empty($definition['type']) && strpos($definition['type'], 'entity:') === 0) { + $entity_type = substr($definition['type'], strlen('entity:')); + return (bool) $this->entityManager->getDefinition($entity_type); } + return FALSE; } + } diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php index 74307c2762cbb610bc3ffb0d30eaa76278aea8d9..957dfd955cd661d66df1cf63fe5dcba467a2165c 100644 --- a/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php @@ -7,6 +7,7 @@ namespace Drupal\Core\ParamConverter; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; /** @@ -17,13 +18,36 @@ interface ParamConverterInterface { /** * Allows to convert variables to their corresponding objects. * - * @param array &$variables - * Array of values to convert to their corresponding objects, if applicable. + * @param mixed $value + * The raw value. + * @param mixed $definition + * The parameter definition provided in the route options. + * @param string $name + * The name of the parameter. + * @param array $defaults + * The route defaults array. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return mixed|null + * The converted parameter value. + */ + public function convert($value, $definition, $name, array $defaults, Request $request); + + /** + * Determines if the converter applies to a specific route and variable. + * + * @param mixed $definition + * The parameter definition provided in the route options. + * @param string $name + * The name of the parameter. * @param \Symfony\Component\Routing\Route $route - * The route object. - * @param array &$converted - * Array collecting the names of all variables which have been - * altered by a converter. + * The route to consider attaching to. + * + * @return bool + * TRUE if the converter applies to the passed route and parameter, FALSE + * otherwise. */ - public function process(array &$variables, Route $route, array &$converted); + public function applies($definition, $name, Route $route); + } diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php index be5676e12f529858b1c55f67eebd9dbf13dc5278..0b494d8b749ba58dc9b0e7a24411144cf0b962cf 100644 --- a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php @@ -11,79 +11,175 @@ use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\RouteCollection; use Symfony\Component\HttpFoundation\Request; -use Drupal\Core\ParamConverter\ParamConverterInterface; - /** - * Provides a service which allows to enhance (say alter) the arguments coming - * from the URL. - * - * A typical use case for this would be upcasting a node id to a node entity. + * Manages converter services for converting request parameters to full objects. * - * This class will not enhance any of the arguments itself, but allow other - * services to register to do so. + * A typical use case for this would be upcasting (converting) a node id to a + * node entity. */ -class ParamConverterManager implements RouteEnhancerInterface { +class ParamConverterManager extends ContainerAware implements RouteEnhancerInterface { + + /** + * An array of registered converter service ids. + * + * @var array + */ + protected $converterIds = array(); + + /** + * Array of registered converter service ids sorted by their priority. + * + * @var array + */ + protected $sortedConverterIds; /** - * Converters managed by the ParamConverterManager. + * Array of loaded converter services keyed by their ids. * * @var array */ - protected $converters; + protected $converters = array(); /** - * Adds a converter to the paramconverter service. + * Registers a parameter converter with the manager. * - * @see \Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass + * @param string $converter + * The parameter converter service id to register. + * @param int $priority + * (optional) The priority of the converter. Defaults to 0. * - * @param \Drupal\Core\ParamConverter\ParamConverterInterface $converter - * The converter to add. + * @return \Drupal\Core\ParamConverter\ParamConverterManager + * The called object for chaining. */ - public function addConverter(ParamConverterInterface $converter) { - $this->converters[] = $converter; + public function addConverter($converter, $priority = 0) { + if (empty($this->converterIds[$priority])) { + $this->converterIds[$priority] = array(); + } + $this->converterIds[$priority][] = $converter; + unset($this->sortedConverterIds); return $this; } /** - * Implements \Symfony\Cmf\Component\Routing\Enhancer\Å–outeEnhancerIterface. + * Sorts the converter service ids and flattens them. + * + * @return array + * The sorted parameter converter service ids. + */ + public function getConverterIds() { + if (!isset($this->sortedConverterIds)) { + krsort($this->converterIds); + $this->sortedConverterIds = array(); + foreach ($this->converterIds as $resolvers) { + $this->sortedConverterIds = array_merge($this->sortedConverterIds, $resolvers); + } + } + return $this->sortedConverterIds; + } + + /** + * Lazy-loads converter services. + * + * @param string $converter + * The service id of converter service to load. + * + * @return \Drupal\Core\ParamConverter\ParamConverterInterface + * The loaded converter service identified by the given service id. + * + * @throws \InvalidArgumentException + * If the given service id is not a registered converter. + */ + public function getConverter($converter) { + if (isset($this->converters[$converter])) { + return $this->converters[$converter]; + } + if (!in_array($converter, $this->getConverterIds())) { + throw new \InvalidArgumentException(sprintf('No converter has been registered for %s', $converter)); + } + return $this->converters[$converter] = $this->container->get($converter); + } + + /** + * Saves a list of applicable converters to each route. * - * Iterates over all registered converters and allows them to alter the - * defaults. + * @param \Symfony\Component\Routing\RouteCollection $routes + * A collection of routes to apply converters to. + */ + public function setRouteParameterConverters(RouteCollection $routes) { + foreach ($routes->all() as $route) { + if (!$parameters = $route->getOption('parameters')) { + // Continue with the next route if no parameters have been defined. + continue; + } + + // Loop over all defined parameters and look up the right converter. + foreach ($parameters as $name => &$definition) { + if (isset($definition['converter'])) { + // Skip parameters that already have a manually set converter. + continue; + } + + foreach ($this->getConverterIds() as $converter) { + if ($this->getConverter($converter)->applies($definition, $name, $route)) { + $definition['converter'] = $converter; + break; + } + } + } + + // Override the parameters array. + $route->setOption('parameters', $parameters); + } + } + + /** + * Invokes the registered converter for each defined parameter on a route. * * @param array $defaults - * The getRouteDefaults array. + * The route defaults array. * @param \Symfony\Component\HttpFoundation\Request $request * The current request. * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * If one of the assigned converters returned NULL because the given + * variable could not be converted. + * * @return array * The modified defaults. */ public function enhance(array $defaults, Request $request) { - // This array will collect the names of all variables which have been - // altered by a converter. - // This serves two purposes: - // 1. It might prevent converters later in the pipeline to process - // a variable again. - // 2. To check if upcasting was successfull after each converter had - // a go. See below. - $converters = array(); - $route = $defaults[RouteObjectInterface::ROUTE_OBJECT]; - foreach ($this->converters as $converter) { - $converter->process($defaults, $route, $converters); + // Skip this enhancer if there are no parameter definitions. + if (!$parameters = $route->getOption('parameters')) { + return $defaults; } - // Check if all upcasting yielded a result. - // If an upcast value is NULL do a 404. - foreach ($converters as $variable) { - if ($defaults[$variable] === NULL) { + // Invoke the registered converter for each parameter. + foreach ($parameters as $name => $definition) { + if (!isset($defaults[$name])) { + // Do not try to convert anything that is already set to NULL. + continue; + } + + if (!isset($definition['converter'])) { + // Continue if no converter has been specified. + continue; + } + + // If a converter returns NULL it means that the parameter could not be + // converted in which case we throw a 404. + $defaults[$name] = $this->getConverter($definition['converter'])->convert($defaults[$name], $definition, $name, $defaults, $request); + if (!isset($defaults[$name])) { throw new NotFoundHttpException(); } } return $defaults; } + } + diff --git a/core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php b/core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php index 11874555c20dc485dd6f1e6f2e3ca251e7b6661c..ad966e2a24dd1d2deaed05daaa603bdae30175c7 100644 --- a/core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php @@ -50,15 +50,13 @@ public function testUpcasting() { $this->assertRaw("user: {$user->label()}, node: {$node->label()}, foo: $foo", 'user and node upcast by entity name'); // paramconverter_test/test_node_user_user/{node}/{foo}/{user} - // converters: - // foo: 'user' - $this->drupalGet('paramconverter_test/test_node_user_user/' . $node->id() . '/' . $user->id() . "/" . $user->id()); + // options.parameters.foo.type = entity:user + $this->drupalGet("paramconverter_test/test_node_user_user/{$node->nid}/" . $user->id() . "/" . $user->id()); $this->assertRaw("user: {$user->label()}, node: {$node->label()}, foo: {$user->label()}", 'foo converted to user as well'); // paramconverter_test/test_node_node_foo/{user}/{node}/{foo} - // converters: - // user: 'node' - $this->drupalGet('paramconverter_test/test_node_node_foo/' . $node->id() . '/' . $node->id() . "/$foo"); + // options.parameters.user.type = entity:node + $this->drupalGet("paramconverter_test/test_node_node_foo/{$node->nid}/{$node->nid}/$foo"); $this->assertRaw("user: {$node->label()}, node: {$node->label()}, foo: $foo", 'user is upcast to node (rather than to user)'); } @@ -69,9 +67,8 @@ public function testSameTypes() { $node = $this->drupalCreateNode(array('title' => $this->randomName(8))); $parent = $this->drupalCreateNode(array('title' => $this->randomName(8))); // paramconverter_test/node/{node}/set/parent/{parent} - // converters: - // parent: 'node' - $this->drupalGet("paramconverter_test/node/" . $node->id() . "/set/parent/" . $parent->id()); + // options.parameters.parent.type = entity:node + $this->drupalGet("paramconverter_test/node/" . $node->nid . "/set/parent/" . $parent->nid); $this->assertRaw("Setting '" . $parent->title . "' as parent of '" . $node->title . "'."); } } diff --git a/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.routing.yml b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.routing.yml index 9d226e422b7b7ab796ac3b5a6ce1261728fb8749..10efb11375363a7634698ab42613ec18a9a3be66 100644 --- a/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.routing.yml +++ b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.routing.yml @@ -12,8 +12,9 @@ paramconverter_test_node_user_user: requirements: _access: 'TRUE' options: - converters: - foo: 'user' + parameters: + foo: + type: 'entity:user' paramconverter_test_node_node_foo: pattern: '/paramconverter_test/test_node_node_foo/{user}/{node}/{foo}' @@ -22,8 +23,9 @@ paramconverter_test_node_node_foo: requirements: _access: 'TRUE' options: - converters: - user: 'node' + parameters: + user: + type: 'entity:node' paramconverter_test_node_set_parent: pattern: '/paramconverter_test/node/{node}/set/parent/{parent}' @@ -32,5 +34,6 @@ paramconverter_test_node_set_parent: defaults: _content: '\Drupal\paramconverter_test\TestControllers::testNodeSetParent' options: - converters: - parent: 'node' + parameters: + parent: + type: 'entity:node' diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ParamConverter/ViewUIConverter.php b/core/modules/views_ui/lib/Drupal/views_ui/ParamConverter/ViewUIConverter.php index f828de1f648b37de00f4116890955ff019119a2c..ba3a030f96d99893b9adba50791b2d3856a40450 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/ParamConverter/ViewUIConverter.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/ParamConverter/ViewUIConverter.php @@ -7,16 +7,31 @@ namespace Drupal\views_ui\ParamConverter; +use Drupal\Core\Entity\EntityManager; +use Drupal\Core\ParamConverter\EntityConverter; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; use Drupal\Core\ParamConverter\ParamConverterInterface; use Drupal\user\TempStoreFactory; -use Drupal\views\ViewStorageInterface; use Drupal\views_ui\ViewUI; /** * Provides upcasting for a view entity to be used in the Views UI. + * + * Example: + * + * pattern: '/some/{view}/and/{bar}' + * options: + * parameters: + * view: + * type: 'entity:view' + * tempstore: TRUE + * + * The value for {view} will be converted to a view entity prepared for the + * Views UI and loaded from the views temp store, but it will not touch the + * value for {bar}. */ -class ViewUIConverter implements ParamConverterInterface { +class ViewUIConverter extends EntityConverter implements ParamConverterInterface { /** * Stores the tempstore factory. @@ -31,77 +46,49 @@ class ViewUIConverter implements ParamConverterInterface { * @param \Drupal\user\TempStoreFactory $temp_store_factory * The factory for the temp store object. */ - public function __construct(TempStoreFactory $temp_store_factory) { + public function __construct(EntityManager $entity_manager, TempStoreFactory $temp_store_factory) { + parent::__construct($entity_manager); + $this->tempStoreFactory = $temp_store_factory; } /** - * Tries to upcast every view entity to a decorated ViewUI object. - * - * The key refers to the portion of the route that is a view entity that - * should be prepared for the Views UI. If there is a non-null value, it will - * be used as the collection of a temp store object used for loading. - * - * Example: - * - * pattern: '/some/{view}/and/{foo}/and/{bar}' - * options: - * converters: - * foo: 'view' - * tempstore: - * view: 'views' - * foo: NULL - * - * The values for {view} and {foo} will be converted to view entities prepared - * for the Views UI, with {view} being loaded from the views temp store, but - * it will not touch the value for {bar}. - * - * Note: This requires that the placeholder either be named {view}, or that a - * converter is specified as done above for {foo}. - * - * It will still process variables which are marked as converted. It will mark - * any variable it processes as converted. - * - * @param array &$variables - * Array of values to convert to their corresponding objects, if applicable. - * @param \Symfony\Component\Routing\Route $route - * The route object. - * @param array &$converted - * Array collecting the names of all variables which have been - * altered by a converter. + * {@inheritdoc} */ - public function process(array &$variables, Route $route, array &$converted) { - // If nothing was specified to convert, return. - $options = $route->getOptions(); - if (!isset($options['tempstore'])) { + public function convert($value, $definition, $name, array $defaults, Request $request) { + if (!$entity = parent::convert($value, $definition, $name, $defaults, $request)) { return; } - foreach ($options['tempstore'] as $name => $collection) { - // Only convert if the variable is a view. - if ($variables[$name] instanceof ViewStorageInterface) { - // Get the temp store for this variable if it needs one. - // Attempt to load the view from the temp store, synchronize its - // status with the existing view, and store the lock metadata. - if ($collection && ($temp_store = $this->tempStoreFactory->get($collection)) && ($view = $temp_store->get($variables[$name]->id()))) { - if ($variables[$name]->status()) { - $view->enable(); - } - else { - $view->disable(); - } - $view->lock = $temp_store->getMetadata($variables[$name]->id()); - } - // Otherwise, decorate the existing view for use in the UI. - else { - $view = new ViewUI($variables[$name]); - } - - // Store the new view and mark this variable as converted. - $variables[$name] = $view; - $converted[] = $name; + // Get the temp store for this variable if it needs one. Attempt to load the + // view from the temp store, synchronize its status with the existing view, + // and store the lock metadata. + $store = $this->tempStoreFactory->get('views'); + if ($view = $store->get($value)) { + if ($entity->status()) { + $view->enable(); } + else { + $view->disable(); + } + $view->lock = $store->getMetadata($value); + } + // Otherwise, decorate the existing view for use in the UI. + else { + $view = new ViewUI($entity); + } + + return $view; + } + + /** + * {@inheritdoc} + */ + public function applies($definition, $name, Route $route) { + if (parent::applies($definition, $name, $route)) { + return !empty($definition['tempstore']) && $definition['type'] === 'entity:view'; } + return FALSE; } } diff --git a/core/modules/views_ui/views_ui.routing.yml b/core/modules/views_ui/views_ui.routing.yml index 0d44630e1e78966abab1f08292596342e67c7fd4..cc56c8bf3963424664277bc6b6903288c47b96aa 100644 --- a/core/modules/views_ui/views_ui.routing.yml +++ b/core/modules/views_ui/views_ui.routing.yml @@ -72,8 +72,9 @@ views_ui.autocomplete: views_ui.edit: pattern: '/admin/structure/views/view/{view}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Controller\ViewsUIController::edit' requirements: @@ -82,8 +83,9 @@ views_ui.edit: views_ui.edit.display: pattern: '/admin/structure/views/view/{view}/edit/{display_id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Controller\ViewsUIController::edit' display_id: NULL @@ -93,8 +95,9 @@ views_ui.edit.display: views_ui.preview: pattern: '/admin/structure/views/view/{view}/preview/{display_id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _entity_form: 'view.preview' display_id: NULL @@ -111,8 +114,9 @@ views_ui.breakLock: views_ui.form.addItem: pattern: '/admin/structure/views/{js}/add-item/{view}/{display_id}/{type}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\AddItem::getForm' requirements: @@ -122,8 +126,9 @@ views_ui.form.addItem: views_ui.form.editDetails: pattern: '/admin/structure/views/{js}/edit-details/{view}/{display_id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\EditDetails::getForm' requirements: @@ -133,8 +138,9 @@ views_ui.form.editDetails: views_ui.form.reorderDisplays: pattern: '/admin/structure/views/{js}/reorder-displays/{view}/{display_id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\ReorderDisplays::getForm' requirements: @@ -144,8 +150,9 @@ views_ui.form.reorderDisplays: views_ui.form.analyze: pattern: '/admin/structure/views/{js}/analyze/{view}/{display_id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\Analyze::getForm' requirements: @@ -155,8 +162,9 @@ views_ui.form.analyze: views_ui.form.rearrange: pattern: '/admin/structure/views/{js}/rearrange/{view}/{display_id}/{type}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\Rearrange::getForm' requirements: @@ -166,8 +174,9 @@ views_ui.form.rearrange: views_ui.form.rearrangeFilter: pattern: '/admin/structure/views/{js}/rearrange-filter/{view}/{display_id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\RearrangeFilter::getForm' requirements: @@ -177,8 +186,9 @@ views_ui.form.rearrangeFilter: views_ui.form.display: pattern: '/admin/structure/views/{js}/display/{view}/{display_id}/{type}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\Display::getForm' requirements: @@ -188,8 +198,9 @@ views_ui.form.display: views_ui.form.configItem: pattern: '/admin/structure/views/{js}/config-item/{view}/{display_id}/{type}/{id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\ConfigItem::getForm' requirements: @@ -199,8 +210,9 @@ views_ui.form.configItem: views_ui.form.configItemExtra: pattern: '/admin/structure/views/{js}/config-item-extra/{view}/{display_id}/{type}/{id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\ConfigItemExtra::getForm' requirements: @@ -210,8 +222,9 @@ views_ui.form.configItemExtra: views_ui.form.configItemGroup: pattern: '/admin/structure/views/{js}/config-item-group/{view}/{display_id}/{type}/{id}' options: - tempstore: - view: 'views' + parameters: + view: + tempstore: TRUE defaults: _controller: '\Drupal\views_ui\Form\Ajax\ConfigItemGroup::getForm' form_state: NULL diff --git a/core/modules/views_ui/views_ui.services.yml b/core/modules/views_ui/views_ui.services.yml index 34b274005ea6f9defd662bbce4c573f0c5f1186e..3120b87b6d4b9f65815bd94df4e73c34d0882476 100644 --- a/core/modules/views_ui/views_ui.services.yml +++ b/core/modules/views_ui/views_ui.services.yml @@ -1,6 +1,6 @@ services: paramconverter.views_ui: class: Drupal\views_ui\ParamConverter\ViewUIConverter - arguments: ['@user.tempstore'] + arguments: ['@plugin.manager.entity', '@user.tempstore'] tags: - - { name: paramconverter } + - { name: paramconverter, priority: 10 } diff --git a/core/tests/Drupal/Tests/Core/ParamConverter/ParamConverterManagerTest.php b/core/tests/Drupal/Tests/Core/ParamConverter/ParamConverterManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6e9a018b82a67dbdccdef66e3a01cd83aee03d53 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/ParamConverter/ParamConverterManagerTest.php @@ -0,0 +1,145 @@ +<?php + +/** + * @file + * Contains Drupal\Tests\Core\ParamConverter\ParamConverterManagerTest. + */ + +namespace Drupal\Tests\Core\ParamConverter; + +use Drupal\Core\ParamConverter\ParamConverterManager; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Tests the typed data resolver manager. + */ +class ParamConverterManagerTest extends UnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Parameter converter manager', + 'description' => 'Tests the parameter converter manager.', + 'group' => 'Routing', + ); + } + + public function setUp() { + parent::setUp(); + + $this->container = new ContainerBuilder(); + $this->manager = new ParamConverterManager(); + $this->manager->setContainer($this->container); + } + + /** + * Tests \Drupal\Core\ParamConverter\ParamConverterManager::addConverter(). + * + * @dataProvider providerTestAddConverter + * + * @see ParamConverterManagerTest::providerTestAddConverter(). + */ + public function testAddConverter($unsorted, $sorted) { + foreach ($unsorted as $data) { + $this->manager->addConverter($data['name'], $data['priority']); + } + + // Test that ResolverManager::getTypedDataResolvers() returns the resolvers + // in the expected order. + foreach ($this->manager->getConverterIds() as $key => $converter) { + $this->assertEquals($sorted[$key], $converter); + } + } + + /** + * Tests \Drupal\Core\ParamConverter\ParamConverterManager::getConverter(). + * + * @dataProvider providerTestGetConverter + * + * @see ParamConverterManagerTest::providerTestGetConverter(). + */ + public function testGetConverter($name, $priority, $class) { + $converter = $this->getMockBuilder('Drupal\Core\ParamConverter\ParamConverterInterface') + ->setMockClassName($class) + ->getMock(); + + $this->manager->addConverter($name, $priority); + $this->container->set($name, $converter); + + $this->assertInstanceOf($class, $this->manager->getConverter($name)); + } + + /** + * Tests \Drupal\Core\ParamConverter\ParamConverterManager::getConverter(). + * + * @expectedException InvalidArgumentException + */ + public function testGetConverterException() { + $this->manager->getConverter('undefined.converter'); + } + + /** + * Provide data for parameter converter manager tests. + * + * @return array + * An array of arrays, each containing the input parameters for + * providerTestResolvers::testAddConverter(). + * + * @see ParamConverterManagerTest::testAddConverter(). + */ + public function providerTestAddConverter() { + $converters[0]['unsorted'] = array( + array('name' => 'raspberry', 'priority' => 10), + array('name' => 'pear', 'priority' => 5), + array('name' => 'strawberry', 'priority' => 20), + array('name' => 'pineapple', 'priority' => 0), + array('name' => 'banana', 'priority' => -10), + array('name' => 'apple', 'priority' => -10), + array('name' => 'peach', 'priority' => 5), + ); + + $converters[0]['sorted'] = array( + 'strawberry', 'raspberry', 'pear', 'peach', + 'pineapple', 'banana', 'apple' + ); + + $converters[1]['unsorted'] = array( + array('name' => 'ape', 'priority' => 0), + array('name' => 'cat', 'priority' => -5), + array('name' => 'puppy', 'priority' => -10), + array('name' => 'llama', 'priority' => -15), + array('name' => 'giraffe', 'priority' => 10), + array('name' => 'zebra', 'priority' => 10), + array('name' => 'eagle', 'priority' => 5), + ); + + $converters[1]['sorted'] = array( + 'giraffe', 'zebra', 'eagle', 'ape', + 'cat', 'puppy', 'llama' + ); + + return $converters; + } + + /** + * Provide data for parameter converter manager tests. + * + * @return array + * An array of arrays, each containing the input parameters for + * providerTestResolvers::testGetConverter(). + * + * @see ParamConverterManagerTest::testGetConverter(). + */ + public function providerTestGetConverter() { + return array( + array('ape', 0, 'ApeConverterClass'), + array('cat', -5, 'CatConverterClass'), + array('puppy', -10, 'PuppyConverterClass'), + array('llama', -15, 'LlamaConverterClass'), + array('giraffe', 10, 'GiraffeConverterClass'), + array('zebra', 10, 'ZebraConverterClass'), + array('eagle', 5, 'EagleConverterClass'), + ); + } + +}