diff --git a/core/includes/path.inc b/core/includes/path.inc index f5a0a9fb8af..6e2c19e2dc9 100644 --- a/core/includes/path.inc +++ b/core/includes/path.inc @@ -85,7 +85,10 @@ function current_path() { // fallback code below, once the path alias logic has been figured out in // http://drupal.org/node/1269742. if (drupal_container()->isScopeActive('request')) { - return drupal_container()->get('request')->attributes->get('system_path'); + $path = drupal_container()->get('request')->attributes->get('system_path'); + if ($path !== NULL) { + return $path; + } } // If we are outside the request scope, fall back to using the path stored in // _current_path(). diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index aebb51444eb..a90415c6643 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -11,7 +11,9 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass; use Drupal\Core\DependencyInjection\Compiler\RegisterMatchersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterSerializationClassesPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -206,6 +208,12 @@ class CoreBundle extends Bundle { $container->register('mime_type_matcher', 'Drupal\Core\Routing\MimeTypeMatcher') ->addTag('route_filter'); + $container->register('paramconverter_manager', 'Drupal\Core\ParamConverter\ParamConverterManager') + ->addTag('route_enhancer'); + $container->register('paramconverter.entity', 'Drupal\Core\ParamConverter\EntityConverter') + ->addArgument(new Reference('plugin.manager.entity')) + ->addTag('paramconverter'); + $container->register('router_processor_subscriber', 'Drupal\Core\EventSubscriber\RouteProcessorSubscriber') ->addArgument(new Reference('content_negotiation')) ->addTag('event_subscriber'); @@ -286,6 +294,9 @@ class CoreBundle extends Bundle { // 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. + $container->addCompilerPass(new RegisterParamConvertersPass()); + $container->addCompilerPass(new RegisterRouteEnhancersPass()); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php new file mode 100644 index 00000000000..6fe1447bc58 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php @@ -0,0 +1,48 @@ +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)); + } + } + } +} diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteEnhancersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteEnhancersPass.php new file mode 100644 index 00000000000..2718b6870ed --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteEnhancersPass.php @@ -0,0 +1,38 @@ +hasDefinition('router.dynamic')) { + return; + } + + $router = $container->getDefinition('router.dynamic'); + + $services = array(); + foreach ($container->findTaggedServiceIds('route_enhancer') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $router->addMethodCall('addRouteEnhancer', array(new Reference($id), $priority)); + } + } +} diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php new file mode 100644 index 00000000000..f28906ff20e --- /dev/null +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -0,0 +1,106 @@ +entityManager = $entityManager; + } + + /** + * 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. + */ + 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); + $entities = $storageController->load(array($value)); + + // Make sure $entities is null, if upcasting fails. + $entity = $entities ? reset($entities) : NULL; + $variables[$name] = $entity; + + // Mark this variable as converted. + $converted[] = $name; + } + } + } +} diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php new file mode 100644 index 00000000000..74307c2762c --- /dev/null +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php @@ -0,0 +1,29 @@ +converters[] = $converter; + return $this; + } + + /** + * Implements \Symfony\Cmf\Component\Routing\Enhancer\Å–outeEnhancerIterface. + * + * Iterates over all registered converters and allows them to alter the + * defaults. + * + * @param array $defaults + * The getRouteDefaults array. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @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); + } + + // Check if all upcasting yielded a result. + // If an upcast value is NULL do a 404. + foreach ($converters as $variable) { + if ($defaults[$variable] === NULL) { + 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 new file mode 100644 index 00000000000..14a04227648 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php @@ -0,0 +1,77 @@ + 'Upcasting tests', + 'description' => 'Tests upcasting of url arguments to entities.', + 'group' => 'ParamConverter', + ); + } + + public static $modules = array('paramconverter_test'); + + /** + * Confirms that all parameters are converted as expected. + * + * All of these requests end up being proccessed by a controller with this + * the signature: f($user, $node, $foo) returning either values or labels + * like "user: Dries, node: First post, foo: bar" + * + * The tests shuffle the parameters around an checks if the right thing is + * happening. + */ + public function testUpcasting() { + $node = $this->drupalCreateNode(array('title' => $this->randomName(8))); + $user = $this->drupalCreateUser(array('access content')); + $foo = 'bar'; + + // paramconverter_test/test_user_node_foo/{user}/{node}/{foo} + $this->drupalGet("paramconverter_test/test_user_node_foo/{$user->uid}/{$node->nid}/$foo"); + $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->nid}/{$user->uid}/{$user->uid}"); + $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->nid}/{$node->nid}/$foo"); + $this->assertRaw("user: {$node->label()}, node: {$node->label()}, foo: $foo", 'user is upcast to node (rather than to user)'); + } + + /** + * Confirms we can upcast to controller arguments of the same type. + */ + 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->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/lib/Drupal/paramconverter_test/TestControllers.php b/core/modules/system/tests/modules/paramconverter_test/lib/Drupal/paramconverter_test/TestControllers.php new file mode 100644 index 00000000000..ddb4833e2f2 --- /dev/null +++ b/core/modules/system/tests/modules/paramconverter_test/lib/Drupal/paramconverter_test/TestControllers.php @@ -0,0 +1,27 @@ +label() : $user); + $retval .= ", node: " . (is_object($node) ? $node->label() : $node); + $retval .= ", foo: " . (is_object($foo) ? $foo->label() : $foo); + return $retval; + } + + public function testNodeSetParent(Node $node, Node $parent) { + return "Setting '{$parent->title}' as parent of '{$node->title}'."; + } +} diff --git a/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.info b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.info new file mode 100644 index 00000000000..3db382a406e --- /dev/null +++ b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.info @@ -0,0 +1,6 @@ +name = "ParamConverter test" +description = "Support module for paramconverter testing." +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.module b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.module new file mode 100644 index 00000000000..c37a9e2fec2 --- /dev/null +++ b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.module @@ -0,0 +1,6 @@ +