diff --git a/core/lib/Drupal/Core/Utility/CallbackResolver.php b/core/lib/Drupal/Core/Utility/CallbackResolver.php index 5cda0a1873..9207103c65 100644 --- a/core/lib/Drupal/Core/Utility/CallbackResolver.php +++ b/core/lib/Drupal/Core/Utility/CallbackResolver.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Utility; +use Drupal\Core\DependencyInjection\ClassResolverInterface; use Psr\Container\ContainerInterface; /** @@ -21,21 +22,49 @@ class CallbackResolver implements CallbackResolverInterface { */ protected $container; + /** + * The class resolver. + * + * @var \Drupal\Core\DependencyInjection\ClassResolverInterface + */ + protected $classResolver; + /** * Create an instance of the callback resolver. */ - public function __construct(ContainerInterface $container) { + public function __construct(ContainerInterface $container, ClassResolverInterface $classResolver) { $this->container = $container; + $this->classResolver = $classResolver; } /** * {@inheritdoc} */ public function getCallableFromDefinition($definition) { + // Check if the definition is in the static method format, allow the + // class resolver the opportunity to create a new instance of the class. + if (is_string($definition) && substr_count($definition, '::') === 1) { + list($class, $method) = explode('::', $definition); + $resolved_class_callable = [$this->classResolver->getInstanceFromDefinition($class), $method]; + if (!is_callable($resolved_class_callable)) { + throw new \InvalidArgumentException(sprintf('The method "%s" was not found on the class "%s".', $method, $class)); + } + return $resolved_class_callable; + } + + // If the definition is natively a callable, we can return it immediately. if (is_callable($definition)) { return $definition; } - if (is_string($definition) && substr_count($definition, ':') == 1) { + + // Support using classes as callables if the __invoke method exists on the + // class. + if (method_exists($definition, '__invoke')) { + return new $definition(); + } + + // Support the service notation syntax. + if (is_string($definition) && substr_count($definition, ':') === 1) { list($service, $method) = explode(':', $definition); if (!$this->container->has($service)) { throw new \InvalidArgumentException(sprintf('The callback specified was invalid. No service found with name "%s".', $service)); @@ -45,6 +74,7 @@ public function getCallableFromDefinition($definition) { } return [$this->container->get($service), $method]; } + throw new \InvalidArgumentException('The callback definition provided was not a valid callable to service method.'); } diff --git a/core/tests/Drupal/Tests/Core/Utility/CallbackResolverTest.php b/core/tests/Drupal/Tests/Core/Utility/CallbackResolverTest.php index 1519eb43d8..3f81d06095 100644 --- a/core/tests/Drupal/Tests/Core/Utility/CallbackResolverTest.php +++ b/core/tests/Drupal/Tests/Core/Utility/CallbackResolverTest.php @@ -2,9 +2,14 @@ namespace Drupal\Tests\Core\Utility; +use Drupal\Core\DependencyInjection\ClassResolver; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Utility\CallbackResolver; use Drupal\Tests\UnitTestCase; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * @coversDefaultClass \Drupal\Core\Utility\CallbackResolver @@ -27,7 +32,11 @@ protected function setUp() { $container = new ContainerBuilder(); $container->set('test_service', $this); - $this->resolver = new CallbackResolver($container); + + $class_resolver = new ClassResolver(); + $class_resolver->setContainer($container); + + $this->resolver = new CallbackResolver($container, $class_resolver); } /** @@ -62,12 +71,24 @@ function ($suffix) { 'Non-static function, array notation, with object' => [ [$this, 'method'], ], + 'Non-static function, instantiated by class resolver' => [ + static::class . '::method', + ], + 'Non-static function, instantiated by class resolver, container injection' => [ + '\Drupal\Tests\Core\Utility\MockContainerInjection::getResult', + ], + 'Non-static function, instantiated by class resolver, container aware' => [ + '\Drupal\Tests\Core\Utility\MockContainerAware::getResult', + ], 'Service notation' => [ 'test_service:method', ], 'Service notation, static method' => [ 'test_service:staticMethod', ], + 'Class with invoke method' => [ + static::class, + ], ]; } @@ -86,16 +107,31 @@ public function testCallbackResolverExceptionHandling($definition, $exception_cl */ public function callbackResolverExceptionHandlingTestCases() { return [ - 'Definition is not callable' => [ + 'String function' => [ 'not_a_callable', \InvalidArgumentException::class, 'The callback definition provided was not a valid callable to service method.', ], - 'Definition is not callable, array' => [ + 'Array notation' => [ ['not_a_callable', 'not_a_callable'], \InvalidArgumentException::class, 'The callback definition provided was not a valid callable to service method.', ], + 'Missing method on class, array notation' => [ + [static::class, 'method_not_exists'], + \InvalidArgumentException::class, + 'The callback definition provided was not a valid callable to service method.', + ], + 'Missing method on class, static notation' => [ + static::class . '::method_not_exists', + \InvalidArgumentException::class, + 'The method "method_not_exists" was not found on the class "Drupal\Tests\Core\Utility\CallbackResolverTest".', + ], + 'Missing class, static notation' => [ + '\NotARealClass::method', + \InvalidArgumentException::class, + 'Class "\NotARealClass" does not exist.', + ], 'Service not in container' => [ 'bad_service:method', \InvalidArgumentException::class, @@ -135,4 +171,48 @@ public function method($suffix) { return 'foo' . $suffix; } + /** + * A test __invoke method. + * + * @param string $suffix + * A suffix to append. + * + * @return string + * A test string. + */ + public function __invoke($suffix) { + return 'foo' . $suffix; + } + +} + +class MockContainerInjection implements ContainerInjectionInterface { + + protected $injected; + + public function __construct($result) { + $this->injected = $result; + } + + public static function create(ContainerInterface $container) { + return new static('foo'); + } + + public function getResult($suffix) { + return $this->injected . $suffix; + } + +} + +class MockContainerAware implements ContainerAwareInterface { + + use ContainerAwareTrait; + + public function getResult($suffix) { + if (empty($this->container)) { + throw new \Exception('Container was not injected.'); + } + return 'foo' . $suffix; + } + }