Issue #1798214 by tnightingale, g.oechsler, fubhy, katbailey, effulgentsia, Crell, dipen chaudhary: Upcast request arguments/attributes to full objects.
parent
5b04198364
commit
c5b09f74eb
|
@ -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().
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 . "'.");
|
||||
}
|
||||
}
|
|
@ -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}'.";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name = "ParamConverter test"
|
||||
description = "Support module for paramconverter testing."
|
||||
package = Testing
|
||||
version = VERSION
|
||||
core = 8.x
|
||||
hidden = TRUE
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Intentionally blank file.
|
||||
*/
|
|
@ -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'
|
Loading…
Reference in New Issue