Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,29 @@ public static function processValue(mixed $value, bool $allowServices = false):

throw new InvalidArgumentException(\sprintf('Cannot use values of type "%s" in service configuration files.', get_debug_type($value)));
}

/**
* Converts a named closure to dumpable callable.
*
* @throws InvalidArgumentException if the closure is anonymous or references a non-static method
*/
final public static function processClosure(\Closure $closure): callable
{
$function = new \ReflectionFunction($closure);
if ($function->isAnonymous()) {
throw new InvalidArgumentException('Anonymous closure not supported. The closure must be created from a static method or a global function.');
}

// Convert global_function(...) closure into 'global_function'
if (!$class = $function->getClosureCalledClass()) {
return $function->name;
}

// Convert Class::method(...) closure into ['Class', 'method']
if ($function->isStatic()) {
return [$class->name, $function->name];
}

throw new InvalidArgumentException(\sprintf('The method "%s::%s(...)" is not static.', $class->name, $function->name));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,12 @@ function service_closure(string $serviceId): ClosureReferenceConfigurator
/**
* Creates a closure.
*/
function closure(string|array|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
function closure(string|array|\Closure|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
{
if ($callable instanceof \Closure) {
$callable = AbstractConfigurator::processClosure($callable);
}

return (new InlineServiceConfigurator(new Definition('Closure')))
->factory(['Closure', 'fromCallable'])
->args([$callable]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ trait ConfiguratorTrait
*
* @return $this
*/
final public function configurator(string|array|ReferenceConfigurator $configurator): static
final public function configurator(string|array|\Closure|ReferenceConfigurator $configurator): static
{
if ($configurator instanceof \Closure) {
$this->definition->setConfigurator(static::processClosure($configurator));

return $this;
}

$this->definition->setConfigurator(static::processValue($configurator, true));

return $this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ trait FactoryTrait
*
* @return $this
*/
final public function factory(string|array|ReferenceConfigurator|Expression $factory): static
final public function factory(string|array|\Closure|ReferenceConfigurator|Expression $factory): static
{
if ($factory instanceof \Closure) {
$this->definition->setFactory(static::processClosure($factory));

return $this;
}

if (\is_string($factory) && 1 === substr_count($factory, ':')) {
$factoryParts = explode(':', $factory);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

trait FromCallableTrait
{
final public function fromCallable(string|array|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
final public function fromCallable(string|array|\Closure|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
{
if ($this->definition instanceof ChildDefinition) {
throw new InvalidArgumentException('The configuration key "parent" is unsupported when using "fromCallable()".');
Expand All @@ -41,6 +41,10 @@ final public function fromCallable(string|array|ReferenceConfigurator|Expression

$this->definition->setFactory(['Closure', 'fromCallable']);

if ($callable instanceof \Closure) {
$callable = static::processClosure($callable);
}

if (\is_string($callable) && 1 === substr_count($callable, ':')) {
$parts = explode(':', $callable);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Testing\NamedClosure;

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

use function Symfony\Component\DependencyInjection\Loader\Configurator\closure;

interface NamedClosureInterface
{
public function theMethod();
}

class NamedClosureClass
{
public function __construct(...$args)
{
}

public static function getInstance(): self
{
return new self();
}

public static function configure(self $instance): void
{
}
}

return function (ContainerConfigurator $c) {
$c->services()
->set('from_callable', NamedClosureInterface::class)
->fromCallable(NamedClosureClass::getInstance(...))
->public()
->set('has_factory', NamedClosureClass::class)
->factory(NamedClosureClass::getInstance(...))
->public()
->set('has_configurator', NamedClosureClass::class)
->configurator(NamedClosureClass::configure(...))
->public()
->set('with_closure', NamedClosureClass::class)
->args([closure(dirname(...))])
->public()
;
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
->tag('foo', ['foo' => 'foo'])
->tag('foo', ['bar' => 'bar', 'baz' => 'baz'])
->tag('foo', ['name' => 'bar', 'baz' => 'baz'])
->factory([FooClass::class, 'getInstance'])
->factory(FooClass::getInstance(...))
->property('foo', 'bar')
->property('moo', service('foo.baz'))
->property('qux', ['%foo%' => 'foo is %foo%', 'foobar' => '%foo%'])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

/**
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
*/
class ProjectServiceContainer extends Container
{
protected $parameters = [];

public function __construct()
{
$this->services = $this->privates = [];
$this->methodMap = [
'from_callable' => 'getFromCallableService',
'has_configurator' => 'getHasConfiguratorService',
'has_factory' => 'getHasFactoryService',
'with_closure' => 'getWithClosureService',
];

$this->aliases = [];
}

public function compile(): void
{
throw new LogicException('You cannot compile a dumped container that was already compiled.');
}

public function isCompiled(): bool
{
return true;
}

protected function createProxy($class, \Closure $factory)
{
return $factory();
}

/**
* Gets the public 'from_callable' shared service.
*
* @return \Testing\NamedClosure\NamedClosureInterface
*/
protected static function getFromCallableService($container, $lazyLoad = true)
{
return $container->services['from_callable'] = new class(fn () => 'Testing\\NamedClosure\\NamedClosureClass') extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Testing\NamedClosure\NamedClosureInterface { public function theMethod() { return $this->service->getInstance(...\func_get_args()); } };
}

/**
* Gets the public 'has_configurator' shared service.
*
* @return \Testing\NamedClosure\NamedClosureClass
*/
protected static function getHasConfiguratorService($container)
{
$container->services['has_configurator'] = $instance = new \Testing\NamedClosure\NamedClosureClass();

\Testing\NamedClosure\NamedClosureClass::configure($instance);

return $instance;
}

/**
* Gets the public 'has_factory' shared service.
*
* @return \Testing\NamedClosure\NamedClosureClass
*/
protected static function getHasFactoryService($container)
{
return $container->services['has_factory'] = \Testing\NamedClosure\NamedClosureClass::getInstance();
}

/**
* Gets the public 'with_closure' shared service.
*
* @return \Testing\NamedClosure\NamedClosureClass
*/
protected static function getWithClosureService($container)
{
return $container->services['with_closure'] = new \Testing\NamedClosure\NamedClosureClass(\dirname(...));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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 Loader\Configurator;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator;

class AbstractConfiguratorTest extends TestCase
{
public function testProcessClosure()
{
$this->assertSame(
[\DateTime::class, 'createFromFormat'],
AbstractConfigurator::processClosure(\DateTime::createFromFormat(...)),
);

$this->assertSame(
'date_create',
AbstractConfigurator::processClosure(date_create(...)),
);
}

public function testProcessNonStaticNamedClosure()
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('The method "DateTime::format(...)" is not static');

AbstractConfigurator::processClosure((new \DateTime())->format(...));
}

public function testProcessAnonymousClosure()
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('Anonymous closure not supported. The closure must be created from a static method or a global function.');

AbstractConfigurator::processClosure(static fn () => new \DateTime());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,14 @@ public function testConfigBuilderEnvConfigurator()

$this->assertIsString($container->getExtensionConfig('acme')[0]['color']);
}

public function testNamedClosure()
{
$container = new ContainerBuilder();
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures/config'), 'some-env');
$loader->load('named_closure.php');
$container->compile();
$dumper = new PhpDumper($container);
$this->assertStringEqualsFile(\dirname(__DIR__).'/Fixtures/php/named_closure_compiled.php', $dumper->dump());
}
}
Loading