Commit c5b09f74 authored by catch's avatar catch

Issue #1798214 by tnightingale, g.oechsler, fubhy, katbailey, effulgentsia,...

Issue #1798214 by tnightingale, g.oechsler, fubhy, katbailey, effulgentsia, Crell, dipen chaudhary: Upcast request arguments/attributes to full objects.
parent 5b041983
......@@ -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\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 @@ public function build(ContainerBuilder $container) {
$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 @@ public function build(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.
$container->addCompilerPass(new RegisterParamConvertersPass());
$container->addCompilerPass(new RegisterRouteEnhancersPass());
}
/**
......
<?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));
}
}
}
}
<?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));
}
}
}
<?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;
}
}
}
}
<?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);
}
<?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;
}
}
<?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 . "'.");
}
}
<?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}'.";
}
}
name = "ParamConverter test"
description = "Support module for paramconverter testing."
package = Testing
version = VERSION
core = 8.x
hidden = TRUE
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'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment