From 0d00ee69b84ac72e71507fed93b14f0d910a7d3d Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Mon, 16 Sep 2019 10:37:01 +0200 Subject: [PATCH 01/37] Refactor for interface mapping --- DIContainer.php | 35 +++++---------------- DIReflector.php | 16 +++++++++- Tests/PhpBench/WithModulesBench.php | 12 ++++---- Tests/PhpBench/WithoutModulesBench.php | 12 ++++---- Tests/Unit/InjectObjectTest.php | 4 +-- Tests/Unit/InterfaceMappingTest.php | 42 ++++++++++++++++++++++++++ Tests/Unit/Psr11Test.php | 8 ++--- Tests/Unit/ShareMethodTest.php | 23 ++++---------- Tests/Unit/SingletonMethodTest.php | 7 +++-- Tests/Unit/fixtures.php | 11 +++++-- composer.json | 2 ++ 11 files changed, 103 insertions(+), 69 deletions(-) create mode 100644 Tests/Unit/InterfaceMappingTest.php diff --git a/DIContainer.php b/DIContainer.php index 636dfd4..17770b2 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -33,16 +33,9 @@ final class DIContainer implements ContainerInterface private $bindings = []; 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); } @@ -58,7 +51,6 @@ public function __destruct() $this->reflection = null; $this->singletons = []; - $this->interfaces = []; $this->bindings = []; $this->named = []; } @@ -74,10 +66,6 @@ public function inject(string $class, array $arguments = []): ?object { $binding = $this->getFromBindings($class); - if (isset($this->singletons[$binding])) { - return $this->singletons[$binding]; - } - if (isset($this->inProgress[$binding])) { throw DIException::forCircularDependency($binding); } @@ -92,14 +80,18 @@ public function inject(string $class, array $arguments = []): ?object public function singleton(string $class, array $arguments = []): object { + $binding = $this->getFromBindings($class); + + if (isset($this->singletons[$binding])) { + return $this->singletons[$binding]; + } + return $this->singletons[$class] = $this->inject($class, $arguments); } public function share(object $instance): DIContainer { - $class = get_class($instance); - $this->mapInterfaces($class, $class); - $this->singletons[$class] = $instance; + $this->singletons[get_class($instance)] = $instance; return $this; } @@ -126,7 +118,6 @@ public function named(string $name, $value): DIContainer throw DIException::forInvalidParameterName(); } $this->named[$name] = $value; - return $this; } @@ -148,7 +139,6 @@ public function getStorage(): array public function has($id): bool { $this->assertEmpty($id, 'dependency'); - return isset($this->bindings[$id]) || isset($this->named[$id]); } @@ -162,7 +152,6 @@ public function get($id) } $dependency = $this->getFromBindings($id); - return $this->singletons[$dependency] ?? $this->named[$dependency] ?? $this->inject($dependency); @@ -181,7 +170,6 @@ private function newInstance(string $class, array $arguments): object private function getFromBindings(string $dependency): string { $this->assertEmpty($dependency, 'class/interface'); - return $this->bindings[$dependency] ?? $dependency; } @@ -191,13 +179,4 @@ private function assertEmpty(string $value, string $type): void throw DIException::forEmptyName($type); } } - - private function mapInterfaces(string $dependency, string $class): void - { - foreach ((@class_implements($dependency, false) ?: []) as $implements) { - if (false === isset($this->interfaces[$implements])) { - $this->bindings[$implements] = $class; - } - } - } } diff --git a/DIReflector.php b/DIReflector.php index d70750b..1130f25 100644 --- a/DIReflector.php +++ b/DIReflector.php @@ -12,6 +12,7 @@ namespace Koded; +use Closure; use ReflectionClass; use ReflectionFunction; use ReflectionFunctionAbstract; @@ -43,6 +44,13 @@ public function newInstance(DIContainer $container, string $class, array $argume return new $class(...$this->processMethodArguments($container, $constructor, $arguments)); } + /** + * @param DIContainer $container + * @param ReflectionFunction | ReflectionMethod $method + * @param array $arguments + * + * @return array + */ public function processMethodArguments( DIContainer $container, ReflectionFunctionAbstract $method, @@ -72,6 +80,12 @@ public function processMethodArguments( return $args; } + /** + * @param callable $callable + * + * @return ReflectionMethod | ReflectionFunction + * @throws \ReflectionException + */ public function newMethodFromCallable(callable $callable): ReflectionFunctionAbstract { switch (gettype($callable)) { @@ -79,7 +93,7 @@ public function newMethodFromCallable(callable $callable): ReflectionFunctionAbs return new ReflectionMethod(...$callable); case 'object'; - if ($callable instanceof \Closure) { + if ($callable instanceof Closure) { return new ReflectionFunction($callable); } diff --git a/Tests/PhpBench/WithModulesBench.php b/Tests/PhpBench/WithModulesBench.php index 028baae..6e1544b 100644 --- a/Tests/PhpBench/WithModulesBench.php +++ b/Tests/PhpBench/WithModulesBench.php @@ -4,7 +4,7 @@ use Koded\{DIContainer, DIModule}; use Koded\Tests\Unit\{TestClassWithInterfaceAndNoConstructor, - TestClassWithInterfaceDependency, + TestClassWithConstructorInterfaceDependency, TestInterface, TestOtherInterface}; @@ -17,7 +17,7 @@ class WithModulesBench extends AbstractBench */ public function benchInject() { - $this->di->inject(TestClassWithInterfaceDependency::class); + $this->di->inject(TestClassWithConstructorInterfaceDependency::class); } /** @@ -26,7 +26,7 @@ public function benchInject() */ public function benchSingleton() { - $this->di->singleton(TestClassWithInterfaceDependency::class); + $this->di->singleton(TestClassWithConstructorInterfaceDependency::class); } /** @@ -35,8 +35,8 @@ public function benchSingleton() */ public function benchPsr11() { - $this->di->singleton(TestClassWithInterfaceDependency::class); - $this->di->get(TestClassWithInterfaceDependency::class); + $this->di->singleton(TestClassWithConstructorInterfaceDependency::class); + $this->di->get(TestClassWithConstructorInterfaceDependency::class); } /** @@ -55,7 +55,7 @@ protected function modules(...$modules) { public function configure(DIContainer $injector): void { - $injector->bind(TestOtherInterface::class, TestClassWithInterfaceDependency::class); + $injector->bind(TestOtherInterface::class, TestClassWithConstructorInterfaceDependency::class); } }, diff --git a/Tests/PhpBench/WithoutModulesBench.php b/Tests/PhpBench/WithoutModulesBench.php index c18b04f..b69d266 100644 --- a/Tests/PhpBench/WithoutModulesBench.php +++ b/Tests/PhpBench/WithoutModulesBench.php @@ -3,7 +3,7 @@ namespace Koded\Tests\PhpBench; use Koded\Tests\Unit\{TestClassWithInterfaceAndNoConstructor, - TestClassWithInterfaceDependency, + TestClassWithConstructorInterfaceDependency, TestInterface, TestOtherInterface}; @@ -17,7 +17,7 @@ class WithoutModulesBench extends AbstractBench public function benchInject() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $this->di->inject(TestClassWithInterfaceDependency::class); + $this->di->inject(TestClassWithConstructorInterfaceDependency::class); } /** @@ -27,7 +27,7 @@ public function benchInject() public function benchSingleton() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $this->di->singleton(TestClassWithInterfaceDependency::class); + $this->di->singleton(TestClassWithConstructorInterfaceDependency::class); } /** @@ -37,8 +37,8 @@ public function benchSingleton() public function benchPsr11() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $this->di->singleton(TestClassWithInterfaceDependency::class); - $this->di->get(TestClassWithInterfaceDependency::class); + $this->di->singleton(TestClassWithConstructorInterfaceDependency::class); + $this->di->get(TestClassWithConstructorInterfaceDependency::class); } /** @@ -48,7 +48,7 @@ public function benchPsr11() public function benchBind() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $this->di->bind(TestOtherInterface::class, TestClassWithInterfaceDependency::class); + $this->di->bind(TestOtherInterface::class, TestClassWithConstructorInterfaceDependency::class); } /** diff --git a/Tests/Unit/InjectObjectTest.php b/Tests/Unit/InjectObjectTest.php index dd7d11c..1442dde 100644 --- a/Tests/Unit/InjectObjectTest.php +++ b/Tests/Unit/InjectObjectTest.php @@ -36,13 +36,13 @@ public function testChildClassWithInterfaceWithoutMapping() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); - $this->di->inject(TestClassWithInterfaceDependency::class); + $this->di->inject(TestClassWithConstructorInterfaceDependency::class); } public function testChildClassWithInterfaceWithMapping() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $instance = $this->di->inject(TestClassWithInterfaceDependency::class); + $instance = $this->di->inject(TestClassWithConstructorInterfaceDependency::class); $this->assertInstanceOf(TestClassWithInterfaceAndNoConstructor::class, $instance->getDependency()); } diff --git a/Tests/Unit/InterfaceMappingTest.php b/Tests/Unit/InterfaceMappingTest.php new file mode 100644 index 0000000..74cc2f0 --- /dev/null +++ b/Tests/Unit/InterfaceMappingTest.php @@ -0,0 +1,42 @@ +di->inject(TestClassWithConstructorArguments::class, [new PDO('sqlite:')]); + $this->di->share($shared); + + $this->assertFalse($this->di->has(JsonSerializable::class), 'JsonSerializable interface is not mapped to the class'); + $this->assertFalse($this->di->has(Countable::class), 'Countable interface is not mapped to the class'); + } + + public function testInterfacesFromParent() + { + $shared = $this->di->inject(TestClassWithoutConstructorArguments::class); + $this->di->share($shared); + + $this->assertFalse( + $this->di->has(ArrayDataFilter::class), + 'TestClassWithoutConstructorArguments extends Config, which also implements ArrayDataFilter interface, + therefore the parent interfaces are NOT bound to this class instance' + ); + } + + protected function createContainer(): DIContainer + { + return new DIContainer; + } +} diff --git a/Tests/Unit/Psr11Test.php b/Tests/Unit/Psr11Test.php index 9a2b548..92325be 100644 --- a/Tests/Unit/Psr11Test.php +++ b/Tests/Unit/Psr11Test.php @@ -8,8 +8,8 @@ class Psr11Test extends DITestCase { public function testGetMethodForInjectedDependency() { - $instance = $this->di->get(TestClassWithInterfaceDependency::class); - $this->assertInstanceOf(TestClassWithInterfaceDependency::class, $instance); + $instance = $this->di->get(TestClassWithConstructorInterfaceDependency::class); + $this->assertInstanceOf(TestClassWithConstructorInterfaceDependency::class, $instance); $this->assertInstanceOf(TestClassWithInterfaceAndNoConstructor::class, $instance->getDependency()); } @@ -17,7 +17,7 @@ public function testHasMethod() { $this->assertFalse($this->di->has('Fubar')); $this->assertTrue($this->di->has(TestInterface::class)); - $this->assertTrue($this->di->has(TestClassWithInterfaceDependency::class)); + $this->assertTrue($this->di->has(TestClassWithConstructorInterfaceDependency::class)); } public function testNamedDependency() @@ -34,7 +34,7 @@ protected function createContainer(): DIContainer public function configure(DIContainer $injector): void { $injector->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $injector->singleton(TestClassWithInterfaceDependency::class); + $injector->singleton(TestClassWithConstructorInterfaceDependency::class); } }); } diff --git a/Tests/Unit/ShareMethodTest.php b/Tests/Unit/ShareMethodTest.php index 0436cae..d2eebbb 100644 --- a/Tests/Unit/ShareMethodTest.php +++ b/Tests/Unit/ShareMethodTest.php @@ -3,30 +3,19 @@ namespace Koded\Tests\Unit; use Koded\{DIContainer, DIModule}; -use Koded\Stdlib\Interfaces\ArrayDataFilter; class ShareMethodTest extends DITestCase { public function testImmutability() { - $actual = $this->di->get(TestClassWithInterfaceDependency::class); - $this->assertInstanceOf(TestClassWithInterfaceDependency::class, $actual); + $actual = $this->di->get(TestClassWithConstructorInterfaceDependency::class); + $this->assertInstanceOf(TestClassWithConstructorInterfaceDependency::class, $actual); - $new = new TestClassWithInterfaceDependency(new TestClassWithInterfaceAndNoConstructor); + $new = $this->di->inject(TestClassWithConstructorInterfaceDependency::class); $this->di->share($new); - $this->assertNotSame($new, $actual); - } - - public function testImplementedInterfaces() - { - $shared = $this->di->inject(TestClassWithoutConstructorArguments::class); - $this->di->share($shared); - - $this->assertTrue( - $this->di->has(ArrayDataFilter::class), - 'TestClassWithoutConstructorArguments extends Config, which also implements ArrayDataFilter interface, - therefore the parent interfaces are bound to this class instance' + $this->assertNotSame($new, $actual, + 'When instance is shared, the existing shared instance is replaced with the new created' ); } @@ -37,7 +26,7 @@ protected function createContainer(): DIContainer public function configure(DIContainer $injector): void { $injector->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $injector->singleton(TestClassWithInterfaceDependency::class); + $injector->singleton(TestClassWithConstructorInterfaceDependency::class); } }); } diff --git a/Tests/Unit/SingletonMethodTest.php b/Tests/Unit/SingletonMethodTest.php index 528b577..8236f85 100644 --- a/Tests/Unit/SingletonMethodTest.php +++ b/Tests/Unit/SingletonMethodTest.php @@ -16,10 +16,13 @@ public function testSingletonCreateWithoutBinding() public function testSingletonCreateWithInjectMethod() { +// $this->markTestIncomplete('Clarify the behavior of inject()'); $singleton = $this->di->singleton(TestClassWithInterfaceAndNoConstructor::class); $other = $this->di->inject(TestClassWithInterfaceAndNoConstructor::class); - $this->assertSame($singleton, $other); + $this->assertNotSame($singleton, $other, + 'inject() method always creates a new instance even if that class exists as singleton' + ); } public function testSingletonInstance() @@ -39,4 +42,4 @@ protected function createContainer(): DIContainer { return new DIContainer; } -} \ No newline at end of file +} diff --git a/Tests/Unit/fixtures.php b/Tests/Unit/fixtures.php index 213509b..826bc6c 100644 --- a/Tests/Unit/fixtures.php +++ b/Tests/Unit/fixtures.php @@ -2,10 +2,11 @@ namespace Koded\Tests\Unit; +use Countable; +use Exception; use JsonSerializable; use Koded\Stdlib\Config; use PDO; -use Exception; interface PostRepository { @@ -125,7 +126,7 @@ public function get($value) } } -class TestClassWithConstructorArguments implements JsonSerializable +class TestClassWithConstructorArguments implements JsonSerializable, Countable { public function __construct(PDO $pdo) { @@ -134,6 +135,10 @@ public function __construct(PDO $pdo) public function jsonSerialize() { } + + public function count() + { + } } class TestClassWithoutConstructorArguments extends Config @@ -144,7 +149,7 @@ class TestClassWithInterfaceAndNoConstructor implements TestInterface { } -class TestClassWithInterfaceDependency +class TestClassWithConstructorInterfaceDependency { private $dependency; diff --git a/composer.json b/composer.json index 02739d8..7c3727b 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,8 @@ } }, "require-dev": { + "ext-pdo": "*", + "ext-json": "*",, "phpunit/phpunit": "~7", "infection/infection": "^0.13", "phpbench/phpbench": "@dev", From 8bb99cf62f676e6063d6d74715244a6983c60536 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Mon, 16 Sep 2019 11:01:51 +0200 Subject: [PATCH 02/37] Typo --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7c3727b..c9c8b4b 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "require-dev": { "ext-pdo": "*", - "ext-json": "*",, + "ext-json": "*", "phpunit/phpunit": "~7", "infection/infection": "^0.13", "phpbench/phpbench": "@dev", From 32e18582a1695be9070d44cd343dfaa8a38906dc Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 18 Sep 2019 09:01:57 +0200 Subject: [PATCH 03/37] - share() method can exclude injections in specific classes - container cloning is allowed - removed assertion and exception for class/interface/identifiers names - PHPdocs added --- DIContainer.php | 124 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 101 insertions(+), 23 deletions(-) diff --git a/DIContainer.php b/DIContainer.php index 17770b2..2a4a7b0 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -12,18 +12,44 @@ namespace Koded; -use Psr\Container\ContainerInterface; +use Psr\Container\{ContainerExceptionInterface, ContainerInterface}; use Throwable; +/** + * 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 DIModule(s) and some bootstrapping code. + */ interface DIModule { + /** + * 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: `$injector->bind(MyInterface::class, MyImplementation::class);` + * + * @param DIContainer $injector + */ public function configure(DIContainer $injector): void; } +/** + * The entry point of the DIContainer that dwars the lines between the + * APIs, implementation of these APIs, modules that configure these + * implementations and applications that consist of 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; @@ -31,6 +57,7 @@ final class DIContainer implements ContainerInterface private $singletons = []; private $bindings = []; + private $exclude = []; private $named = []; public function __construct(DIModule ...$modules) @@ -43,7 +70,9 @@ public function __construct(DIModule ...$modules) public function __clone() { - throw DIException::forCloningNotAllowed(); + $this->inProgress = []; + $this->singletons = []; + $this->named = []; } public function __destruct() @@ -52,20 +81,35 @@ public function __destruct() $this->singletons = []; $this->bindings = []; + $this->exclude = []; $this->named = []; } public function __invoke(callable $callable, array $arguments = []) { - return call_user_func_array($callable, $this->reflection->processMethodArguments( - $this, $this->reflection->newMethodFromCallable($callable), $arguments - )); + try { + return call_user_func_array($callable, $this->reflection->processMethodArguments( + $this, $this->reflection->newMethodFromCallable($callable), $arguments + )); + } catch (Throwable $e) { + throw DIException::from($e); + } } + /** + * 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 scenes (ex. 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 inject(string $class, array $arguments = []): ?object { $binding = $this->getFromBindings($class); - if (isset($this->inProgress[$binding])) { throw DIException::forCircularDependency($binding); } @@ -78,28 +122,61 @@ 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 inject() description + * + * @return object + */ public function singleton(string $class, array $arguments = []): object { $binding = $this->getFromBindings($class); - if (isset($this->singletons[$binding])) { return $this->singletons[$binding]; } - return $this->singletons[$class] = $this->inject($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 $excludedClasses [optional] A list of FQCN 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 $excludedClasses = []): DIContainer { - $this->singletons[get_class($instance)] = $instance; + $class = get_class($instance); + $this->singletons[$class] = $instance; + foreach ($excludedClasses as $exclude) { + $this->exclude[$exclude][$class] = $class; + } return $this; } + /** + * 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 + * + * @return DIContainer + */ public function bind(string $interface, string $class): DIContainer { - $this->assertEmpty($class, 'class'); - $this->assertEmpty($interface, 'interface'); + assert(false === empty($class), 'Dependency name for bind() method'); + assert(false === empty($class), 'Class name for bind() method'); if ('$' === $class[0]) { $this->bindings[$interface] = $interface; @@ -108,10 +185,17 @@ public function bind(string $interface, string $class): DIContainer $this->bindings[$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)) { @@ -129,6 +213,7 @@ public function getStorage(): array return [ self::SINGLETONS => $this->singletons, self::BINDINGS => $this->bindings, + self::EXCLUDE => $this->exclude, self::NAMED => $this->named, ]; } @@ -138,7 +223,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]); } @@ -163,20 +248,13 @@ private function newInstance(string $class, array $arguments): object $this->bindings[$class] = $class; return $this->reflection->newInstance($this, $class, $arguments); } catch (Throwable $e) { - throw $e; + throw DIException::from($e); } } private function getFromBindings(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); - } - } } From bea617ff0d366ea629e596cd94d8090427d01ef3 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 18 Sep 2019 09:03:12 +0200 Subject: [PATCH 04/37] - removed exception for cloning - added exception for missing method argument --- DIException.php | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/DIException.php b/DIException.php index bf977d1..a1083b1 100644 --- a/DIException.php +++ b/DIException.php @@ -17,22 +17,21 @@ class DIException extends KodedException implements ContainerExceptionInterface { - const E_CIRCULAR_DEPENDENCY = 1; - const E_NON_PUBLIC_METHOD = 2; - const E_CANNOT_INSTANTIATE = 3; - const E_EMPTY_NAME = 4; - const E_INVALID_PARAMETER_NAME = 5; - const E_INSTANCE_NOT_FOUND = 6; - const E_CLONING_NOT_ALLOWED = 7; + public const + E_CIRCULAR_DEPENDENCY = 1, + E_NON_PUBLIC_METHOD = 2, + E_CANNOT_INSTANTIATE = 3, + E_INVALID_PARAMETER_NAME = 4, + E_INSTANCE_NOT_FOUND = 5, + E_MISSING_ARGUMENT = 6; protected $messages = [ self::E_CIRCULAR_DEPENDENCY => 'Circular dependency detected while creating an instance for ":class"', self::E_NON_PUBLIC_METHOD => 'Failed to create an instance, because the method ":method" is not public', self::E_CANNOT_INSTANTIATE => 'Cannot instantiate the ":type" :class', - self::E_EMPTY_NAME => 'Empty :type name. Provide a valid FQCN', self::E_INVALID_PARAMETER_NAME => 'Provide a valid name for the global parameter', self::E_INSTANCE_NOT_FOUND => 'The requested instance :id is not found in the container', - self::E_CLONING_NOT_ALLOWED => 'Cloning the DIContainer is not allowed', + self::E_MISSING_ARGUMENT => 'Required parameter "$:name" is missing at position :position in :function()', ]; @@ -51,19 +50,18 @@ public static function cannotInstantiate(string $class, string $type): Container return new self(self::E_CANNOT_INSTANTIATE, [':class' => $class, ':type' => $type]); } - public static function forEmptyName(string $type): ContainerExceptionInterface - { - return new self(self::E_EMPTY_NAME, [':type' => $type]); - } - public static function forInvalidParameterName(): ContainerExceptionInterface { return new self(self::E_INVALID_PARAMETER_NAME); } - public static function forCloningNotAllowed(): ContainerExceptionInterface + public static function forMissingArgument(string $name, int $position, string $function): ContainerExceptionInterface { - return new self(self::E_CLONING_NOT_ALLOWED); + return new self(self::E_MISSING_ARGUMENT, [ + ':name' => $name, + ':position' => $position, + ':function' => $function, + ]); } } From 54c665742b1479d05349040d64202a40128befe1 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 18 Sep 2019 09:06:41 +0200 Subject: [PATCH 05/37] - added exclude mechanism for dependencies injections - throws exception if the method argument has built-in type and is unable to resolve the argument --- DIReflector.php | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/DIReflector.php b/DIReflector.php index 1130f25..c09a3e8 100644 --- a/DIReflector.php +++ b/DIReflector.php @@ -14,6 +14,7 @@ use Closure; use ReflectionClass; +use ReflectionException; use ReflectionFunction; use ReflectionFunctionAbstract; use ReflectionMethod; @@ -62,19 +63,19 @@ public function processMethodArguments( $name = $method->getNamespaceName() ?: $method->getName(); } - $args = $arguments + $method->getParameters(); // TODO use args positions? + $args = array_replace($method->getParameters(), $arguments); + + // PHP quirks... + + if ($name === \ArrayObject::class) { + $args[2] = \ArrayIterator::class; + } foreach ($args as $i => $param) { if (!$param instanceof ReflectionParameter) { continue; } - $args[$i] = $this->getFromParameterType($container, $param, $arguments); - } - - // PHP quirks... - - if ($name === 'ArrayObject' && null === $args[2]) { - $args[2] = 'ArrayIterator'; + $args[$i] = $this->getFromParameterType($container, $param); } return $args; @@ -84,7 +85,7 @@ public function processMethodArguments( * @param callable $callable * * @return ReflectionMethod | ReflectionFunction - * @throws \ReflectionException + * @throws ReflectionException */ public function newMethodFromCallable(callable $callable): ReflectionFunctionAbstract { @@ -96,7 +97,6 @@ public function newMethodFromCallable(callable $callable): ReflectionFunctionAbs if ($callable instanceof Closure) { return new ReflectionFunction($callable); } - return (new ReflectionClass($callable))->getMethod('__invoke'); default: @@ -104,15 +104,15 @@ public function newMethodFromCallable(callable $callable): ReflectionFunctionAbs } } - private function getFromParameterType(DIContainer $container, ReflectionParameter $parameter, array $arguments) + private function getFromParameterType(DIContainer $container, ReflectionParameter $parameter) { if (!$dependency = $parameter->getClass()) { return $arguments[$parameter->getPosition()] - ?? $this->getFromParameter($parameter, $container->getStorage()); + ?? $this->getFromParameter($container, $parameter); } // Global parameter overriding / singleton instance? - if ($param = $this->getFromParameter($parameter, $container->getStorage())) { + if ($param = $this->getFromParameter($container, $parameter)) { return $param; } @@ -123,15 +123,22 @@ private function getFromParameterType(DIContainer $container, ReflectionParamete return $container->inject($dependency->name); } - private function getFromParameter(ReflectionParameter $parameter, array $storage) + private function getFromParameter(DIContainer $container, ReflectionParameter $parameter) { - $name = ($parameter->getClass() ?: $parameter)->name; + $storage = $container->getStorage(); + $name = ($parameter->getClass() ?: $parameter)->name; - if ($storage[DIContainer::SINGLETONS][$name] ?? false) { + if (isset($storage[DIContainer::EXCLUDE][$name])) { + if (array_intersect($storage[DIContainer::EXCLUDE][$name], array_keys($storage[DIContainer::SINGLETONS]))) { + return (clone $container)->inject($name); + } + } + + if (isset($storage[DIContainer::SINGLETONS][$name])) { return $storage[DIContainer::SINGLETONS][$name]; } - if ($storage[DIContainer::NAMED]['$' . $parameter->name] ?? false) { + if (isset($storage[DIContainer::NAMED]['$' . $parameter->name])) { return $storage[DIContainer::NAMED]['$' . $parameter->name]; } @@ -139,6 +146,12 @@ private function getFromParameter(ReflectionParameter $parameter, array $storage return $parameter->getDefaultValue(); } + $type = $parameter->getType(); + if ($type && $type->isBuiltin()) { + throw DIException::forMissingArgument($name, $parameter->getPosition(), + $parameter->getDeclaringClass()->name . '::' . $parameter->getDeclaringFunction()->name); + } + return null; } } From dbb7364d8c5fe3dc8a300ddf170ab085ed07ee2c Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 18 Sep 2019 09:08:27 +0200 Subject: [PATCH 06/37] Updates the unit tests --- Tests/Unit/ExceptionsTest.php | 24 ++++++++--------- Tests/Unit/InjectObjectTest.php | 20 +++++--------- Tests/Unit/ShareWithExclusionTest.php | 28 ++++++++++++++++++++ Tests/Unit/fixtures.php | 38 +++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 Tests/Unit/ShareWithExclusionTest.php diff --git a/Tests/Unit/ExceptionsTest.php b/Tests/Unit/ExceptionsTest.php index adbf17c..c7019a4 100644 --- a/Tests/Unit/ExceptionsTest.php +++ b/Tests/Unit/ExceptionsTest.php @@ -7,14 +7,6 @@ class ExceptionsTest extends DITestCase { - public function testForInvalidClassName() - { - $this->expectException(DIException::class); - $this->expectExceptionCode(DIException::E_EMPTY_NAME); - - $this->di->inject(''); - } - public function testForCircularDependency() { $this->expectException(DIException::class); @@ -61,11 +53,19 @@ public function testForAbstractClassWithArguments() $this->di->inject(TestAbstractClass::class, ['arg1', 'arg2']); } - public function testForCloningNotAllowed() + public function testChildClassWithInterfaceWithoutMapping() + { + $this->expectException(DIException::class); + $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); + + $this->di->inject(TestClassWithConstructorInterfaceDependency::class); + } + + public function testMissingParameterForBuiltinParameterType() { $this->expectException(DIException::class); - $this->expectExceptionCode(DIException::E_CLONING_NOT_ALLOWED); - clone $this->di; + $this->expectExceptionCode(DIException::E_MISSING_ARGUMENT); + ($this->di)([TestClassForInvokeMethod::class, 'value']); } public function testForPsr11GetMethod() @@ -79,4 +79,4 @@ protected function createContainer(): DIContainer { return new DIContainer; } -} \ No newline at end of file +} diff --git a/Tests/Unit/InjectObjectTest.php b/Tests/Unit/InjectObjectTest.php index 1442dde..02335bd 100644 --- a/Tests/Unit/InjectObjectTest.php +++ b/Tests/Unit/InjectObjectTest.php @@ -3,7 +3,7 @@ namespace Koded\Tests\Unit; use ArrayObject; -use Koded\{DIContainer, DIException}; +use Koded\DIContainer; use PDO; class InjectObjectTest extends DITestCase @@ -11,8 +11,8 @@ class InjectObjectTest extends DITestCase public function testInjectOnDemand() { $this->assertNotSame( - $this->di->inject(TestChildClassWithNonPublicConstructor::class), - $this->di->inject(TestChildClassWithNonPublicConstructor::class), + $this->di->inject(TestChildClassAndParentWithNonPublicConstructor::class), + $this->di->inject(TestChildClassAndParentWithNonPublicConstructor::class), 'Injecting the same class always yields a new instance' ); } @@ -31,14 +31,6 @@ public function testClassWithConstructorArguments() $this->assertInstanceOf(TestClassWithConstructorArguments::class, $instance); } - public function testChildClassWithInterfaceWithoutMapping() - { - $this->expectException(DIException::class); - $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); - - $this->di->inject(TestClassWithConstructorInterfaceDependency::class); - } - public function testChildClassWithInterfaceWithMapping() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); @@ -60,11 +52,11 @@ public function testClassWithMultipleDependencies() $this->assertSame(FILE_APPEND, $instance->g); } - public function testChildClassWithNonPublicConstructor() + public function testChildClassAndParentWithWithNonPublicConstructor() { $this->assertInstanceOf( - TestChildClassWithNonPublicConstructor::class, - $this->di->inject(TestChildClassWithNonPublicConstructor::class) + TestChildClassAndParentWithNonPublicConstructor::class, + $this->di->inject(TestChildClassAndParentWithNonPublicConstructor::class) ); } diff --git a/Tests/Unit/ShareWithExclusionTest.php b/Tests/Unit/ShareWithExclusionTest.php new file mode 100644 index 0000000..a33cae1 --- /dev/null +++ b/Tests/Unit/ShareWithExclusionTest.php @@ -0,0 +1,28 @@ +di->share(new TestClassD, [TestClassB::class]); + $this->di->share(new TestClassWithoutConstructorArguments, [TestClassB::class]); + $this->di->share(new TestClassD, [TestClassA::class, TestClassB::class]); + $this->di->share(new TestClassD, [TestClassWithoutConstructorArguments::class]); + + $first = $this->di->inject(TestClassA::class); + $second = $this->di->inject(TestClassA::class); + + $this->assertSame($first->c->d, $second->c->d); + $this->assertNotSame($first->b->d, $first->c->d); + $this->assertNotSame($first->b->d, $second->b->d); + } + + protected function createContainer(): DIContainer + { + return new DIContainer; + } +} diff --git a/Tests/Unit/fixtures.php b/Tests/Unit/fixtures.php index 826bc6c..1495285 100644 --- a/Tests/Unit/fixtures.php +++ b/Tests/Unit/fixtures.php @@ -110,7 +110,7 @@ public function __construct($value) $this->value = $value; } - public static function value($value) + public static function value(string $value) { return $value; } @@ -226,7 +226,7 @@ protected function __construct() } } -class TestChildClassWithNonPublicConstructor extends TestClassWithNonPublicConstructor +class TestChildClassAndParentWithNonPublicConstructor extends TestClassWithNonPublicConstructor { public function __construct() { @@ -240,3 +240,37 @@ public function __construct() } } +class TestClassA +{ + public $b, $c; + + public function __construct(TestClassB $b, TestClassC $c) + { + $this->b = $b; + $this->c = $c; + } +} + +class TestClassB +{ + public $d; + + public function __construct(TestClassD $d) + { + $this->d = $d; + } +} + +class TestClassC +{ + public $d; + + public function __construct(TestClassD $d) + { + $this->d = $d; + } +} + +class TestClassD +{ +} From 975be35b9c1baba3ac66f7eeecdf38d0645ee3f3 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 18 Sep 2019 21:04:30 +0200 Subject: [PATCH 07/37] Fixes the missing method argument --- DIReflector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DIReflector.php b/DIReflector.php index c09a3e8..d18ef8a 100644 --- a/DIReflector.php +++ b/DIReflector.php @@ -75,7 +75,7 @@ public function processMethodArguments( if (!$param instanceof ReflectionParameter) { continue; } - $args[$i] = $this->getFromParameterType($container, $param); + $args[$i] = $this->getFromParameterType($container, $param, $arguments); } return $args; @@ -104,7 +104,7 @@ public function newMethodFromCallable(callable $callable): ReflectionFunctionAbs } } - private function getFromParameterType(DIContainer $container, ReflectionParameter $parameter) + private function getFromParameterType(DIContainer $container, ReflectionParameter $parameter, array $arguments) { if (!$dependency = $parameter->getClass()) { return $arguments[$parameter->getPosition()] From ed4a7514be0891a2e51680d0195de4909bb13201 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 18 Sep 2019 21:04:44 +0200 Subject: [PATCH 08/37] Cleanup --- DIContainer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/DIContainer.php b/DIContainer.php index 2a4a7b0..22ee221 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -78,7 +78,6 @@ public function __clone() public function __destruct() { $this->reflection = null; - $this->singletons = []; $this->bindings = []; $this->exclude = []; From 13132da602c7ce86f55b5a0e3b0fb70b149b2278 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Mon, 23 Sep 2019 19:41:34 +0200 Subject: [PATCH 09/37] Method signature changed inject() -> new() --- DIContainer.php | 10 +++++----- DIReflector.php | 4 ++-- Tests/PhpBench/SimpleAppBench.php | 2 +- Tests/PhpBench/WithModulesBench.php | 2 +- Tests/PhpBench/WithoutModulesBench.php | 2 +- Tests/Unit/BindMethodTest.php | 2 +- Tests/Unit/ExceptionsTest.php | 12 ++++++------ Tests/Unit/InjectObjectTest.php | 16 ++++++++-------- Tests/Unit/InterfaceMappingTest.php | 4 ++-- Tests/Unit/InvokeTest.php | 2 +- Tests/Unit/ShareMethodTest.php | 2 +- Tests/Unit/ShareWithExclusionTest.php | 4 ++-- Tests/Unit/SingletonMethodTest.php | 4 ++-- Tests/Unit/WiringAndInjectionTest.php | 6 +++--- 14 files changed, 36 insertions(+), 36 deletions(-) diff --git a/DIContainer.php b/DIContainer.php index 22ee221..fe7f63f 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -97,7 +97,7 @@ public function __invoke(callable $callable, array $arguments = []) /** * 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 scenes (ex. with singleton and share). + * It can also inject already created dependencies behind the scenes (with singleton and share). * * @param string $class FQCN * @param array $arguments [optional] The arguments for the class constructor. @@ -106,7 +106,7 @@ public function __invoke(callable $callable, array $arguments = []) * @return object|callable|null * @throws ContainerExceptionInterface */ - public function inject(string $class, array $arguments = []): ?object + public function new(string $class, array $arguments = []): ?object { $binding = $this->getFromBindings($class); if (isset($this->inProgress[$binding])) { @@ -126,7 +126,7 @@ public function inject(string $class, array $arguments = []): ?object * Internally the object is immutable, but it can be replaced with share() method. * * @param string $class FQCN - * @param array $arguments [optional] See inject() description + * @param array $arguments [optional] See new() description * * @return object */ @@ -136,7 +136,7 @@ public function singleton(string $class, array $arguments = []): object if (isset($this->singletons[$binding])) { return $this->singletons[$binding]; } - return $this->singletons[$class] = $this->inject($class, $arguments); + return $this->singletons[$class] = $this->new($class, $arguments); } /** @@ -238,7 +238,7 @@ public function get($id) $dependency = $this->getFromBindings($id); return $this->singletons[$dependency] ?? $this->named[$dependency] - ?? $this->inject($dependency); + ?? $this->new($dependency); } private function newInstance(string $class, array $arguments): object diff --git a/DIReflector.php b/DIReflector.php index d18ef8a..eb7e983 100644 --- a/DIReflector.php +++ b/DIReflector.php @@ -120,7 +120,7 @@ private function getFromParameterType(DIContainer $container, ReflectionParamete return $parameter->getDefaultValue(); } - return $container->inject($dependency->name); + return $container->new($dependency->name); } private function getFromParameter(DIContainer $container, ReflectionParameter $parameter) @@ -130,7 +130,7 @@ private function getFromParameter(DIContainer $container, ReflectionParameter $p if (isset($storage[DIContainer::EXCLUDE][$name])) { if (array_intersect($storage[DIContainer::EXCLUDE][$name], array_keys($storage[DIContainer::SINGLETONS]))) { - return (clone $container)->inject($name); + return (clone $container)->new($name); } } diff --git a/Tests/PhpBench/SimpleAppBench.php b/Tests/PhpBench/SimpleAppBench.php index f78095d..112400d 100644 --- a/Tests/PhpBench/SimpleAppBench.php +++ b/Tests/PhpBench/SimpleAppBench.php @@ -18,7 +18,7 @@ class SimpleAppBench extends AbstractBench */ public function benchAppInvoke() { - $dispatcher = $this->di->inject(PostCommandDispatcher::class, ['hello']); + $dispatcher = $this->di->new(PostCommandDispatcher::class, ['hello']); ($this->di)([$dispatcher, 'get']); } diff --git a/Tests/PhpBench/WithModulesBench.php b/Tests/PhpBench/WithModulesBench.php index 6e1544b..7720e22 100644 --- a/Tests/PhpBench/WithModulesBench.php +++ b/Tests/PhpBench/WithModulesBench.php @@ -17,7 +17,7 @@ class WithModulesBench extends AbstractBench */ public function benchInject() { - $this->di->inject(TestClassWithConstructorInterfaceDependency::class); + $this->di->new(TestClassWithConstructorInterfaceDependency::class); } /** diff --git a/Tests/PhpBench/WithoutModulesBench.php b/Tests/PhpBench/WithoutModulesBench.php index b69d266..02fc3c7 100644 --- a/Tests/PhpBench/WithoutModulesBench.php +++ b/Tests/PhpBench/WithoutModulesBench.php @@ -17,7 +17,7 @@ class WithoutModulesBench extends AbstractBench public function benchInject() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $this->di->inject(TestClassWithConstructorInterfaceDependency::class); + $this->di->new(TestClassWithConstructorInterfaceDependency::class); } /** diff --git a/Tests/Unit/BindMethodTest.php b/Tests/Unit/BindMethodTest.php index c154f47..c227369 100644 --- a/Tests/Unit/BindMethodTest.php +++ b/Tests/Unit/BindMethodTest.php @@ -11,7 +11,7 @@ public function testUntargetedBinding() $this->di->named('$arg', 'foobar'); $this->di->bind(TestClassWithPrimitiveConstructorArgument::class, '$arg'); - $obj = $this->di->inject(TestClassWithPrimitiveConstructorArgument::class); + $obj = $this->di->new(TestClassWithPrimitiveConstructorArgument::class); $this->assertInstanceOf(TestClassWithPrimitiveConstructorArgument::class, $obj); $this->assertSame('foobar', $obj->arg); diff --git a/Tests/Unit/ExceptionsTest.php b/Tests/Unit/ExceptionsTest.php index c7019a4..b69e4e7 100644 --- a/Tests/Unit/ExceptionsTest.php +++ b/Tests/Unit/ExceptionsTest.php @@ -12,7 +12,7 @@ public function testForCircularDependency() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_CIRCULAR_DEPENDENCY); - $this->di->inject(TestCircularDependencyA::class); + $this->di->new(TestCircularDependencyA::class); } public function testInvokeMethodForInvalidMethod() @@ -26,7 +26,7 @@ public function testForClassWithNonPublicConstructor() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_NON_PUBLIC_METHOD); - $this->di->inject(TestClassWithNonPublicConstructor::class); + $this->di->new(TestClassWithNonPublicConstructor::class); } public function testForInstantiatingInterface() @@ -34,7 +34,7 @@ public function testForInstantiatingInterface() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); - $this->di->inject(TestInterface::class); + $this->di->new(TestInterface::class); } public function testForAbstractClass() @@ -42,7 +42,7 @@ public function testForAbstractClass() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); - $this->di->inject(TestAbstractClass::class); + $this->di->new(TestAbstractClass::class); } public function testForAbstractClassWithArguments() @@ -50,7 +50,7 @@ public function testForAbstractClassWithArguments() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); - $this->di->inject(TestAbstractClass::class, ['arg1', 'arg2']); + $this->di->new(TestAbstractClass::class, ['arg1', 'arg2']); } public function testChildClassWithInterfaceWithoutMapping() @@ -58,7 +58,7 @@ public function testChildClassWithInterfaceWithoutMapping() $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_CANNOT_INSTANTIATE); - $this->di->inject(TestClassWithConstructorInterfaceDependency::class); + $this->di->new(TestClassWithConstructorInterfaceDependency::class); } public function testMissingParameterForBuiltinParameterType() diff --git a/Tests/Unit/InjectObjectTest.php b/Tests/Unit/InjectObjectTest.php index 02335bd..ed12123 100644 --- a/Tests/Unit/InjectObjectTest.php +++ b/Tests/Unit/InjectObjectTest.php @@ -11,22 +11,22 @@ class InjectObjectTest extends DITestCase public function testInjectOnDemand() { $this->assertNotSame( - $this->di->inject(TestChildClassAndParentWithNonPublicConstructor::class), - $this->di->inject(TestChildClassAndParentWithNonPublicConstructor::class), + $this->di->new(TestChildClassAndParentWithNonPublicConstructor::class), + $this->di->new(TestChildClassAndParentWithNonPublicConstructor::class), 'Injecting the same class always yields a new instance' ); } public function testClassWithoutConstructorArguments() { - $instance = $this->di->inject(TestClassWithoutConstructorArguments::class); + $instance = $this->di->new(TestClassWithoutConstructorArguments::class); $this->assertInstanceOf(TestClassWithoutConstructorArguments::class, $instance); } public function testClassWithConstructorArguments() { $this->di->named('$pdo', new PDO('sqlite:')); - $instance = $this->di->inject(TestClassWithConstructorArguments::class); + $instance = $this->di->new(TestClassWithConstructorArguments::class); $this->assertInstanceOf(TestClassWithConstructorArguments::class, $instance); } @@ -34,14 +34,14 @@ public function testClassWithConstructorArguments() public function testChildClassWithInterfaceWithMapping() { $this->di->bind(TestInterface::class, TestClassWithInterfaceAndNoConstructor::class); - $instance = $this->di->inject(TestClassWithConstructorInterfaceDependency::class); + $instance = $this->di->new(TestClassWithConstructorInterfaceDependency::class); $this->assertInstanceOf(TestClassWithInterfaceAndNoConstructor::class, $instance->getDependency()); } public function testClassWithMultipleDependencies() { - $instance = $this->di->inject(TestClassWithMultipleDependencies::class, ['val1', 42, false, ['val2']]); + $instance = $this->di->new(TestClassWithMultipleDependencies::class, ['val1', 42, false, ['val2']]); $this->assertSame('val1', $instance->a); $this->assertSame(42, $instance->b); @@ -56,14 +56,14 @@ public function testChildClassAndParentWithWithNonPublicConstructor() { $this->assertInstanceOf( TestChildClassAndParentWithNonPublicConstructor::class, - $this->di->inject(TestChildClassAndParentWithNonPublicConstructor::class) + $this->di->new(TestChildClassAndParentWithNonPublicConstructor::class) ); } public function testArrayObject() { /** @var ArrayObject $instance */ - $instance = $this->di->inject(ArrayObject::class, [['foo' => 'bar'], ArrayObject::ARRAY_AS_PROPS]); + $instance = $this->di->new(ArrayObject::class, [['foo' => 'bar'], ArrayObject::ARRAY_AS_PROPS]); $this->assertInstanceOf(ArrayObject::class, $instance); $this->assertSame('bar', $instance->foo); diff --git a/Tests/Unit/InterfaceMappingTest.php b/Tests/Unit/InterfaceMappingTest.php index 74cc2f0..fe985a0 100644 --- a/Tests/Unit/InterfaceMappingTest.php +++ b/Tests/Unit/InterfaceMappingTest.php @@ -16,7 +16,7 @@ class InterfaceMappingTest extends DITestCase { public function testImplementedInterfaces() { - $shared = $this->di->inject(TestClassWithConstructorArguments::class, [new PDO('sqlite:')]); + $shared = $this->di->new(TestClassWithConstructorArguments::class, [new PDO('sqlite:')]); $this->di->share($shared); $this->assertFalse($this->di->has(JsonSerializable::class), 'JsonSerializable interface is not mapped to the class'); @@ -25,7 +25,7 @@ public function testImplementedInterfaces() public function testInterfacesFromParent() { - $shared = $this->di->inject(TestClassWithoutConstructorArguments::class); + $shared = $this->di->new(TestClassWithoutConstructorArguments::class); $this->di->share($shared); $this->assertFalse( diff --git a/Tests/Unit/InvokeTest.php b/Tests/Unit/InvokeTest.php index f16d30c..0053c09 100644 --- a/Tests/Unit/InvokeTest.php +++ b/Tests/Unit/InvokeTest.php @@ -8,7 +8,7 @@ class InvokeTest extends DITestCase { public function testInvokeMethod() { - $instance = $this->di->inject(TestClassForInvokeMethod::class, ['initial value']); + $instance = $this->di->new(TestClassForInvokeMethod::class, ['initial value']); $this->assertSame('initial value', ($this->di)([$instance, 'get'])); $this->assertSame('from arguments', ($this->di)([$instance, 'get'], ['from arguments'])); diff --git a/Tests/Unit/ShareMethodTest.php b/Tests/Unit/ShareMethodTest.php index d2eebbb..83741bd 100644 --- a/Tests/Unit/ShareMethodTest.php +++ b/Tests/Unit/ShareMethodTest.php @@ -11,7 +11,7 @@ public function testImmutability() $actual = $this->di->get(TestClassWithConstructorInterfaceDependency::class); $this->assertInstanceOf(TestClassWithConstructorInterfaceDependency::class, $actual); - $new = $this->di->inject(TestClassWithConstructorInterfaceDependency::class); + $new = $this->di->new(TestClassWithConstructorInterfaceDependency::class); $this->di->share($new); $this->assertNotSame($new, $actual, diff --git a/Tests/Unit/ShareWithExclusionTest.php b/Tests/Unit/ShareWithExclusionTest.php index a33cae1..804303d 100644 --- a/Tests/Unit/ShareWithExclusionTest.php +++ b/Tests/Unit/ShareWithExclusionTest.php @@ -13,8 +13,8 @@ public function testExcludedSharedInstance() $this->di->share(new TestClassD, [TestClassA::class, TestClassB::class]); $this->di->share(new TestClassD, [TestClassWithoutConstructorArguments::class]); - $first = $this->di->inject(TestClassA::class); - $second = $this->di->inject(TestClassA::class); + $first = $this->di->new(TestClassA::class); + $second = $this->di->new(TestClassA::class); $this->assertSame($first->c->d, $second->c->d); $this->assertNotSame($first->b->d, $first->c->d); diff --git a/Tests/Unit/SingletonMethodTest.php b/Tests/Unit/SingletonMethodTest.php index 8236f85..99bd570 100644 --- a/Tests/Unit/SingletonMethodTest.php +++ b/Tests/Unit/SingletonMethodTest.php @@ -18,10 +18,10 @@ public function testSingletonCreateWithInjectMethod() { // $this->markTestIncomplete('Clarify the behavior of inject()'); $singleton = $this->di->singleton(TestClassWithInterfaceAndNoConstructor::class); - $other = $this->di->inject(TestClassWithInterfaceAndNoConstructor::class); + $other = $this->di->new(TestClassWithInterfaceAndNoConstructor::class); $this->assertNotSame($singleton, $other, - 'inject() method always creates a new instance even if that class exists as singleton' + 'new() method always creates a new instance even if that class exists as singleton' ); } diff --git a/Tests/Unit/WiringAndInjectionTest.php b/Tests/Unit/WiringAndInjectionTest.php index 674de36..82b876c 100644 --- a/Tests/Unit/WiringAndInjectionTest.php +++ b/Tests/Unit/WiringAndInjectionTest.php @@ -10,21 +10,21 @@ class WiringAndInjectionTest extends DITestCase public function testWithSingleton() { $this->di->singleton(PDO::class, ['sqlite:']); - $dispatcher = $this->di->inject(PostCommandDispatcher::class, ['hello']); + $dispatcher = $this->di->new(PostCommandDispatcher::class, ['hello']); $this->assert($dispatcher); } public function testWithNamedValueParameter() { $this->di->named('$dsn', 'sqlite:'); - $dispatcher = $this->di->inject(PostCommandDispatcher::class, ['hello']); + $dispatcher = $this->di->new(PostCommandDispatcher::class, ['hello']); $this->assert($dispatcher); } public function testWithNamedInstanceParameter() { $this->di->named('$pdo', new PDO('sqlite:')); - $dispatcher = $this->di->inject(PostCommandDispatcher::class, ['hello']); + $dispatcher = $this->di->new(PostCommandDispatcher::class, ['hello']); $this->assert($dispatcher); } From f0aca99588aab3ad6534b792925575443853674b Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 22:40:09 +0200 Subject: [PATCH 10/37] Removed dump() function --- Tests/Unit/DIModuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Unit/DIModuleTest.php b/Tests/Unit/DIModuleTest.php index 84d43f1..1b5c15c 100644 --- a/Tests/Unit/DIModuleTest.php +++ b/Tests/Unit/DIModuleTest.php @@ -3,7 +3,6 @@ namespace Koded\Tests\Unit; use Koded\{DIContainer, DIModule}; -use function Koded\Stdlib\dump; class ModuleTest extends DITestCase { From a9137aa892557ea3ea705733c17755c6d6039447 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 22:52:44 +0200 Subject: [PATCH 11/37] Cleanup --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c9c8b4b..9a08a48 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,8 @@ "prefer-stable": true, "require": { "php": "^7.2", - "koded/stdlib": "~4", - "psr/container": "~1" + "psr/container": "~1", + "koded/stdlib": "~4" }, "autoload": { "exclude-from-classmap": [ @@ -49,4 +49,4 @@ "dev-master": "1.x-dev" } } -} +} \ No newline at end of file From 794850bc087781c37d5a6fa314f1bd9ca8121138 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 22:53:47 +0200 Subject: [PATCH 12/37] Small updates --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 74d79c8..e2e79d9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ Dependency Injection Container - Koded [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/kodedphp/container/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/kodedphp/container/?branch=master) [![Infection MSI](https://badge.stryker-mutator.io/github.com/kodedphp/container/master)](https://github.com/kodedphp/container) [![Minimum PHP Version: 7.2](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg)](https://php.net/) -[![Software license](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE) `koded/container` is a SOLID OOP application bootstrapping and wiring library. @@ -120,4 +119,9 @@ $response = (new DIContainer(new BlogModule))([$resolvedDispatcher, $resolvedMet // ex. `echo $response->getBody()->getContents();` ``` -> To be continued... \ No newline at end of file +> To be continued... + +License +------- +[![Software license](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE) +The code is distributed under the terms of [The 3-Clause BSD license](LICENSE). From c04191ab78257461a7c693c2c24724de15cc7652 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 22:57:38 +0200 Subject: [PATCH 13/37] Implementation of deferred binding --- DIContainer.php | 43 ++++++++++++++++++++++++++++--------------- DIReflector.php | 6 +++++- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/DIContainer.php b/DIContainer.php index fe7f63f..dbd8489 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -30,9 +30,9 @@ interface DIModule * * ex: `$injector->bind(MyInterface::class, MyImplementation::class);` * - * @param DIContainer $injector + * @param DIContainer $container */ - public function configure(DIContainer $injector): void; + public function configure(DIContainer $container): void; } /** @@ -143,20 +143,23 @@ public function singleton(string $class, array $arguments = []): object * Share already created instance of an object throughout the app lifecycle. * * @param object $instance The object that will be shared as dependency - * @param array $excludedClasses [optional] A list of FQCN that should + * @param array $exclude [optional] A list of FQCN 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 $excludedClasses = []): DIContainer + public function share(object $instance, array $exclude = []): DIContainer { - $class = get_class($instance); + $class = get_class($instance); + $this->bindnterface($instance, $class); + $this->singletons[$class] = $instance; + $this->bindings[$class] = $class; - foreach ($excludedClasses as $exclude) { - $this->exclude[$exclude][$class] = $class; + foreach ($exclude as $name) { + $this->exclude[$name][$class] = $class; } return $this; } @@ -168,21 +171,21 @@ public function share(object $instance, array $excludedClasses = []): DIContaine * 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 + * @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 + public function bind(string $interface, string $class = ''): DIContainer { - assert(false === empty($class), 'Dependency name for bind() method'); - assert(false === empty($class), 'Class name for bind() method'); + 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; } @@ -256,4 +259,14 @@ private function getFromBindings(string $dependency): string assert(false === empty($dependency), 'Dependency name for class/interface'); return $this->bindings[$dependency] ?? $dependency; } + + private function bindnterface(object $dependency, string $class): void + { + foreach (class_implements($dependency) as $interface) { + if (isset($this->bindings[$interface])) { + $this->bindings[$interface] = $class; + break; + } + } + } } diff --git a/DIReflector.php b/DIReflector.php index eb7e983..647672a 100644 --- a/DIReflector.php +++ b/DIReflector.php @@ -60,7 +60,7 @@ public function processMethodArguments( try { $name = $method->getDeclaringClass()->name; } catch (Throwable $e) { - $name = $method->getNamespaceName() ?: $method->getName(); + $name = $method->getNamespaceName() ?: $method->name; } $args = array_replace($method->getParameters(), $arguments); @@ -128,6 +128,10 @@ private function getFromParameter(DIContainer $container, ReflectionParameter $p $storage = $container->getStorage(); $name = ($parameter->getClass() ?: $parameter)->name; + if (isset($storage[DIContainer::BINDINGS][$name])) { + $name = $storage[DIContainer::BINDINGS][$name]; + } + if (isset($storage[DIContainer::EXCLUDE][$name])) { if (array_intersect($storage[DIContainer::EXCLUDE][$name], array_keys($storage[DIContainer::SINGLETONS]))) { return (clone $container)->new($name); From c75f56699e416cc728a884c76d3dfcedbf551f3b Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 22:58:21 +0200 Subject: [PATCH 14/37] Improved unit tests --- Tests/Unit/BindMethodTest.php | 7 +++++++ Tests/Unit/InterfaceMappingTest.php | 18 ++++++++++-------- Tests/Unit/SingletonMethodTest.php | 1 - Tests/Unit/fixtures.php | 9 ++++++--- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Tests/Unit/BindMethodTest.php b/Tests/Unit/BindMethodTest.php index c227369..b2c78f2 100644 --- a/Tests/Unit/BindMethodTest.php +++ b/Tests/Unit/BindMethodTest.php @@ -17,6 +17,13 @@ public function testUntargetedBinding() $this->assertSame('foobar', $obj->arg); } + public function testDeferredBindingWithShareMethod() + { + $this->di->bind(TestInterface::class); + $this->di->share(new TestClassWithInterfaceAndNoConstructor); + $this->assertTrue($this->di->has(TestClassWithInterfaceAndNoConstructor::class)); + } + protected function createContainer(): DIContainer { return new DIContainer; diff --git a/Tests/Unit/InterfaceMappingTest.php b/Tests/Unit/InterfaceMappingTest.php index fe985a0..3d7a8a6 100644 --- a/Tests/Unit/InterfaceMappingTest.php +++ b/Tests/Unit/InterfaceMappingTest.php @@ -2,16 +2,14 @@ namespace Koded\Tests\Unit; +use ArrayAccess; use Countable; use JsonSerializable; use Koded\DIContainer; -use Koded\Stdlib\Interfaces\ArrayDataFilter; use PDO; +use SeekableIterator; +use Serializable; -/** - * @since v1.2.0 Validate from previous implementation: - * - Interface mapping and binding is removed - */ class InterfaceMappingTest extends DITestCase { public function testImplementedInterfaces() @@ -29,10 +27,14 @@ public function testInterfacesFromParent() $this->di->share($shared); $this->assertFalse( - $this->di->has(ArrayDataFilter::class), - 'TestClassWithoutConstructorArguments extends Config, which also implements ArrayDataFilter interface, - therefore the parent interfaces are NOT bound to this class instance' + $this->di->has(SeekableIterator::class), + 'TestClassWithoutConstructorArguments extends ArrayIterator, which also implements SeekableIterator interface. + The parent interfaces should NOT be bounded to this class instance' ); + + $this->assertFalse($this->di->has(ArrayAccess::class)); + $this->assertFalse($this->di->has(Serializable::class)); + $this->assertFalse($this->di->has(Countable::class)); } protected function createContainer(): DIContainer diff --git a/Tests/Unit/SingletonMethodTest.php b/Tests/Unit/SingletonMethodTest.php index 99bd570..e4d8acf 100644 --- a/Tests/Unit/SingletonMethodTest.php +++ b/Tests/Unit/SingletonMethodTest.php @@ -16,7 +16,6 @@ public function testSingletonCreateWithoutBinding() public function testSingletonCreateWithInjectMethod() { -// $this->markTestIncomplete('Clarify the behavior of inject()'); $singleton = $this->di->singleton(TestClassWithInterfaceAndNoConstructor::class); $other = $this->di->new(TestClassWithInterfaceAndNoConstructor::class); diff --git a/Tests/Unit/fixtures.php b/Tests/Unit/fixtures.php index 1495285..82ff82c 100644 --- a/Tests/Unit/fixtures.php +++ b/Tests/Unit/fixtures.php @@ -2,10 +2,10 @@ namespace Koded\Tests\Unit; +use ArrayIterator; use Countable; use Exception; use JsonSerializable; -use Koded\Stdlib\Config; use PDO; interface PostRepository @@ -74,7 +74,6 @@ public function findBlogPostBySlug(string $slug): array { $post = $this->post->findBySlug($slug); $user = $this->user->findById($post[0]); - // ... do something with the results return [$user, $post]; } } @@ -141,8 +140,12 @@ public function count() } } -class TestClassWithoutConstructorArguments extends Config +class TestClassWithoutConstructorArguments extends ArrayIterator { + public function __construct() + { + parent::__construct([]); + } } class TestClassWithInterfaceAndNoConstructor implements TestInterface From 38b572fcaf1b0b5afa3105fe49a5fe67e0f649b1 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 23:03:21 +0200 Subject: [PATCH 15/37] updated php matrix --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b138c4e..9cf4622 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,12 @@ cache: php: - 7.2 - 7.3 - - 7.4snapshot + - nightly matrix: fast_finish: true allow_failures: - - php: 7.4snapshot + - php: nightly install: - travis_retry composer update -o --no-interaction --prefer-source From bdca06c7893ba988a87e65183a6ef2355c570e01 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 23:29:46 +0200 Subject: [PATCH 16/37] updated php matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9cf4622..38babea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ cache: php: - 7.2 - 7.3 + - 7.4 - nightly matrix: From e1439e1b27b63bfc770494c3c4e908317b9b0990 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 23:30:36 +0200 Subject: [PATCH 17/37] Typos --- DIContainer.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/DIContainer.php b/DIContainer.php index dbd8489..9169c40 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -19,7 +19,7 @@ * 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 DIModule(s) and some bootstrapping code. + * The application is composed of a set of DIModules and some bootstrapping code. */ interface DIModule { @@ -28,7 +28,7 @@ interface DIModule * Also reduces the repetition and results in a more readable configuration. * Implement the `configure()` method to bind your interfaces. * - * ex: `$injector->bind(MyInterface::class, MyImplementation::class);` + * ex: `$container->bind(MyInterface::class, MyImplementation::class);` * * @param DIContainer $container */ @@ -36,9 +36,9 @@ public function configure(DIContainer $container): void; } /** - * The entry point of the DIContainer that dwars the lines between the + * 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 of a collection of modules. + * implementations and applications that consist of a collection of modules. * * ``` * $container = new DIContainer(new ModuleA, new ModuleB, ... new ModuleZ); @@ -97,7 +97,7 @@ public function __invoke(callable $callable, array $arguments = []) /** * 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 scenes (with singleton and share). + * 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. @@ -143,9 +143,9 @@ public function singleton(string $class, array $arguments = []): object * 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 FQCN that should + * @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 + * In this case, a new object will be created and * injected for these classes * * @return DIContainer @@ -153,7 +153,7 @@ public function singleton(string $class, array $arguments = []): object public function share(object $instance, array $exclude = []): DIContainer { $class = get_class($instance); - $this->bindnterface($instance, $class); + $this->bindInterface($instance, $class); $this->singletons[$class] = $instance; $this->bindings[$class] = $class; @@ -260,7 +260,7 @@ private function getFromBindings(string $dependency): string return $this->bindings[$dependency] ?? $dependency; } - private function bindnterface(object $dependency, string $class): void + private function bindInterface(object $dependency, string $class): void { foreach (class_implements($dependency) as $interface) { if (isset($this->bindings[$interface])) { From e299e7a98cf22478a2f2fec77fa99c64fa0e8805 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Wed, 2 Oct 2019 23:36:37 +0200 Subject: [PATCH 18/37] updated php matrix --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 38babea..9cf4622 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ cache: php: - 7.2 - 7.3 - - 7.4 - nightly matrix: From a7260e52d9861463ab1dec298bb008b3f725d37b Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 7 Dec 2019 20:30:49 +0100 Subject: [PATCH 19/37] Changes the module example --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e2e79d9..8f4746f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Dependency Injection Container - Koded [![Minimum PHP Version: 7.2](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg)](https://php.net/) -`koded/container` is a SOLID OOP application bootstrapping and wiring library. +`koded/container` is an OOP application bootstrapping and wiring library. In other words, `Koded\DIContainer` implements a **design pattern** called **Dependency Injection**. The main principle of DIP is to separate the behavior from dependency resolution. @@ -92,14 +92,14 @@ class HttpPostHandler { This is the bootstrapping / wiring application module ```php class BlogModule implements DIModule { - public function configure(DIContainer $injector): void { + public function configure(DIContainer $container): void { // bind interfaces to concrete class implementations - $injector->bind(PostRepository::class, DatabasePostRepository::class); - $injector->bind(UserRepository::class, DatabaseUserRepository::class); - $injector->bind(ServerRequestInterface::class, /*some PSR-7 server request class name*/); + $container->bind(PostRepository::class, DatabasePostRepository::class); + $container->bind(UserRepository::class, DatabaseUserRepository::class); + $container->bind(ServerRequestInterface::class, /*some PSR-7 server request class name*/); // share one PDO instance - $injector->singleton(PDO::class, ['sqlite:database.db']); + $container->singleton(PDO::class, ['sqlite:database.db']); } } ``` @@ -119,9 +119,17 @@ $response = (new DIContainer(new BlogModule))([$resolvedDispatcher, $resolvedMet // ex. `echo $response->getBody()->getContents();` ``` +The container implements the [__invoke()][invoke] method, so the instance can be used as a function: +```php +$container('method', 'arguments'); +``` + > To be continued... License ------- [![Software license](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE) The code is distributed under the terms of [The 3-Clause BSD license](LICENSE). + + +[invoke]: https://php.net/manual/en/language.oop5.magic.php#object.invoke \ No newline at end of file From b736370a22f37ef9af08f785d8c608a490c02208 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 7 Dec 2019 20:32:43 +0100 Subject: [PATCH 20/37] - removes the try/catch blocks - changes some method names for clarity --- DIContainer.php | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/DIContainer.php b/DIContainer.php index 9169c40..be74413 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -86,13 +86,9 @@ public function __destruct() public function __invoke(callable $callable, array $arguments = []) { - try { - return call_user_func_array($callable, $this->reflection->processMethodArguments( - $this, $this->reflection->newMethodFromCallable($callable), $arguments - )); - } catch (Throwable $e) { - throw DIException::from($e); - } + return call_user_func_array($callable, $this->reflection->processMethodArguments( + $this, $this->reflection->newMethodFromCallable($callable), $arguments + )); } /** @@ -108,7 +104,7 @@ public function __invoke(callable $callable, array $arguments = []) */ public function new(string $class, array $arguments = []): ?object { - $binding = $this->getFromBindings($class); + $binding = $this->getNameFromBindings($class); if (isset($this->inProgress[$binding])) { throw DIException::forCircularDependency($binding); } @@ -132,7 +128,7 @@ public function new(string $class, array $arguments = []): ?object */ public function singleton(string $class, array $arguments = []): object { - $binding = $this->getFromBindings($class); + $binding = $this->getNameFromBindings($class); if (isset($this->singletons[$binding])) { return $this->singletons[$binding]; } @@ -153,7 +149,7 @@ public function singleton(string $class, array $arguments = []): object public function share(object $instance, array $exclude = []): DIContainer { $class = get_class($instance); - $this->bindInterface($instance, $class); + $this->bindInterfaces($instance, $class); $this->singletons[$class] = $instance; $this->bindings[$class] = $class; @@ -238,7 +234,7 @@ public function get($id) throw DIInstanceNotFound::for($id); } - $dependency = $this->getFromBindings($id); + $dependency = $this->getNameFromBindings($id); return $this->singletons[$dependency] ?? $this->named[$dependency] ?? $this->new($dependency); @@ -246,21 +242,17 @@ public function get($id) private function newInstance(string $class, array $arguments): object { - try { - $this->bindings[$class] = $class; - return $this->reflection->newInstance($this, $class, $arguments); - } catch (Throwable $e) { - throw DIException::from($e); - } + $this->bindings[$class] = $class; + return $this->reflection->newInstance($this, $class, $arguments); } - private function getFromBindings(string $dependency): string + private function getNameFromBindings(string $dependency): string { assert(false === empty($dependency), 'Dependency name for class/interface'); return $this->bindings[$dependency] ?? $dependency; } - private function bindInterface(object $dependency, string $class): void + private function bindInterfaces(object $dependency, string $class): void { foreach (class_implements($dependency) as $interface) { if (isset($this->bindings[$interface])) { From e503050f58f284cd8f0399e8746ece84bf332bff Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 7 Dec 2019 20:34:50 +0100 Subject: [PATCH 21/37] Reorders the conditionals in `newInstance` method --- DIReflector.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/DIReflector.php b/DIReflector.php index 647672a..89e29d8 100644 --- a/DIReflector.php +++ b/DIReflector.php @@ -28,21 +28,19 @@ public function newInstance(DIContainer $container, string $class, array $argume $dependency = new ReflectionClass($class); $constructor = $dependency->getConstructor(); - if (false === $dependency->isInstantiable()) { - if (null !== $constructor && false === $constructor->isPublic()) { - throw DIException::forNonPublicMethod($constructor->getDeclaringClass()->name . '::' . $constructor->name); - } - - throw DIException::cannotInstantiate( - $dependency->name, $dependency->isInterface() ? 'interface' : 'abstract class' - ); + if ($dependency->isInstantiable()) { + return $constructor + ? new $class(...$this->processMethodArguments($container, $constructor, $arguments)) + : new $class; } - if (null === $constructor) { - return new $class; + if (null !== $constructor && false === $constructor->isPublic()) { + throw DIException::forNonPublicMethod($constructor->getDeclaringClass()->name . '::' . $constructor->name); } - return new $class(...$this->processMethodArguments($container, $constructor, $arguments)); + throw DIException::cannotInstantiate( + $dependency->name, $dependency->isInterface() ? 'interface' : 'abstract class' + ); } /** From d1ddace3bbf0e6b83cfc0d7cb3eab53fe3bce58f Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 7 Dec 2019 20:36:16 +0100 Subject: [PATCH 22/37] Adds tests for try/catch removal in DIContainer methods --- Tests/Unit/ExceptionsTest.php | 9 +++++++++ Tests/Unit/fixtures.php | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/Tests/Unit/ExceptionsTest.php b/Tests/Unit/ExceptionsTest.php index b69e4e7..f31bbcd 100644 --- a/Tests/Unit/ExceptionsTest.php +++ b/Tests/Unit/ExceptionsTest.php @@ -3,6 +3,7 @@ namespace Koded\Tests\Unit; use Koded\{DIContainer, DIException}; +use OutOfBoundsException; use Psr\Container\NotFoundExceptionInterface; class ExceptionsTest extends DITestCase @@ -75,6 +76,14 @@ public function testForPsr11GetMethod() $this->di->get('Fubar'); } + public function testExceptionForInvokeMethod() + { + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionCode(400); + $this->expectExceptionMessage('out of bounds'); + ($this->di)([TestExceptionForInvokeMethod::class, 'fail']); + } + protected function createContainer(): DIContainer { return new DIContainer; diff --git a/Tests/Unit/fixtures.php b/Tests/Unit/fixtures.php index 82ff82c..1d11556 100644 --- a/Tests/Unit/fixtures.php +++ b/Tests/Unit/fixtures.php @@ -6,6 +6,7 @@ use Countable; use Exception; use JsonSerializable; +use OutOfBoundsException; use PDO; interface PostRepository @@ -243,6 +244,14 @@ public function __construct() } } +class TestExceptionForInvokeMethod +{ + public function fail() + { + throw new OutOfBoundsException('out of bounds', 400); + } +} + class TestClassA { public $b, $c; From 0dc2f42eb30735f28d35ea684d6bc2434c66d2a9 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 7 Dec 2019 21:19:18 +0100 Subject: [PATCH 23/37] Fixes the test --- Tests/Unit/ExceptionsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Unit/ExceptionsTest.php b/Tests/Unit/ExceptionsTest.php index f31bbcd..c6f6cf4 100644 --- a/Tests/Unit/ExceptionsTest.php +++ b/Tests/Unit/ExceptionsTest.php @@ -81,7 +81,7 @@ public function testExceptionForInvokeMethod() $this->expectException(OutOfBoundsException::class); $this->expectExceptionCode(400); $this->expectExceptionMessage('out of bounds'); - ($this->di)([TestExceptionForInvokeMethod::class, 'fail']); + ($this->di)([new TestExceptionForInvokeMethod, 'fail']); } protected function createContainer(): DIContainer From 7f97c3bd7bb5cb73852249212e57c4f5928f983b Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 28 Mar 2020 11:28:51 +0100 Subject: [PATCH 24/37] cleanup --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index ca2ed74..bf04a06 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,4 +7,3 @@ /phpunit.* export-ignore /infection.* export-ignore /phpbench.* export-ignore -/.* export-ignore From 18fc8f123569dc928020129bef03d05ff62c3500 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 28 Mar 2020 11:29:35 +0100 Subject: [PATCH 25/37] - updates CI scripts --- .scrutinizer.yml | 16 +++++++++------- .travis.yml | 17 ++++++++++------- composer.json | 8 +------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index b44a99f..e84b0f2 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -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 diff --git a/.travis.yml b/.travis.yml index 9cf4622..b5a2c92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ -sudo: false language: php +os: linux +dist: xenial notifications: email: false @@ -11,19 +12,21 @@ cache: php: - 7.2 - 7.3 + - 7.4 - nightly -matrix: +jobs: fast_finish: true allow_failures: - php: nightly install: - - travis_retry composer update -o --no-interaction --prefer-source + - composer update -o --no-interaction --prefer-source 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: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover build/clover.xml + - vendor/bin/infection --threads=4 --min-msi=80 --min-covered-msi=80 --log-verbosity=none diff --git a/composer.json b/composer.json index 9a08a48..b99a228 100644 --- a/composer.json +++ b/composer.json @@ -36,17 +36,11 @@ "ext-json": "*", "phpunit/phpunit": "~7", "infection/infection": "^0.13", - "phpbench/phpbench": "@dev", - "scrutinizer/ocular": "^1.6" + "phpbench/phpbench": "@dev" }, "autoload-dev": { "psr-4": { "Koded\\Tests\\": "Tests/" } - }, - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } } } \ No newline at end of file From 673f7c7f6ad00564b4a8dbb6e6ebb4c4f5b58063 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Sat, 28 Mar 2020 11:31:14 +0100 Subject: [PATCH 26/37] cleanup --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a368ac9..db3b240 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,6 +29,6 @@ - + \ No newline at end of file From 8cde7326af5cca6e57fa809062fa627d11f3830e Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 10:47:16 +0200 Subject: [PATCH 27/37] - removes KodedException - changes the error codes --- DIException.php | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/DIException.php b/DIException.php index a1083b1..69a2d10 100644 --- a/DIException.php +++ b/DIException.php @@ -12,28 +12,36 @@ namespace Koded; -use Koded\Exceptions\KodedException; use Psr\Container\{ContainerExceptionInterface, NotFoundExceptionInterface}; +use LogicException; +use Throwable; -class DIException extends KodedException implements ContainerExceptionInterface +class DIException extends LogicException implements ContainerExceptionInterface { public const - E_CIRCULAR_DEPENDENCY = 1, - E_NON_PUBLIC_METHOD = 2, - E_CANNOT_INSTANTIATE = 3, - E_INVALID_PARAMETER_NAME = 4, - E_INSTANCE_NOT_FOUND = 5, - E_MISSING_ARGUMENT = 6; + E_CIRCULAR_DEPENDENCY = 7001, + E_NON_PUBLIC_METHOD = 7002, + E_CANNOT_INSTANTIATE = 7003, + E_INVALID_PARAMETER_NAME = 7004, + E_INSTANCE_NOT_FOUND = 7005, + E_MISSING_ARGUMENT = 7006; protected $messages = [ self::E_CIRCULAR_DEPENDENCY => 'Circular dependency detected while creating an instance for ":class"', self::E_NON_PUBLIC_METHOD => 'Failed to create an instance, because the method ":method" is not public', - self::E_CANNOT_INSTANTIATE => 'Cannot instantiate the ":type" :class', - self::E_INVALID_PARAMETER_NAME => 'Provide a valid name for the global parameter', + self::E_CANNOT_INSTANTIATE => 'Cannot instantiate the ":type :class"', + self::E_INVALID_PARAMETER_NAME => 'Provide a valid name for the global parameter: ":name"', self::E_INSTANCE_NOT_FOUND => 'The requested instance :id is not found in the container', self::E_MISSING_ARGUMENT => 'Required parameter "$:name" is missing at position :position in :function()', ]; + public function __construct(int $code, array $arguments = [], Throwable $previous = null) + { + parent::__construct(strtr( + $this->messages[$code] ?? '[Exception] :message', + $arguments + [':message' => $this->message] + ), $code, $previous); + } public static function forCircularDependency(string $class): ContainerExceptionInterface { @@ -50,9 +58,9 @@ public static function cannotInstantiate(string $class, string $type): Container return new self(self::E_CANNOT_INSTANTIATE, [':class' => $class, ':type' => $type]); } - public static function forInvalidParameterName(): ContainerExceptionInterface + public static function forInvalidParameterName(string $name): ContainerExceptionInterface { - return new self(self::E_INVALID_PARAMETER_NAME); + return new self(self::E_INVALID_PARAMETER_NAME, [':name' => $name]); } public static function forMissingArgument(string $name, int $position, string $function): ContainerExceptionInterface From 11b54af3190f1db641a3430229551b24334e1354 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 10:48:51 +0200 Subject: [PATCH 28/37] - more mutations --- infection.json.dist | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/infection.json.dist b/infection.json.dist index 1638fa6..65569f3 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -25,18 +25,7 @@ "customPath": "./vendor/bin/phpunit" }, "mutators": { - "@default": true, - "MethodCallRemoval": false, - "ArrayItemRemoval": false, - "TrueValue": { - "ignore": ["Koded\\DIContainer"] - }, - "FalseValue": { - "ignore": ["Koded\\DIContainer"] - }, - "Throw_": { - "ignore": ["Koded\\DIContainer"] - } + "@default": true }, "testFramework": "phpunit", "bootstrap": "./Tests/bootstrap.php" From ad1b2c5e5c82e4a35d6a66d2483004e29c032d3b Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 10:49:17 +0200 Subject: [PATCH 29/37] - cleanup --- phpbench.json.dist | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/phpbench.json.dist b/phpbench.json.dist index b6aa51b..9b2ccd4 100644 --- a/phpbench.json.dist +++ b/phpbench.json.dist @@ -3,11 +3,9 @@ "path": "Tests/PhpBench", "time_unit": "milliseconds", "php_disable_ini": false, - "reports": [ - { - "default": { - "generator": "table" - } + "reports": { + "default": { + "generator": "table" } - ] + } } \ No newline at end of file From c2dffcfe0c3b81df13a41f6175848f6f52b88039 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 10:49:40 +0200 Subject: [PATCH 30/37] - added VERSION file --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..867e524 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.2.0 \ No newline at end of file From 6e680088f3b0424a2d3489775def159458b9691c Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 10:50:54 +0200 Subject: [PATCH 31/37] - tweaks infection expectations --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b5a2c92..5c41100 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,4 +29,4 @@ script: after_script: - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover build/clover.xml - - vendor/bin/infection --threads=4 --min-msi=80 --min-covered-msi=80 --log-verbosity=none + - vendor/bin/infection --threads=4 --min-msi=77 --min-covered-msi=77 --log-verbosity=none From a0a05896ba3ea29d3f796558c1ba395216be3fe6 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 10:53:23 +0200 Subject: [PATCH 32/37] - updates the error message --- DIContainer.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DIContainer.php b/DIContainer.php index be74413..bc97bce 100644 --- a/DIContainer.php +++ b/DIContainer.php @@ -13,7 +13,6 @@ namespace Koded; use Psr\Container\{ContainerExceptionInterface, ContainerInterface}; -use Throwable; /** * Interface DIModule contributes the application configuration, @@ -197,7 +196,7 @@ public function bind(string $interface, string $class = ''): 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; From e678af3ace5f065e85db652e6b9f71774b510cb0 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 11:00:31 +0200 Subject: [PATCH 33/37] - updates --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f4746f..02bc807 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ composer require koded/container ## Example -Let's look at a simple blog application that has +Let's look at a blog application that has - interfaces for the database repositories and corresponding implementations - a shared PDO instance - a service class for the blog content fetching @@ -126,9 +126,21 @@ $container('method', 'arguments'); > To be continued... + +Code quality +------------ + +```shell script +vendor/bin/infection --threads=4 +vendor/bin/phpbench run --report=default +vendor/bin/phpunit +``` + + License ------- [![Software license](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE) + The code is distributed under the terms of [The 3-Clause BSD license](LICENSE). From 4cabe07be45a82a3a4c7512afad4db3132f12c56 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 11:21:25 +0200 Subject: [PATCH 34/37] - cleanup --- DIException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIException.php b/DIException.php index 69a2d10..4691d60 100644 --- a/DIException.php +++ b/DIException.php @@ -38,7 +38,7 @@ class DIException extends LogicException implements ContainerExceptionInterface public function __construct(int $code, array $arguments = [], Throwable $previous = null) { parent::__construct(strtr( - $this->messages[$code] ?? '[Exception] :message', + $this->messages[$code] ?? ':message', $arguments + [':message' => $this->message] ), $code, $previous); } From c6ca939a6d95e8b5d5623790f2006fde63b3185e Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 11:22:16 +0200 Subject: [PATCH 35/37] - removes the `koded/stdlib` dependency --- composer.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index b99a228..a544ed0 100644 --- a/composer.json +++ b/composer.json @@ -5,11 +5,11 @@ "description": "A simple Dependency Injection Container with application modules support", "license": "BSD-3-Clause", "keywords": [ - "dic", "dependency", "injection", "container", - "psr-11" + "psr-11", + "dic" ], "authors": [ { @@ -20,8 +20,7 @@ "prefer-stable": true, "require": { "php": "^7.2", - "psr/container": "~1", - "koded/stdlib": "~4" + "psr/container": "~1" }, "autoload": { "exclude-from-classmap": [ From 9a33c5b68ba1e623a5d342eb97cf470ba1d5fc5f Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 11:23:38 +0200 Subject: [PATCH 36/37] - updates the test --- Tests/Unit/NamedMethodTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/Unit/NamedMethodTest.php b/Tests/Unit/NamedMethodTest.php index 8e79cae..1519f79 100644 --- a/Tests/Unit/NamedMethodTest.php +++ b/Tests/Unit/NamedMethodTest.php @@ -24,8 +24,9 @@ public function testShouldThrowExceptionForInvalidParameterName($name) { $this->expectException(DIException::class); $this->expectExceptionCode(DIException::E_INVALID_PARAMETER_NAME); + $this->expectExceptionMessage('Provide a valid name for the global parameter: "'); - $this->di->named($name, null); + $this->di->named($name, 'test'); } public function invalidNames() From 912843e598cb9147f70df786f0302ae371977668 Mon Sep 17 00:00:00 2001 From: Mihail Binev Date: Fri, 5 Jun 2020 11:25:55 +0200 Subject: [PATCH 37/37] - updates configuration --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5c41100..df94ae5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,11 +22,11 @@ jobs: install: - composer update -o --no-interaction --prefer-source + - wget https://scrutinizer-ci.com/ocular.phar script: - vendor/bin/phpunit --coverage-clover=build/clover.xml after_script: - - wget https://scrutinizer-ci.com/ocular.phar - 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