[Contracts] Add traits+interfaces from the DI component

This commit is contained in:
Nicolas Grekas 2018-08-16 10:51:22 +02:00
parent a0e21f8d19
commit 675abdcfee
11 changed files with 272 additions and 88 deletions

View File

@ -22,7 +22,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition1;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition2;
@ -31,6 +30,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberChild;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberParent;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
require_once __DIR__.'/../Fixtures/includes/classes.php';

View File

@ -2,7 +2,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
class TestServiceSubscriberChild extends TestServiceSubscriberParent
{

View File

@ -3,7 +3,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
class TestServiceSubscriberParent implements ServiceSubscriberInterface
{

View File

@ -11,52 +11,16 @@
namespace Symfony\Component\DependencyInjection\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Contracts\Tests\Service\ServiceLocatorTest as BaseServiceLocatorTest;
class ServiceLocatorTest extends TestCase
class ServiceLocatorTest extends BaseServiceLocatorTest
{
public function testHas()
public function getServiceLocator(array $factories)
{
$locator = new ServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
function () { return 'dummy'; },
));
$this->assertTrue($locator->has('foo'));
$this->assertTrue($locator->has('bar'));
$this->assertFalse($locator->has('dummy'));
}
public function testGet()
{
$locator = new ServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
));
$this->assertSame('bar', $locator->get('foo'));
$this->assertSame('baz', $locator->get('bar'));
}
public function testGetDoesNotMemoize()
{
$i = 0;
$locator = new ServiceLocator(array(
'foo' => function () use (&$i) {
++$i;
return 'bar';
},
));
$this->assertSame('bar', $locator->get('foo'));
$this->assertSame('bar', $locator->get('foo'));
$this->assertSame(2, $i);
return new ServiceLocator($factories);
}
/**
@ -65,7 +29,7 @@ class ServiceLocatorTest extends TestCase
*/
public function testGetThrowsOnUndefinedService()
{
$locator = new ServiceLocator(array(
$locator = $this->getServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
));
@ -73,32 +37,13 @@ class ServiceLocatorTest extends TestCase
$locator->get('dummy');
}
/**
* @expectedException \Psr\Container\NotFoundExceptionInterface
* @expectedExceptionMessage The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.
*/
public function testThrowsOnUndefinedInternalService()
{
$locator = new ServiceLocator(array(
'foo' => function () use (&$locator) { return $locator->get('bar'); },
));
$locator->get('foo');
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException
* @expectedExceptionMessage Circular reference detected for service "bar", path: "bar -> baz -> bar".
*/
public function testThrowsOnCircularReference()
{
$locator = new ServiceLocator(array(
'foo' => function () use (&$locator) { return $locator->get('bar'); },
'bar' => function () use (&$locator) { return $locator->get('baz'); },
'baz' => function () use (&$locator) { return $locator->get('bar'); },
));
$locator->get('foo');
parent::testThrowsOnCircularReference();
}
/**
@ -110,7 +55,7 @@ class ServiceLocatorTest extends TestCase
$container = new Container();
$container->set('foo', new \stdClass());
$subscriber = new SomeServiceSubscriber();
$subscriber->container = new ServiceLocator(array('bar' => function () {}));
$subscriber->container = $this->getServiceLocator(array('bar' => function () {}));
$subscriber->container = $subscriber->container->withContext('caller', $container);
$subscriber->getFoo();
@ -118,7 +63,7 @@ class ServiceLocatorTest extends TestCase
public function testInvoke()
{
$locator = new ServiceLocator(array(
$locator = $this->getServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
));
@ -127,20 +72,6 @@ class ServiceLocatorTest extends TestCase
$this->assertSame('baz', $locator('bar'));
$this->assertNull($locator('dummy'), '->__invoke() should return null on invalid service');
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Invalid service "foo" required by "external-id".
*/
public function testRuntimeException()
{
$locator = new ServiceLocator(array(
'foo' => function () { throw new RuntimeException('Invalid service ".service_locator.abcdef".'); },
));
$locator = $locator->withContext('external-id', new Container());
$locator->get('foo');
}
}
class SomeServiceSubscriber implements ServiceSubscriberinterface

View File

@ -7,3 +7,6 @@ CHANGELOG
* added `Service\ResetInterface` to provide a way to reset an object to its initial state
* added `Translation\TranslatorInterface` and `Translation\TranslatorTrait`
* added `Cache` contract to extend PSR-6 with tag invalidation, callback-based computation and stampede protection
* added `Service\ServiceSubscriberInterface` to declare the dependencies of a class that consumes a service locator
* added `Service\ServiceSubscriberTrait` to implement `Service\ServiceSubscriberInterface` using methods' return types
* added `Service\ServiceLocatorTrait` to help implement PSR-11 service locators

View File

