From d1bd25a41adb2e338632254358684cd6704e9642 Mon Sep 17 00:00:00 2001 From: Martin Hughes Date: Mon, 7 Mar 2022 22:34:01 +0000 Subject: [PATCH 01/68] Add notice pointing at new repos. --- README.md | 12 ++++++++++++ composer.json | 1 + 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 23237e9..d211afe 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ auryn is a recursive dependency injector. Use auryn to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications. +## Maintenance status + +`rdlowrey/auryn` is no longer maintained. For a repo that is still under maintenance you have two +options: + +* Switch to [`amphp/injector`](https://github.com/amphp/injector). It is a significant rewrite and + uses a new namespace and slightly different interfaces, requiring you to update your code. It + will introduce new features and diverge over time from this repo. +* Use [`martin-hughes/auryn`](https://github.com/martin-hughes/auryn). It is a fork from this repo + and maintains the current namespace and interfaces. It is unlikely to introduce significant new + features, instead focussing on bugfixes and testing. + ##### How It Works Among other things, auryn recursively instantiates class dependencies based on the parameter diff --git a/composer.json b/composer.json index 8c79baf..da96040 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "rdlowrey/auryn", + "abandoned": true, "homepage": "https://github.com/rdlowrey/auryn", "description": "Auryn is a dependency injector for bootstrapping object-oriented PHP applications.", "keywords": ["dependency injection", "dic", "ioc"], From 283779d24cc6bdf206679c2a1fbf4442e8122b84 Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 9 Dec 2022 17:08:30 +0000 Subject: [PATCH 02/68] Updated words to be accurate for Dec 2022. --- README.md | 18 ++++++++++-------- composer.json | 1 - 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d211afe..65bb394 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,17 @@ S.O.L.I.D., object-oriented PHP applications. ## Maintenance status -`rdlowrey/auryn` is no longer maintained. For a repo that is still under maintenance you have two -options: - -* Switch to [`amphp/injector`](https://github.com/amphp/injector). It is a significant rewrite and - uses a new namespace and slightly different interfaces, requiring you to update your code. It - will introduce new features and diverge over time from this repo. -* Use [`martin-hughes/auryn`](https://github.com/martin-hughes/auryn). It is a fork from this repo - and maintains the current namespace and interfaces. It is unlikely to introduce significant new +`rdlowrey/auryn` is in low maintenance mode. i.e. new features are very unlikely to be added, and +new releases to support new versions of PHP are not guaranteed to be timely. + +There are similar libraries available at: + +* [`martin-hughes/auryn`](https://github.com/martin-hughes/auryn) is a fork from this repo + and maintains the current namespace and interfaces. It is unlikely to introduce significant new features, instead focussing on bugfixes and testing. +* [`amphp/injector`](https://github.com/amphp/injector) is a significant rewrite using a new + namespace and slightly different interfaces, requiring you to update your code. It will + introduce new features and diverge over time from this repo. ##### How It Works diff --git a/composer.json b/composer.json index da96040..8c79baf 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,5 @@ { "name": "rdlowrey/auryn", - "abandoned": true, "homepage": "https://github.com/rdlowrey/auryn", "description": "Auryn is a dependency injector for bootstrapping object-oriented PHP applications.", "keywords": ["dependency injection", "dic", "ioc"], From 57a427522350eaaaca763b408b7c898f5d60edee Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 19:12:45 +0000 Subject: [PATCH 03/68] Updated to modern version of PHPUnit and add phpunit switcher for CI. --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++++++++++++++ composer.json | 4 ++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..16ff785 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: "CI" + +on: + pull_request: + push: + +permissions: + contents: read + +jobs: + tests: + name: "PHP ${{ matrix.php-version }}" + + runs-on: 'ubuntu-latest' + + continue-on-error: ${{ matrix.experimental }} + + strategy: + matrix: + php-version: + - '7.1.28' + - '7.2.5' + - '7.3' + - '7.4' + - '8.0' + - '8.1' + experimental: [false] + + steps: + - name: "Checkout code" + uses: actions/checkout@v2 + + - name: "Install PHP with extensions" + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + php-version: ${{ matrix.php-version }} + ini-values: memory_limit=-1 + + - name: "Add PHPUnit matcher" + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - run: composer install + + - name: "Install PHPUnit" + run: vendor/bin/simple-phpunit install + + - name: "PHPUnit version" + run: vendor/bin/simple-phpunit --version + + - name: "Run tests" + run: vendor/bin/simple-phpunit diff --git a/composer.json b/composer.json index 8c79baf..6a3b62f 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,8 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8", - "fabpot/php-cs-fixer": "~1.9", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^6.2.0", "athletic/athletic": "~0.1" }, "autoload": { From 93920b09f32a5c3e393c5bdf008c34dde09e6f88 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 19:20:07 +0000 Subject: [PATCH 04/68] Dynamic property creation is no longer a thing. --- test/fixtures.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/fixtures.php b/test/fixtures.php index c562e0a..b77d762 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -158,6 +158,8 @@ class SpecdTestDependency extends TestDependency class TestNeedsDep { + private $testDep; + public function __construct(TestDependency $testDep) { $this->testDep = $testDep; @@ -184,6 +186,7 @@ public function __construct(TestDependency $val1, TestDependency2 $val2) class TestMultiDepsWithCtor { + private $testDep; public function __construct(TestDependency $val1, TestNeedsDep $val2) { $this->testDep = $val1; @@ -231,6 +234,7 @@ class DepImplementation implements DepInterface class RequiresInterface { public $dep; + private $testDep; public function __construct(DepInterface $dep) { $this->testDep = $dep; @@ -262,6 +266,7 @@ public function __construct(ClassInnerA $dep) class ProvTestNoDefinitionNullDefaultClass { + private $arg; public function __construct($arg = null) { $this->arg = $arg; @@ -322,6 +327,8 @@ public function __construct($arg1) class InjectorTestChildClass extends InjectorTestParentClass { + private $arg1; + private $arg2; public function __construct($arg1, $arg2) { parent::__construct($arg1); @@ -573,6 +580,7 @@ public static function create() class TestNeedsDepWithProtCons { + private $dep; public function __construct(TestDependencyWithProtectedConstructor $dep) { $this->dep = $dep; @@ -595,12 +603,12 @@ class SomeClassName class TestDelegationSimple { - public $delgateCalled = false; + public $delegateCalled = false; } class TestDelegationDependency { - public $delgateCalled = false; + public $delegateCalled = false; public function __construct(TestDelegationSimple $testDelegationSimple) { } @@ -721,6 +729,7 @@ class ChildWithoutConstructor extends ParentWithConstructor { class DelegateA {} class DelegatingInstanceA { + private $a; public function __construct(DelegateA $a) { $this->a = $a; } @@ -728,6 +737,7 @@ public function __construct(DelegateA $a) { class DelegateB {} class DelegatingInstanceB { + private $b; public function __construct(DelegateB $b) { $this->b = $b; } From 0460307d721377da13ff6ee3d02fa8a1a02754e0 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 19:22:01 +0000 Subject: [PATCH 05/68] Change config to avoid warnings on coverage only tests. --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index c997caa..8e03afb 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,6 +7,7 @@ processIsolation="false" stopOnFailure="false" syntaxCheck="false" + beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="vendor/autoload.php" > From c2549963c1c6e0a95f7e3806e7ffbb2ae6eefd63 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 19:25:03 +0000 Subject: [PATCH 06/68] Relax access for properties so that they can be read by tests. --- test/fixtures.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/fixtures.php b/test/fixtures.php index b77d762..5b62ae0 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -158,7 +158,7 @@ class SpecdTestDependency extends TestDependency class TestNeedsDep { - private $testDep; + public $testDep; public function __construct(TestDependency $testDep) { @@ -234,7 +234,7 @@ class DepImplementation implements DepInterface class RequiresInterface { public $dep; - private $testDep; + public $testDep; public function __construct(DepInterface $dep) { $this->testDep = $dep; @@ -266,7 +266,7 @@ public function __construct(ClassInnerA $dep) class ProvTestNoDefinitionNullDefaultClass { - private $arg; + public $arg; public function __construct($arg = null) { $this->arg = $arg; @@ -319,6 +319,7 @@ public function __construct($string, $obj, $int, $array, $float, $bool, $null) class InjectorTestParentClass { + public $arg1; public function __construct($arg1) { $this->arg1 = $arg1; @@ -327,8 +328,8 @@ public function __construct($arg1) class InjectorTestChildClass extends InjectorTestParentClass { - private $arg1; - private $arg2; + public $arg1; + public $arg2; public function __construct($arg1, $arg2) { parent::__construct($arg1); @@ -580,7 +581,7 @@ public static function create() class TestNeedsDepWithProtCons { - private $dep; + public $dep; public function __construct(TestDependencyWithProtectedConstructor $dep) { $this->dep = $dep; @@ -729,7 +730,7 @@ class ChildWithoutConstructor extends ParentWithConstructor { class DelegateA {} class DelegatingInstanceA { - private $a; + public $a; public function __construct(DelegateA $a) { $this->a = $a; } @@ -737,7 +738,7 @@ public function __construct(DelegateA $a) { class DelegateB {} class DelegatingInstanceB { - private $b; + public $b; public function __construct(DelegateB $b) { $this->b = $b; } From c911e1857114cef34c0c8b5546c16219555017c8 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 19:28:20 +0000 Subject: [PATCH 07/68] Ignore files that should never be added. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 61064cb..3e06538 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ vendor/ composer.lock .idea +/.phpunit.result.cache +/composer.phar From 15d947be6766ae23103f76f46c30b921c84a38e6 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 19:41:14 +0000 Subject: [PATCH 08/68] Migrated PHPUnit config file to modern format. --- phpunit.xml | 52 +++++++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 8e03afb..cdd5b7c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,35 +1,21 @@ - - - - ./test - - - - - ./lib - - - - - - - + - + From 1e59a90983eb6edcbd199b0c04b63106994a2b5d Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 20:37:49 +0000 Subject: [PATCH 09/68] Fix majority of tests. Remaining ones are more difficult. --- test/InjectorTest.php | 265 ++++++++++++++++++++++-------------------- 1 file changed, 142 insertions(+), 123 deletions(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 60977b8..afef2f8 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -35,24 +35,21 @@ public function testMakeInstanceReturnsAliasInstanceOnNonConcreteTypehint() $this->assertEquals(new DepImplementation, $injector->make('Auryn\Test\DepInterface')); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - * @expectedExceptionCode \Auryn\Injector::E_NEEDS_DEFINITION - */ public function testMakeInstanceThrowsExceptionOnInterfaceWithoutAlias() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); + $this->expectExceptionCode(\Auryn\Injector::E_NEEDS_DEFINITION); $injector = new Injector; $injector->make('Auryn\Test\DepInterface'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - * @expectedExceptionCode \Auryn\Injector::E_NEEDS_DEFINITION - */ public function testMakeInstanceThrowsExceptionOnNonConcreteCtorParamWithoutImplementation() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); + $this->expectExceptionCode(\Auryn\Injector::E_NEEDS_DEFINITION); + $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -93,6 +90,9 @@ public function testMakeInstanceReturnsSharedInstanceIfAvailable() */ public function testMakeInstanceThrowsExceptionOnClassLoadFailure() { + $this->expectException(\Auryn\InjectorException::class); + $this->expectExceptionMessage("Could not make ClassThatDoesntExist: Class \"ClassThatDoesntExist\" does not exist"); + $injector = new Injector; $injector->make('ClassThatDoesntExist'); } @@ -169,28 +169,28 @@ public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadi array('arg'=>'Auryn\Test\TestDependency') ); $this->assertInstanceOf('Auryn\Test\TypehintNoDefaultConstructorVariadicClass', $obj); - $this->assertInternalType("array", $obj->testParam); + $this->assertIsArray($obj->testParam); $this->assertInstanceOf('Auryn\Test\TestDependency', $obj->testParam[0]); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault:: - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM - */ public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDefinitionOrDefault() { + $this->expectException(\Auryn\InjectionException::class); + // TODO - why does this message end with double-colon? + $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::'); + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector = new Injector; $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault:: - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM - */ public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDefinitionOrDefaultThroughAliasedTypehint() { + $this->expectException(\Auryn\InjectionException::class); + // TODO - why does this message end with double-colon? + $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::'); + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector = new Injector; $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault'); $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefaultDependent'); @@ -198,11 +198,15 @@ public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDef /** * @TODO - * @expectedException \Auryn\InjectorException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface + * @expectedException + * @expectedExceptionMessage */ public function testMakeInstanceThrowsExceptionOnUninstantiableTypehintWithoutDefinition() { + $this->expectException(\Auryn\InjectorException::class); + $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); + + $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -239,20 +243,15 @@ public function testMakeInstanceInjectsRawParametersDirectly() )); $obj = $injector->make('Auryn\Test\InjectorTestRawCtorParams'); - $this->assertInternalType('string', $obj->string); + $this->assertIsString($obj->string); $this->assertInstanceOf('StdClass', $obj->obj); - $this->assertInternalType('int', $obj->int); - $this->assertInternalType('array', $obj->array); - $this->assertInternalType('float', $obj->float); - $this->assertInternalType('bool', $obj->bool); + $this->assertIsInt($obj->int); + $this->assertIsArray($obj->array); + $this->assertIsFloat($obj->float); + $this->assertIsBool($obj->bool); $this->assertNull($obj->null); } - /** - * @TODO - * @expectedException \Exception - * @expectedExceptionMessage - */ public function testMakeInstanceThrowsExceptionWhenDelegateDoes() { $injector= new Injector; @@ -264,9 +263,13 @@ public function testMakeInstanceThrowsExceptionWhenDelegateDoes() $injector->delegate('TestDependency', $callable); + $message = "This is the expected exception."; $callable->expects($this->once()) ->method('__invoke') - ->will($this->throwException(new \Exception())); + ->will($this->throwException(new \Exception($message))); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage($message); $injector->make('TestDependency'); } @@ -304,13 +307,13 @@ public function testMakeInstanceWithStringDelegate() $this->assertEquals(42, $obj->test); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'StringDelegateWithNoInvokeMethod' - */ public function testMakeInstanceThrowsExceptionIfStringDelegateClassHasNoInvokeMethod() { $injector= new Injector; + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage("Auryn\\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'StringDelegateWithNoInvokeMethod'"); + $injector->delegate('StdClass', 'StringDelegateWithNoInvokeMethod'); } @@ -320,16 +323,18 @@ public function testMakeInstanceThrowsExceptionIfStringDelegateClassHasNoInvokeM */ public function testMakeInstanceThrowsExceptionIfStringDelegateClassInstantiationFails() { + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage("Auryn\\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'SomeClassThatDefinitelyDoesNotExistForReal'"); + $injector= new Injector; $injector->delegate('StdClass', 'SomeClassThatDefinitelyDoesNotExistForReal'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - */ public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithNoDefinition() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Injection definition required for interface Auryn\Test\DepInterface'); + $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -359,12 +364,11 @@ public function testShareMarksClassSharedOnNullObjectParameter() $this->assertInstanceOf('Auryn\Injector', $injector->share('SomeClass')); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::share() requires a string class name or object instance at Argument 1; integer specified - */ public function testShareThrowsExceptionOnInvalidArgument() { + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Auryn\Injector::share() requires a string class name or object instance at Argument 1; integer specified'); + $injector = new Injector; $injector->share(42); } @@ -386,11 +390,14 @@ public function provideInvalidDelegates() /** * @dataProvider provideInvalidDelegates - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2 + * @expectedException + * @expectedExceptionMessage */ public function testDelegateThrowsExceptionIfDelegateIsNotCallableOrString($badDelegate) { + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2'); + $injector = new Injector; $injector->delegate('Auryn\Test\TestDependency', $badDelegate); } @@ -416,7 +423,10 @@ public function testUnknownDelegationFunction() $injector->delegate('Auryn\Test\DelegatableInterface', 'FunctionWhichDoesNotExist'); $this->fail("Delegation was supposed to fail."); } catch (\Auryn\InjectorException $ie) { - $this->assertContains('FunctionWhichDoesNotExist', $ie->getMessage()); + $this->assertStringContainsString( + 'FunctionWhichDoesNotExist', + $ie->getMessage() + ); $this->assertEquals(\Auryn\Injector::E_DELEGATE_ARGUMENT, $ie->getCode()); } } @@ -428,8 +438,14 @@ public function testUnknownDelegationMethod() $injector->delegate('Auryn\Test\DelegatableInterface', array('stdClass', 'methodWhichDoesNotExist')); $this->fail("Delegation was supposed to fail."); } catch (\Auryn\InjectorException $ie) { - $this->assertContains('stdClass', $ie->getMessage()); - $this->assertContains('methodWhichDoesNotExist', $ie->getMessage()); + $this->assertStringContainsString( + 'stdClass', + $ie->getMessage() + ); + $this->assertStringContainsString( + 'methodWhichDoesNotExist', + $ie->getMessage() + ); $this->assertEquals(\Auryn\Injector::E_DELEGATE_ARGUMENT, $ie->getCode()); } } @@ -612,12 +628,11 @@ public function testInterfaceFactoryDelegation() $requiresDelegatedInterface->foo(); } - /** - * @expectedException \Auryn\InjectorException - * @expectedExceptionMessage Could not make Auryn\Test\TestMissingDependency: Class Auryn\Test\TypoInTypehint does not exist - */ public function testMissingAlias() { + $this->expectException(\Auryn\InjectorException::class); + $this->expectExceptionMessage('Could not make Auryn\Test\TypoInTypehint: Class "Auryn\Test\TypoInTypehint" does not exist'); + $injector = new Injector; $testClass = $injector->make('Auryn\Test\TestMissingDependency'); } @@ -738,11 +753,12 @@ public function provideCyclicDependencies() /** * @dataProvider provideCyclicDependencies - * @expectedException \Auryn\InjectionException - * @expectedExceptionCode \Auryn\Injector::E_CYCLIC_DEPENDENCY */ public function testCyclicDependencies($class) { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionCode(\Auryn\Injector::E_CYCLIC_DEPENDENCY); + $injector = new Injector; $injector->make($class); } @@ -790,16 +806,16 @@ public function testDependencyWithDefaultValueThroughShare() $this->assertInstanceOf('StdClass', $instance->dependency); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Cannot share class stdclass because it is currently aliased to Auryn\Test\SomeOtherClass - * @expectedExceptionCode \Auryn\Injector::E_ALIASED_CANNOT_SHARE - */ public function testShareAfterAliasException() { $injector = new Injector(); $testClass = new \StdClass(); $injector->alias('StdClass', 'Auryn\Test\SomeOtherClass'); + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Cannot share class stdclass because it is currently aliased to Auryn\Test\SomeOtherClass'); + $this->expectExceptionCode(\Auryn\Injector::E_ALIASED_CANNOT_SHARE); + $injector->share($testClass); } @@ -835,37 +851,39 @@ public function testAliasAfterShareBySharingAliasAllowed() $this->assertEquals($obj, $obj2); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Cannot alias class stdclass to Auryn\Test\SomeOtherClass because it is currently shared - * @expectedExceptionCode \Auryn\Injector::E_SHARED_CANNOT_ALIAS - */ public function testAliasAfterShareException() { $injector = new Injector(); $testClass = new \StdClass(); $injector->share($testClass); + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Cannot alias class stdclass to Auryn\Test\SomeOtherClass because it is currently shared'); + $this->expectExceptionCode(\Auryn\Injector::E_SHARED_CANNOT_ALIAS); $injector->alias('StdClass', 'Auryn\Test\SomeOtherClass'); } /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructor - * @expectedExceptionCode \Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR + * @expectedException + * @expectedExceptionMessage + * @expectedExceptionCode */ public function testAppropriateExceptionThrownOnNonPublicConstructor() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructor'); + $this->expectExceptionCode(\Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR); + $injector = new Injector(); $injector->make('Auryn\Test\HasNonPublicConstructor'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructorWithArgs - * @expectedExceptionCode \Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR - */ public function testAppropriateExceptionThrownOnNonPublicConstructorWithArgs() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructorWithArgs'); + $this->expectExceptionCode(\Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR); + $injector = new Injector(); $injector->make('Auryn\Test\HasNonPublicConstructorWithArgs'); } @@ -873,11 +891,11 @@ public function testAppropriateExceptionThrownOnNonPublicConstructorWithArgs() public function testMakeExecutableFailsOnNonExistentFunction() { $injector = new Injector(); - $this->setExpectedException( - 'Auryn\InjectionException', - 'nonExistentFunction', - \Auryn\Injector::E_INVOKABLE - ); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('nonExistentFunction'); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); + + $injector->buildExecutable('nonExistentFunction'); } @@ -885,46 +903,43 @@ public function testMakeExecutableFailsOnNonExistentInstanceMethod() { $injector = new Injector(); $object = new \StdClass(); - $this->setExpectedException( - 'Auryn\InjectionException', - "[object(stdClass), 'nonExistentMethod']", - \Auryn\Injector::E_INVOKABLE - ); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("[object(stdClass), 'nonExistentMethod']"); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); $injector->buildExecutable(array($object, 'nonExistentMethod')); } public function testMakeExecutableFailsOnNonExistentStaticMethod() { $injector = new Injector(); - $this->setExpectedException( - 'Auryn\InjectionException', - "StdClass::nonExistentMethod", - \Auryn\Injector::E_INVOKABLE - ); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("StdClass::nonExistentMethod"); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); + $injector->buildExecutable(array('StdClass', 'nonExistentMethod')); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Invalid invokable: callable or provisional string required - * @expectedExceptionCode \Auryn\Injector::E_INVOKABLE - */ public function testMakeExecutableFailsOnClassWithoutInvoke() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Invalid invokable: callable or provisional string required'); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); + $injector = new Injector(); $object = new \StdClass(); $injector->buildExecutable($object); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Invalid alias: non-empty string required at arguments 1 and 2 - * @expectedExceptionCode \Auryn\Injector::E_NON_EMPTY_STRING_ALIAS - */ public function testBadAlias() { $injector = new Injector(); $injector->share('Auryn\Test\DepInterface'); + + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Invalid alias: non-empty string required at arguments 1 and 2'); + $this->expectExceptionCode(\Auryn\Injector::E_NON_EMPTY_STRING_ALIAS); + $injector->alias('Auryn\Test\DepInterface', ''); } @@ -982,14 +997,17 @@ public function testInterfaceMutate() /** * Test that custom definitions are not passed through to dependencies. * Surprising things would happen if this did occur. - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\DependencyWithDefinedParam::__construct() declared in Auryn\Test\DependencyWithDefinedParam:: - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM */ public function testCustomDefinitionNotPassedThrough() { $injector = new Injector(); $injector->share('Auryn\Test\DependencyWithDefinedParam'); + + $this->expectException(\Auryn\InjectionException::class); + // TODO - why does this message end with double-colon? + $this->expectExceptionMessage('No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\DependencyWithDefinedParam::__construct() declared in Auryn\Test\DependencyWithDefinedParam::'); + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector->make('Auryn\Test\RequiresDependencyWithDefinedParam', array(':foo' => 5)); } @@ -1115,11 +1133,6 @@ public function testInspectAll() $this->assertCount(2, array_filter($some)); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Making auryn\test\someclassname did not result in an object, instead result is of type 'NULL' - * @expectedExceptionCode \Auryn\Injector::E_MAKING_FAILED - */ public function testDelegationDoesntMakeObject() { $delegate = function () { @@ -1127,14 +1140,14 @@ public function testDelegationDoesntMakeObject() }; $injector = new Injector(); $injector->delegate('Auryn\Test\SomeClassName', $delegate); + + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Making auryn\test\someclassname did not result in an object, instead result is of type \'NULL\''); + $this->expectExceptionCode(\Auryn\Injector::E_MAKING_FAILED); + $injector->make('Auryn\Test\SomeClassName'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Making auryn\test\someclassname did not result in an object, instead result is of type 'string' - * @expectedExceptionCode \Auryn\Injector::E_MAKING_FAILED - */ public function testDelegationDoesntMakeObjectMakesString() { $delegate = function () { @@ -1142,6 +1155,11 @@ public function testDelegationDoesntMakeObjectMakesString() }; $injector = new Injector(); $injector->delegate('Auryn\Test\SomeClassName', $delegate); + + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Making auryn\test\someclassname did not result in an object, instead result is of type \'string\''); + $this->expectExceptionCode(\Auryn\Injector::E_MAKING_FAILED); + $injector->make('Auryn\Test\SomeClassName'); } @@ -1149,7 +1167,9 @@ public function testPrepareInvalidCallable() { $injector = new Injector; $invalidCallable = 'This_does_not_exist'; - $this->setExpectedException('Auryn\InjectionException', $invalidCallable); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage($invalidCallable); + $injector->prepare("StdClass", $invalidCallable); } @@ -1197,15 +1217,15 @@ public function testChildWithoutConstructorWorks() { } } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM - * @expectedExceptionMessage No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\ChildWithoutConstructor::__construct() declared in Auryn\Test\ParentWithConstructor - */ public function testChildWithoutConstructorMissingParam() { $injector = new Injector; $injector->define('Auryn\Test\ParentWithConstructor', array(':foo' => 'parent')); + + + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\ChildWithoutConstructor::__construct() declared in Auryn\Test\ParentWithConstructor'); + $injector->make('Auryn\Test\ChildWithoutConstructor'); } @@ -1226,10 +1246,6 @@ public function testInstanceClosureDelegates() $this->assertInstanceOf('Auryn\Test\DelegateB', $b->b); } - /** - * @expectedException \Exception - * @expectedExceptionMessage Exception in constructor - */ public function testThatExceptionInConstructorDoesntCauseCyclicDependencyException() { $injector = new Injector; @@ -1240,6 +1256,9 @@ public function testThatExceptionInConstructorDoesntCauseCyclicDependencyExcepti catch (\Exception $e) { } + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Exception in constructor'); + $injector->make('Auryn\Test\ThrowsExceptionInConstructor'); } } From 2ad6fc330e2a777f40f9def1b3eea672fd8e683f Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 20:43:13 +0000 Subject: [PATCH 10/68] Fix two mocking errors. --- test/InjectorTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index afef2f8..516fc4a 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -256,8 +256,8 @@ public function testMakeInstanceThrowsExceptionWhenDelegateDoes() { $injector= new Injector; - $callable = $this->getMock( - 'CallableMock', + $callable = $this->createPartialMock( + 'Auryn\test\CallableMock', array('__invoke') ); @@ -284,10 +284,11 @@ public function testMakeInstanceDelegate() { $injector= new Injector; - $callable = $this->getMock( - 'CallableMock', + $callable = $this->createPartialMock( + 'Auryn\test\CallableMock', array('__invoke') ); + $callable->expects($this->once()) ->method('__invoke') ->will($this->returnValue(new TestDependency())); From e57ba16c06a1d7a988d073fc669210669e533a5e Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:07:17 +0000 Subject: [PATCH 11/68] Skip tests that need proper investigation. --- test/InjectorTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 516fc4a..f478c5a 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -774,6 +774,7 @@ public function testNonConcreteDependencyWithDefault() public function testNonConcreteDependencyWithDefaultValueThroughAlias() { + $this->markTestSkipped("seems to be a legitimate failure"); $injector = new Injector; $injector->alias( 'Auryn\Test\DelegatableInterface', @@ -786,6 +787,7 @@ public function testNonConcreteDependencyWithDefaultValueThroughAlias() public function testNonConcreteDependencyWithDefaultValueThroughDelegation() { + $this->markTestSkipped("seems to be a legitimate failure"); $injector = new Injector; $injector->delegate('Auryn\Test\DelegatableInterface', 'Auryn\Test\ImplementsInterfaceFactory'); $class = $injector->make('Auryn\Test\NonConcreteDependencyWithDefaultValue'); @@ -795,6 +797,7 @@ public function testNonConcreteDependencyWithDefaultValueThroughDelegation() public function testDependencyWithDefaultValueThroughShare() { + $this->markTestSkipped("seems to be a legitimate failure"); $injector = new Injector; //Instance is not shared, null default is used for dependency $instance = $injector->make('Auryn\Test\ConcreteDependencyWithDefaultValue'); From c6709a7da652cc3e18b2bb299c35cd0aa63ac81d Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:09:07 +0000 Subject: [PATCH 12/68] Fix test testDelegationDoesntMakeObject --- lib/Injector.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Injector.php b/lib/Injector.php index 1b5b3c0..b2dc0c0 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -593,7 +593,13 @@ private function prepareInstance($obj, $normalizedClass) } } - $interfaces = @class_implements($obj); + // TODO - this is inelegant + if ($obj === null) { + $interfaces = false; + } + else { + $interfaces = @class_implements($obj); + } if ($interfaces === false) { throw new InjectionException( From d01a31a440d9ace385f23675a9a84265cfc43181 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:16:37 +0000 Subject: [PATCH 13/68] Continue running in CI on failures for now. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16ff785..4f794c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - '7.4' - '8.0' - '8.1' - experimental: [false] + experimental: [true] steps: - name: "Checkout code" From 23274eaeae1594e87aaf5d4a138023758e24cdc1 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:19:35 +0000 Subject: [PATCH 14/68] Try removing actual phpunit to see if it makes tests run. --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 6a3b62f..5e23012 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,6 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.26", "symfony/phpunit-bridge": "^6.2.0", "athletic/athletic": "~0.1" }, From f6d498efe764f2473e092604075ad25b7354cfd1 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:22:05 +0000 Subject: [PATCH 15/68] Remove old annotations as they are giving deprecation warnings. --- test/InjectorTest.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index f478c5a..2d26075 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -84,10 +84,6 @@ public function testMakeInstanceReturnsSharedInstanceIfAvailable() $this->assertEquals('something else', $injected2->testDep->testProp); } - /** - * @expectedException \Auryn\InjectorException - * @expectedExceptionMessage Could not make ClassThatDoesntExist: Class ClassThatDoesntExist does not exist - */ public function testMakeInstanceThrowsExceptionOnClassLoadFailure() { $this->expectException(\Auryn\InjectorException::class); @@ -196,17 +192,11 @@ public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDef $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefaultDependent'); } - /** - * @TODO - * @expectedException - * @expectedExceptionMessage - */ public function testMakeInstanceThrowsExceptionOnUninstantiableTypehintWithoutDefinition() { $this->expectException(\Auryn\InjectorException::class); $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); - $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -318,10 +308,6 @@ public function testMakeInstanceThrowsExceptionIfStringDelegateClassHasNoInvokeM $injector->delegate('StdClass', 'StringDelegateWithNoInvokeMethod'); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'SomeClassThatDefinitelyDoesNotExistForReal' - */ public function testMakeInstanceThrowsExceptionIfStringDelegateClassInstantiationFails() { $this->expectException(\Auryn\ConfigException::class); @@ -391,8 +377,6 @@ public function provideInvalidDelegates() /** * @dataProvider provideInvalidDelegates - * @expectedException - * @expectedExceptionMessage */ public function testDelegateThrowsExceptionIfDelegateIsNotCallableOrString($badDelegate) { @@ -867,11 +851,6 @@ public function testAliasAfterShareException() $injector->alias('StdClass', 'Auryn\Test\SomeOtherClass'); } - /** - * @expectedException - * @expectedExceptionMessage - * @expectedExceptionCode - */ public function testAppropriateExceptionThrownOnNonPublicConstructor() { $this->expectException(\Auryn\InjectionException::class); From 321aae32ceeeaeda0de50de7cca18b3c08185bd0 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:28:10 +0000 Subject: [PATCH 16/68] Double-check having phpunit breaks CI. --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 5e23012..6a3b62f 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "php": ">=5.3.0" }, "require-dev": { + "phpunit/phpunit": "^9.5.26", "symfony/phpunit-bridge": "^6.2.0", "athletic/athletic": "~0.1" }, From b2fbae6ab681bc3c1abb798f9bdbd5b91fcd8e83 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:33:50 +0000 Subject: [PATCH 17/68] Update badge to my repo for now. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65bb394..b87d575 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# auryn [![Build Status](https://travis-ci.org/rdlowrey/auryn.svg?branch=master)](https://travis-ci.org/rdlowrey/auryn) +# auryn [![Build Status](https://github.com/Danack/Auryn/actions/workflows/ci.yml/badge.svg?branch=adding_ci)](https://github.com/Danack/Auryn/actions) auryn is a recursive dependency injector. Use auryn to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications. From 067bc792fdf6f9f5a2ecdffd29f0dec7d6e92954 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:47:48 +0000 Subject: [PATCH 18/68] Make name easier to understand. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f794c5..ffabe1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ permissions: jobs: tests: - name: "PHP ${{ matrix.php-version }}" + name: "Test on PHP ${{ matrix.php-version }}" runs-on: 'ubuntu-latest' From 0f3b07f8f453c8cdb6f67dddc68ea8f1c8227486 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 22:08:01 +0000 Subject: [PATCH 19/68] Investigate when behaviour changed. --- test/InjectorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 2d26075..5c57299 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -758,7 +758,6 @@ public function testNonConcreteDependencyWithDefault() public function testNonConcreteDependencyWithDefaultValueThroughAlias() { - $this->markTestSkipped("seems to be a legitimate failure"); $injector = new Injector; $injector->alias( 'Auryn\Test\DelegatableInterface', From aff99a56237da19b3936c44a0bd37f0b6840da65 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 22:10:58 +0000 Subject: [PATCH 20/68] Remove phpunit again, it is causing a problem as a direct dependency. --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 6a3b62f..5e23012 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,6 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.26", "symfony/phpunit-bridge": "^6.2.0", "athletic/athletic": "~0.1" }, From 8b46ab08da94863e630f090627a51231d4a048c2 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 22:46:13 +0000 Subject: [PATCH 21/68] Adjust tests expectation based on which version of PHP they are being run on, as the PHP exception message changed. --- test/InjectorTest.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 5c57299..7c7b71e 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -86,8 +86,13 @@ public function testMakeInstanceReturnsSharedInstanceIfAvailable() public function testMakeInstanceThrowsExceptionOnClassLoadFailure() { + $classname = 'Auryn\Test\TypoInTypehint'; + if (PHP_VERSION_ID >= 80000) { + $classname = "\n" . $classname . "\n"; + } + $this->expectException(\Auryn\InjectorException::class); - $this->expectExceptionMessage("Could not make ClassThatDoesntExist: Class \"ClassThatDoesntExist\" does not exist"); + $this->expectExceptionMessage("Could not make ClassThatDoesntExist: Class $classname does not exist"); $injector = new Injector; $injector->make('ClassThatDoesntExist'); @@ -615,8 +620,15 @@ public function testInterfaceFactoryDelegation() public function testMissingAlias() { + $classname = 'Auryn\Test\TypoInTypehint'; + if (PHP_VERSION_ID >= 80000) { + $classname = "\n" . $classname . "\n"; + } + $this->expectException(\Auryn\InjectorException::class); - $this->expectExceptionMessage('Could not make Auryn\Test\TypoInTypehint: Class "Auryn\Test\TypoInTypehint" does not exist'); + $this->expectExceptionMessage( + "Could not make Auryn\Test\TypoInTypehint: Class $classname does not exist" + ); $injector = new Injector; $testClass = $injector->make('Auryn\Test\TestMissingDependency'); From ae2dee408fc987991799e00e4fe48a36db4c0505 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 22:52:25 +0000 Subject: [PATCH 22/68] Correct typos. --- test/InjectorTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 7c7b71e..a4dfbbc 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -86,9 +86,9 @@ public function testMakeInstanceReturnsSharedInstanceIfAvailable() public function testMakeInstanceThrowsExceptionOnClassLoadFailure() { - $classname = 'Auryn\Test\TypoInTypehint'; + $classname = 'ClassThatDoesntExist'; if (PHP_VERSION_ID >= 80000) { - $classname = "\n" . $classname . "\n"; + $classname = "\"" . $classname . "\""; } $this->expectException(\Auryn\InjectorException::class); @@ -622,7 +622,7 @@ public function testMissingAlias() { $classname = 'Auryn\Test\TypoInTypehint'; if (PHP_VERSION_ID >= 80000) { - $classname = "\n" . $classname . "\n"; + $classname = "\"" . $classname . "\""; } $this->expectException(\Auryn\InjectorException::class); From b7771b8a01166668b9f46136f9ec18a889c6e417 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 22:55:31 +0000 Subject: [PATCH 23/68] Fix spelling to work on PHP 7.x - might still be broken on 8+ --- test/InjectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index a4dfbbc..0c908ca 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -627,7 +627,7 @@ public function testMissingAlias() $this->expectException(\Auryn\InjectorException::class); $this->expectExceptionMessage( - "Could not make Auryn\Test\TypoInTypehint: Class $classname does not exist" + "Could not make Auryn\\Test\\TestMissingDependency: Class $classname does not exist" ); $injector = new Injector; From d037f7dc70adc111ae07412048923521e9fabf49 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 23:56:24 +0000 Subject: [PATCH 24/68] Hack fix for InjectorTest::testNonConcreteDependencyWithDefaultValueThroughAlias. --- lib/Injector.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Injector.php b/lib/Injector.php index b2dc0c0..9d28bf7 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -141,7 +141,9 @@ public function alias($original, $alias) private function normalizeName($className) { - return ltrim(strtolower($className), '\\'); + $tmp = ltrim(strtolower($className), '\\'); + + return ltrim($tmp, '?'); } /** From 7ecc162c09c578b01d28dfe700668f03772d9447 Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 9 Dec 2022 16:47:56 +0000 Subject: [PATCH 25/68] Unskip other tests that are now working. --- test/InjectorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 0c908ca..0a673d0 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -782,7 +782,6 @@ public function testNonConcreteDependencyWithDefaultValueThroughAlias() public function testNonConcreteDependencyWithDefaultValueThroughDelegation() { - $this->markTestSkipped("seems to be a legitimate failure"); $injector = new Injector; $injector->delegate('Auryn\Test\DelegatableInterface', 'Auryn\Test\ImplementsInterfaceFactory'); $class = $injector->make('Auryn\Test\NonConcreteDependencyWithDefaultValue'); @@ -792,7 +791,6 @@ public function testNonConcreteDependencyWithDefaultValueThroughDelegation() public function testDependencyWithDefaultValueThroughShare() { - $this->markTestSkipped("seems to be a legitimate failure"); $injector = new Injector; //Instance is not shared, null default is used for dependency $instance = $injector->make('Auryn\Test\ConcreteDependencyWithDefaultValue'); From 874b26b6b1c871e1a15451d92037bddc152d4461 Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 9 Dec 2022 17:44:25 +0000 Subject: [PATCH 26/68] Drop support for PHP < 7.3 --- .github/workflows/ci.yml | 13 ++----------- composer.json | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffabe1a..65b1714 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,18 +13,15 @@ jobs: runs-on: 'ubuntu-latest' - continue-on-error: ${{ matrix.experimental }} + continue-on-error: true strategy: matrix: php-version: - - '7.1.28' - - '7.2.5' - '7.3' - '7.4' - '8.0' - '8.1' - experimental: [true] steps: - name: "Checkout code" @@ -42,11 +39,5 @@ jobs: - run: composer install - - name: "Install PHPUnit" - run: vendor/bin/simple-phpunit install - - - name: "PHPUnit version" - run: vendor/bin/simple-phpunit --version - - name: "Run tests" - run: vendor/bin/simple-phpunit + run: vendor/bin/phpunit diff --git a/composer.json b/composer.json index 5e23012..4802028 100644 --- a/composer.json +++ b/composer.json @@ -25,10 +25,10 @@ } ], "require": { - "php": ">=5.3.0" + "php": ">=7.3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^6.2.0", + "phpunit/phpunit": "^9.5.27", "athletic/athletic": "~0.1" }, "autoload": { From 113f8c79f30d02c70862b526f2036ed6f753292b Mon Sep 17 00:00:00 2001 From: koenhoeymans Date: Tue, 7 Aug 2018 16:41:12 +0200 Subject: [PATCH 27/68] fix rdlowrey/auryn#129 --- lib/Injector.php | 2 +- test/InjectorTest.php | 12 ++++++++++-- test/fixtures.php | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/Injector.php b/lib/Injector.php index 9d28bf7..ee49553 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -476,7 +476,7 @@ private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) { $arg = $this->buildArgFromReflParam($reflParam, $className); - if ($arg === null && PHP_VERSION_ID >= 50600 && $reflParam->isVariadic()) { + if ($arg === null && (PHP_VERSION_ID >= 50600 && $reflParam->isVariadic() || $reflParam->isOptional())) { // buildArgFromReflParam might return null in case the parameter is optional // in case of variadics, the parameter is optional, but null might not be allowed continue; diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 0a673d0..5044c3a 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1238,14 +1238,14 @@ public function testInstanceClosureDelegates() $this->assertInstanceOf('Auryn\Test\DelegateB', $b->b); } + public function testThatExceptionInConstructorDoesntCauseCyclicDependencyException() { $injector = new Injector; try { $injector->make('Auryn\Test\ThrowsExceptionInConstructor'); - } - catch (\Exception $e) { + } catch (\Exception $e) { } $this->expectException(\Exception::class); @@ -1253,4 +1253,12 @@ public function testThatExceptionInConstructorDoesntCauseCyclicDependencyExcepti $injector->make('Auryn\Test\ThrowsExceptionInConstructor'); } + + public function testProvidesExtensionsOfArrayMap() + { + $injector = New Injector; + $obj = $injector->make('\Auryn\Test\ExtendedExtendedArrayObject'); + + $this->assertInstanceOf('\ArrayObject', $obj); + } } diff --git a/test/fixtures.php b/test/fixtures.php index 5b62ae0..668b142 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -744,8 +744,12 @@ public function __construct(DelegateB $b) { } } + class ThrowsExceptionInConstructor { public function __construct() { throw new \Exception('Exception in constructor'); } } + +class ExtendedArrayObject extends \ArrayObject {} +class ExtendedExtendedArrayObject extends ExtendedArrayObject {} From 1f8809a5163d54fd24758dcd315f3810cc99b440 Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 9 Dec 2022 18:04:01 +0000 Subject: [PATCH 28/68] Add PHP 8.2 to testing. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65b1714..c8dd1ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: - '7.4' - '8.0' - '8.1' + - '8.2' steps: - name: "Checkout code" From 76070d031a7a17b34007e29f9356f60d767d318a Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 9 Dec 2022 18:38:56 +0000 Subject: [PATCH 29/68] Change test rather than changing code...technically a BC break, but probably appropriate. --- test/InjectorTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 5044c3a..e23aff9 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -620,14 +620,16 @@ public function testInterfaceFactoryDelegation() public function testMissingAlias() { + $reportedClassname = 'TestMissingDependency'; $classname = 'Auryn\Test\TypoInTypehint'; if (PHP_VERSION_ID >= 80000) { $classname = "\"" . $classname . "\""; + $reportedClassname = 'TypoInTypehint'; } $this->expectException(\Auryn\InjectorException::class); $this->expectExceptionMessage( - "Could not make Auryn\\Test\\TestMissingDependency: Class $classname does not exist" + "Could not make Auryn\\Test\\$reportedClassname: Class $classname does not exist" ); $injector = new Injector; From 95067d2e24704fb04750584aa2d16d48db62b797 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 10 Dec 2022 14:37:43 +0000 Subject: [PATCH 30/68] Run CI against PHP 7.2 --- .github/workflows/ci.yml | 1 + composer.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8dd1ba..f57a2ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: strategy: matrix: php-version: + - '7.2' - '7.3' - '7.4' - '8.0' diff --git a/composer.json b/composer.json index 4802028..18e32e7 100644 --- a/composer.json +++ b/composer.json @@ -25,10 +25,10 @@ } ], "require": { - "php": ">=7.3.0" + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.27", + "phpunit/phpunit": "^8.5.31", "athletic/athletic": "~0.1" }, "autoload": { From 5ed3ece8a3d5f78ba81d3008ec6aff9c1d8b8f4b Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 10 Dec 2022 14:45:53 +0000 Subject: [PATCH 31/68] Remove leading slashes and question marks in one go. --- lib/Injector.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Injector.php b/lib/Injector.php index ee49553..cec5cdc 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -141,9 +141,7 @@ public function alias($original, $alias) private function normalizeName($className) { - $tmp = ltrim(strtolower($className), '\\'); - - return ltrim($tmp, '?'); + return ltrim(strtolower($className), '?\\'); } /** From 9f3bd4bfca4114ad0d44e805ab011ffe4d302cb1 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 10 Dec 2022 15:07:28 +0000 Subject: [PATCH 32/68] Remove duplicate line. That has been there for seven years. --- lib/InjectionException.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/InjectionException.php b/lib/InjectionException.php index d7ab70b..5e7f96d 100644 --- a/lib/InjectionException.php +++ b/lib/InjectionException.php @@ -27,7 +27,6 @@ public static function fromInvalidCallable( if (is_string($callableOrMethodStr)) { $callableString .= $callableOrMethodStr; } else if (is_array($callableOrMethodStr) && - array_key_exists(0, $callableOrMethodStr) && array_key_exists(0, $callableOrMethodStr)) { if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { $callableString .= $callableOrMethodStr[0].'::'.$callableOrMethodStr[1]; From 4707dad5bd4ccee7d822332ce943cac6b1b78b7c Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 10 Dec 2022 15:30:24 +0000 Subject: [PATCH 33/68] Switch back to simple-phpunit as although PHPUnit 8.5.31 works on PHP 8, it doesn't generate coverage reports. --- .github/workflows/ci.yml | 8 +++++++- README.md | 21 +++++++++++++++++++++ composer.json | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f57a2ec..20bdf8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,5 +41,11 @@ jobs: - run: composer install + - name: "Install PHPUnit" + run: vendor/bin/simple-phpunit install + + - name: "PHPUnit version" + run: vendor/bin/simple-phpunit --version + - name: "Run tests" - run: vendor/bin/phpunit + run: vendor/bin/simple-phpunit \ No newline at end of file diff --git a/README.md b/README.md index b87d575..a9e13b9 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,27 @@ Archived tagged release versions are also available for manual download on the p [tags page](https://github.com/rdlowrey/auryn/tags) +##### Running tests + +To allow an appropriate version of PHPUnit to be installed across all of the supported +versions of PHP, instead of directly depending on PHPUnit, Auryn instead depends on +simple-phpunit. + +After doing composer update, you need to tell simple-phpunit to install an PHPUnit for you: + +```bash +vendor/bin/simple-phpunit install + +vendor/bin/simple-phpunit --version +``` + +The tests can then be run with the command: + +```bash +vendor/bin/simple-phpunit +``` + + ## Basic Usage To start using the injector, simply create a new instance of the `Auryn\Injector` ("the Injector") diff --git a/composer.json b/composer.json index 18e32e7..b3e816d 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^8.5.31", + "symfony/phpunit-bridge": "^6.2.0", "athletic/athletic": "~0.1" }, "autoload": { From fac542d27711cccd7f8e94add01fbf27a1d9e601 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 10 Dec 2022 20:16:36 +0000 Subject: [PATCH 34/68] Fewer words = more better. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9e13b9..e2c9bab 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ To allow an appropriate version of PHPUnit to be installed across all of the sup versions of PHP, instead of directly depending on PHPUnit, Auryn instead depends on simple-phpunit. -After doing composer update, you need to tell simple-phpunit to install an PHPUnit for you: +After doing composer update, you need to tell simple-phpunit to install PHPUnit: ```bash vendor/bin/simple-phpunit install From d730160de423923fd38b2dda550fdee4b296e3f9 Mon Sep 17 00:00:00 2001 From: Danack Date: Tue, 20 Dec 2022 18:41:00 +0000 Subject: [PATCH 35/68] Added link to lazy instantiation version of Auryn, and words on why it wasn't included. --- README.md | 9 ++++- excluded_features.md | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 excluded_features.md diff --git a/README.md b/README.md index e2c9bab..07fc92d 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,20 @@ S.O.L.I.D., object-oriented PHP applications. ## Maintenance status `rdlowrey/auryn` is in low maintenance mode. i.e. new features are very unlikely to be added, and -new releases to support new versions of PHP are not guaranteed to be timely. +new releases to support new versions of PHP are not guaranteed to be timely. Notes on why some +features were not added to Auryn are listed [here](https://github.com/rdlowrey/excluded_features.md). There are similar libraries available at: * [`martin-hughes/auryn`](https://github.com/martin-hughes/auryn) is a fork from this repo and maintains the current namespace and interfaces. It is unlikely to introduce significant new features, instead focussing on bugfixes and testing. + +* [`overclokk/auryn`](https://github.com/overclokk/auryn) is a fork from this repo + and maintains the current namespace and interfaces. It has added the ability to lazy + instantiate dependencies using + [`Ocramius/ProxyManager`](https://github.com/Ocramius/ProxyManager). + * [`amphp/injector`](https://github.com/amphp/injector) is a significant rewrite using a new namespace and slightly different interfaces, requiring you to update your code. It will introduce new features and diverge over time from this repo. diff --git a/excluded_features.md b/excluded_features.md new file mode 100644 index 0000000..31a7abf --- /dev/null +++ b/excluded_features.md @@ -0,0 +1,78 @@ + +# Excluded features + +Sometimes, you just have to say 'no'. + +Although none of the features below would be ridiculous to include in this library, for various reasons they just haven't felt quite 'felt right' to include. + +The notes below try to describe the feature, explain why it was not included, and suggest some alternative solutions. + +## Lazy instantiation + +### Description + +Some dependencies can be costly to create, either in terms of CPU time or memory, or both. + +If the dependency is not guaranteed to be used for all code paths then it can be worth delaying the initialization of that dependency until it is actually used. + +### Alternative solution - use a factory + +Instead of having a direct dependency on the class that is costly to create: + +```php +class CostlyDependency { + function do_the_needful() { + ... + } +} + +class Foo { + + function __construct(private CostlyDependency $costlyDependency) {} + + function bar() { + $this->costlyDependency->do_the_needful(); + } +} +``` + +Create a factory class and depend on that instead, using it to create the costly dependency just before you know it's going to be used. + +```php +class CostlyDependency { + ... +} + +class CostlyDependencyFactory { + + function __construct(private Injector $injector) {} + + function create(): CostlyDependency { + return $this->injector->make(CostlyDependency::class); + } +} + +class Foo { + + function __construct(private CostlyDependencyFactory $costlyDependencyFactory) {} + + function bar() { + $costlyDependency = $this->costlyDependencyFactory->create(); + $costlyDependency->do_the_needful(); + } +} +``` + +And so that it is available to factory classes, in your bootstrap code share the injector: + +```php +$injector->share($injector); +``` + +Although people frown on Service Locators, using the injector in a factory class is a fine tradeoff. + +### Why Lazy Instantiation wasn't included + +* Fewer dependencies is nicer. Having Auryn remain a library with zero external dependencies is nicer than having a non-trivial external dependency. +* Debugging problems inside the injector is a nightmare. Due to how complicated the call-stack is within the injector itself, it can be quite hard to understand what the problem is when an error occurs. By using a factory class, any problem should be a lot easier to debug. +* The alternative solution is trivial and better. In particular, it leaves the place where your code is going to have a costly instantiation of an object obvious, rather than it becoming hidden magic. From 7d500bc0e6a9c76504e9ba7694f6f563aff592f2 Mon Sep 17 00:00:00 2001 From: Danack Date: Wed, 21 Dec 2022 16:39:27 +0000 Subject: [PATCH 36/68] Added words to explain why https://github.com/rdlowrey/auryn/issues/35 was rejected. --- excluded_features.md | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/excluded_features.md b/excluded_features.md index 31a7abf..94a2c62 100644 --- a/excluded_features.md +++ b/excluded_features.md @@ -9,6 +9,8 @@ The notes below try to describe the feature, explain why it was not included, an ## Lazy instantiation +Original discussion: https://github.com/rdlowrey/auryn/issues/143 + ### Description Some dependencies can be costly to create, either in terms of CPU time or memory, or both. @@ -76,3 +78,65 @@ Although people frown on Service Locators, using the injector in a factory class * Fewer dependencies is nicer. Having Auryn remain a library with zero external dependencies is nicer than having a non-trivial external dependency. * Debugging problems inside the injector is a nightmare. Due to how complicated the call-stack is within the injector itself, it can be quite hard to understand what the problem is when an error occurs. By using a factory class, any problem should be a lot easier to debug. * The alternative solution is trivial and better. In particular, it leaves the place where your code is going to have a costly instantiation of an object obvious, rather than it becoming hidden magic. + + +## Resolving dependencies based on constructor chain + +Original discussion https://github.com/rdlowrey/auryn/issues/35 + +### Description + +Imagine you have two classes that each depend on a UtilityClass: + +```php +interface Logger{} + +class UtilityClass { + function __construct(private Logger $logger) {} + + function foo() { + $this->logger->log(Logger::info, "About to foo."); + } +} + +class ClassThatIsWorkingCorrectly{ + function __construct(UtilityClass $utilityClass) {} +} + +class ClassRelatedToReportedBugs{ + function __construct(UtilityClass $utilityClass) {} +} +``` + +One of the classes is working correctly and you aren't interested in logging information it generates. The other class is misbehaving slightly. + +Being able to configure the injector so that: + +* the 'Logger' injected into most classes is a logger that only reports notices at the 'error' level. + +* any 'Logger' created by 'ClassRelatedToReportedBugs' or any of its dependencies uses a Logger that reports notices at the 'info' level. + +would allow you to manage your logging behaviour, so that your log files only contain relevant info. + +### Alternative solutions + +#### Get a more powerful logger or logging system + +One example would be the `FingersCrossedHandler` listed in the [Monolog wiki](https://seldaek.github.io/monolog/doc/02-handlers-formatters-processors.html). It allows you to configure logging so that log messages are only actually logged if a certain triggering event has occurred. + +Most hosted logging systems are quite powerful and can be configured in real-time, without having to touch the config of individual servers. + +#### Use more contextual logging + +Logging plain text is just not great. Due to it lacking structure, it makes it harder than it could be to filter, display and understand the logging information. Instead of logging plain text, logging structured data that other tools can understand programmatically makes understanding what is happening in your application much easier. + +### Why it wasn't included + +Mostly, it's just too much complexity for an injector. Although the complexity needs to live somewhere, it should be in the logger code and infrastructure, not in the injector. + +Also, as only one person has requested this feature, it sounds like it shouldn't be included. + + + + + From 150f057a2ff52108804130207993bd0dcfb52109 Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 26 Dec 2022 18:02:08 +0000 Subject: [PATCH 37/68] Wrote some words on why variadics aren't supported. --- excluded_features.md | 95 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/excluded_features.md b/excluded_features.md index 94a2c62..6224baa 100644 --- a/excluded_features.md +++ b/excluded_features.md @@ -82,7 +82,7 @@ Although people frown on Service Locators, using the injector in a factory class ## Resolving dependencies based on constructor chain -Original discussion https://github.com/rdlowrey/auryn/issues/35 +Original discussion: https://github.com/rdlowrey/auryn/issues/35 ### Description @@ -137,6 +137,99 @@ Mostly, it's just too much complexity for an injector. Although the complexity n Also, as only one person has requested this feature, it sounds like it shouldn't be included. +## Variadic dependencies +Original discussion: https://github.com/rdlowrey/auryn/issues/134 +### Description + +Sometimes, particularly when dealing with legacy code, you might have a dependency on multiple instances on the same type of object: + +```php + +interface Repository { + function findInfo(): Info|null; +} + +class Foo { + public function __construct(Repository ...$repositories) { + // ... + } +} +``` + +Auryn does not support injecting variadic dependencies, so any class that has variadic dependencies cannot be directly instantiated by Auryn. + +### Alternative solutions + +#### Use a delegate method + +The simplest work around is to use a delegate function for creating objects that have variadic dependencies: + +```php +function createFoo(RepositoryLocator $repoLocator) +{ + // Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + + return new Foo($repositories); +} + +$injector->delegate('Foo', 'createFoo'); +``` + +#### Use an object that collects the variadic dependencies + +If you have many objects that have variadic dependencies, creating a separate delegate function for each of them might be a tedious task. + +Instead of that, creating a single class that collects the variadic dependencies, and then can be injected through autowiring into other classes, could be substantially less work and also easier to understand. + +```php +interface Repository { + public function findInfo(): Info|null {} +} + +class RepositoryCollection { + private $repos = []; + + public function __construct(Repository ...$repositories) { + foreach ($repositories as $repo) { + $this->repos[] = $repo; + } + } + + public function findInfo(): Info|null { + foreach ($this->repos as $repo) { + $info = $repo->findInfo(); + if ($info !== null) { + return $info; + } + } + + return null; + } +} + +class Foo { + public function __construct(RepositoryCollection $repositories) { + // ... + } +} + +function createRepositoryCollection(RepositoryLocator $repoLocator) +{ + // Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + + return new Foo($repositories); +} + +$injector->delegate(RepositoryCollection::class, 'createRepositoryCollection'); + +``` + +### Why support for variadic dependency wasn't included + +Variadics aren't a type and so can't be reasoned about by a dependency injector. +People should either use delegation or contexts to achieve what they're trying to do in a way that is comportable with dependency injection. From d52795d0baa91f2d6af343c5326fdeb2b7812cad Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 26 Dec 2022 19:04:15 +0000 Subject: [PATCH 38/68] Correct badge URL. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07fc92d..0c19277 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# auryn [![Build Status](https://github.com/Danack/Auryn/actions/workflows/ci.yml/badge.svg?branch=adding_ci)](https://github.com/Danack/Auryn/actions) +# auryn [![Build Status](https://github.com/rdlowrey/Auryn/actions/workflows/ci.yml/badge.svg?branch=adding_ci)](https://github.com/rdlowrey/Auryn/actions) auryn is a recursive dependency injector. Use auryn to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications. From 0f440006933b4df8a4fd8efb6414a8fa5e1b65fa Mon Sep 17 00:00:00 2001 From: Danack Date: Wed, 11 Jan 2023 14:59:27 +0000 Subject: [PATCH 39/68] Changed 'typehint' to 'type'. --- README.md | 2 +- lib/CachingReflector.php | 12 +++++----- lib/Injector.php | 22 ++++++++--------- lib/Reflector.php | 2 +- lib/StandardReflector.php | 2 +- test/Benchmark/Noop.php | 2 +- test/InjectorTest.php | 50 +++++++++++++++++++-------------------- test/fixtures.php | 18 +++++++------- test/fixtures_5_6.php | 4 ++-- 9 files changed, 57 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 0c19277..7efb604 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ Because we specified a global definition for `myValue`, all parameters that are way defined (as below) that match the specified parameter name are auto-filled with the global value. If a parameter matches any of the following criteria the global value is not used: -- A typehint +- A parameter type - A predefined injection definition - A custom call time definition diff --git a/lib/CachingReflector.php b/lib/CachingReflector.php index 7784bc4..76535a8 100644 --- a/lib/CachingReflector.php +++ b/lib/CachingReflector.php @@ -52,7 +52,7 @@ public function getCtorParams($class) return $reflectedCtorParams; } - public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) + public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) { $lowParam = strtolower($param->name); @@ -67,16 +67,16 @@ public function getParamTypeHint(\ReflectionFunctionAbstract $function, \Reflect : null; } - $typeHint = ($paramCacheKey === null) ? false : $this->cache->fetch($paramCacheKey); + $type = ($paramCacheKey === null) ? false : $this->cache->fetch($paramCacheKey); - if (false === $typeHint) { - $typeHint = $this->reflector->getParamTypeHint($function, $param); + if (false === $type) { + $type = $this->reflector->getParamType($function, $param); if ($paramCacheKey !== null) { - $this->cache->store($paramCacheKey, $typeHint); + $this->cache->store($paramCacheKey, $type); } } - return $typeHint; + return $type; } public function getFunction($functionName) diff --git a/lib/Injector.php b/lib/Injector.php index cec5cdc..2fdacae 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -76,7 +76,7 @@ public function define($name, array $args) /** * Assign a global default value for all parameters named $paramName * - * Global parameter definitions are only used for parameters with no typehint, pre-defined or + * Global parameter definitions are only used for parameters with no type, pre-defined or * call-time definition. * * @param string $paramName The parameter name for which this value applies @@ -91,11 +91,11 @@ public function defineParam($paramName, $value) } /** - * Define an alias for all occurrences of a given typehint + * Define an alias for all occurrences of a given type * - * Use this method to specify implementation classes for interface and abstract class typehints. + * Use this method to specify implementation classes for interface and abstract class types. * - * @param string $original The typehint to replace + * @param string $original The type to replace * @param string $alias The implementation name * @throws ConfigException if any argument is empty or not a string * @return self @@ -471,7 +471,7 @@ private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array } elseif (($prefix = self::A_DEFINE . $name) && isset($definition[$prefix])) { // interpret the param as a class definition $arg = $this->buildArgFromParamDefineArr($definition[$prefix]); - } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) { + } elseif (!$arg = $this->buildArgFromType($reflFunc, $reflParam)) { $arg = $this->buildArgFromReflParam($reflParam, $className); if ($arg === null && (PHP_VERSION_ID >= 50600 && $reflParam->isVariadic() || $reflParam->isOptional())) { @@ -522,24 +522,24 @@ private function buildArgFromDelegate($paramName, $callableOrMethodStr) return $executable($paramName, $this); } - private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam) + private function buildArgFromType(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam) { - $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam); + $type = $this->reflector->getParamType($reflFunc, $reflParam); - if (!$typeHint) { + if (!$type) { $obj = null; } elseif ($reflParam->isDefaultValueAvailable()) { - $normalizedName = $this->normalizeName($typeHint); + $normalizedName = $this->normalizeName($type); // Injector has been told explicitly how to make this type if (isset($this->aliases[$normalizedName]) || isset($this->delegates[$normalizedName]) || isset($this->shares[$normalizedName])) { - $obj = $this->make($typeHint); + $obj = $this->make($type); } else { $obj = $reflParam->getDefaultValue(); } } else { - $obj = $this->make($typeHint); + $obj = $this->make($type); } return $obj; diff --git a/lib/Reflector.php b/lib/Reflector.php index afe052d..e3ce787 100644 --- a/lib/Reflector.php +++ b/lib/Reflector.php @@ -40,7 +40,7 @@ public function getCtorParams($class); * @param \ReflectionFunctionAbstract $function * @param \ReflectionParameter $param */ - public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param); + public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionParameter $param); /** * Retrieves and caches a reflection for the specified function diff --git a/lib/StandardReflector.php b/lib/StandardReflector.php index 17ae1ef..6c4a071 100644 --- a/lib/StandardReflector.php +++ b/lib/StandardReflector.php @@ -23,7 +23,7 @@ public function getCtorParams($class) : null; } - public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) + public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) { // php 8 deprecates getClass method if (PHP_VERSION_ID >= 80000) { diff --git a/test/Benchmark/Noop.php b/test/Benchmark/Noop.php index 52049fe..0a7a98a 100644 --- a/test/Benchmark/Noop.php +++ b/test/Benchmark/Noop.php @@ -14,7 +14,7 @@ public function namedNoop($name) // call-target, intenionally left empty } - public function typehintedNoop(noop $noop) + public function typedNoop(noop $noop) { // call-target, intenionally left empty } diff --git a/test/InjectorTest.php b/test/InjectorTest.php index e23aff9..c47df71 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -7,7 +7,7 @@ class InjectorTest extends TestCase { - public function testArrayTypehintDoesNotEvaluatesAsClass() + public function testArrayTypeDoesNotEvaluatesAsClass() { $injector = new Injector; $injector->defineParam('parameter', []); @@ -28,7 +28,7 @@ public function testMakeInstanceReturnsNewInstanceIfClassHasNoConstructor() $this->assertEquals(new TestNoConstructor, $injector->make('Auryn\Test\TestNoConstructor')); } - public function testMakeInstanceReturnsAliasInstanceOnNonConcreteTypehint() + public function testMakeInstanceReturnsAliasInstanceOnNonConcreteType() { $injector = new Injector; $injector->alias('Auryn\Test\DepInterface', 'Auryn\Test\DepImplementation'); @@ -62,7 +62,7 @@ public function testMakeInstanceBuildsNonConcreteCtorParamWithAlias() $this->assertInstanceOf('Auryn\Test\RequiresInterface', $obj); } - public function testMakeInstancePassesNullCtorParameterIfNoTypehintOrDefaultCanBeDetermined() + public function testMakeInstancePassesNullCtorParameterIfNoTypeOrDefaultCanBeDetermined() { $injector = new Injector; $nullCtorParamObj = $injector->make('Auryn\Test\ProvTestNoDefinitionNullDefaultClass'); @@ -128,10 +128,10 @@ public function testMakeInstanceUsesReflectionForUnknownParamsInMultiBuildWithDe $obj = $injector->make('Auryn\Test\TestMultiDepsWithCtor', array('val1'=>'Auryn\Test\TestDependency')); $this->assertInstanceOf('Auryn\Test\TestMultiDepsWithCtor', $obj); - $obj = $injector->make('Auryn\Test\NoTypehintNoDefaultConstructorClass', + $obj = $injector->make('Auryn\Test\NoTypeNoDefaultConstructorClass', array('val1'=>'Auryn\Test\TestDependency') ); - $this->assertInstanceOf('Auryn\Test\NoTypehintNoDefaultConstructorClass', $obj); + $this->assertInstanceOf('Auryn\Test\NoTypeNoDefaultConstructorClass', $obj); $this->assertNull($obj->testParam); } @@ -147,17 +147,17 @@ public function testMakeInstanceUsesReflectionForUnknownParamsInMultiBuildWithDe require_once __DIR__ . "/fixtures_5_6.php"; $injector = new Injector; - $obj = $injector->make('Auryn\Test\NoTypehintNoDefaultConstructorVariadicClass', + $obj = $injector->make('Auryn\Test\NoTypeNoDefaultConstructorVariadicClass', array('val1'=>'Auryn\Test\TestDependency') ); - $this->assertInstanceOf('Auryn\Test\NoTypehintNoDefaultConstructorVariadicClass', $obj); + $this->assertInstanceOf('Auryn\Test\NoTypeNoDefaultConstructorVariadicClass', $obj); $this->assertEquals(array(), $obj->testParam); } /** * @requires PHP 5.6 */ - public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadicsWithTypeHint() + public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadicsWithType() { if (defined('HHVM_VERSION')) { $this->markTestSkipped("HHVM doesn't support variadics with type declarations."); @@ -166,38 +166,38 @@ public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadi require_once __DIR__ . "/fixtures_5_6.php"; $injector = new Injector; - $obj = $injector->make('Auryn\Test\TypehintNoDefaultConstructorVariadicClass', + $obj = $injector->make('Auryn\Test\TypeNoDefaultConstructorVariadicClass', array('arg'=>'Auryn\Test\TestDependency') ); - $this->assertInstanceOf('Auryn\Test\TypehintNoDefaultConstructorVariadicClass', $obj); + $this->assertInstanceOf('Auryn\Test\TypeNoDefaultConstructorVariadicClass', $obj); $this->assertIsArray($obj->testParam); $this->assertInstanceOf('Auryn\Test\TestDependency', $obj->testParam[0]); } - public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDefinitionOrDefault() + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithoutDefinitionOrDefault() { $this->expectException(\Auryn\InjectionException::class); // TODO - why does this message end with double-colon? - $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::'); + $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::'); $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); $injector = new Injector; - $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault'); + $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault'); } - public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDefinitionOrDefaultThroughAliasedTypehint() + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithoutDefinitionOrDefaultThroughAliasedType() { $this->expectException(\Auryn\InjectionException::class); // TODO - why does this message end with double-colon? - $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::'); + $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::'); $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); $injector = new Injector; - $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault'); - $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefaultDependent'); + $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault'); + $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefaultDependent'); } - public function testMakeInstanceThrowsExceptionOnUninstantiableTypehintWithoutDefinition() + public function testMakeInstanceThrowsExceptionOnUninstantiableTypeWithoutDefinition() { $this->expectException(\Auryn\InjectorException::class); $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); @@ -220,8 +220,8 @@ public function testTypelessDefineForAliasedDependency() $injector = new Injector; $injector->defineParam('val', 42); - $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\ProviderTestCtorParamWithNoTypehintOrDefault'); - $obj = $injector->make('Auryn\Test\ProviderTestCtorParamWithNoTypehintOrDefaultDependent'); + $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\ProviderTestCtorParamWithNoTypeOrDefault'); + $obj = $injector->make('Auryn\Test\ProviderTestCtorParamWithNoTypeOrDefaultDependent'); } public function testMakeInstanceInjectsRawParametersDirectly() @@ -322,7 +322,7 @@ public function testMakeInstanceThrowsExceptionIfStringDelegateClassInstantiatio $injector->delegate('StdClass', 'SomeClassThatDefinitelyDoesNotExistForReal'); } - public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithNoDefinition() + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithNoDefinition() { $this->expectException(\Auryn\InjectionException::class); $this->expectExceptionMessage('Injection definition required for interface Auryn\Test\DepInterface'); @@ -621,10 +621,10 @@ public function testInterfaceFactoryDelegation() public function testMissingAlias() { $reportedClassname = 'TestMissingDependency'; - $classname = 'Auryn\Test\TypoInTypehint'; + $classname = 'Auryn\Test\TypoInType'; if (PHP_VERSION_ID >= 80000) { $classname = "\"" . $classname . "\""; - $reportedClassname = 'TypoInTypehint'; + $reportedClassname = 'TypoInType'; } $this->expectException(\Auryn\InjectorException::class); @@ -947,8 +947,8 @@ public function testShareNewAlias() public function testDefineWithBackslashAndMakeWithoutBackslash() { $injector = new Injector(); - $injector->define('Auryn\Test\SimpleNoTypehintClass', array(':arg' => 'tested')); - $testClass = $injector->make('Auryn\Test\SimpleNoTypehintClass'); + $injector->define('Auryn\Test\SimpleNoTypeClass', array(':arg' => 'tested')); + $testClass = $injector->make('Auryn\Test\SimpleNoTypeClass'); $this->assertEquals('tested', $testClass->testParam); } diff --git a/test/fixtures.php b/test/fixtures.php index 668b142..f791931 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -166,7 +166,7 @@ public function __construct(TestDependency $testDep) } } -class TestClassWithNoCtorTypehints +class TestClassWithNoCtorTypes { public function __construct($val = 42) { @@ -194,7 +194,7 @@ public function __construct(TestDependency $val1, TestNeedsDep $val2) } } -class NoTypehintNullDefaultConstructorClass +class NoTypeNullDefaultConstructorClass { public $testParam = 1; public function __construct(TestDependency $val1, $arg=42) @@ -203,7 +203,7 @@ public function __construct(TestDependency $val1, $arg=42) } } -class NoTypehintNoDefaultConstructorClass +class NoTypeNoDefaultConstructorClass { public $testParam = 1; public function __construct(TestDependency $val1, $arg = null) @@ -277,7 +277,7 @@ interface TestNoExplicitDefine { } -class InjectorTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine +class InjectorTestCtorParamWithNoTypeOrDefault implements TestNoExplicitDefine { public $val = 42; public function __construct($val) @@ -286,7 +286,7 @@ public function __construct($val) } } -class InjectorTestCtorParamWithNoTypehintOrDefaultDependent +class InjectorTestCtorParamWithNoTypeOrDefaultDependent { private $param; public function __construct(TestNoExplicitDefine $param) @@ -344,7 +344,7 @@ public function __invoke() } } -class ProviderTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine +class ProviderTestCtorParamWithNoTypeOrDefault implements TestNoExplicitDefine { public $val = 42; public function __construct($val) @@ -353,7 +353,7 @@ public function __construct($val) } } -class ProviderTestCtorParamWithNoTypehintOrDefaultDependent +class ProviderTestCtorParamWithNoTypeOrDefaultDependent { private $param; public function __construct(TestNoExplicitDefine $param) @@ -497,7 +497,7 @@ public function foo() class TestMissingDependency { - public function __construct(TypoInTypehint $class) + public function __construct(TypoInType $class) { } } @@ -588,7 +588,7 @@ public function __construct(TestDependencyWithProtectedConstructor $dep) } } -class SimpleNoTypehintClass +class SimpleNoTypeClass { public $testParam = 1; diff --git a/test/fixtures_5_6.php b/test/fixtures_5_6.php index f23dda5..0f85ea3 100644 --- a/test/fixtures_5_6.php +++ b/test/fixtures_5_6.php @@ -2,7 +2,7 @@ namespace Auryn\Test; -class NoTypehintNoDefaultConstructorVariadicClass +class NoTypeNoDefaultConstructorVariadicClass { public $testParam = 1; public function __construct(TestDependency $val1, ...$arg) @@ -11,7 +11,7 @@ public function __construct(TestDependency $val1, ...$arg) } } -class TypehintNoDefaultConstructorVariadicClass +class TypeNoDefaultConstructorVariadicClass { public $testParam = 1; public function __construct(TestDependency ...$arg) From 6c06f163800693b712ae9c9bdd601d1c912356eb Mon Sep 17 00:00:00 2001 From: Danack Date: Wed, 11 Jan 2023 15:45:51 +0000 Subject: [PATCH 40/68] Added more words to document why features were excluded. https://github.com/rdlowrey/auryn/issues/145 https://github.com/rdlowrey/auryn/issues/133 --- excluded_features.md | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/excluded_features.md b/excluded_features.md index 6224baa..9b54676 100644 --- a/excluded_features.md +++ b/excluded_features.md @@ -233,3 +233,120 @@ $injector->delegate(RepositoryCollection::class, 'createRepositoryCollection'); Variadics aren't a type and so can't be reasoned about by a dependency injector. People should either use delegation or contexts to achieve what they're trying to do in a way that is comportable with dependency injection. + +## Parameter definitions not inherited from parent classes + +Original discussion: https://github.com/rdlowrey/auryn/issues/133 + +### Description + +Some people expect classes to 'inherit' definitions when definitions exist for their parent class. + +```php +class Foo +{ + public function __construct($key) { } +} + +class Bar extends Foo +{ +} + +$injector = new Auryn\Injector; +$injector->define('Foo', [ + ':key' => 'secret', +]); + +$foo_object = $injector->make(Foo::class); +$bar_object = $injector->make(Bar::class); + +// Gives error: +// Uncaught Auryn\InjectionException: No definition available to provision +// typeless parameter $key at position 0 in Bar::__construct() declared in... +``` + +### Why support for parameter definitions are not inherited from parent classes + +The choice for this library is that all configuration must be explicit, which helps make it easier to reason about the configuration. + +If a class 'inherited' parameter definitions from a parent class, you would have to know that it had a parent class, and that a definition existed for that to be able to understand what parameters the object was going to receive. + +### Alternative solutions + +#### Explicit definition + +Just define parameters for each class that needs them. + +```php +$injector->define('Foo', [':key' => 'secret']); +$injector->define('Bar', [':key' => 'secret']); +``` + +#### Wrap values in a type and share it + +As types can be shared, that can avoid needing to define a scalar value multiple times. + +```php +class ApiKey +{ + public function __construct(private string $value) {} + + public function getValue(): string + { + return $this->value; + } +} + +class Foo +{ + public function __construct(ApiKey $apiKey) { } +} + +class Bar extends Foo +{ +} + +$injector = new Auryn\Injector; +$injector->share(new ApiKey('secret')); + +$foo_object = $injector->make(Foo::class); +$bar_object = $injector->make(Bar::class); +``` + +## Resolve interfaces and abstracts to shared instances + +Original discussion: https://github.com/rdlowrey/auryn/issues/145 + +### Description + +Some people expect an injector to find classes that implement an interface whe + +```php +interface A { } +class B implements A { } +class C { + function __construct(A $a) { } +} + +$b = new B(); +$injector->share($b); + +$injector->make(C::class); + +// Gives error: +// Uncaught Auryn\InjectionException: Injection definition required for interface A in... +``` + +### Alternative solutions + +Just define aliases for each interface to a concrete class. That will only take a few minutes per environment your code runs in. + +### Why support for Resolve interfaces and abstracts to shared instances wasn't included + +The choice for this library is that all configuration must be explicit, which helps make it easier to reason about the configuration. + +If you wanted to see what class was going to be created for an interface, not being able to inspect the configuration of the injector, and instead having to either search through the code or run some test code would be 'ungood'. + +Additionally, if a second class that implements the interface was added, the injector would need to either pick one or throw an exception of "multiple available types". Either choice would be quite surprising and take more time to resolve than sim + + From 6e92e2678b6b7b27cad2b5ee1e7676360fc1f06c Mon Sep 17 00:00:00 2001 From: Danack Date: Wed, 11 Jan 2023 15:51:52 +0000 Subject: [PATCH 41/68] Completed sentence in docs. --- excluded_features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/excluded_features.md b/excluded_features.md index 9b54676..9e70eb6 100644 --- a/excluded_features.md +++ b/excluded_features.md @@ -347,6 +347,6 @@ The choice for this library is that all configuration must be explicit, which he If you wanted to see what class was going to be created for an interface, not being able to inspect the configuration of the injector, and instead having to either search through the code or run some test code would be 'ungood'. -Additionally, if a second class that implements the interface was added, the injector would need to either pick one or throw an exception of "multiple available types". Either choice would be quite surprising and take more time to resolve than sim +Additionally, if a second class that implements the interface was added, the injector would need to either pick one or throw an exception of "multiple available types". Either choice would be quite surprising and take more time to resolve than simply writing the explicit configuration. From f93349d5dd98399aa0ac69d2c62ba7cc93e2f3b1 Mon Sep 17 00:00:00 2001 From: Danack Date: Sun, 22 Jan 2023 17:37:03 +0000 Subject: [PATCH 42/68] Added words for static initialisation. --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 7efb604..10295c0 100644 --- a/README.md +++ b/README.md @@ -887,3 +887,22 @@ eliminate evil Singletons using the sharing capabilities of the auryn DIC. In th code, we share the request object so that any classes instantiated by the `Auryn\Injector` that ask for a `Request` will receive the same instance. This feature not only helps eliminate Singletons, but also the need for hard-to-test `static` properties. + +### When app-bootstrapping by Auryn is not possible + +Sometimes, the initialisation of the application is outside of your control. One example would be writing plugins for Wordpress, where Wordpress is initialising your plugin, not the other way round. + +You can still use Auryn by using a function to make a single instance of the injector: + +```php +function getAurynInjector() +{ + static $injector = null; + if ($injector == null) { + $injector = new \Auryn\Injector(); + // Do injector defines/shares/aliases/delegates here + } + + return $injector; +} +``` From bcaa1d30a9b30e91836e7179ffc3bd35afd91063 Mon Sep 17 00:00:00 2001 From: Danack Date: Sun, 22 Jan 2023 17:49:48 +0000 Subject: [PATCH 43/68] More words. --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 10295c0..c553f92 100644 --- a/README.md +++ b/README.md @@ -906,3 +906,41 @@ function getAurynInjector() return $injector; } ``` + +### Running tests and benchmarks + +#### Running tests + +As there is no single version of PHPUnit that works on all the versions of PHP that Auryn supports, we use simple-phpunit to install an appropriate version of PHP. + +After running the `composer update` to get the latest dependencies, run: + +``` +php vendor/bin/simple-phpunit install +``` + +to make simple-phpunit install PHPUnit. The tests can then be with with the command: + +``` +php vendor/bin/simple-phpunit +``` + +simple-phpunit accepts PHPUnit commandline options and passes them through to PHPUnit e.g. `php php vendor/bin/simple-phpunit --group wip` to only run the tests tagged as being part of group 'wip'. + +#### Running benchamarks + +We use PHPBench to allow checking performance gains/regressions when making code changes. The simplest way to use it is as follows: + +1. Create a benchmark baseline by running: + +``` +vendor/bin/phpbench run --tag=benchmark_original --retry-threshold=5 --iterations=10 +``` + +2. Apply your code changes. + +3. Run a benchmark, and compare the results to the 'benchmark_original' by running: + +``` +vendor/bin/phpbench run --report=aggregate --ref=benchmark_original --retry-threshold=5 --iterations=10 +``` From af851c186ef73856efc0320a44ed41d622c5413b Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 23 Jan 2023 22:57:16 +0000 Subject: [PATCH 44/68] Update php-cs-fixer and fix CS issues. --- .php_cs | 25 ++++++++++++------------- composer.json | 5 +++-- lib/InjectionException.php | 6 +++--- lib/Injector.php | 9 +++------ 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.php_cs b/.php_cs index 0f1e9ba..58f9369 100644 --- a/.php_cs +++ b/.php_cs @@ -1,15 +1,14 @@ level(Symfony\CS\FixerInterface::NONE_LEVEL) - ->fixers([ - "-psr0", - "psr2", - "psr4", - ]) - ->finder( - Symfony\CS\Finder\DefaultFinder::create() - ->in(__DIR__ . "/lib") - ->in(__DIR__ . "/test") - ) -; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__ . "/lib") + ->in(__DIR__ . "/test"); + +$config = new PhpCsFixer\Config(); + +return $config + ->setRules([ + '@PSR2' => true, +]) + ->setFinder($finder); diff --git a/composer.json b/composer.json index b3e816d..7dcf4ec 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,9 @@ "php": ">=7.2.0" }, "require-dev": { - "symfony/phpunit-bridge": "^6.2.0", - "athletic/athletic": "~0.1" + "athletic/athletic": "~0.1", + "friendsofphp/php-cs-fixer" : "3.3.1", + "symfony/phpunit-bridge": "^6.2.0" }, "autoload": { "psr-4": { diff --git a/lib/InjectionException.php b/lib/InjectionException.php index 5e7f96d..8a41455 100644 --- a/lib/InjectionException.php +++ b/lib/InjectionException.php @@ -26,11 +26,11 @@ public static function fromInvalidCallable( if (is_string($callableOrMethodStr)) { $callableString .= $callableOrMethodStr; - } else if (is_array($callableOrMethodStr) && + } elseif (is_array($callableOrMethodStr) && array_key_exists(0, $callableOrMethodStr)) { if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { $callableString .= $callableOrMethodStr[0].'::'.$callableOrMethodStr[1]; - } else if (is_object($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { + } elseif (is_object($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { $callableString .= sprintf( "[object(%s), '%s']", get_class($callableOrMethodStr[0]), @@ -40,7 +40,7 @@ public static function fromInvalidCallable( } if ($callableString) { - // Prevent accidental usage of long strings from filling logs. + // Prevent accidental usage of long strings from filling logs. $callableString = substr($callableString, 0, 250); $message = sprintf( "%s. Invalid callable was '%s'", diff --git a/lib/Injector.php b/lib/Injector.php index 2fdacae..65dae13 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -380,12 +380,10 @@ public function make($name, array $args = array()) } unset($this->inProgressMakes[$normalizedClass]); - } - catch (\Throwable $exception) { + } catch (\Throwable $exception) { unset($this->inProgressMakes[$normalizedClass]); throw $exception; - } - catch (\Exception $exception) { + } catch (\Exception $exception) { unset($this->inProgressMakes[$normalizedClass]); throw $exception; } @@ -596,8 +594,7 @@ private function prepareInstance($obj, $normalizedClass) // TODO - this is inelegant if ($obj === null) { $interfaces = false; - } - else { + } else { $interfaces = @class_implements($obj); } From d8bcb0a00a9db9b58aca4af728dfa32dc2b22c5e Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 21 Jan 2023 11:55:58 +0000 Subject: [PATCH 45/68] Remove 5.3 compatibility code. --- lib/Executable.php | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/lib/Executable.php b/lib/Executable.php index dc1b77a..069dd23 100644 --- a/lib/Executable.php +++ b/lib/Executable.php @@ -42,27 +42,7 @@ public function __invoke() return $reflection->invokeArgs($this->invocationObject, $args); } - return $this->callableReflection->isClosure() - ? $this->invokeClosureCompat($reflection, $args) - : $reflection->invokeArgs($args); - } - - /** - * @TODO Remove this extra indirection when 5.3 support is dropped - */ - private function invokeClosureCompat($reflection, $args) - { - if (version_compare(PHP_VERSION, '5.4.0') >= 0) { - $scope = $reflection->getClosureScopeClass(); - $closure = \Closure::bind( - $reflection->getClosure(), - $reflection->getClosureThis(), - $scope ? $scope->name : null - ); - return call_user_func_array($closure, $args); - } else { - return $reflection->invokeArgs($args); - } + return $reflection->invokeArgs($args); } public function getCallableReflection() From de20dcf5f214a6f1ad7c3a47d209be55fe8d8281 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 21 Jan 2023 11:59:24 +0000 Subject: [PATCH 46/68] Throwable has been the base exception since PHP 7.0, so the second catch would only have been of use on versions < 7.0 --- lib/Injector.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/Injector.php b/lib/Injector.php index 65dae13..60f8d67 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -383,9 +383,6 @@ public function make($name, array $args = array()) } catch (\Throwable $exception) { unset($this->inProgressMakes[$normalizedClass]); throw $exception; - } catch (\Exception $exception) { - unset($this->inProgressMakes[$normalizedClass]); - throw $exception; } return $obj; From d5d05ddba573129ee1baafc169f7f84cfefc4484 Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 12:01:47 +0100 Subject: [PATCH 47/68] Upgrading github actions/checkout --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20bdf8f..9cd8215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 From a9a6146b876fa7f626e8ca41a360e044017b299b Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 8 Dec 2022 21:47:27 +0000 Subject: [PATCH 48/68] Use more appropriate exception for during config process. --- lib/ConfigException.php | 15 +++++++++++++++ lib/InjectionException.php | 29 ++++++++++++++++++----------- lib/Injector.php | 7 +++---- test/InjectorTest.php | 2 +- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/ConfigException.php b/lib/ConfigException.php index f7a0ae0..b12576d 100644 --- a/lib/ConfigException.php +++ b/lib/ConfigException.php @@ -4,4 +4,19 @@ class ConfigException extends InjectorException { + /** + * Add a human readable version of the invalid callable to the standard 'invalid invokable' message. + */ + public static function fromInvalidCallable( + $callableOrMethodStr, + \Exception $previous = null + ) { + + $message = InjectionException::getInvalidCallableMessage( + $callableOrMethodStr + ); + + return new self($message, Injector::E_INVOKABLE, $previous); + } + } diff --git a/lib/InjectionException.php b/lib/InjectionException.php index 8a41455..aebe243 100644 --- a/lib/InjectionException.php +++ b/lib/InjectionException.php @@ -14,14 +14,8 @@ public function __construct(array $inProgressMakes, $message = "", $code = 0, \E parent::__construct($message, $code, $previous); } - /** - * Add a human readable version of the invalid callable to the standard 'invalid invokable' message. - */ - public static function fromInvalidCallable( - array $inProgressMakes, - $callableOrMethodStr, - \Exception $previous = null - ) { + public static function getInvalidCallableMessage($callableOrMethodStr) + { $callableString = null; if (is_string($callableOrMethodStr)) { @@ -42,15 +36,28 @@ public static function fromInvalidCallable( if ($callableString) { // Prevent accidental usage of long strings from filling logs. $callableString = substr($callableString, 0, 250); - $message = sprintf( + return sprintf( "%s. Invalid callable was '%s'", Injector::M_INVOKABLE, $callableString ); - } else { - $message = \Auryn\Injector::M_INVOKABLE; } + return Injector::M_INVOKABLE; + } + + /** + * Add a human readable version of the invalid callable to the standard 'invalid invokable' message. + */ + public static function fromInvalidCallable( + array $inProgressMakes, + $callableOrMethodStr, + \Exception $previous = null + ) { + $message = self::getInvalidCallableMessage( + $callableOrMethodStr + ); + return new self($inProgressMakes, $message, Injector::E_INVOKABLE, $previous); } diff --git a/lib/Injector.php b/lib/Injector.php index 60f8d67..212bd01 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -215,15 +215,14 @@ private function shareInstance($obj) * * @param string $name * @param mixed $callableOrMethodStr Any callable or provisionable invokable method - * @throws InjectionException if $callableOrMethodStr is not a callable. - * See https://github.com/rdlowrey/auryn#injecting-for-execution + * @throws ConfigException if $callableOrMethodStr is not a callable. + * * @return self */ public function prepare($name, $callableOrMethodStr) { if ($this->isExecutable($callableOrMethodStr) === false) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, + throw ConfigException::fromInvalidCallable( $callableOrMethodStr ); } diff --git a/test/InjectorTest.php b/test/InjectorTest.php index c47df71..db274ce 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1161,7 +1161,7 @@ public function testPrepareInvalidCallable() { $injector = new Injector; $invalidCallable = 'This_does_not_exist'; - $this->expectException(\Auryn\InjectionException::class); + $this->expectException(\Auryn\ConfigException::class); $this->expectExceptionMessage($invalidCallable); $injector->prepare("StdClass", $invalidCallable); From dfde7c8a0310776d8f77e2b1baaa638e1d464eb6 Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 12:11:24 +0100 Subject: [PATCH 49/68] CS --- lib/ConfigException.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ConfigException.php b/lib/ConfigException.php index b12576d..a94eb63 100644 --- a/lib/ConfigException.php +++ b/lib/ConfigException.php @@ -18,5 +18,4 @@ public static function fromInvalidCallable( return new self($message, Injector::E_INVOKABLE, $previous); } - } From f46c70cb3446220fd97e22214e4ad483e7234444 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 21 Jan 2023 20:50:48 +0000 Subject: [PATCH 50/68] Switch to phpbench as Atheltic was abandoned. --- .github/workflows/ci.yml | 5 +- composer.json | 2 +- test/Benchmark/ExecuteBench.php | 77 +++++++++++++++++++++++++++++ test/Benchmark/ExecuteBenchmark.php | 63 ----------------------- test/Benchmark/TwoDeps.php | 10 ++++ 5 files changed, 92 insertions(+), 65 deletions(-) create mode 100644 test/Benchmark/ExecuteBench.php delete mode 100644 test/Benchmark/ExecuteBenchmark.php create mode 100644 test/Benchmark/TwoDeps.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cd8215..f24fc23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,4 +48,7 @@ jobs: run: vendor/bin/simple-phpunit --version - name: "Run tests" - run: vendor/bin/simple-phpunit \ No newline at end of file + run: vendor/bin/simple-phpunit + + - name: Execute benchmarks + run: vendor/bin/phpbench run \ No newline at end of file diff --git a/composer.json b/composer.json index 7dcf4ec..431be65 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,8 @@ "php": ">=7.2.0" }, "require-dev": { - "athletic/athletic": "~0.1", "friendsofphp/php-cs-fixer" : "3.3.1", + "phpbench/phpbench": "1.1.1", "symfony/phpunit-bridge": "^6.2.0" }, "autoload": { diff --git a/test/Benchmark/ExecuteBench.php b/test/Benchmark/ExecuteBench.php new file mode 100644 index 0000000..72a8a5a --- /dev/null +++ b/test/Benchmark/ExecuteBench.php @@ -0,0 +1,77 @@ +injector = new Injector(); + $this->noop = new Noop(); + } + + /** + * @Revs(10000) + */ + public function benchnative_invoke_closure() + { + call_user_func(function () { + // call-target, intenionally left empty + }); + } + + /** + * @Revs(10000) + */ + public function benchnative_invoke_method() + { + call_user_func(array($this->noop, 'noop')); + } + + /** + * @Revs(10000) + */ + public function benchinvoke_closure() + { + $this->injector->execute(function () { + // call-target, intentionally left empty + }); + } + + /** + * @Revs(10000) + */ + public function benchinvoke_method() + { + $this->injector->execute(array($this->noop, 'noop')); + } + + /** + * @Revs(10000) + */ + public function benchinvoke_with_named_parameters() + { + $this->injector->execute(array($this->noop, 'namedNoop'), array(':name' => 'foo')); + } + + /** + * @Revs(10000) + */ + public function bench_make_noop() + { + $this->injector->make(Noop::class); + } + + /** + * @Revs(10000) + */ + public function bench_make_two_dependency_object() + { + $this->injector->make(TwoDeps::class); + } +} diff --git a/test/Benchmark/ExecuteBenchmark.php b/test/Benchmark/ExecuteBenchmark.php deleted file mode 100644 index 85f74ed..0000000 --- a/test/Benchmark/ExecuteBenchmark.php +++ /dev/null @@ -1,63 +0,0 @@ -injector = new Injector(); - $this->noop = new Noop(); - } - - /** - * @baseline - * @iterations 10000 - */ - public function native_invoke_closure() - { - call_user_func(function () { - // call-target, intenionally left empty - }); - } - - /** - * @iterations 10000 - */ - public function native_invoke_method() - { - call_user_func(array($this->noop, 'noop')); - } - - /** - * @iterations 10000 - */ - public function invoke_closure() - { - $this->injector->execute(function () { - // call-target, intenionally left empty - }); - } - - /** - * @iterations 10000 - */ - public function invoke_method() - { - $this->injector->execute(array($this->noop, 'noop')); - } - - /** - * @iterations 10000 - */ - public function invoke_with_named_parameters() - { - $this->injector->execute(array($this->noop, 'namedNoop'), array(':name' => 'foo')); - } -} diff --git a/test/Benchmark/TwoDeps.php b/test/Benchmark/TwoDeps.php new file mode 100644 index 0000000..85eb6e1 --- /dev/null +++ b/test/Benchmark/TwoDeps.php @@ -0,0 +1,10 @@ + Date: Sun, 22 Jan 2023 13:03:46 +0000 Subject: [PATCH 51/68] Add coverage tests for Executable. --- test/ExecutableTest.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/ExecutableTest.php diff --git a/test/ExecutableTest.php b/test/ExecutableTest.php new file mode 100644 index 0000000..dfafa9b --- /dev/null +++ b/test/ExecutableTest.php @@ -0,0 +1,34 @@ +getMethod('foo'); + $obj = $rc->newInstanceWithoutConstructor(); + $executable = new Executable($rm, $obj); + + $this->assertSame($obj, $executable->getInvocationObject()); + $this->assertSame($rm, $executable->getCallableReflection()); + $this->assertTrue($executable->isInstanceMethod()); + } + + public function testBasicErrors() + { + $rc = new \ReflectionClass(ExecutableHelper::class); + $rm = $rc->getMethod('foo'); + $this->expectExceptionMessage("ReflectionMethod callables must specify an invocation object"); + $this->expectException(\InvalidArgumentException::class); + new Executable($rm, null); + } +} \ No newline at end of file From e00a69b16ff38ef6c8ee5b78242190b39552e889 Mon Sep 17 00:00:00 2001 From: Danack Date: Sun, 22 Jan 2023 17:27:49 +0000 Subject: [PATCH 52/68] Added tests and improved error messages, and wrote documentation for injector->make()/excute() args. --- README.md | 34 +++++++ lib/InjectionException.php | 36 +++++++ lib/Injector.php | 14 ++- phpunit.xml | 1 + test/BaseTest.php | 87 +++++++++++++++++ test/InjectorTest.php | 195 ++++++++++++++++++++++++++++++++++++- test/fixtures.php | 70 +++++++++++++ 7 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 test/BaseTest.php diff --git a/README.md b/README.md index c553f92..9bbbb53 100644 --- a/README.md +++ b/README.md @@ -664,6 +664,40 @@ $injector = new Auryn\Injector; var_dump($injector->execute('Example::myMethod', $args = [':arg2' => 42])); ``` +### Injector::make and Injector::execute custom args + +The args parameter in both of Injector::make($name, array $args = array()) and Injector::execute($callableOrMethodStr, array $args = array())) allow you to pass in a bespoke set of parameters to be used during the creation/execution. + +The rules for how those injector args are used is as follows. + +Given a parameter named 'foo' at parameter position 'i' which has a type of 'bar', for the thing being created/executed: + +1. If an integer indexed key 'i' is present (i.e. does `$args[$i]` exist?) then use the value of `$args[$i]` directly for that parameter. + +2. If an string indexed key 'foo' is present (i.e. does `$args['foo']` exist?) then use the value of `$args['foo']` for that parameter. + +3. If a string indexed key `Injector::A_DELEGATE . 'foo'` is present (i.e. does `$args['+foo']` exist?) then interpret `$args['+' . $i]` as a delegate callable to be invoked, and the return value to be used for that parameter. + +4. If a string indexed key `Injector::A_DEFINE . 'foo'` is present (i.e. does `$args['@foo']` exist?) then interpret `$args['+' . $i]` as an array with + +``` +$params = [ + PrefixDefineDependency::class, + [Injector::A_RAW . 'message' => $message] +]; + +$object = $injector->make( + PrefixDefineTest::class, + [Injector::A_DEFINE . 'pdd' => $params] +); +``` +i.e. when the injector is making the class `'PrefixDefineTest` which has a dependency on the class `PrefixDefineDependency`, which is named as parameter 'pdd' in the constructor, use the values in the array `$params[1]`, to instantiate the `PrefixDefineDependency` class. + + +5. If a string indexed key `Injector::A_DEFINE . '+foo'` is present (i.e. does `$args[':foo']` exist?) then interpret `$args['+' . $i]` as a value to be used a parameter defined by name. This is similar behaviour to `$injector->define('foo', 'bar');` + +6. Try to build the arg through the normal Auryn argument building process. + ### Dependency Resolution diff --git a/lib/InjectionException.php b/lib/InjectionException.php index aebe243..fc359dd 100644 --- a/lib/InjectionException.php +++ b/lib/InjectionException.php @@ -14,6 +14,42 @@ public function __construct(array $inProgressMakes, $message = "", $code = 0, \E parent::__construct($message, $code, $previous); } + public static function fromInvalidDefineParamsNotArray($definition, array $inProgressMakes): self + { + $message = sprintf( + Injector::M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY, + gettype($definition) + ); + + return new self( + $inProgressMakes, + $message, + Injector::E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY + ); + } + + public static function fromInvalidDefineParamsBadKeys($definition, array $inProgressMakes) + { + $missingKeys = []; + if (!isset($definition[0])) { + $missingKeys[] = "array key 0 not set"; + } + if (!isset($definition[1])) { + $missingKeys[] = "array key 1 not set"; + } + + $message = sprintf( + Injector::M_INVALID_DEFINE_ARGUMENT_BAD_KEYS, + implode(" ", $missingKeys) + ); + + return new self( + $inProgressMakes, + $message, + Injector::E_INVALID_DEFINE_ARGUMENT_BAD_KEYS + ); + } + public static function getInvalidCallableMessage($callableOrMethodStr) { $callableString = null; diff --git a/lib/Injector.php b/lib/Injector.php index 212bd01..001b728 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -39,6 +39,12 @@ class Injector const E_MAKING_FAILED = 12; const M_MAKING_FAILED = "Making %s did not result in an object, instead result is of type '%s'"; + const E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY = 12; + const M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY = "Define parameters needs to be an array with contents of {0:class-string, 1:array of injector params}. Value passed was of type '%s'."; + + const E_INVALID_DEFINE_ARGUMENT_BAD_KEYS = 13; + const M_INVALID_DEFINE_ARGUMENT_BAD_KEYS = "Define parameters needs to be an array with contents of {0:class-string, 1:array of injector params}. %s."; + private $reflector; private $classDefinitions = array(); private $paramDefinitions = array(); @@ -484,16 +490,16 @@ private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array private function buildArgFromParamDefineArr($definition) { if (!is_array($definition)) { - throw new InjectionException( + throw InjectionException::fromInvalidDefineParamsNotArray( + $definition, $this->inProgressMakes - // @TODO Add message ); } if (!isset($definition[0], $definition[1])) { - throw new InjectionException( + throw InjectionException::fromInvalidDefineParamsBadKeys( + $definition, $this->inProgressMakes - // @TODO Add message ); } diff --git a/phpunit.xml b/phpunit.xml index cdd5b7c..e86a108 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ + diff --git a/test/BaseTest.php b/test/BaseTest.php new file mode 100644 index 0000000..f7656cf --- /dev/null +++ b/test/BaseTest.php @@ -0,0 +1,87 @@ + '.*', // strings can be empty, so * + // decimal (integer) number (base 10) + '%d' => '[+-]?\d+', // numbers can't be empty so + + + // character + '%c' => '[\s\S]', + + // todo + // exponential floating-point number + // %e + + // floating-point number + '%f' => "[+-]?([0-9]*[.])?[0-9]+", + + // integer (base 10) + '%i' => '[+-]?\d+', + + // octal number (base 8) + '%o' => "[+-]?([0-7])+", + + // unsigned decimal (integer) number + '%u' => '[0-9][0-9]*', + + // number in hexadecimal (base 16) + '%x' => '[[:xdigit:]]+' + ]; + + $string = str_replace( + array_keys($replacements), + array_values($replacements), + $string + ); + + return '#' . $string . '#iu'; +} + +abstract class BaseTest extends TestCase +{ + /** + * @param string $templateString A template string for printf e.g. "Hello %s" + * @param string $actualString The string to test to see if it matches e.g. "Hello John" + */ + public function assertStringMatchesTemplateString(string $templateString, string $actualString): void + { + $regExp = templateStringToRegExp($templateString); + $this->assertMatchesRegularExpression($regExp, $actualString); + } + + /** + * @param string $templateString A template string for printf e.g. "Hello %s" + */ + public function expectExceptionMessageMatchesTemplateString(string $templateString): void + { + $regexp = templateStringToRegExp($templateString); + $this->expectExceptionMessageMatches($regexp); + } + + /** + * @param string $text The string to look for. + * @param string $flags The regexp flags to use. + */ + public function expectExceptionMessageContains(string $text, string $flags = 'iu'): void + { + $regexp = '/' . preg_quote($text, '/') . '/' . $flags; + $this->expectExceptionMessageMatches($regexp); + } +} \ No newline at end of file diff --git a/test/InjectorTest.php b/test/InjectorTest.php index db274ce..44bb5da 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1,11 +1,10 @@ make('Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault'); } + public function testbuildArgFromReflParamCoverageNonClassCase() + { + $injector= new Injector; + + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector->execute('Auryn\Test\aFunctionWithAParam'); + } + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithoutDefinitionOrDefaultThroughAliasedType() { $this->expectException(\Auryn\InjectionException::class); @@ -924,7 +931,18 @@ public function testMakeExecutableFailsOnClassWithoutInvoke() $injector->buildExecutable($object); } - public function testBadAlias() + public function testBadAliasFirstArg() + { + $injector = new Injector; + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage(Injector::M_NON_EMPTY_STRING_ALIAS); + $this->expectExceptionCode(\Auryn\Injector::E_NON_EMPTY_STRING_ALIAS); + + $injector->alias('', 'Auryn\Test\DepImplementation'); + } + + public function testBadAliasSecondArg() { $injector = new Injector(); $injector->share('Auryn\Test\DepInterface'); @@ -1258,9 +1276,176 @@ public function testThatExceptionInConstructorDoesntCauseCyclicDependencyExcepti public function testProvidesExtensionsOfArrayMap() { - $injector = New Injector; + $injector = new Injector; $obj = $injector->make('\Auryn\Test\ExtendedExtendedArrayObject'); $this->assertInstanceOf('\ArrayObject', $obj); } + + // interpret the param as an invokable delegate + public function testMakeWithParameter_delegate() + { + $value = 'testMakeWithParameter_delegate'; + $closure_was_called = false; + $fn = function () use (&$closure_was_called, $value) { + $closure_was_called = true; + return \Auryn\Test\PrefixDelegateTestDependency::create($value); + }; + + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DELEGATE . 'b' => $fn] + ); + $this->assertInstanceOf( + \Auryn\Test\PrefixDelegateTest::class, + $object + ); + $this->assertTrue($closure_was_called); + $this->assertSame($value, $object->getB()->getValue()); + } + + // // interpret the param as a raw value to be injected + public function testMakeWithParameter_raw() + { + $value = 'testMakeWithParameter_raw'; + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_RAW . 'b' => \Auryn\Test\PrefixDelegateTestDependency::create($value)] + ); + $this->assertInstanceOf( + \Auryn\Test\PrefixDelegateTest::class, + $object + ); + + $this->assertSame($value, $object->getB()->getValue()); + } + + // interpret the param as a class definition + public function testMakeWithParameter_define() + { + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => [\Auryn\Test\PrefixDelegateTestDependencyInstantiable::class, []]] + ); + $this->assertInstanceOf( + \Auryn\Test\PrefixDelegateTest::class, + $object + ); + + $this->assertSame('this is the child class', $object->getB()->getValue()); + } + + /** + * interpret the param as a class definition + * @return void + * @throws \Auryn\InjectionException + */ + public function testMakeWithParameter_define_uses_info() + { + $message = "great success"; + $injector = new Injector; + + $params = [ + \Auryn\Test\PrefixDefineDependency::class, + [Injector::A_RAW . 'message' => $message] + ]; + + $object = $injector->make( + \Auryn\Test\PrefixDefineTest::class, + [Injector::A_DEFINE . 'pdd' => $params] + ); + + $this->assertInstanceOf( + \Auryn\Test\PrefixDefineTest::class, + $object + ); + + $dependency = $object->getPdd(); + $this->assertInstanceOf(\Auryn\Test\PrefixDefineDependency::class, $dependency); + $this->assertSame($message, $dependency->message); + } + + public function testIndexedArrayElementOverridesNamedPostion() + { + $message = "This is used."; + $params = [ + Injector::A_RAW . 'message' => "This is not used", + 0 => $message + ]; + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDefineDependency::class, + $params + ); + + $this->assertInstanceOf( + \Auryn\Test\PrefixDefineDependency::class, + $object + ); + + $this->assertSame($message, $object->message); + } + + public function testMakeWithParameter_define_errors_not_array() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY); + $this->expectExceptionMessageMatchesTemplateString( + Injector::M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY + ); + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => 'this is not an array'] + ); + } + + public function testMakeWithParameter_define_errors_bad_indexed_array_empty() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVALID_DEFINE_ARGUMENT_BAD_KEYS); + $this->expectExceptionMessageMatchesTemplateString( + Injector::M_INVALID_DEFINE_ARGUMENT_BAD_KEYS + ); + + $this->expectExceptionMessageContains("array key 0 not set array key 1 not set"); + + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => []] + ); + } + + public function testMakeWithParameter_define_errors_bad_indexed_array_wrong_position() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVALID_DEFINE_ARGUMENT_BAD_KEYS); + $this->expectExceptionMessageMatchesTemplateString( + Injector::M_INVALID_DEFINE_ARGUMENT_BAD_KEYS + ); + + $this->expectExceptionMessageContains("array key 1 not set"); + + $params = [ + \Auryn\Test\PrefixDelegateTestDependencyInstantiable::class, + 2 => [] + ]; + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => $params] + ); + } + + public function testMakeWithParameter_delegate_errors_not_callable() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVOKABLE); + $this->expectExceptionMessage(Injector::M_INVOKABLE); + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DELEGATE . 'b' => 'this is not callable'] + ); + } } diff --git a/test/fixtures.php b/test/fixtures.php index f791931..c92a526 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -753,3 +753,73 @@ public function __construct() { class ExtendedArrayObject extends \ArrayObject {} class ExtendedExtendedArrayObject extends ExtendedArrayObject {} + +class PrefixDelegateTestDependency { + private $value; + protected function __construct($value) + { + $this->value = $value; + } + public static function create($value) + { + return new static($value); + } + + public function getValue() + { + return $this->value; + } +} + +class PrefixDelegateTestDependencyInstantiable extends PrefixDelegateTestDependency +{ + public function __construct() + { + parent::__construct('this is the child class'); + } +} + +class PrefixDelegateTest +{ + private $b; + public function __construct(PrefixDelegateTestDependency $b) + { + $this->b = $b; + } + public function getB() + { + return $this->b; + } +} + +class PrefixDefineDependency { + public $message; + public function __construct($message) + { + $this->message = $message; + } +} + +class PrefixDefineTest +{ + private $pdd; + public function __construct(PrefixDefineDependency $pdd) + { + $this->pdd = $pdd; + } + public function getPdd() + { + return $this->pdd; + } +} + +function aFunctionWithAParam($foo) +{ + return $foo; +} + + +class ExecutableHelper +{ + public function foo() {} +} \ No newline at end of file From 65a1517746ff84f6c9b2688c5da89a7c94f7b219 Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 12:36:43 +0100 Subject: [PATCH 53/68] Added note on static method of how it could be private. --- lib/InjectionException.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/InjectionException.php b/lib/InjectionException.php index fc359dd..2c4ba96 100644 --- a/lib/InjectionException.php +++ b/lib/InjectionException.php @@ -50,6 +50,12 @@ public static function fromInvalidDefineParamsBadKeys($definition, array $inProg ); } + /** + * If PHP had package based privacy rules, this could be package private + * or this could be 'just' a function. + * @param $callableOrMethodStr + * @return string + */ public static function getInvalidCallableMessage($callableOrMethodStr) { $callableString = null; From 83592b6e034b261943a4ec0bcd76c3e4a9bd52dd Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 12:42:55 +0100 Subject: [PATCH 54/68] Added phpbench config file. --- phpbench.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 phpbench.json diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..ec0a8b3 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,5 @@ +{ + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "test/Benchmark", + "runner.file_pattern": "*Bench.php" +} \ No newline at end of file From cef65065c62e337a843d8be1c7e62d9dc8fcd72c Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 13:15:31 +0100 Subject: [PATCH 55/68] Ignore lines for code coverage, so that testing can be at 100% locally. --- lib/StandardReflector.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/StandardReflector.php b/lib/StandardReflector.php index 6c4a071..723d394 100644 --- a/lib/StandardReflector.php +++ b/lib/StandardReflector.php @@ -34,6 +34,8 @@ public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionP return $type ? (string) $type : null; } else { + // @codeCoverageIgnoreStart + // Can't be tested on PHP 8 /** @var ?\ReflectionClass $reflectionClass */ $reflectionClass = $param->getClass(); if ($reflectionClass) { @@ -41,6 +43,7 @@ public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionP } return null; + // @codeCoverageIgnoreEnd } } From 829fd9513974172a6842e30856308a1afb962479 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 21 Jan 2023 11:31:51 +0000 Subject: [PATCH 56/68] Document behaviour of prepare replacement. --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 9bbbb53..fe88f64 100644 --- a/README.md +++ b/README.md @@ -622,6 +622,35 @@ var_dump($myObj->myProperty); // int(42) While the above example is contrived, the usefulness should be clear. +Additionally, the prepare method is able to replace the object being prepared with another of the same or descendant type: + +```php +prepare(FooGreeter::class, function($myObj, $injector) { + return new BarGreeter(); +}); + +$myObj = $injector->make(FooGreeter::class); +echo $myObj->getMessage(); // Output is: "Hello, I am bar." +``` +The usefulness of this is much less clear. + +Any value returned that is not the same or descendant type will be ignored. ### Injecting for Execution From 12e8f2c40c9f1e89a9df8701b8cf76ab83f6a7cd Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 21 Jan 2023 11:48:34 +0000 Subject: [PATCH 57/68] Move check for whether an object was created by a delegate method to a better place. Also avoids the need for an error suppression usage. --- lib/Injector.php | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/Injector.php b/lib/Injector.php index 001b728..e16b27a 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -374,7 +374,18 @@ public function make($name, array $args = array()) $reflectionFunction = $executable->getCallableReflection(); $args = $this->provisionFuncArgs($reflectionFunction, $args, null, $className); $obj = call_user_func_array(array($executable, '__invoke'), $args); - } else { + if (!($obj instanceof $normalizedClass)) { + throw new InjectionException( + $this->inProgressMakes, + sprintf( + self::M_MAKING_FAILED, + $normalizedClass, + gettype($obj) + ), + self::E_MAKING_FAILED + ); + } + } else { $obj = $this->provisionInstance($className, $normalizedClass, $args); } @@ -584,6 +595,7 @@ private function buildArgFromReflParam(\ReflectionParameter $reflParam, $classNa private function prepareInstance($obj, $normalizedClass) { + // Check and call any prepares for a class. if (isset($this->prepares[$normalizedClass])) { $prepare = $this->prepares[$normalizedClass]; $executable = $this->buildExecutable($prepare); @@ -593,24 +605,7 @@ private function prepareInstance($obj, $normalizedClass) } } - // TODO - this is inelegant - if ($obj === null) { - $interfaces = false; - } else { - $interfaces = @class_implements($obj); - } - - if ($interfaces === false) { - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_MAKING_FAILED, - $normalizedClass, - gettype($obj) - ), - self::E_MAKING_FAILED - ); - } + $interfaces = class_implements($obj); if (empty($interfaces)) { return $obj; From 2d03a9efa2e6f5444fb9293fd01c28ea5f79b741 Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 19:23:15 +0100 Subject: [PATCH 58/68] Prevent double sharing of same type. --- lib/Injector.php | 14 ++++++++++++++ test/InjectorTest.php | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/Injector.php b/lib/Injector.php index e16b27a..4633d2d 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -38,6 +38,8 @@ class Injector const M_CYCLIC_DEPENDENCY = "Detected a cyclic dependency while provisioning %s"; const E_MAKING_FAILED = 12; const M_MAKING_FAILED = "Making %s did not result in an object, instead result is of type '%s'"; + const E_DOUBLE_SHARE = 13; + const M_DOUBLE_SHARE = "An instance of type %s has already been shared. Cannot share a second instance of the same type."; const E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY = 12; const M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY = "Define parameters needs to be an array with contents of {0:class-string, 1:array of injector params}. Value passed was of type '%s'."; @@ -159,6 +161,8 @@ private function normalizeName($className) */ public function share($nameOrInstance) { + + if (is_string($nameOrInstance)) { $this->shareClass($nameOrInstance); } elseif (is_object($nameOrInstance)) { @@ -210,6 +214,16 @@ private function shareInstance($obj) self::E_ALIASED_CANNOT_SHARE ); } + + if (isset($this->shares[$normalizedName])) { + throw new ConfigException( + sprintf( + self::M_DOUBLE_SHARE, + get_class($obj), + ), + self::E_DOUBLE_SHARE + ); + } $this->shares[$normalizedName] = $obj; } diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 44bb5da..17bd2a6 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1448,4 +1448,16 @@ public function testMakeWithParameter_delegate_errors_not_callable() [Injector::A_DELEGATE . 'b' => 'this is not callable'] ); } + + public function testDoubleShareClassThrows() + { + $injector = new Injector; + $injector->share(new \StdClass); + + $this->expectExceptionCode(Injector::E_DOUBLE_SHARE); + $this->expectExceptionMessageMatchesTemplateString(Injector::M_DOUBLE_SHARE); + $this->expectExceptionMessageContains('stdclass'); + + $injector->share(new \StdClass); + } } From f2f81c2fe32a134e58579aa1037134936492bc83 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 20 Apr 2023 10:07:02 +0100 Subject: [PATCH 59/68] Add test coverage for new in initializer. --- test/InjectorTest.php | 14 ++++++++++++++ test/fixtures_8_1.php | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 test/fixtures_8_1.php diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 17bd2a6..1f68e56 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1460,4 +1460,18 @@ public function testDoubleShareClassThrows() $injector->share(new \StdClass); } + + /** + * @requires PHP 8.1 + */ + public function testNewInIntializer() + { + require_once __DIR__ . "/fixtures_8_1.php"; + + $injector = new Injector; + $obj = $injector->make(\NewInInitializer::class); + + $this->assertInstanceOf(\NewInInitializer::class, $obj); + $this->assertInstanceOf(\NewInInitializerDependency::class, $obj->instance); + } } diff --git a/test/fixtures_8_1.php b/test/fixtures_8_1.php new file mode 100644 index 0000000..d6b4d4b --- /dev/null +++ b/test/fixtures_8_1.php @@ -0,0 +1,16 @@ +instance = $instance; + } +} + + + + + From f7cb77243c77ad10954a34aba84a1dfe8795ec00 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 20 Apr 2023 11:02:27 +0100 Subject: [PATCH 60/68] Added test coverage and note on why optional dependencies are not created. --- README.md | 2 +- excluded_features.md | 59 +++++++++++++++++++++++++++++++++++++++++++ test/InjectorTest.php | 26 +++++++++++++++++++ test/fixtures_8_0.php | 37 +++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 test/fixtures_8_0.php diff --git a/README.md b/README.md index fe88f64..02e20b8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ S.O.L.I.D., object-oriented PHP applications. `rdlowrey/auryn` is in low maintenance mode. i.e. new features are very unlikely to be added, and new releases to support new versions of PHP are not guaranteed to be timely. Notes on why some -features were not added to Auryn are listed [here](https://github.com/rdlowrey/excluded_features.md). +features were not added to Auryn are listed [here](excluded_features.md). There are similar libraries available at: diff --git a/excluded_features.md b/excluded_features.md index 9e70eb6..4b1b056 100644 --- a/excluded_features.md +++ b/excluded_features.md @@ -350,3 +350,62 @@ If you wanted to see what class was going to be created for an interface, not be Additionally, if a second class that implements the interface was added, the injector would need to either pick one or throw an exception of "multiple available types". Either choice would be quite surprising and take more time to resolve than simply writing the explicit configuration. +## Optional aka null default parameters + +Original discussion: https://github.com/rdlowrey/auryn/issues/190 + +### Description + + +When a dependency is optional and/or nullable, then Auryn will always pass null as the parameter: + +```php +class NullableDependency {} + +class DependsOnNullableDependency +{ + public ?NullableDependency $string; + + public function __construct(?NullableDependency $instance = null) + { + $this->instance = $instance; + } +} +``` + +Some people might expect that the NullableDependency would be created. + + +### Why the parameter is always null + + +* Auryn defaults to explicit configuration. If an dependency is optional, then defaulting to not creating it, is more consistent with that principle. + +* Some classes are not instantiable e.g. abstract classes, or classes with uncreatable dependencies. If Auryn defaulted to attempting to create the type, we would need to add "don't create this class" rules/config. + + +### Alternative solution - be explicit + +```php +$injector->alias(NullableDependency::class, NullableDependency::class); +``` + + +### Alternative solution - new in the initializer + +As of PHP 8.1, you can use create objects the initializer: + + +```php +class NullableDependency {} + +class DependsOnNullableDependency +{ + public ?NullableDependency $string; + + public function __construct(?NullableDependency $instance = new NullableDependency) + { + $this->instance = $instance; + } +} +``` \ No newline at end of file diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 1f68e56..b344f3a 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1461,6 +1461,32 @@ public function testDoubleShareClassThrows() $injector->share(new \StdClass); } + + /** + * This test is duplication of other tests. It is present to check + * that the behaviour of three different ways of params being null-ish + * are consistent. + * + * @requires PHP 8.0 + */ + public function testNullConsistency() + { + require_once __DIR__ . "/fixtures_8_0.php"; + + $injector = new Injector; + $obj = $injector->make(\NullableDependency::class); + $this->assertInstanceOf(\NullableDependency::class, $obj); + $this->assertNull($obj->instance); + + $obj = $injector->make(\UnionNullDependency::class); + $this->assertInstanceOf(\UnionNullDependency::class, $obj); + $this->assertNull($obj->instance); + + $obj = $injector->make(\DefaultNullDependency::class); + $this->assertInstanceOf(\DefaultNullDependency::class, $obj); + $this->assertNull($obj->instance); + } + /** * @requires PHP 8.1 */ diff --git a/test/fixtures_8_0.php b/test/fixtures_8_0.php new file mode 100644 index 0000000..8128b18 --- /dev/null +++ b/test/fixtures_8_0.php @@ -0,0 +1,37 @@ +instance = $instance; + } +} + +class UnionNullDependency +{ + public ?Dependency $string; + + public function __construct(?Dependency $instance = null) + { + $this->instance = $instance; + } +} + + + +class DefaultNullDependency +{ + public ?Dependency $string; + + public function __construct(Dependency $instance = null) + { + $this->instance = $instance; + } +} \ No newline at end of file From 46b7998d8be7313017c5d1d7a1308b61760f3bd8 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 20 Apr 2023 13:03:38 +0100 Subject: [PATCH 61/68] Fix property name. --- test/fixtures_8_0.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fixtures_8_0.php b/test/fixtures_8_0.php index 8128b18..a1c3354 100644 --- a/test/fixtures_8_0.php +++ b/test/fixtures_8_0.php @@ -6,7 +6,7 @@ class Dependency {} class NullableDependency { - public ?Dependency $string; + public ?Dependency $instance; public function __construct(?Dependency $instance = null) { @@ -16,7 +16,7 @@ public function __construct(?Dependency $instance = null) class UnionNullDependency { - public ?Dependency $string; + public ?Dependency $instance; public function __construct(?Dependency $instance = null) { @@ -28,7 +28,7 @@ public function __construct(?Dependency $instance = null) class DefaultNullDependency { - public ?Dependency $string; + public ?Dependency $instance; public function __construct(Dependency $instance = null) { From 322c91d90ecd36215d51eddab213d810a4c17c5e Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 20 Apr 2023 13:05:37 +0100 Subject: [PATCH 62/68] Fix trailing comma. --- lib/Injector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Injector.php b/lib/Injector.php index 4633d2d..2abf789 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -219,7 +219,7 @@ private function shareInstance($obj) throw new ConfigException( sprintf( self::M_DOUBLE_SHARE, - get_class($obj), + get_class($obj) ), self::E_DOUBLE_SHARE ); From ae0e21e6332176761a797f289415b0ad036ab4e4 Mon Sep 17 00:00:00 2001 From: Danack Date: Thu, 20 Apr 2023 21:31:31 +0100 Subject: [PATCH 63/68] Minimum version is PHP 7.2 which is > 5.6 so check is not needed. --- lib/Injector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Injector.php b/lib/Injector.php index 2abf789..c504b06 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -499,7 +499,7 @@ private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array } elseif (!$arg = $this->buildArgFromType($reflFunc, $reflParam)) { $arg = $this->buildArgFromReflParam($reflParam, $className); - if ($arg === null && (PHP_VERSION_ID >= 50600 && $reflParam->isVariadic() || $reflParam->isOptional())) { + if ($arg === null && ($reflParam->isVariadic() || $reflParam->isOptional())) { // buildArgFromReflParam might return null in case the parameter is optional // in case of variadics, the parameter is optional, but null might not be allowed continue; From e3e9d00785dad03ecb91e8c43b353222fb92a4eb Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 21 Apr 2023 18:49:57 +0100 Subject: [PATCH 64/68] Adding non-trivial benchmark. --- test/Benchmark/AliasedImplementation.php | 8 +++++ test/Benchmark/AliasedInterface.php | 8 +++++ test/Benchmark/DelegatedClass.php | 15 ++++++++ test/Benchmark/NonTrivial.php | 19 +++++++++++ test/Benchmark/SharedInstance.php | 8 +++++ .../SlightylyMoreComplicatedBench.php | 34 +++++++++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 test/Benchmark/AliasedImplementation.php create mode 100644 test/Benchmark/AliasedInterface.php create mode 100644 test/Benchmark/DelegatedClass.php create mode 100644 test/Benchmark/NonTrivial.php create mode 100644 test/Benchmark/SharedInstance.php create mode 100644 test/Benchmark/SlightylyMoreComplicatedBench.php diff --git a/test/Benchmark/AliasedImplementation.php b/test/Benchmark/AliasedImplementation.php new file mode 100644 index 0000000..9e22c92 --- /dev/null +++ b/test/Benchmark/AliasedImplementation.php @@ -0,0 +1,8 @@ +injector = new Injector(); + + $this->injector->delegate( + DelegatedClass::class, + [\Auryn\Test\Benchmark\DelegatedClass::class, 'create'] + ); + $this->injector->alias( + AliasedInterface::class, + AliasedImplementation::class + ); + + $this->injector->share(new SharedInstance('John')); + } + + /** + * @Revs(10000) + */ + public function bench_make_non_trivial_object() + { + $this->injector->make(NonTrivial::class); + } +} From 64f32c4de08b4b61d791b32ef2165c2daf2f7e90 Mon Sep 17 00:00:00 2001 From: Danack Date: Mon, 17 Apr 2023 16:28:31 +0100 Subject: [PATCH 65/68] WIP words... --- README.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/README.md b/README.md index 02e20b8..3c7835e 100644 --- a/README.md +++ b/README.md @@ -970,6 +970,112 @@ function getAurynInjector() } ``` + +## Advanced patterns + +### "Variadic" dependencies + + +```php + +class Foo { + public function __construct(array ...$repositories) { + // ... + } +} +``` + +In that scenario $repositories does not represent a single simple variable and so Auryn's ability to make life a bit easier for the programmer does not apply. + +Instead ...$repositories represents a complex type. This will need a more advanced technique to be able to inject. I think the two possible solutions are to either use + +Use a delegate method +The simplest way to achieve what you want to do is to use a delegate function for creating objects that have this variable dependency. + +```php +function createFoo(RepositoryLocator $repoLocator) +{ + //Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + + return new Foo($repositories); +} + +$injector->delegate('Foo', 'createFoo') +``` + +This does what you want....however it has some limitations. In particular it can only be used for constructor injection. It cannot be used to inject the dependencies as a parameter in normal functions or methods. + +Refactor the code to use a context +Using a 'context' doesn't have these limitations. Or to give it the full name, using the 'Encapsulated context patten' + +The trade-off is that it would require you to refactor your code a little bit. This is a good trade-off to make, in this case. In fact it's a fantastic trade-off. It makes your code far easier to reason about. + +// This is the context that holds the 'repositories' +class FooRepositories { +private $repositories; + + private function __construct(RepositoryLocator $repoLocator) + { + //Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + } +} + +class Foo { +public function __construct(FooRepositories $fooRepositories) { +// ... +} +} + +There are a couple of reasons why using a context is a suprior solution: + +The dependency is now a named type, which means that you can see how it is used in your codebase without having to know which class it is used in. +If some other code has a dependency on a separate set of repositories, you can create a separate context for that code. It is far easier to understand that FooRepositories is separate from BarRepositories compared to trying to reason about ...$repositories used in one place, and ...$repositories used in a separate place. +If you're using a framework such as Tier that allows multiple levels of execution then it's much easier to create specific context types and pass them around as dependencies rather than hoping for the best by creating a generically named variadic parameter and hoping for the best when passing that around. +TL:DR - Auryn shouldn't handle variadics at all imo. They aren't a type and so can't be reasoned about by a DIC. People should either use delegation or contexts to achieve what they're trying to do in a sane way. + + + +### Context objects and multiple instances of the same types + +Sometimes you might need to have + + +$injector->alias('WriteDB', 'DbAdapter'); +$injector->alias('ReadDb', 'DbAdapter'); + +Depending on the exact reason + +Or to give it the full name, using the 'Encapsulated context pattern'](https://hillside.net/europlop/HillsideEurope/Papers/EuroPLoP2003/2003_Kelly_EncapsulateContext.pdf). + +It makes your code far easier to reason about. + +// This is the context that holds the 'repositories' +class FooRepositories { + + private $repositories; + + private function __construct(RepositoryLocator $repoLocator) + { + // Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + } +} + +class Foo { +public function __construct(FooRepositories $fooRepositories) { +// ... +} +} +``` +There are a couple of reasons why using a context is a superior solution: + +The dependency is now a named type, which means that you can see how it is used in your codebase without having to know which class it is used in. + +If some other code has a dependency on a separate set of repositories, you can create a separate context for that code. It is far easier to understand that FooRepositories is separate from BarRepositories compared to trying to reason about ...$repositories used in one place, and ...$repositories used in a separate place. + + ### Running tests and benchmarks #### Running tests From 5a3e75190df8f3ee3c25c4ff32eb8fd6f4dfaf99 Mon Sep 17 00:00:00 2001 From: Danack Date: Fri, 21 Apr 2023 16:52:59 +0100 Subject: [PATCH 66/68] Reasonable draft of words. --- README.md | 140 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3c7835e..632291d 100644 --- a/README.md +++ b/README.md @@ -973,29 +973,32 @@ function getAurynInjector() ## Advanced patterns -### "Variadic" dependencies +### "Variadic" dependencies +Sometimes your code might need a variable number of objects to be passed as a parameter. ```php class Foo { - public function __construct(array ...$repositories) { - // ... + public function __construct(Repository ...$repositories) { + // do stuff with $repositories } } ``` -In that scenario $repositories does not represent a single simple variable and so Auryn's ability to make life a bit easier for the programmer does not apply. +In this scenario $repositories does not represent a single simple variable, instead ...$repositories represents a complex type. -Instead ...$repositories represents a complex type. This will need a more advanced technique to be able to inject. I think the two possible solutions are to either use +As Auryn works by defining rules about types, Auryn isn't able to do injection and so you'll need to use a more advanced technique to be able to inject. -Use a delegate method -The simplest way to achieve what you want to do is to use a delegate function for creating objects that have this variable dependency. + +#### Variadics using delegate function + +The simplest way to support being able to create objects that themselves have variadic dependencies, is to use a delegate function to create it: ```php function createFoo(RepositoryLocator $repoLocator) { - //Or whatever code is needed to find the repos. + // Or whatever code is needed to find the repos. $repositories = $repoLocator->getRepos('Foo'); return new Foo($repositories); @@ -1004,77 +1007,122 @@ function createFoo(RepositoryLocator $repoLocator) $injector->delegate('Foo', 'createFoo') ``` -This does what you want....however it has some limitations. In particular it can only be used for constructor injection. It cannot be used to inject the dependencies as a parameter in normal functions or methods. +This should only take a few moments to write the code for, but it has the downside that it moves some application logic into injector. -Refactor the code to use a context -Using a 'context' doesn't have these limitations. Or to give it the full name, using the 'Encapsulated context patten' +#### Variadics using factory classes -The trade-off is that it would require you to refactor your code a little bit. This is a good trade-off to make, in this case. In fact it's a fantastic trade-off. It makes your code far easier to reason about. +A very slightly longer way to create objects that themselves have variadic dependencies, is to refactor them to use a factory object to get the dependencies: -// This is the context that holds the 'repositories' -class FooRepositories { -private $repositories; +```php - private function __construct(RepositoryLocator $repoLocator) - { - //Or whatever code is needed to find the repos. - $repositories = $repoLocator->getRepos('Foo'); +class RepositoryList +{ + /** + * @return Repository[] + */ + public function getRelevantRepositories() { + // do stuff with $repositories } } class Foo { -public function __construct(FooRepositories $fooRepositories) { -// ... -} + public function __construct(RepositoryList $respositoryList) + { + $repositories = $respositoryList->getRelevantRepositories(); + + // error handling goes here + + // do stuff with $repositories + } } +``` -There are a couple of reasons why using a context is a suprior solution: +This probably a slightly better approach than using the delegate method, as it avoids business/application logic being in the dependency injector, and give you an appropriate place inside your own code to handle errors. -The dependency is now a named type, which means that you can see how it is used in your codebase without having to know which class it is used in. -If some other code has a dependency on a separate set of repositories, you can create a separate context for that code. It is far easier to understand that FooRepositories is separate from BarRepositories compared to trying to reason about ...$repositories used in one place, and ...$repositories used in a separate place. -If you're using a framework such as Tier that allows multiple levels of execution then it's much easier to create specific context types and pass them around as dependencies rather than hoping for the best by creating a generically named variadic parameter and hoping for the best when passing that around. -TL:DR - Auryn shouldn't handle variadics at all imo. They aren't a type and so can't be reasoned about by a DIC. People should either use delegation or contexts to achieve what they're trying to do in a sane way. +### Context objects and multiple instances of the same types +Sometimes you might need to have multiple instances of the same type. +For example, a background job that moves data from the live database, into the archive database might need to have two instances of a DB class injected -### Context objects and multiple instances of the same types +```php -Sometimes you might need to have +class DataArchiver +{ + public function __construct(private PDO $live_db, private PDO $archive_db) + { + } +} +``` +This _can_ be worked around by using the type system to create more specific types: -$injector->alias('WriteDB', 'DbAdapter'); -$injector->alias('ReadDb', 'DbAdapter'); +```php +class LivePDO extends PDO {} +class ArchivePDO extends PDO {} -Depending on the exact reason +class DataArchiver +{ + public function __construct(private LivePDO $live_db, private ArchivePDO $archive_db) + { + } +} +``` -Or to give it the full name, using the 'Encapsulated context pattern'](https://hillside.net/europlop/HillsideEurope/Papers/EuroPLoP2003/2003_Kelly_EncapsulateContext.pdf). +The more specific types can then be created through Auryn, by configuring an appropriate delegate function for each of them -It makes your code far easier to reason about. +This approach works, and is actually a reasonable one for small projects, there is an more comprehesive approach that is more appropriate for larger projects. -// This is the context that holds the 'repositories' -class FooRepositories { +#### Encapsulated contexts - private $repositories; +Or to give it the full name, using the 'Encapsulated context pattern'](https://www.allankelly.net/static/patterns/encapsulatecontext.pdf). - private function __construct(RepositoryLocator $repoLocator) +The short description of 'Encapsulated contexts' is that you create specific types that hold all of the needed types for a particular business/domain problem, and allow you to wire them up specifically: + +```php +class DataArchiverContext +{ + public function __construct( + private PDO $live_db, + private PDO $archive_db + ) { + + public function get_live_db(): PDO { - // Or whatever code is needed to find the repos. - $repositories = $repoLocator->getRepos('Foo'); + return $this->live_db; + } + + public function get_archive_db(): PDO + { + return $this->archive_db; } } -class Foo { -public function __construct(FooRepositories $fooRepositories) { -// ... +class DataArchiver +{ + public function __construct(private DataArchiverContext $dac) + { + } } + +function createDataArchiver() +{ + return new DataArchiver( + createLiveDB(), + createArchiveDB() + ); } + +$injector->delegate(DataArchiverContext::class, 'createDataArchiver'); ``` -There are a couple of reasons why using a context is a superior solution: -The dependency is now a named type, which means that you can see how it is used in your codebase without having to know which class it is used in. +Encapsulated contexts makes your code far easier to reason about. You can see: -If some other code has a dependency on a separate set of repositories, you can create a separate context for that code. It is far easier to understand that FooRepositories is separate from BarRepositories compared to trying to reason about ...$repositories used in one place, and ...$repositories used in a separate place. +* where a particular context is used. +* what types are in it. +* how it is created, including any special rules for it. +This makes maintaining and reasoning about large programs much easier. ### Running tests and benchmarks From 4cd4b4e066aca9add697142062d7bdc7a4311645 Mon Sep 17 00:00:00 2001 From: Danack Date: Sat, 22 Apr 2023 12:26:47 +0100 Subject: [PATCH 67/68] Typos. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 632291d..12be920 100644 --- a/README.md +++ b/README.md @@ -986,7 +986,7 @@ class Foo { } ``` -In this scenario $repositories does not represent a single simple variable, instead ...$repositories represents a complex type. +In this scenario `$repositories` does not represent a single simple variable, instead `$repositories` represents a complex type. As Auryn works by defining rules about types, Auryn isn't able to do injection and so you'll need to use a more advanced technique to be able to inject. @@ -1004,7 +1004,7 @@ function createFoo(RepositoryLocator $repoLocator) return new Foo($repositories); } -$injector->delegate('Foo', 'createFoo') +$injector->delegate('Foo', 'createFoo'); ``` This should only take a few moments to write the code for, but it has the downside that it moves some application logic into injector. @@ -1071,7 +1071,7 @@ class DataArchiver The more specific types can then be created through Auryn, by configuring an appropriate delegate function for each of them -This approach works, and is actually a reasonable one for small projects, there is an more comprehesive approach that is more appropriate for larger projects. +This approach works, and is actually a reasonable one for small projects, there is an more comprehensive approach that is more appropriate for larger projects. #### Encapsulated contexts @@ -1122,7 +1122,7 @@ Encapsulated contexts makes your code far easier to reason about. You can see: * what types are in it. * how it is created, including any special rules for it. -This makes maintaining and reasoning about large programs much easier. +This makes maintaining and reasoning about large programs easier. ### Running tests and benchmarks From b739eb9aaa3d99ef74f6e34154f68ac30a832c70 Mon Sep 17 00:00:00 2001 From: Danack Date: Tue, 23 May 2023 11:05:38 +0100 Subject: [PATCH 68/68] Move benchmarks to their own job, and only run on two versions of PHP. --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f24fc23..c2431df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,5 +50,33 @@ jobs: - name: "Run tests" run: vendor/bin/simple-phpunit + + + benchmark: + name: "Benchmark on PHP ${{ matrix.php-version }}" + + runs-on: 'ubuntu-latest' + + continue-on-error: true + + strategy: + matrix: + php-version: + - '7.4' + - '8.2' + + steps: + - name: "Checkout code" + uses: actions/checkout@v3 + + - name: "Install PHP with extensions" + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + php-version: ${{ matrix.php-version }} + ini-values: memory_limit=-1 + + - run: composer install + - name: Execute benchmarks run: vendor/bin/phpbench run \ No newline at end of file