diff --git a/core/core.services.yml b/core/core.services.yml index b8dd86a..b18fb74 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -206,8 +206,9 @@ services: arguments: [slave] typed_data: class: Drupal\Core\TypedData\TypedDataManager - arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@module_handler', '@service_container'] + arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@module_handler'] calls: + - [setContainer, ['@service_container']] - [setValidationConstraintManager, ['@validation.constraint']] typed_data_subscriber: class: Drupal\Core\EventSubscriber\TypedDataSubscriber @@ -319,7 +320,7 @@ services: - { name: event_subscriber } arguments: ['@paramconverter_manager'] paramconverter.typeddata: - class: Drupal\Core\ParamConverter\TypedDataConverter + class: Drupal\Core\TypedData\TypedDataConverter tags: - { name: paramconverter } arguments: ['@typed_data'] diff --git a/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php index 1afff6c..f9a0a69 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ParamConverterSubscriber.php @@ -46,10 +46,7 @@ public function onRoutingRouteAlterSetParameterConverters(RouteBuildEvent $event } /** - * Registers the methods in this class that should be listeners. - * - * @return array - * An array of event listener definitions. + * {@inheritdoc} */ static function getSubscribedEvents() { $events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetParameterConverters', 10); diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php index 5f4bd4f..c1265fc 100644 --- a/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterInterface.php @@ -27,7 +27,7 @@ * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * - * @return mixed + * @return mixed|null * The converted parameter value. */ public function convert($definition, $name, array $defaults, Request $request); diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php index 0c0990a..2bb6532 100644 --- a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php @@ -91,8 +91,8 @@ public function getConverterIds() { * A collection of routes to apply converters to. */ public function setRouteParameterConverters(RouteCollection $routes) { - foreach ($routes as $route) { - if (!$parameters = $route->getOption('parameters') ?: array()) { + foreach ($routes->all() as $route) { + if (!$parameters = $route->getOption('parameters')) { // Continue with the next route if no parameters have been defined. continue; } @@ -125,7 +125,8 @@ public function setRouteParameterConverters(RouteCollection $routes) { * The current request. * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - * If a variable has been converted to NULL. + * If one of the assigned converters returned NULL because the given + * variable could not be converted. * * @return array * The modified defaults. diff --git a/core/lib/Drupal/Core/TypedData/LoadableInterface.php b/core/lib/Drupal/Core/TypedData/LoadableInterface.php index 0405256..12eeaea 100644 --- a/core/lib/Drupal/Core/TypedData/LoadableInterface.php +++ b/core/lib/Drupal/Core/TypedData/LoadableInterface.php @@ -2,7 +2,7 @@ /** * @file - * Contains LoadableInterface. + * Contains \Drupal\Core\TypedData\LoadableInterface. */ namespace Drupal\Core\TypedData; diff --git a/core/lib/Drupal/Core/TypedData/Resolver/EntityResolver.php b/core/lib/Drupal/Core/TypedData/Resolver/EntityResolver.php index 228bcf0..cf90187 100644 --- a/core/lib/Drupal/Core/TypedData/Resolver/EntityResolver.php +++ b/core/lib/Drupal/Core/TypedData/Resolver/EntityResolver.php @@ -9,7 +9,6 @@ use Drupal\Core\TypedData\TypedDataManager; use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; /** * Generates typed data definitions for specific entity routes. @@ -59,7 +58,8 @@ public function resolveParameterTypes(Route $route) { } // Check if there is a variable that matches the provided entity type. - if (!in_array($entity_type, $route->compile()->getVariables())) { + $variables = $route->compile()->getVariables(); + if (!in_array($entity_type, $variables)) { return; } diff --git a/core/lib/Drupal/Core/TypedData/Resolver/ReflectionResolver.php b/core/lib/Drupal/Core/TypedData/Resolver/ReflectionResolver.php index 2d6e618..dd20fe2 100644 --- a/core/lib/Drupal/Core/TypedData/Resolver/ReflectionResolver.php +++ b/core/lib/Drupal/Core/TypedData/Resolver/ReflectionResolver.php @@ -38,6 +38,8 @@ public function __construct(TypedDataManager $typed_data_manager) { */ public function resolveParameterTypes(Route $route) { $defaults = $route->getDefaults(); + $predefined = $route->getOption('parameters') ?: array(); + $predefined = array_keys($predefined); // Iterate over all entries in the defaults array to find any callable // that might have type hints from which we can draw clues. @@ -45,12 +47,7 @@ public function resolveParameterTypes(Route $route) { foreach ($defaults as $default) { if (is_string($default) && strpos($default, '::') !== FALSE) { list($class, $method) = explode('::', $default, 2); - if (!class_exists($class)) { - throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); - } - - $callable = array($class, $method); - if ($parameters = $this->getParameterTypes($callable)) { + if ($parameters = $this->getParameterTypes($class, $method, $predefined)) { $definitions = array_merge($parameters, $definitions); } } @@ -60,29 +57,36 @@ public function resolveParameterTypes(Route $route) { } /** - * Tries to determine the typed data types via the type hints on a callable. + * Tries to determine the typed data types via the type hints on a method. + * + * @param string $class + * The name of a class. + * @param string $method + * The name of a method on the given class. + * @param array $predefined + * A list of pre-defined parameter definitions. * - * @param mixed $callable - * A callable. + * @throws \InvalidArgumentException + * If the given class or method does not exist. * * @return array * An array of typed data definitions keyed by parameter name. */ - protected function getParameterTypes($callable) { - if (is_array($callable)) { - $reflection = new \ReflectionMethod($callable[0], $callable[1]); + protected function getParameterTypes($class, $method, array $predefined) { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); } - elseif (is_object($callable) && !$callable instanceof \Closure) { - $reflection = new \ReflectionObject($callable); - $reflection = $reflection->getMethod('__invoke'); + if (!method_exists($class, $method)) { + throw new \InvalidArgumentException(sprintf('Method "%s::%s" does not exist.', $class, $method)); } - else { - $reflection = new \ReflectionFunction($callable); - } - + $reflection = new \ReflectionMethod($class, $method); $definitions = array(); foreach ($reflection->getParameters() as $parameter) { - $definitions[$parameter->getName()] = $this->getParameterType($parameter); + $name = $parameter->getName(); + // Do not try to resolve parameters that have been manually set. + if (!isset($predefined[$name])) { + $definitions[$name] = $this->getParameterType($parameter); + } } return $definitions; } @@ -110,7 +114,7 @@ protected function getParameterType(\ReflectionParameter $parameter) { } // Try to find a typed data type that matches the type hint. - foreach ($this->typedDataManager->getDefinitions() as $type => $definition) { + foreach ($this->typedDataManager->getDefinitions() as $definition) { if ($definition['class'] == $name) { // Return the first match. return array( diff --git a/core/lib/Drupal/Core/TypedData/Resolver/ResolverManager.php b/core/lib/Drupal/Core/TypedData/Resolver/ResolverManager.php index 23f752e..7860b39 100644 --- a/core/lib/Drupal/Core/TypedData/Resolver/ResolverManager.php +++ b/core/lib/Drupal/Core/TypedData/Resolver/ResolverManager.php @@ -13,6 +13,21 @@ /** * Generates typed data definitions for route parameters. + * + * By defining the typed data type of a route argument, other systems (like + * page manager) get the chance to manipulate the behavior of a given route + * while being fully aware of the available contexts. + * + * Typed data resolver services can employ different strategies to automatically + * discover and return the typed data type of certain route parameters e.g. + * by reflecting the type hints on the assigned controller or by parsing other + * metadata in a route definition that clearly identifies the type of a given + * parameter. + * + * This greatly increases the developer experience when defining new routes as + * it removes the need to repeat the type definition in the route options even + * though it may already be obvious e.g. due to a type hint or a special, + * generic controller like '_entity_form' or '_entity_list'. */ class ResolverManager { @@ -77,6 +92,7 @@ public function resolveParameterTypes(RouteCollection $routes) { $resolvers = $this->getTypedDataResolvers(); foreach ($routes->all() as $route) { $definitions = array(); + $before = $route->getOption('parameters') ?: array(); foreach ($resolvers as $resolver) { if ($parameters = $resolver->resolveParameterTypes($route)) { $definitions = array_merge($parameters, $definitions); @@ -86,7 +102,7 @@ public function resolveParameterTypes(RouteCollection $routes) { // Merge the resolved typed data definitions into the route options. if (!empty($definitions)) { // Do not override manually defined typed data definitions. - $definitions = array_merge($definitions, ($route->getOption('parameters') ?: array())); + $definitions = array_merge($definitions, $before); $route->setOption('parameters', $definitions); } } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataConverter.php b/core/lib/Drupal/Core/TypedData/TypedDataConverter.php index 91bffc9..83a5b5b 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataConverter.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataConverter.php @@ -5,14 +5,13 @@ * Contains Drupal\Core\ParamConverter\TypedDataConverter. */ -namespace Drupal\Core\ParamConverter; +namespace Drupal\Core\TypedData; -use Drupal\Component\Plugin\Context\Context; use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Component\Utility\String; +use Drupal\Core\ParamConverter\ParamConverterInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; -use Drupal\Core\TypedData\TypedDataManager; /** * Upcasts request attributes to typed data objects. @@ -42,7 +41,7 @@ public function __construct(TypedDataManager $typed_data_manager) { public function convert($definition, $name, array $defaults, Request $request) { // Only continue if there is a value for the given parameter. if (!isset($defaults[$name])) { - return NULL; + return; } // Remove keys that do not belong to the typed data definition. diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index 89a0ddd..e2425aa 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -55,7 +55,7 @@ class TypedDataManager extends DefaultPluginManager implements ContainerAwareInt */ protected $container; - public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler, ContainerInterface $container) { + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) { $this->alterInfo($module_handler, 'data_type_info'); $this->setCacheBackend($cache_backend, $language_manager, 'typed_data:types'); @@ -63,7 +63,6 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac 'Drupal\Core\TypedData\Annotation' => DRUPAL_ROOT . '/core/lib', ); parent::__construct('DataType', $namespaces, $annotation_namespaces, 'Drupal\Core\TypedData\Annotation\DataType'); - $this->container = $container; } /** diff --git a/core/tests/Drupal/Tests/Core/TypedData/EntityResolverTest.php b/core/tests/Drupal/Tests/Core/TypedData/EntityResolverTest.php index ff89ba0..91e76e7 100644 --- a/core/tests/Drupal/Tests/Core/TypedData/EntityResolverTest.php +++ b/core/tests/Drupal/Tests/Core/TypedData/EntityResolverTest.php @@ -7,10 +7,9 @@ namespace Drupal\Tests\Core\TypedData; -use Drupal\Core\TypedData\Resolver\ResolverManager; +use Drupal\Core\TypedData\Resolver\EntityResolver; use Drupal\Tests\UnitTestCase; use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; /** * Tests the typed data entity type resolver. @@ -18,16 +17,9 @@ class EntityResolverTest extends UnitTestCase { /** - * The mocked type data manager to use for the typed data resolvers. + * The entity type resolver. * - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $typedDataManager; - - /** - * The mocked typed data entity type resolver. - * - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\TypedData\Resolver\EntityResolver */ protected $entityResolver; @@ -48,6 +40,8 @@ public function setUp() { * Tests \Drupal\Core\TypedData\Resolver\ResolverManager::resolveParamaterTypes(). * * @dataProvider providerTestParameters + * + * @see \Drupal\Tests\Core\TypedData\EntityResolverTest::providerTestParameters() */ public function testResolveParameterTypes(Route $route, $expected) { $this->assertEquals($expected, $this->entityResolver->resolveParameterTypes($route)); @@ -60,20 +54,27 @@ public function testResolveParameterTypes(Route $route, $expected) { * An array of arrays, each containing the input parameters for * ResolverManagerTest::testResolveParameterTypes(). * - * @see ResolverManagerTest::testResolveParameterTypes(). + * @see \Drupal\Tests\Core\TypedData\EntityResolverTest::testResolveParameterTypes(). */ public function providerTestParameters() { return array( + // Test that the entity resolver automatically finds the entity:node type + // if _entity_form is set to 'node.foo'. array(new Route('/test-1/{node}', array( '_entity_form' => 'node.edit', )), array( 'node' => array('type' => 'entity:node'), )), + // Test that the entity resolver automatically finds the entity:user type + // if _entity_list is set to 'user'. array(new Route('/test-2/{user}', array( '_entity_list' => 'user', )), array( 'user' => array('type' => 'entity:user'), )), + // Test that the entity resolver doesn't do anything if neither + // _entity_form nor _entity_list are defined. + array(new Route('/test-3/{user}/{node}'), NULL), ); } @@ -81,7 +82,7 @@ public function providerTestParameters() { * Sets up a mocked reflection resolver for the test. */ protected function setupEntityResolver() { - $this->typedDataManager = $this + $manager = $this ->getMockBuilder('Drupal\Core\TypedData\TypedDataManager') ->disableOriginalConstructor() ->setMethods(array('getDefinitions')) @@ -94,17 +95,9 @@ protected function setupEntityResolver() { $property = new \ReflectionProperty('Drupal\Core\TypedData\TypedDataManager', 'definitions'); $property->setAccessible(TRUE); - $property->setValue($this->typedDataManager, $definitions); + $property->setValue($manager, $definitions); - $this->entityResolver = $this - ->getMockBuilder('Drupal\Core\TypedData\Resolver\EntityResolver') - ->disableOriginalConstructor() - ->setMethods(NULL) - ->getMock(); - - $property = new \ReflectionProperty('Drupal\Core\TypedData\Resolver\EntityResolver', 'typedDataManager'); - $property->setAccessible(TRUE); - $property->setValue($this->entityResolver, $this->typedDataManager); + $this->entityResolver = new EntityResolver($manager); } } diff --git a/core/tests/Drupal/Tests/Core/TypedData/ReflectionResolverTest.php b/core/tests/Drupal/Tests/Core/TypedData/ReflectionResolverTest.php index 2c29f4b..7301a7d 100644 --- a/core/tests/Drupal/Tests/Core/TypedData/ReflectionResolverTest.php +++ b/core/tests/Drupal/Tests/Core/TypedData/ReflectionResolverTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\TypedData; +use Drupal\Core\TypedData\Resolver\ReflectionResolver; use Drupal\Core\TypedData\Resolver\ResolverManager; use Drupal\Tests\UnitTestCase; use Symfony\Component\Routing\Route; @@ -18,16 +19,9 @@ class ReflectionResolverTest extends UnitTestCase { /** - * The mocked type data manager to use for the typed data resolvers. + * The typed data reflection resolver. * - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $typedDataManager; - - /** - * The mocked typed data reflection resolver. - * - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\TypedData\Resolver\ReflectionResolver */ protected $reflectionResolver; @@ -82,7 +76,7 @@ public function providerTestParameters() { * Sets up a mocked reflection resolver for the test. */ protected function setupReflectionResolver() { - $this->typedDataManager = $this + $manager = $this ->getMockBuilder('Drupal\Core\TypedData\TypedDataManager') ->disableOriginalConstructor() ->setMethods(NULL) @@ -101,17 +95,9 @@ protected function setupReflectionResolver() { $property = new \ReflectionProperty('Drupal\Core\TypedData\TypedDataManager', 'definitions'); $property->setAccessible(TRUE); - $property->setValue($this->typedDataManager, $definitions); + $property->setValue($manager, $definitions); - $this->reflectionResolver = $this - ->getMockBuilder('Drupal\Core\TypedData\Resolver\ReflectionResolver') - ->disableOriginalConstructor() - ->setMethods(NULL) - ->getMock(); - - $property = new \ReflectionProperty('Drupal\Core\TypedData\Resolver\ReflectionResolver', 'typedDataManager'); - $property->setAccessible(TRUE); - $property->setValue($this->reflectionResolver, $this->typedDataManager); + $this->reflectionResolver = new ReflectionResolver($manager); } }