Skip to content

[FrameworkBundle] Allow using the kernel as a registry of controllers and service factories #34881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
;
Expand All @@ -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);
Expand All @@ -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');
});
}

Expand All @@ -139,13 +153,22 @@ 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();

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))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Symfony\Bundle\FrameworkBundle\FrameworkBundle;

return [
FrameworkBundle::class => ['all' => true],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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%');
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ abstract class AbstractConfigurator
{
const FACTORY = 'unknown';

/**
* @var callable(mixed $value, bool $allowService)|null
*/
public static $valuePreProcessor;

/** @internal */
protected $definition;

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
}

Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
Expand All @@ -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([]);
Expand All @@ -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()
Expand Down