API PSR-11 compliance (fixes #6594) (#6931)

Note that our usage of `$asSingleton` in `get()` is fine. Quote from the PSR:

> Two successive calls to get with the same identifier SHOULD return the same value. However, depending on the implementor design and/or user configuration, different values might be returned, so user SHOULD NOT rely on getting the same value on 2 successive calls.
This commit is contained in:
Ingo Schommer 2017-05-19 13:45:07 +12:00 committed by Damian Mooyman
parent db3e3d51fd
commit 100048da33
7 changed files with 130 additions and 42 deletions

View File

@ -39,7 +39,8 @@
"ext-tokenizer": "*", "ext-tokenizer": "*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-hash": "*", "ext-hash": "*",
"ext-session": "*" "ext-session": "*",
"psr/container-implementation": "1.0.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7", "phpunit/phpunit": "^5.7",
@ -48,6 +49,9 @@
"silverstripe/serve": "dev-master", "silverstripe/serve": "dev-master",
"se/selenium-server-standalone": "2.41.0" "se/selenium-server-standalone": "2.41.0"
}, },
"provide": {
"psr/container-implementation": "1.0.0"
},
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.0.x-dev" "dev-master": "4.0.x-dev"

View File

@ -1292,6 +1292,9 @@ After (`mysite/_config/config.yml`):
* Removed `CreditCardField`, `CountryDropdownField`, `PhoneNumberField`, `MemberDatetimeOptionsetField`, `InlineFormAction`. * Removed `CreditCardField`, `CountryDropdownField`, `PhoneNumberField`, `MemberDatetimeOptionsetField`, `InlineFormAction`.
Use custom code instead Use custom code instead
* Removed `ResetFormAction`, use `FormAction::create()->setAttribute('type', 'reset')` instead * Removed `ResetFormAction`, use `FormAction::create()->setAttribute('type', 'reset')` instead
* `Injector` now complies with [PSR-11](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md).
Accordingly, `hasService()` has been renamed to `has()`, and `get()` will throw
`SilverStripe\Core\Injector\InjectorNotFoundException` when the service can't be found.
#### <a name="overview-general-deprecated"></a>General and Core Deprecated API #### <a name="overview-general-deprecated"></a>General and Core Deprecated API

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Core\Injector; namespace SilverStripe\Core\Injector;
use ReflectionClass; use ReflectionClass;
use ReflectionException;
/** /**
* A class for creating new objects by the injector. * A class for creating new objects by the injector.
@ -12,7 +13,11 @@ class InjectionCreator implements Factory
public function create($class, array $params = array()) public function create($class, array $params = array())
{ {
$reflector = new ReflectionClass($class); try {
$reflector = new ReflectionClass($class);
} catch (ReflectionException $e) {
throw new InjectorNotFoundException($e);
}
if (count($params)) { if (count($params)) {
return $reflector->newInstanceArgs($params); return $reflector->newInstanceArgs($params);

View File

@ -2,11 +2,14 @@
namespace SilverStripe\Core\Injector; namespace SilverStripe\Core\Injector;
use Psr\Container\NotFoundExceptionInterface;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use ReflectionProperty; use ReflectionProperty;
use ArrayObject; use ArrayObject;
use ReflectionObject; use ReflectionObject;
use ReflectionMethod; use ReflectionMethod;
use Psr\Container\ContainerInterface;
use SilverStripe\Dev\Deprecation;
/** /**
* A simple injection manager that manages creating objects and injecting * A simple injection manager that manages creating objects and injecting
@ -122,7 +125,7 @@ use ReflectionMethod;
* @author marcus@silverstripe.com.au * @author marcus@silverstripe.com.au
* @license BSD License http://silverstripe.org/bsd-license/ * @license BSD License http://silverstripe.org/bsd-license/
*/ */
class Injector class Injector implements ContainerInterface
{ {
/** /**
@ -233,7 +236,7 @@ class Injector
* If a user wants to use the injector as a static reference * If a user wants to use the injector as a static reference
* *
* @param array $config * @param array $config
* @return Injector * @return ContainerInterface
*/ */
public static function inst($config = null) public static function inst($config = null)
{ {
@ -246,10 +249,10 @@ class Injector
/** /**
* Sets the default global injector instance. * Sets the default global injector instance.
* *
* @param Injector $instance * @param ContainerInterface $instance
* @return Injector Reference to new active Injector instance * @return Injector Reference to new active Injector instance
*/ */
public static function set_inst(Injector $instance) public static function set_inst(ContainerInterface $instance)
{ {
return self::$instance = $instance; return self::$instance = $instance;
} }
@ -693,7 +696,7 @@ class Injector
if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) { if ($propertyObject->isPublic() && !$propertyObject->getValue($object)) {
$origName = $propertyObject->getName(); $origName = $propertyObject->getName();
$name = ucfirst($origName); $name = ucfirst($origName);
if ($this->hasService($name)) { if ($this->has($name)) {
// Pull the name out of the registry // Pull the name out of the registry
$value = $this->get($name); $value = $this->get($name);
$propertyObject->setValue($object, $value); $propertyObject->setValue($object, $value);
@ -710,7 +713,7 @@ class Injector
$methName = $methodObj->getName(); $methName = $methodObj->getName();
if (strpos($methName, 'set') === 0) { if (strpos($methName, 'set') === 0) {
$pname = substr($methName, 3); $pname = substr($methName, 3);
if ($this->hasService($pname)) { if ($this->has($pname)) {
// Pull the name out of the registry // Pull the name out of the registry
$value = $this->get($pname); $value = $this->get($pname);
$methodObj->invoke($object, $value); $methodObj->invoke($object, $value);
@ -782,6 +785,35 @@ class Injector
} }
} }
/**
* @deprecated 4.0.0:5.0.0 Use Injector::has() instead
* @param $name
* @return string
*/
public function hasService($name)
{
Deprecation::notice('5.0', 'Use Injector::has() instead');
return $this->has($name);
}
/**
* Does the given service exist?
*
* We do a special check here for services that are using compound names. For example,
* we might want to say that a property should be injected with Log.File or Log.Memory,
* but have only registered a 'Log' service, we'll instead return that.
*
* Will recursively call itself for each depth of dotting.
*
* @param string $name
* @return boolean
*/
public function has($name)
{
return (bool)$this->getServiceName($name);
}
/** /**
* Does the given service exist, and if so, what's the stored name for it? * Does the given service exist, and if so, what's the stored name for it?
* *
@ -789,15 +821,14 @@ class Injector
* we might want to say that a property should be injected with Log.File or Log.Memory, * we might want to say that a property should be injected with Log.File or Log.Memory,
* but have only registered a 'Log' service, we'll instead return that. * but have only registered a 'Log' service, we'll instead return that.
* *
* Will recursively call hasService for each depth of dotting * Will recursively call itself for each depth of dotting.
* *
* @param string $name * @param string $name
* @return string The name of the service (as it might be different from the one passed in) * @return string|null The name of the service (as it might be different from the one passed in)
* The name of the service (as it might be different from the one passed in)
*/ */
public function hasService($name) public function getServiceName($name)
{ {
// common case, get it overwith first // common case, get it over with first
if (isset($this->specs[$name])) { if (isset($this->specs[$name])) {
return $name; return $name;
} }
@ -808,7 +839,7 @@ class Injector
return null; return null;
} }
return $this->hasService(substr($name, 0, strrpos($name, '.'))); return $this->getServiceName(substr($name, 0, strrpos($name, '.')));
} }
/** /**
@ -859,30 +890,45 @@ class Injector
* Next, will check to see if there's any registered configuration for the given type * Next, will check to see if there's any registered configuration for the given type
* and will then try and load that * and will then try and load that
* *
* Failing all of that, will just return a new instance of the * Failing all of that, will just return a new instance of the specified object.
* specificied object.
* *
* @param string $name * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
* the name of the service to retrieve. If not a registered *
* service, then a class of the given name is instantiated * @param string $name The name of the service to retrieve. If not a registered
* @param boolean $asSingleton * service, then a class of the given name is instantiated
* Whether to register the created object as a singleton * @param boolean $asSingleton Whether to register the created object as a singleton
* if no other configuration is found * if no other configuration is found
* @param array $constructorArgs * @param array $constructorArgs Optional set of arguments to pass as constructor arguments
* Optional set of arguments to pass as constructor arguments * if this object is to be created from scratch (with $asSingleton = false)
* if this object is to be created from scratch * @return mixed Instance of the specified object
* (ie asSingleton = false)
* @return mixed the instance of the specified object
*/ */
public function get($name, $asSingleton = true, $constructorArgs = null) public function get($name, $asSingleton = true, $constructorArgs = null)
{
$object = $this->getNamedService($name, $asSingleton, $constructorArgs);
if (!$object) {
throw new InjectorNotFoundException("The '{$name}' service could not be found");
}
return $object;
}
/**
* Returns the service, or `null` if it doesnt' exist. See {@link get()} for main usage.
*
* @param string $name
* @param boolean $asSingleton
* @param array $constructorArgs
* @return mixed|null Instance of the specified object (if it exists)
*/
protected function getNamedService($name, $asSingleton = true, $constructorArgs = null)
{ {
// reassign the name as it might actually be a compound name // reassign the name as it might actually be a compound name
if ($serviceName = $this->hasService($name)) { if ($serviceName = $this->getServiceName($name)) {
// check to see what the type of bean is. If it's a prototype, // check to see what the type of bean is. If it's a prototype,
// we don't want to return the singleton version of it. // we don't want to return the singleton version of it.
$spec = $this->specs[$serviceName]; $spec = $this->specs[$serviceName];
$type = isset($spec['type']) ? $spec['type'] : null; $type = isset($spec['type']) ? $spec['type'] : null;
// if we're explicitly a prototype OR we're not wanting a singleton // if we're explicitly a prototype OR we're not wanting a singleton
if (($type && $type == 'prototype') || !$asSingleton) { if (($type && $type == 'prototype') || !$asSingleton) {
if ($spec && $constructorArgs) { if ($spec && $constructorArgs) {
@ -903,7 +949,6 @@ class Injector
return $this->serviceCache[$serviceName]; return $this->serviceCache[$serviceName];
} }
} }
$config = $this->configLocator->locateConfigFor($name); $config = $this->configLocator->locateConfigFor($name);
if ($config) { if ($config) {
$this->load(array($name => $config)); $this->load(array($name => $config));
@ -916,7 +961,6 @@ class Injector
return $this->instantiate($spec, $name); return $this->instantiate($spec, $name);
} }
} }
// If we've got this far, we're dealing with a case of a user wanting // If we've got this far, we're dealing with a case of a user wanting
// to create an object based on its name. So, we need to fake its config // to create an object based on its name. So, we need to fake its config
// if the user wants it managed as a singleton service style object // if the user wants it managed as a singleton service style object

View File

@ -0,0 +1,9 @@
<?php
namespace SilverStripe\Core\Injector;
use Psr\Container\NotFoundExceptionInterface;
class InjectorNotFoundException extends \Exception implements NotFoundExceptionInterface
{
}

View File

@ -6,6 +6,7 @@ use InvalidArgumentException;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Factory; use SilverStripe\Core\Injector\Factory;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorNotFoundException;
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator; use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
use SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService; use SilverStripe\Core\Tests\Injector\AopProxyServiceTest\AnotherService;
use SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService; use SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService;
@ -80,9 +81,15 @@ class InjectorTest extends SapphireTest
); );
$injector->load($config); $injector->load($config);
$this->assertFalse($injector->has('UnknownService'));
$this->assertNull($injector->getServiceName('UnknownService'));
$this->assertTrue($injector->has('SampleService'));
$this->assertEquals( $this->assertEquals(
'SampleService', 'SampleService',
$injector->hasService('SampleService') $injector->getServiceName('SampleService')
); );
$myObject = new TestObject(); $myObject = new TestObject();
@ -110,15 +117,17 @@ class InjectorTest extends SapphireTest
); );
$injector->load($services); $injector->load($services);
$this->assertTrue($injector->has('SampleService'));
$this->assertEquals( $this->assertEquals(
'SampleService', 'SampleService',
$injector->hasService('SampleService') $injector->getServiceName('SampleService')
); );
// We expect a false because the AnotherService::class is actually // We expect a false because the AnotherService::class is actually
// just a replacement of the SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService // just a replacement of the SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService
$this->assertTrue($injector->has('SampleService'));
$this->assertEquals( $this->assertEquals(
'AnotherService', 'AnotherService',
$injector->hasService('AnotherService') $injector->getServiceName('AnotherService')
); );
$item = $injector->get('AnotherService'); $item = $injector->get('AnotherService');
@ -136,8 +145,11 @@ class InjectorTest extends SapphireTest
$injector->load($services); $injector->load($services);
$this->assertTrue($injector->hasService('FirstId') == 'FirstId'); $this->assertTrue($injector->has('FirstId'));
$this->assertTrue($injector->hasService('SecondId') == 'SecondId'); $this->assertEquals($injector->getServiceName('FirstId'), 'FirstId');
$this->assertTrue($injector->has('SecondId'));
$this->assertEquals($injector->getServiceName('SecondId'), 'SecondId');
$this->assertTrue($injector->get('FirstId') instanceof AnotherService); $this->assertTrue($injector->get('FirstId') instanceof AnotherService);
$this->assertTrue($injector->get('SecondId') instanceof SampleService); $this->assertTrue($injector->get('SecondId') instanceof SampleService);
@ -251,9 +263,10 @@ class InjectorTest extends SapphireTest
); );
$injector->load($config); $injector->load($config);
$this->assertTrue($injector->has('SampleService'));
$this->assertEquals( $this->assertEquals(
'SampleService', 'SampleService',
$injector->hasService('SampleService') $injector->getServiceName('SampleService')
); );
// We expect a false because the AnotherService::class is actually // We expect a false because the AnotherService::class is actually
// just a replacement of the SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService // just a replacement of the SilverStripe\Core\Tests\Injector\AopProxyServiceTest\SampleService
@ -419,7 +432,8 @@ class InjectorTest extends SapphireTest
); );
$injector->load($config); $injector->load($config);
$this->assertEquals('SampleService', $injector->hasService('SampleService')); $this->assertTrue($injector->has('SampleService'));
$this->assertEquals('SampleService', $injector->getServiceName('SampleService'));
$myObject = new InjectorTest\OtherTestObject(); $myObject = new InjectorTest\OtherTestObject();
$injector->inject($myObject); $injector->inject($myObject);
@ -453,7 +467,8 @@ class InjectorTest extends SapphireTest
); );
$injector->load($config); $injector->load($config);
$this->assertEquals('SampleService', $injector->hasService('SampleService')); $this->assertTrue($injector->has('SampleService'));
$this->assertEquals('SampleService', $injector->getServiceName('SampleService'));
$myObject = $injector->get(OtherTestObject::class); $myObject = $injector->get(OtherTestObject::class);
$this->assertInstanceOf( $this->assertInstanceOf(
@ -919,7 +934,14 @@ class InjectorTest extends SapphireTest
$item = $injector->get('TestService'); $item = $injector->get('TestService');
} }
/**
* @expectedException \SilverStripe\Core\Injector\InjectorNotFoundException
*/
public function testGetThrowsOnNotFound()
{
$injector = new Injector();
$injector->get('UnknownService');
}
/** /**
* Test nesting of injector * Test nesting of injector

View File

@ -3,6 +3,7 @@
namespace SilverStripe\ORM\Tests; namespace SilverStripe\ORM\Tests;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\InjectorNotFoundException;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\Filterable; use SilverStripe\ORM\Filterable;
@ -744,7 +745,7 @@ class DataListTest extends SapphireTest
public function testSimpleFilterWithNonExistingComparisator() public function testSimpleFilterWithNonExistingComparisator()
{ {
$this->setExpectedException( $this->setExpectedException(
'ReflectionException', InjectorNotFoundException::class,
'Class DataListFilter.Bogus does not exist' 'Class DataListFilter.Bogus does not exist'
); );
$list = TeamComment::get(); $list = TeamComment::get();
@ -755,7 +756,7 @@ class DataListTest extends SapphireTest
{ {
// Invalid modifiers are treated as failed filter construction // Invalid modifiers are treated as failed filter construction
$this->setExpectedException( $this->setExpectedException(
'ReflectionException', InjectorNotFoundException::class,
'Class DataListFilter.invalidmodifier does not exist' 'Class DataListFilter.invalidmodifier does not exist'
); );
$list = TeamComment::get(); $list = TeamComment::get();