From 9c9b99cc6562450410ee29022bec9b7b0afd9bf1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 8 Dec 2019 14:14:17 +0100 Subject: [PATCH] [FrameworkBundle] Allow using the kernel as a registry of controllers and service factories --- .../Kernel/MicroKernelTrait.php | 35 ++++++-- .../Tests/Kernel/MicroKernelTraitTest.php | 13 +++ .../Kernel/flex-style/config/bundles.php | 7 ++ .../flex-style/src/FlexStyleMicroKernel.php | 85 +++++++++++++++++++ .../Bundle/FrameworkBundle/composer.json | 2 +- .../Configurator/AbstractConfigurator.php | 11 ++- .../Configurator/ContainerConfigurator.php | 6 +- .../Configurator/ServicesConfigurator.php | 9 +- ...RegisterControllerArgumentLocatorsPass.php | 11 ++- ...sterControllerArgumentLocatorsPassTest.php | 16 +++- 10 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php index f4ac7e4b16174..4125a9a0d9766 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -13,9 +13,13 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader as ContainerPhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Routing\Loader\PhpFileLoader as RoutingPhpFileLoader; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RouteCollectionBuilder; @@ -93,6 +97,8 @@ public function registerContainerConfiguration(LoaderInterface $loader) if (!$container->hasDefinition('kernel')) { $container->register('kernel', static::class) + ->addTag('controller.service_arguments') + ->setAutoconfigured(true) ->setSynthetic(true) ->setPublic(true) ; @@ -101,12 +107,9 @@ public function registerContainerConfiguration(LoaderInterface $loader) $kernelDefinition = $container->getDefinition('kernel'); $kernelDefinition->addTag('routing.route_loader'); - if ($this instanceof EventSubscriberInterface) { - $kernelDefinition->addTag('kernel.event_subscriber'); - } - $container->addObjectResource($this); $container->fileExists($this->getProjectDir().'/config/bundles.php'); + $container->setParameter('kernel.secret', '%env(APP_SECRET)%'); try { $this->configureContainer($container, $loader); @@ -120,16 +123,27 @@ public function registerContainerConfiguration(LoaderInterface $loader) } } + // the user has opted into using the ContainerConfigurator + $defaultDefinition = (new Definition())->setAutowired(true)->setAutoconfigured(true); + /* @var ContainerPhpFileLoader $kernelLoader */ $kernelLoader = $loader->getResolver()->resolve($file); $kernelLoader->setCurrentDir(\dirname($file)); $instanceof = &\Closure::bind(function &() { return $this->instanceof; }, $kernelLoader, $kernelLoader)(); + $valuePreProcessor = AbstractConfigurator::$valuePreProcessor; + AbstractConfigurator::$valuePreProcessor = function ($value) { + return $this === $value ? new Reference('kernel') : $value; + }; + try { - $this->configureContainer(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file), $loader); + $this->configureContainer(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file, $defaultDefinition), $loader); } finally { $instanceof = []; $kernelLoader->registerAliasesForSinglyImplementedInterfaces(); + AbstractConfigurator::$valuePreProcessor = $valuePreProcessor; } + + $container->setAlias(static::class, 'kernel'); }); } @@ -139,6 +153,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) public function loadRoutes(LoaderInterface $loader) { $file = (new \ReflectionObject($this))->getFileName(); + /* @var RoutingPhpFileLoader $kernelLoader */ $kernelLoader = $loader->getResolver()->resolve($file); $kernelLoader->setCurrentDir(\dirname($file)); $collection = new RouteCollection(); @@ -146,6 +161,14 @@ public function loadRoutes(LoaderInterface $loader) try { $this->configureRoutes(new RoutingConfigurator($collection, $kernelLoader, $file, $file)); + foreach ($collection as $route) { + $controller = $route->getDefault('_controller'); + + if (\is_array($controller) && [0, 1] === array_keys($controller) && $this === $controller[0]) { + $route->setDefault('_controller', ['kernel', $controller[1]]); + } + } + return $collection; } catch (\TypeError $e) { if (0 !== strpos($e->getMessage(), sprintf('Argument 1 passed to %s::configureRoutes() must be an instance of %s,', static::class, RouteCollectionBuilder::class))) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php index dd909ea6fc8ce..3f61496bc2574 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php @@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\HttpFoundation\Request; +require_once __DIR__.'/flex-style/src/FlexStyleMicroKernel.php'; + class MicroKernelTraitTest extends TestCase { public function test() @@ -56,4 +58,15 @@ public function testRoutingRouteLoaderTagIsAdded() $kernel->registerContainerConfiguration(new ClosureLoader($container)); $this->assertTrue($container->getDefinition('kernel')->hasTag('routing.route_loader')); } + + public function testFlexStyle() + { + $kernel = new FlexStyleMicroKernel('test', false); + $kernel->boot(); + + $request = Request::create('/'); + $response = $kernel->handle($request); + + $this->assertEquals('Have a great day!', $response->getContent()); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php new file mode 100644 index 0000000000000..0691b2b32d19c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php @@ -0,0 +1,7 @@ + ['all' => true], +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php new file mode 100644 index 0000000000000..016c66f612c2b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel; + +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + +class FlexStyleMicroKernel extends Kernel +{ + use MicroKernelTrait; + + private $cacheDir; + + public function halloweenAction(\stdClass $o) + { + return new Response($o->halloween); + } + + public function createHalloween(LoggerInterface $logger, string $halloween) + { + $o = new \stdClass(); + $o->logger = $logger; + $o->halloween = $halloween; + + return $o; + } + + public function getCacheDir(): string + { + return $this->cacheDir = sys_get_temp_dir().'/sf_flex_kernel'; + } + + public function getLogDir(): string + { + return $this->cacheDir; + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $fs = new Filesystem(); + $fs->remove($this->cacheDir); + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->add('halloween', '/')->controller([$this, 'halloweenAction']); + } + + protected function configureContainer(ContainerConfigurator $c) + { + $c->parameters() + ->set('halloween', 'Have a great day!'); + + $c->services() + ->set('logger', NullLogger::class) + ->set('stdClass', 'stdClass') + ->factory([$this, 'createHalloween']) + ->arg('$halloween', '%halloween%'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 46183969a8457..896d149b10097 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -20,7 +20,7 @@ "ext-xml": "*", "symfony/cache": "^4.4|^5.0", "symfony/config": "^5.0", - "symfony/dependency-injection": "^5.0.1", + "symfony/dependency-injection": "^5.1", "symfony/error-handler": "^4.4.1|^5.0.1", "symfony/http-foundation": "^4.4|^5.0", "symfony/http-kernel": "^5.0", diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php index 539eb3914d1e1..db0b488426b51 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php @@ -22,6 +22,11 @@ abstract class AbstractConfigurator { const FACTORY = 'unknown'; + /** + * @var callable(mixed $value, bool $allowService)|null + */ + public static $valuePreProcessor; + /** @internal */ protected $definition; @@ -49,7 +54,11 @@ public static function processValue($value, $allowServices = false) $value[$k] = static::processValue($v, $allowServices); } - return $value; + return self::$valuePreProcessor ? (self::$valuePreProcessor)($value, $allowServices) : $value; + } + + if (self::$valuePreProcessor) { + $value = (self::$valuePreProcessor)($value, $allowServices); } if ($value instanceof ReferenceConfigurator) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php index 28c9d7958591d..61fd4ee38a329 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -34,14 +34,16 @@ class ContainerConfigurator extends AbstractConfigurator private $path; private $file; private $anonymousCount = 0; + private $defaultDefinition; - public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path, string $file) + public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path, string $file, Definition $defaultDefinition = null) { $this->container = $container; $this->loader = $loader; $this->instanceof = &$instanceof; $this->path = $path; $this->file = $file; + $this->defaultDefinition = $defaultDefinition; } final public function extension(string $namespace, array $config) @@ -67,7 +69,7 @@ final public function parameters(): ParametersConfigurator final public function services(): ServicesConfigurator { - return new ServicesConfigurator($this->container, $this->loader, $this->instanceof, $this->path, $this->anonymousCount); + return new ServicesConfigurator($this->container, $this->loader, $this->instanceof, $this->path, $this->anonymousCount, $this->defaultDefinition); } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php index f0fdde81c33a4..358303f660b80 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php @@ -32,16 +32,19 @@ class ServicesConfigurator extends AbstractConfigurator private $path; private $anonymousHash; private $anonymousCount; + private $defaultDefinition; - public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path = null, int &$anonymousCount = 0) + public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path = null, int &$anonymousCount = 0, Definition $defaultDefinition = null) { - $this->defaults = new Definition(); + $defaultDefinition = $defaultDefinition ?? new Definition(); + $this->defaults = clone $defaultDefinition; $this->container = $container; $this->loader = $loader; $this->instanceof = &$instanceof; $this->path = $path; $this->anonymousHash = ContainerBuilder::hash($path ?: mt_rand()); $this->anonymousCount = &$anonymousCount; + $this->defaultDefinition = $defaultDefinition; $instanceof = []; } @@ -50,7 +53,7 @@ public function __construct(ContainerBuilder $container, PhpFileLoader $loader, */ final public function defaults(): DefaultsConfigurator { - return new DefaultsConfigurator($this, $this->defaults = new Definition(), $this->path); + return new DefaultsConfigurator($this, $this->defaults = clone $this->defaultDefinition, $this->path); } /** diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index a3f5012e3268f..475b5d756a3d3 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -170,11 +170,14 @@ public function process(ContainerBuilder $container) $message .= ' Did you forget to add a use statement?'; } - throw new InvalidArgumentException($message); - } + $container->register($erroredId = '.errored.'.$container->hash($message), $type) + ->addError($message); - $target = ltrim($target, '\\'); - $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $p->name) : new Reference($target, $invalidBehavior); + $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); + } else { + $target = ltrim($target, '\\'); + $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $p->name) : new Reference($target, $invalidBehavior); + } } // register the maps as a per-method service-locators if ($args) { diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index a3b7969be172c..0746643036517 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -197,7 +197,7 @@ public function testSkipSetContainer() public function testExceptionOnNonExistentTypeHint() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException('RuntimeException'); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClass". Did you forget to add a use statement?'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -207,11 +207,17 @@ public function testExceptionOnNonExistentTypeHint() $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); + + $error = $container->getDefinition('argument_resolver.service')->getArgument(0); + $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0]; + $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0]; + + $container->get($error); } public function testExceptionOnNonExistentTypeHintDifferentNamespace() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException('RuntimeException'); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassDifferentNamespaceController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Acme\NonExistentClass".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -221,6 +227,12 @@ public function testExceptionOnNonExistentTypeHintDifferentNamespace() $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); + + $error = $container->getDefinition('argument_resolver.service')->getArgument(0); + $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0]; + $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0]; + + $container->get($error); } public function testNoExceptionOnNonExistentTypeHintOptionalArg()