From aaf5422cfbcd889226ba7f615bb5a975281335ea Mon Sep 17 00:00:00 2001 From: Mateusz Sip Date: Sun, 7 Jan 2018 04:01:46 +0100 Subject: [PATCH] [DI][Contracts] add and implement ServiceProviderInterface --- .../Argument/ServiceLocator.php | 12 ++++++- .../DependencyInjection/CHANGELOG.md | 1 + .../ResolveServiceSubscribersPass.php | 3 +- .../DependencyInjection/ContainerBuilder.php | 5 +-- .../DependencyInjection/Dumper/PhpDumper.php | 4 ++- .../DependencyInjection/ServiceLocator.php | 4 +-- .../Tests/Fixtures/php/services_rot13_env.php | 2 ++ .../php/services_service_locator_argument.php | 6 ++++ .../Fixtures/php/services_subscriber.php | 5 +++ .../Tests/ServiceLocatorTest.php | 15 ++++++++ .../DependencyInjection/composer.json | 2 +- src/Symfony/Contracts/CHANGELOG.md | 1 + .../Contracts/Service/ServiceLocatorTrait.php | 25 ++++++++++++- .../Service/ServiceProviderInterface.php | 36 +++++++++++++++++++ 14 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Contracts/Service/ServiceProviderInterface.php diff --git a/src/Symfony/Component/DependencyInjection/Argument/ServiceLocator.php b/src/Symfony/Component/DependencyInjection/Argument/ServiceLocator.php index fcb2b6a6b906..2001a9561705 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/ServiceLocator.php +++ b/src/Symfony/Component/DependencyInjection/Argument/ServiceLocator.php @@ -22,11 +22,13 @@ class ServiceLocator extends BaseServiceLocator { private $factory; private $serviceMap; + private $serviceTypes; - public function __construct(\Closure $factory, array $serviceMap) + public function __construct(\Closure $factory, array $serviceMap, array $serviceTypes = null) { $this->factory = $factory; $this->serviceMap = $serviceMap; + $this->serviceTypes = $serviceTypes; parent::__construct($serviceMap); } @@ -37,4 +39,12 @@ public function get($id) { return isset($this->serviceMap[$id]) ? ($this->factory)(...$this->serviceMap[$id]) : parent::get($id); } + + /** + * {@inheritdoc} + */ + public function getProvidedServices(): array + { + return $this->serviceTypes ?? $this->serviceTypes = array_map(function () { return '?'; }, $this->serviceMap); + } } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index edc6ddf11577..3d71d72844d2 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG * added `ReverseContainer`: a container that turns services back to their ids * added ability to define an index for a tagged collection * added ability to define an index for services in an injected service locator argument + * made `ServiceLocator` implement `ServiceProviderInterface` * deprecated support for non-string default env() parameters 4.2.0 diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php index cc87f3a8b753..399f349046a3 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php @@ -14,6 +14,7 @@ use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Contracts\Service\ServiceProviderInterface; /** * Compiler pass to inject their service locator to service subscribers. @@ -26,7 +27,7 @@ class ResolveServiceSubscribersPass extends AbstractRecursivePass protected function processValue($value, $isRoot = false) { - if ($value instanceof Reference && $this->serviceLocator && ContainerInterface::class === (string) $value) { + if ($value instanceof Reference && $this->serviceLocator && \in_array((string) $value, [ContainerInterface::class, ServiceProviderInterface::class], true)) { return new Reference($this->serviceLocator); } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 943df8d314f1..94a95661a41b 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1234,13 +1234,14 @@ private function doResolveServices($value, array &$inlineServices = [], $isConst return $count; }); } elseif ($value instanceof ServiceLocatorArgument) { - $refs = []; + $refs = $types = []; foreach ($value->getValues() as $k => $v) { if ($v) { $refs[$k] = [$v]; + $types[$k] = $v instanceof TypedReference ? $v->getType() : '?'; } } - $value = new ServiceLocator(\Closure::fromCallable([$this, 'resolveServices']), $refs); + $value = new ServiceLocator(\Closure::fromCallable([$this, 'resolveServices']), $refs, $types); } elseif ($value instanceof Reference) { $value = $this->doGet((string) $value, $value->getInvalidBehavior(), $inlineServices, $isConstructorArgument); } elseif ($value instanceof Definition) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 4cf663b3dd23..cfac05ddd0e9 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1546,6 +1546,7 @@ private function dumpValue($value, bool $interpolate = true): string if ($value instanceof ServiceLocatorArgument) { $serviceMap = ''; + $serviceTypes = ''; foreach ($value->getValues() as $k => $v) { if (!$v) { continue; @@ -1559,11 +1560,12 @@ private function dumpValue($value, bool $interpolate = true): string $this->export(ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $v->getInvalidBehavior() && !\is_string($load) ? $this->generateMethodName($id).($load ? '.php' : '') : null), $this->export($load) ); + $serviceTypes .= sprintf("\n %s => %s,", $this->export($k), $this->export($v instanceof TypedReference ? $v->getType() : '?')); $this->locatedIds[$id] = true; } $this->addGetService = true; - return sprintf('new \%s($this->getService, [%s%s])', ServiceLocator::class, $serviceMap, $serviceMap ? "\n " : ''); + return sprintf('new \%s($this->getService, [%s%s], [%s%s])', ServiceLocator::class, $serviceMap, $serviceMap ? "\n " : '', $serviceTypes, $serviceTypes ? "\n " : ''); } } finally { list($this->definitionVariables, $this->referenceVariables) = $scope; diff --git a/src/Symfony/Component/DependencyInjection/ServiceLocator.php b/src/Symfony/Component/DependencyInjection/ServiceLocator.php index 45a352413563..6741281d9032 100644 --- a/src/Symfony/Component/DependencyInjection/ServiceLocator.php +++ b/src/Symfony/Component/DependencyInjection/ServiceLocator.php @@ -12,19 +12,19 @@ namespace Symfony\Component\DependencyInjection; use Psr\Container\ContainerExceptionInterface; -use Psr\Container\ContainerInterface as PsrContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Contracts\Service\ServiceLocatorTrait; +use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; /** * @author Robin Chalas * @author Nicolas Grekas */ -class ServiceLocator implements PsrContainerInterface +class ServiceLocator implements ServiceProviderInterface { use ServiceLocatorTrait { get as private doGet; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php index 64cc5df2580c..bf3caef842af 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php @@ -72,6 +72,8 @@ protected function getContainer_EnvVarProcessorsLocatorService() { return $this->services['container.env_var_processors_locator'] = new \Symfony\Component\DependencyInjection\Argument\ServiceLocator($this->getService, [ 'rot13' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Dumper\\Rot13EnvVarProcessor', 'getRot13EnvVarProcessorService', false], + ], [ + 'rot13' => '?', ]); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php index c30a6c9e4479..94b5931ed3ac 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php @@ -72,6 +72,12 @@ protected function getBarService() 'foo3' => [false, 'foo3', 'getFoo3Service', false], 'foo4' => ['privates', 'foo4', NULL, 'BOOM'], 'foo5' => ['services', 'foo5', NULL, false], + ], [ + 'foo1' => '?', + 'foo2' => '?', + 'foo3' => '?', + 'foo4' => '?', + 'foo5' => '?', ]); return $instance; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index b60b3b87b3de..ac00d4bf361d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -75,6 +75,11 @@ protected function getFooServiceService() 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false], 'bar' => ['services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false], 'baz' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false], + ], [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', + 'bar' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', + 'baz' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', ]))->withContext('foo_service', $this)); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php b/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php index 9c0962880fc9..eb43029696f8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php @@ -86,6 +86,21 @@ public function testInvoke() $this->assertSame('baz', $locator('bar')); $this->assertNull($locator('dummy'), '->__invoke() should return null on invalid service'); } + + public function testProvidesServicesInformation() + { + $locator = new ServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function (): string { return 'baz'; }, + 'baz' => function (): ?string { return 'zaz'; }, + ]); + + $this->assertSame($locator->getProvidedServices(), [ + 'foo' => '?', + 'bar' => 'string', + 'baz' => '?string', + ]); + } } class SomeServiceSubscriber implements ServiceSubscriberInterface diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 4acf55465075..94a729df8dd2 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1.3", "psr/container": "^1.0", - "symfony/contracts": "^1.0" + "symfony/contracts": "^1.1" }, "require-dev": { "symfony/yaml": "~3.4|~4.0", diff --git a/src/Symfony/Contracts/CHANGELOG.md b/src/Symfony/Contracts/CHANGELOG.md index b5f1d76aecda..1106a6f5a038 100644 --- a/src/Symfony/Contracts/CHANGELOG.md +++ b/src/Symfony/Contracts/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * added `HttpClient` namespace with contracts for implementing flexible HTTP clients * added `EventDispatcher\EventDispatcherInterface` + * added `ServiceProviderInterface` 1.0.0 ----- diff --git a/src/Symfony/Contracts/Service/ServiceLocatorTrait.php b/src/Symfony/Contracts/Service/ServiceLocatorTrait.php index eed44f5d33e7..71b1b7460dff 100644 --- a/src/Symfony/Contracts/Service/ServiceLocatorTrait.php +++ b/src/Symfony/Contracts/Service/ServiceLocatorTrait.php @@ -15,7 +15,7 @@ use Psr\Container\NotFoundExceptionInterface; /** - * A trait to help implement PSR-11 service locators. + * A trait to help implement ServiceProviderInterface. * * @author Robin Chalas * @author Nicolas Grekas @@ -24,6 +24,7 @@ trait ServiceLocatorTrait { private $factories; private $loading = []; + private $providedTypes; /** * @param callable[] $factories @@ -66,6 +67,28 @@ public function get($id) } } + /** + * {@inheritdoc} + */ + public function getProvidedServices(): array + { + if (null === $this->providedTypes) { + $this->providedTypes = []; + + foreach ($this->factories as $name => $factory) { + if (!\is_callable($factory)) { + $this->providedTypes[$name] = '?'; + } else { + $type = (new \ReflectionFunction($factory))->getReturnType(); + + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').$type->getName() : '?'; + } + } + } + + return $this->providedTypes; + } + private function createNotFoundException(string $id): NotFoundExceptionInterface { if (!$alternatives = array_keys($this->factories)) { diff --git a/src/Symfony/Contracts/Service/ServiceProviderInterface.php b/src/Symfony/Contracts/Service/ServiceProviderInterface.php new file mode 100644 index 000000000000..c60ad0bd4bf2 --- /dev/null +++ b/src/Symfony/Contracts/Service/ServiceProviderInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. + * + * @author Nicolas Grekas + * @author Mateusz Sip + */ +interface ServiceProviderInterface extends ContainerInterface +{ + /** + * Returns an associative array of service types keyed by the identifiers provided by the current container. + * + * Examples: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface + * * ['foo' => '?'] means the container provides service name "foo" of unspecified type + * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null + * + * @return string[] The provided service types, keyed by service names + */ + public function getProvidedServices(): array; +}