Skip to content

Commit

Permalink
feature #25707 [DI] ServiceProviderInterface, implementation for Serv…
Browse files Browse the repository at this point in the history
…iceLocator (kejwmen)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[DI] ServiceProviderInterface, implementation for ServiceLocator

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #25686
| License       | MIT
| Doc PR        | applicable here?

Implements #25686

Not sure if it needs any additional documentation, @nicolas-grekas?

Commits
-------

aaf5422 [DI][Contracts] add and implement ServiceProviderInterface
  • Loading branch information
nicolas-grekas committed Mar 28, 2019
2 parents 708639f + aaf5422 commit 8551ec7
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 9 deletions.
Expand Up @@ -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);
}

Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -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
Expand Down
Expand Up @@ -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.
Expand All @@ -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);
}

Expand Down
Expand Up @@ -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) {
Expand Down
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/DependencyInjection/ServiceLocator.php
Expand Up @@ -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 <robin.chalas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class ServiceLocator implements PsrContainerInterface
class ServiceLocator implements ServiceProviderInterface
{
use ServiceLocatorTrait {
get as private doGet;
Expand Down
Expand Up @@ -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' => '?',
]);
}

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

Expand Down
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/DependencyInjection/composer.json
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Contracts/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* added `HttpClient` namespace with contracts for implementing flexible HTTP clients
* added `EventDispatcher\EventDispatcherInterface`
* added `ServiceProviderInterface`

1.0.0
-----
Expand Down
25 changes: 24 additions & 1 deletion src/Symfony/Contracts/Service/ServiceLocatorTrait.php
Expand Up @@ -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 <robin.chalas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
Expand All @@ -24,6 +24,7 @@ trait ServiceLocatorTrait
{
private $factories;
private $loading = [];
private $providedTypes;

/**
* @param callable[] $factories
Expand Down Expand Up @@ -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)) {
Expand Down
36 changes: 36 additions & 0 deletions src/Symfony/Contracts/Service/ServiceProviderInterface.php
@@ -0,0 +1,36 @@
<?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\Contracts\Service;

use Psr\Container\ContainerInterface;

/**
* A ServiceProviderInterface exposes the identifiers and the types of services provided by a container.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Mateusz Sip <mateusz.sip@gmail.com>
*/
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;
}

0 comments on commit 8551ec7

Please sign in to comment.