diff --git a/.travis.yml b/.travis.yml index 7d8abee..4c3f4e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,4 @@ before_script: - composer install script: - - phpunit + - ./vendor/bin/phpunit diff --git a/README.md b/README.md index 9579b97..374e25e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ PHPBench Service Container ========================== [![Build Status](https://travis-ci.org/phpbench/container.svg?branch=master)](https://travis-ci.org/phpbench/container) -[![StyleCI](https://styleci.io/repos/55606670/shield)](https://styleci.io/repos/55606670) Simple, extensible dependency injection container with parameters and service tagging. Implements [container @@ -47,7 +46,7 @@ class MyExtension implements ExtensionInterface $container->get('some_other_service') ); - foreach ($container->getServiceIdsForTag() as $serviceId) { + foreach ($container->getServiceIdsForTag('tag') as $serviceId => $params) { $service->add($container->get($serviceId)); } @@ -59,7 +58,7 @@ class MyExtension implements ExtensionInterface $container->getParameter('foo_bar'), $container->get('some_other_service') ); - }, [ 'tag' => []); + }, [ 'tag' => [ 'param1' => 'foobar' ]); } /** diff --git a/composer.json b/composer.json index f8af677..9388bd1 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,13 @@ } ], "require": { - "container-interop/container-interop": "^1.1" + "psr/container": "^1.0|^2.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { + "phpunit/phpunit": "^8", + "phpstan/phpstan": "^0.12.52", + "friendsofphp/php-cs-fixer": "^2.16" }, "autoload": { "psr-4": { @@ -27,7 +31,14 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.x-dev" } + }, + "scripts": { + "integrate": [ + "vendor/bin/phpunit", + "vendor/bin/php-cs-fixer fix lib", + "vendor/bin/phpstan analyse lib --level=7" + ] } } diff --git a/lib/Container.php b/lib/Container.php index 5556c69..d1b12b2 100644 --- a/lib/Container.php +++ b/lib/Container.php @@ -11,7 +11,10 @@ namespace PhpBench\DependencyInjection; -use Interop\Container\ContainerInterface; +use Closure; +use Psr\Container\ContainerInterface; +use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * PHPBench Container. @@ -20,18 +23,39 @@ */ class Container implements ContainerInterface { + /** + * @var array + */ private $instantiators = []; + + /** + * @var array + */ private $services = []; + + /** + * @var array> + */ private $tags = []; + + /** + * @var array + */ private $config = []; - private $userConfig = []; + /** + * @var array + */ private $extensionClasses = []; - public function __construct(array $extensionClasses = [], array $userConfig = []) + /** + * @param array $config + * @param array $extensionClasses + */ + public function __construct(array $extensionClasses = [], array $config = []) { $this->extensionClasses = $extensionClasses; - $this->userConfig = $userConfig; + $this->config = $config; } /** @@ -41,11 +65,13 @@ public function __construct(array $extensionClasses = [], array $userConfig = [] * * This method must be called before `build()`. */ - public function init() + public function init(): void { + $resolver = new OptionsResolver(); $extensions = []; + $config = []; - if (empty($this->extensionClasses) && empty($this->userConfig)) { + if (empty($this->extensionClasses) && empty($this->config)) { return; } @@ -68,26 +94,27 @@ public function init() } $extensions[] = $extension; - - $this->config = array_merge( - $this->config, - $extension->getDefaultConfig() - ); + $extension->configure($resolver); } - $diff = array_diff(array_keys($this->userConfig), array_keys($this->config)); + $diff = array_diff(array_keys($this->config), array_keys($this->config)); if ($diff) { throw new \InvalidArgumentException(sprintf( 'Unknown configuration keys: "%s". Permitted keys: "%s"', - implode('", "', $diff), implode('", "', array_keys($this->config)) + implode('", "', $diff), + implode('", "', array_keys($this->config)) )); } - $this->config = array_merge( - $this->config, - $this->userConfig - ); + try { + $this->config = $resolver->resolve($this->config); + } catch (ExceptionInterface $resolverException) { + throw new InvalidConfigurationException(sprintf( + 'Invalid user configuration: %s', + $resolverException->getMessage() + ), 0, $resolverException); + } foreach ($extensions as $extension) { $extension->load($this); @@ -99,8 +126,6 @@ public function init() * Note that this method will return the same instance on subsequent calls. * * @param string $serviceId - * - * @return mixed */ public function get($serviceId) { @@ -120,7 +145,10 @@ public function get($serviceId) return $this->services[$serviceId]; } - public function has($serviceId) + /** + * @param string $serviceId + */ + public function has($serviceId): bool { return isset($this->instantiators[$serviceId]); } @@ -128,10 +156,9 @@ public function has($serviceId) /** * Set a service instance. * - * @param string $serviceId * @param mixed $instance */ - public function set($serviceId, $instance) + public function set(string $serviceId, $instance): void { $this->services[$serviceId] = $instance; } @@ -139,11 +166,9 @@ public function set($serviceId, $instance) /** * Return services IDs for the given tag. * - * @param string $tag - * - * @return string[] + * @return array> */ - public function getServiceIdsForTag($tag) + public function getServiceIdsForTag(string $tag): array { $serviceIds = []; foreach ($this->tags as $serviceId => $tags) { @@ -161,15 +186,15 @@ public function getServiceIdsForTag($tag) * The instantiator is a closure which accepts an instance of this container and * returns a new instance of the service class. * - * @param string $serviceId - * @param \Closure $instantiator - * @param string[] $tags + * @param array> $tags */ - public function register($serviceId, \Closure $instantiator, array $tags = []) + public function register(string $serviceId, Closure $instantiator, array $tags = []): void { if (isset($this->instantiators[$serviceId])) { throw new \InvalidArgumentException(sprintf( - 'Service with ID "%s" has already been registered', $serviceId)); + 'Service with ID "%s" has already been registered', + $serviceId + )); } $this->instantiators[$serviceId] = $instantiator; @@ -179,14 +204,33 @@ public function register($serviceId, \Closure $instantiator, array $tags = []) /** * Set the value of the parameter with the given name. * - * @param string $name * @param mixed $value */ - public function setParameter($name, $value) + public function setParameter(string $name, $value): void { $this->config[$name] = $value; } + /** + * @param array $values + */ + public function mergeParameter(string $name, array $values): void + { + $actual = $this->getParameter($name); + + if (!is_array($actual)) { + throw new \InvalidArgumentException(sprintf( + 'Cannot merge values on to a scalar parameter "%s"', + $name + )); + } + + $this->setParameter($name, array_merge( + $actual, + $values + )); + } + /** * Return the parameter with the given name. * @@ -208,6 +252,14 @@ public function getParameter($name) return $this->config[$name]; } + /** + * @return array + */ + public function getParameters(): array + { + return $this->config; + } + /** * Return true if the named parameter exists. * @@ -219,4 +271,12 @@ public function hasParameter($name) { return array_key_exists($name, $this->config); } + + /** + * @return class-string[] + */ + public function getExtensionClasses(): array + { + return $this->extensionClasses; + } } diff --git a/lib/ExtensionInterface.php b/lib/ExtensionInterface.php index 55066e9..33c6943 100644 --- a/lib/ExtensionInterface.php +++ b/lib/ExtensionInterface.php @@ -11,6 +11,8 @@ namespace PhpBench\DependencyInjection; +use Symfony\Component\OptionsResolver\OptionsResolver; + interface ExtensionInterface { /** @@ -18,12 +20,10 @@ interface ExtensionInterface * * @param Container $container */ - public function load(Container $container); + public function load(Container $container): void; /** - * Return the default parameters for the container. - * - * @return array + * Configure the parameters which can be accessed by the extension. */ - public function getDefaultConfig(); + public function configure(OptionsResolver $resolver): void; } diff --git a/lib/InvalidConfigurationException.php b/lib/InvalidConfigurationException.php new file mode 100644 index 0000000..2227177 --- /dev/null +++ b/lib/InvalidConfigurationException.php @@ -0,0 +1,9 @@ +container = new Container(); } @@ -78,12 +82,11 @@ public function testServiceIdTags() /** * Its should throw an exception if a service is already registered. - * - * @expectedException InvalidArgumentException - * @expectedExceptionMessge Service with ID "stdclass" */ public function testServiceAlreadyRegistered() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Service with ID "stdclass"'); $this->container->register('stdclass', function () { return new \stdClass(); }); @@ -107,6 +110,9 @@ public function testRegisterExtension() $object = $container->get('foobar'); $this->assertInstanceOf('stdClass', $object); $this->assertEquals('bar', $object->foobar); + $this->assertEquals([ + __NAMESPACE__ . '\\TestExtension', + ], $container->getExtensionClasses());; } /** @@ -129,14 +135,39 @@ public function testRegisterExtensionWithUserConfig() $this->assertEquals('bazz', $object->foobar); } + /** + * It should merge parameters. + */ + public function testMergeParameters() + { + $this->container->setParameter('foo', ['foo' => 'bar']); + $this->container->mergeParameter('foo', ['bar' => 'boo']); + $this->assertEquals([ + 'foo' => 'bar', + 'bar' => 'boo', + ], $this->container->getParameter('foo')); + } + + /** + * It should throw an exception when trying to merge a value into a non-array parameter. + */ + public function testMergeParameterNonArray() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('scalar'); + + $this->container->setParameter('foo', 'bar'); + $this->container->mergeParameter('foo', ['bar' => 'boo']); + } + /** * It should throw an exception if an extension class does not exist. - * - * @expectedException InvalidArgumentException - * @expectedExceptionMessage "NotExistingExtension" does not exist */ public function testRegisterNotExistingExtension() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('"NotExistingExtension" does not exist'); + $container = new Container(['NotExistingExtension']); $container->init(); } @@ -144,24 +175,23 @@ public function testRegisterNotExistingExtension() /** * It should throw an exception if an extension class does not implement * the ExtensionInterface. - * - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Extension "stdClass" must implement the */ public function testRegisterNotImplementingExtension() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Extension "stdClass" must implement the'); + $container = new Container(['stdClass']); $container->init(); } /** * It should throw an exception if an unknown user configuration key is used. - * - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Unknown configuration keys: "not". Permitted keys: */ public function testUnknownUserConfig() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Invalid user configuration'); $container = new Container([], [ 'not' => 'existing', ]); @@ -170,12 +200,12 @@ public function testUnknownUserConfig() /** * It should throw an exception if a requested parameter does not exist. - * - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Parameter "foo" has not been registered */ public function testUnknownParameter() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Parameter "foo" has not been registered'); + $container = new Container(); $container->getParameter('foo'); } @@ -183,14 +213,14 @@ public function testUnknownParameter() class TestExtension implements ExtensionInterface { - public function getDefaultConfig() + public function configure(OptionsResolver $resolver): void { - return [ + $resolver->setDefaults([ 'foo' => 'bar', - ]; + ]); } - public function load(Container $container) + public function load(Container $container): void { $container->register('foobar', function ($container) { $stdClass = new \stdClass();