summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcatch2013-02-06 12:47:22 (GMT)
committercatch2013-02-06 12:47:22 (GMT)
commitc5b09f74ebeae95b10b72d206f4ab7c19b440b5d (patch)
treee71602f4e3df16d439ef957ca49e781ec7242ce4
parent5b0419836431476a5aaad54f15525a6aeec2abd2 (diff)
Issue #1798214 by tnightingale, g.oechsler, fubhy, katbailey, effulgentsia, Crell, dipen chaudhary: Upcast request arguments/attributes to full objects.
-rw-r--r--core/includes/path.inc5
-rw-r--r--core/lib/Drupal/Core/CoreBundle.php11
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php48
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteEnhancersPass.php38
-rw-r--r--core/lib/Drupal/Core/ParamConverter/EntityConverter.php106
-rw-r--r--core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php29
-rw-r--r--core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php89
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php77
-rw-r--r--core/modules/system/tests/modules/paramconverter_test/lib/Drupal/paramconverter_test/TestControllers.php27
-rw-r--r--core/modules/system/tests/modules/paramconverter_test/paramconverter_test.info6
-rw-r--r--core/modules/system/tests/modules/paramconverter_test/paramconverter_test.module6
-rw-r--r--core/modules/system/tests/modules/paramconverter_test/paramconverter_test.routing.yml36
12 files changed, 477 insertions, 1 deletions
diff --git a/core/includes/path.inc b/core/includes/path.inc
index f5a0a9f..6e2c19e 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 aebb514..a90415c 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 0000000..6fe1447
--- /dev/null
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterParamConvertersPass.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass.
+ */
+
+namespace Drupal\Core\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Registers EntityConverter services with the ParamConverterManager.
+ */
+class RegisterParamConvertersPass implements CompilerPassInterface {
+
+ /**
+ * Adds services tagged with "paramconverter" to the param converter service.
+ *
+ * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+ * 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));
+ }
+ }
+ }
+}
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 0000000..2718b68
--- /dev/null
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteEnhancersPass.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass.
+ */
+
+namespace Drupal\Core\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Registers route enhancer services with the router.
+ */
+class RegisterRouteEnhancersPass implements CompilerPassInterface {
+
+ /**
+ * Adds services tagged with "route_enhancer" to the router.
+ *
+ * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+ * The container to process.
+ */
+ public function process(ContainerBuilder $container) {
+ if (!$container->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 0000000..f28906f
--- /dev/null
+++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\ParamConverter\EntityConverter.
+ */
+
+namespace Drupal\Core\ParamConverter;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Drupal\Core\Entity\EntityManager;
+
+/**
+ * This class allows the upcasting of entity ids to the respective entity
+ * object.
+ */
+class EntityConverter implements ParamConverterInterface {
+
+ /**
+ * Entity manager which performs the upcasting in the end.
+ *
+ * @var \Drupal\Core\Entity\EntityManager
+ */
+ protected $entityManager;
+
+ /**
+ * Constructs a new EntityConverter.
+ *
+ * @param \Drupal\Core\Entity\EntityManager $entityManager
+ * The entity manager.
+ */
+ public function __construct(EntityManager $entityManager) {
+ $this->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 0000000..74307c2
--- /dev/null
+++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\ParamConverter\ParamConverterInterface.
+ */
+
+namespace Drupal\Core\ParamConverter;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * Interface for parameter converters.
+ */
+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 \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);
+}
diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php
new file mode 100644
index 0000000..be5676e
--- /dev/null
+++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\ParamConverter\ParamConverterManager.
+ */
+
+namespace Drupal\Core\ParamConverter;
+
+use Symfony\Component\DependencyInjection\ContainerAware;
+use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+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.
+ *
+ * This class will not enhance any of the arguments itself, but allow other
+ * services to register to do so.
+ */
+class ParamConverterManager implements RouteEnhancerInterface {
+
+ /**
+ * Converters managed by the ParamConverterManager.
+ *
+ * @var array
+ */
+ protected $converters;
+
+ /**
+ * Adds a converter to the paramconverter service.
+ *
+ * @see \Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass
+ *
+ * @param \Drupal\Core\ParamConverter\ParamConverterInterface $converter
+ * The converter to add.
+ */
+ public function addConverter(ParamConverterInterface $converter) {
+ $this->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 0000000..14a0422
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/ParamConverter/UpcastingTest.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\system\Tests\ParamConverter\UpcastingTest.
+ */
+
+namespace Drupal\system\Tests\ParamConverter;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Web tests for the upcasting.
+ */
+class UpcastingTest extends WebTestBase {
+
+ /**
+ * Implement getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => '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 0000000..ddb4833
--- /dev/null
+++ b/core/modules/system/tests/modules/paramconverter_test/lib/Drupal/paramconverter_test/TestControllers.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\paramconverter_test\TestControllers.
+ */
+
+namespace Drupal\paramconverter_test;
+
+use Drupal\node\Plugin\Core\Entity\Node;
+
+/**
+ * Controller routine for testing the paramconverter.
+ */
+class TestControllers {
+
+ public function testUserNodeFoo($user, $node, $foo) {
+ $retval = "user: " . (is_object($user) ? $user->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 0000000..3db382a
--- /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 0000000..c37a9e2
--- /dev/null
+++ b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.module
@@ -0,0 +1,6 @@
+<?php
+
+/**
+ * @file
+ * Intentionally blank file.
+ */
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
new file mode 100644
index 0000000..9d226e4
--- /dev/null
+++ b/core/modules/system/tests/modules/paramconverter_test/paramconverter_test.routing.yml
@@ -0,0 +1,36 @@
+paramconverter_test_user_node_foo:
+ pattern: '/paramconverter_test/test_user_node_foo/{user}/{node}/{foo}'
+ defaults:
+ _content: '\Drupal\paramconverter_test\TestControllers::testUserNodeFoo'
+ requirements:
+ _access: 'TRUE'
+
+paramconverter_test_node_user_user:
+ pattern: '/paramconverter_test/test_node_user_user/{node}/{foo}/{user}'
+ defaults:
+ _content: '\Drupal\paramconverter_test\TestControllers::testUserNodeFoo'
+ requirements:
+ _access: 'TRUE'
+ options:
+ converters:
+ foo: 'user'
+
+paramconverter_test_node_node_foo:
+ pattern: '/paramconverter_test/test_node_node_foo/{user}/{node}/{foo}'
+ defaults:
+ _content: '\Drupal\paramconverter_test\TestControllers::testUserNodeFoo'
+ requirements:
+ _access: 'TRUE'
+ options:
+ converters:
+ user: 'node'
+
+paramconverter_test_node_set_parent:
+ pattern: '/paramconverter_test/node/{node}/set/parent/{parent}'
+ requirements:
+ _access: 'TRUE'
+ defaults:
+ _content: '\Drupal\paramconverter_test\TestControllers::testNodeSetParent'
+ options:
+ converters:
+ parent: 'node'