@ -0,0 +1,97 @@
<?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\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
/**
* A trait to help implement PSR-11 service locators.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ServiceLocatorTrait
{
private $factories;
private $loading = array();
/**
* @param callable[] $factories
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
/**
* {@inheritdoc}
*/
public function has($id)
{
return isset($this->factories[$id]);
}
/**
* {@inheritdoc}
*/
public function get($id)
{
if (!isset($this->factories[$id])) {
throw $this->createNotFoundException($id);
}
if (isset($this->loading[$id])) {
$ids = array_values($this->loading);
$ids = \array_slice($this->loading, array_search($id, $ids));
$ids[] = $id;
throw $this->createCircularReferenceException($id, $ids);
}
$this->loading[$id] = $id;
try {
return $this->factories[$id]($this);
} finally {
unset($this->loading[$id]);
}
}
private function createNotFoundException(string $id): NotFoundExceptionInterface
{
if (!$alternatives = array_keys($this->factories)) {
$message = 'is empty...';
} else {
$last = array_pop($alternatives);
if ($alternatives) {
$message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last);
} else {
$message = sprintf('only knows about the "%s" service.', $last);
}
}
if ($this->loading) {
$message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message);
} else {
$message = sprintf('Service "%s" not found: the current service locator %s', $id, $message);
}
return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface {
};
}
private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface
{
return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface {
};
}
}

View File

@ -0,0 +1,53 @@
<?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;
/**
* A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method.
*
* The getSubscribedServices method returns an array of service types required by such instances,
* optionally keyed by the service names used internally. Service types that start with an interrogation
* mark "?" are optional, while the other ones are mandatory service dependencies.
*
* The injected service locators SHOULD NOT allow access to any other services not specified by the method.
*
* It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally.
* This interface does not dictate any injection method for these service locators, although constructor
* injection is recommended.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ServiceSubscriberInterface
{
/**
* Returns an array of service types required by such instances, optionally keyed by the service names used internally.
*
* For mandatory dependencies:
*
* * array('logger' => 'Psr\Log\LoggerInterface') means the objects use the "logger" name
* internally to fetch a service which must implement Psr\Log\LoggerInterface.
* * array('loggers' => 'Psr\Log\LoggerInterface[]') means the objects use the "loggers" name
* internally to fetch an iterable of Psr\Log\LoggerInterface instances.
* * array('Psr\Log\LoggerInterface') is a shortcut for
* * array('Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface')
*
* otherwise:
*
* * array('logger' => '?Psr\Log\LoggerInterface') denotes an optional dependency
* * array('loggers' => '?Psr\Log\LoggerInterface[]') denotes an optional iterable dependency
* * array('?Psr\Log\LoggerInterface') is a shortcut for
* * array('Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface')
*
* @return array The required service types, optionally keyed by service names
*/
public static function getSubscribedServices();
}

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection;
namespace Symfony\Contracts\Service;
use Psr\Container\ContainerInterface;

View File

@ -0,0 +1,94 @@
<?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\Tests\Service;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceLocatorTrait;
class ServiceLocatorTest extends TestCase
{
public function getServiceLocator(array $factories)
{
return new class($factories) implements ContainerInterface {
use ServiceLocatorTrait;
};
}
public function testHas()
{
$locator = $this->getServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
function () { return 'dummy'; },
));
$this->assertTrue($locator->has('foo'));
$this->assertTrue($locator->has('bar'));
$this->assertFalse($locator->has('dummy'));
}
public function testGet()
{
$locator = $this->getServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
));
$this->assertSame('bar', $locator->get('foo'));
$this->assertSame('baz', $locator->get('bar'));
}
public function testGetDoesNotMemoize()
{
$i = 0;
$locator = $this->getServiceLocator(array(
'foo' => function () use (&$i) {
++$i;
return 'bar';
},
));
$this->assertSame('bar', $locator->get('foo'));
$this->assertSame('bar', $locator->get('foo'));
$this->assertSame(2, $i);
}
/**
* @expectedException \Psr\Container\NotFoundExceptionInterface
* @expectedExceptionMessage The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.
*/
public function testThrowsOnUndefinedInternalService()
{
$locator = $this->getServiceLocator(array(
'foo' => function () use (&$locator) { return $locator->get('bar'); },
));
$locator->get('foo');
}
/**
* @expectedException \Psr\Container\ContainerExceptionInterface
* @expectedExceptionMessage Circular reference detected for service "bar", path: "bar -> baz -> bar".
*/
public function testThrowsOnCircularReference()
{
$locator = $this->getServiceLocator(array(
'foo' => function () use (&$locator) { return $locator->get('bar'); },
'bar' => function () use (&$locator) { return $locator->get('baz'); },
'baz' => function () use (&$locator) { return $locator->get('bar'); },
));
$locator->get('foo');
}
}

View File

@ -9,26 +9,28 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests;
namespace Symfony\Contracts\Tests\Service;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
use Symfony\Contracts\Service\ServiceLocatorTrait;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
class ServiceSubscriberTraitTest extends TestCase
{
public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices()
{
$expected = array(TestService::class.'::aService' => '?Symfony\Component\DependencyInjection\Tests\Service2');
$expected = array(TestService::class.'::aService' => '?Symfony\Contracts\Tests\Service\Service2');
$this->assertEquals($expected, ChildTestService::getSubscribedServices());
}
public function testSetContainerIsCalledOnParent()
{
$container = new Container();
$container = new class(array()) implements ContainerInterface {
use ServiceLocatorTrait;
};
$this->assertSame($container, (new TestService())->setContainer($container));
}

View File

@ -18,8 +18,12 @@
"require": {
"php": "^7.1.3"
},
"require-dev": {
"psr/container": "^1.0"
},
"suggest": {
"psr/cache": "When using the Cache contract"
"psr/cache": "When using the Cache contracts",
"psr/container": "When using the Service contracts"
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\": "" },