Commit 70366223 authored by Avris's avatar Avris

init

parents
Pipeline #15965186 passed with stage
in 3 seconds
/.idea/*
**/.DS_Store
/vendor/*
/tests/_output/*
ci_tests:
script:
- composer install --dev
- vendor/bin/phpunit
- vendor/bin/phpcs --standard=PSR2 src/
## Avris Container ##
A dependency injection container with autowiring
## Installation
composer require avris/container
## Usage
### Basics
Container resolves dependencies between defined services,
in order to simplify the development process, avoid duplication of code,
facilitate interoperability and improve maintainability and testability.
See: [Dependency Injection pattern](https://en.wikipedia.org/wiki/Dependency_injection).
$parameterProvider = new SimpleParameterProvider(['ROOT_DIR' => __DIR__]);
// parameter provider is optional
$container = new Container($parameterProvider);
$container->set('number', 4);
$container->set(Foo::class, new Foo);
$container->setDefinition(Bar::class, [
'arguments' => [
'$foo' => '@' . Foo::class,
'$dir' => '%ROOT_DIR%/bar',
'$number' => '@number',
'$float' => 69.123,
],
'public' => true,
]);
$container->setDefinition(BarInterface::class, Bar::class); // alias
$container->get('number'); // 4
$container->get(Foo::class); // new Foo
$container->get(BarInterface::class); // new Bar(new Foo, __DIR__ . '/bar', 4, 69.123)
$container->getParameter('ROOT_DIR'); // __DIR__
### Options
* `class` -- the class of the service, will default to the service name if not given.
If the given class implements the `Resolver` interface, it will be instantiated,
and it's `resolve` method executed to provide an actual value to be put in the container.
* `arguments` -- constructor arguments.
* `calls` -- method calls to be executed right after constructing the service (setter injection etc.)
'calls' => [
['setLogger', ['@logger']],
['registerListener', ['@listenerA']],
['registerListener', ['@listenerB']],
],
* `tags` -- an array of string that help group similar services together; tagged services can be injected with `#tagName`:
$container->setDefinition(HandlerA::class, ['tags' => 'handler']);
$container->setDefinition(HandlerB::class, ['tags' => 'handler']);
$container->setDefinition(HandlerC::class, ['tags' => 'handler']);
$container->setDefinition(Manager::class, ['arguments' => ['$handlers' => '#handler']]);
* `factory` -- determines if each `get` should create a new service (`true`),
or should one service be reused (`false`, default).
* `resolve` -- instead of using `class` + `arguments` to construct the service,
you can use `resolve` to define how it should be created:
$container->setDefinition('foo', ['resolve' => 4]); // 4
$container->setDefinition('language', ['resolve' => '@Request.locale.language']); // $container->get('Request')->getLocale()->getLanguage()
* `public` -- determines if the service should be accessible directly with `get`,
or can it only be injected into other services.
### ContainerCompiler: autowiring and autoconfiguration
Usually it's obvious, which service should be injected into another.
For instance when your service has a constructor argument `Psr\Cache\CacheItemPoolInterface $cache`,
and the container does have a service named `Psr\Cache\CacheItemPoolInterface`,
then explicitly writing `['arguments' => ['$cache' => '@Psr\Cache\CacheItemPoolInterface']` is redundant.
You can always specify the dependencies manually, then autowiring won't overwrite them.
Autowiring is not magic -- it just follows simple rules to determine,
which service should be injected into the constructor:
* if the argument is a class which is defined in the container, use this service,
* if the argument is a class which is not defined in the container,
try to autowire that class and create a private service out of it,
* if the argument is an array and its name ends with `s` (e.g. `array $helpers`),
inject an array of services with a specific tag (`#helper`).
* if the argument is of type `Bag`, inject the config value with its name
(e.g. `Bag $localisation` -> `@config.localisation`),
* if its name starts with `env`, inject a parameter:
(e.g. `string $envCacheDir` -> `%CACHE_DIR%`)
* if none of the above is true, but there is a default value, just use it,
* if none of the above is true, throw an exception -- this argument should be defined explicitly.
Autoconfiguration is another way to make your life simpler.
For instance, if you're using [Twig](https://twig.symfony.com/),
you might want all the classes in your code that extend `Twig\Extension\AbstractExtension`
to be automatically registered as twig extension.
Autoconfiguration lets you define what default config (tags, public etc.) should be added to them.
To use autowiring and autoconfiguration, run the `ContainerCompiler`:
$container = new Container;
$services = [
'App\' => [
'dir' => '%MODULE_DIR%/src/',
'exclude' => ['#^Entity/#'],
],
'App\Foo' => [
'arguments' => [
'$bar' => 5,
],
],
'App\Bar' => [
'public' => true,
],
];
$autoconfiguration = [
'Twig\Extension\AbstractExtension' => [
'tags' => ['twigExtension'],
],
];
$definitions = new ContainerCompiler(
$container,
$services,
$autoconfiguration
))->compile();
/** @var ServiceDefinition $definition */
foreach ($definitions as $name => $definition) {
if (!$container->has($name)) {
$container->setDefinition($name, $definition);
}
}
In this example, the whole `%MODULE_DIR%/src/` except for (the `/src/Entity` dir) will be scanned for PHP files
and all the found classes will be autowired as private services. If some of them are not used and not public,
they will be removed from the container.
Compiling the container has **no impact on performance** on production environment,
as long as you **cache the result of `compile()`**.
### ServiceLocator
Service locator restricts access to services in the container only to a selected list of names:
$container = new Container();
$container->set('foo', 'abc');
$container->set('bar', 'def');
$container->set('secret', 'XYZ');
$locator = new ServiceLocator($container, ['foo', 'bar']);
$locator->get('foo'); // 'abc'
$locator->get('bar'); // 'def'
$locator->get('secret'); // Exception
### ContainerAssistedBuilder
`ContainerAssistedBuilder` can be used to join together a couple of `ContainerBuilderExtension`s
which encapsulate a set of service definitions that form a library together.
For an example, see [Avris Localisator](https://gitlab.com/Avris/Localisator).
### Micrus
This container was originally built as a part of the [Micrus framework](https://micrus.avris.it).
### Copyright ###
* **Author:** Andre Prusinowski [(Avris.it)](https://avris.it)
* **Licence:** [MIT](https://mit.avris.it)
{
"name": "avris/container",
"type": "library",
"description": "A dependency injection container with autowiring",
"keywords": ["di","dependency injection","container","autowiring"],
"license": "MIT",
"homepage": "https://micrus.avris.it",
"authors": [{
"name": "Avris",
"email": "andre@avris.it",
"homepage": "https://avris.it"
}],
"require": {
"avris/bag": "^4.0",
"symfony/finder": "^3.0|^4.0",
"psr/container": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
"squizlabs/php_codesniffer": "^3.2",
"symfony/var-dumper": "^4.0"
},
"autoload": {
"psr-4": { "Avris\\Container\\": "src" }
},
"autoload-dev": {
"psr-4": {
"Avris\\Container\\": "tests",
"TestProjectContainer\\": "tests/_help/container/src",
"TestProjectCompiler\\": "tests/_help/compiler/src",
"TestProjectExternal\\": "tests/_help/external/src"
}
}
}
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/_bootstrap.php">
<testsuites>
<testsuite name="main">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/_output/coverage"/>
<log type="coverage-clover" target="tests/_output/coverage.xml"/>
<log type="coverage-php" target="tests/_output/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showOnlySummary="true"/>
</logging>
</phpunit>
This diff is collapsed.
<?php
namespace Avris\Container;
class ContainerAssistedBuilder
{
/** @var Container */
protected $container;
public function __construct()
{
$this->container = new Container();
}
public function registerExtension(ContainerBuilderExtension $extension): self
{
$extension->extend($this->container);
return $this;
}
public function build(string $service)
{
return $this->container->get($service);
}
}
<?php
namespace Avris\Container;
interface ContainerBuilderExtension
{
public function extend(ContainerInterface $container);
}
<?php
namespace Avris\Container;
use Avris\Bag\Bag;
use Avris\Container\Exception\AutowiringException;
use Avris\Container\Service\ServiceDefinition;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
final class ContainerCompiler
{
/** @var ContainerInterface */
private $container;
/** @var array */
private $config;
/** @var array */
private $autoconfiguration;
/** @var ServiceDefinition[] */
private $definitions = [];
/** @var string[] */
private $implementations = [];
/** @var AutowiringException[] */
private $autowiringExceptions = [];
public function __construct(
ContainerInterface $container,
array $definitions,
array $autoconfiguration = []
) {
$this->container = $container;
$this->config = $definitions;
$this->autoconfiguration = $autoconfiguration;
}
public function compile(): array
{
$this->loadConfig();
$this->autowireAll();
$this->cleanup();
if (count($this->autowiringExceptions)) {
throw reset($this->autowiringExceptions);
}
return $this->definitions;
}
private function loadConfig()
{
foreach ($this->config as $name => $options) {
if ($options === null) {
unset($this->definitions[$name]);
continue;
}
if (is_string($options)) {
$this->addAlias($name, $options);
continue;
}
$definition = ServiceDefinition::fromArray($options, $name);
if ($definition->getDir()) {
$this->registerDir($name, $definition);
continue;
}
$this->addDefinition($name, $definition);
}
}
private function registerDir(string $namespace, ServiceDefinition $definition)
{
$finder = (new Finder())
->files()
->in($definition->getDir())
->name('*.php')
->ignoreDotFiles(true)
->ignoreUnreadableDirs(true)
->ignoreVCS(true);
foreach ($definition->getExclude() as $notPath) {
$finder->notPath($notPath);
}
/** @var SplFileInfo $file */
foreach ($finder as $file) {
$className = $this->buildClassName($namespace, $file->getRelativePathname());
$reflection = new \ReflectionClass($className);
if (!$reflection->isInterface() && !$reflection->isAbstract()) {
$this->addDefinition($className, $definition->createFromDir($className));
}
}
}
private function buildClassName(string $namespace, string $relativePathName)
{
return $namespace . strtr(substr($relativePathName, 0, -4), ['/' => '\\']);
}
private function addDefinition(string $name, ServiceDefinition $definition)
{
if ($existingDefinition = $this->getDefinition($name)) {
$existingDefinition->merge($definition->toArray(), $name);
} else {
$definition->setClassIfNotGiven($name);
$this->autoconfigure($definition);
$this->definitions[$name] = $definition;
}
if ($definition->getClass()) {
$reflection = new \ReflectionClass($definition->getClass());
foreach ($reflection->getInterfaceNames() as $interface) {
$this->implementations[$interface][] = $definition->getClass();
}
do {
$this->implementations[$reflection->getName()][] = $definition->getClass();
} while ($reflection = $reflection->getParentClass());
}
}
private function addAlias(string $from, string $to)
{
$this->definitions[$from] = $to;
}
private function autoconfigure(ServiceDefinition $definition)
{
if (!$definition->getClass()) {
return;
}
$reflection = new \ReflectionClass($definition->getClass());
foreach ($this->autoconfiguration as $class => $defaultOptions) {
if ($reflection->isSubclassOf($class)) {
$definition->merge($defaultOptions);
}
}
}
private function autowireAll()
{
foreach ($this->definitions as $name => $definition) {
if ($definition instanceof ServiceDefinition) {
$this->autowire($name, $definition);
}
}
}
private function autowire(string $name, ServiceDefinition $definition)
{
if (!$definition->getClass()) {
return;
}
$reflection = new \ReflectionClass($definition->getClass());
$constructor = $reflection->getConstructor();
if (!$constructor) {
return;
}
foreach ($constructor->getParameters() as $parameter) {
if ($definition->hasArgument($parameter->getName())) {
$this->markUsages($definition, $definition->getArgument($parameter->getName()));
continue;
}
try {
$value = $this->autowireParameter($name, $definition, $parameter);
if ($value !== null) {
$definition->setArgument($parameter->getName(), $value);
}
} catch (AutowiringException $e) {
$this->autowiringExceptions[$name] = $e;
}
}
}
private function markUsages(ServiceDefinition $definition, $argument)
{
if (!is_string($argument)) {
return;
}
preg_match_all('#@([A-Za-z0-9\\\\]+)#', $argument, $matches, PREG_SET_ORDER);
foreach ($matches as list(,$used)) {
if (isset($this->definitions[$used])) {
$this->getDefinition($used)->used($definition);
}
}
}
private function autowireParameter(
string $name,
ServiceDefinition $definition,
\ReflectionParameter $parameter
): ?string {
if (!$parameter->getClass()) {
return $this->autowireParameterNotClass($name, $parameter);
}
$className = $parameter->getClass()->getName();
if ($className === Bag::class && substr($parameter->getName(), 0, 6) === 'config') {
return '@config.' . strtr(lcfirst(substr($parameter->getName(), 6)), ['_' => '.']);
}
if ($dependencyDefinition = $this->getDefinition($className)) {
$dependencyDefinition->used($definition);
return '@' . $className;
}
if ($this->container->has($className)) {
return '@' . $className;
}
if ($parameter->isOptional()) {
return null;
}
$implementations = $this->implementations[$className] ?? [];
if (count($implementations) === 0
&& !$parameter->getClass()->isInterface()
&& !$parameter->getClass()->isAbstract()
) {
$dependencyDefinition = $this->buildAutowiredService($className);
$dependencyDefinition->used($definition);
return '@' . $className;
}
throw new AutowiringException(sprintf(
'Cannot autowire parameter $%s of service %s. ' .
'Either wire the value manually or create an alias (candidates: %s)',
$parameter->getName(),
$name, // @codeCoverageIgnore
join(', ', $implementations)
));
}
private function autowireParameterNotClass(string $name, \ReflectionParameter $parameter)
{
if ($parameter->getType()
&& $parameter->getType()->getName() === 'array'
&& substr($parameter->getName(), -1) === 's'
) {
return '#' . substr($parameter->getName(), 0, -1);
}
if (preg_match('#^env([A-Z][A-Za-z]*)$#Uu', $parameter->getName(), $matches)) {
return '%' . strtoupper(preg_replace('#([^\\^])([A-Z])#Uu', '$1_$2', $matches[1])) . '%';
}
if ($parameter->isDefaultValueAvailable()) {
return null;
}
throw new AutowiringException(sprintf(
'Cannot autowire parameter $%s of service %s',
$parameter->getName(),
$name
));
}
private function getDefinition(string $name): ?ServiceDefinition
{
do {
$definition = $this->definitions[$name] ?? null;
if ($definition === null || $definition instanceof ServiceDefinition) {
return $definition;
}
} while ($name = $definition);
} // @codeCoverageIgnore
private function buildAutowiredService(string $className): ServiceDefinition
{
$definition = ServiceDefinition::fromArray([]);
$this->addDefinition($className, $definition);
$this->autowire($className, $definition);
return $definition;
}
private function cleanup()
{
foreach ($this->definitions as $name => $definition) {
if ($definition instanceof ServiceDefinition) {
if (!$definition->shouldStay()) {
unset($this->definitions[$name]);
unset($this->autowiringExceptions[$name]);
}
}
}
foreach ($this->definitions as $name => $definition) {
if ($this->getDefinition($name) === null) {
$this->removeAliases($name);
}
}
}
private function removeAliases(string $name)
{
do {
$definition = $this->definitions[$name] ?? null;
unset($this->definitions[$name]);
} while ($name = $definition);
}
}
<?php
namespace Avris\Container;
interface ContainerInterface extends \Psr\Container\ContainerInterface
{
public function set(string $name, $service, array $tags = []): self;
public function setDefinition(string $name, $definition): self;
public function setWithAlias($service, string $alias): self;
public function has($name): bool;
public function get($name);
public function getByTag(string $tag): array;
public function restart(string $serviceName): self;
public function delete(string $serviceName): self;
public function clear(): self;
public function getParameter(string $name);
}
<?php
namespace Avris\Container\Exception;
class AutowiringException extends ContainerException
{
}
<?php
namespace Avris\Container\Exception;
use Psr\Container\ContainerExceptionInterface;
class ContainerException extends \Exception implements ContainerExceptionInterface
{
}
<?php
namespace Avris\Container\Exception;
use Psr\Container\NotFoundExceptionInterface;
class NotFoundException extends ContainerException implements NotFoundExceptionInterface
{
}
<?php
namespace Avris\Container\Parameters;
interface ParameterProvider
{
public function replaceParameters(string $content, $context = null): string;
public function getParameter(string $name, $context = null);
}
<?php
namespace Avris\Container\Parameters;
use Avris\Container\Exception\NotFoundException;
class SimpleParameterProvider implements ParameterProvider
{
/** @var string[] */
protected $parameters;
public function __construct(array $parameters = [])
{
$this->parameters = $parameters;
}
public function replaceParameters(string $content, $context = null): string
{
return preg_replace_callback('#%([A-Za-z0-9_]+)%#', function ($matches) {
return $this->getParameter($matches[1]);
}, $content);
}
public function getParameter(string $name, $context = null)
{
if (!isset($this->parameters[$name])) {
throw new NotFoundException(sprintf('Undefined parameter "%s"', $name));
}
return $this->parameters[$name];
}
}