8000 [Contracts] Add traits+interfaces from the DI component by nicolas-grekas · Pull Request #28206 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Contracts] Add traits+interfaces from the DI component #28206

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
Sep 4, 2018
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 @@ -22,7 +22,6 @@
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;
Expand All @@ -31,6 +30,7 @@
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';

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

namespace Symfony\Component\DependencyInjection\Tests\Fixtures;

use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberTrait;

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

/**
Expand All @@ -65,40 +29,21 @@ public function testGetDoesNotMemoize()
*/
public function testGetThrowsOnUndefinedService()
{
$locator = new ServiceLocator(array(
$locator = $this->getServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
));

$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();
}

/**
Expand All @@ -110,15 +55,15 @@ public function testThrowsInServiceSubscriber()
$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();
}

public function testInvoke()
{
$locator = new ServiceLocator(array(
$locator = $this->getServiceLocator(array(
'foo' => function () { return 'bar'; },
'bar' => function () { return 'baz'; },
));
Expand All @@ -127,20 +72,6 @@ public function testInvoke()
$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
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Contracts/CHANGELOG.md
A36C
Original file line number Diff line number Diff line change
Expand Up @@ -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
97 changes: 97 additions & 0 deletions src/Symfony/Contracts/Service/ServiceLocatorTrait.php
Original file line number Diff line number Diff line change
@@ -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 {
};
}
}
53 changes: 53 additions & 0 deletions src/Symfony/Contracts/Service/ServiceSubscriberInterface.php
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection;
namespace Symfony\Contracts\Service;

use Psr\Container\ContainerInterface;

Expand Down
Loading
0