Skip to content

Commit

Permalink
Merge pull request #1 from kodedphp/refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
kodeart authored Jun 5, 2020
2 parents 6aee2bd + 912843e commit 11f44b9
Show file tree
Hide file tree
Showing 28 changed files with 480 additions and 252 deletions.
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@
/phpunit.* export-ignore
/infection.* export-ignore
/phpbench.* export-ignore
/.* export-ignore
16 changes: 9 additions & 7 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ build:
- php-scrutinizer-run
environment:
php:
version: '7.2'
dependencies:
override:
- composer install --no-interaction --prefer-source
version: '7.3'

before_commands:
- 'composer update -o --prefer-source --no-interaction'

filter:
excluded_paths:
- 'Tests/'
- 'vendor/'
- 'build/*'
- 'vendor/*'
- 'Tests/*'

tools:
php_analyzer: true
external_code_coverage: true
php_analyzer: true
php_code_sniffer: true
21 changes: 12 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
sudo: false
language: php
os: linux
dist: xenial

notifications:
email: false
Expand All @@ -11,19 +12,21 @@ cache:
php:
- 7.2
- 7.3
- 7.4snapshot
- 7.4
- nightly

matrix:
jobs:
fast_finish: true
allow_failures:
- php: 7.4snapshot
- php: nightly

install:
- travis_retry composer update -o --no-interaction --prefer-source
- composer update -o --no-interaction --prefer-source
- wget https://scrutinizer-ci.com/ocular.phar

script:
- vendor/bin/phpunit --coverage-clover=build/coverage/clover.xml
- vendor/bin/phpunit --coverage-clover=build/clover.xml

after_success:
- travis_retry vendor/bin/ocular code-coverage:upload --format=php-clover build/coverage/clover.xml
- travis_retry vendor/bin/infection --threads=4 --min-msi=80 --min-covered-msi=80 --log-verbosity=none
after_script:
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
- vendor/bin/infection --threads=4 --min-msi=77 --min-covered-msi=77 --log-verbosity=none
176 changes: 118 additions & 58 deletions DIContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,74 @@

namespace Koded;

use Psr\Container\ContainerInterface;
use Throwable;
use Psr\Container\{ContainerExceptionInterface, ContainerInterface};

/**
* Interface DIModule contributes the application configuration,
* typically the interface binding which are used to inject the dependencies.
*
* The application is composed of a set of DIModules and some bootstrapping code.
*/
interface DIModule
{
public function configure(DIContainer $injector): void;
/**
* Provides bindings and other configurations for this app module.
* Also reduces the repetition and results in a more readable configuration.
* Implement the `configure()` method to bind your interfaces.
*
* ex: `$container->bind(MyInterface::class, MyImplementation::class);`
*
* @param DIContainer $container
*/
public function configure(DIContainer $container): void;
}

/**
* The entry point of the DIContainer that draws the lines between the
* APIs, implementation of these APIs, modules that configure these
* implementations and applications that consist of a collection of modules.
*
* ```
* $container = new DIContainer(new ModuleA, new ModuleB, ... new ModuleZ);
* ($container)([AppEntry::class, 'method']);
* ```
*/
final class DIContainer implements ContainerInterface
{
public const SINGLETONS = 'singletons';
public const BINDINGS = 'bindings';
public const EXCLUDE = 'exclude';
public const NAMED = 'named';

private $reflection;
private $inProgress = [];

private $singletons = [];
private $bindings = [];
private $exclude = [];
private $named = [];

private $interfaces = [];

public function __construct(DIModule ...$modules)
{
$this->reflection = new DIReflector;
$this->interfaces = array_filter(get_declared_interfaces(), function(string $name) {
return false === strpos($name, '\\');
});
$this->interfaces = array_flip($this->interfaces);

foreach ((array)$modules as $module) {
$module->configure($this);
}
}

public function __clone()
{
throw DIException::forCloningNotAllowed();
$this->inProgress = [];
$this->singletons = [];
$this->named = [];
}

public function __destruct()
{
$this->reflection = null;

$this->singletons = [];
$this->interfaces = [];
$this->bindings = [];
$this->exclude = [];
$this->named = [];
}

Expand All @@ -70,14 +90,20 @@ public function __invoke(callable $callable, array $arguments = [])
));
}

public function inject(string $class, array $arguments = []): ?object
/**
* Creates a new instance of a class. Builds the graph of objects that make up the application.
* It can also inject already created dependencies behind the scene (with singleton and share).
*
* @param string $class FQCN
* @param array $arguments [optional] The arguments for the class constructor.
* They have top precedence over the shared dependencies
*
* @return object|callable|null
* @throws ContainerExceptionInterface
*/
public function new(string $class, array $arguments = []): ?object
{
$binding = $this->getFromBindings($class);

if (isset($this->singletons[$binding])) {
return $this->singletons[$binding];
}

$binding = $this->getNameFromBindings($class);
if (isset($this->inProgress[$binding])) {
throw DIException::forCircularDependency($binding);
}
Expand All @@ -90,43 +116,89 @@ public function inject(string $class, array $arguments = []): ?object
}
}

