From 3079ebdaf5d195724f694859abbe8ef71e6faa49 Mon Sep 17 00:00:00 2001 From: Enea Date: Sat, 19 Aug 2023 19:22:25 +0200 Subject: [PATCH 01/46] Introduce ProvidersCollection::class --- composer.json | 16 +- src/PhpFileProvider.php | 41 +++++ src/ProvidersCollection.php | 154 ++++++++++++++++++ .../config/autoload/config.global.php | 14 ++ .../fixtures/config/autoload/config.local.php | 12 ++ tests/_data/fixtures/config/test.global.php | 12 ++ tests/_data/fixtures/modules/ModuleStub1.php | 24 +++ tests/src/UnitTestCase.php | 39 ++++- tests/unit/AurynConfigTest.php | 11 +- tests/unit/PhpFileProviderTest.php | 38 +++++ .../ProvidersCollectionIntegrationTest.php | 88 ++++++++++ tests/unit/ProvidersCollectionTest.php | 51 ++++++ 12 files changed, 484 insertions(+), 16 deletions(-) create mode 100644 src/PhpFileProvider.php create mode 100644 src/ProvidersCollection.php create mode 100644 tests/_data/fixtures/config/autoload/config.global.php create mode 100644 tests/_data/fixtures/config/autoload/config.local.php create mode 100644 tests/_data/fixtures/config/test.global.php create mode 100644 tests/_data/fixtures/modules/ModuleStub1.php create mode 100644 tests/unit/PhpFileProviderTest.php create mode 100644 tests/unit/ProvidersCollectionIntegrationTest.php create mode 100644 tests/unit/ProvidersCollectionTest.php diff --git a/composer.json b/composer.json index 09f2684..84a96b9 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,9 @@ "php" : ">=7.4", "rdlowrey/auryn": "^1.4", "italystrap/config": "^2.2", - "ocramius/proxy-manager": "~2.11.0" + "ocramius/proxy-manager": "~2.11.0", + "brick/varexporter": "^0.3.8", + "webimpress/safe-writer": "^2.2" }, "require-dev": { "lucatume/wp-browser": "^3.0", @@ -37,7 +39,10 @@ "infection/codeception-adapter": "^0.4.1", "rector/rector": "^0.15.17", - "italystrap/debug": "^2.1" + "italystrap/debug": "dev-master", + "italystrap/finder": "dev-master", + "laminas/laminas-config-aggregator": "^1.9", + "crellbar/prophecy-extensions": "^1.1" }, "autoload": { "psr-4": { @@ -50,6 +55,7 @@ }, "autoload-dev": { "psr-4": { + "ItalyStrap\\Tests\\Modules\\": "tests/_data/fixtures/modules/", "ItalyStrap\\Tests\\": "tests/src/", "ItalyStrap\\Tests\\Unit\\": "tests/unit/" }, @@ -73,12 +79,10 @@ "@php ./vendor/bin/psalm --no-cache" ], "unit": [ - "@php vendor/bin/codecept run unit", - "@clean" + "@php vendor/bin/codecept run unit" ], "unit:debug": [ - "@php vendor/bin/codecept run unit --debug", - "@clean" + "@php vendor/bin/codecept run unit --debug" ], "infection": [ "@php vendor/bin/infection --threads=4" diff --git a/src/PhpFileProvider.php b/src/PhpFileProvider.php new file mode 100644 index 0000000..8c7371b --- /dev/null +++ b/src/PhpFileProvider.php @@ -0,0 +1,41 @@ +pattern = $pattern; + $this->finder = $finder; + } + + /** + * @return \Generator + */ + public function __invoke(): \Generator + { + $this->finder->names([$this->pattern]); + /** + * @var \SplFileInfo $file + */ + foreach ($this->finder as $file) { + /** @psalm-suppress UnresolvableInclude */ + yield include $file; + } + } +} diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php new file mode 100644 index 0000000..bff7165 --- /dev/null +++ b/src/ProvidersCollection.php @@ -0,0 +1,154 @@ + $providers + * @param string|null $cachedConfigFile + * @throws \ErrorException + */ + public function __construct( + Injector $injector, + ConfigInterface $config, + iterable $providers = [], + ?string $cachedConfigFile = null + ) { + $this->injector = $injector; + $this->config = $config; + + $collection = $this->loadCollectionFromProviders($providers); + $this->config->merge(...$collection); + $this->cacheConfig($this->config, $cachedConfigFile); + } + + /** + * @return ConfigInterface + */ + public function collection(): ConfigInterface + { + return $this->config; + } + + /** + * @param iterable $providers + * @return array + * @throws \ErrorException + */ + private function loadCollectionFromProviders(iterable $providers): array + { + $collection = []; + foreach ($providers as $provider) { + try { + $result = $this->injector->execute($provider); + } catch (InjectionException $e) { + throw new \ErrorException( + \sprintf( + 'An error occurred when executing %s: %s', + is_object($provider) ? get_class($provider) : gettype($provider), + $e->getMessage() + ), + 0, + 1, + __FILE__, + __LINE__, + $e + ); + } catch (\Throwable $e) { + throw new \ErrorException( + \sprintf( + 'An error occurred when executing %s: %s', + is_object($provider) ? get_class($provider) : gettype($provider), + $e->getMessage() + ), + 0, + 1, + __FILE__, + __LINE__, + $e + ); + } + + if ($result instanceof \Generator) { + foreach ($result as $item) { + $collection[] = (array)$item; + } + continue; + } + + $collection[] = (array)$result; + } + + return $collection; + } + + private function cacheConfig(ConfigInterface $config, ?string $cachedConfigFile): void + { + if (null === $cachedConfigFile) { + return; + } + +// if (empty($config[static::ENABLE_CACHE])) { +// return; +// } + + try { + $contents = sprintf( + <<<'EOT' +toArray(), + VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES + ) + ); + } catch (ExportException $e) { + throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); + } + +// $mode = $config[self::CACHE_FILEMODE] ?? null; + $this->writeCache($cachedConfigFile, $contents, null); + } + + private function writeCache(string $cachedConfigFile, ?string $contents, ?int $mode): void + { + try { + if ($mode !== null) { + FileWriter::writeFile($cachedConfigFile, $contents, $mode); + } else { + FileWriter::writeFile($cachedConfigFile, $contents); + } + } catch (FileWriterException $e) { + // ignore errors writing cache file + } + } +} diff --git a/tests/_data/fixtures/config/autoload/config.global.php b/tests/_data/fixtures/config/autoload/config.global.php new file mode 100644 index 0000000..32e19a6 --- /dev/null +++ b/tests/_data/fixtures/config/autoload/config.global.php @@ -0,0 +1,14 @@ + [ + ProvidersCollectionIntegrationTest::CONFIG_KEY_1 => 'global config', + ], + AurynConfig::SHARING => [ + ], +]; diff --git a/tests/_data/fixtures/config/autoload/config.local.php b/tests/_data/fixtures/config/autoload/config.local.php new file mode 100644 index 0000000..cac54b7 --- /dev/null +++ b/tests/_data/fixtures/config/autoload/config.local.php @@ -0,0 +1,12 @@ + [ + ProvidersCollectionIntegrationTest::CONFIG_KEY_1 => 'local config', + ], +]; diff --git a/tests/_data/fixtures/config/test.global.php b/tests/_data/fixtures/config/test.global.php new file mode 100644 index 0000000..3720f0e --- /dev/null +++ b/tests/_data/fixtures/config/test.global.php @@ -0,0 +1,12 @@ + [ + ProvidersCollectionIntegrationTest::CONFIG_KEY_3 => 'test.global.php', + ], +]; diff --git a/tests/_data/fixtures/modules/ModuleStub1.php b/tests/_data/fixtures/modules/ModuleStub1.php new file mode 100644 index 0000000..86a3356 --- /dev/null +++ b/tests/_data/fixtures/modules/ModuleStub1.php @@ -0,0 +1,24 @@ + [ + SharedAliasedInterface::class => SharedClass::class, + ], + AurynConfig::SHARING => [ + ], + AurynConfig::DEFINITIONS => [ + ], + ]; + } +} diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 49de625..8cbc1e3 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -5,30 +5,67 @@ namespace ItalyStrap\Tests; use Codeception\Test\Unit; +use ItalyStrap\Config\Config; +use ItalyStrap\Config\ConfigInterface; use ItalyStrap\Empress\Injector; +use ItalyStrap\Finder\FinderInterface; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophet; use UnitTester; class UnitTestCase extends Unit { + use ProphecyTrait; + protected UnitTester $tester; + protected ObjectProphecy $injector; - protected Prophet $prophet; protected function makeInjector(): Injector { return $this->injector->reveal(); } + protected ObjectProphecy $config; + + protected function makeConfig(): ConfigInterface + { + return $this->config->reveal(); + } + + protected ConfigInterface $configReal; + + protected function makeConfigReal(): ConfigInterface + { + return $this->configReal; + } + + protected ObjectProphecy $finder; + + protected function makeFinder(): FinderInterface + { + return $this->finder->reveal(); + } + + protected string $cachedConfigFile; + // phpcs:ignore -- Method from Codeception protected function _before(): void { $this->prophet = new Prophet(); $this->injector = $this->prophet->prophesize(Injector::class); + $this->configReal = new Config(); + $this->config = $this->prophet->prophesize(Config::class); + $this->finder = $this->prophet->prophesize(FinderInterface::class); + + $this->cachedConfigFile = codecept_output_dir('config-cache.php'); } // phpcs:ignore -- Method from Codeception protected function _after(): void { + $this->configReal = clone $this->configReal; $this->prophet->checkPredictions(); + unset($this->config); + \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); } } diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index 77ad747..1357ada 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -292,21 +292,14 @@ public function name(): string public function execute(AurynConfigInterface $application) { - $application->walk(self::SUBSCRIBERS, [ $this, 'method' ]); + $application->walk(self::SUBSCRIBERS, $this); } - public function method(string $class, $index_or_optionName, Injector $injector) + public function __invoke(string $class, $index_or_optionName, Injector $injector) { Assert::assertStringContainsString($class, 'ClassName', ''); $injector->share($class); $injector->make($class, []); - -// if ( empty( $config->get( $index_or_optionName, '' ) ) ) { -// return; -// } -// -// $event_manager = $injector->make( EventManager::class ); -// $event_manager->add_subscriber( $injector->share( $class )->make( $class ) ); } }); diff --git a/tests/unit/PhpFileProviderTest.php b/tests/unit/PhpFileProviderTest.php new file mode 100644 index 0000000..39ca227 --- /dev/null +++ b/tests/unit/PhpFileProviderTest.php @@ -0,0 +1,38 @@ +makeFinder()); + } + + public function testShouldBeInvokable() + { + $file = \codecept_data_dir('fixtures/config/autoload/config.global.php'); + + $this->finder->names(['pattern'])->will(function ($args): void { + Assert::assertSame('pattern', $args[0][0], 'Should be the same pattern'); + }); + + $this->finder->getIterator()->willReturn(new \ArrayIterator([ + $file, + ])); + + $expected = require $file; + + $sut = $this->makeInstance(); + foreach ($sut() as $actual) { + $this->assertEquals($expected, $actual, 'Should be expected file'); + break; // Only do on first iteration + } + } +} diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php new file mode 100644 index 0000000..d538ce5 --- /dev/null +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -0,0 +1,88 @@ +makeConfigReal(), + [ + new PhpFileProvider( + '/config/autoload/{{,*.}global,{,*.}local}.php', + (new FinderFactory()) + ->make() + ->in(codecept_data_dir('fixtures')) + ), + function (): array { + return [ + AurynConfig::ALIASES => [ + self::CONFIG_KEY_2 => 'array config', + ], + AurynConfig::SHARING => [ + ], + ]; + }, + function (): iterable { + yield [ + AurynConfig::ALIASES => [ + self::CONFIG_KEY_2 => 'iterable config', + ], + ]; + }, + ModuleStub1::class, + function (): array { + return require \codecept_data_dir('fixtures/config/test.global.php'); + }, + ] + ); + } + + public function testIntegration() + { + $sut = $this->makeInstance(); + + $this->assertSame( + 'local config', + $sut->collection()->get(\implode('.', [ + AurynConfig::ALIASES, + self::CONFIG_KEY_1, + ])) + ); + + $this->assertSame( + 'iterable config', + $sut->collection()->get(\implode('.', [ + AurynConfig::ALIASES, + self::CONFIG_KEY_2, + ])) + ); + + $this->assertSame( + 'test.global.php', + $sut->collection()->get(\implode('.', [ + AurynConfig::ALIASES, + self::CONFIG_KEY_3, + ])) + ); + } +} diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php new file mode 100644 index 0000000..8d8ba72 --- /dev/null +++ b/tests/unit/ProvidersCollectionTest.php @@ -0,0 +1,51 @@ +makeInjector(), + $this->makeConfig(), + [ + ], + $this->cachedConfigFile + ); + } + + public function testShouldBeInstantiable() + { + $this->assertInstanceOf(ConfigInterface::class, $this->makeConfig()); + + $this->config + ->merge(Argument::cetera()) + ->shouldBeCalledTimes(1); + + $this->config + ->toArray() + ->shouldBeCalledTimes(1) + ->willReturn([ + 'key' => 'value', + ]); + + $sut = $this->makeInstance(); + + $this->assertFileExists($this->cachedConfigFile); + $this->assertFileIsReadable($this->cachedConfigFile); + + $file = require $this->cachedConfigFile; + + $this->assertIsArray($file); + $this->assertArrayHasKey('key', $file); + } +} From e25a7798de38892da96c242eb9d9a3c43342bc61 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 20 Aug 2023 16:28:38 +0200 Subject: [PATCH 02/46] Introduce a sort of file cache --- src/ProvidersCollection.php | 53 ++++++++++--------- tests/src/UnitTestCase.php | 2 +- .../ProvidersCollectionIntegrationTest.php | 12 +++-- tests/unit/ProvidersCollectionTest.php | 10 ++++ 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index bff7165..50d7bfe 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -16,6 +16,23 @@ */ class ProvidersCollection { + public const ENABLE_CACHE = 'config_cache_enabled'; + + public const CACHE_FILEMODE = 'config_cache_filemode'; + + private const CACHE_TEMPLATE = <<<'EOT' +loadCollectionFromProviders($providers); $this->config->merge(...$collection); - $this->cacheConfig($this->config, $cachedConfigFile); + $this->cacheConfig($cachedConfigFile); } /** @@ -100,34 +117,23 @@ private function loadCollectionFromProviders(iterable $providers): array return $collection; } - private function cacheConfig(ConfigInterface $config, ?string $cachedConfigFile): void + private function cacheConfig(?string $cachedConfigFile): void { if (null === $cachedConfigFile) { return; } -// if (empty($config[static::ENABLE_CACHE])) { -// return; -// } + if (!$this->config->get(static::ENABLE_CACHE, false)) { + return; + } try { $contents = sprintf( - <<<'EOT' -toArray(), + $this->config->toArray(), VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES ) ); @@ -135,18 +141,13 @@ private function cacheConfig(ConfigInterface $config, ?string $cachedConfigFile) throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); } -// $mode = $config[self::CACHE_FILEMODE] ?? null; - $this->writeCache($cachedConfigFile, $contents, null); + $this->writeCache($cachedConfigFile, $contents, $this->config->get(self::CACHE_FILEMODE, 0666)); } - private function writeCache(string $cachedConfigFile, ?string $contents, ?int $mode): void + private function writeCache(string $cachedConfigFile, string $contents, int $mode): void { try { - if ($mode !== null) { - FileWriter::writeFile($cachedConfigFile, $contents, $mode); - } else { - FileWriter::writeFile($cachedConfigFile, $contents); - } + FileWriter::writeFile($cachedConfigFile, $contents, $mode); } catch (FileWriterException $e) { // ignore errors writing cache file } diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 8cbc1e3..c856ce7 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -66,6 +66,6 @@ protected function _after(): void { $this->configReal = clone $this->configReal; $this->prophet->checkPredictions(); unset($this->config); - \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); +// \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); } } diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index d538ce5..d59f84f 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -4,7 +4,6 @@ namespace ItalyStrap\Tests\Unit; -use Brick\VarExporter\VarExporter; use ItalyStrap\Empress\Injector; use ItalyStrap\Tests\Modules\ModuleStub1; use ItalyStrap\Empress\AurynConfig; @@ -12,7 +11,6 @@ use ItalyStrap\Empress\ProvidersCollection; use ItalyStrap\Finder\FinderFactory; use ItalyStrap\Tests\UnitTestCase; -use Prophecy\Argument; class ProvidersCollectionIntegrationTest extends UnitTestCase { @@ -53,7 +51,15 @@ function (): iterable { function (): array { return require \codecept_data_dir('fixtures/config/test.global.php'); }, - ] + function (): array { + return [ + 'cache_config_path' => $this->cachedConfigFile, + ]; + }, + ], +// (string)(new FinderFactory())->make() +// ->in(codecept_output_dir('')) +// ->firstFile('config-cache') ); } diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index 8d8ba72..e340845 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -38,6 +38,16 @@ public function testShouldBeInstantiable() 'key' => 'value', ]); + $this->config + ->get('config_cache_enabled', false) + ->willReturn(true); + + $this->config + ->get('config_cache_filemode', Argument::type('int')) + ->will(function ($args): int { + return (int)$args[1]; + }); + $sut = $this->makeInstance(); $this->assertFileExists($this->cachedConfigFile); From defe457bf3088ead82faca04a21866ed40c02411 Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 27 Sep 2023 13:02:19 +0200 Subject: [PATCH 03/46] improve implementation --- composer.json | 5 +- src/AurynConfig.php | 19 +++- src/ProvidersCache.php | 88 +++++++++++++++ src/ProvidersCacheInterface.php | 14 +++ src/ProvidersCollection.php | 100 +++++++----------- .../src/ConcreteNeedsSomeInterface.php | 23 ++++ tests/_data/fixtures/src/SomeConcrete.php | 13 +++ tests/_data/fixtures/src/SomeInterface.php | 10 ++ tests/src/UnitTestCase.php | 2 +- tests/unit/IntegrationTest.php | 40 +++++++ .../ProvidersCollectionIntegrationTest.php | 37 ++++++- tests/unit/ProvidersCollectionTest.php | 11 +- 12 files changed, 287 insertions(+), 75 deletions(-) create mode 100644 src/ProvidersCache.php create mode 100644 src/ProvidersCacheInterface.php create mode 100644 tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php create mode 100644 tests/_data/fixtures/src/SomeConcrete.php create mode 100644 tests/_data/fixtures/src/SomeInterface.php create mode 100644 tests/unit/IntegrationTest.php diff --git a/composer.json b/composer.json index 84a96b9..5e4d07b 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,10 @@ "autoload-dev": { "psr-4": { "ItalyStrap\\Tests\\Modules\\": "tests/_data/fixtures/modules/", - "ItalyStrap\\Tests\\": "tests/src/", + "ItalyStrap\\Tests\\": [ + "tests/src/", + "tests/_data/fixtures/src/" + ], "ItalyStrap\\Tests\\Unit\\": "tests/unit/" }, "files": [ diff --git a/src/AurynConfig.php b/src/AurynConfig.php index 1def70c..b38a83a 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -43,6 +43,7 @@ class AurynConfig implements AurynConfigInterface private array $extensions = []; private ProxyFactoryInterface $proxy_factory; + private array $extensionsClasses = []; /** * @param Config $dependencies @@ -71,11 +72,21 @@ public function resolve(): void $this->walk($key, $callback); } + foreach ($this->extensionsClasses as $extensionClass) { + $extension = $this->injector->share($extensionClass)->make($extensionClass); + $extension->execute($this); + } + foreach ($this->extensions as $extension) { $extension->execute($this); } } + public function extendFromClassName(string $className): void + { + $this->extensionsClasses[] = $className; + } + public function extend(Extension ...$extensions): void { foreach ($extensions as $extension) { @@ -115,13 +126,13 @@ protected function proxy(string $name, int $index): void } /** - * @param string $implementation - * @param string $interface + * @param string $alias + * @param string $typeHint * @throws ConfigException */ - protected function alias(string $implementation, string $interface): void + protected function alias(string $alias, string $typeHint): void { - $this->injector->alias($interface, $implementation); + $this->injector->alias($typeHint, $alias); } /** diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php new file mode 100644 index 0000000..cc44034 --- /dev/null +++ b/src/ProvidersCache.php @@ -0,0 +1,88 @@ +file = $file; + } + + public function read( + ConfigInterface $config + ): bool { + + $cachedConfigFile = (string)$config->get(static::CACHE_PATH, $this->file); + + if ( + !$cachedConfigFile + || !file_exists($cachedConfigFile) + ) { + return false; + } + + $config->merge((array)require $cachedConfigFile); + return true; + } + + public function write( + ConfigInterface $config + ): void { + $cachedConfigFile = (string)$config->get(static::CACHE_PATH, $this->file); + + if (!$cachedConfigFile) { + return; + } + + try { + $contents = sprintf( + static::CACHE_TEMPLATE, + static::class, + // Write an alternative to date('c') + (new DateTimeImmutable('now'))->format('c'), + VarExporter::export( + $config->toArray(), + VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES + ) + ); + } catch (ExportException $e) { + throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); + } + + $this->writeCache($cachedConfigFile, $contents, (int)$config->get(self::CACHE_FILEMODE, 0666)); + + return; + } + + private function writeCache(string $cachedConfigFile, string $contents, int $mode): void + { + try { + FileWriter::writeFile($cachedConfigFile, $contents, $mode); + } catch (FileWriterException $e) { + // ignore errors writing cache file + } + } +} diff --git a/src/ProvidersCacheInterface.php b/src/ProvidersCacheInterface.php new file mode 100644 index 0000000..a3dedf0 --- /dev/null +++ b/src/ProvidersCacheInterface.php @@ -0,0 +1,14 @@ + $providers - * @param string|null $cachedConfigFile + * @param ProvidersCacheInterface|null $cache * @throws \ErrorException */ public function __construct( Injector $injector, ConfigInterface $config, iterable $providers = [], - ?string $cachedConfigFile = null + ProvidersCacheInterface $cache = null ) { $this->injector = $injector; $this->config = $config; + $this->providers = $providers; + $this->cache = $cache ?? new ProvidersCache(); + } + + public function build(): void + { + if ($this->cache->read($this->config)) { + return; + } + + $result = []; + foreach ($this->loadCollectionFromProviders() as $subArray) { + foreach ($subArray as $key => $value) { + if (!array_key_exists($key, $result)) { + $result[$key] = []; + } + + if (!is_array($value)) { + $result[$key] = $value; + continue; + } - $collection = $this->loadCollectionFromProviders($providers); - $this->config->merge(...$collection); - $this->cacheConfig($cachedConfigFile); + $result[$key] = \array_merge($result[$key], $value); + } + } + + $this->config->merge($result); + + if ($this->config->get(ProvidersCacheInterface::ENABLE_CACHE, false)) { + $this->cache->write($this->config); + } } /** @@ -66,14 +81,13 @@ public function collection(): ConfigInterface } /** - * @param iterable $providers * @return array * @throws \ErrorException */ - private function loadCollectionFromProviders(iterable $providers): array + private function loadCollectionFromProviders(): array { $collection = []; - foreach ($providers as $provider) { + foreach ($this->providers as $provider) { try { $result = $this->injector->execute($provider); } catch (InjectionException $e) { @@ -116,40 +130,4 @@ private function loadCollectionFromProviders(iterable $providers): array return $collection; } - - private function cacheConfig(?string $cachedConfigFile): void - { - if (null === $cachedConfigFile) { - return; - } - - if (!$this->config->get(static::ENABLE_CACHE, false)) { - return; - } - - try { - $contents = sprintf( - static::CACHE_TEMPLATE, - static::class, - date('c'), - VarExporter::export( - $this->config->toArray(), - VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES - ) - ); - } catch (ExportException $e) { - throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); - } - - $this->writeCache($cachedConfigFile, $contents, $this->config->get(self::CACHE_FILEMODE, 0666)); - } - - private function writeCache(string $cachedConfigFile, string $contents, int $mode): void - { - try { - FileWriter::writeFile($cachedConfigFile, $contents, $mode); - } catch (FileWriterException $e) { - // ignore errors writing cache file - } - } } diff --git a/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php b/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php new file mode 100644 index 0000000..fb73972 --- /dev/null +++ b/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php @@ -0,0 +1,23 @@ +someInterface = $someInterface; + } + + public function someInterface(): SomeInterface + { + return $this->someInterface; + } +} diff --git a/tests/_data/fixtures/src/SomeConcrete.php b/tests/_data/fixtures/src/SomeConcrete.php new file mode 100644 index 0000000..02e61e4 --- /dev/null +++ b/tests/_data/fixtures/src/SomeConcrete.php @@ -0,0 +1,13 @@ +configReal = clone $this->configReal; $this->prophet->checkPredictions(); unset($this->config); -// \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); + \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); } } diff --git a/tests/unit/IntegrationTest.php b/tests/unit/IntegrationTest.php new file mode 100644 index 0000000..7fdd8b7 --- /dev/null +++ b/tests/unit/IntegrationTest.php @@ -0,0 +1,40 @@ + [ + SomeInterface::class => SomeConcrete::class, + ], + ] + ) + ); + + $aurynConfig->resolve(); + + $this->assertInstanceOf(SomeConcrete::class, $injector->make(SomeInterface::class)); + $this->assertInstanceOf(SomeConcrete::class, $injector->make(SomeConcrete::class)); + $object = $injector->make(ConcreteNeedsSomeInterface::class); + $actual = $object->someInterface(); + $this->assertInstanceOf(SomeConcrete::class, $actual); + $this->assertSame('SomeConcrete', $actual->render()); + } +} diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index d59f84f..7c9ff0f 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -47,25 +47,44 @@ function (): iterable { ], ]; }, + function (): array { + return [ + AurynConfig::ALIASES => [ + 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\GlobalDispatcher", + 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\SubscriberRegister", + 'ItalyStrap\View\ViewInterface' => "ItalyStrap\View\View", + 15 => 'value', + ], + ]; + }, + function (): array { + return [ + AurynConfig::ALIASES => [ + 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\DifferentDispatcher", + 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\DifferentRegister", + 'ItalyStrap\HTML\TagInterface' => "ItalyStrap\HTML\Tag", + 15 => 'newValue', + ], + ]; + }, ModuleStub1::class, function (): array { return require \codecept_data_dir('fixtures/config/test.global.php'); }, function (): array { return [ + 'config_cache_enabled' => true, 'cache_config_path' => $this->cachedConfigFile, ]; }, ], -// (string)(new FinderFactory())->make() -// ->in(codecept_output_dir('')) -// ->firstFile('config-cache') ); } public function testIntegration() { $sut = $this->makeInstance(); + $sut->build(); $this->assertSame( 'local config', @@ -90,5 +109,17 @@ public function testIntegration() self::CONFIG_KEY_3, ])) ); + + $this->assertFileExists($this->cachedConfigFile); + $this->assertFileIsReadable($this->cachedConfigFile); + + $file = require $this->cachedConfigFile; + $this->assertIsArray($file); + + \codecept_debug($sut->collection()->get(AurynConfig::ALIASES)); + /** + * \array_merge() will append the value if the kew is numeric + */ + $this->assertCount(10, $sut->collection()->get(AurynConfig::ALIASES), 'Should be 10'); } } diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index e340845..9afb83c 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -4,8 +4,6 @@ namespace ItalyStrap\Tests\Unit; -use ItalyStrap\Config\ConfigFactory; -use ItalyStrap\Config\ConfigInterface; use ItalyStrap\Empress\ProvidersCollection; use ItalyStrap\Tests\UnitTestCase; use Prophecy\Argument; @@ -18,14 +16,12 @@ private function makeInstance(): ProvidersCollection $this->makeInjector(), $this->makeConfig(), [ - ], - $this->cachedConfigFile + ] ); } public function testShouldBeInstantiable() { - $this->assertInstanceOf(ConfigInterface::class, $this->makeConfig()); $this->config ->merge(Argument::cetera()) @@ -48,7 +44,12 @@ public function testShouldBeInstantiable() return (int)$args[1]; }); + $this->config + ->get('cache_config_path', Argument::type('null')) + ->willReturn($this->cachedConfigFile); + $sut = $this->makeInstance(); + $sut->build(); $this->assertFileExists($this->cachedConfigFile); $this->assertFileIsReadable($this->cachedConfigFile); From 9245e6d7cd4b698ed182355aa96d20f805eb48a3 Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 26 Jul 2024 18:57:53 +0200 Subject: [PATCH 04/46] cs:fix --- namespace-bc-aliases.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/namespace-bc-aliases.php b/namespace-bc-aliases.php index a0ddbc4..4d80999 100644 --- a/namespace-bc-aliases.php +++ b/namespace-bc-aliases.php @@ -4,7 +4,8 @@ \class_alias( \ItalyStrap\Empress\AurynConfigInterface::class, - \ItalyStrap\Empress\AurynResolverInterface::class ); + \ItalyStrap\Empress\AurynResolverInterface::class +); \class_alias( \ItalyStrap\Empress\AurynConfig::class, From 8d13f7a2cb93d8ca07142a32d133b4b2b19dc0a7 Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 26 Jul 2024 19:01:40 +0200 Subject: [PATCH 05/46] updates Prophecy instance --- tests/src/UnitTestCase.php | 7 +++---- tests/unit/AurynConfigTest.php | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 8cbc1e3..1c355a7 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -52,11 +52,10 @@ protected function makeFinder(): FinderInterface // phpcs:ignore -- Method from Codeception protected function _before(): void { - $this->prophet = new Prophet(); - $this->injector = $this->prophet->prophesize(Injector::class); + $this->injector = $this->prophesize(Injector::class); $this->configReal = new Config(); - $this->config = $this->prophet->prophesize(Config::class); - $this->finder = $this->prophet->prophesize(FinderInterface::class); + $this->config = $this->prophesize(Config::class); + $this->finder = $this->prophesize(FinderInterface::class); $this->cachedConfigFile = codecept_output_dir('config-cache.php'); } diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index 1357ada..e4ac28f 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -181,7 +181,7 @@ public function testItShouldPrepare(): void Assert::assertInstanceOf(Injector::class, $injector, ''); }; - $test = $this->prophet; + $test = $this; $this->injector ->prepare(Argument::type('string'), Argument::any()) @@ -247,7 +247,7 @@ public function testItShouldExtendFakeClass(): void ] ); - $extension = $this->prophet->prophesize(Extension::class); + $extension = $this->prophesize(Extension::class); $extension->name()->willReturn('ExtensionName'); From b54cddd930339ccffcfe7473430f869c19e3bdea Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 26 Jul 2024 19:02:14 +0200 Subject: [PATCH 06/46] update composer.json --- composer.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5e4d07b..426a109 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "infection/infection": "^0.26.6", "infection/codeception-adapter": "^0.4.1", - "rector/rector": "^0.15.17", + "rector/rector": "^0.18.0", "italystrap/debug": "dev-master", "italystrap/finder": "dev-master", "laminas/laminas-config-aggregator": "^1.9", @@ -46,7 +46,10 @@ }, "autoload": { "psr-4": { - "ItalyStrap\\Empress\\": "src/" + "ItalyStrap\\Empress\\": [ + "src/", + "bridge/" + ] }, "files": [ "namespace-bc-aliases.php", From d49bd701c8c6be71ff354b9caf13c9822783baf5 Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 26 Jul 2024 19:31:16 +0200 Subject: [PATCH 07/46] cs:fix --- src/AurynConfig.php | 8 ++++++++ src/ProvidersCache.php | 18 +++++++++++++----- src/ProvidersCollection.php | 7 +++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/AurynConfig.php b/src/AurynConfig.php index b38a83a..be6f87e 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -43,6 +43,10 @@ class AurynConfig implements AurynConfigInterface private array $extensions = []; private ProxyFactoryInterface $proxy_factory; + + /** + * @var array + */ private array $extensionsClasses = []; /** @@ -73,6 +77,7 @@ public function resolve(): void } foreach ($this->extensionsClasses as $extensionClass) { + /** @var Extension $extension */ $extension = $this->injector->share($extensionClass)->make($extensionClass); $extension->execute($this); } @@ -82,6 +87,9 @@ public function resolve(): void } } + /** + * @param class-string $className + */ public function extendFromClassName(string $className): void { $this->extensionsClasses[] = $className; diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index cc44034..d2153ce 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -7,10 +7,14 @@ use Brick\VarExporter\ExportException; use Brick\VarExporter\VarExporter; use ItalyStrap\Config\ConfigInterface; +use phpDocumentor\Reflection\Types\Self_; use Safe\DateTimeImmutable; use Webimpress\SafeWriter\Exception\ExceptionInterface as FileWriterException; use Webimpress\SafeWriter\FileWriter; +/** + * @psalm-api + */ class ProvidersCache implements ProvidersCacheInterface { private const CACHE_TEMPLATE = <<<'EOT' @@ -35,15 +39,19 @@ public function read( ConfigInterface $config ): bool { - $cachedConfigFile = (string)$config->get(static::CACHE_PATH, $this->file); + $cachedConfigFile = (string)$config->get(self::CACHE_PATH, $this->file); if ( - !$cachedConfigFile + $cachedConfigFile === '' || !file_exists($cachedConfigFile) + || !is_readable($cachedConfigFile) ) { return false; } + /** + * @psalm-suppress UnresolvableInclude + */ $config->merge((array)require $cachedConfigFile); return true; } @@ -51,15 +59,15 @@ public function read( public function write( ConfigInterface $config ): void { - $cachedConfigFile = (string)$config->get(static::CACHE_PATH, $this->file); + $cachedConfigFile = (string)$config->get(self::CACHE_PATH, $this->file); - if (!$cachedConfigFile) { + if ($cachedConfigFile === '') { return; } try { $contents = sprintf( - static::CACHE_TEMPLATE, + self::CACHE_TEMPLATE, static::class, // Write an alternative to date('c') (new DateTimeImmutable('now'))->format('c'), diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index 6d2a69c..e854c52 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -18,7 +18,7 @@ class ProvidersCollection { private ConfigInterface $config; private Injector $injector; - private ProvidersCacheInterface $cache; + private ProvidersCache $cache; /** * @var array|callable[]|iterable|string[] */ @@ -28,14 +28,13 @@ class ProvidersCollection * @param Injector $injector * @param ConfigInterface $config * @param iterable $providers - * @param ProvidersCacheInterface|null $cache - * @throws \ErrorException + * @param ProvidersCache|null $cache */ public function __construct( Injector $injector, ConfigInterface $config, iterable $providers = [], - ProvidersCacheInterface $cache = null + ProvidersCache $cache = null ) { $this->injector = $injector; $this->config = $config; From 5982105721bb252201a851b912783cffcc6b73ea Mon Sep 17 00:00:00 2001 From: Enea Date: Sat, 27 Jul 2024 17:11:16 +0200 Subject: [PATCH 08/46] try new WF for PHP tests --- .github/workflows/test.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bea3074..c91150a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,26 +8,37 @@ on: jobs: tests: - name: Test on PHP ${{ matrix.php_versions }} + name: Test on PHP ${{ matrix.php }} runs-on: ubuntu-latest - continue-on-error: ${{ matrix.php_versions == '8.1' }} + continue-on-error: ${{ matrix.php == '8.1' }} if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" strategy: matrix: - php_versions: ['7.4', '8.0', '8.1'] + php: + - '7.4' + - '8.0' + dependencies: + - "lowest" + - "highest" + include: + - php: '8.1' + composer-options: "--ignore-platform-reqs" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php_versions }} + php-version: ${{ matrix.php }} coverage: xdebug2 - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + composer-options: ${{ matrix.composer-options }} - name: Run test suite run: vendor/bin/codecept run unit --coverage-text From f78c79ae15df5e09de200a032ea5bdc320723264 Mon Sep 17 00:00:00 2001 From: Enea Date: Sat, 27 Jul 2024 17:22:49 +0200 Subject: [PATCH 09/46] try new WF for PHP tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c91150a..15e0d0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: name: Test on PHP ${{ matrix.php }} runs-on: ubuntu-latest - continue-on-error: ${{ matrix.php == '8.1' }} + continue-on-error: ${{ contains('8.1,8.2', matrix.php) }} if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" strategy: From 3071e6932638011839749647a9d6cbc6dbc3b48e Mon Sep 17 00:00:00 2001 From: Enea Date: Sat, 27 Jul 2024 17:36:19 +0200 Subject: [PATCH 10/46] try 2 version for ocramius/proxy-manager --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 426a109..9b93374 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "php" : ">=7.4", "rdlowrey/auryn": "^1.4", "italystrap/config": "^2.2", - "ocramius/proxy-manager": "~2.11.0", + "ocramius/proxy-manager": "~2.11.0 || ~2.14.1", "brick/varexporter": "^0.3.8", "webimpress/safe-writer": "^2.2" }, From ca43de461b69d29d308154b0ca81e986158fa203 Mon Sep 17 00:00:00 2001 From: Enea Date: Sat, 27 Jul 2024 18:05:40 +0200 Subject: [PATCH 11/46] Rector --- composer.json | 5 +++-- src/ProvidersCache.php | 1 - src/ProvidersCollection.php | 16 ++-------------- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 9b93374..bdb0908 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "role": "Developer" } ], - "minimum-stability": "stable", + "minimum-stability": "dev", + "prefer-stable": true, "require": { "php" : ">=7.4", "rdlowrey/auryn": "^1.4", @@ -38,7 +39,7 @@ "infection/infection": "^0.26.6", "infection/codeception-adapter": "^0.4.1", - "rector/rector": "^0.18.0", + "rector/rector": "^1.2", "italystrap/debug": "dev-master", "italystrap/finder": "dev-master", "laminas/laminas-config-aggregator": "^1.9", diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index d2153ce..4eea7fd 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -7,7 +7,6 @@ use Brick\VarExporter\ExportException; use Brick\VarExporter\VarExporter; use ItalyStrap\Config\ConfigInterface; -use phpDocumentor\Reflection\Types\Self_; use Safe\DateTimeImmutable; use Webimpress\SafeWriter\Exception\ExceptionInterface as FileWriterException; use Webimpress\SafeWriter\FileWriter; diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index e854c52..8bf407f 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -19,6 +19,7 @@ class ProvidersCollection private ConfigInterface $config; private Injector $injector; private ProvidersCache $cache; + /** * @var array|callable[]|iterable|string[] */ @@ -89,20 +90,7 @@ private function loadCollectionFromProviders(): array foreach ($this->providers as $provider) { try { $result = $this->injector->execute($provider); - } catch (InjectionException $e) { - throw new \ErrorException( - \sprintf( - 'An error occurred when executing %s: %s', - is_object($provider) ? get_class($provider) : gettype($provider), - $e->getMessage() - ), - 0, - 1, - __FILE__, - __LINE__, - $e - ); - } catch (\Throwable $e) { + } catch (InjectionException|\Throwable $e) { throw new \ErrorException( \sprintf( 'An error occurred when executing %s: %s', From f4df09f4aab6a5f3216838d3b87ec7b8a5337736 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 28 Jul 2024 17:48:51 +0200 Subject: [PATCH 12/46] Psalm --- composer.json | 5 ++ src/ProvidersCache.php | 2 - src/ProvidersCollection.php | 86 +++++++++---------- tests/src/UnitTestCase.php | 1 - .../ProvidersCollectionIntegrationTest.php | 3 +- tests/unit/ProvidersCollectionTest.php | 1 + 6 files changed, 47 insertions(+), 51 deletions(-) diff --git a/composer.json b/composer.json index bdb0908..09d1a6d 100644 --- a/composer.json +++ b/composer.json @@ -108,6 +108,11 @@ ], "clean": [ "@php vendor/bin/codecept clean" + ], + "qa": [ + "@cs", + "@psalm", + "@unit" ] }, "support" : { diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index 4eea7fd..cc09718 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -80,8 +80,6 @@ public function write( } $this->writeCache($cachedConfigFile, $contents, (int)$config->get(self::CACHE_FILEMODE, 0666)); - - return; } private function writeCache(string $cachedConfigFile, string $contents, int $mode): void diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index 8bf407f..20f3a72 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -5,11 +5,7 @@ namespace ItalyStrap\Empress; use Auryn\InjectionException; -use Brick\VarExporter\ExportException; -use Brick\VarExporter\VarExporter; use ItalyStrap\Config\ConfigInterface; -use Webimpress\SafeWriter\Exception\ExceptionInterface as FileWriterException; -use Webimpress\SafeWriter\FileWriter; /** * @psalm-api @@ -19,28 +15,18 @@ class ProvidersCollection private ConfigInterface $config; private Injector $injector; private ProvidersCache $cache; - - /** - * @var array|callable[]|iterable|string[] - */ private iterable $providers; - /** - * @param Injector $injector - * @param ConfigInterface $config - * @param iterable $providers - * @param ProvidersCache|null $cache - */ public function __construct( Injector $injector, ConfigInterface $config, - iterable $providers = [], - ProvidersCache $cache = null + ProvidersCache $cache = null, + iterable $providers = [] ) { $this->injector = $injector; $this->config = $config; - $this->providers = $providers; $this->cache = $cache ?? new ProvidersCache(); + $this->providers = $providers; } public function build(): void @@ -50,47 +36,42 @@ public function build(): void } $result = []; + /** @var array $subArray */ foreach ($this->loadCollectionFromProviders() as $subArray) { - foreach ($subArray as $key => $value) { - if (!array_key_exists($key, $result)) { - $result[$key] = []; - } - - if (!is_array($value)) { - $result[$key] = $value; - continue; - } - - $result[$key] = \array_merge($result[$key], $value); - } + $this->processCollections($subArray, $result); } $this->config->merge($result); - if ($this->config->get(ProvidersCacheInterface::ENABLE_CACHE, false)) { + if ((bool)$this->config->get(ProvidersCacheInterface::ENABLE_CACHE, false)) { $this->cache->write($this->config); } } - /** - * @return ConfigInterface - */ - public function collection(): ConfigInterface + private function processCollections(array $subArray, array &$result): void { - return $this->config; + foreach ($subArray as $key => $value) { + if (!array_key_exists($key, $result)) { + $result[$key] = []; + } + + if (!is_array($value)) { + /** @psalm-suppress MixedAssignment */ + $result[$key] = $value; + continue; + } + + $result[$key] = \array_merge((array)$result[$key], $value); + } } - /** - * @return array - * @throws \ErrorException - */ - private function loadCollectionFromProviders(): array + private function loadCollectionFromProviders(): \Generator { - $collection = []; + /** @var object|array|class-string $provider */ foreach ($this->providers as $provider) { try { $result = $this->injector->execute($provider); - } catch (InjectionException|\Throwable $e) { + } catch (InjectionException | \Throwable $e) { throw new \ErrorException( \sprintf( 'An error occurred when executing %s: %s', @@ -106,15 +87,26 @@ private function loadCollectionFromProviders(): array } if ($result instanceof \Generator) { - foreach ($result as $item) { - $collection[] = (array)$item; - } + yield from $result; continue; } - $collection[] = (array)$result; + if (!\is_array($result)) { + throw new \RuntimeException( + \sprintf( + 'The provider %s must return an array or a Generator, %s given', + is_object($provider) ? get_class($provider) : gettype($provider), + \gettype($result) + ) + ); + } + + yield $result; } + } - return $collection; + public function collection(): ConfigInterface + { + return $this->config; } } diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 1c355a7..3f8fa7d 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -11,7 +11,6 @@ use ItalyStrap\Finder\FinderInterface; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Prophecy\Prophet; use UnitTester; class UnitTestCase extends Unit diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index 7c9ff0f..a8cdaad 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -24,6 +24,7 @@ private function makeInstance(): ProvidersCollection return new ProvidersCollection( new Injector(), $this->makeConfigReal(), + null, [ new PhpFileProvider( '/config/autoload/{{,*.}global,{,*.}local}.php', @@ -68,6 +69,7 @@ function (): array { ]; }, ModuleStub1::class, + [ModuleStub1::class, '__invoke'], function (): array { return require \codecept_data_dir('fixtures/config/test.global.php'); }, @@ -116,7 +118,6 @@ public function testIntegration() $file = require $this->cachedConfigFile; $this->assertIsArray($file); - \codecept_debug($sut->collection()->get(AurynConfig::ALIASES)); /** * \array_merge() will append the value if the kew is numeric */ diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index 9afb83c..7b4f26c 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -15,6 +15,7 @@ private function makeInstance(): ProvidersCollection return new ProvidersCollection( $this->makeInjector(), $this->makeConfig(), + null, [ ] ); From a765c3bc576f0e0453481364eae6b3fa0b8bb709 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 28 Jul 2024 20:35:29 +0200 Subject: [PATCH 13/46] Psalm and Infection --- infection.json.dist | 8 ++++- src/AurynConfig.php | 41 +++++++++++---------- src/AurynConfigInterface.php | 4 +-- src/Extension.php | 6 ---- src/ProxyFactory.php | 3 ++ tests/_data/fixtures/src/SomeExtension.php | 21 +++++++++++ tests/src/UnitTestCase.php | 4 +++ tests/unit/AurynConfigIntegrationTest.php | 42 ++++++++++++++++++++++ tests/unit/AurynConfigTest.php | 30 ++++++++++++++-- tests/unit/IntegrationTest.php | 40 --------------------- 10 files changed, 130 insertions(+), 69 deletions(-) create mode 100644 tests/_data/fixtures/src/SomeExtension.php create mode 100644 tests/unit/AurynConfigIntegrationTest.php delete mode 100644 tests/unit/IntegrationTest.php diff --git a/infection.json.dist b/infection.json.dist index 7c632e8..31b576c 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -2,13 +2,19 @@ "source": { "directories": [ "src" + ], + "excludes": [ + "/Provider/" ] }, "timeout": 10, + "logs": { + "text": "php://stdout" + }, "mutators": { "@default": true }, - "tmpDir": "\/tests\/_output", + "tmpDir": "tests\/_output", "testFramework": "codeception", "testFrameworkOptions": "--skip=wpunit --skip=acceptance --skip=functional" } \ No newline at end of file diff --git a/src/AurynConfig.php b/src/AurynConfig.php index be6f87e..5892ed5 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -37,22 +37,18 @@ class AurynConfig implements AurynConfigInterface private Config $dependencies; + private ProxyFactoryInterface $proxy_factory; + /** - * @var array + * @var array */ private array $extensions = []; - private ProxyFactoryInterface $proxy_factory; - /** * @var array */ private array $extensionsClasses = []; - /** - * @param Config $dependencies - * @param Injector $injector - */ public function __construct( Injector $injector, Config $dependencies, @@ -79,7 +75,7 @@ public function resolve(): void foreach ($this->extensionsClasses as $extensionClass) { /** @var Extension $extension */ $extension = $this->injector->share($extensionClass)->make($extensionClass); - $extension->execute($this); + $this->extensions[$extension->name()] = $extension; } foreach ($this->extensions as $extension) { @@ -87,18 +83,27 @@ public function resolve(): void } } - /** - * @param class-string $className - */ - public function extendFromClassName(string $className): void - { - $this->extensionsClasses[] = $className; - } - - public function extend(Extension ...$extensions): void + public function extend(...$extensions): void { foreach ($extensions as $extension) { - $this->extensions[$extension->name()] = $extension; + if ( + \is_string($extension) + && \class_exists($extension) + && \is_subclass_of($extension, Extension::class) + ) { + $this->extensionsClasses[] = $extension; + continue; + } + + if ($extension instanceof Extension) { + $this->extensions[$extension->name()] = $extension; + continue; + } + + throw new \InvalidArgumentException(\sprintf( + 'Invalid extension type, given: %s', + \gettype($extension) + )); } } diff --git a/src/AurynConfigInterface.php b/src/AurynConfigInterface.php index a027536..739d8f0 100644 --- a/src/AurynConfigInterface.php +++ b/src/AurynConfigInterface.php @@ -15,10 +15,10 @@ interface AurynConfigInterface public function resolve(); /** - * @param Extension ...$extensions + * @param class-string|Extension ...$extensions * @return void */ - public function extend(Extension ...$extensions); + public function extend(...$extensions); /** * @param string $key diff --git a/src/Extension.php b/src/Extension.php index bd90282..1646b07 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -4,12 +4,6 @@ namespace ItalyStrap\Empress; -use ItalyStrap\Config\ConfigInterface; - -/** - * Interface Extension - * @package ItalyStrap\Empress - */ interface Extension { /** diff --git a/src/ProxyFactory.php b/src/ProxyFactory.php index 49137c8..653acc4 100644 --- a/src/ProxyFactory.php +++ b/src/ProxyFactory.php @@ -8,6 +8,9 @@ use ProxyManager\Factory\LazyLoadingValueHolderFactory; use ProxyManager\Proxy\VirtualProxyInterface; +/** + * @infection-ignore-all + */ class ProxyFactory implements ProxyFactoryInterface { /** diff --git a/tests/_data/fixtures/src/SomeExtension.php b/tests/_data/fixtures/src/SomeExtension.php new file mode 100644 index 0000000..d42d14d --- /dev/null +++ b/tests/_data/fixtures/src/SomeExtension.php @@ -0,0 +1,21 @@ +name(); + } +} diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 3f8fa7d..426a40e 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -19,6 +19,8 @@ class UnitTestCase extends Unit protected UnitTester $tester; + protected ?Injector $realInjector; + protected ObjectProphecy $injector; protected function makeInjector(): Injector @@ -51,6 +53,7 @@ protected function makeFinder(): FinderInterface // phpcs:ignore -- Method from Codeception protected function _before(): void { + $this->realInjector = new Injector(); $this->injector = $this->prophesize(Injector::class); $this->configReal = new Config(); $this->config = $this->prophesize(Config::class); @@ -64,6 +67,7 @@ protected function _after(): void { $this->configReal = clone $this->configReal; $this->prophet->checkPredictions(); unset($this->config); + unset($this->realInjector); \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); } } diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php new file mode 100644 index 0000000..3ca631f --- /dev/null +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -0,0 +1,42 @@ +realInjector, ConfigFactory::make($config), new ProxyFactory()); + } + + public function testItShouldAlias(): void + { + $aurynConfig = $this->makeInstance( + [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ] + ); + + $aurynConfig->resolve(); + + $this->assertInstanceOf(SomeConcrete::class, $this->realInjector->make(SomeInterface::class)); + $this->assertInstanceOf(SomeConcrete::class, $this->realInjector->make(SomeConcrete::class)); + $object = $this->realInjector->make(ConcreteNeedsSomeInterface::class); + $actual = $object->someInterface(); + $this->assertInstanceOf(SomeConcrete::class, $actual); + $this->assertSame('SomeConcrete', $actual->render()); + } +} diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index e4ac28f..af6d6b4 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -9,18 +9,29 @@ use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\Extension; +use ItalyStrap\Empress\ProxyFactory; +use ItalyStrap\Tests\SomeExtension; use ItalyStrap\Tests\UnitTestCase; use PHPUnit\Framework\Assert; use Prophecy\Argument; class AurynConfigTest extends UnitTestCase { + private ?ProxyFactory $proxyFactory = null; + protected function makeInstance(array $config = []): AurynConfig { - return new AurynConfig($this->makeInjector(), ConfigFactory::make($config)); + return new AurynConfig($this->makeInjector(), ConfigFactory::make($config), $this->proxyFactory); + } + + public function testItShouldBeInstantiable(): void + { + $this->proxyFactory = new ProxyFactory(); + $sut = $this->makeInstance(); + $this->assertInstanceOf(AurynConfig::class, $sut); } - public function shareProvider() + public function shareProvider(): iterable { return [ 'ClassName' => [ @@ -306,6 +317,21 @@ public function __invoke(string $class, $index_or_optionName, Injector $injector $sut->resolve(); } + public function testItShouldExtendClassString(): void + { + $sut = new AurynConfig(new Injector(), ConfigFactory::make()); + $sut->extend(SomeExtension::class); + $this->expectOutputString(SomeExtension::class); + $sut->resolve(); + } + + public function testItShouldNotExtend(): void + { + $sut = new AurynConfig(new Injector(), ConfigFactory::make()); + $this->expectException(\InvalidArgumentException::class); + $sut->extend('SomeGenericClass'); + } + public function testOldClassNameShouldBeAliasedCorrectly(): void { /** diff --git a/tests/unit/IntegrationTest.php b/tests/unit/IntegrationTest.php deleted file mode 100644 index 7fdd8b7..0000000 --- a/tests/unit/IntegrationTest.php +++ /dev/null @@ -1,40 +0,0 @@ - [ - SomeInterface::class => SomeConcrete::class, - ], - ] - ) - ); - - $aurynConfig->resolve(); - - $this->assertInstanceOf(SomeConcrete::class, $injector->make(SomeInterface::class)); - $this->assertInstanceOf(SomeConcrete::class, $injector->make(SomeConcrete::class)); - $object = $injector->make(ConcreteNeedsSomeInterface::class); - $actual = $object->someInterface(); - $this->assertInstanceOf(SomeConcrete::class, $actual); - $this->assertSame('SomeConcrete', $actual->render()); - } -} From 3911dc64e3aabb939aa8c72d766e5c8e40bd1b8c Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 29 Jul 2024 11:39:29 +0200 Subject: [PATCH 14/46] more tests --- codeception.dist.yml | 8 - example.php | 399 ++++++++++------------ index.php | 43 --- phpcs.xml | 1 + tests/src/UnitTestCase.php | 21 +- tests/unit/AurynConfigIntegrationTest.php | 50 ++- tests/unit/AurynConfigTest.php | 73 ++-- 7 files changed, 296 insertions(+), 299 deletions(-) delete mode 100644 index.php diff --git a/codeception.dist.yml b/codeception.dist.yml index af68839..d5b4fbf 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -8,14 +8,6 @@ actor_suffix: Tester extensions: enabled: - Codeception\Extension\RunFailed - commands: - - Codeception\Command\GenerateWPUnit - - Codeception\Command\GenerateWPRestApi - - Codeception\Command\GenerateWPRestController - - Codeception\Command\GenerateWPRestPostTypeController - - Codeception\Command\GenerateWPAjax - - Codeception\Command\GenerateWPCanonical - - Codeception\Command\GenerateWPXMLRPC params: - .env coverage: diff --git a/example.php b/example.php index 1ce893d..ac63a7f 100644 --- a/example.php +++ b/example.php @@ -1,4 +1,5 @@ class = $class; - $this->config = $config; - $this->param = $param; - } - - /** - * @return ConfigInterface - */ - public function getConfig(): ConfigInterface { - return $this->config; - } - - /** - * @return stdClass - */ - public function getClass(): stdClass { - return $this->class; - } - - public function execute( string $text ) { - return $text; - } +class Example +{ + private stdClass $class; + private ConfigInterface $config; + private string $param; + + public function __construct(stdClass $class, ConfigInterface $config, string $param) + { + $this->class = $class; + $this->config = $config; + $this->param = $param; + } + + public function getConfig(): ConfigInterface + { + return $this->config; + } + + public function getClass(): stdClass + { + return $this->class; + } + + public function execute(string $text): string + { + return $text; + } } /** * The better way to add keys to the array configuration is to use the - * AurynResolver:: + * AurynConfig:: * * Keys available are: * - * AurynResolver::PROXY = 'proxies'; - * AurynResolver::SHARING = 'sharing'; - * AurynResolver::ALIASES = 'aliases'; - * AurynResolver::DEFINITIONS = 'definitions'; - * AurynResolver::DEFINE_PARAM = 'define_param'; - * AurynResolver::DELEGATIONS = 'delegations'; - * AurynResolver::PREPARATIONS = 'preparations'; + * AurynConfig::PROXY = 'proxies'; + * AurynConfig::SHARING = 'sharing'; + * AurynConfig::ALIASES = 'aliases'; + * AurynConfig::DEFINITIONS = 'definitions'; + * AurynConfig::DEFINE_PARAM = 'define_param'; + * AurynConfig::DELEGATIONS = 'delegations'; + * AurynConfig::PREPARATIONS = 'preparations'; */ /** - * First off all we need a configuration + * First of all we need a configuration */ $config = [ - /** - * Example: - * class MyCLass( ConfigInterface $config ) {} - * You alias a `ConfigInterface::class` to `Config::class` - * $injector->make(MyCLass::class); will be injected with a Config object - * @see [Type-Hint Aliasing](https://github.com/rdlowrey/auryn#type-hint-aliasing) - */ - AurynConfig::ALIASES => [ - ConfigInterface::class => Config::class, - ], - - /** - * Example: - * class MyCLass( ConfigInterface $global_config, \stdClass $class ) {} - * class MyOtherCLass( ConfigInterface $global_config, \stdClass $class ) {} - * A new Config instance will be shared, think of it like a singleton but more better and OOP oriented (You can mock it ;-)) - * The same instance of Config will be injected to MyCLass and MyOtherCLass - * $injector->make(MyCLass::class); // Will have $global_config - * $injector->make(MyOtherCLass::class); // Will have $global_config - * @see [Instance Sharing](https://github.com/rdlowrey/auryn#instance-sharing) - */ - AurynConfig::SHARING => [ - stdClass::class, - ConfigInterface::class, - ], - - /** - * This is the new feature for the Auryn\Injector implemented in the bridge adapter - * You usually need a lazy value holder in cases where the following applies: - * * your object takes a lot of time and memory to be initialized (with all dependencies) - * * your object is not always used, and the instantiation overhead is avoidable - * - * Example: - * class HeavyComplexObject( ...HeavyDependency ){}; // Declared somewhere - * $object = $injector->make(HeavyComplexObject::class); - * add_{filter|action}( 'event_name', [ $object, 'doSomeStuff' ] ); - * - * You can proxies the `HeavyComplexObject::class` dependency - * $injector->proxy(HeavyDependency::class); - * Or the `HeavyComplexObject::class` - * $injector->proxy(HeavyComplexObject::class); - * - * It depends on your business logic. - * - * Let see for example if you have proxies the HeavyComplexObject::class - * - * $proxy = $injector->make(HeavyComplexObject::class); - * - * Now $proxy will be the lazy version of the object (as a proxy) and when the event call it - * add_{filter|action}( 'event_name', [ $proxy, 'doSomeStuff' ] ); - * ::doSomeStuff() will just work as before. - * - * @see [Lazy Loading Value Holder Proxy](https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md) - */ - AurynConfig::PROXY => [ - Config::class, - ], - - /** - * Define global parameter - * class SomeCLass(string $text) {} - * class SomeOtherCLass(string $text) {} - * $injector->make(SomeCLass::class); - * $injector->make(SomeOtherCLass::class); - * Now the `$text` param will be decorated with 'Some Text' - * @see [Global Parameter Definitions](https://github.com/rdlowrey/auryn#global-parameter-definitions) - */ - AurynConfig::DEFINE_PARAM => [ - 'text' => 'Some Text' - ], - - /** - * Definition for class specific - * Example: - * class Example(int $param) {} - * Now the `$param` will be decorated with 42 - * class OtherExample(int $param) {} - * This will not be decorated with 42 because you have defined only the Example::class parameter - * @see [Injection Definitions](https://github.com/rdlowrey/auryn#injection-definitions) - */ - AurynConfig::DEFINITIONS => [ - Example::class => [ - ':param' => 42, - ] - ], - - /** - * As soon as the instance is created you can prepare some action before use the new created instance - * This is the same as: - * $class = new \stdClass; - * $class->param = 42; - * echo $class->param; - * @see [Prepares and Setter Injection](https://github.com/rdlowrey/auryn#prepares-and-setter-injection) - */ - AurynConfig::PREPARATIONS => [ - stdClass::class => function ( stdClass $class, Injector $injector ) { - $class->param = 42; - }, - ], - - /** - * You can delegate the instantiation of an object to a some kind of callable factory - * This will be always used to get the instance of a class. - * @see [Instantiation Delegates](https://github.com/rdlowrey/auryn#instantiation-delegates) - */ - AurynConfig::DELEGATIONS => [ - ConfigInterface::class => [ ConfigFactory::class, 'make'] - ], + /** + * Example: + * class MyCLass(ConfigInterface $config) {} + * You alias a `ConfigInterface::class` to `Config::class` + * $injector->make(MyCLass::class); will be injected with a Config object + * @see [Type-Hint Aliasing](https://github.com/rdlowrey/auryn#type-hint-aliasing) + */ + AurynConfig::ALIASES => [ + ConfigInterface::class => Config::class, + ], + + /** + * Example: + * class MyCLass( ConfigInterface $global_config, \stdClass $class ) {} + * class MyOtherCLass( ConfigInterface $global_config, \stdClass $class ) {} + * A new Config instance will be shared, think of it like a singleton but more better and OOP oriented (You can mock it ;-)) + * The same instance of Config will be injected to MyCLass and MyOtherCLass + * $injector->make(MyCLass::class); // Will have $global_config + * $injector->make(MyOtherCLass::class); // Will have $global_config + * @see [Instance Sharing](https://github.com/rdlowrey/auryn#instance-sharing) + */ + AurynConfig::SHARING => [ + stdClass::class, + ConfigInterface::class, + ], + + /** + * This is the new feature for the Auryn\Injector implemented in the bridge adapter + * You usually need a lazy value holder in cases where the following applies: + * * your object takes a lot of time and memory to be initialized (with all dependencies) + * * your object is not always used, and the instantiation overhead is avoidable + * + * Example: + * class HeavyComplexObject( ...HeavyDependency ){}; // Declared somewhere + * $object = $injector->make(HeavyComplexObject::class); + * add_{filter|action}( 'event_name', [ $object, 'doSomeStuff' ] ); + * + * You can proxies the `HeavyComplexObject::class` dependency + * $injector->proxy(HeavyDependency::class); + * Or the `HeavyComplexObject::class` + * $injector->proxy(HeavyComplexObject::class); + * + * It depends on your business logic. + * + * Let see for example if you have proxies the HeavyComplexObject::class + * + * $proxy = $injector->make(HeavyComplexObject::class); + * + * Now $proxy will be the lazy version of the object (as a proxy) and when the event call it + * add_{filter|action}( 'event_name', [ $proxy, 'doSomeStuff' ] ); + * ::doSomeStuff() will just work as before. + * + * @see [Lazy Loading Value Holder Proxy](https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md) + */ + AurynConfig::PROXY => [ + Config::class, + ], + + /** + * Define global parameter + * class SomeCLass(string $text) {} + * class SomeOtherCLass(string $text) {} + * $injector->make(SomeCLass::class); + * $injector->make(SomeOtherCLass::class); + * Now the `$text` param will be decorated with 'Some Text' + * @see [Global Parameter Definitions](https://github.com/rdlowrey/auryn#global-parameter-definitions) + */ + AurynConfig::DEFINE_PARAM => [ + 'text' => 'Some Text' + ], + + /** + * Definition for class specific + * Example: + * class Example(int $param) {} + * Now the `$param` will be decorated with 42 + * class OtherExample(int $param) {} + * This will not be decorated with 42 because you have defined only the Example::class parameter + * @see [Injection Definitions](https://github.com/rdlowrey/auryn#injection-definitions) + */ + AurynConfig::DEFINITIONS => [ + Example::class => [ + ':param' => 42, + ] + ], + + /** + * As soon as the instance is created you can prepare some action before use the new created instance + * This is the same as: + * $class = new \stdClass; + * $class->param = 42; + * echo $class->param; + * @see [Prepares and Setter Injection](https://github.com/rdlowrey/auryn#prepares-and-setter-injection) + */ + AurynConfig::PREPARATIONS => [ + stdClass::class => function (stdClass $class, Injector $injector) { + $class->param = 42; + }, + ], + + /** + * You can delegate the instantiation of an object to a some kind of callable factory + * This will be always used to get the instance of a class. + * @see [Instantiation Delegates](https://github.com/rdlowrey/auryn#instantiation-delegates) + */ + AurynConfig::DELEGATIONS => [ + ConfigInterface::class => [ ConfigFactory::class, 'make'] + ], ]; /** @@ -196,21 +180,21 @@ public function execute( string $text ) { $injector = new Injector(); /** - * Pass the $injector instance to the AurynResolver::class as first parameter and a + * Pass the $injector instance to the AurynConfig::class as first parameter and a * Config::class instance at the second parameters with the configuration array. */ -$app = new AurynConfig( $injector, ConfigFactory::make( $config ) ); +$app = new AurynConfig($injector, ConfigFactory::make($config)); /** - * Call the AurynResolver::resolve() method to do the autowiring of the application + * Call the AurynConfig::resolve() method to do the autowiring of the application */ $app->resolve(); /** - * Now that you have autoload your application dependency you can call $injector for instantiating objects + * Now that you have autoloaded your application dependency you can call $injector for instantiating objects * when you need them */ -$example = $injector->make( Example::class ); +$example = $injector->make(Example::class); // $example instanceof Example::class @@ -219,63 +203,56 @@ public function execute( string $text ) { // //$result = $injector->execute( [ $example, 'execute' ] ); -//d_footer( -// $app, -// $example, -// $example2, -// $example !== $example2, -// $result, -// $example->getConfig() -//); - /** * Advanced usage */ /** - * If you need more power you can extend the AurynResolver::class BEFORE calling the AurynResolver::resolve() method + * If you need more power you can extend the AurynConfig::class BEFORE calling the AurynConfig::resolve() method * Create your custom configuration like the follow: * $config = [ - * 'your-key' => [ - * 'Key' => 'Value', + * 'your-key' => [ + * 'Key' => 'Value', * ], * ]; * - * Now extend the AurynResolver: + * Now extend the AurynConfig: */ $app->extend( - new class implements Extension { - - /** @var string */ - const YOUR_KEY = 'your-key'; - - public function name(): string { - return (string) self::YOUR_KEY; - } - - /** - * Called inside the AurynResolver instance - * @param AurynConfigInterface $application - */ - public function execute( AurynConfigInterface $application ) { - - /** - * ::walk() accept: - * self::YOUR_KEY will be a key to search against the config array - * [ $this, 'doSomeStuff' ] will be a valid callable to do the work you need. - */ - $application->walk( (string) self::YOUR_KEY, [$this, 'doSomeStuff'] ); - } - - /** - * @param string $array_value Array value from yous configuration - * @param int|string $array_key Array key from your configuration - * @param Injector $injector An instance of the Injector::class - */ - public function doSomeStuff( string $array_value, $array_key, Injector $injector ) { - // Do your logic here - } - } + new class implements Extension { + /** @var string */ + const YOUR_KEY = 'your-key'; + + public function name(): string + { + return (string) self::YOUR_KEY; + } + + /** + * Called inside the AurynConfig instance + * @param AurynConfigInterface $application + */ + public function execute(AurynConfigInterface $application) + { + + /** + * ::walk() accept: + * self::YOUR_KEY will be a key to search against the config array + * [ $this, 'doSomeStuff' ] will be a valid callable to do the work you need. + */ + $application->walk((string) self::YOUR_KEY, [$this, 'doSomeStuff']); + } + + /** + * @param string $array_value Array value from yous configuration + * @param int|string $array_key Array key from your configuration + * @param Injector $injector An instance of the Injector::class + */ + public function doSomeStuff(string $array_value, $array_key, Injector $injector) + { + // Do your logic here + } + } ); /** diff --git a/index.php b/index.php deleted file mode 100644 index aefbe26..0000000 --- a/index.php +++ /dev/null @@ -1,43 +0,0 @@ -./src/ ./tests/ + example.php diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 426a40e..d79f2ee 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -8,6 +8,8 @@ use ItalyStrap\Config\Config; use ItalyStrap\Config\ConfigInterface; use ItalyStrap\Empress\Injector; +use ItalyStrap\Empress\ProxyFactory; +use ItalyStrap\Empress\ProxyFactoryInterface; use ItalyStrap\Finder\FinderInterface; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; @@ -19,7 +21,7 @@ class UnitTestCase extends Unit protected UnitTester $tester; - protected ?Injector $realInjector; + protected Injector $realInjector; protected ObjectProphecy $injector; @@ -28,6 +30,15 @@ protected function makeInjector(): Injector return $this->injector->reveal(); } + protected ProxyFactoryInterface $realProxyFactory; + + protected ?ObjectProphecy $proxyFactory; + + protected function makeProxyFactory(): ProxyFactoryInterface + { + return $this->proxyFactory->reveal(); + } + protected ObjectProphecy $config; protected function makeConfig(): ConfigInterface @@ -54,8 +65,10 @@ protected function makeFinder(): FinderInterface // phpcs:ignore -- Method from Codeception protected function _before(): void { $this->realInjector = new Injector(); - $this->injector = $this->prophesize(Injector::class); + $this->realProxyFactory = new ProxyFactory(); $this->configReal = new Config(); + $this->injector = $this->prophesize(Injector::class); + $this->proxyFactory = $this->prophesize(ProxyFactoryInterface::class); $this->config = $this->prophesize(Config::class); $this->finder = $this->prophesize(FinderInterface::class); @@ -64,10 +77,10 @@ protected function _before(): void { // phpcs:ignore -- Method from Codeception protected function _after(): void { - $this->configReal = clone $this->configReal; $this->prophet->checkPredictions(); - unset($this->config); + unset($this->configReal); unset($this->realInjector); + unset($this->realProxyFactory); \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); } } diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php index 3ca631f..e476f6a 100644 --- a/tests/unit/AurynConfigIntegrationTest.php +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -6,18 +6,18 @@ use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; -use ItalyStrap\Empress\Injector; -use ItalyStrap\Empress\ProxyFactory; +use ItalyStrap\Empress\ProxyFactoryInterface; use ItalyStrap\Tests\ConcreteNeedsSomeInterface; use ItalyStrap\Tests\SomeConcrete; use ItalyStrap\Tests\SomeInterface; use ItalyStrap\Tests\UnitTestCase; +use Prophecy\Argument; class AurynConfigIntegrationTest extends UnitTestCase { private function makeInstance(array $config = []): AurynConfig { - return new AurynConfig($this->realInjector, ConfigFactory::make($config), new ProxyFactory()); + return new AurynConfig($this->realInjector, ConfigFactory::make($config), $this->realProxyFactory); } public function testItShouldAlias(): void @@ -39,4 +39,48 @@ public function testItShouldAlias(): void $this->assertInstanceOf(SomeConcrete::class, $actual); $this->assertSame('SomeConcrete', $actual->render()); } + + public function testItShouldShare(): void + { + $aurynConfig = $this->makeInstance( + [ + AurynConfig::SHARING => [ + SomeConcrete::class, + ], + ] + ); + + $aurynConfig->resolve(); + + $shared = $this->realInjector->make(SomeConcrete::class); + $this->assertSame($shared, $this->realInjector->make(SomeConcrete::class)); + } + + public function testItShouldProxy(): void + { + $this->proxyFactory->__invoke(Argument::type('string'), Argument::type('callable')) + ->willReturn(new class + { + public function render(): string + { + return 'DifferentConcrete'; + } + }) + ->shouldBeCalledTimes(1); + + $this->realProxyFactory = $this->proxyFactory->reveal(); + $sut = $this->makeInstance( + [ + AurynConfig::PROXY => [ + SomeConcrete::class + ], + ] + ); + + $sut->resolve(); + + /** @var SomeConcrete $concrete */ + $concrete = $this->realInjector->make(SomeConcrete::class); + $this->assertSame('DifferentConcrete', $concrete->render()); + } } diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index af6d6b4..646b002 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -10,6 +10,8 @@ use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\Extension; use ItalyStrap\Empress\ProxyFactory; +use ItalyStrap\Empress\ProxyFactoryInterface; +use ItalyStrap\Tests\SomeConcrete; use ItalyStrap\Tests\SomeExtension; use ItalyStrap\Tests\UnitTestCase; use PHPUnit\Framework\Assert; @@ -17,18 +19,52 @@ class AurynConfigTest extends UnitTestCase { - private ?ProxyFactory $proxyFactory = null; - protected function makeInstance(array $config = []): AurynConfig { - return new AurynConfig($this->makeInjector(), ConfigFactory::make($config), $this->proxyFactory); + return new AurynConfig($this->makeInjector(), ConfigFactory::make($config), $this->makeProxyFactory()); } - public function testItShouldBeInstantiable(): void +// public function testItShouldProxy(): void +// { +// $mockProxyFactory = $this->prophesize(ProxyFactoryInterface::class); +// $mockProxyFactory->__invoke(Argument::type('string'), Argument::type('callable')) +// ->shouldBeCalledTimes(1); +// +// $this->proxyFactory = $mockProxyFactory->reveal(); +// $sut = $this->makeInstance( +// [ +// AurynConfig::PROXY => [ +// SomeConcrete::class +// ], +// ] +// ); +// +// $sut->resolve(); +// +// $concrete = $this->realInjector->make(SomeConcrete::class); +// } + + public function testItShouldProxy01(): void { - $this->proxyFactory = new ProxyFactory(); - $sut = $this->makeInstance(); - $this->assertInstanceOf(AurynConfig::class, $sut); + + $expected = 'SomeClassProxies'; + + $this->injector->proxy( + Argument::type('string'), + Argument::type('callable') + )->will(function ($args) use ($expected) { + Assert::assertEquals($expected, $args[0], ''); + }); + + $sut = $this->makeInstance( + [ + AurynConfig::PROXY => [ + $expected, + ], + ] + ); + + $sut->resolve(); } public function shareProvider(): iterable @@ -65,29 +101,6 @@ public function testItShouldShare($expected): void $sut->resolve(); } - public function testItShouldProxy(): void - { - - $expected = 'SomeClassProxies'; - - $this->injector->proxy( - Argument::type('string'), - Argument::type('callable') - )->will(function ($args) use ($expected) { - Assert::assertEquals($expected, $args[0], ''); - }); - - $sut = $this->makeInstance( - [ - AurynConfig::PROXY => [ - $expected, - ], - ] - ); - - $sut->resolve(); - } - public function testItShouldAlias(): void { From 6b4576e73cc7bb3c82c1c734cd48e7b5e5def85e Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 19 Oct 2025 15:23:37 +0200 Subject: [PATCH 15/46] chore: updates tests --- .github/workflows/test.yml | 4 +++- .gitignore | 4 +++- codeception.dist.yml | 2 ++ composer.json | 6 +++--- .../fixtures/config/autoload/config.global.php | 2 +- .../fixtures/config/autoload/config.local.php | 2 +- tests/_data/fixtures/config/test.global.php | 2 +- tests/_data/fixtures/modules/ModuleStub1.php | 2 +- .../fixtures/src/ConcreteNeedsSomeInterface.php | 2 +- tests/_data/fixtures/src/SomeConcrete.php | 2 +- tests/_data/fixtures/src/SomeExtension.php | 2 +- tests/_data/fixtures/src/SomeInterface.php | 2 +- tests/_support/Helper/Unit.php | 2 +- tests/_support/UnitTester.php | 8 ++++---- tests/_support/_generated/.gitignore | 2 -- tests/_support/_generated/UnitTesterActions.php | 16 ++++++++++++++++ tests/src/UnitTestCase.php | 3 +-- tests/unit.suite.yml | 8 +------- tests/unit/AurynConfigIntegrationTest.php | 10 +++++----- tests/unit/AurynConfigTest.php | 8 ++++---- tests/unit/PhpFileProviderTest.php | 4 ++-- .../unit/ProvidersCollectionIntegrationTest.php | 6 +++--- tests/unit/ProvidersCollectionTest.php | 4 ++-- tests/unit/ProxyInjectorTest.php | 4 ++-- 24 files changed, 60 insertions(+), 47 deletions(-) delete mode 100644 tests/_support/_generated/.gitignore create mode 100644 tests/_support/_generated/UnitTesterActions.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15e0d0f..79f6004 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,11 +19,13 @@ jobs: php: - '7.4' - '8.0' + - '8.1' + - '8.2' dependencies: - "lowest" - "highest" include: - - php: '8.1' + - php: '8.5' composer-options: "--ignore-platform-reqs" steps: diff --git a/.gitignore b/.gitignore index 1907aa0..d1f300e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.vscode/ /vendor/ /_others/ @@ -6,4 +7,5 @@ codeception.yml *.lock c3.php -rector.php \ No newline at end of file +rector.php +bone.json \ No newline at end of file diff --git a/codeception.dist.yml b/codeception.dist.yml index d5b4fbf..da6a54a 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -1,3 +1,5 @@ +namespace: ItalyStrap\Empress\Tests +support: Support paths: tests: tests output: tests/_output diff --git a/composer.json b/composer.json index 09d1a6d..d63ba13 100644 --- a/composer.json +++ b/composer.json @@ -59,12 +59,12 @@ }, "autoload-dev": { "psr-4": { - "ItalyStrap\\Tests\\Modules\\": "tests/_data/fixtures/modules/", - "ItalyStrap\\Tests\\": [ + "ItalyStrap\\Empress\\Tests\\": [ "tests/src/", "tests/_data/fixtures/src/" ], - "ItalyStrap\\Tests\\Unit\\": "tests/unit/" + "ItalyStrap\\Empress\\Tests\\Modules\\": "tests/_data/fixtures/modules/", + "ItalyStrap\\Empress\\Tests\\Unit\\": "tests/unit/" }, "files": [ "tests/_data/fixtures/fixtures.php", diff --git a/tests/_data/fixtures/config/autoload/config.global.php b/tests/_data/fixtures/config/autoload/config.global.php index 32e19a6..977905f 100644 --- a/tests/_data/fixtures/config/autoload/config.global.php +++ b/tests/_data/fixtures/config/autoload/config.global.php @@ -3,7 +3,7 @@ declare(strict_types=1); use ItalyStrap\Empress\AurynConfig; -use ItalyStrap\Tests\Unit\ProvidersCollectionIntegrationTest; +use ItalyStrap\Empress\Tests\Unit\ProvidersCollectionIntegrationTest; return [ AurynConfig::ALIASES => [ diff --git a/tests/_data/fixtures/config/autoload/config.local.php b/tests/_data/fixtures/config/autoload/config.local.php index cac54b7..b5b4ce2 100644 --- a/tests/_data/fixtures/config/autoload/config.local.php +++ b/tests/_data/fixtures/config/autoload/config.local.php @@ -3,7 +3,7 @@ declare(strict_types=1); use ItalyStrap\Empress\AurynConfig; -use ItalyStrap\Tests\Unit\ProvidersCollectionIntegrationTest; +use ItalyStrap\Empress\Tests\Unit\ProvidersCollectionIntegrationTest; return [ AurynConfig::ALIASES => [ diff --git a/tests/_data/fixtures/config/test.global.php b/tests/_data/fixtures/config/test.global.php index 3720f0e..d978331 100644 --- a/tests/_data/fixtures/config/test.global.php +++ b/tests/_data/fixtures/config/test.global.php @@ -3,7 +3,7 @@ declare(strict_types=1); use ItalyStrap\Empress\AurynConfig; -use ItalyStrap\Tests\Unit\ProvidersCollectionIntegrationTest; +use ItalyStrap\Empress\Tests\Unit\ProvidersCollectionIntegrationTest; return [ AurynConfig::ALIASES => [ diff --git a/tests/_data/fixtures/modules/ModuleStub1.php b/tests/_data/fixtures/modules/ModuleStub1.php index 86a3356..0101352 100644 --- a/tests/_data/fixtures/modules/ModuleStub1.php +++ b/tests/_data/fixtures/modules/ModuleStub1.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\Tests\Modules; +namespace ItalyStrap\Empress\Tests\Modules; use Auryn\Test\{SharedAliasedInterface, SharedClass}; use ItalyStrap\Empress\AurynConfig; diff --git a/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php b/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php index fb73972..45ebcef 100644 --- a/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php +++ b/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\Tests; +namespace ItalyStrap\Empress\Tests; class ConcreteNeedsSomeInterface { diff --git a/tests/_data/fixtures/src/SomeConcrete.php b/tests/_data/fixtures/src/SomeConcrete.php index 02e61e4..9398f50 100644 --- a/tests/_data/fixtures/src/SomeConcrete.php +++ b/tests/_data/fixtures/src/SomeConcrete.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\Tests; +namespace ItalyStrap\Empress\Tests; class SomeConcrete implements SomeInterface { diff --git a/tests/_data/fixtures/src/SomeExtension.php b/tests/_data/fixtures/src/SomeExtension.php index d42d14d..7052395 100644 --- a/tests/_data/fixtures/src/SomeExtension.php +++ b/tests/_data/fixtures/src/SomeExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\Tests; +namespace ItalyStrap\Empress\Tests; use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\Extension; diff --git a/tests/_data/fixtures/src/SomeInterface.php b/tests/_data/fixtures/src/SomeInterface.php index 3f6c4ba..f040b05 100644 --- a/tests/_data/fixtures/src/SomeInterface.php +++ b/tests/_data/fixtures/src/SomeInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\Tests; +namespace ItalyStrap\Empress\Tests; interface SomeInterface { diff --git a/tests/_support/Helper/Unit.php b/tests/_support/Helper/Unit.php index 6064d37..a8750e6 100644 --- a/tests/_support/Helper/Unit.php +++ b/tests/_support/Helper/Unit.php @@ -1,5 +1,5 @@ Date: Wed, 22 Oct 2025 06:27:04 +0200 Subject: [PATCH 16/46] chore: introduces Dockerfile --- .docker/.env | 1 + .docker/docker-compose.yml | 29 +++++++++++++++++++++++++++++ .docker/php/Dockerfile | 27 +++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 .docker/.env create mode 100644 .docker/docker-compose.yml create mode 100644 .docker/php/Dockerfile diff --git a/.docker/.env b/.docker/.env new file mode 100644 index 0000000..81ff041 --- /dev/null +++ b/.docker/.env @@ -0,0 +1 @@ +PROJECT_NAME= diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml new file mode 100644 index 0000000..a2bc9b5 --- /dev/null +++ b/.docker/docker-compose.yml @@ -0,0 +1,29 @@ +x-php-base: &php-base + build: + context: . + dockerfile: php/Dockerfile + container_name: ${PROJECT_NAME:-package}_php + volumes: + - ../:/app + environment: + UID: "${UID:-1000}" + GID: "${GID:-1000}" + working_dir: /app + +services: + php: + <<: *php-base + build: + context: . + dockerfile: php/Dockerfile + args: + PHP_VERSION: 7.4 + container_name: ${PROJECT_NAME:-package}_php74 + php83: + <<: *php-base + build: + context: . + dockerfile: php/Dockerfile + args: + PHP_VERSION: 8.3 + container_name: ${PROJECT_NAME:-package}_php83 diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 0000000..7f11927 --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,27 @@ +ARG PHP_VERSION + +FROM php:${PHP_VERSION}-cli + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + unzip \ + zip \ + libzip-dev \ + libonig-dev \ + && docker-php-ext-install zip \ + && pecl install pcov \ + && docker-php-ext-enable pcov \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +WORKDIR /app + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +ARG UID=1000 +ARG GID=1000 +RUN groupadd -g ${GID} appgroup && \ + useradd -u ${UID} -g appgroup -m appuser && \ + chown -R appuser:appgroup /app +USER appuser + +CMD ["php", "-v"] \ No newline at end of file From 70118e385c35b354098ae329d59b241b3312142d Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 29 Oct 2025 08:16:18 +0100 Subject: [PATCH 17/46] fix: updates dependencies --- bridge/Injector.php | 838 --------------------------------------- composer.json | 12 +- namespace-bc-aliases.php | 13 +- phpcs.xml | 1 + 4 files changed, 15 insertions(+), 849 deletions(-) delete mode 100644 bridge/Injector.php diff --git a/bridge/Injector.php b/bridge/Injector.php deleted file mode 100644 index 4a40c06..0000000 --- a/bridge/Injector.php +++ /dev/null @@ -1,838 +0,0 @@ -reflector = $reflector ?: new CachingReflector(); - } - - public function __clone() - { - $this->inProgressMakes = array(); - } - - /** - * Define instantiation directives for the specified class - * - * @param string $name The class (or alias) whose constructor arguments we wish to define - * @param array $args An array mapping parameter names to values/instructions - * - * @return \Auryn\Injector - */ - public function define($name, array $args) - { - list(, $normalizedName) = $this->resolveAlias($name); - $this->classDefinitions[$normalizedName] = $args; - - return $this; - } - - /** - * Assign a global default value for all parameters named $paramName - * - * Global parameter definitions are only used for parameters with no typehint, pre-defined or - * call-time definition. - * - * @param string $paramName The parameter name for which this value applies - * @param mixed $value The value to inject for this parameter name - * @return self - */ - public function defineParam($paramName, $value) - { - $this->paramDefinitions[$paramName] = $value; - - return $this; - } - - /** - * Define an alias for all occurrences of a given typehint - * - * Use this method to specify implementation classes for interface and abstract class typehints. - * - * @param string $original The typehint to replace - * @param string $alias The implementation name - * @throws ConfigException if any argument is empty or not a string - * @return self - */ - public function alias($original, $alias) - { - if (empty($original) || !is_string($original)) { - throw new ConfigException( - self::M_NON_EMPTY_STRING_ALIAS, - self::E_NON_EMPTY_STRING_ALIAS - ); - } - if (empty($alias) || !is_string($alias)) { - throw new ConfigException( - self::M_NON_EMPTY_STRING_ALIAS, - self::E_NON_EMPTY_STRING_ALIAS - ); - } - - $originalNormalized = $this->normalizeName($original); - - if (isset($this->shares[$originalNormalized])) { - throw new ConfigException( - sprintf( - self::M_SHARED_CANNOT_ALIAS, - $this->normalizeName(get_class($this->shares[$originalNormalized])), - $alias - ), - self::E_SHARED_CANNOT_ALIAS - ); - } - - if (array_key_exists($originalNormalized, $this->shares)) { - $aliasNormalized = $this->normalizeName($alias); - $this->shares[$aliasNormalized] = null; - unset($this->shares[$originalNormalized]); - } - - $this->aliases[$originalNormalized] = $alias; - - return $this; - } - - private function normalizeName($className) - { - return ltrim(strtolower($className), '\\'); - } - - /** - * Share the specified class/instance across the Injector context - * - * @param mixed $nameOrInstance The class or object to share - * @throws ConfigException if $nameOrInstance is not a string or an object - * @return self - */ - public function share($nameOrInstance) - { - if (is_string($nameOrInstance)) { - $this->shareClass($nameOrInstance); - } elseif (is_object($nameOrInstance)) { - $this->shareInstance($nameOrInstance); - } else { - throw new ConfigException( - sprintf( - self::M_SHARE_ARGUMENT, - __CLASS__, - gettype($nameOrInstance) - ), - self::E_SHARE_ARGUMENT - ); - } - - return $this; - } - - private function shareClass($nameOrInstance) - { - list(, $normalizedName) = $this->resolveAlias($nameOrInstance); - $this->shares[$normalizedName] = isset($this->shares[$normalizedName]) - ? $this->shares[$normalizedName] - : null; - } - - private function resolveAlias($name) - { - $normalizedName = $this->normalizeName($name); - if (isset($this->aliases[$normalizedName])) { - $name = $this->aliases[$normalizedName]; - $normalizedName = $this->normalizeName($name); - } - - return array($name, $normalizedName); - } - - private function shareInstance($obj) - { - $normalizedName = $this->normalizeName(get_class($obj)); - if (isset($this->aliases[$normalizedName])) { - // You cannot share an instance of a class name that is already aliased - throw new ConfigException( - sprintf( - self::M_ALIASED_CANNOT_SHARE, - $normalizedName, - $this->aliases[$normalizedName] - ), - self::E_ALIASED_CANNOT_SHARE - ); - } - $this->shares[$normalizedName] = $obj; - } - - /** - * Register a prepare callable to modify/prepare objects of type $name after instantiation - * - * Any callable or provisionable invokable may be specified. Preparers are passed two - * arguments: the instantiated object to be mutated and the current Injector instance. - * - * @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 - * @return self - */ - public function prepare($name, $callableOrMethodStr) - { - if ($this->isExecutable($callableOrMethodStr) === false) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr - ); - } - - list(, $normalizedName) = $this->resolveAlias($name); - $this->prepares[$normalizedName] = $callableOrMethodStr; - - return $this; - } - - private function isExecutable($exe) - { - if (is_callable($exe)) { - return true; - } - if (is_string($exe) && method_exists($exe, '__invoke')) { - return true; - } - if (is_array($exe) && isset($exe[0], $exe[1]) && method_exists($exe[0], $exe[1])) { - return true; - } - - return false; - } - - /** - * Delegate the creation of $name instances to the specified callable, receiving arguments based on the callables - * signature. - * - * @param string $name - * @param mixed $callableOrMethodStr Any callable or provisionable invokable method - * @throws ConfigException if $callableOrMethodStr is not a callable. - * @return self - */ - public function delegate($name, $callableOrMethodStr) - { - if (!$this->isExecutable($callableOrMethodStr)) { - $this->generateInvalidCallableError($callableOrMethodStr); - } - - $normalizedName = $this->normalizeName($name); - $this->delegates[$normalizedName] = $callableOrMethodStr; - - return $this; - } - - /** - * Retrieve stored data for the specified definition type - * - * Exposes introspection of existing binds/delegates/shares/etc for decoration and composition. - * - * @param string $nameFilter An optional class name filter - * @param int $typeFilter A bitmask of Injector::* type constant flags - * @return array - */ - public function inspect($nameFilter = null, $typeFilter = null) - { - $result = array(); - $name = $nameFilter ? $this->normalizeName($nameFilter) : null; - - if (empty($typeFilter)) { - $typeFilter = self::I_ALL; - } - - $types = array( - self::I_BINDINGS => "classDefinitions", - self::I_DELEGATES => "delegates", - self::I_PREPARES => "prepares", - self::I_ALIASES => "aliases", - self::I_SHARES => "shares" - ); - - foreach ($types as $type => $source) { - if ($typeFilter & $type) { - $result[$type] = $this->filter($this->{$source}, $name); - } - } - - return $result; - } - - private function filter($source, $name) - { - if (empty($name)) { - return $source; - } elseif (array_key_exists($name, $source)) { - return array($name => $source[$name]); - } else { - return array(); - } - } - - /** - * Proxy the specified class across the Injector context. - * - * @param string $name The class to proxy - * - * @param $callableOrMethodStr - * @return Injector - * @throws ConfigException - */ - public function proxy(string $name, $callableOrMethodStr) - { - if (!$this->isExecutable($callableOrMethodStr)) { - $this->generateInvalidCallableError($callableOrMethodStr); - } - - list($className, $normalizedName) = $this->resolveAlias($name); - $this->proxies[$normalizedName] = $callableOrMethodStr; - - return $this; - } - - /** - * Instantiate/provision a class instance - * - * @param string $name - * @param array $args - * @throws InjectionException if a cyclic gets detected when provisioning - * @return mixed - */ - public function make($name, array $args = array()) - { - list($className, $normalizedClass) = $this->resolveAlias($name); - - if (isset($this->inProgressMakes[$normalizedClass])) { - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_CYCLIC_DEPENDENCY, - $className - ), - self::E_CYCLIC_DEPENDENCY - ); - } - - $this->inProgressMakes[$normalizedClass] = count($this->inProgressMakes); - - // isset() is used specifically here because classes may be marked as "shared" before an - // instance is stored. In these cases the class is "shared," but it has a null value and - // instantiation is needed. - if (isset($this->shares[$normalizedClass])) { - unset($this->inProgressMakes[$normalizedClass]); - - return $this->shares[$normalizedClass]; - } - - try { - if (isset($this->delegates[$normalizedClass])) { - $executable = $this->buildExecutable($this->delegates[$normalizedClass]); - $reflectionFunction = $executable->getCallableReflection(); - $args = $this->provisionFuncArgs($reflectionFunction, $args, null, $className); - $obj = call_user_func_array(array($executable, '__invoke'), $args); - } elseif (isset($this->proxies[$normalizedClass])) { - if (isset($this->prepares[$normalizedClass])) { - $this->preparesProxy[$normalizedClass] = $this->prepares[$normalizedClass]; - } - $obj = $this->resolveProxy($className, $normalizedClass, $args); - unset($this->prepares[$normalizedClass]); - } else { - $obj = $this->provisionInstance($className, $normalizedClass, $args); - } - - $obj = $this->prepareInstance($obj, $normalizedClass); - - if (array_key_exists($normalizedClass, $this->shares)) { - $this->shares[$normalizedClass] = $obj; - } - - unset($this->inProgressMakes[$normalizedClass]); - } - catch (\Exception $exception) { - unset($this->inProgressMakes[$normalizedClass]); - throw $exception; - } - catch (\Throwable $exception) { - unset($this->inProgressMakes[$normalizedClass]); - throw $exception; - } - - return $obj; - } - - private function resolveProxy(string $className, string $normalizedClass, array $args) - { - $callback = function () use ($className, $normalizedClass, $args) { - return $this->buildWrappedObject( $className, $normalizedClass, $args ); - }; - - $proxy = $this->proxies[$normalizedClass]; - - return $proxy( $className, $callback ); - } - - /** - * @param string $className - * @param string $normalizedClass - * @param array $args - * @return mixed|object - * @throws InjectionException - */ - private function buildWrappedObject( $className, $normalizedClass, array $args ) { - $wrappedObject = $this->provisionInstance( $className, $normalizedClass, $args ); - - if ( isset( $this->preparesProxy[ $normalizedClass ] ) ) { - $this->prepares[ $normalizedClass ] = $this->preparesProxy[ $normalizedClass ]; - } - - return $this->prepareInstance( $wrappedObject, $normalizedClass ); - } - - private function provisionInstance($className, $normalizedClass, array $definition) - { - try { - $ctor = $this->reflector->getCtor($className); - - if (!$ctor) { - $obj = $this->instantiateWithoutCtorParams($className); - } elseif (!$ctor->isPublic()) { - throw new InjectionException( - $this->inProgressMakes, - sprintf(self::M_NON_PUBLIC_CONSTRUCTOR, $className), - self::E_NON_PUBLIC_CONSTRUCTOR - ); - } elseif ($ctorParams = $this->reflector->getCtorParams($className)) { - $reflClass = $this->reflector->getClass($className); - $definition = isset($this->classDefinitions[$normalizedClass]) - ? array_replace($this->classDefinitions[$normalizedClass], $definition) - : $definition; - $args = $this->provisionFuncArgs($ctor, $definition, $ctorParams, $className); - $obj = $reflClass->newInstanceArgs($args); - } else { - $obj = $this->instantiateWithoutCtorParams($className); - } - - return $obj; - } catch (\ReflectionException $e) { - throw new InjectionException( - $this->inProgressMakes, - sprintf(self::M_MAKE_FAILURE, $className, $e->getMessage()), - self::E_MAKE_FAILURE, - $e - ); - } - } - - private function instantiateWithoutCtorParams($className) - { - $reflClass = $this->reflector->getClass($className); - - if (!$reflClass->isInstantiable()) { - $type = $reflClass->isInterface() ? 'interface' : 'abstract class'; - throw new InjectionException( - $this->inProgressMakes, - sprintf(self::M_NEEDS_DEFINITION, $type, $className), - self::E_NEEDS_DEFINITION - ); - } - - return new $className(); - } - - private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array $definition, array $reflParams = null, $className = null) - { - $args = array(); - - // @TODO store this in ReflectionStorage - if (!isset($reflParams)) { - $reflParams = $reflFunc->getParameters(); - } - - foreach ($reflParams as $i => $reflParam) { - $name = $reflParam->name; - - if (isset($definition[$i]) || array_key_exists($i, $definition)) { - // indexed arguments take precedence over named parameters - $arg = $definition[$i]; - } elseif (isset($definition[$name]) || array_key_exists($name, $definition)) { - // interpret the param as a class name to be instantiated - $arg = $this->make($definition[$name]); - } elseif (($prefix = self::A_RAW . $name) && (isset($definition[$prefix]) || array_key_exists($prefix, $definition))) { - // interpret the param as a raw value to be injected - $arg = $definition[$prefix]; - } elseif (($prefix = self::A_DELEGATE . $name) && isset($definition[$prefix])) { - // interpret the param as an invokable delegate - $arg = $this->buildArgFromDelegate($name, $definition[$prefix]); - } 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)) { - $arg = $this->buildArgFromReflParam($reflParam, $className); - - if ($arg === null && PHP_VERSION_ID >= 50600 && $reflParam->isVariadic()) { - // 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; - } - } - - $args[] = $arg; - } - - return $args; - } - - private function buildArgFromParamDefineArr($definition) - { - if (!is_array($definition)) { - throw new InjectionException( - $this->inProgressMakes - // @TODO Add message - ); - } - - if (!isset($definition[0], $definition[1])) { - throw new InjectionException( - $this->inProgressMakes - // @TODO Add message - ); - } - - list($class, $definition) = $definition; - - return $this->make($class, $definition); - } - - private function buildArgFromDelegate($paramName, $callableOrMethodStr) - { - if ($this->isExecutable($callableOrMethodStr) === false) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr - ); - } - - $executable = $this->buildExecutable($callableOrMethodStr); - - return $executable($paramName, $this); - } - - private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam) - { - $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam); - - if (!$typeHint) { - $obj = null; - } elseif ($reflParam->isDefaultValueAvailable()) { - $normalizedName = $this->normalizeName($typeHint); - // 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); - } else { - $obj = $reflParam->getDefaultValue(); - } - } else { - $obj = $this->make($typeHint); - } - - return $obj; - } - - private function buildArgFromReflParam(\ReflectionParameter $reflParam, $className = null) - { - if (array_key_exists($reflParam->name, $this->paramDefinitions)) { - $arg = $this->paramDefinitions[$reflParam->name]; - } elseif ($reflParam->isDefaultValueAvailable()) { - $arg = $reflParam->getDefaultValue(); - } elseif ($reflParam->isOptional()) { - // This branch is required to work around PHP bugs where a parameter is optional - // but has no default value available through reflection. Specifically, PDO exhibits - // this behavior. - $arg = null; - } else { - $reflFunc = $reflParam->getDeclaringFunction(); - $classDeclare = ($reflFunc instanceof \ReflectionMethod) - ? " declared in " . $reflFunc->getDeclaringClass()->name . "::" - : ""; - $classWord = ($reflFunc instanceof \ReflectionMethod) - ? $className . '::' - : ''; - $funcWord = $classWord . $reflFunc->name; - - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_UNDEFINED_PARAM, - $reflParam->name, - $reflParam->getPosition(), - $funcWord, - $classDeclare - ), - self::E_UNDEFINED_PARAM - ); - } - - return $arg; - } - - private function prepareInstance($obj, $normalizedClass) - { - if (isset($this->prepares[$normalizedClass])) { - $prepare = $this->prepares[$normalizedClass]; - $executable = $this->buildExecutable($prepare); - $result = $executable($obj, $this); - if ($result instanceof $normalizedClass) { - $obj = $result; - } - } - - $interfaces = @class_implements($obj); - - if ($interfaces === false) { - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_MAKING_FAILED, - $normalizedClass, - gettype($obj) - ), - self::E_MAKING_FAILED - ); - } - - if (empty($interfaces)) { - return $obj; - } - - $interfaces = array_flip(array_map(array($this, 'normalizeName'), $interfaces)); - $prepares = array_intersect_key($this->prepares, $interfaces); - foreach ($prepares as $interfaceName => $prepare) { - $executable = $this->buildExecutable($prepare); - $result = $executable($obj, $this); - if ($result instanceof $normalizedClass) { - $obj = $result; - } - } - - return $obj; - } - - /** - * Invoke the specified callable or class::method string, provisioning dependencies along the way - * - * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string - * @param array $args Optional array specifying params with which to invoke the provisioned callable - * @throws \Auryn\InjectionException - * @return mixed Returns the invocation result returned from calling the generated executable - */ - public function execute($callableOrMethodStr, array $args = array()) - { - list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr); - $executable = new Executable($reflFunc, $invocationObj); - $args = $this->provisionFuncArgs($reflFunc, $args, null, $invocationObj === null ? null : get_class($invocationObj)); - - return call_user_func_array(array($executable, '__invoke'), $args); - } - - /** - * Provision an Executable instance from any valid callable or class::method string - * - * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string - * @return \Auryn\Executable - */ - public function buildExecutable($callableOrMethodStr) - { - try { - list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr); - } catch (\ReflectionException $e) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr, - $e - ); - } - - return new Executable($reflFunc, $invocationObj); - } - - private function buildExecutableStruct($callableOrMethodStr) - { - if (is_string($callableOrMethodStr)) { - $executableStruct = $this->buildExecutableStructFromString($callableOrMethodStr); - } elseif ($callableOrMethodStr instanceof \Closure) { - $callableRefl = new \ReflectionFunction($callableOrMethodStr); - $executableStruct = array($callableRefl, null); - } elseif (is_object($callableOrMethodStr) && is_callable($callableOrMethodStr)) { - $invocationObj = $callableOrMethodStr; - $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke'); - $executableStruct = array($callableRefl, $invocationObj); - } elseif (is_array($callableOrMethodStr) - && isset($callableOrMethodStr[0], $callableOrMethodStr[1]) - && count($callableOrMethodStr) === 2 - ) { - $executableStruct = $this->buildExecutableStructFromArray($callableOrMethodStr); - } else { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr - ); - } - - return $executableStruct; - } - - private function buildExecutableStructFromString($stringExecutable) - { - if (function_exists($stringExecutable)) { - $callableRefl = $this->reflector->getFunction($stringExecutable); - $executableStruct = array($callableRefl, null); - } elseif (method_exists($stringExecutable, '__invoke')) { - $invocationObj = $this->make($stringExecutable); - $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke'); - $executableStruct = array($callableRefl, $invocationObj); - } elseif (strpos($stringExecutable, '::') !== false) { - list($class, $method) = explode('::', $stringExecutable, 2); - $executableStruct = $this->buildStringClassMethodCallable($class, $method); - } else { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $stringExecutable - ); - } - - return $executableStruct; - } - - private function buildStringClassMethodCallable($class, $method) - { - $relativeStaticMethodStartPos = strpos($method, 'parent::'); - - if ($relativeStaticMethodStartPos === 0) { - $childReflection = $this->reflector->getClass($class); - $class = $childReflection->getParentClass()->name; - $method = substr($method, $relativeStaticMethodStartPos + 8); - } - - list($className, $normalizedClass) = $this->resolveAlias($class); - $reflectionMethod = $this->reflector->getMethod($className, $method); - - if ($reflectionMethod->isStatic()) { - return array($reflectionMethod, null); - } - - $instance = $this->make($className); - // If the class was delegated, the instance may not be of the type - // $class but some other type. We need to get the reflection on the - // actual class to be able to call the method correctly. - $reflectionMethod = $this->reflector->getMethod($instance, $method); - - return array($reflectionMethod, $instance); - } - - private function buildExecutableStructFromArray($arrayExecutable) - { - list($classOrObj, $method) = $arrayExecutable; - - if (is_object($classOrObj) && method_exists($classOrObj, $method)) { - $callableRefl = $this->reflector->getMethod($classOrObj, $method); - $executableStruct = array($callableRefl, $classOrObj); - } elseif (is_string($classOrObj)) { - $executableStruct = $this->buildStringClassMethodCallable($classOrObj, $method); - } else { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $arrayExecutable - ); - } - - return $executableStruct; - } - - /** - * @param $callableOrMethodStr - * - * @throws ConfigException - */ - private function generateInvalidCallableError($callableOrMethodStr) - { - $errorDetail = ''; - if (is_string($callableOrMethodStr)) { - $errorDetail = " but received '$callableOrMethodStr'"; - } elseif (is_array($callableOrMethodStr) && - count($callableOrMethodStr) === 2 && - array_key_exists(0, $callableOrMethodStr) && - array_key_exists(1, $callableOrMethodStr) - ) { - if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { - $errorDetail = " but received ['" . $callableOrMethodStr[0] . "', '" . $callableOrMethodStr[1] . "']"; - } - } - throw new ConfigException( - sprintf(self::M_DELEGATE_ARGUMENT, __CLASS__, $errorDetail), - self::E_DELEGATE_ARGUMENT - ); - } -} diff --git a/composer.json b/composer.json index d63ba13..1fd9595 100644 --- a/composer.json +++ b/composer.json @@ -15,10 +15,10 @@ "prefer-stable": true, "require": { "php" : ">=7.4", - "rdlowrey/auryn": "^1.4", - "italystrap/config": "^2.2", - "ocramius/proxy-manager": "~2.11.0 || ~2.14.1", "brick/varexporter": "^0.3.8", + "friendsofphp/proxy-manager-lts": "^1.0", + "italystrap/config": "^2.10", + "overclokk/auryn": "dev-master", "webimpress/safe-writer": "^2.2" }, "require-dev": { @@ -48,13 +48,11 @@ "autoload": { "psr-4": { "ItalyStrap\\Empress\\": [ - "src/", - "bridge/" + "src/" ] }, "files": [ - "namespace-bc-aliases.php", - "bridge/Injector.php" + "namespace-bc-aliases.php" ] }, "autoload-dev": { diff --git a/namespace-bc-aliases.php b/namespace-bc-aliases.php index 4d80999..d9222e2 100644 --- a/namespace-bc-aliases.php +++ b/namespace-bc-aliases.php @@ -3,11 +3,16 @@ declare(strict_types=1); \class_alias( - \ItalyStrap\Empress\AurynConfigInterface::class, - \ItalyStrap\Empress\AurynResolverInterface::class + \ItalyStrap\Empress\AurynConfigInterface::class, + \ItalyStrap\Empress\AurynResolverInterface::class ); \class_alias( - \ItalyStrap\Empress\AurynConfig::class, - \ItalyStrap\Empress\AurynResolver::class + \ItalyStrap\Empress\AurynConfig::class, + \ItalyStrap\Empress\AurynResolver::class +); + +\class_alias( + \Auryn\Injector::class, + \ItalyStrap\Empress\Injector::class ); diff --git a/phpcs.xml b/phpcs.xml index d871ee0..b76ad9e 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -15,6 +15,7 @@ ./src/ ./tests/ example.php + namespace-bc-aliases.php From e996c76eed6832291aa5cfdd04dc25b0262a52e5 Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 29 Oct 2025 21:50:02 +0100 Subject: [PATCH 18/46] fix: removes the FQCN of the deleted \ItalyStrap\Empress\Injector::class --- namespace-bc-aliases.php | 5 ----- src/AurynConfig.php | 1 + src/ProvidersCollection.php | 3 ++- tests/benchmarks/AurynConfigBench.php | 2 +- tests/src/UnitTestCase.php | 2 +- tests/unit/AurynConfigTest.php | 2 +- tests/unit/ProvidersCollectionIntegrationTest.php | 2 +- tests/unit/ProxyInjectorTest.php | 2 +- 8 files changed, 8 insertions(+), 11 deletions(-) diff --git a/namespace-bc-aliases.php b/namespace-bc-aliases.php index d9222e2..577abf1 100644 --- a/namespace-bc-aliases.php +++ b/namespace-bc-aliases.php @@ -11,8 +11,3 @@ \ItalyStrap\Empress\AurynConfig::class, \ItalyStrap\Empress\AurynResolver::class ); - -\class_alias( - \Auryn\Injector::class, - \ItalyStrap\Empress\Injector::class -); diff --git a/src/AurynConfig.php b/src/AurynConfig.php index 5892ed5..bc69e99 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -4,6 +4,7 @@ namespace ItalyStrap\Empress; +use Auryn\Injector; use Auryn\ConfigException; use Auryn\InjectionException; use ItalyStrap\Config\ConfigInterface as Config; diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index 20f3a72..e3e0127 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -4,6 +4,7 @@ namespace ItalyStrap\Empress; +use Auryn\Injector; use Auryn\InjectionException; use ItalyStrap\Config\ConfigInterface; @@ -25,7 +26,7 @@ public function __construct( ) { $this->injector = $injector; $this->config = $config; - $this->cache = $cache ?? new ProvidersCache(); + $this->cache = $cache ??= new ProvidersCache(); $this->providers = $providers; } diff --git a/tests/benchmarks/AurynConfigBench.php b/tests/benchmarks/AurynConfigBench.php index d6a7509..602b738 100644 --- a/tests/benchmarks/AurynConfigBench.php +++ b/tests/benchmarks/AurynConfigBench.php @@ -4,9 +4,9 @@ namespace ItalyStrap\Tests\Benchmark; +use Auryn\Injector; use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; -use ItalyStrap\Empress\Injector; use stdClass; class AurynConfigBench diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 1594298..fdf6a9a 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -4,10 +4,10 @@ namespace ItalyStrap\Empress\Tests; +use Auryn\Injector; use Codeception\Test\Unit; use ItalyStrap\Config\Config; use ItalyStrap\Config\ConfigInterface; -use ItalyStrap\Empress\Injector; use ItalyStrap\Empress\ProxyFactory; use ItalyStrap\Empress\ProxyFactoryInterface; use ItalyStrap\Finder\FinderInterface; diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index 51a8f41..ec56ac9 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -4,8 +4,8 @@ namespace ItalyStrap\Empress\Tests\Unit; +use Auryn\Injector; use ItalyStrap\Config\ConfigFactory; -use ItalyStrap\Empress\Injector; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\Extension; diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index d2619d1..555f778 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -4,7 +4,7 @@ namespace ItalyStrap\Empress\Tests\Unit; -use ItalyStrap\Empress\Injector; +use Auryn\Injector; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\PhpFileProvider; use ItalyStrap\Empress\ProvidersCollection; diff --git a/tests/unit/ProxyInjectorTest.php b/tests/unit/ProxyInjectorTest.php index 0e527ea..88a4286 100644 --- a/tests/unit/ProxyInjectorTest.php +++ b/tests/unit/ProxyInjectorTest.php @@ -4,7 +4,7 @@ namespace ItalyStrap\Empress\Tests\Unit; -use ItalyStrap\Empress\Injector; +use Auryn\Injector; use Auryn\Test\PreparesImplementationTest; use ItalyStrap\Empress\Tests\UnitTestCase; use ProxyManager\Factory\LazyLoadingValueHolderFactory; From 1cfa56247ac9fb21a81cb6fc3af5979731f4d4a9 Mon Sep 17 00:00:00 2001 From: Enea Date: Thu, 30 Oct 2025 08:31:15 +0100 Subject: [PATCH 19/46] fix: PHP 8.4 compat and CS:FIX --- .gitattributes | 22 ++++++++++++++++++++++ example.php | 21 +++++++++++++++++---- src/AurynConfig.php | 2 +- src/ProvidersCollection.php | 4 ++-- tests/benchmarks/AurynConfigBench.php | 2 +- tests/unit/AurynConfigIntegrationTest.php | 2 +- tests/unit/AurynConfigTest.php | 8 ++++---- 7 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6740ef3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ + +## Directories to ignore when exporting the package +.docker/ export-ignore +.github/ export-ignore +tests/ export-ignore + +## Files to ignore when exporting the package +/.env export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/AGENTS.md export-ignore +/bone.json export-ignore +/codeception.* export-ignore +/codeception.yml export-ignore +/example.php export-ignore +/infection.json.dist export-ignore +/Makefile export-ignore +/phpbench.json export-ignore +/phpcs.xml export-ignore +/psalm.xml export-ignore +/index.php export-ignore +/rector.php export-ignore \ No newline at end of file diff --git a/example.php b/example.php index ac63a7f..09c3297 100644 --- a/example.php +++ b/example.php @@ -38,6 +38,11 @@ public function getClass(): stdClass return $this->class; } + public function getParam(): string + { + return $this->param; + } + public function execute(string $text): string { return $text; @@ -79,7 +84,8 @@ public function execute(string $text): string * Example: * class MyCLass( ConfigInterface $global_config, \stdClass $class ) {} * class MyOtherCLass( ConfigInterface $global_config, \stdClass $class ) {} - * A new Config instance will be shared, think of it like a singleton but more better and OOP oriented (You can mock it ;-)) + * A new Config instance will be shared, think of it like a singleton + * but better and OOP oriented (You can mock it ;-)) * The same instance of Config will be injected to MyCLass and MyOtherCLass * $injector->make(MyCLass::class); // Will have $global_config * $injector->make(MyOtherCLass::class); // Will have $global_config @@ -166,7 +172,7 @@ public function execute(string $text): string /** * You can delegate the instantiation of an object to a some kind of callable factory - * This will be always used to get the instance of a class. + * This will always be used to get the instance of a class. * @see [Instantiation Delegates](https://github.com/rdlowrey/auryn#instantiation-delegates) */ AurynConfig::DELEGATIONS => [ @@ -183,7 +189,7 @@ public function execute(string $text): string * Pass the $injector instance to the AurynConfig::class as first parameter and a * Config::class instance at the second parameters with the configuration array. */ -$app = new AurynConfig($injector, ConfigFactory::make($config)); +$app = new AurynConfig($injector, (new ConfigFactory())->make($config)); /** * Call the AurynConfig::resolve() method to do the autowiring of the application @@ -196,7 +202,14 @@ public function execute(string $text): string */ $example = $injector->make(Example::class); // $example instanceof Example::class +\var_dump( + $example instanceof Example + ? 'Yes, $example is an instance of Example::class' + : 'No, $example is NOT an instance of Example::class' +); +echo $example->execute('Hello World!'); +echo PHP_EOL; //$example2 = $injector->make( Example::class ); @@ -221,7 +234,7 @@ public function execute(string $text): string $app->extend( new class implements Extension { /** @var string */ - const YOUR_KEY = 'your-key'; + public const YOUR_KEY = 'your-key'; public function name(): string { diff --git a/src/AurynConfig.php b/src/AurynConfig.php index bc69e99..55f8fdf 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -53,7 +53,7 @@ class AurynConfig implements AurynConfigInterface public function __construct( Injector $injector, Config $dependencies, - ProxyFactoryInterface $proxyFactory = null + ?ProxyFactoryInterface $proxyFactory = null ) { $this->injector = $injector; $this->dependencies = $dependencies; diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index e3e0127..d50cbd6 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -21,12 +21,12 @@ class ProvidersCollection public function __construct( Injector $injector, ConfigInterface $config, - ProvidersCache $cache = null, + ?ProvidersCache $cache = null, iterable $providers = [] ) { $this->injector = $injector; $this->config = $config; - $this->cache = $cache ??= new ProvidersCache(); + $this->cache = $cache ?: new ProvidersCache(); $this->providers = $providers; } diff --git a/tests/benchmarks/AurynConfigBench.php b/tests/benchmarks/AurynConfigBench.php index 602b738..f0b49dc 100644 --- a/tests/benchmarks/AurynConfigBench.php +++ b/tests/benchmarks/AurynConfigBench.php @@ -19,7 +19,7 @@ class AurynConfigBench public function benchResolver() { $injector = new Injector(); - $config = ConfigFactory::make([ + $config = (new ConfigFactory())->make([ AurynConfig::SHARING => [ stdClass::class, ], diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php index 95d4f06..747abf7 100644 --- a/tests/unit/AurynConfigIntegrationTest.php +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -17,7 +17,7 @@ class AurynConfigIntegrationTest extends UnitTestCase { private function makeInstance(array $config = []): AurynConfig { - return new AurynConfig($this->realInjector, ConfigFactory::make($config), $this->realProxyFactory); + return new AurynConfig($this->realInjector, (new ConfigFactory())->make($config), $this->realProxyFactory); } public function testItShouldAlias(): void diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index ec56ac9..7042547 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -21,7 +21,7 @@ class AurynConfigTest extends UnitTestCase { protected function makeInstance(array $config = []): AurynConfig { - return new AurynConfig($this->makeInjector(), ConfigFactory::make($config), $this->makeProxyFactory()); + return new AurynConfig($this->makeInjector(), (new ConfigFactory())->make($config), $this->makeProxyFactory()); } // public function testItShouldProxy(): void @@ -332,7 +332,7 @@ public function __invoke(string $class, $index_or_optionName, Injector $injector public function testItShouldExtendClassString(): void { - $sut = new AurynConfig(new Injector(), ConfigFactory::make()); + $sut = new AurynConfig(new Injector(), (new ConfigFactory())->make()); $sut->extend(SomeExtension::class); $this->expectOutputString(SomeExtension::class); $sut->resolve(); @@ -340,7 +340,7 @@ public function testItShouldExtendClassString(): void public function testItShouldNotExtend(): void { - $sut = new AurynConfig(new Injector(), ConfigFactory::make()); + $sut = new AurynConfig(new Injector(), (new ConfigFactory())->make()); $this->expectException(\InvalidArgumentException::class); $sut->extend('SomeGenericClass'); } @@ -350,7 +350,7 @@ public function testOldClassNameShouldBeAliasedCorrectly(): void /** * New name is AurynConfig::class */ - $auryn_config = new \ItalyStrap\Empress\AurynResolver(new Injector(), ConfigFactory::make([])); + $auryn_config = new \ItalyStrap\Empress\AurynResolver(new Injector(), (new ConfigFactory())->make([])); $auryn_config->resolve(); } } From 82d47f2f1fd35b70a0abaee4b4f5fc8c4b8886c2 Mon Sep 17 00:00:00 2001 From: Enea Date: Thu, 30 Oct 2025 19:06:58 +0100 Subject: [PATCH 20/46] fix: Rector --- composer.json | 6 +- tests/_data/fixtures/fixtures.php | 756 ------------------ tests/_data/fixtures/fixtures_5_6.php | 24 - tests/benchmarks/AurynConfigBench.php | 4 +- tests/src/UnitTestCase.php | 2 +- tests/unit/AurynConfigTest.php | 36 +- tests/unit/PhpFileProviderTest.php | 2 +- .../ProvidersCollectionIntegrationTest.php | 68 +- tests/unit/ProvidersCollectionTest.php | 6 +- tests/unit/ProxyInjectorTest.php | 133 --- 10 files changed, 53 insertions(+), 984 deletions(-) delete mode 100644 tests/_data/fixtures/fixtures.php delete mode 100644 tests/_data/fixtures/fixtures_5_6.php delete mode 100644 tests/unit/ProxyInjectorTest.php diff --git a/composer.json b/composer.json index 1fd9595..ab7f4a5 100644 --- a/composer.json +++ b/composer.json @@ -63,11 +63,7 @@ ], "ItalyStrap\\Empress\\Tests\\Modules\\": "tests/_data/fixtures/modules/", "ItalyStrap\\Empress\\Tests\\Unit\\": "tests/unit/" - }, - "files": [ - "tests/_data/fixtures/fixtures.php", - "tests/_data/fixtures/fixtures_5_6.php" - ] + } }, "suggest": { "elazar/auryn-container-interop": "Only if you want to add a psr/container adapter, not required for this package", diff --git a/tests/_data/fixtures/fixtures.php b/tests/_data/fixtures/fixtures.php deleted file mode 100644 index 0319156..0000000 --- a/tests/_data/fixtures/fixtures.php +++ /dev/null @@ -1,756 +0,0 @@ -foo = $foo; - } -} - -class RequiresDependencyWithDefinedParam -{ - public $obj; - public function __construct(DependencyWithDefinedParam $obj) - { - $this->obj = $obj; - } -} - - -class ClassWithAliasAsParameter -{ - public $sharedClass; - - public function __construct(SharedClass $sharedClass) - { - $this->sharedClass = $sharedClass; - } -} - -class ConcreteClass1 -{ -} - -class ConcreteClass2 -{ -} - -class ClassWithoutMagicInvoke -{ -} - -class TestNoConstructor -{ -} - -class TestDependency -{ - public $testProp = 'testVal'; -} - -class TestDependency2 extends TestDependency -{ - public $testProp = 'testVal2'; -} - -class SpecdTestDependency extends TestDependency -{ - public $testProp = 'testVal'; -} - -class TestNeedsDep -{ - public function __construct(TestDependency $testDep) - { - $this->testDep = $testDep; - } -} - -class TestClassWithNoCtorTypehints -{ - public function __construct($val = 42) - { - $this->test = $val; - } -} - -class TestMultiDepsNeeded -{ - public function __construct(TestDependency $val1, TestDependency2 $val2) - { - $this->testDep = $val1; - $this->testDep = $val2; - } -} - - -class TestMultiDepsWithCtor -{ - public function __construct(TestDependency $val1, TestNeedsDep $val2) - { - $this->testDep = $val1; - $this->testDep = $val2; - } -} - -class NoTypehintNullDefaultConstructorClass -{ - public $testParam = 1; - public function __construct(TestDependency $val1, $arg = 42) - { - $this->testParam = $arg; - } -} - -class NoTypehintNoDefaultConstructorClass -{ - public $testParam = 1; - public function __construct(TestDependency $val1, $arg = null) - { - $this->testParam = $arg; - } -} - -interface DepInterface -{ -} -interface SomeInterface -{ -} -class SomeImplementation implements SomeInterface -{ -} -class PreparesImplementationTest implements SomeInterface -{ - public $testProp = 0; -} - -class DepImplementation implements DepInterface -{ - public $testProp = 'something'; -} - -class RequiresInterface -{ - public $dep; - public function __construct(DepInterface $dep) - { - $this->testDep = $dep; - } -} - -class ClassInnerA -{ - public $dep; - public function __construct(ClassInnerB $dep) - { - $this->dep = $dep; - } -} -class ClassInnerB -{ - public function __construct() - { - } -} -class ClassOuter -{ - public $dep; - public function __construct(ClassInnerA $dep) - { - $this->dep = $dep; - } -} - -class ProvTestNoDefinitionNullDefaultClass -{ - public function __construct($arg = null) - { - $this->arg = $arg; - } -} - -interface TestNoExplicitDefine -{ -} - -class InjectorTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine -{ - public $val = 42; - public function __construct($val) - { - $this->val = $val; - } -} - -class InjectorTestCtorParamWithNoTypehintOrDefaultDependent -{ - private $param; - public function __construct(TestNoExplicitDefine $param) - { - $this->param = $param; - } -} - -class InjectorTestRawCtorParams -{ - public $string; - public $obj; - public $int; - public $array; - public $float; - public $bool; - public $null; - - public function __construct($string, $obj, $int, $array, $float, $bool, $null) - { - $this->string = $string; - $this->obj = $obj; - $this->int = $int; - $this->array = $array; - $this->float = $float; - $this->bool = $bool; - $this->null = $null; - } -} - -class InjectorTestParentClass -{ - public function __construct($arg1) - { - $this->arg1 = $arg1; - } -} - -class InjectorTestChildClass extends InjectorTestParentClass -{ - public function __construct($arg1, $arg2) - { - parent::__construct($arg1); - $this->arg2 = $arg2; - } -} - -class CallableMock -{ - public function __invoke() - { - } -} - -class ProviderTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine -{ - public $val = 42; - public function __construct($val) - { - $this->val = $val; - } -} - -class ProviderTestCtorParamWithNoTypehintOrDefaultDependent -{ - private $param; - public function __construct(TestNoExplicitDefine $param) - { - $this->param = $param; - } -} - -class StringStdClassDelegateMock -{ - public function __invoke() - { - return $this->make(); - } - private function make() - { - $obj = new \StdClass(); - $obj->test = 42; - return $obj; - } -} - -class StringDelegateWithNoInvokeMethod -{ -} - -class ExecuteClassNoDeps -{ - public function execute() - { - return 42; - } -} - -class ExecuteClassDeps -{ - public function __construct(TestDependency $testDep) - { - } - public function execute() - { - return 42; - } -} - -class ExecuteClassDepsWithMethodDeps -{ - public function __construct(TestDependency $testDep) - { - } - public function execute(TestDependency $dep, $arg = null) - { - return isset($arg) ? $arg : 42; - } -} - -class ExecuteClassStaticMethod -{ - public static function execute() - { - return 42; - } -} - -class ExecuteClassRelativeStaticMethod extends ExecuteClassStaticMethod -{ - public static function execute() - { - return 'this should NEVER be seen since we are testing against parent::execute()'; - } -} - -class ExecuteClassInvokable -{ - public function __invoke() - { - return 42; - } -} - -function hasArrayDependency(array $parameter) -{ - return 42; -} - -function testExecuteFunction() -{ - return 42; -} - -function testExecuteFunctionWithArg(ConcreteClass1 $foo) -{ - return 42; -} - -class MadeByDelegate -{ -} - -class CallableDelegateClassTest -{ - public function __invoke() - { - return new MadeByDelegate(); - } -} - -interface DelegatableInterface -{ - public function foo(); -} - -class ImplementsInterface implements DelegatableInterface -{ - public function foo() - { - } -} - -class ImplementsInterfaceFactory -{ - public function __invoke() - { - return new ImplementsInterface(); - } -} - -class RequiresDelegatedInterface -{ - private $interface; - - public function __construct(DelegatableInterface $interface) - { - $this->interface = $interface; - } - public function foo() - { - $this->interface->foo(); - } -} - -class TestMissingDependency -{ - public function __construct(TypoInTypehint $class) - { - } -} - -class NonConcreteDependencyWithDefaultValue -{ - public $interface; - public function __construct(DelegatableInterface $i = null) - { - $this->interface = $i; - } -} - - -class ConcreteDependencyWithDefaultValue -{ - public $dependency; - public function __construct(\StdClass $instance = null) - { - $this->dependency = $instance; - } -} - -class TypelessParameterDependency -{ - public $thumbnailSize; - - public function __construct($thumbnailSize) - { - $this->thumbnailSize = $thumbnailSize; - } -} - -class RequiresDependencyWithTypelessParameters -{ - public $dependency; - - public function __construct(TypelessParameterDependency $dependency) - { - $this->dependency = $dependency; - } - - public function getThumbnailSize() - { - return $this->dependency->thumbnailSize; - } -} - -class HasNonPublicConstructor -{ - protected function __construct() - { - } -} - -class HasNonPublicConstructorWithArgs -{ - protected function __construct($arg1, $arg2, $arg3) - { - } -} - -class ClassWithCtor -{ - public function __construct() - { - } -} - -class TestDependencyWithProtectedConstructor -{ - protected function __construct() - { - } - - public static function create() - { - return new self(); - } -} - -class TestNeedsDepWithProtCons -{ - public function __construct(TestDependencyWithProtectedConstructor $dep) - { - $this->dep = $dep; - } -} - -class SimpleNoTypehintClass -{ - public $testParam = 1; - - public function __construct($arg) - { - $this->testParam = $arg; - } -} - -class SomeClassName -{ -} - -class TestDelegationSimple -{ - public $delgateCalled = false; -} - -class TestDelegationDependency -{ - public $delgateCalled = false; - public function __construct(TestDelegationSimple $testDelegationSimple) - { - } -} - -function createTestDelegationSimple() -{ - $instance = new TestDelegationSimple(); - $instance->delegateCalled = true; - - return $instance; -} - -function createTestDelegationDependency(TestDelegationSimple $testDelegationSimple) -{ - $instance = new TestDelegationDependency($testDelegationSimple); - $instance->delegateCalled = true; - - return $instance; -} - - -class BaseExecutableClass -{ - public function foo() - { - return 'This is the BaseExecutableClass'; - } - public static function bar() - { - return 'This is the BaseExecutableClass'; - } -} - -class ExtendsExecutableClass extends BaseExecutableClass -{ - public function foo() - { - return 'This is the ExtendsExecutableClass'; - } - public static function bar() - { - return 'This is the ExtendsExecutableClass'; - } -} - -class ReturnsCallable -{ - private $value = 'original'; - - public function __construct($value) - { - $this->value = $value; - } - - public function getCallable() - { - $callable = function () { - return $this->value; - }; - - return $callable; - } -} - -class DelegateClosureInGlobalScope -{ -} - -function getDelegateClosureInGlobalScope() -{ - return function () { - return new DelegateClosureInGlobalScope(); - }; -} - -class CloneTest -{ - public $injector; - public function __construct(\Auryn\Injector $injector) - { - $this->injector = clone $injector; - } -} - -abstract class AbstractExecuteTest -{ - public function process() - { - return "Abstract"; - } -} - -class ConcreteExexcuteTest extends AbstractExecuteTest -{ - public function process() - { - return "Concrete"; - } -} - -class DependencyChainTest -{ - public function __construct(DepInterface $dep) - { - } -} - -class ParentWithConstructor -{ - public $foo; - function __construct($foo) - { - $this->foo = $foo; - } -} - -class ChildWithoutConstructor extends ParentWithConstructor -{ -} - -class DelegateA -{ -} -class DelegatingInstanceA -{ - public function __construct(DelegateA $a) - { - $this->a = $a; - } -} - -class DelegateB -{ -} -class DelegatingInstanceB -{ - public function __construct(DelegateB $b) - { - $this->b = $b; - } -} - -class ThrowsExceptionInConstructor -{ - public function __construct() - { - throw new \Exception('Exception in constructor'); - } -} diff --git a/tests/_data/fixtures/fixtures_5_6.php b/tests/_data/fixtures/fixtures_5_6.php deleted file mode 100644 index 6888047..0000000 --- a/tests/_data/fixtures/fixtures_5_6.php +++ /dev/null @@ -1,24 +0,0 @@ -testParam = $arg; - } -} - -class TypehintNoDefaultConstructorVariadicClass -{ - public $testParam = 1; - public function __construct(TestDependency ...$arg) - { - $this->testParam = $arg; - } -} diff --git a/tests/benchmarks/AurynConfigBench.php b/tests/benchmarks/AurynConfigBench.php index f0b49dc..b258eb5 100644 --- a/tests/benchmarks/AurynConfigBench.php +++ b/tests/benchmarks/AurynConfigBench.php @@ -16,7 +16,7 @@ class AurynConfigBench * @Revs(1000) * @Iterations(5) */ - public function benchResolver() + public function benchResolver(): void { $injector = new Injector(); $config = (new ConfigFactory())->make([ @@ -36,7 +36,7 @@ public function benchResolver() * @Revs(1000) * @Iterations(5) */ - public function benchResolverP() + public function benchResolverP(): void { $injector = new Injector(); $injector->share(stdClass::class); diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index fdf6a9a..b5748da 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -31,7 +31,7 @@ protected function makeInjector(): Injector protected ProxyFactoryInterface $realProxyFactory; - protected ?ObjectProphecy $proxyFactory; + protected ?ObjectProphecy $proxyFactory = null; protected function makeProxyFactory(): ProxyFactoryInterface { diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index 7042547..e994794 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -52,7 +52,7 @@ public function testItShouldProxy01(): void $this->injector->proxy( Argument::type('string'), Argument::type('callable') - )->will(function ($args) use ($expected) { + )->will(function ($args) use ($expected): void { Assert::assertEquals($expected, $args[0], ''); }); @@ -86,7 +86,7 @@ public function shareProvider(): iterable public function testItShouldShare($expected): void { - $this->injector->share(Argument::any())->will(function ($args) use ($expected) { + $this->injector->share(Argument::any())->will(function ($args) use ($expected): void { Assert::assertEquals($expected, $args[0], ''); }); @@ -106,7 +106,7 @@ public function testItShouldAlias(): void $this->injector ->alias(Argument::type('string'), Argument::type('string')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertEquals('InterfaceName', $args[0], ''); Assert::assertEquals('ClassName', $args[1], ''); }); @@ -127,7 +127,7 @@ public function testItShouldDefine(): void $this->injector ->define(Argument::type('string'), Argument::type('array')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertEquals('ClassName', $args[0], ''); Assert::assertArrayHasKey(':config', $args[1], ''); }); @@ -154,7 +154,7 @@ public function testItShouldDefineParam(): void $this->injector ->defineParam(Argument::type('string'), Argument::any()) - ->will(function ($args) use ($param_expected) { + ->will(function ($args) use ($param_expected): void { Assert::assertEquals(':config', $args[0], ''); Assert::assertEquals($param_expected, $args[1], ''); }); @@ -173,14 +173,12 @@ public function testItShouldDefineParam(): void public function testItShouldDelegate(): void { - $factory_delegation = function () { - return new class { - }; + $factory_delegation = fn(): object => new class { }; $this->injector ->delegate(Argument::type('string'), Argument::any()) - ->will(function ($args) use ($factory_delegation) { + ->will(function ($args) use ($factory_delegation): void { Assert::assertEquals(':config', $args[0], ''); Assert::assertEquals($factory_delegation, $args[1], ''); Assert::assertIsCallable($args[1], ''); @@ -200,7 +198,7 @@ public function testItShouldDelegate(): void public function testItShouldPrepare(): void { - $preparation_callback = function ($class, $injector) { + $preparation_callback = function ($class, $injector): void { Assert::assertEquals('ClassName', $class, ''); Assert::assertInstanceOf(Injector::class, $injector, ''); }; @@ -209,7 +207,7 @@ public function testItShouldPrepare(): void $this->injector ->prepare(Argument::type('string'), Argument::any()) - ->will(function ($args) use ($preparation_callback, $test) { + ->will(function ($args) use ($preparation_callback, $test): void { Assert::assertEquals('ClassName', $args[0], ''); Assert::assertEquals($preparation_callback, $args[1], ''); Assert::assertIsCallable($args[1], ''); @@ -241,7 +239,7 @@ public function testItShouldWalk(): void ] ); - $sut->walk('Test', function (string $value, $key) { + $sut->walk('Test', function (string $value, $key): void { Assert::assertStringContainsString($value, 'ClassName', ''); Assert::assertStringContainsString($key, 'Key', ''); }); @@ -253,12 +251,12 @@ public function testItShouldExtendFakeClass(): void $this ->injector ->share(Argument::type('string'), Argument::any()) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); $this->injector->make(Argument::type('string'), Argument::type('array')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); @@ -275,7 +273,7 @@ public function testItShouldExtendFakeClass(): void $extension->name()->willReturn('ExtensionName'); - $extension->execute(Argument::exact($sut))->will(function ($args) { + $extension->execute(Argument::exact($sut))->will(function ($args): void { }); $sut->extend($extension->reveal()); @@ -287,12 +285,12 @@ public function testItShouldExtendRealClass(): void { $this->injector->share(Argument::type('string'), Argument::any()) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); $this->injector->make(Argument::type('string'), Argument::type('array')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); @@ -314,12 +312,12 @@ public function name(): string return self::SUBSCRIBERS; } - public function execute(AurynConfigInterface $application) + public function execute(AurynConfigInterface $application): void { $application->walk(self::SUBSCRIBERS, $this); } - public function __invoke(string $class, $index_or_optionName, Injector $injector) + public function __invoke(string $class, $index_or_optionName, Injector $injector): void { Assert::assertStringContainsString($class, 'ClassName', ''); $injector->share($class); diff --git a/tests/unit/PhpFileProviderTest.php b/tests/unit/PhpFileProviderTest.php index c6b26e2..b0c013f 100644 --- a/tests/unit/PhpFileProviderTest.php +++ b/tests/unit/PhpFileProviderTest.php @@ -15,7 +15,7 @@ protected function makeInstance(): PhpFileProvider return new PhpFileProvider('pattern', $this->makeFinder()); } - public function testShouldBeInvokable() + public function testShouldBeInvokable(): void { $file = \codecept_data_dir('fixtures/config/autoload/config.global.php'); diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index 555f778..50be8a2 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -32,15 +32,13 @@ private function makeInstance(): ProvidersCollection ->make() ->in(codecept_data_dir('fixtures')) ), - function (): array { - return [ - AurynConfig::ALIASES => [ - self::CONFIG_KEY_2 => 'array config', - ], - AurynConfig::SHARING => [ - ], - ]; - }, + fn(): array => [ + AurynConfig::ALIASES => [ + self::CONFIG_KEY_2 => 'array config', + ], + AurynConfig::SHARING => [ + ], + ], function (): iterable { yield [ AurynConfig::ALIASES => [ @@ -48,42 +46,34 @@ function (): iterable { ], ]; }, - function (): array { - return [ - AurynConfig::ALIASES => [ - 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\GlobalDispatcher", - 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\SubscriberRegister", - 'ItalyStrap\View\ViewInterface' => "ItalyStrap\View\View", - 15 => 'value', - ], - ]; - }, - function (): array { - return [ - AurynConfig::ALIASES => [ - 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\DifferentDispatcher", - 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\DifferentRegister", - 'ItalyStrap\HTML\TagInterface' => "ItalyStrap\HTML\Tag", - 15 => 'newValue', - ], - ]; - }, + fn(): array => [ + AurynConfig::ALIASES => [ + 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\GlobalDispatcher", + 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\SubscriberRegister", + 'ItalyStrap\View\ViewInterface' => "ItalyStrap\View\View", + 15 => 'value', + ], + ], + fn(): array => [ + AurynConfig::ALIASES => [ + 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\DifferentDispatcher", + 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\DifferentRegister", + 'ItalyStrap\HTML\TagInterface' => "ItalyStrap\HTML\Tag", + 15 => 'newValue', + ], + ], ModuleStub1::class, [ModuleStub1::class, '__invoke'], - function (): array { - return require \codecept_data_dir('fixtures/config/test.global.php'); - }, - function (): array { - return [ - 'config_cache_enabled' => true, - 'cache_config_path' => $this->cachedConfigFile, - ]; - }, + fn(): array => require \codecept_data_dir('fixtures/config/test.global.php'), + fn(): array => [ + 'config_cache_enabled' => true, + 'cache_config_path' => $this->cachedConfigFile, + ], ], ); } - public function testIntegration() + public function testIntegration(): void { $sut = $this->makeInstance(); $sut->build(); diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index 1fa7039..608d226 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -21,7 +21,7 @@ private function makeInstance(): ProvidersCollection ); } - public function testShouldBeInstantiable() + public function testShouldBeInstantiable(): void { $this->config @@ -41,9 +41,7 @@ public function testShouldBeInstantiable() $this->config ->get('config_cache_filemode', Argument::type('int')) - ->will(function ($args): int { - return (int)$args[1]; - }); + ->will(fn($args): int => (int)$args[1]); $this->config ->get('cache_config_path', Argument::type('null')) diff --git a/tests/unit/ProxyInjectorTest.php b/tests/unit/ProxyInjectorTest.php deleted file mode 100644 index 88a4286..0000000 --- a/tests/unit/ProxyInjectorTest.php +++ /dev/null @@ -1,133 +0,0 @@ -expectException(\Auryn\ConfigException::class); - $injector->proxy('1', 'string'); - } - - public function testInstanceProxy() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\TestDependency', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $class = $injector->make('Auryn\Test\TestDependency'); - - $this->assertInstanceOf('Auryn\Test\TestDependency', $class, ''); - $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $class, ''); - $this->assertEquals('testVal', $class->testProp, ''); - } - - public function testMakeInstanceInjectsSimpleConcreteDependencyProxy() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\TestDependency', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $need_dep = $injector->make('Auryn\Test\TestNeedsDep'); - - $this->assertInstanceOf('Auryn\Test\TestNeedsDep', $need_dep, ''); - } - - public function testShareInstanceProxy() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\TestDependency', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $injector->share('Auryn\Test\TestDependency'); - $class = $injector->make('Auryn\Test\TestDependency'); - $class2 = $injector->make('Auryn\Test\TestDependency'); - - $this->assertEquals($class, $class2, ''); - } - - public function testProxyMakeInstanceReturnsAliasInstanceOnNonConcreteTypehint() - { - $injector = new Injector(); - $injector->alias('Auryn\Test\DepInterface', 'Auryn\Test\DepImplementation'); - $injector->proxy( - 'Auryn\Test\DepInterface', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $object = $injector->make('Auryn\Test\DepInterface'); - - $this->assertInstanceOf('Auryn\Test\DepInterface', $object, ''); - $this->assertInstanceOf('Auryn\Test\DepImplementation', $object, ''); - $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $object, ''); - } - - public function testProxyPrepare() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\PreparesImplementationTest', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $injector->prepare( - 'Auryn\Test\PreparesImplementationTest', - function (PreparesImplementationTest $obj, $injector) { - $obj->testProp = 42; - } - ); - $obj = $injector->make('Auryn\Test\PreparesImplementationTest'); - - $this->assertSame(42, $obj->testProp); - } -} From 2e02d75187f2c2c7ea8685ec5057c419ab3f1991 Mon Sep 17 00:00:00 2001 From: Enea Date: Thu, 30 Oct 2025 19:21:41 +0100 Subject: [PATCH 21/46] fix: CS:FIX --- composer.json | 5 ++++- example.php | 2 +- phpcs.xml | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index ab7f4a5..26f0bfc 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,10 @@ "@php vendor/bin/phpcs -p" ], "cs:fix": [ - "@php vendor/bin/phpcbf -p" + "echo Fixing the package", + "@php vendor/bin/phpcbf -p", + "echo Fixing example.php", + "@php vendor/bin/phpcbf example.php" ], "psalm": [ "@php ./vendor/bin/psalm --no-cache" diff --git a/example.php b/example.php index 09c3297..8aa3ee7 100644 --- a/example.php +++ b/example.php @@ -13,7 +13,7 @@ use ItalyStrap\Empress\Injector; use stdClass; -require_once __DIR__ . '/vendor/autoload.php'; +require_once __DIR__ . '/vendor/autoload.php'; // phpcs:ignore PSR1.Files.SideEffects class Example { diff --git a/phpcs.xml b/phpcs.xml index b76ad9e..4e098e4 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,7 +14,6 @@ ./src/ ./tests/ - example.php namespace-bc-aliases.php From dc5a5d6abe91b8c3b6ca75d927535e211567546a Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 31 Oct 2025 20:20:32 +0100 Subject: [PATCH 22/46] fix: try to fix tests --- .github/workflows/test.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79f6004..ba5b9c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: name: Test on PHP ${{ matrix.php }} runs-on: ubuntu-latest - continue-on-error: ${{ contains('8.1,8.2', matrix.php) }} + continue-on-error: ${{ contains('8.1,8.2,8.5', matrix.php) }} if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" strategy: diff --git a/composer.json b/composer.json index 26f0bfc..1bd1d5b 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "webimpress/safe-writer": "^2.2" }, "require-dev": { - "lucatume/wp-browser": "^3.0", + "lucatume/wp-browser": "<3.5", "lucatume/function-mocker-le": "^1.0.1", "codeception/module-asserts": "^1.0", "phpspec/prophecy-phpunit": "^2.0", From 5d73f2316d411b224852b9c1a9bb6f2b243d8a9c Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 2 Nov 2025 19:22:23 +0100 Subject: [PATCH 23/46] chore: introduces PSR-11 Container --- src/Container.php | 45 ++++++++++++++++++++++++++++++++++ tests/src/UnitTestCase.php | 5 ++++ tests/unit/ComtainerTest.php | 47 ++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/Container.php create mode 100644 tests/unit/ComtainerTest.php diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..f313bdc --- /dev/null +++ b/src/Container.php @@ -0,0 +1,45 @@ +injector = $injector; + } + + public function get(string $id) + { + if (!$this->has($id)) { + throw new class ("Service '$id' not found") extends \Exception implements NotFoundExceptionInterface { + }; + } + + return $this->injector->make($id); + } + + public function has(string $id): bool + { + if (\class_exists($id)) { + return true; + } + + return $this->injectorHas($id); + } + + private function injectorHas(string $id): bool + { + $details = $this->injector->inspect($id, 31); + return (bool) \array_filter($details); + } +} diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index b5748da..0e7ee23 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -22,6 +22,11 @@ class UnitTestCase extends Unit protected Injector $realInjector; + protected function makeRealInjector(): Injector + { + return $this->realInjector; + } + protected ObjectProphecy $injector; protected function makeInjector(): Injector diff --git a/tests/unit/ComtainerTest.php b/tests/unit/ComtainerTest.php new file mode 100644 index 0000000..5771b39 --- /dev/null +++ b/tests/unit/ComtainerTest.php @@ -0,0 +1,47 @@ +makeRealInjector()); + } + + public function testHasReturnsTrueForExistingClass(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->has(SomeConcrete::class)); + } + + public function testHasReturnsFalseForNonExistingService(): void + { + $sut = $this->makeInstance(); + $this->assertFalse($sut->has('non.existing.service')); + } + + public function testGetThrowsNotFoundExceptionForNonExistingService(): void + { + $sut = $this->makeInstance(); + + $this->expectException(NotFoundExceptionInterface::class); + + $sut->get('non.existing.service'); + } + + public function testGetReturnsInstanceForExistingClass(): void + { + $sut = $this->makeInstance(); + $instance = $sut->get(SomeConcrete::class); + $this->assertInstanceOf(SomeConcrete::class, $instance); + } +} From 86d2776c8fabf0421ddcab33ef080ed3ce416ccc Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 3 May 2026 16:36:36 +0200 Subject: [PATCH 24/46] chore: add psr/container and rector/swiss-knife to dependencies --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1bd1d5b..7d70b56 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "friendsofphp/proxy-manager-lts": "^1.0", "italystrap/config": "^2.10", "overclokk/auryn": "dev-master", - "webimpress/safe-writer": "^2.2" + "webimpress/safe-writer": "^2.2", + "psr/container": "^1.1" }, "require-dev": { "lucatume/wp-browser": "<3.5", @@ -43,7 +44,8 @@ "italystrap/debug": "dev-master", "italystrap/finder": "dev-master", "laminas/laminas-config-aggregator": "^1.9", - "crellbar/prophecy-extensions": "^1.1" + "crellbar/prophecy-extensions": "^1.1", + "rector/swiss-knife": "^2.3" }, "autoload": { "psr-4": { From b4c309ca0e5d882526375be63fca357416eaa16c Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 3 May 2026 21:52:16 +0200 Subject: [PATCH 25/46] feat: implement custom PSR-11 exceptions and enhance error handling in container --- src/Container.php | 14 ++++++++++---- src/ContainerException.php | 11 +++++++++++ src/NotFoundException.php | 11 +++++++++++ tests/unit/ComtainerTest.php | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/ContainerException.php create mode 100644 src/NotFoundException.php diff --git a/src/Container.php b/src/Container.php index f313bdc..bffc6c8 100644 --- a/src/Container.php +++ b/src/Container.php @@ -6,7 +6,6 @@ use Auryn\Injector; use Psr\Container\ContainerInterface; -use Psr\Container\NotFoundExceptionInterface; final class Container implements ContainerInterface { @@ -21,11 +20,18 @@ public function __construct( public function get(string $id) { if (!$this->has($id)) { - throw new class ("Service '$id' not found") extends \Exception implements NotFoundExceptionInterface { - }; + throw new NotFoundException(\sprintf("Service '%s' not found", $id)); } - return $this->injector->make($id); + try { + return $this->injector->make($id); + } catch (\Throwable $throwable) { + throw new ContainerException( + \sprintf("Error while retrieving service '%s'", $id), + 0, + $throwable + ); + } } public function has(string $id): bool diff --git a/src/ContainerException.php b/src/ContainerException.php new file mode 100644 index 0000000..2c81822 --- /dev/null +++ b/src/ContainerException.php @@ -0,0 +1,11 @@ +get('non.existing.service'); } + public function testGetNotFoundExceptionIsAlsoContainerException(): void + { + $sut = $this->makeInstance(); + + try { + $sut->get('non.existing.service'); + $this->fail('Expected a not found exception'); + } catch (NotFoundExceptionInterface $exception) { + $this->assertInstanceOf(ContainerExceptionInterface::class, $exception); + $this->assertSame("Service 'non.existing.service' not found", $exception->getMessage()); + } + } + public function testGetReturnsInstanceForExistingClass(): void { $sut = $this->makeInstance(); $instance = $sut->get(SomeConcrete::class); $this->assertInstanceOf(SomeConcrete::class, $instance); } + + public function testGetWrapsAurynResolutionErrorsInContainerException(): void + { + $sut = $this->makeInstance(); + + try { + $sut->get(ConcreteNeedsSomeInterface::class); + $this->fail('Expected a container exception'); + } catch (ContainerExceptionInterface $exception) { + $this->assertNotInstanceOf(NotFoundExceptionInterface::class, $exception); + $this->assertStringContainsString( + ConcreteNeedsSomeInterface::class, + $exception->getMessage() + ); + $this->assertNotNull($exception->getPrevious()); + } + } } From 484c4afc0c0ea3cc1bf2346ce33107f28c68d580 Mon Sep 17 00:00:00 2001 From: Enea Date: Tue, 5 May 2026 07:27:06 +0200 Subject: [PATCH 26/46] feat: enhance `ProvidersCollection` with new aggregation logic, improved error handling, and caching capabilities in tests and core --- src/ProvidersCollection.php | 159 ++++++-- .../ProvidersCollectionIntegrationTest.php | 42 +- tests/unit/ProvidersCollectionTest.php | 380 ++++++++++++++++-- 3 files changed, 505 insertions(+), 76 deletions(-) diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index d50cbd6..948dc1c 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -5,78 +5,132 @@ namespace ItalyStrap\Empress; use Auryn\Injector; -use Auryn\InjectionException; use ItalyStrap\Config\ConfigInterface; +use ItalyStrap\Config\NodeManipulationInterface; /** - * @psalm-api + * @phpstan-type Provider callable|class-string|array{0: class-string|object, 1: non-empty-string}|object + * @phpstan-type Configuration array */ -class ProvidersCollection +final class ProvidersCollection { - private ConfigInterface $config; private Injector $injector; - private ProvidersCache $cache; + private ConfigInterface $config; + private ProvidersCacheInterface $cache; + /** + * @var iterable + */ private iterable $providers; + /** + * @param ConfigInterface&NodeManipulationInterface $config + * @param iterable $providers + */ public function __construct( Injector $injector, ConfigInterface $config, - ?ProvidersCache $cache = null, + ?ProvidersCacheInterface $cache = null, iterable $providers = [] ) { + if (!$config instanceof NodeManipulationInterface) { + throw new \InvalidArgumentException(\sprintf( + '$config must implement %s', + NodeManipulationInterface::class + )); + } + $this->injector = $injector; $this->config = $config; $this->cache = $cache ?: new ProvidersCache(); $this->providers = $providers; } - public function build(): void + public function aggregate(): void { if ($this->cache->read($this->config)) { return; } $result = []; - /** @var array $subArray */ - foreach ($this->loadCollectionFromProviders() as $subArray) { - $this->processCollections($subArray, $result); + $appendSections = []; + foreach ($this->loadConfigurationsFromProviders() as $configuration) { + $this->mergeConfiguration($configuration, $result, $appendSections); } $this->config->merge($result); + foreach ($appendSections as $key => $value) { + $this->config->appendTo($key, $value); + } if ((bool)$this->config->get(ProvidersCacheInterface::ENABLE_CACHE, false)) { $this->cache->write($this->config); } } - private function processCollections(array $subArray, array &$result): void - { - foreach ($subArray as $key => $value) { - if (!array_key_exists($key, $result)) { - $result[$key] = []; + /** + * @param Configuration $configuration + * @param array $result + * @param array $appendSections + */ + private function mergeConfiguration( + array $configuration, + array &$result, + array &$appendSections + ): void { + foreach ($configuration as $key => $value) { + if ($this->isAppendSection((string)$key)) { + $appendSections[$key] = $this->uniqueValues(\array_merge( + (array)($appendSections[$key] ?? []), + (array)$value + )); + continue; } if (!is_array($value)) { - /** @psalm-suppress MixedAssignment */ $result[$key] = $value; continue; } - $result[$key] = \array_merge((array)$result[$key], $value); + $current = []; + + if (array_key_exists($key, $result) && is_array($result[$key])) { + $current = $result[$key]; + } + + $result[$key] = \array_replace_recursive($current, $value); } } - private function loadCollectionFromProviders(): \Generator + private function isAppendSection(string $key): bool + { + return \in_array($key, [ + AurynConfig::PROXY, + AurynConfig::SHARING, + ], true); + } + + /** + * @param array $values + * @return array + */ + private function uniqueValues(array $values): array + { + return \array_values(\array_unique($values, \SORT_REGULAR)); + } + + /** + * @return \Generator + */ + private function loadConfigurationsFromProviders(): \Generator { - /** @var object|array|class-string $provider */ foreach ($this->providers as $provider) { try { $result = $this->injector->execute($provider); - } catch (InjectionException | \Throwable $e) { + } catch (\Throwable $e) { throw new \ErrorException( \sprintf( 'An error occurred when executing %s: %s', - is_object($provider) ? get_class($provider) : gettype($provider), + $this->providerName($provider), $e->getMessage() ), 0, @@ -87,27 +141,72 @@ private function loadCollectionFromProviders(): \Generator ); } - if ($result instanceof \Generator) { - yield from $result; + if (\is_array($result)) { + yield $result; continue; } - if (!\is_array($result)) { + if ($result instanceof \Traversable) { + yield from $this->validateIterableConfiguration($result, $provider); + continue; + } + + throw new \RuntimeException( + \sprintf( + 'The provider %s must return an array or iterable of arrays, %s given', + $this->providerName($provider), + \gettype($result) + ) + ); + } + } + + /** + * @param \Traversable $configuration + * @param mixed $provider + * @return \Generator + */ + private function validateIterableConfiguration(\Traversable $configuration, $provider): \Generator + { + foreach ($configuration as $item) { + if (!\is_array($item)) { throw new \RuntimeException( \sprintf( - 'The provider %s must return an array or a Generator, %s given', - is_object($provider) ? get_class($provider) : gettype($provider), - \gettype($result) + 'The provider %s yielded %s, expected array', + $this->providerName($provider), + \gettype($item) ) ); } - yield $result; + yield $item; } } - public function collection(): ConfigInterface + /** + * @param mixed $provider + */ + private function providerName($provider): string { - return $this->config; + if (\is_object($provider)) { + return \get_class($provider); + } + + if (\is_array($provider)) { + $target = $provider[0] ?? 'unknown'; + $method = $provider[1] ?? 'unknown'; + + if (\is_object($target)) { + $target = \get_class($target); + } + + return \sprintf('%s::%s', (string)$target, (string)$method); + } + + if (\is_string($provider)) { + return $provider; + } + + return \gettype($provider); } } diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index 50be8a2..fc8f3be 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -6,13 +6,17 @@ use Auryn\Injector; use ItalyStrap\Empress\AurynConfig; +use ItalyStrap\Empress\ModuleInterface; use ItalyStrap\Empress\PhpFileProvider; use ItalyStrap\Empress\ProvidersCollection; +use ItalyStrap\Empress\Tests\ConcreteNeedsSomeInterface; +use ItalyStrap\Empress\Tests\SomeInterface; use ItalyStrap\Finder\FinderFactory; use ItalyStrap\Empress\Tests\Modules\ModuleStub1; use ItalyStrap\Empress\Tests\UnitTestCase; +use Psr\Container\ContainerInterface; -class ProvidersCollectionIntegrationTest extends UnitTestCase +final class ProvidersCollectionIntegrationTest extends UnitTestCase { public const CONFIG_KEY_1 = 'Test alias should be override by local config'; public const CONFIG_KEY_2 = 'Iterable below should override this'; @@ -32,21 +36,21 @@ private function makeInstance(): ProvidersCollection ->make() ->in(codecept_data_dir('fixtures')) ), - fn(): array => [ + static fn(): array => [ AurynConfig::ALIASES => [ self::CONFIG_KEY_2 => 'array config', ], AurynConfig::SHARING => [ ], ], - function (): iterable { + static function (): iterable { yield [ AurynConfig::ALIASES => [ self::CONFIG_KEY_2 => 'iterable config', ], ]; }, - fn(): array => [ + static fn(): array => [ AurynConfig::ALIASES => [ 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\GlobalDispatcher", 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\SubscriberRegister", @@ -54,7 +58,7 @@ function (): iterable { 15 => 'value', ], ], - fn(): array => [ + static fn(): array => [ AurynConfig::ALIASES => [ 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\DifferentDispatcher", 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\DifferentRegister", @@ -64,6 +68,20 @@ function (): iterable { ], ModuleStub1::class, [ModuleStub1::class, '__invoke'], + new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::DELEGATIONS => [ + ConcreteNeedsSomeInterface::class + => static function (ContainerInterface $container): ConcreteNeedsSomeInterface { + $some = $container->get(SomeInterface::class); + return new ConcreteNeedsSomeInterface($some); + } + ], + ]; + } + }, fn(): array => require \codecept_data_dir('fixtures/config/test.global.php'), fn(): array => [ 'config_cache_enabled' => true, @@ -76,11 +94,12 @@ function (): iterable { public function testIntegration(): void { $sut = $this->makeInstance(); - $sut->build(); + $sut->aggregate(); + $config = $this->makeConfigReal(); $this->assertSame( 'local config', - $sut->collection()->get(\implode('.', [ + $config->get(\implode('.', [ AurynConfig::ALIASES, self::CONFIG_KEY_1, ])) @@ -88,7 +107,7 @@ public function testIntegration(): void $this->assertSame( 'iterable config', - $sut->collection()->get(\implode('.', [ + $config->get(\implode('.', [ AurynConfig::ALIASES, self::CONFIG_KEY_2, ])) @@ -96,7 +115,7 @@ public function testIntegration(): void $this->assertSame( 'test.global.php', - $sut->collection()->get(\implode('.', [ + $config->get(\implode('.', [ AurynConfig::ALIASES, self::CONFIG_KEY_3, ])) @@ -108,9 +127,6 @@ public function testIntegration(): void $file = require $this->cachedConfigFile; $this->assertIsArray($file); - /** - * \array_merge() will append the value if the kew is numeric - */ - $this->assertCount(10, $sut->collection()->get(AurynConfig::ALIASES), 'Should be 10'); + $this->assertCount(9, $config->get(AurynConfig::ALIASES), 'Should be 9'); } } diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index 608d226..a4b2577 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -4,58 +4,372 @@ namespace ItalyStrap\Empress\Tests\Unit; +use Auryn\Injector; +use Auryn\Test\SharedAliasedInterface; +use Auryn\Test\SharedClass; +use ItalyStrap\Config\Config; +use ItalyStrap\Config\ConfigInterface; +use ItalyStrap\Config\NodeManipulationInterface; +use ItalyStrap\Empress\AurynConfig; +use ItalyStrap\Empress\ProvidersCacheInterface; use ItalyStrap\Empress\ProvidersCollection; +use ItalyStrap\Empress\Tests\Modules\ModuleStub1; use ItalyStrap\Empress\Tests\UnitTestCase; -use Prophecy\Argument; -class ProvidersCollectionTest extends UnitTestCase +final class ProvidersCollectionTest extends UnitTestCase { - private function makeInstance(): ProvidersCollection - { + private function makeInstance( + iterable $providers = [], + ?ProvidersCacheInterface $cache = null, + ?ConfigInterface $config = null + ): ProvidersCollection { return new ProvidersCollection( - $this->makeInjector(), - $this->makeConfig(), - null, + new Injector(), + $config ?: new Config(), + $cache, + $providers + ); + } + + public function testAggregatePopulatesConfig(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + static fn(): array => [ + AurynConfig::ALIASES => [ + 'InterfaceName' => 'ClassName', + ], + ], + ], null, $config); + + $sut->aggregate(); + + $this->assertSame('ClassName', $config->get(AurynConfig::ALIASES . '.InterfaceName')); + } + + public function testConstructorRequiresConfigWithNodeManipulationSupport(): void + { + $config = $this->prophesize(ConfigInterface::class)->reveal(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(NodeManipulationInterface::class); + + new ProvidersCollection(new Injector(), $config); + } + + public function testAggregateAppendsListSectionsAndReplacesMapSectionsRecursively(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + static fn(): array => [ + AurynConfig::PROXY => [ + 'FirstProxy', + 'SecondProxy', + ], + AurynConfig::SHARING => [ + 'FirstShared', + 'SecondShared', + ], + AurynConfig::ALIASES => [ + 'InterfaceName' => 'FirstClass', + 15 => 'first numeric value', + ], + AurynConfig::DEFINITIONS => [ + 'ServiceName' => [ + ':overridden' => 'first', + ':kept' => 'kept', + ], + ], + ], + static fn(): array => [ + AurynConfig::PROXY => [ + 'SecondProxy', + 'FirstProxy', + ], + AurynConfig::SHARING => [ + 'SecondShared', + 'FirstShared', + ], + AurynConfig::ALIASES => [ + 'InterfaceName' => 'SecondClass', + 15 => 'second numeric value', + ], + AurynConfig::DEFINITIONS => [ + 'ServiceName' => [ + ':overridden' => 'second', + ], + ], + ], + ], null, $config); + + $sut->aggregate(); + + $this->assertSame([ + 'FirstProxy', + 'SecondProxy', + ], $config->get(AurynConfig::PROXY)); + $this->assertSame([ + 'FirstShared', + 'SecondShared', + ], $config->get(AurynConfig::SHARING)); + $this->assertSame('SecondClass', $config->get(AurynConfig::ALIASES . '.InterfaceName')); + $this->assertSame('second numeric value', $config->get([AurynConfig::ALIASES, 15])); + $this->assertSame( [ - ] + ':overridden' => 'second', + ':kept' => 'kept', + ], + $config->get(AurynConfig::DEFINITIONS . '.ServiceName') ); } - public function testShouldBeInstantiable(): void + public function testAggregateUsesAppendToForListSections(): void { + $config = new class extends Config { + /** + * @var array + */ + public array $appendCalls = []; + + public function appendTo($key, $value): bool + { + $this->appendCalls[] = [$key, $value]; + + return parent::appendTo($key, $value); + } + }; - $this->config - ->merge(Argument::cetera()) - ->shouldBeCalledTimes(1); + $sut = $this->makeInstance([ + static fn(): array => [ + AurynConfig::PROXY => [ + 'FirstProxy', + ], + AurynConfig::SHARING => [ + 'FirstShared', + ], + ], + static fn(): array => [ + AurynConfig::PROXY => [ + 'SecondProxy', + ], + AurynConfig::SHARING => [ + 'SecondShared', + ], + ], + ], null, $config); + + $sut->aggregate(); + + $this->assertSame([ + [ + AurynConfig::PROXY, + [ + 'FirstProxy', + 'SecondProxy', + ], + ], + [ + AurynConfig::SHARING, + [ + 'FirstShared', + 'SecondShared', + ], + ], + ], $config->appendCalls); + } + + public function testAggregateAcceptsTraversableProvidersYieldingConfigurationArrays(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + static function (): \Generator { + yield [ + AurynConfig::SHARING => [ + 'FirstShared', + ], + ]; + + yield [ + AurynConfig::SHARING => [ + 'SecondShared', + ], + ]; + }, + ], null, $config); + + $sut->aggregate(); + + $this->assertSame([ + 'FirstShared', + 'SecondShared', + ], $config->get(AurynConfig::SHARING)); + } + + public function testAggregateAcceptsInvokableObjectProviders(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + new class { + public function __invoke(): array + { + return [ + 'custom' => [ + 'key' => 'value', + ], + ]; + } + }, + ], null, $config); + + $sut->aggregate(); + + $this->assertSame('value', $config->get('custom.key')); + } + + public function testAggregateAcceptsClassStringAndArrayCallableProviders(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + ModuleStub1::class, + [ModuleStub1::class, '__invoke'], + ], null, $config); - $this->config - ->toArray() - ->shouldBeCalledTimes(1) - ->willReturn([ + $sut->aggregate(); + + $this->assertSame( + SharedClass::class, + $config->get([AurynConfig::ALIASES, SharedAliasedInterface::class]) + ); + } + + public function testAggregateDoesNotExecuteProvidersWhenCacheReadSucceeds(): void + { + $cache = new class implements ProvidersCacheInterface { + public bool $written = false; + + public function read(ConfigInterface $config): bool + { + $config->merge([ + 'from_cache' => true, + ]); + + return true; + } + + public function write(ConfigInterface $config): void + { + $this->written = true; + } + }; + + $config = new Config(); + $sut = $this->makeInstance([ + static function (): array { + throw new \RuntimeException('Provider should not be executed'); + }, + ], $cache, $config); + + $sut->aggregate(); + + $this->assertTrue($config->get('from_cache')); + $this->assertFalse($cache->written); + } + + public function testAggregateWritesCacheWhenEnabledByProviders(): void + { + $cache = new class implements ProvidersCacheInterface { + /** + * @var array + */ + public array $writtenConfig = []; + + public function read(ConfigInterface $config): bool + { + return false; + } + + public function write(ConfigInterface $config): void + { + $this->writtenConfig = $config->toArray(); + } + }; + + $sut = $this->makeInstance([ + static fn(): array => [ + ProvidersCacheInterface::ENABLE_CACHE => true, + 'key' => 'value', + ], + ], $cache); + + $sut->aggregate(); + + $this->assertSame('value', $cache->writtenConfig['key']); + } + + public function testAggregateDoesNotWriteCacheWhenCacheIsDisabled(): void + { + $cache = new class implements ProvidersCacheInterface { + public bool $written = false; + + public function read(ConfigInterface $config): bool + { + return false; + } + + public function write(ConfigInterface $config): void + { + $this->written = true; + } + }; + + $sut = $this->makeInstance([ + static fn(): array => [ 'key' => 'value', - ]); + ], + ], $cache); + + $sut->aggregate(); - $this->config - ->get('config_cache_enabled', false) - ->willReturn(true); + $this->assertFalse($cache->written); + } - $this->config - ->get('config_cache_filemode', Argument::type('int')) - ->will(fn($args): int => (int)$args[1]); + public function testAggregateThrowsWhenProviderReturnsInvalidResult(): void + { + $sut = $this->makeInstance([ + static fn(): string => 'invalid', + ]); - $this->config - ->get('cache_config_path', Argument::type('null')) - ->willReturn($this->cachedConfigFile); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('must return an array or iterable of arrays'); - $sut = $this->makeInstance(); - $sut->build(); + $sut->aggregate(); + } + + public function testAggregateThrowsWhenProviderYieldsInvalidResult(): void + { + $sut = $this->makeInstance([ + static function (): \Generator { + yield 'invalid'; + }, + ]); - $this->assertFileExists($this->cachedConfigFile); - $this->assertFileIsReadable($this->cachedConfigFile); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('yielded string, expected array'); + + $sut->aggregate(); + } + + public function testAggregateWrapsProviderExecutionErrors(): void + { + $sut = $this->makeInstance([ + static function (): array { + throw new \RuntimeException('Provider failed'); + }, + ]); - $file = require $this->cachedConfigFile; + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage('Provider failed'); - $this->assertIsArray($file); - $this->assertArrayHasKey('key', $file); + $sut->aggregate(); } } From 24f2f24b10bf1f3b97a569e0eb2e6f8f689ab221 Mon Sep 17 00:00:00 2001 From: Enea Date: Tue, 5 May 2026 08:52:47 +0200 Subject: [PATCH 27/46] feat: add `ProvidersCache` unit tests and improve cache handling with stricter validations and error wrapping --- src/ProvidersCache.php | 29 ++++- src/ProvidersCacheInterface.php | 6 + tests/unit/ProvidersCacheTest.php | 205 ++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 tests/unit/ProvidersCacheTest.php diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index cc09718..1ed07fe 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -14,7 +14,7 @@ /** * @psalm-api */ -class ProvidersCache implements ProvidersCacheInterface +final class ProvidersCache implements ProvidersCacheInterface { private const CACHE_TEMPLATE = <<<'EOT' merge((array)require $cachedConfigFile); + $config->merge($this->loadCacheFile($cachedConfigFile)); return true; } @@ -87,7 +85,26 @@ private function writeCache(string $cachedConfigFile, string $contents, int $mod try { FileWriter::writeFile($cachedConfigFile, $contents, $mode); } catch (FileWriterException $e) { - // ignore errors writing cache file + throw new \ErrorException('Configuration cache cannot be written', 0, 1, __FILE__, __LINE__, $e); } } + + /** + * @return array + */ + private function loadCacheFile(string $cachedConfigFile): array + { + try { + /** @psalm-suppress UnresolvableInclude */ + $config = require $cachedConfigFile; + } catch (\Throwable $e) { + throw new \ErrorException('Configuration cache cannot be read', 0, 1, __FILE__, __LINE__, $e); + } + + if (!\is_array($config)) { + throw new \ErrorException('Configuration cache must return an array'); + } + + return $config; + } } diff --git a/src/ProvidersCacheInterface.php b/src/ProvidersCacheInterface.php index a3dedf0..fc037a5 100644 --- a/src/ProvidersCacheInterface.php +++ b/src/ProvidersCacheInterface.php @@ -4,6 +4,8 @@ namespace ItalyStrap\Empress; +use ItalyStrap\Config\ConfigInterface; + interface ProvidersCacheInterface { public const ENABLE_CACHE = 'config_cache_enabled'; @@ -11,4 +13,8 @@ interface ProvidersCacheInterface public const CACHE_FILEMODE = 'config_cache_filemode'; public const CACHE_PATH = 'cache_config_path'; + + public function read(ConfigInterface $config): bool; + + public function write(ConfigInterface $config): void; } diff --git a/tests/unit/ProvidersCacheTest.php b/tests/unit/ProvidersCacheTest.php new file mode 100644 index 0000000..176d399 --- /dev/null +++ b/tests/unit/ProvidersCacheTest.php @@ -0,0 +1,205 @@ + + */ + private array $pathsToRemove = []; + + // phpcs:ignore -- Method from Codeception + protected function _after(): void { + foreach (\array_reverse($this->pathsToRemove) as $path) { + if (\is_file($path)) { + \unlink($path); + continue; + } + + if (\is_dir($path)) { + \rmdir($path); + } + } + + parent::_after(); + } + + public function testReadReturnsFalseWhenNoCachePathIsConfigured(): void + { + $sut = new ProvidersCache(); + + $this->assertFalse($sut->read(new Config())); + } + + public function testReadReturnsFalseWhenCachePathDoesNotExist(): void + { + $sut = new ProvidersCache($this->cacheFile('missing-cache.php')); + + $this->assertFalse($sut->read(new Config())); + } + + public function testReadReturnsFalseWhenCachePathIsNotAFile(): void + { + $directory = $this->cacheDirectory('cache-directory'); + $sut = new ProvidersCache($directory); + + $this->assertFalse($sut->read(new Config())); + } + + public function testReadMergesArrayReturnedByCacheFile(): void + { + $file = $this->writeCacheFile('read-cache.php', <<<'PHP' + 'value', + 'nested' => [ + 'key' => 'nested-value', + ], +]; +PHP); + $config = new Config(); + $sut = new ProvidersCache($file); + + $this->assertTrue($sut->read($config)); + $this->assertSame('value', $config->get('key')); + $this->assertSame('nested-value', $config->get('nested.key')); + } + + public function testReadUsesConfigCachePathBeforeConstructorPath(): void + { + $constructorFile = $this->writeCacheFile('constructor-cache.php', <<<'PHP' + 'constructor', +]; +PHP); + $configFile = $this->writeCacheFile('config-cache-override.php', <<<'PHP' + 'config', +]; +PHP); + $config = new Config([ + ProvidersCacheInterface::CACHE_PATH => $configFile, + ]); + $sut = new ProvidersCache($constructorFile); + + $this->assertTrue($sut->read($config)); + $this->assertSame('config', $config->get('source')); + } + + public function testReadThrowsWhenCacheFileDoesNotReturnArray(): void + { + $file = $this->writeCacheFile('non-array-cache.php', <<<'PHP' +expectException(\ErrorException::class); + $this->expectExceptionMessage('Configuration cache must return an array'); + + $sut->read(new Config()); + } + + public function testReadWrapsErrorsThrownByCacheFile(): void + { + $file = $this->writeCacheFile('throwing-cache.php', <<<'PHP' +read(new Config()); + $this->fail('Expected cache read failure'); + } catch (\ErrorException $exception) { + $this->assertSame('Configuration cache cannot be read', $exception->getMessage()); + $this->assertInstanceOf(\RuntimeException::class, $exception->getPrevious()); + $this->assertSame('Broken cache file', $exception->getPrevious()->getMessage()); + } + } + + public function testWriteReturnsWhenNoCachePathIsConfigured(): void + { + $sut = new ProvidersCache(); + + $sut->write(new Config([ + 'key' => 'value', + ])); + + $this->assertTrue(true); + } + + public function testWriteCreatesReadableCacheFile(): void + { + $file = $this->cacheFile('written-cache.php'); + $this->pathsToRemove[] = $file; + $config = new Config([ + ProvidersCacheInterface::CACHE_PATH => $file, + 'key' => 'value', + ]); + $sut = new ProvidersCache(); + + $sut->write($config); + + $this->assertFileExists($file); + $this->assertFileIsReadable($file); + $cachedConfig = require $file; + + $this->assertIsArray($cachedConfig); + $this->assertSame('value', $cachedConfig['key']); + } + + public function testWriteThrowsWhenCacheFileCannotBeWritten(): void + { + $config = new Config([ + ProvidersCacheInterface::CACHE_PATH => $this->cacheFile('missing-directory/cache.php'), + ]); + $sut = new ProvidersCache(); + + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage('Configuration cache cannot be written'); + + $sut->write($config); + } + + private function cacheFile(string $name): string + { + return \codecept_output_dir($name); + } + + private function cacheDirectory(string $name): string + { + $directory = \codecept_output_dir($name); + + if (!\is_dir($directory)) { + \mkdir($directory); + } + + $this->pathsToRemove[] = $directory; + return $directory; + } + + private function writeCacheFile(string $name, string $contents): string + { + $file = $this->cacheFile($name); + \file_put_contents($file, $contents); + $this->pathsToRemove[] = $file; + + return $file; + } +} From f6c842a1ca7ce3948cb50845ca538cfd85ec9b8f Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 6 May 2026 06:49:11 +0200 Subject: [PATCH 28/46] refactor: rename `resolve()` to `apply()` and deprecate `resolve()`, mark test classes as `final`, and add support for `factories` in method mappings --- src/AurynConfig.php | 10 ++++++++- src/AurynConfigInterface.php | 2 +- tests/unit/AurynConfigIntegrationTest.php | 8 +++---- tests/unit/AurynConfigTest.php | 26 +++++++++++------------ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/AurynConfig.php b/src/AurynConfig.php index 55f8fdf..b47a9ba 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -22,6 +22,7 @@ class AurynConfig implements AurynConfigInterface public const DEFINITIONS = 'definitions'; public const DEFINE_PARAM = 'define_param'; public const DELEGATIONS = 'delegations'; + public const FACTORIES = 'factories'; public const PREPARATIONS = 'preparations'; private const METHODS = [ @@ -31,6 +32,7 @@ class AurynConfig implements AurynConfigInterface self::DEFINITIONS => 'define', self::DEFINE_PARAM => 'defineParam', self::DELEGATIONS => 'delegate', + self::FACTORIES => 'delegate', self::PREPARATIONS => 'prepare', ]; @@ -60,7 +62,7 @@ public function __construct( $this->proxy_factory = $proxyFactory ?? new ProxyFactory(); } - public function resolve(): void + public function apply(): void { /** @@ -84,6 +86,12 @@ public function resolve(): void } } + #[\Deprecated(message: 'Use apply() instead', since: '2.0.0')] + public function resolve(): void + { + $this->apply(); + } + public function extend(...$extensions): void { foreach ($extensions as $extension) { diff --git a/src/AurynConfigInterface.php b/src/AurynConfigInterface.php index 739d8f0..96c82ba 100644 --- a/src/AurynConfigInterface.php +++ b/src/AurynConfigInterface.php @@ -12,7 +12,7 @@ interface AurynConfigInterface /** * @return void */ - public function resolve(); + public function apply(); /** * @param class-string|Extension ...$extensions diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php index 747abf7..d619122 100644 --- a/tests/unit/AurynConfigIntegrationTest.php +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -13,7 +13,7 @@ use ItalyStrap\Empress\Tests\UnitTestCase; use Prophecy\Argument; -class AurynConfigIntegrationTest extends UnitTestCase +final class AurynConfigIntegrationTest extends UnitTestCase { private function makeInstance(array $config = []): AurynConfig { @@ -30,7 +30,7 @@ public function testItShouldAlias(): void ] ); - $aurynConfig->resolve(); + $aurynConfig->apply(); $this->assertInstanceOf(SomeConcrete::class, $this->realInjector->make(SomeInterface::class)); $this->assertInstanceOf(SomeConcrete::class, $this->realInjector->make(SomeConcrete::class)); @@ -50,7 +50,7 @@ public function testItShouldShare(): void ] ); - $aurynConfig->resolve(); + $aurynConfig->apply(); $shared = $this->realInjector->make(SomeConcrete::class); $this->assertSame($shared, $this->realInjector->make(SomeConcrete::class)); @@ -77,7 +77,7 @@ public function render(): string ] ); - $sut->resolve(); + $sut->apply(); /** @var SomeConcrete $concrete */ $concrete = $this->realInjector->make(SomeConcrete::class); diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index e994794..370a3ce 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -17,7 +17,7 @@ use PHPUnit\Framework\Assert; use Prophecy\Argument; -class AurynConfigTest extends UnitTestCase +final class AurynConfigTest extends UnitTestCase { protected function makeInstance(array $config = []): AurynConfig { @@ -39,7 +39,7 @@ protected function makeInstance(array $config = []): AurynConfig // ] // ); // -// $sut->resolve(); +// $sut->$this->apply(); // // $concrete = $this->realInjector->make(SomeConcrete::class); // } @@ -64,7 +64,7 @@ public function testItShouldProxy01(): void ] ); - $sut->resolve(); + $sut->apply(); } public function shareProvider(): iterable @@ -98,7 +98,7 @@ public function testItShouldShare($expected): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldAlias(): void @@ -119,7 +119,7 @@ public function testItShouldAlias(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldDefine(): void @@ -143,7 +143,7 @@ public function testItShouldDefine(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldDefineParam(): void @@ -167,7 +167,7 @@ public function testItShouldDefineParam(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldDelegate(): void @@ -192,7 +192,7 @@ public function testItShouldDelegate(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldPrepare(): void @@ -226,7 +226,7 @@ public function testItShouldPrepare(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldWalk(): void @@ -278,7 +278,7 @@ public function testItShouldExtendFakeClass(): void $sut->extend($extension->reveal()); - $sut->resolve(); + $sut->apply(); } public function testItShouldExtendRealClass(): void @@ -325,7 +325,7 @@ public function __invoke(string $class, $index_or_optionName, Injector $injector } }); - $sut->resolve(); + $sut->apply(); } public function testItShouldExtendClassString(): void @@ -333,7 +333,7 @@ public function testItShouldExtendClassString(): void $sut = new AurynConfig(new Injector(), (new ConfigFactory())->make()); $sut->extend(SomeExtension::class); $this->expectOutputString(SomeExtension::class); - $sut->resolve(); + $sut->apply(); } public function testItShouldNotExtend(): void @@ -349,6 +349,6 @@ public function testOldClassNameShouldBeAliasedCorrectly(): void * New name is AurynConfig::class */ $auryn_config = new \ItalyStrap\Empress\AurynResolver(new Injector(), (new ConfigFactory())->make([])); - $auryn_config->resolve(); + $auryn_config->apply(); } } From 00493744238be7bbae4ae398ca5c64cfb5092cf2 Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 6 May 2026 06:52:54 +0200 Subject: [PATCH 29/46] feat: add `ContainerBuilder` and `ModuleInterface` with comprehensive unit tests, supporting provider aggregation, extensions, caching, and proxy factories --- src/ContainerBuilder.php | 89 ++++++ src/ModuleInterface.php | 10 + .../src/ContainerBuilderExtensionStub.php | 27 ++ tests/unit/ContainerBuilderTest.php | 278 ++++++++++++++++++ .../{ComtainerTest.php => ContainerTest.php} | 2 +- tests/unit/PhpFileProviderTest.php | 2 +- 6 files changed, 406 insertions(+), 2 deletions(-) create mode 100644 src/ContainerBuilder.php create mode 100644 src/ModuleInterface.php create mode 100644 tests/_data/fixtures/src/ContainerBuilderExtensionStub.php create mode 100644 tests/unit/ContainerBuilderTest.php rename tests/unit/{ComtainerTest.php => ContainerTest.php} (98%) diff --git a/src/ContainerBuilder.php b/src/ContainerBuilder.php new file mode 100644 index 0000000..eceeba2 --- /dev/null +++ b/src/ContainerBuilder.php @@ -0,0 +1,89 @@ + + */ + private array $providers = []; + + /** + * @var array + */ + private array $extensions = []; + + public function __construct( + ?Injector $injector = null, + ?ConfigInterface $config = null, + ?ProvidersCacheInterface $cache = null, + ?ProxyFactoryInterface $proxyFactory = null + ) { + $this->injector = $injector ?: new Injector(); + $this->config = $config ?: new Config(); + $this->cache = $cache; + $this->proxyFactory = $proxyFactory; + } + + /** + * @param callable|class-string|array{0: class-string|object, 1: non-empty-string}|object $provider + */ + public function addProvider($provider): self + { + $this->providers[] = $provider; + + return $this; + } + + /** + * @param callable|class-string|array{0: class-string|object, 1: non-empty-string}|object $module + */ + public function addModule($module): self + { + return $this->addProvider($module); + } + + /** + * @param class-string|Extension ...$extensions + */ + public function extend(...$extensions): self + { + $this->extensions = $extensions; + return $this; + } + + public function build(): ContainerInterface + { + $injector = $this->injector; + $injector->share($injector); + + $container = new Container($injector); + $injector->alias(ContainerInterface::class, \get_class($container)); + $injector->share($container); + + $providersCollection = new ProvidersCollection($injector, $this->config, $this->cache, $this->providers); + $providersCollection->aggregate(); + + $injectorConfig = new AurynConfig($injector, $this->config, $this->proxyFactory); + $injectorConfig->extend(...$this->extensions); + $injectorConfig->apply(); + + return $container; + } +} diff --git a/src/ModuleInterface.php b/src/ModuleInterface.php new file mode 100644 index 0000000..12f9d58 --- /dev/null +++ b/src/ModuleInterface.php @@ -0,0 +1,10 @@ +walk( + 'container_builder_test_aliases', + static function (string $alias, string $typeHint, Injector $injector): void { + $injector->alias($typeHint, $alias); + } + ); + } +} diff --git a/tests/unit/ContainerBuilderTest.php b/tests/unit/ContainerBuilderTest.php new file mode 100644 index 0000000..697b139 --- /dev/null +++ b/tests/unit/ContainerBuilderTest.php @@ -0,0 +1,278 @@ +addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + AurynConfig::SHARING => [ + SomeConcrete::class, + ], + ]; + } + }); + + $this->assertSame($builder, $result); + + $container = $builder->build(); + + $this->assertInstanceOf(ContainerInterface::class, $container); + $this->assertSame($container, $container->get(ContainerInterface::class)); + + $service = $container->get(SomeInterface::class); + $this->assertInstanceOf(SomeConcrete::class, $service); + $this->assertSame('SomeConcrete', $service->render()); + } + + public function testAddModuleIsFluentAliasForAddProvider(): void + { + $builder = new ContainerBuilder(); + + $result = $builder->addModule(static fn(): array => [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]); + + $this->assertSame($builder, $result); + $this->assertInstanceOf(SomeConcrete::class, $builder->build()->get(SomeInterface::class)); + } + + public function testExtendIsFluentAndAppliesExtensionInstance(): void + { + $extension = new class implements Extension { + public bool $executed = false; + + public function name(): string + { + return self::class; + } + + public function execute(AurynConfigInterface $application): void + { + $this->executed = true; + } + }; + $builder = new ContainerBuilder(); + + $result = $builder->extend($extension); + $builder->build(); + + $this->assertSame($builder, $result); + $this->assertTrue($extension->executed); + } + + public function testExtendAcceptsExtensionClassString(): void + { + $config = new Config([ + 'container_builder_test_aliases' => [ + SomeInterface::class => SomeConcrete::class, + ], + ]); + $builder = new ContainerBuilder(null, $config); + + $builder->extend(ContainerBuilderExtensionStub::class); + $container = $builder->build(); + + $this->assertInstanceOf(SomeConcrete::class, $container->get(SomeInterface::class)); + } + + public function testBuildMergesMultipleModulesAndSharesAliasedService(): void + { + $builder = new ContainerBuilder(); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]; + } + }); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::SHARING => [ + SomeConcrete::class, + ], + ]; + } + }); + + $container = $builder->build(); + + $first = $container->get(SomeInterface::class); + $second = $container->get(SomeConcrete::class); + + $this->assertInstanceOf(SomeConcrete::class, $first); + $this->assertSame($first, $second, 'Aliased service should be shared across resolutions'); + } + + public function testBuildDelegationsCanRequestContainerInterface(): void + { + $builder = new ContainerBuilder(); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]; + } + }); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::DELEGATIONS => [ + ConcreteNeedsSomeInterface::class + => static function (ContainerInterface $container): ConcreteNeedsSomeInterface { + $some = $container->get(SomeInterface::class); + return new ConcreteNeedsSomeInterface($some); + } + ], + ]; + } + }); + + $container = $builder->build(); + + $service = $container->get(ConcreteNeedsSomeInterface::class); + $this->assertInstanceOf( + ConcreteNeedsSomeInterface::class, + $service, + 'Service should be an instance of ConcreteNeedsSomeInterface' + ); + $this->assertInstanceOf( + SomeConcrete::class, + $service->someInterface(), + 'Dependency should be an instance of SomeConcrete' + ); + } + + public function testBuildUsesCustomInjectorAndSharesIt(): void + { + $injector = new Injector(); + $builder = new ContainerBuilder($injector); + + $container = $builder->build(); + + $this->assertSame($injector, $injector->make(Injector::class)); + $this->assertSame($container, $injector->make(ContainerInterface::class)); + } + + public function testBuildPopulatesCustomConfig(): void + { + $config = new Config(); + $builder = new ContainerBuilder(null, $config); + + $builder->addProvider(static fn(): array => [ + 'custom' => [ + 'key' => 'value', + ], + ]); + + $builder->build(); + + $this->assertSame('value', $config->get('custom.key')); + } + + public function testBuildPassesCustomCacheToProvidersCollection(): void + { + $cache = new class implements ProvidersCacheInterface { + public bool $written = false; + + public function read(ConfigInterface $config): bool + { + $config->merge([ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]); + + return true; + } + + public function write(ConfigInterface $config): void + { + $this->written = true; + } + }; + $builder = new ContainerBuilder(null, null, $cache); + $builder->addProvider(static function (): array { + throw new \RuntimeException('Provider should not be executed when cache is warm'); + }); + + $container = $builder->build(); + + $this->assertInstanceOf(SomeConcrete::class, $container->get(SomeInterface::class)); + $this->assertFalse($cache->written); + } + + public function testBuildPassesCustomProxyFactoryToAurynConfig(): void + { + $factory = new class implements ProxyFactoryInterface { + public bool $called = false; + + public function __invoke(string $className, callable $callback): object + { + $this->called = true; + + return new class extends SomeConcrete { + public function render(): string + { + return 'ProxiedConcrete'; + } + }; + } + }; + $builder = new ContainerBuilder(null, null, null, $factory); + $builder->addProvider(static fn(): array => [ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]); + + /** @var SomeConcrete $service */ + $service = $builder->build()->get(SomeConcrete::class); + + $this->assertTrue($factory->called); + $this->assertSame('ProxiedConcrete', $service->render()); + } +} diff --git a/tests/unit/ComtainerTest.php b/tests/unit/ContainerTest.php similarity index 98% rename from tests/unit/ComtainerTest.php rename to tests/unit/ContainerTest.php index f90d656..968f1e7 100644 --- a/tests/unit/ComtainerTest.php +++ b/tests/unit/ContainerTest.php @@ -11,7 +11,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; -final class ComtainerTest extends UnitTestCase +final class ContainerTest extends UnitTestCase { private function makeInstance(): Container { diff --git a/tests/unit/PhpFileProviderTest.php b/tests/unit/PhpFileProviderTest.php index b0c013f..9af6995 100644 --- a/tests/unit/PhpFileProviderTest.php +++ b/tests/unit/PhpFileProviderTest.php @@ -8,7 +8,7 @@ use ItalyStrap\Empress\Tests\UnitTestCase; use PHPUnit\Framework\Assert; -class PhpFileProviderTest extends UnitTestCase +final class PhpFileProviderTest extends UnitTestCase { protected function makeInstance(): PhpFileProvider { From 9788a335f45189ca8f93e21760c717d66230daec Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 6 May 2026 07:32:21 +0200 Subject: [PATCH 30/46] refactor: migrate from Psalm to PHPStan for static analysis, remove psalm.xml, and update workflow and composer scripts accordingly --- .gitattributes | 4 ++-- .github/workflows/static-analysis.yml | 4 ++-- composer.json | 8 ++++---- phpcs.xml | 1 + psalm.xml | 18 ------------------ src/AurynConfig.php | 9 +++------ src/AurynConfigInterface.php | 3 --- src/ContainerBuilder.php | 11 +++++++++++ src/PhpFileProvider.php | 4 ---- src/ProvidersCache.php | 6 +----- src/ProvidersCollection.php | 3 +++ src/ProxyFactory.php | 15 +++++---------- tests/benchmarks/AurynConfigBench.php | 2 +- 13 files changed, 33 insertions(+), 55 deletions(-) delete mode 100644 psalm.xml diff --git a/.gitattributes b/.gitattributes index 6740ef3..4c3840c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,6 +17,6 @@ tests/ export-ignore /Makefile export-ignore /phpbench.json export-ignore /phpcs.xml export-ignore -/psalm.xml export-ignore +/phpstan.neon.dist export-ignore /index.php export-ignore -/rector.php export-ignore \ No newline at end of file +/rector.php export-ignore diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 41d71af..d786a95 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -22,5 +22,5 @@ jobs: - uses: ramsey/composer-install@v2 - - name: Psalm - run: composer run psalm \ No newline at end of file + - name: PHPStan + run: composer run stan diff --git a/composer.json b/composer.json index 7d70b56..1a1cea7 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "phpcompatibility/php-compatibility": "^9.3", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "vimeo/psalm": "^5.6", + "phpstan/phpstan": "^1.12", "phpbench/phpbench": "^1.2", "phpmetrics/phpmetrics": "^2.8", @@ -81,8 +81,8 @@ "echo Fixing example.php", "@php vendor/bin/phpcbf example.php" ], - "psalm": [ - "@php ./vendor/bin/psalm --no-cache" + "stan": [ + "@php ./vendor/bin/phpstan analyse --debug --no-progress" ], "unit": [ "@php vendor/bin/codecept run unit" @@ -110,7 +110,7 @@ ], "qa": [ "@cs", - "@psalm", + "@stan", "@unit" ] }, diff --git a/phpcs.xml b/phpcs.xml index 4e098e4..573d4e0 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -21,4 +21,5 @@ */vendor/* */tests/_support/* + */tests/_output/* diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index eef60d0..0000000 --- a/psalm.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - diff --git a/src/AurynConfig.php b/src/AurynConfig.php index b47a9ba..5f070f8 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -11,9 +11,6 @@ use function array_walk; -/** - * @psalm-api - */ class AurynConfig implements AurynConfigInterface { public const PROXY = 'proxies'; @@ -86,7 +83,9 @@ public function apply(): void } } - #[\Deprecated(message: 'Use apply() instead', since: '2.0.0')] + /** + * @deprecated Use apply() instead. + */ public function resolve(): void { $this->apply(); @@ -129,7 +128,6 @@ public function walk(string $key, callable $callback): void * @param mixed $nameOrInstance * @param int $index * @throws ConfigException - * @psalm-suppress PossiblyUnusedParam */ protected function share($nameOrInstance, int $index): void { @@ -140,7 +138,6 @@ protected function share($nameOrInstance, int $index): void * @param string $name * @param int $index * @throws ConfigException - * @psalm-suppress PossiblyUnusedParam */ protected function proxy(string $name, int $index): void { diff --git a/src/AurynConfigInterface.php b/src/AurynConfigInterface.php index 96c82ba..6834879 100644 --- a/src/AurynConfigInterface.php +++ b/src/AurynConfigInterface.php @@ -4,9 +4,6 @@ namespace ItalyStrap\Empress; -/** - * @psalm-api - */ interface AurynConfigInterface { /** diff --git a/src/ContainerBuilder.php b/src/ContainerBuilder.php index eceeba2..66fdb3c 100644 --- a/src/ContainerBuilder.php +++ b/src/ContainerBuilder.php @@ -7,12 +7,16 @@ use Auryn\Injector; use ItalyStrap\Config\Config; use ItalyStrap\Config\ConfigInterface; +use ItalyStrap\Config\NodeManipulationInterface; use Psr\Container\ContainerInterface; class ContainerBuilder { private Injector $injector; + /** + * @var ConfigInterface&NodeManipulationInterface + */ private ConfigInterface $config; private ?ProvidersCacheInterface $cache; @@ -35,6 +39,13 @@ public function __construct( ?ProvidersCacheInterface $cache = null, ?ProxyFactoryInterface $proxyFactory = null ) { + if ($config instanceof ConfigInterface && !$config instanceof NodeManipulationInterface) { + throw new \InvalidArgumentException(\sprintf( + '$config must implement %s', + NodeManipulationInterface::class + )); + } + $this->injector = $injector ?: new Injector(); $this->config = $config ?: new Config(); $this->cache = $cache; diff --git a/src/PhpFileProvider.php b/src/PhpFileProvider.php index 8c7371b..41c7f76 100644 --- a/src/PhpFileProvider.php +++ b/src/PhpFileProvider.php @@ -6,9 +6,6 @@ use ItalyStrap\Finder\FinderInterface; -/** - * @psalm-api - */ final class PhpFileProvider { private string $pattern; @@ -34,7 +31,6 @@ public function __invoke(): \Generator * @var \SplFileInfo $file */ foreach ($this->finder as $file) { - /** @psalm-suppress UnresolvableInclude */ yield include $file; } } diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index 1ed07fe..5563ca4 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -11,9 +11,6 @@ use Webimpress\SafeWriter\Exception\ExceptionInterface as FileWriterException; use Webimpress\SafeWriter\FileWriter; -/** - * @psalm-api - */ final class ProvidersCache implements ProvidersCacheInterface { private const CACHE_TEMPLATE = <<<'EOT' @@ -65,7 +62,7 @@ public function write( try { $contents = sprintf( self::CACHE_TEMPLATE, - static::class, + self::class, // Write an alternative to date('c') (new DateTimeImmutable('now'))->format('c'), VarExporter::export( @@ -95,7 +92,6 @@ private function writeCache(string $cachedConfigFile, string $contents, int $mod private function loadCacheFile(string $cachedConfigFile): array { try { - /** @psalm-suppress UnresolvableInclude */ $config = require $cachedConfigFile; } catch (\Throwable $e) { throw new \ErrorException('Configuration cache cannot be read', 0, 1, __FILE__, __LINE__, $e); diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index 948dc1c..129578e 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -15,6 +15,9 @@ final class ProvidersCollection { private Injector $injector; + /** + * @var ConfigInterface&NodeManipulationInterface + */ private ConfigInterface $config; private ProvidersCacheInterface $cache; /** diff --git a/src/ProxyFactory.php b/src/ProxyFactory.php index 653acc4..047d14e 100644 --- a/src/ProxyFactory.php +++ b/src/ProxyFactory.php @@ -13,22 +13,17 @@ */ class ProxyFactory implements ProxyFactoryInterface { - /** - * @psalm-suppress ArgumentTypeCoercion - */ public function __invoke(string $className, callable $callback): VirtualProxyInterface { - /** @psalm-suppress MixedArgumentTypeCoercion */ return (new LazyLoadingValueHolderFactory())->createProxy( $className, static function ( - ?object &$object, - ?object $proxy, - string $method, - array $parameters, - ?Closure &$initializer + ?object &$object = null, + ?object $proxy = null, + string $method = '', + array $parameters = [], + ?Closure &$initializer = null ) use ($callback): bool { - /** @psalm-suppress MixedAssignment */ $object = $callback(); $initializer = null; return true; diff --git a/tests/benchmarks/AurynConfigBench.php b/tests/benchmarks/AurynConfigBench.php index b258eb5..358fab1 100644 --- a/tests/benchmarks/AurynConfigBench.php +++ b/tests/benchmarks/AurynConfigBench.php @@ -26,7 +26,7 @@ public function benchResolver(): void ]); $resolver = new AurynConfig($injector, $config); - $resolver->resolve(); + $resolver->apply(); $class = $injector->make(stdClass::class); } From 0b18a094ee249d3e8996bec0592aa0af9e69d14c Mon Sep 17 00:00:00 2001 From: Enea Date: Wed, 6 May 2026 07:44:17 +0200 Subject: [PATCH 31/46] chore: add generic type annotations across core classes and interfaces for improved static analysis compatibility --- src/AurynConfig.php | 8 ++++- src/ContainerBuilder.php | 9 ++++-- src/ModuleInterface.php | 3 ++ src/ProvidersCache.php | 54 +++++++++++++++++++++++++++------ src/ProvidersCacheInterface.php | 6 ++++ src/ProvidersCollection.php | 8 ++--- src/ProxyFactory.php | 3 ++ src/ProxyFactoryInterface.php | 3 ++ 8 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/AurynConfig.php b/src/AurynConfig.php index 5f070f8..d77706c 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -35,6 +35,9 @@ class AurynConfig implements AurynConfigInterface private Injector $injector; + /** + * @var Config + */ private Config $dependencies; private ProxyFactoryInterface $proxy_factory; @@ -49,6 +52,9 @@ class AurynConfig implements AurynConfigInterface */ private array $extensionsClasses = []; + /** + * @param Config $dependencies + */ public function __construct( Injector $injector, Config $dependencies, @@ -155,7 +161,7 @@ protected function alias(string $alias, string $typeHint): void } /** - * @param array $class_args + * @param array $class_args * @param string $class_name */ protected function define(array $class_args, string $class_name): void diff --git a/src/ContainerBuilder.php b/src/ContainerBuilder.php index 66fdb3c..9f11360 100644 --- a/src/ContainerBuilder.php +++ b/src/ContainerBuilder.php @@ -15,7 +15,7 @@ class ContainerBuilder private Injector $injector; /** - * @var ConfigInterface&NodeManipulationInterface + * @var ConfigInterface&NodeManipulationInterface */ private ConfigInterface $config; @@ -33,6 +33,9 @@ class ContainerBuilder */ private array $extensions = []; + /** + * @param ConfigInterface|null $config + */ public function __construct( ?Injector $injector = null, ?ConfigInterface $config = null, @@ -47,7 +50,9 @@ public function __construct( } $this->injector = $injector ?: new Injector(); - $this->config = $config ?: new Config(); + /** @var ConfigInterface&NodeManipulationInterface $configInstance */ + $configInstance = $config ?: new Config(); + $this->config = $configInstance; $this->cache = $cache; $this->proxyFactory = $proxyFactory; } diff --git a/src/ModuleInterface.php b/src/ModuleInterface.php index 12f9d58..98dbcb6 100644 --- a/src/ModuleInterface.php +++ b/src/ModuleInterface.php @@ -6,5 +6,8 @@ interface ModuleInterface { + /** + * @return iterable + */ public function __invoke(): iterable; } diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index 5563ca4..1eb34ed 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -31,11 +31,13 @@ public function __construct(?string $file = null) $this->file = $file; } - public function read( - ConfigInterface $config - ): bool { + /** + * @param ConfigInterface $config + */ + public function read(ConfigInterface $config): bool + { - $cachedConfigFile = (string)$config->get(self::CACHE_PATH, $this->file); + $cachedConfigFile = $this->stringValue($config->get(self::CACHE_PATH, $this->file)); if ( $cachedConfigFile === '' @@ -50,10 +52,12 @@ public function read( return true; } - public function write( - ConfigInterface $config - ): void { - $cachedConfigFile = (string)$config->get(self::CACHE_PATH, $this->file); + /** + * @param ConfigInterface $config + */ + public function write(ConfigInterface $config): void + { + $cachedConfigFile = $this->stringValue($config->get(self::CACHE_PATH, $this->file)); if ($cachedConfigFile === '') { return; @@ -74,7 +78,7 @@ public function write( throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); } - $this->writeCache($cachedConfigFile, $contents, (int)$config->get(self::CACHE_FILEMODE, 0666)); + $this->writeCache($cachedConfigFile, $contents, $this->intValue($config->get(self::CACHE_FILEMODE, 0666))); } private function writeCache(string $cachedConfigFile, string $contents, int $mode): void @@ -86,6 +90,38 @@ private function writeCache(string $cachedConfigFile, string $contents, int $mod } } + /** + * @param mixed $value + */ + private function stringValue($value): string + { + if ($value === null) { + return ''; + } + + if (\is_scalar($value) || $value instanceof \Stringable) { + return (string)$value; + } + + throw new \ErrorException('Configuration cache path must be a string'); + } + + /** + * @param mixed $value + */ + private function intValue($value): int + { + if (\is_int($value)) { + return $value; + } + + if (\is_string($value) && \is_numeric($value)) { + return (int)$value; + } + + throw new \ErrorException('Configuration cache file mode must be an integer'); + } + /** * @return array */ diff --git a/src/ProvidersCacheInterface.php b/src/ProvidersCacheInterface.php index fc037a5..671af56 100644 --- a/src/ProvidersCacheInterface.php +++ b/src/ProvidersCacheInterface.php @@ -14,7 +14,13 @@ interface ProvidersCacheInterface public const CACHE_PATH = 'cache_config_path'; + /** + * @param ConfigInterface $config + */ public function read(ConfigInterface $config): bool; + /** + * @param ConfigInterface $config + */ public function write(ConfigInterface $config): void; } diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index 129578e..cb91dc4 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -16,7 +16,7 @@ final class ProvidersCollection { private Injector $injector; /** - * @var ConfigInterface&NodeManipulationInterface + * @var ConfigInterface&NodeManipulationInterface */ private ConfigInterface $config; private ProvidersCacheInterface $cache; @@ -26,7 +26,7 @@ final class ProvidersCollection private iterable $providers; /** - * @param ConfigInterface&NodeManipulationInterface $config + * @param ConfigInterface&NodeManipulationInterface $config * @param iterable $providers */ public function __construct( @@ -72,8 +72,8 @@ public function aggregate(): void /** * @param Configuration $configuration - * @param array $result - * @param array $appendSections + * @param array $result + * @param array> $appendSections */ private function mergeConfiguration( array $configuration, diff --git a/src/ProxyFactory.php b/src/ProxyFactory.php index 047d14e..67bd3e4 100644 --- a/src/ProxyFactory.php +++ b/src/ProxyFactory.php @@ -13,6 +13,9 @@ */ class ProxyFactory implements ProxyFactoryInterface { + /** + * @param class-string $className + */ public function __invoke(string $className, callable $callback): VirtualProxyInterface { return (new LazyLoadingValueHolderFactory())->createProxy( diff --git a/src/ProxyFactoryInterface.php b/src/ProxyFactoryInterface.php index 34067f8..8f265ce 100644 --- a/src/ProxyFactoryInterface.php +++ b/src/ProxyFactoryInterface.php @@ -6,5 +6,8 @@ interface ProxyFactoryInterface { + /** + * @param class-string $className + */ public function __invoke(string $className, callable $callback): object; } From e02921b980553dedf8b88b5a75910686aa0dd354 Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 8 May 2026 06:36:28 +0200 Subject: [PATCH 32/46] chore: remove `--debug` flag from PHPStan script in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1a1cea7..d9c9be6 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,7 @@ "@php vendor/bin/phpcbf example.php" ], "stan": [ - "@php ./vendor/bin/phpstan analyse --debug --no-progress" + "@php ./vendor/bin/phpstan analyse --no-progress" ], "unit": [ "@php vendor/bin/codecept run unit" From 08da48c1c354cfb397a67f58a727eedc9b1e73f7 Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 8 May 2026 06:38:31 +0200 Subject: [PATCH 33/46] chore: add PHPStan configuration file with level 9 for static analysis setup --- phpstan.neon.dist | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 phpstan.neon.dist diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..13fd118 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + level: 9 + phpVersion: 70400 + paths: + - src + tmpDir: tests/_output/phpstan + parallel: + maximumNumberOfProcesses: 1 From 71f5f092184ac6699c07827de0a940677cf686bc Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 8 May 2026 06:39:29 +0200 Subject: [PATCH 34/46] chore: remove redundant PHPDoc comments from core classes, interfaces, and tests --- src/AurynConfig.php | 9 --------- src/AurynConfigInterface.php | 2 -- src/Extension.php | 4 ---- src/PhpFileProvider.php | 3 --- tests/benchmarks/AurynConfigBench.php | 4 ++-- 5 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/AurynConfig.php b/src/AurynConfig.php index d77706c..d99278b 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -132,7 +132,6 @@ public function walk(string $key, callable $callback): void /** * @param mixed $nameOrInstance - * @param int $index * @throws ConfigException */ protected function share($nameOrInstance, int $index): void @@ -141,8 +140,6 @@ protected function share($nameOrInstance, int $index): void } /** - * @param string $name - * @param int $index * @throws ConfigException */ protected function proxy(string $name, int $index): void @@ -151,8 +148,6 @@ protected function proxy(string $name, int $index): void } /** - * @param string $alias - * @param string $typeHint * @throws ConfigException */ protected function alias(string $alias, string $typeHint): void @@ -162,7 +157,6 @@ protected function alias(string $alias, string $typeHint): void /** * @param array $class_args - * @param string $class_name */ protected function define(array $class_args, string $class_name): void { @@ -171,7 +165,6 @@ protected function define(array $class_args, string $class_name): void /** * @param mixed $param_args - * @param string $param_name */ protected function defineParam($param_args, string $param_name): void { @@ -180,7 +173,6 @@ protected function defineParam($param_args, string $param_name): void /** * @param string $callableOrMethodStr - * @param string $name * @throws ConfigException */ protected function delegate($callableOrMethodStr, string $name): void @@ -190,7 +182,6 @@ protected function delegate($callableOrMethodStr, string $name): void /** * @param mixed $callableOrMethodStr - * @param string $name * @throws InjectionException */ protected function prepare($callableOrMethodStr, string $name): void diff --git a/src/AurynConfigInterface.php b/src/AurynConfigInterface.php index 6834879..16a4e58 100644 --- a/src/AurynConfigInterface.php +++ b/src/AurynConfigInterface.php @@ -18,8 +18,6 @@ public function apply(); public function extend(...$extensions); /** - * @param string $key - * @param callable $callback * @return void */ public function walk(string $key, callable $callback); diff --git a/src/Extension.php b/src/Extension.php index 1646b07..dc9154d 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -6,13 +6,9 @@ interface Extension { - /** - * @return string - */ public function name(): string; /** - * @param AurynConfigInterface $application * @return void */ public function execute(AurynConfigInterface $application); diff --git a/src/PhpFileProvider.php b/src/PhpFileProvider.php index 41c7f76..7e29696 100644 --- a/src/PhpFileProvider.php +++ b/src/PhpFileProvider.php @@ -21,9 +21,6 @@ public function __construct(string $pattern, FinderInterface $finder) $this->finder = $finder; } - /** - * @return \Generator - */ public function __invoke(): \Generator { $this->finder->names([$this->pattern]); diff --git a/tests/benchmarks/AurynConfigBench.php b/tests/benchmarks/AurynConfigBench.php index 358fab1..9bdc5e5 100644 --- a/tests/benchmarks/AurynConfigBench.php +++ b/tests/benchmarks/AurynConfigBench.php @@ -28,7 +28,7 @@ public function benchResolver(): void $resolver = new AurynConfig($injector, $config); $resolver->apply(); - $class = $injector->make(stdClass::class); + $injector->make(stdClass::class); } /** @@ -40,6 +40,6 @@ public function benchResolverP(): void { $injector = new Injector(); $injector->share(stdClass::class); - $class = $injector->make(stdClass::class); + $injector->make(stdClass::class); } } From 53efcb01d52d86830a3e6509674ca09b743a5642 Mon Sep 17 00:00:00 2001 From: Enea Date: Fri, 8 May 2026 06:40:02 +0200 Subject: [PATCH 35/46] chore: update `example.php` to use `ContainerBuilder`, refactor `resolve()` to `apply()`, and enhance usage examples with detailed configuration and container integration --- example.php | 67 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/example.php b/example.php index 8aa3ee7..c50262b 100644 --- a/example.php +++ b/example.php @@ -4,13 +4,15 @@ namespace ItalyStrap; +use Auryn\Injector; use ItalyStrap\Config\Config; use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Config\ConfigInterface; use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\AurynConfig; +use ItalyStrap\Empress\ContainerBuilder; use ItalyStrap\Empress\Extension; -use ItalyStrap\Empress\Injector; +use Psr\Container\ContainerInterface; use stdClass; require_once __DIR__ . '/vendor/autoload.php'; // phpcs:ignore PSR1.Files.SideEffects @@ -122,7 +124,7 @@ public function execute(string $text): string * add_{filter|action}( 'event_name', [ $proxy, 'doSomeStuff' ] ); * ::doSomeStuff() will just work as before. * - * @see [Lazy Loading Value Holder Proxy](https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md) + * @see https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md */ AurynConfig::PROXY => [ Config::class, @@ -192,9 +194,9 @@ public function execute(string $text): string $app = new AurynConfig($injector, (new ConfigFactory())->make($config)); /** - * Call the AurynConfig::resolve() method to do the autowiring of the application + * Call the AurynConfig::$this->apply() method to do the autowiring of the application */ -$app->resolve(); +$app->apply(); /** * Now that you have autoloaded your application dependency you can call $injector for instantiating objects @@ -211,6 +213,57 @@ public function execute(string $text): string echo $example->execute('Hello World!'); echo PHP_EOL; +/** + * ContainerBuilder usage + * + * This is the quickest way to aggregate providers, configure Auryn, + * and receive a PSR-11 container. + */ +$containerBuilder = new ContainerBuilder(); + +$containerBuilder + ->addProvider(static fn(): array => [ + AurynConfig::ALIASES => [ + ConfigInterface::class => Config::class, + ], + AurynConfig::SHARING => [ + stdClass::class, + ConfigInterface::class, + ], + AurynConfig::DEFINE_PARAM => [ + 'param' => 'Builder Text', + ], + AurynConfig::DELEGATIONS => [ + Example::class => static function (ContainerInterface $container): Example { + return new Example( + $container->get(stdClass::class), + $container->get(ConfigInterface::class), + 'Builder Text' + ); + }, + ], + ]) + ->extend(new class implements Extension { + public function name(): string + { + return 'container-builder-extension'; + } + + public function execute(AurynConfigInterface $application): void + { + // Add custom Auryn configuration logic here. + } + }); + +$container = $containerBuilder->build(); +$builderExample = $container->get(Example::class); + +\var_dump( + $builderExample instanceof Example + ? 'Yes, $builderExample is an instance of Example::class' + : 'No, $builderExample is NOT an instance of Example::class' +); + //$example2 = $injector->make( Example::class ); // @@ -221,7 +274,7 @@ public function execute(string $text): string */ /** - * If you need more power you can extend the AurynConfig::class BEFORE calling the AurynConfig::resolve() method + * If you need more power you can extend the AurynConfig::class BEFORE calling the AurynConfig::$this->apply() method * Create your custom configuration like the follow: * $config = [ * 'your-key' => [ @@ -270,8 +323,8 @@ public function doSomeStuff(string $array_value, $array_key, Injector $injector) /** * You can add as many extensions as you need - * Now you can call the ::resolve() method + * Now you can call the ::$this->apply() method */ -$app->resolve(); +$app->apply(); // Do the rest of your stuff From d97c2de946733ceb60aebb9086ecab8ad6ad740c Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 10 May 2026 17:39:00 +0200 Subject: [PATCH 36/46] chore: refactor configuration merging logic in ProvidersCollection --- src/ProvidersCollection.php | 68 +++++++------ .../ProvidersCollectionIntegrationTest.php | 29 +++--- tests/unit/ProvidersCollectionTest.php | 97 ++++++++++--------- 3 files changed, 103 insertions(+), 91 deletions(-) diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index cb91dc4..bab3117 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -54,16 +54,12 @@ public function aggregate(): void return; } - $result = []; - $appendSections = []; + $result = $this->config->toArray(); foreach ($this->loadConfigurationsFromProviders() as $configuration) { - $this->mergeConfiguration($configuration, $result, $appendSections); + $this->mergeConfiguration($configuration, $result); } $this->config->merge($result); - foreach ($appendSections as $key => $value) { - $this->config->appendTo($key, $value); - } if ((bool)$this->config->get(ProvidersCacheInterface::ENABLE_CACHE, false)) { $this->cache->write($this->config); @@ -73,52 +69,54 @@ public function aggregate(): void /** * @param Configuration $configuration * @param array $result - * @param array> $appendSections */ private function mergeConfiguration( array $configuration, - array &$result, - array &$appendSections + array &$result ): void { - foreach ($configuration as $key => $value) { - if ($this->isAppendSection((string)$key)) { - $appendSections[$key] = $this->uniqueValues(\array_merge( - (array)($appendSections[$key] ?? []), - (array)$value - )); - continue; - } + $result = $this->mergeValue($result, $configuration); + } - if (!is_array($value)) { - $result[$key] = $value; - continue; - } + /** + * @param mixed $current + * @param mixed $incoming + * @return mixed + */ + private function mergeValue($current, $incoming) + { + if (!\is_array($incoming)) { + return $incoming; + } - $current = []; + $current = \is_array($current) ? $current : []; - if (array_key_exists($key, $result) && is_array($result[$key])) { - $current = $result[$key]; + foreach ($incoming as $key => $value) { + if (!\is_int($key)) { + $current[$key] = $this->mergeValue($current[$key] ?? null, $value); + continue; } - $result[$key] = \array_replace_recursive($current, $value); + if (!$this->hasListValue($current, $value)) { + $current[] = $value; + } } - } - private function isAppendSection(string $key): bool - { - return \in_array($key, [ - AurynConfig::PROXY, - AurynConfig::SHARING, - ], true); + return $current; } /** * @param array $values - * @return array + * @param mixed $valueToFind */ - private function uniqueValues(array $values): array + private function hasListValue(array $values, $valueToFind): bool { - return \array_values(\array_unique($values, \SORT_REGULAR)); + foreach ($values as $key => $value) { + if (\is_int($key) && $value === $valueToFind) { + return true; + } + } + + return false; } /** diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index fc8f3be..0c1d256 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -25,9 +25,14 @@ final class ProvidersCollectionIntegrationTest extends UnitTestCase private function makeInstance(): ProvidersCollection { + $config = $this->makeConfigReal(); + $config->merge([ + 'config_cache_enabled' => true, + 'cache_config_path' => $this->cachedConfigFile, + ]); return new ProvidersCollection( new Injector(), - $this->makeConfigReal(), + $config, null, [ new PhpFileProvider( @@ -83,10 +88,6 @@ public function __invoke(): iterable } }, fn(): array => require \codecept_data_dir('fixtures/config/test.global.php'), - fn(): array => [ - 'config_cache_enabled' => true, - 'cache_config_path' => $this->cachedConfigFile, - ], ], ); } @@ -99,26 +100,26 @@ public function testIntegration(): void $this->assertSame( 'local config', - $config->get(\implode('.', [ + $config->get([ AurynConfig::ALIASES, self::CONFIG_KEY_1, - ])) + ]) ); $this->assertSame( 'iterable config', - $config->get(\implode('.', [ + $config->get([ AurynConfig::ALIASES, self::CONFIG_KEY_2, - ])) + ]) ); $this->assertSame( 'test.global.php', - $config->get(\implode('.', [ + $config->get([ AurynConfig::ALIASES, self::CONFIG_KEY_3, - ])) + ]) ); $this->assertFileExists($this->cachedConfigFile); @@ -127,6 +128,10 @@ public function testIntegration(): void $file = require $this->cachedConfigFile; $this->assertIsArray($file); - $this->assertCount(9, $config->get(AurynConfig::ALIASES), 'Should be 9'); + $aliases = $config->get(AurynConfig::ALIASES); + + $this->assertSame('value', $aliases[0]); + $this->assertSame('newValue', $aliases[1]); + $this->assertCount(10, $aliases, 'Should be 10'); } } diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index a4b2577..d178b48 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -57,7 +57,7 @@ public function testConstructorRequiresConfigWithNodeManipulationSupport(): void new ProvidersCollection(new Injector(), $config); } - public function testAggregateAppendsListSectionsAndReplacesMapSectionsRecursively(): void + public function testAggregateAppendsListValuesAndReplacesMapValuesRecursively(): void { $config = new Config(); $sut = $this->makeInstance([ @@ -68,11 +68,9 @@ public function testAggregateAppendsListSectionsAndReplacesMapSectionsRecursivel ], AurynConfig::SHARING => [ 'FirstShared', - 'SecondShared', ], AurynConfig::ALIASES => [ 'InterfaceName' => 'FirstClass', - 15 => 'first numeric value', ], AurynConfig::DEFINITIONS => [ 'ServiceName' => [ @@ -88,11 +86,9 @@ public function testAggregateAppendsListSectionsAndReplacesMapSectionsRecursivel ], AurynConfig::SHARING => [ 'SecondShared', - 'FirstShared', ], AurynConfig::ALIASES => [ 'InterfaceName' => 'SecondClass', - 15 => 'second numeric value', ], AurynConfig::DEFINITIONS => [ 'ServiceName' => [ @@ -112,8 +108,9 @@ public function testAggregateAppendsListSectionsAndReplacesMapSectionsRecursivel 'FirstShared', 'SecondShared', ], $config->get(AurynConfig::SHARING)); - $this->assertSame('SecondClass', $config->get(AurynConfig::ALIASES . '.InterfaceName')); - $this->assertSame('second numeric value', $config->get([AurynConfig::ALIASES, 15])); + $this->assertSame([ + 'InterfaceName' => 'SecondClass', + ], $config->get(AurynConfig::ALIASES)); $this->assertSame( [ ':overridden' => 'second', @@ -123,59 +120,71 @@ public function testAggregateAppendsListSectionsAndReplacesMapSectionsRecursivel ); } - public function testAggregateUsesAppendToForListSections(): void + public function testAggregateMergesMixedExternalSectionsByArrayShape(): void { - $config = new class extends Config { - /** - * @var array - */ - public array $appendCalls = []; - - public function appendTo($key, $value): bool - { - $this->appendCalls[] = [$key, $value]; - - return parent::appendTo($key, $value); - } - }; - + $config = new Config(); $sut = $this->makeInstance([ static fn(): array => [ - AurynConfig::PROXY => [ - 'FirstProxy', - ], - AurynConfig::SHARING => [ - 'FirstShared', + 'external_section' => [ + 'FirstService', + 'feature_flag' => 'ConditionalService', + 'nested' => [ + 'kept' => 'kept', + 'overridden' => 'first', + ], ], ], static fn(): array => [ - AurynConfig::PROXY => [ - 'SecondProxy', - ], - AurynConfig::SHARING => [ - 'SecondShared', + 'external_section' => [ + 'FirstService', + 'SecondService', + 'feature_flag' => 'UpdatedConditionalService', + 'nested' => [ + 'overridden' => 'second', + 'added' => 'added', + ], ], ], ], null, $config); $sut->aggregate(); + $externalSection = $config->get('external_section'); + + $this->assertSame('FirstService', $externalSection[0]); + $this->assertSame('SecondService', $externalSection[1]); + $this->assertSame('UpdatedConditionalService', $externalSection['feature_flag']); $this->assertSame([ - [ - AurynConfig::PROXY, - [ - 'FirstProxy', - 'SecondProxy', - ], + 'kept' => 'kept', + 'overridden' => 'second', + 'added' => 'added', + ], $externalSection['nested']); + } + + public function testAggregateAppendsProviderListsToPreloadedConfig(): void + { + $config = new Config([ + 'external_section' => [ + 'PreloadedService', + 'feature_flag' => 'PreloadedConditionalService', ], - [ - AurynConfig::SHARING, - [ - 'FirstShared', - 'SecondShared', + ]); + $sut = $this->makeInstance([ + static fn(): array => [ + 'external_section' => [ + 'ProviderService', + 'feature_flag' => 'ProviderConditionalService', ], ], - ], $config->appendCalls); + ], null, $config); + + $sut->aggregate(); + + $externalSection = $config->get('external_section'); + + $this->assertSame('PreloadedService', $externalSection[0]); + $this->assertSame('ProviderService', $externalSection[1]); + $this->assertSame('ProviderConditionalService', $externalSection['feature_flag']); } public function testAggregateAcceptsTraversableProvidersYieldingConfigurationArrays(): void From 409f6037b7ee3f226d566a4413f5065ef9cca028 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 10 May 2026 21:57:37 +0200 Subject: [PATCH 37/46] chore: update composer.json and test.yml for dependency and coverage adjustments --- .github/workflows/test.yml | 3 ++- composer.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba5b9c9..a99e1b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: xdebug2 + coverage: xdebug + ini-values: register_argc_argv=On - uses: ramsey/composer-install@v3 with: diff --git a/composer.json b/composer.json index d9c9be6..c8de064 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "psr/container": "^1.1" }, "require-dev": { - "lucatume/wp-browser": "<3.5", + "lucatume/wp-browser": ">=3.2.3 <3.5", "lucatume/function-mocker-le": "^1.0.1", "codeception/module-asserts": "^1.0", "phpspec/prophecy-phpunit": "^2.0", From 0ab1d68d57ae4d8a6801a716b7dd0e2b4c47bafe Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 10 May 2026 22:25:21 +0200 Subject: [PATCH 38/46] chore: update italystrap/config version to ^2.11 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c8de064..35c4ae8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "php" : ">=7.4", "brick/varexporter": "^0.3.8", "friendsofphp/proxy-manager-lts": "^1.0", - "italystrap/config": "^2.10", + "italystrap/config": "^2.11", "overclokk/auryn": "dev-master", "webimpress/safe-writer": "^2.2", "psr/container": "^1.1" From 00bb17ac42df5f0e54ea21b62c0808ee67169593 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 10 May 2026 22:42:00 +0200 Subject: [PATCH 39/46] chore: add behat/gherkin as a development dependency --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 35c4ae8..7f748d8 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "require-dev": { "lucatume/wp-browser": ">=3.2.3 <3.5", "lucatume/function-mocker-le": "^1.0.1", + "behat/gherkin": ">=4.4 <4.11", "codeception/module-asserts": "^1.0", "phpspec/prophecy-phpunit": "^2.0", From c321f6f62113af0d8841923b510c9090571ba7b9 Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 11 May 2026 09:30:30 +0200 Subject: [PATCH 40/46] chore: refactor ProvidersCache to improve configuration handling and add file mode and enabled options --- src/ProvidersCache.php | 61 +++++-------------- src/ProvidersCacheInterface.php | 6 -- src/ProvidersCollection.php | 5 +- tests/unit/ProvidersCacheTest.php | 52 ++++------------ .../ProvidersCollectionIntegrationTest.php | 7 +-- tests/unit/ProvidersCollectionTest.php | 25 +++----- 6 files changed, 40 insertions(+), 116 deletions(-) diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index 1eb34ed..c471621 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -24,11 +24,15 @@ final class ProvidersCache implements ProvidersCacheInterface %s EOT; - private ?string $file; + private string $file; + private int $fileMode; + private bool $enabled; - public function __construct(?string $file = null) + public function __construct(string $file = '', int $fileMode = 0666, bool $enabled = false) { $this->file = $file; + $this->fileMode = $fileMode; + $this->enabled = $enabled; } /** @@ -36,19 +40,20 @@ public function __construct(?string $file = null) */ public function read(ConfigInterface $config): bool { - - $cachedConfigFile = $this->stringValue($config->get(self::CACHE_PATH, $this->file)); + if (!$this->enabled) { + return false; + } if ( - $cachedConfigFile === '' - || !file_exists($cachedConfigFile) - || !is_file($cachedConfigFile) - || !is_readable($cachedConfigFile) + $this->file === '' + || !file_exists($this->file) + || !is_file($this->file) + || !is_readable($this->file) ) { return false; } - $config->merge($this->loadCacheFile($cachedConfigFile)); + $config->merge($this->loadCacheFile($this->file)); return true; } @@ -57,9 +62,7 @@ public function read(ConfigInterface $config): bool */ public function write(ConfigInterface $config): void { - $cachedConfigFile = $this->stringValue($config->get(self::CACHE_PATH, $this->file)); - - if ($cachedConfigFile === '') { + if (!$this->enabled || $this->file === '') { return; } @@ -78,7 +81,7 @@ public function write(ConfigInterface $config): void throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); } - $this->writeCache($cachedConfigFile, $contents, $this->intValue($config->get(self::CACHE_FILEMODE, 0666))); + $this->writeCache($this->file, $contents, $this->fileMode); } private function writeCache(string $cachedConfigFile, string $contents, int $mode): void @@ -90,38 +93,6 @@ private function writeCache(string $cachedConfigFile, string $contents, int $mod } } - /** - * @param mixed $value - */ - private function stringValue($value): string - { - if ($value === null) { - return ''; - } - - if (\is_scalar($value) || $value instanceof \Stringable) { - return (string)$value; - } - - throw new \ErrorException('Configuration cache path must be a string'); - } - - /** - * @param mixed $value - */ - private function intValue($value): int - { - if (\is_int($value)) { - return $value; - } - - if (\is_string($value) && \is_numeric($value)) { - return (int)$value; - } - - throw new \ErrorException('Configuration cache file mode must be an integer'); - } - /** * @return array */ diff --git a/src/ProvidersCacheInterface.php b/src/ProvidersCacheInterface.php index 671af56..cef3ad0 100644 --- a/src/ProvidersCacheInterface.php +++ b/src/ProvidersCacheInterface.php @@ -8,12 +8,6 @@ interface ProvidersCacheInterface { - public const ENABLE_CACHE = 'config_cache_enabled'; - - public const CACHE_FILEMODE = 'config_cache_filemode'; - - public const CACHE_PATH = 'cache_config_path'; - /** * @param ConfigInterface $config */ diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index bab3117..4bcd7ed 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -59,11 +59,10 @@ public function aggregate(): void $this->mergeConfiguration($configuration, $result); } + $this->config->exchangeArray([]); $this->config->merge($result); - if ((bool)$this->config->get(ProvidersCacheInterface::ENABLE_CACHE, false)) { - $this->cache->write($this->config); - } + $this->cache->write($this->config); } /** diff --git a/tests/unit/ProvidersCacheTest.php b/tests/unit/ProvidersCacheTest.php index 176d399..092313c 100644 --- a/tests/unit/ProvidersCacheTest.php +++ b/tests/unit/ProvidersCacheTest.php @@ -6,7 +6,6 @@ use ItalyStrap\Config\Config; use ItalyStrap\Empress\ProvidersCache; -use ItalyStrap\Empress\ProvidersCacheInterface; use ItalyStrap\Empress\Tests\UnitTestCase; final class ProvidersCacheTest extends UnitTestCase @@ -41,7 +40,7 @@ public function testReadReturnsFalseWhenNoCachePathIsConfigured(): void public function testReadReturnsFalseWhenCachePathDoesNotExist(): void { - $sut = new ProvidersCache($this->cacheFile('missing-cache.php')); + $sut = new ProvidersCache($this->cacheFile('missing-cache.php'), 0666, true); $this->assertFalse($sut->read(new Config())); } @@ -49,7 +48,7 @@ public function testReadReturnsFalseWhenCachePathDoesNotExist(): void public function testReadReturnsFalseWhenCachePathIsNotAFile(): void { $directory = $this->cacheDirectory('cache-directory'); - $sut = new ProvidersCache($directory); + $sut = new ProvidersCache($directory, 0666, true); $this->assertFalse($sut->read(new Config())); } @@ -67,38 +66,13 @@ public function testReadMergesArrayReturnedByCacheFile(): void ]; PHP); $config = new Config(); - $sut = new ProvidersCache($file); + $sut = new ProvidersCache($file, 0666, true); $this->assertTrue($sut->read($config)); $this->assertSame('value', $config->get('key')); $this->assertSame('nested-value', $config->get('nested.key')); } - public function testReadUsesConfigCachePathBeforeConstructorPath(): void - { - $constructorFile = $this->writeCacheFile('constructor-cache.php', <<<'PHP' - 'constructor', -]; -PHP); - $configFile = $this->writeCacheFile('config-cache-override.php', <<<'PHP' - 'config', -]; -PHP); - $config = new Config([ - ProvidersCacheInterface::CACHE_PATH => $configFile, - ]); - $sut = new ProvidersCache($constructorFile); - - $this->assertTrue($sut->read($config)); - $this->assertSame('config', $config->get('source')); - } - public function testReadThrowsWhenCacheFileDoesNotReturnArray(): void { $file = $this->writeCacheFile('non-array-cache.php', <<<'PHP' @@ -106,7 +80,7 @@ public function testReadThrowsWhenCacheFileDoesNotReturnArray(): void return 'invalid'; PHP); - $sut = new ProvidersCache($file); + $sut = new ProvidersCache($file, 0666, true); $this->expectException(\ErrorException::class); $this->expectExceptionMessage('Configuration cache must return an array'); @@ -121,7 +95,7 @@ public function testReadWrapsErrorsThrownByCacheFile(): void throw new RuntimeException('Broken cache file'); PHP); - $sut = new ProvidersCache($file); + $sut = new ProvidersCache($file, 0666, true); try { $sut->read(new Config()); @@ -133,15 +107,16 @@ public function testReadWrapsErrorsThrownByCacheFile(): void } } - public function testWriteReturnsWhenNoCachePathIsConfigured(): void + public function testWriteReturnsWhenCacheIsDisabled(): void { - $sut = new ProvidersCache(); + $file = $this->cacheFile('disabled-write-cache.php'); + $sut = new ProvidersCache($file); $sut->write(new Config([ 'key' => 'value', ])); - $this->assertTrue(true); + $this->assertFalse(\is_file($file)); } public function testWriteCreatesReadableCacheFile(): void @@ -149,10 +124,9 @@ public function testWriteCreatesReadableCacheFile(): void $file = $this->cacheFile('written-cache.php'); $this->pathsToRemove[] = $file; $config = new Config([ - ProvidersCacheInterface::CACHE_PATH => $file, 'key' => 'value', ]); - $sut = new ProvidersCache(); + $sut = new ProvidersCache($file, 0666, true); $sut->write($config); @@ -166,10 +140,8 @@ public function testWriteCreatesReadableCacheFile(): void public function testWriteThrowsWhenCacheFileCannotBeWritten(): void { - $config = new Config([ - ProvidersCacheInterface::CACHE_PATH => $this->cacheFile('missing-directory/cache.php'), - ]); - $sut = new ProvidersCache(); + $config = new Config(); + $sut = new ProvidersCache($this->cacheFile('missing-directory/cache.php'), 0666, true); $this->expectException(\ErrorException::class); $this->expectExceptionMessage('Configuration cache cannot be written'); diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php index 0c1d256..d43bdb2 100644 --- a/tests/unit/ProvidersCollectionIntegrationTest.php +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -8,6 +8,7 @@ use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\ModuleInterface; use ItalyStrap\Empress\PhpFileProvider; +use ItalyStrap\Empress\ProvidersCache; use ItalyStrap\Empress\ProvidersCollection; use ItalyStrap\Empress\Tests\ConcreteNeedsSomeInterface; use ItalyStrap\Empress\Tests\SomeInterface; @@ -26,14 +27,10 @@ final class ProvidersCollectionIntegrationTest extends UnitTestCase private function makeInstance(): ProvidersCollection { $config = $this->makeConfigReal(); - $config->merge([ - 'config_cache_enabled' => true, - 'cache_config_path' => $this->cachedConfigFile, - ]); return new ProvidersCollection( new Injector(), $config, - null, + new ProvidersCache($this->cachedConfigFile, 0666, true), [ new PhpFileProvider( '/config/autoload/{{,*.}global,{,*.}local}.php', diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php index d178b48..5a02781 100644 --- a/tests/unit/ProvidersCollectionTest.php +++ b/tests/unit/ProvidersCollectionTest.php @@ -11,6 +11,7 @@ use ItalyStrap\Config\ConfigInterface; use ItalyStrap\Config\NodeManipulationInterface; use ItalyStrap\Empress\AurynConfig; +use ItalyStrap\Empress\ProvidersCache; use ItalyStrap\Empress\ProvidersCacheInterface; use ItalyStrap\Empress\ProvidersCollection; use ItalyStrap\Empress\Tests\Modules\ModuleStub1; @@ -284,7 +285,7 @@ static function (): array { $this->assertFalse($cache->written); } - public function testAggregateWritesCacheWhenEnabledByProviders(): void + public function testAggregateWritesCacheWhenCacheIsProvided(): void { $cache = new class implements ProvidersCacheInterface { /** @@ -305,7 +306,6 @@ public function write(ConfigInterface $config): void $sut = $this->makeInstance([ static fn(): array => [ - ProvidersCacheInterface::ENABLE_CACHE => true, 'key' => 'value', ], ], $cache); @@ -317,29 +317,20 @@ public function write(ConfigInterface $config): void public function testAggregateDoesNotWriteCacheWhenCacheIsDisabled(): void { - $cache = new class implements ProvidersCacheInterface { - public bool $written = false; - - public function read(ConfigInterface $config): bool - { - return false; - } - - public function write(ConfigInterface $config): void - { - $this->written = true; - } - }; + $file = \codecept_output_dir('disabled-providers-cache.php'); + if (\is_file($file)) { + \unlink($file); + } $sut = $this->makeInstance([ static fn(): array => [ 'key' => 'value', ], - ], $cache); + ], new ProvidersCache($file)); $sut->aggregate(); - $this->assertFalse($cache->written); + $this->assertFalse(\is_file($file)); } public function testAggregateThrowsWhenProviderReturnsInvalidResult(): void From 2768faa4b4c649539e0a3b2ff49a30d690b5d10f Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 11 May 2026 11:27:22 +0200 Subject: [PATCH 41/46] chore: add ConfigReplacementTrait to handle configuration replacement --- src/ConfigReplacementTrait.php | 22 ++++++++++++++++++++++ src/ProvidersCache.php | 4 +++- src/ProvidersCollection.php | 5 +++-- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/ConfigReplacementTrait.php diff --git a/src/ConfigReplacementTrait.php b/src/ConfigReplacementTrait.php new file mode 100644 index 0000000..76d07f6 --- /dev/null +++ b/src/ConfigReplacementTrait.php @@ -0,0 +1,22 @@ + $config + * @param array $values + */ + private function replaceConfig(ConfigInterface $config, array $values): void + { + // @phpstan-ignore-next-line Config supports exchangeArray(), but ConfigInterface does not expose it yet. + $config->exchangeArray($values); + } +} diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php index c471621..22e5629 100644 --- a/src/ProvidersCache.php +++ b/src/ProvidersCache.php @@ -13,6 +13,8 @@ final class ProvidersCache implements ProvidersCacheInterface { + use ConfigReplacementTrait; + private const CACHE_TEMPLATE = <<<'EOT' merge($this->loadCacheFile($this->file)); + $this->replaceConfig($config, $this->loadCacheFile($this->file)); return true; } diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php index 4bcd7ed..359c4f0 100644 --- a/src/ProvidersCollection.php +++ b/src/ProvidersCollection.php @@ -14,6 +14,8 @@ */ final class ProvidersCollection { + use ConfigReplacementTrait; + private Injector $injector; /** * @var ConfigInterface&NodeManipulationInterface @@ -59,8 +61,7 @@ public function aggregate(): void $this->mergeConfiguration($configuration, $result); } - $this->config->exchangeArray([]); - $this->config->merge($result); + $this->replaceConfig($this->config, $result); $this->cache->write($this->config); } From 27be167ea9a92b6ad15705b6e3a485c51b35d44d Mon Sep 17 00:00:00 2001 From: Enea Date: Thu, 14 May 2026 08:13:46 +0200 Subject: [PATCH 42/46] chore: update psr/container version constraint to support v2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7f748d8..8cea885 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "italystrap/config": "^2.11", "overclokk/auryn": "dev-master", "webimpress/safe-writer": "^2.2", - "psr/container": "^1.1" + "psr/container": "^1.1 || ^2.0" }, "require-dev": { "lucatume/wp-browser": ">=3.2.3 <3.5", From fe386dff02dbc484023b61c1fabdcff9fbfac634 Mon Sep 17 00:00:00 2001 From: Enea Date: Sat, 16 May 2026 16:23:53 +0200 Subject: [PATCH 43/46] chore: adjust proxy-manager-lts dependency placement and add it to suggests section in composer.json --- composer.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 8cea885..6b20ff2 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "require": { "php" : ">=7.4", "brick/varexporter": "^0.3.8", - "friendsofphp/proxy-manager-lts": "^1.0", "italystrap/config": "^2.11", "overclokk/auryn": "dev-master", "webimpress/safe-writer": "^2.2", @@ -46,7 +45,9 @@ "italystrap/finder": "dev-master", "laminas/laminas-config-aggregator": "^1.9", "crellbar/prophecy-extensions": "^1.1", - "rector/swiss-knife": "^2.3" + "rector/swiss-knife": "^2.3", + + "friendsofphp/proxy-manager-lts": "^1.0" }, "autoload": { "psr-4": { @@ -70,7 +71,8 @@ }, "suggest": { "elazar/auryn-container-interop": "Only if you want to add a psr/container adapter, not required for this package", - "northwoods/container": "Only if you want to add a psr/container adapter, not required for this package" + "northwoods/container": "Only if you want to add a psr/container adapter, not required for this package", + "friendsofphp/proxy-manager-lts": "Only if you want to add support for lazy loading classes" }, "scripts": { "cs": [ From 5feda0b08c2f131fa7e144d72048815ac29c0999 Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 18 May 2026 11:43:30 +0200 Subject: [PATCH 44/46] chore: handle missing ProxyFactory gracefully, update tests to cover proxy configuration behavior --- src/AurynConfig.php | 8 ++++++-- tests/unit/AurynConfigIntegrationTest.php | 19 ++++++++++++++++++- tests/unit/AurynConfigTest.php | 20 ++++++++++++++++++-- tests/unit/ContainerBuilderTest.php | 15 +++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/AurynConfig.php b/src/AurynConfig.php index d99278b..cd81477 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -40,7 +40,7 @@ class AurynConfig implements AurynConfigInterface */ private Config $dependencies; - private ProxyFactoryInterface $proxy_factory; + private ?ProxyFactoryInterface $proxy_factory; /** * @var array @@ -62,7 +62,7 @@ public function __construct( ) { $this->injector = $injector; $this->dependencies = $dependencies; - $this->proxy_factory = $proxyFactory ?? new ProxyFactory(); + $this->proxy_factory = $proxyFactory; } public function apply(): void @@ -144,6 +144,10 @@ protected function share($nameOrInstance, int $index): void */ protected function proxy(string $name, int $index): void { + if ($this->proxy_factory === null) { + return; + } + $this->injector->proxy($name, $this->proxy_factory); } diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php index d619122..36a7d62 100644 --- a/tests/unit/AurynConfigIntegrationTest.php +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -6,7 +6,6 @@ use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; -use ItalyStrap\Empress\ProxyFactoryInterface; use ItalyStrap\Empress\Tests\ConcreteNeedsSomeInterface; use ItalyStrap\Empress\Tests\SomeConcrete; use ItalyStrap\Empress\Tests\SomeInterface; @@ -83,4 +82,22 @@ public function render(): string $concrete = $this->realInjector->make(SomeConcrete::class); $this->assertSame('DifferentConcrete', $concrete->render()); } + + public function testItShouldIgnoreProxyConfigurationWhenProxyFactoryIsMissing(): void + { + $sut = new AurynConfig( + $this->realInjector, + (new ConfigFactory())->make([ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]) + ); + + $sut->apply(); + + /** @var SomeConcrete $concrete */ + $concrete = $this->realInjector->make(SomeConcrete::class); + $this->assertSame('SomeConcrete', $concrete->render()); + } } diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index 370a3ce..df77a75 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -9,8 +9,6 @@ use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\Extension; -use ItalyStrap\Empress\ProxyFactory; -use ItalyStrap\Empress\ProxyFactoryInterface; use ItalyStrap\Empress\Tests\SomeConcrete; use ItalyStrap\Empress\Tests\SomeExtension; use ItalyStrap\Empress\Tests\UnitTestCase; @@ -67,6 +65,24 @@ public function testItShouldProxy01(): void $sut->apply(); } + public function testItShouldSkipProxyConfigurationWhenProxyFactoryIsMissing(): void + { + $this->injector + ->proxy(Argument::any(), Argument::any()) + ->shouldNotBeCalled(); + + $sut = new AurynConfig( + $this->makeInjector(), + (new ConfigFactory())->make([ + AurynConfig::PROXY => [ + 'SomeClassProxies', + ], + ]) + ); + + $sut->apply(); + } + public function shareProvider(): iterable { return [ diff --git a/tests/unit/ContainerBuilderTest.php b/tests/unit/ContainerBuilderTest.php index 697b139..37c4316 100644 --- a/tests/unit/ContainerBuilderTest.php +++ b/tests/unit/ContainerBuilderTest.php @@ -275,4 +275,19 @@ public function render(): string $this->assertTrue($factory->called); $this->assertSame('ProxiedConcrete', $service->render()); } + + public function testBuildIgnoresProxyConfigurationWhenProxyFactoryIsMissing(): void + { + $builder = new ContainerBuilder(); + $builder->addProvider(static fn(): array => [ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]); + + /** @var SomeConcrete $service */ + $service = $builder->build()->get(SomeConcrete::class); + + $this->assertSame('SomeConcrete', $service->render()); + } } From 918b961a26c149fdb0994fb7b56842fada84cbf8 Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 18 May 2026 12:21:11 +0200 Subject: [PATCH 45/46] chore: add exception for missing ProxyFactory in proxy configuration, update tests --- src/AurynConfig.php | 7 +++++++ tests/unit/AurynConfigTest.php | 17 +++++++++++++++++ tests/unit/ContainerBuilderTest.php | 17 ++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/AurynConfig.php b/src/AurynConfig.php index cd81477..68a1f12 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -144,6 +144,13 @@ protected function share($nameOrInstance, int $index): void */ protected function proxy(string $name, int $index): void { + if ($name !== '' && \class_exists($name) && $this->proxy_factory === null) { + throw new ConfigException(\sprintf( + 'Proxy factory is required for proxying %s', + $name + )); + } + if ($this->proxy_factory === null) { return; } diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index df77a75..fb07649 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -5,6 +5,7 @@ namespace ItalyStrap\Empress\Tests\Unit; use Auryn\Injector; +use Auryn\ConfigException; use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\AurynConfigInterface; @@ -83,6 +84,22 @@ public function testItShouldSkipProxyConfigurationWhenProxyFactoryIsMissing(): v $sut->apply(); } + public function testItShouldThrowWhenProxyFactoryIsMissingForRealClassProxyConfiguration(): void + { + $sut = new AurynConfig( + $this->makeInjector(), + (new ConfigFactory())->make([ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]) + ); + + $this->expectException(ConfigException::class); + + $sut->apply(); + } + public function shareProvider(): iterable { return [ diff --git a/tests/unit/ContainerBuilderTest.php b/tests/unit/ContainerBuilderTest.php index 37c4316..c3666ac 100644 --- a/tests/unit/ContainerBuilderTest.php +++ b/tests/unit/ContainerBuilderTest.php @@ -4,6 +4,7 @@ namespace ItalyStrap\Empress\Tests\Unit; +use Auryn\ConfigException; use Auryn\Injector; use ItalyStrap\Config\Config; use ItalyStrap\Config\ConfigInterface; @@ -281,7 +282,7 @@ public function testBuildIgnoresProxyConfigurationWhenProxyFactoryIsMissing(): v $builder = new ContainerBuilder(); $builder->addProvider(static fn(): array => [ AurynConfig::PROXY => [ - SomeConcrete::class, + 'SomeClassProxies', ], ]); @@ -290,4 +291,18 @@ public function testBuildIgnoresProxyConfigurationWhenProxyFactoryIsMissing(): v $this->assertSame('SomeConcrete', $service->render()); } + + public function testBuildThrowsWhenProxyFactoryIsMissingForRealClassProxyConfiguration(): void + { + $builder = new ContainerBuilder(); + $builder->addProvider(static fn(): array => [ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]); + + $this->expectException(ConfigException::class); + + $builder->build(); + } } From 15ae482a1530715a533e514c989fc070789a7906 Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 18 May 2026 12:24:42 +0200 Subject: [PATCH 46/46] chore: update AurynConfigIntegrationTest to throw ConfigException for missing ProxyFactory --- tests/unit/AurynConfigIntegrationTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php index 36a7d62..21d8be7 100644 --- a/tests/unit/AurynConfigIntegrationTest.php +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -4,6 +4,7 @@ namespace ItalyStrap\Empress\Tests\Unit; +use Auryn\ConfigException; use ItalyStrap\Config\ConfigFactory; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\Tests\ConcreteNeedsSomeInterface; @@ -83,7 +84,7 @@ public function render(): string $this->assertSame('DifferentConcrete', $concrete->render()); } - public function testItShouldIgnoreProxyConfigurationWhenProxyFactoryIsMissing(): void + public function testItShouldThrowWhenProxyFactoryIsMissingForRealClassProxyConfiguration(): void { $sut = new AurynConfig( $this->realInjector, @@ -94,10 +95,8 @@ public function testItShouldIgnoreProxyConfigurationWhenProxyFactoryIsMissing(): ]) ); - $sut->apply(); + $this->expectException(ConfigException::class); - /** @var SomeConcrete $concrete */ - $concrete = $this->realInjector->make(SomeConcrete::class); - $this->assertSame('SomeConcrete', $concrete->render()); + $sut->apply(); } }