/**
* Create once and share an object throughout the application lifecycle.
* Internally the object is immutable, but it can be replaced with share() method.
*
* @param string $class FQCN
* @param array $arguments [optional] See new() description
*
* @return object
*/
public function singleton(string $class, array $arguments = []): object
{
return $this->singletons[$class] = $this->inject($class, $arguments);
$binding = $this->getNameFromBindings($class);
if (isset($this->singletons[$binding])) {
return $this->singletons[$binding];
}
return $this->singletons[$class] = $this->new($class, $arguments);
}

public function share(object $instance): DIContainer
/**
* Share already created instance of an object throughout the app lifecycle.
*
* @param object $instance The object that will be shared as dependency
* @param array $exclude [optional] A list of FQCNs that should
* be excluded from injecting this instance.
* In this case, a new object will be created and
* injected for these classes
*
* @return DIContainer
*/
public function share(object $instance, array $exclude = []): DIContainer
{
$class = get_class($instance);
$this->mapInterfaces($class, $class);
$this->bindInterfaces($instance, $class);

$this->singletons[$class] = $instance;
$this->bindings[$class] = $class;

foreach ($exclude as $name) {
$this->exclude[$name][$class] = $class;
}
return $this;
}

public function bind(string $interface, string $class): DIContainer
/**
* Binds the interface to concrete class implementation.
* It does not create objects, but prepares the container for dependency injection.
*
* This method should be used in the app modules (DIModule).
*
* @param string $interface FQN of the interface
* @param string $class FQCN of the concrete class implementation,
* or empty value for deferred binding
*
* @return DIContainer
*/
public function bind(string $interface, string $class = ''): DIContainer
{
$this->assertEmpty($class, 'class');
$this->assertEmpty($interface, 'interface');
assert(false === empty($interface), 'Dependency name for bind() method');

if ('$' === $class[0]) {
if ('$' === ($class[0] ?? null)) {
$this->bindings[$interface] = $interface;
$this->bindings[$class] = $interface;
$class && $this->bindings[$class] = $interface;
} else {
$this->bindings[$interface] = $class;
$this->bindings[$class] = $class;
$this->bindings[$interface] = $class ?: $interface;
$class && $this->bindings[$class] = $class;
}

return $this;
}

/**
* Shares an object globally by argument name.
*
* @param string $name The name of the argument
* @param mixed $value The actual value
*
* @return DIContainer
*/
public function named(string $name, $value): DIContainer
{
if (1 !== preg_match('/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name)) {
throw DIException::forInvalidParameterName();
throw DIException::forInvalidParameterName($name);
}
$this->named[$name] = $value;

return $this;
}

Expand All @@ -138,6 +210,7 @@ public function getStorage(): array
return [
self::SINGLETONS => $this->singletons,
self::BINDINGS => $this->bindings,
self::EXCLUDE => $this->exclude,
self::NAMED => $this->named,
];
}
Expand All @@ -147,8 +220,7 @@ public function getStorage(): array
*/
public function has($id): bool
{
$this->assertEmpty($id, 'dependency');

assert(false === empty($id), 'Dependency name for has() method');
return isset($this->bindings[$id]) || isset($this->named[$id]);
}

Expand All @@ -161,42 +233,30 @@ public function get($id)
throw DIInstanceNotFound::for($id);
}

$dependency = $this->getFromBindings($id);

$dependency = $this->getNameFromBindings($id);
return $this->singletons[$dependency]
?? $this->named[$dependency]
?? $this->inject($dependency);
?? $this->new($dependency);
}

private function newInstance(string $class, array $arguments): object
{
try {
$this->bindings[$class] = $class;
return $this->reflection->newInstance($this, $class, $arguments);
} catch (Throwable $e) {
throw $e;
}
$this->bindings[$class] = $class;
return $this->reflection->newInstance($this, $class, $arguments);
}

private function getFromBindings(string $dependency): string
private function getNameFromBindings(string $dependency): string
{
$this->assertEmpty($dependency, 'class/interface');

assert(false === empty($dependency), 'Dependency name for class/interface');
return $this->bindings[$dependency] ?? $dependency;
}

private function assertEmpty(string $value, string $type): void
{
if (empty($value)) {
throw DIException::forEmptyName($type);
}
}

private function mapInterfaces(string $dependency, string $class): void
private function bindInterfaces(object $dependency, string $class): void
{
foreach ((@class_implements($dependency, false) ?: []) as $implements) {
if (false === isset($this->interfaces[$implements])) {
$this->bindings[$implements] = $class;
foreach (class_implements($dependency) as $interface) {
if (isset($this->bindings[$interface])) {
$this->bindings[$interface] = $class;
break;
}
}
}
Expand Down
Loading

0 comments on commit 11f44b9

Please sign in to comment.