├── phpstan.neon.dist ├── .gitignore ├── tests ├── mocks │ ├── ExampleContainer.php │ ├── NotContainerAdapter.php │ ├── ExampleExtendedContainer.php │ ├── ExampleInterface.php │ ├── ExampleFactory.php │ ├── BootableServiceProvider.php │ ├── ExampleClass.php │ ├── ExampleClassWithArgs.php │ ├── FileReader │ │ └── CustomFileReader.php │ └── ExampleContainerAdapter.php ├── acceptance │ ├── PimpleContainerAdapterTest.php │ ├── LeagueContainerAdapterTest.php │ ├── PimpleContainerWrapper.php │ ├── AbstractContainerAdapterTest.php │ ├── SupportsInflectorConfig.php │ ├── SupportsApplicationConfig.php │ └── SupportsServiceConfig.php ├── unit │ ├── InflectorConfigTest.php │ ├── Exception │ │ ├── ReadOnlyExceptionTest.php │ │ ├── EntryDoesNotExistExceptionTest.php │ │ ├── NoMatchingFilesExceptionTest.php │ │ ├── NotContainerAdapterExceptionTest.php │ │ ├── MissingDependencyExceptionTest.php │ │ ├── NotClassDefinitionExceptionTest.php │ │ ├── UnknownSettingExceptionTest.php │ │ ├── UnknownFileTypeExceptionTest.php │ │ ├── UnknownContainerExceptionTest.php │ │ └── InvalidConfigExceptionTest.php │ ├── InflectorDefinitionTest.php │ ├── FileReader │ │ ├── FileLocatorTest.php │ │ ├── JSONFileReaderTest.php │ │ ├── YAMLFileReaderTest.php │ │ ├── PHPFileReaderTest.php │ │ ├── HJSONFileReaderTest.php │ │ └── ReaderFactoryTest.php │ ├── ServiceConfigTest.php │ ├── ContainerAdapterFactoryTest.php │ ├── ConfiguratorTest.php │ ├── ApplicationConfigIteratorTest.php │ ├── ServiceDefinitionTest.php │ └── ApplicationConfigTest.php └── support │ └── TestFileCreator.php ├── src ├── Exception │ ├── Exception.php │ ├── ReadOnlyException.php │ ├── EntryDoesNotExistException.php │ ├── NoMatchingFilesException.php │ ├── NotFactoryException.php │ ├── NotFileReaderException.php │ ├── NotContainerAdapterException.php │ ├── NotClassDefinitionException.php │ ├── MissingDependencyException.php │ ├── UnknownSettingException.php │ ├── UnknownContainerException.php │ ├── UnknownFileTypeException.php │ └── InvalidConfigException.php ├── FileReader │ ├── FileReader.php │ ├── FileLocator.php │ ├── PHPFileReader.php │ ├── YAMLFileReader.php │ ├── JSONFileReader.php │ ├── ReaderFactory.php │ └── HJSONFileReader.php ├── ContainerAdapter.php ├── InflectorDefinition.php ├── InflectorConfig.php ├── League │ ├── LeagueContainerAdapter.php │ ├── InflectorServiceProvider.php │ ├── ApplicationConfigServiceProvider.php │ └── ServiceServiceProvider.php ├── ContainerAdapterFactory.php ├── ApplicationConfigIterator.php ├── ServiceConfig.php ├── ServiceDefinition.php ├── ApplicationConfig.php ├── Pimple │ └── PimpleContainerAdapter.php └── Configurator.php ├── .travis.yml ├── phpunit.xml ├── LICENSE ├── CONTRIBUTING.md ├── rector.php ├── UPGRADE.md ├── composer.json ├── .php-cs-fixer.php ├── CHANGELOG.md └── README.md /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | 4 | paths: 5 | - src/ 6 | 7 | treatPhpDocTypesAsCertain: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | tests/.test-config 3 | .php_cs.cache 4 | composer.lock 5 | .php-cs-fixer.cache 6 | .phpunit.result.cache 7 | .devcontainer 8 | -------------------------------------------------------------------------------- /tests/mocks/ExampleContainer.php: -------------------------------------------------------------------------------- 1 | container = new PimpleContainerWrapper(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/acceptance/LeagueContainerAdapterTest.php: -------------------------------------------------------------------------------- 1 | container = new Container(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 8.1 5 | - 8.2 6 | - 8.3 7 | - 8.4 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache 12 | 13 | before_install: 14 | - composer self-update 15 | - composer validate 16 | 17 | install: 18 | - composer install --prefer-dist 19 | 20 | script: 21 | - composer test 22 | 23 | matrix: 24 | fast_finish: true -------------------------------------------------------------------------------- /tests/mocks/ExampleClass.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public function getValue() 17 | { 18 | return $this->value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/acceptance/PimpleContainerWrapper.php: -------------------------------------------------------------------------------- 1 | constructorArgs = $constructorArgs; 14 | } 15 | 16 | public function getConstructorArgs(): array 17 | { 18 | return $this->constructorArgs; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/ReadOnlyException.php: -------------------------------------------------------------------------------- 1 | > $methods 14 | */ 15 | public function __construct(private readonly string $interface, private readonly array $methods) 16 | { 17 | } 18 | 19 | public function getInterface(): string 20 | { 21 | return $this->interface; 22 | } 23 | 24 | /** 25 | * @return array> 26 | */ 27 | public function getMethods(): array 28 | { 29 | return $this->methods; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exception/UnknownSettingException.php: -------------------------------------------------------------------------------- 1 | ['arg1', 'arg2']]; 17 | 18 | $inflectorConfig = new InflectorConfig([$interface => $methods]); 19 | 20 | $this->assertEquals( 21 | [new InflectorDefinition($interface, $methods)], 22 | iterator_to_array($inflectorConfig) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/FileReader/PHPFileReader.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 22 | 23 | $config = include $this->filename; 24 | 25 | $this->assertConfigIsValid($config); 26 | 27 | return $config; 28 | } 29 | 30 | private function assertConfigIsValid(mixed $config): void 31 | { 32 | if (!is_array($config)) { 33 | throw InvalidConfigException::fromPHPFileError($this->filename); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | tests/unit 22 | 23 | 24 | tests/acceptance 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/InflectorConfig.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class InflectorConfig implements IteratorAggregate 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private array $inflectors = []; 22 | 23 | /** 24 | * @param array>> $config 25 | */ 26 | public function __construct(array $config) 27 | { 28 | foreach ($config as $interfaceName => $methods) { 29 | $this->inflectors[] = new InflectorDefinition( 30 | $interfaceName, 31 | $methods 32 | ); 33 | } 34 | } 35 | 36 | public function getIterator(): Traversable 37 | { 38 | return new ArrayIterator($this->inflectors); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/Exception/ReadOnlyExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new ReadOnlyException()); 17 | } 18 | 19 | public function testItIsALogicException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new ReadOnlyException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromThePatterns(): void 25 | { 26 | $this->assertSame( 27 | '"ClassName" is read only.', 28 | ReadOnlyException::fromClassName('ClassName')->getMessage() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/InflectorDefinitionTest.php: -------------------------------------------------------------------------------- 1 | inflectorDefinition = new InflectorDefinition( 17 | 'interface_name', 18 | ['method1' => ['arg1', 'arg2']] 19 | ); 20 | } 21 | 22 | public function testGetInterfaceReturnsTheInterfaceName(): void 23 | { 24 | $this->assertSame('interface_name', $this->inflectorDefinition->getInterface()); 25 | } 26 | 27 | public function testGetMethodsReturnsTheMethods(): void 28 | { 29 | $this->assertSame( 30 | ['method1' => ['arg1', 'arg2']], 31 | $this->inflectorDefinition->getMethods() 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Exception/EntryDoesNotExistExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new EntryDoesNotExistException()); 17 | } 18 | 19 | public function testItIsADomainException(): void 20 | { 21 | $this->assertInstanceOf(DomainException::class, new EntryDoesNotExistException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromTheKey(): void 25 | { 26 | $this->assertSame( 27 | 'No entry found for "example-key".', 28 | EntryDoesNotExistException::fromKey('example-key')->getMessage() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/Exception/NoMatchingFilesExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new NoMatchingFilesException()); 17 | } 18 | 19 | public function testItIsALogicException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new NoMatchingFilesException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromThePattern(): void 25 | { 26 | $this->assertSame( 27 | 'No files found matching pattern: "*.json".', 28 | NoMatchingFilesException::fromPattern('*.json')->getMessage() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/Exception/NotContainerAdapterExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new NotContainerAdapterException()); 17 | } 18 | 19 | public function testItIsALogicException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new NotContainerAdapterException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromThePatterns(): void 25 | { 26 | $this->assertSame( 27 | 'Class "Foo" is not a container adapter.', 28 | NotContainerAdapterException::fromClassName('Foo')->getMessage() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/Exception/MissingDependencyExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new MissingDependencyException()); 17 | } 18 | 19 | public function testItIsALogicException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new MissingDependencyException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromPackageName(): void 25 | { 26 | $this->assertSame( 27 | 'The package "foo/bar" is missing. Please run "composer require foo/bar" to install it.', 28 | MissingDependencyException::fromPackageName('foo/bar')->getMessage() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/Exception/NotClassDefinitionExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new NotClassDefinitionException()); 17 | } 18 | 19 | public function testItIsALogicException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new NotClassDefinitionException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromThePatterns(): void 25 | { 26 | $this->assertSame( 27 | 'Service configuration for "example-service" did not create a class definition.', 28 | NotClassDefinitionException::fromServiceName('example-service')->getMessage() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Tom Oram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/FileReader/YAMLFileReader.php: -------------------------------------------------------------------------------- 1 | getMessage()); 35 | } 36 | 37 | return $config; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/unit/Exception/UnknownSettingExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new UnknownSettingException()); 17 | } 18 | 19 | public function testItIsADomainException(): void 20 | { 21 | $this->assertInstanceOf(DomainException::class, new UnknownSettingException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromSetting(): void 25 | { 26 | $unknownSettingException = UnknownSettingException::fromSetting('unknown', ['setting_a', 'setting_b']); 27 | 28 | $this->assertSame( 29 | 'Setting "unknown" is unknown; valid settings are ["setting_a", "setting_b"].', 30 | $unknownSettingException->getMessage() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/Exception/UnknownFileTypeExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new UnknownFileTypeException()); 17 | } 18 | 19 | public function testItIsADomainException(): void 20 | { 21 | $this->assertInstanceOf(DomainException::class, new UnknownFileTypeException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromFileExtension(): void 25 | { 26 | $unknownFileTypeException = UnknownFileTypeException::fromFileExtension('.yml', ['.json', '.php']); 27 | 28 | $this->assertSame( 29 | 'No reader configured for ".yml" files; readers are available for [".json", ".php"].', 30 | $unknownFileTypeException->getMessage() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/Exception/UnknownContainerExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new UnknownContainerException()); 17 | } 18 | 19 | public function testItIsADomainException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new UnknownContainerException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromFileExtension(): void 25 | { 26 | $unknownContainerException = 27 | UnknownContainerException::fromContainerName('example-container', ['container-a', 'container-b']); 28 | 29 | $this->assertSame( 30 | 'Container example-container is unknown; known containers are ["container-a", "container-b"].', 31 | $unknownContainerException->getMessage() 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering to contribute to this project. 4 | 5 | ## Submitting Pull Requests 6 | 7 | Please submit your pull requests to the `master` branch. 8 | 9 | ## Tests 10 | 11 | All new features or bug fixes should include supporting tests. To run the test 12 | suite you need to first install the dependencies using composer: 13 | 14 | ``` 15 | $ composer install 16 | ``` 17 | 18 | Then the tests can be run using the following command: 19 | 20 | ``` 21 | $ composer test 22 | ``` 23 | 24 | This will also check for the PSR-2 Coding Standard compliance. 25 | 26 | ## Travis 27 | 28 | Once you have submitted a pull request, Travis CI will automatically run the 29 | tests. The tests **must** pass for the PR to be accepted. 30 | 31 | ## Coding Standard 32 | 33 | Please stick PSR-1 and PSR-2 standards - this will be verified by Travis CI: 34 | 35 | * https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 36 | * https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 37 | 38 | Also, keep the code tidy and well refactored - don't let methods get too long 39 | or there be too many levels of indentation. 40 | 41 | Happy coding and thank you for contributing. 42 | -------------------------------------------------------------------------------- /tests/mocks/ExampleContainerAdapter.php: -------------------------------------------------------------------------------- 1 | container = $container; 36 | } 37 | 38 | public function getContainer(): ?object 39 | { 40 | return $this->container; 41 | } 42 | 43 | public function addApplicationConfig(ApplicationConfig $applicationConfig, string $prefix = 'config'): void 44 | { 45 | } 46 | 47 | public function addServiceConfig(ServiceConfig $serviceConfig): void 48 | { 49 | } 50 | 51 | public function addInflectorConfig(InflectorConfig $inflectorConfig): void 52 | { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]) 14 | // Ensure Rector knows we are targeting PHP 8.1+ syntax and features 15 | ->withPhpSets(php81: true) 16 | ->withPreparedSets( 17 | typeDeclarations: true, // Add param/return/var types where possible 18 | earlyReturn: true, // Replace nested ifs with early returns 19 | strictBooleans: true, // Use strict bool checks 20 | phpunitCodeQuality: true, // Modernise PHPUnit usage 21 | deadCode: true // Remove unused code, vars, imports, etc. 22 | ) 23 | ->withSets([ 24 | LevelSetList::UP_TO_PHP_81, // Enforces all rules up to PHP 8.1 25 | SetList::CODE_QUALITY, 26 | SetList::CODING_STYLE, 27 | SetList::DEAD_CODE, 28 | SetList::NAMING, // Enforce clear & consistent naming 29 | SetList::TYPE_DECLARATION, // Add missing type declarations aggressively 30 | SetList::PRIVATIZATION, // Make props/methods private if possible 31 | SetList::STRICT_BOOLEANS, // Strict boolean expressions 32 | ])->withPHPStanConfigs([ 33 | __DIR__ . '/phpstan.neon.dist', 34 | ]); 35 | -------------------------------------------------------------------------------- /src/League/LeagueContainerAdapter.php: -------------------------------------------------------------------------------- 1 | container = $container; 30 | } 31 | 32 | public function addApplicationConfig(ApplicationConfig $applicationConfig, string $prefix = 'config'): void 33 | { 34 | Assertion::string($prefix); 35 | 36 | $this->container->addServiceProvider(new ApplicationConfigServiceProvider($applicationConfig, $prefix)); 37 | } 38 | 39 | public function addServiceConfig(ServiceConfig $serviceConfig): void 40 | { 41 | $this->container->addServiceProvider(new ServiceServiceProvider($serviceConfig)); 42 | } 43 | 44 | public function addInflectorConfig(InflectorConfig $inflectorConfig): void 45 | { 46 | $this->container->addServiceProvider(new InflectorServiceProvider($inflectorConfig)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ContainerAdapterFactory.php: -------------------------------------------------------------------------------- 1 | $config 17 | */ 18 | public function __construct(private readonly array $config) 19 | { 20 | } 21 | 22 | /** 23 | * @param object $container 24 | * 25 | * @throws UnknownContainerException 26 | * @throws NotContainerAdapterException 27 | * 28 | * @return ContainerAdapter 29 | */ 30 | public function create($container) 31 | { 32 | $class = ''; 33 | 34 | foreach ($this->config as $containerClass => $configuratorClass) { 35 | if ($container instanceof $containerClass) { 36 | $class = $configuratorClass; 37 | break; 38 | } 39 | } 40 | 41 | if (!$class) { 42 | throw UnknownContainerException::fromContainerName( 43 | $container::class, 44 | array_keys($this->config) 45 | ); 46 | } 47 | 48 | $instance = new $class(); 49 | 50 | if (!$instance instanceof ContainerAdapter) { 51 | throw NotContainerAdapterException::fromClassName($class); 52 | } 53 | 54 | $instance->setContainer($container); 55 | 56 | return $instance; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ApplicationConfigIterator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ApplicationConfigIterator extends RecursiveIteratorIterator 16 | { 17 | /** 18 | * @var string[] 19 | */ 20 | private array $path = []; 21 | 22 | private readonly string $separator; 23 | 24 | public function __construct(ApplicationConfig $applicationConfig) 25 | { 26 | parent::__construct( 27 | new RecursiveArrayIterator($applicationConfig->asArray()), 28 | RecursiveIteratorIterator::SELF_FIRST 29 | ); 30 | $this->separator = $applicationConfig->getSeparator(); 31 | } 32 | 33 | public function key(): mixed 34 | { 35 | return implode($this->separator, array_merge($this->path, [parent::key()])); 36 | } 37 | 38 | public function next(): void 39 | { 40 | if ($this->callHasChildren()) { 41 | $key = parent::key(); 42 | if (is_string($key) || is_int($key) || ($key instanceof \Stringable)) { 43 | $this->path[] = (string) $key; 44 | } 45 | } 46 | 47 | parent::next(); 48 | } 49 | 50 | public function rewind(): void 51 | { 52 | $this->path = []; 53 | 54 | parent::rewind(); 55 | } 56 | 57 | public function endChildren(): void 58 | { 59 | array_pop($this->path); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ServiceConfig.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class ServiceConfig implements IteratorAggregate 19 | { 20 | /** 21 | * @var ServiceDefinition[] 22 | */ 23 | private array $config = []; 24 | 25 | /** 26 | * @param array, 31 | * methods?:array> 32 | * }> $config 33 | * 34 | * @throws InvalidArgumentException 35 | */ 36 | public function __construct(array $config, bool $singletonDefault = false) 37 | { 38 | Assertion::boolean($singletonDefault); 39 | 40 | foreach ($config as $key => $serviceConfig) { 41 | $this->config[] = new ServiceDefinition($key, $serviceConfig, $singletonDefault); 42 | } 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function getKeys(): array 49 | { 50 | return array_map( 51 | fn (ServiceDefinition $serviceDefinition): string => $serviceDefinition->getName(), 52 | $this->config 53 | ); 54 | } 55 | 56 | public function getIterator(): Traversable 57 | { 58 | return new ArrayIterator($this->config); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/FileReader/FileLocatorTest.php: -------------------------------------------------------------------------------- 1 | fileLocator = new FileLocator(); 20 | } 21 | 22 | public function testItFindsFilesByGlobbing(): void 23 | { 24 | $this->createTestFile('config1.php'); 25 | $this->createTestFile('config2.php'); 26 | $this->createTestFile('config.json'); 27 | 28 | $files = $this->fileLocator->locate($this->getTestPath('*.php')); 29 | 30 | $this->assertSame([ 31 | $this->getTestPath('config1.php'), 32 | $this->getTestPath('config2.php'), 33 | ], $files); 34 | } 35 | 36 | public function testItFindsFindsFilesByGlobbingWithBraces(): void 37 | { 38 | $this->createTestFile('global.php'); 39 | $this->createTestFile('database.local.php'); 40 | $this->createTestFile('nothing.php'); 41 | $this->createTestFile('nothing.php.dist'); 42 | 43 | $files = $this->fileLocator->locate($this->getTestPath('{,*.}{global,local}.php')); 44 | 45 | $this->assertSame([ 46 | $this->getTestPath('global.php'), 47 | $this->getTestPath('database.local.php'), 48 | ], $files); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit/FileReader/JSONFileReaderTest.php: -------------------------------------------------------------------------------- 1 | jsonFileReader = new JSONFileReader(); 23 | } 24 | 25 | public function testItIsAFileReader(): void 26 | { 27 | $this->assertInstanceOf(FileReader::class, $this->jsonFileReader); 28 | } 29 | 30 | public function testItThrowsIfFileDoesNotExist(): void 31 | { 32 | $this->expectException(InvalidArgumentException::class); 33 | 34 | $this->jsonFileReader->read('file-which-does-not-exist'); 35 | } 36 | 37 | public function testReadsAPHPConfigFile(): void 38 | { 39 | $config = ['key' => 'value', 'sub' => ['key' => 'value']]; 40 | 41 | $this->createTestFile('config.json', json_encode($config)); 42 | 43 | $this->assertEquals($config, $this->jsonFileReader->read($this->getTestPath('config.json'))); 44 | } 45 | 46 | public function testItThrowsIfTheConfigIsInvalid(): void 47 | { 48 | $this->expectException(InvalidConfigException::class); 49 | 50 | $this->createTestFile('config.json', 'not json'); 51 | 52 | $this->jsonFileReader->read($this->getTestPath('config.json')); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit/ServiceConfigTest.php: -------------------------------------------------------------------------------- 1 | self::class, 17 | 'singleton' => false, 18 | 'arguments' => ['argument1', 'argument2'], 19 | 'method' => ['setSomething' => ['value']], 20 | ]; 21 | 22 | $config = new ServiceConfig(['service_name' => $serviceConfig]); 23 | 24 | $this->assertEquals( 25 | [new ServiceDefinition('service_name', $serviceConfig)], 26 | iterator_to_array($config) 27 | ); 28 | } 29 | 30 | public function testItProvidesAListOfKeys(): void 31 | { 32 | $serviceConfig = [ 33 | 'class' => self::class, 34 | 'singleton' => false, 35 | 'arguments' => ['argument1', 'argument2'], 36 | 'method' => ['setSomething' => ['value']], 37 | ]; 38 | 39 | $config = new ServiceConfig([ 40 | 'service1' => $serviceConfig, 41 | 'service2' => $serviceConfig, 42 | ]); 43 | 44 | $this->assertSame(['service1', 'service2'], $config->getKeys()); 45 | } 46 | 47 | public function testDefaultValueForSingletonCanBeSetToTrue(): void 48 | { 49 | $serviceConfig = ['class' => self::class]; 50 | 51 | $config = new ServiceConfig(['service_name' => $serviceConfig], true); 52 | 53 | $this->assertTrue(iterator_to_array($config)[0]->isSingleton()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/unit/FileReader/YAMLFileReaderTest.php: -------------------------------------------------------------------------------- 1 | yamlFileReader = new YAMLFileReader(); 24 | } 25 | 26 | public function testItIsAFileReader(): void 27 | { 28 | $this->assertInstanceOf(FileReader::class, $this->yamlFileReader); 29 | } 30 | 31 | public function testItThrowsIfFileDoesNotExist(): void 32 | { 33 | $this->expectException(InvalidArgumentException::class); 34 | 35 | $this->yamlFileReader->read('file-which-does-not-exist'); 36 | } 37 | 38 | public function testReadsAYAMLConfigFile(): void 39 | { 40 | $config = ['key' => 'value', 'sub' => ['key' => 'value']]; 41 | 42 | $this->createTestFile('config.yml', Yaml\Yaml::dump($config)); 43 | 44 | $this->assertEquals($config, $this->yamlFileReader->read($this->getTestPath('config.yml'))); 45 | } 46 | 47 | public function testItThrowsIfTheConfigIsInvalid(): void 48 | { 49 | $this->expectException(InvalidConfigException::class); 50 | 51 | $this->createTestFile('config.yml', '[not yaml;'); 52 | 53 | $this->yamlFileReader->read($this->getTestPath('config.yml')); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/unit/FileReader/PHPFileReaderTest.php: -------------------------------------------------------------------------------- 1 | phpFileReader = new PHPFileReader(); 23 | } 24 | 25 | public function testItIsAFileReader(): void 26 | { 27 | $this->assertInstanceOf(FileReader::class, $this->phpFileReader); 28 | } 29 | 30 | public function testItThrowsIfFileDoesNotExist(): void 31 | { 32 | $this->expectException(InvalidArgumentException::class); 33 | 34 | $this->phpFileReader->read('file-which-does-not-exist'); 35 | } 36 | 37 | public function testReadsAPHPConfigFile(): void 38 | { 39 | $config = ['key' => 'value']; 40 | $code = 'createTestFile('config.php', $code); 43 | 44 | $this->assertEquals($config, $this->phpFileReader->read($this->getTestPath('config.php'))); 45 | } 46 | 47 | public function testItThrowsIfTheConfigIsInvalid(): void 48 | { 49 | $this->expectException(InvalidConfigException::class); 50 | 51 | $code = 'createTestFile('config.php', $code); 53 | 54 | $this->phpFileReader->read($this->getTestPath('config.php')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/League/InflectorServiceProvider.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private readonly \TomPHP\ContainerConfigurator\InflectorConfig $inflectorConfig 22 | ) { 23 | } 24 | 25 | public function provides(string $id): bool 26 | { 27 | return false; 28 | } 29 | 30 | public function register(): void 31 | { 32 | } 33 | 34 | public function boot(): void 35 | { 36 | foreach ($this->inflectorConfig as $definition) { 37 | $this->configureInterface($definition); 38 | } 39 | } 40 | 41 | private function configureInterface(InflectorDefinition $inflectorDefinition): void 42 | { 43 | foreach ($inflectorDefinition->getMethods() as $method => $args) { 44 | $this->addInflectorMethod( 45 | $inflectorDefinition->getInterface(), 46 | $method, 47 | $args 48 | ); 49 | } 50 | } 51 | 52 | /** 53 | * @param array $args 54 | */ 55 | private function addInflectorMethod(string $interface, string $method, array $args): void 56 | { 57 | $this->getContainer() 58 | ->inflector($interface) 59 | ->invokeMethod($method, $args); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/unit/FileReader/HJSONFileReaderTest.php: -------------------------------------------------------------------------------- 1 | hjsonFileReader = new HJSONFileReader(); 23 | } 24 | 25 | public function testItIsAFileReader(): void 26 | { 27 | $this->assertInstanceOf(FileReader::class, $this->hjsonFileReader); 28 | } 29 | 30 | public function testItThrowsIfFileDoesNotExist(): void 31 | { 32 | $this->expectException(InvalidArgumentException::class); 33 | 34 | $this->hjsonFileReader->read('file-which-does-not-exist'); 35 | } 36 | 37 | public function testReadsAPHPConfigFile(): void 38 | { 39 | $config = ['key' => 'value', 'sub' => ['key' => 'value']]; 40 | 41 | $this->createTestFile('config.hjson', json_encode($config)); 42 | 43 | $this->assertEquals($config, $this->hjsonFileReader->read($this->getTestPath('config.hjson'))); 44 | } 45 | 46 | public function testItThrowsIfTheConfigIsInvalid(): void 47 | { 48 | $this->expectException(InvalidConfigException::class); 49 | 50 | $invalidHjson = <<createTestFile('config.hjson', $invalidHjson); 55 | 56 | $this->hjsonFileReader->read($this->getTestPath('config.hjson')); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/FileReader/JSONFileReader.php: -------------------------------------------------------------------------------- 1 | null, 17 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 18 | JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', 19 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 20 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', 21 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', 22 | JSON_ERROR_RECURSION => 'One or more recursive references in the value to be encoded', 23 | JSON_ERROR_INF_OR_NAN => 'One or more NAN or INF values in the value to be encoded', 24 | JSON_ERROR_UNSUPPORTED_TYPE => 'A value of a type that cannot be encoded was given', 25 | JSON_ERROR_INVALID_PROPERTY_NAME => 'A property name that cannot be encoded was given', 26 | JSON_ERROR_UTF16 => 'Malformed UTF-16 characters, possibly incorrectly encoded', 27 | ]; 28 | 29 | public function read(string $filename): mixed 30 | { 31 | Assertion::file($filename); 32 | 33 | $config = json_decode(file_get_contents($filename) ?: '', true); 34 | 35 | if (json_last_error() !== JSON_ERROR_NONE) { 36 | throw InvalidConfigException::fromJSONFileError($filename, $this->getJsonError()); 37 | } 38 | 39 | return $config; 40 | } 41 | 42 | private function getJsonError(): string 43 | { 44 | if (function_exists('json_last_error_msg')) { 45 | return json_last_error_msg(); 46 | } 47 | 48 | return self::JSON_ERRORS[json_last_error()] ?? 'Unknown error'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/acceptance/AbstractContainerAdapterTest.php: -------------------------------------------------------------------------------- 1 | 'example-value']; 27 | 28 | $this->createJSONConfigFile('config.json', $config); 29 | 30 | Configurator::apply() 31 | ->configFromFile($this->getTestPath('config.json')) 32 | ->to($this->container); 33 | 34 | $this->assertSame('example-value', $this->container->get('config.example-key')); 35 | } 36 | 37 | public function testItCanBeConfiguredFromFiles(): void 38 | { 39 | $config = ['example-key' => 'example-value']; 40 | 41 | $this->createJSONConfigFile('config.json', $config); 42 | 43 | Configurator::apply() 44 | ->configFromFiles($this->getTestPath('*')) 45 | ->to($this->container); 46 | 47 | $this->assertSame('example-value', $this->container->get('config.example-key')); 48 | } 49 | 50 | public function testItAddToConfigUsingFiles(): void 51 | { 52 | $config = ['keyB' => 'valueB']; 53 | 54 | $this->createJSONConfigFile('config.json', $config); 55 | 56 | Configurator::apply() 57 | ->configFromArray(['keyA' => 'valueA', 'keyB' => 'valueX']) 58 | ->configFromFiles($this->getTestPath('*')) 59 | ->to($this->container); 60 | 61 | $this->assertSame('valueA', $this->container->get('config.keyA')); 62 | $this->assertSame('valueB', $this->container->get('config.keyB')); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/support/TestFileCreator.php: -------------------------------------------------------------------------------- 1 | deleteTestFiles(); 17 | } 18 | 19 | /** 20 | * @param string $name 21 | */ 22 | protected function getTestPath($name): string 23 | { 24 | $this->ensurePathExists(); 25 | 26 | return sprintf('%s/%s', $this->configFilePath, $name); 27 | } 28 | 29 | /** 30 | * @param string $filename 31 | */ 32 | protected function createPHPConfigFile($filename, array $config) 33 | { 34 | $code = 'createTestFile($filename, $code); 37 | } 38 | 39 | /** 40 | * @param string $filename 41 | */ 42 | protected function createJSONConfigFile($filename, array $config) 43 | { 44 | $code = json_encode($config); 45 | 46 | $this->createTestFile($filename, $code); 47 | } 48 | 49 | /** 50 | * @param string $name 51 | * @param string $content 52 | */ 53 | protected function createTestFile($name, $content = 'test content') 54 | { 55 | $this->ensurePathExists(); 56 | 57 | file_put_contents(sprintf('%s/%s', $this->configFilePath, $name), $content); 58 | } 59 | 60 | private function deleteTestFiles(): void 61 | { 62 | $this->ensurePathExists(); 63 | 64 | // Test for safety! 65 | if (!str_starts_with($this->configFilePath, __DIR__)) { 66 | throw new \Exception('DANGER!!! - Config file is not local to this project'); 67 | } 68 | 69 | $files = glob($this->configFilePath . '/*'); 70 | 71 | foreach ($files as $file) { 72 | unlink($file); 73 | } 74 | } 75 | 76 | private function ensurePathExists(): void 77 | { 78 | $this->configFilePath = __DIR__ . '/../.test-config'; 79 | 80 | if (!file_exists($this->configFilePath)) { 81 | mkdir($this->configFilePath); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/FileReader/ReaderFactory.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $readers = []; 21 | 22 | /** 23 | * @param array $config 24 | */ 25 | public function __construct(private readonly array $config) 26 | { 27 | } 28 | 29 | /** 30 | * @throws InvalidArgumentException 31 | */ 32 | public function create(string $filename): FileReader 33 | { 34 | Assertion::file($filename); 35 | 36 | $readerClass = $this->getReaderClass($filename); 37 | 38 | if (!isset($this->readers[$readerClass])) { 39 | $readerClassInstance = new $readerClass(); 40 | if (!$readerClassInstance instanceof FileReader) { 41 | throw NotFileReaderException::fromClassName($readerClass); 42 | } 43 | 44 | $this->readers[$readerClass] = $readerClassInstance; 45 | } 46 | 47 | return $this->readers[$readerClass]; 48 | } 49 | 50 | /** 51 | * @throws UnknownFileTypeException 52 | */ 53 | private function getReaderClass(string $filename): string 54 | { 55 | $readerClass = null; 56 | 57 | foreach ($this->config as $extension => $className) { 58 | if ($this->endsWith($filename, $extension)) { 59 | $readerClass = $className; 60 | break; 61 | } 62 | } 63 | 64 | if ($readerClass === null) { 65 | throw UnknownFileTypeException::fromFileExtension( 66 | $filename, 67 | array_keys($this->config) 68 | ); 69 | } 70 | 71 | return $readerClass; 72 | } 73 | 74 | private function endsWith(string $haystack, string $needle): bool 75 | { 76 | return str_ends_with($haystack, $needle); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/League/ApplicationConfigServiceProvider.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private readonly array $provides; 23 | 24 | /** 25 | * @throws InvalidArgumentException 26 | */ 27 | public function __construct(/** 28 | * @var ApplicationConfig 29 | */ 30 | private readonly ApplicationConfig $applicationConfig, 31 | string $prefix 32 | ) { 33 | Assertion::string($prefix); 34 | 35 | $this->identifier = self::class . ($prefix !== '' ? $prefix : uniqid()); 36 | $this->prefix = $prefix; 37 | $this->provides = array_merge(array_map( 38 | fn (int|string $key): string => $this->keyPrefix() . $key, 39 | $this->applicationConfig->getKeys() 40 | ), [$this->prefix]); 41 | } 42 | 43 | public function provides(string $id): bool 44 | { 45 | return in_array($id, $this->provides); 46 | } 47 | 48 | public function register(): void 49 | { 50 | $prefix = $this->keyPrefix(); 51 | 52 | if ($prefix !== '' && $prefix !== '0') { 53 | $this->container?->addShared($this->prefix, fn (): array => $this->applicationConfig->asArray()); 54 | } 55 | 56 | foreach ($this->applicationConfig as $key => $value) { 57 | // @phpstan-ignore-next-line 58 | if (!is_string($key) && !is_int($key) && !($key instanceof \Stringable)) { 59 | continue; 60 | } 61 | 62 | $this->container?->addShared($prefix . $key, fn (): mixed => $value); 63 | } 64 | } 65 | 66 | private function keyPrefix(): string 67 | { 68 | if ($this->prefix === '' || $this->prefix === '0') { 69 | return ''; 70 | } 71 | 72 | return $this->prefix . $this->applicationConfig->getSeparator(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Exception/InvalidConfigException.php: -------------------------------------------------------------------------------- 1 | containerAdapterFactory = new ContainerAdapterFactory([ 23 | ExampleContainer::class => ExampleContainerAdapter::class, 24 | ]); 25 | } 26 | 27 | public function testItCreatesAnInstanceOfTheContainerAdapter(): void 28 | { 29 | $this->assertInstanceOf( 30 | ExampleContainerAdapter::class, 31 | $this->containerAdapterFactory->create(new ExampleContainer()) 32 | ); 33 | } 34 | 35 | public function testItCreatesAnInstanceOfTheConfiguratorForSubclassedContainer(): void 36 | { 37 | $this->assertInstanceOf( 38 | ExampleContainerAdapter::class, 39 | $this->containerAdapterFactory->create(new ExampleExtendedContainer()) 40 | ); 41 | } 42 | 43 | public function testItThrowsIfContainerIsNotKnown(): void 44 | { 45 | $this->expectException(UnknownContainerException::class); 46 | 47 | $this->containerAdapterFactory->create(new \stdClass()); 48 | } 49 | 50 | public function testItThrowsIfNotAContainerAdapter(): void 51 | { 52 | $this->containerAdapterFactory = new ContainerAdapterFactory([ 53 | ExampleContainer::class => NotContainerAdapter::class, 54 | ]); 55 | 56 | $this->expectException(NotContainerAdapterException::class); 57 | 58 | $this->containerAdapterFactory->create(new ExampleContainer()); 59 | } 60 | 61 | public function testItSetsTheContainerOnTheConfigurator(): void 62 | { 63 | $exampleContainer = new ExampleContainer(); 64 | $containerAdapter = $this->containerAdapterFactory->create($exampleContainer); 65 | 66 | $this->assertSame($exampleContainer, $containerAdapter->getContainer()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/unit/ConfiguratorTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 25 | 26 | Configurator::apply()->configFromFile($this->getTestPath('config.php')); 27 | } 28 | 29 | public function testItThrowsAnExceptionWhenNoFilesAreNotFound(): void 30 | { 31 | $this->expectException(NoMatchingFilesException::class); 32 | 33 | Configurator::apply()->configFromFiles($this->getTestPath('config.php')); 34 | } 35 | 36 | public function testItThrowsWhenAnUnknownSettingIsSet(): void 37 | { 38 | $this->expectException(UnknownSettingException::class); 39 | 40 | Configurator::apply()->withSetting('unknown_setting', 'value'); 41 | } 42 | 43 | public function testTheContainerIdentifierStringIsAlwaysTheSame(): void 44 | { 45 | $this->assertSame(Configurator::container(), Configurator::container()); 46 | } 47 | 48 | public function testItCanAcceptADifferentFileReader(): void 49 | { 50 | $container = new Container(); 51 | $this->createTestFile('custom.xxx'); 52 | CustomFileReader::reset(); 53 | 54 | $configFile = $this->getTestPath('custom.xxx'); 55 | Configurator::apply() 56 | ->withFileReader('.xxx', CustomFileReader::class) 57 | ->configFromFile($configFile) 58 | ->to($container); 59 | 60 | $this->assertSame([$configFile], CustomFileReader::getReads()); 61 | } 62 | 63 | public function testItCanUseDifferentContainerAdapters(): void 64 | { 65 | $exampleContainer = new ExampleContainer(); 66 | ExampleContainerAdapter::reset(); 67 | 68 | Configurator::apply() 69 | ->withContainerAdapter(ExampleContainer::class, ExampleContainerAdapter::class) 70 | ->configFromArray([]) 71 | ->to($exampleContainer); 72 | 73 | $this->assertSame(1, ExampleContainerAdapter::getNumberOfInstances()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Instructions 2 | 3 | ## v0.4.\* -> v0.5.\* 4 | 5 | There are three major changes which occurred in this update: 6 | 7 | ### Package Renamed 8 | 9 | This package is now called **Container Configurator**. It's not installed with 10 | the command: 11 | 12 | ``` 13 | composer require tomphp/container-configurator 14 | ``` 15 | 16 | For existing projects, you can fix this by running: 17 | 18 | ``` 19 | composer remove tomphp/config-service-provider && composer require tomphp/container-configurator 20 | ``` 21 | 22 | To require it in your project you need to: 23 | 24 | ``` 25 | use TomPHP\ContainerConfigurator\Configurator; 26 | ``` 27 | 28 | ### New API 29 | 30 | The API had a major rewrite in v0.5.0. Rather than creating a service provider 31 | like this: 32 | 33 | ```php 34 | $container->addServiceProvider(ConfigServiceProvider::fromFile(['config.php']); 35 | ``` 36 | 37 | You now configure the container like this: 38 | 39 | ```php 40 | Configurator::apply() 41 | ->configFromFiles('config/*.inc.php') 42 | ->to($container); 43 | ``` 44 | 45 | If you had multiple file pattern matches, you can chain more `configFromFiles` 46 | calls like so: 47 | 48 | ```php 49 | Configurator::apply() 50 | ->configFromFiles('*.global.php') 51 | ->configFromFiles('*.local.php') 52 | ->to($container); 53 | ``` 54 | 55 | Also: 56 | 57 | ```php 58 | $container->addServiceProvider(ConfigServiceProvider::fromConfig($config); 59 | ``` 60 | 61 | Would now be replaced with: 62 | 63 | ```php 64 | Configurator::apply() 65 | ->configFromArray($config) 66 | ->to($container); 67 | ``` 68 | 69 | ### Default DI Config Has Changed Structure 70 | 71 | There's been some changes to where the service and inflector config go by 72 | default. Before v0.5.0 the config would look like this: 73 | 74 | ```php 75 | $config = [ 76 | 'di' => [ 77 | // Config for services goes here 78 | ], 79 | 'inflectors' => [ 80 | // Config for inflectors goes here 81 | ], 82 | ]; 83 | ``` 84 | 85 | By default, v0.5.0 now has this format: 86 | 87 | ```php 88 | $config = [ 89 | 'di' => [ 90 | 'services' => [ 91 | // Config for services goes here 92 | ], 93 | 'inflectors' => [ 94 | // Config for inflectors goes here 95 | ], 96 | ], 97 | ]; 98 | ``` 99 | 100 | If you don't wish to adopt this new format, you can set the Configurator up to 101 | behave in the old way by using the following settings: 102 | 103 | ```php 104 | Configurator::apply() 105 | ->configFromArray($config) 106 | ->withSetting(Configurator::SETTING_SERVICES_KEY, 'di') 107 | ->withSetting(Configurator::SETTING_INFLECTORS_KEY, 'inflectors') 108 | ->to($container); 109 | ``` 110 | -------------------------------------------------------------------------------- /tests/unit/Exception/InvalidConfigExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, new InvalidConfigException()); 17 | } 18 | 19 | public function testItIsALogicException(): void 20 | { 21 | $this->assertInstanceOf(LogicException::class, new InvalidConfigException()); 22 | } 23 | 24 | public function testItCanBeCreatedFromTheFileName(): void 25 | { 26 | $this->assertSame( 27 | '"example.cfg" does not return a PHP array.', 28 | InvalidConfigException::fromPHPFileError('example.cfg')->getMessage() 29 | ); 30 | } 31 | 32 | public function testItCanBeCreatedWithAJSONFileError(): void 33 | { 34 | $this->assertSame( 35 | 'Invalid JSON in "example.json": JSON Error Message', 36 | InvalidConfigException::fromJSONFileError('example.json', 'JSON Error Message')->getMessage() 37 | ); 38 | } 39 | 40 | public function testItCanBeCreatedFromYAMLFileError(): void 41 | { 42 | $this->assertSame( 43 | 'Invalid YAML in "example.yml": YAML Error Message', 44 | InvalidConfigException::fromYAMLFileError('example.yml', 'YAML Error Message')->getMessage() 45 | ); 46 | } 47 | 48 | public function testItCanBeCreatedFromNameWhenClassAndFactoryAreSpecified(): void 49 | { 50 | $this->assertSame( 51 | 'Both "class" and "factory" are specified for service "example"; these cannot be used together.', 52 | InvalidConfigException::fromNameWhenClassAndFactorySpecified('example')->getMessage() 53 | ); 54 | } 55 | 56 | public function testItCanBeCreatedFromNameWhenClassAndServiceAreSpecified(): void 57 | { 58 | $this->assertSame( 59 | 'Both "class" and "service" are specified for service "example"; these cannot be used together.', 60 | InvalidConfigException::fromNameWhenClassAndServiceSpecified('example')->getMessage() 61 | ); 62 | } 63 | 64 | public function testItCanBeCreatedFromNameWhenFactoryAndServiceAreSpecified(): void 65 | { 66 | $this->assertSame( 67 | 'Both "factory" and "service" are specified for service "example"; these cannot be used together.', 68 | InvalidConfigException::fromNameWhenFactoryAndServiceSpecified('example')->getMessage() 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomphp/container-configurator", 3 | "description": "Configure your application and the Dependency Injection Container (DIC) via config arrays or config files.", 4 | "license": "MIT", 5 | "type": "library", 6 | "homepage": "https://github.com/tomphp/config-service-provider", 7 | "keywords": ["di", "dependency injection", "container", "league"], 8 | "authors": [ 9 | { 10 | "name": "Tom Oram", 11 | "email": "tom@x2k.co.uk", 12 | "homepage": "https://github.com/tomphp", 13 | "role": "Developer" 14 | } 15 | ], 16 | "suggest": { 17 | "league/container": "Small but powerful dependency injection container http://container.thephpleague.com", 18 | "pimple/pimple": "A small PHP dependency injection container https://github.com/silexphp/Pimple", 19 | "symfony/yaml": "For reading configuration from YAML files", 20 | "laktak/hjson": "For reading configuration from HJSON files" 21 | }, 22 | "require": { 23 | "php": "^8.1", 24 | "beberlei/assert": "^3.3", 25 | "tomphp/exception-constructor-tools": "^2.0" 26 | }, 27 | "require-dev": { 28 | "friendsofphp/php-cs-fixer": "^3.85", 29 | "laktak/hjson": "^2.3", 30 | "league/container": "^5.1", 31 | "phpstan/phpstan": "^2.1", 32 | "phpunit/phpunit": "^9.6 || ^7.5 || ^11.5", 33 | "pimple/pimple": "^3.5", 34 | "rector/rector": "^2.1", 35 | "squizlabs/php_codesniffer": "^3.9", 36 | "symfony/yaml": "^6.4" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "TomPHP\\ContainerConfigurator\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "tests\\unit\\TomPHP\\ContainerConfigurator\\": "tests/unit/", 46 | "tests\\acceptance\\": "tests/acceptance/", 47 | "tests\\support\\": "tests/support/", 48 | "tests\\mocks\\": "tests/mocks/" 49 | }, 50 | "files": [ 51 | "vendor/phpunit/phpunit/src/Framework/Assert/Functions.php" 52 | ] 53 | }, 54 | "config": { 55 | "sort-packages": true 56 | }, 57 | "scripts": { 58 | "cs:fix": [ 59 | "phpcbf --standard=psr2 src tests; exit 0", 60 | "php-cs-fixer fix --verbose; exit 0" 61 | ], 62 | "cs:check": [ 63 | "phpcs --standard=psr2 src tests", 64 | "php-cs-fixer fix --dry-run --verbose" 65 | ], 66 | "analyse": [ 67 | "phpstan analyse --memory-limit=1G" 68 | ], 69 | "rector": [ 70 | "rector process src tests --ansi" 71 | ], 72 | "rector:dry": [ 73 | "rector process src tests --dry-run --ansi" 74 | ], 75 | "test": [ 76 | "@cs:check", 77 | "@analyse", 78 | "phpunit --colors=always" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/FileReader/HJSONFileReader.php: -------------------------------------------------------------------------------- 1 | null, 20 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 21 | JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', 22 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 23 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', 24 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', 25 | JSON_ERROR_RECURSION => 'One or more recursive references in the value to be encoded', 26 | JSON_ERROR_INF_OR_NAN => 'One or more NAN or INF values in the value to be encoded', 27 | JSON_ERROR_UNSUPPORTED_TYPE => 'A value of a type that cannot be encoded was given', 28 | JSON_ERROR_INVALID_PROPERTY_NAME => 'A property name that cannot be encoded was given', 29 | JSON_ERROR_UTF16 => 'Malformed UTF-16 characters, possibly incorrectly encoded', 30 | ]; 31 | 32 | private readonly HJSONParser $hjsonParser; 33 | 34 | /** 35 | * @throws MissingDependencyException 36 | */ 37 | public function __construct() 38 | { 39 | if (!class_exists(HJSONParser::class)) { 40 | throw MissingDependencyException::fromPackageName('laktak/hjson'); 41 | } 42 | 43 | $this->hjsonParser = new HJSONParser(); 44 | } 45 | 46 | public function read(string $filename): mixed 47 | { 48 | Assertion::file($filename); 49 | 50 | try { 51 | $config = $this->hjsonParser->parse( 52 | file_get_contents($filename), 53 | [ 54 | 'assoc' => true, // boolean, return associative array instead of object 55 | ] 56 | ); 57 | } catch (HJSONException $hjsonException) { 58 | throw InvalidConfigException::fromHJSONFileError($filename, $hjsonException->getMessage()); 59 | } 60 | 61 | if (JSON_ERROR_NONE !== json_last_error()) { 62 | throw InvalidConfigException::fromHJSONFileError($filename, $this->getJsonError()); 63 | } 64 | 65 | return $config; 66 | } 67 | 68 | private function getJsonError(): string 69 | { 70 | if (function_exists('json_last_error_msg')) { 71 | return json_last_error_msg(); 72 | } 73 | 74 | return self::JSON_ERRORS[json_last_error()] ?? 'Unknown error'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/unit/FileReader/ReaderFactoryTest.php: -------------------------------------------------------------------------------- 1 | readerFactory = new ReaderFactory([ 25 | '.php' => PHPFileReader::class, 26 | '.json' => JSONFileReader::class, 27 | '.yaml' => YAMLFileReader::class, 28 | '.yml' => YAMLFileReader::class, 29 | ]); 30 | } 31 | 32 | /** 33 | * @dataProvider providerCreatesAppropriateFileReader 34 | */ 35 | public function testCreatesAppropriateFileReader(string $extension, string $fileReaderClass): void 36 | { 37 | $filename = 'test' . $extension; 38 | 39 | $this->createTestFile($filename); 40 | 41 | $fileReader = $this->readerFactory->create($this->getTestPath($filename)); 42 | 43 | $this->assertInstanceOf($fileReaderClass, $fileReader); 44 | } 45 | 46 | /** 47 | * @return \Generator 48 | */ 49 | public function providerCreatesAppropriateFileReader() 50 | { 51 | $extensions = [ 52 | '.json' => JSONFileReader::class, 53 | '.php' => PHPFileReader::class, 54 | '.yaml' => YAMLFileReader::class, 55 | '.yml' => YAMLFileReader::class, 56 | ]; 57 | 58 | foreach ($extensions as $extension => $fileReaderClass) { 59 | yield [ 60 | $extension, 61 | $fileReaderClass, 62 | ]; 63 | } 64 | } 65 | 66 | public function testReturnsTheSameReaderForTheSameFileType(): void 67 | { 68 | $this->createTestFile('test1.php'); 69 | $this->createTestFile('test2.php'); 70 | 71 | $fileReader = $this->readerFactory->create($this->getTestPath('test1.php')); 72 | $reader2 = $this->readerFactory->create($this->getTestPath('test2.php')); 73 | 74 | $this->assertSame($fileReader, $reader2); 75 | } 76 | 77 | public function testItThrowsIfTheArgumentIsNotAFileName(): void 78 | { 79 | $this->expectException(InvalidArgumentException::class); 80 | 81 | $this->readerFactory->create('missing-file.xxx'); 82 | } 83 | 84 | public function testItThrowsIfThereIsNoRegisteredReaderForGivenFileType(): void 85 | { 86 | $this->createTestFile('test.unknown'); 87 | 88 | $this->expectException(UnknownFileTypeException::class); 89 | 90 | $this->readerFactory->create($this->getTestPath('test.unknown')); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/acceptance/SupportsInflectorConfig.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'services' => [ 19 | 'example' => [ 20 | 'class' => ExampleClass::class, 21 | ], 22 | ], 23 | 'inflectors' => [ 24 | ExampleInterface::class => [ 25 | 'setValue' => ['test_value'], 26 | ], 27 | ], 28 | ], 29 | ]; 30 | 31 | Configurator::apply() 32 | ->configFromArray($config) 33 | ->to($this->container); 34 | 35 | $this->assertEquals( 36 | 'test_value', 37 | $this->container->get('example')->getValue() 38 | ); 39 | } 40 | 41 | public function testItSetsUpAnInflectorForServiceFactory(): void 42 | { 43 | $config = [ 44 | 'class_name' => ExampleClass::class, 45 | 'di' => [ 46 | 'services' => [ 47 | 'example' => [ 48 | 'factory' => ExampleFactory::class, 49 | 'arguments' => [ 50 | 'config.class_name', 51 | ], 52 | ], 53 | ], 54 | 'inflectors' => [ 55 | ExampleInterface::class => [ 56 | 'setValue' => ['test_value'], 57 | ], 58 | ], 59 | ], 60 | ]; 61 | 62 | Configurator::apply() 63 | ->configFromArray($config) 64 | ->to($this->container); 65 | 66 | $this->assertEquals( 67 | 'test_value', 68 | $this->container->get('example')->getValue() 69 | ); 70 | } 71 | 72 | public function testItResolvesInflectorArguments(): void 73 | { 74 | $config = [ 75 | 'argument' => 'test_value', 76 | 'di' => [ 77 | 'services' => [ 78 | 'example' => [ 79 | 'class' => ExampleClass::class, 80 | ], 81 | ], 82 | 'inflectors' => [ 83 | ExampleInterface::class => [ 84 | 'setValue' => ['config.argument'], 85 | ], 86 | ], 87 | ], 88 | ]; 89 | 90 | Configurator::apply() 91 | ->configFromArray($config) 92 | ->to($this->container); 93 | 94 | $this->assertEquals( 95 | 'test_value', 96 | $this->container->get('example')->getValue() 97 | ); 98 | } 99 | 100 | public function testItSetsUpAnInflectorUsingCustomInflectorsKey(): void 101 | { 102 | $config = [ 103 | 'di' => [ 104 | 'services' => [ 105 | 'example' => [ 106 | 'class' => ExampleClass::class, 107 | ], 108 | ], 109 | ], 110 | 'inflectors' => [ 111 | ExampleInterface::class => [ 112 | 'setValue' => ['test_value'], 113 | ], 114 | ], 115 | ]; 116 | 117 | Configurator::apply() 118 | ->configFromArray($config) 119 | ->withSetting(Configurator::SETTING_INFLECTORS_KEY, 'inflectors') 120 | ->to($this->container); 121 | 122 | $this->assertEquals( 123 | 'test_value', 124 | $this->container->get('example')->getValue() 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/unit/ApplicationConfigIteratorTest.php: -------------------------------------------------------------------------------- 1 | 'valueA', 16 | 'keyB' => 'valueB', 17 | ]); 18 | 19 | $this->assertSame( 20 | [ 21 | 'keyA' => 'valueA', 22 | 'keyB' => 'valueB', 23 | ], 24 | iterator_to_array($applicationConfig) 25 | ); 26 | } 27 | 28 | public function testItIteratesRecursively(): void 29 | { 30 | $applicationConfig = new ApplicationConfig([ 31 | 'group1' => [ 32 | 'keyA' => 'valueA', 33 | ], 34 | 'group2' => [ 35 | 'keyB' => 'valueB', 36 | ], 37 | ]); 38 | 39 | $this->assertSame( 40 | [ 41 | 'group1' => [ 42 | 'keyA' => 'valueA', 43 | ], 44 | 'group1.keyA' => 'valueA', 45 | 'group2' => [ 46 | 'keyB' => 'valueB', 47 | ], 48 | 'group2.keyB' => 'valueB', 49 | ], 50 | iterator_to_array($applicationConfig) 51 | ); 52 | } 53 | 54 | public function testItGoesMultipleLevels(): void 55 | { 56 | $applicationConfig = new ApplicationConfig([ 57 | 'group1' => [ 58 | 'keyA' => 'valueA', 59 | 'group2' => [ 60 | 'keyB' => 'valueB', 61 | ], 62 | ], 63 | ]); 64 | 65 | $this->assertSame( 66 | [ 67 | 'group1' => [ 68 | 'keyA' => 'valueA', 69 | 'group2' => [ 70 | 'keyB' => 'valueB', 71 | ], 72 | ], 73 | 'group1.keyA' => 'valueA', 74 | 'group1.group2' => [ 75 | 'keyB' => 'valueB', 76 | ], 77 | 'group1.group2.keyB' => 'valueB', 78 | ], 79 | iterator_to_array($applicationConfig) 80 | ); 81 | } 82 | 83 | public function testItRewinds(): void 84 | { 85 | $applicationConfig = new ApplicationConfig([ 86 | 'group1' => [ 87 | 'keyA' => 'valueA', 88 | 'keyB' => 'valueB', 89 | 'keyC' => 'valueC', 90 | ], 91 | ]); 92 | 93 | $applicationConfig->getIterator()->next(); 94 | $applicationConfig->getIterator()->next(); 95 | $applicationConfig->getIterator()->next(); 96 | 97 | $this->assertSame( 98 | [ 99 | 'group1' => [ 100 | 'keyA' => 'valueA', 101 | 'keyB' => 'valueB', 102 | 'keyC' => 'valueC', 103 | ], 104 | 'group1.keyA' => 'valueA', 105 | 'group1.keyB' => 'valueB', 106 | 'group1.keyC' => 'valueC', 107 | ], 108 | iterator_to_array($applicationConfig) 109 | ); 110 | } 111 | 112 | public function testItUsesADifferentSeparator(): void 113 | { 114 | $applicationConfig = new ApplicationConfig([ 115 | 'group1' => [ 116 | 'keyA' => 'valueA', 117 | ], 118 | ], '->'); 119 | 120 | $this->assertSame( 121 | [ 122 | 'group1' => [ 123 | 'keyA' => 'valueA', 124 | ], 125 | 'group1->keyA' => 'valueA', 126 | ], 127 | iterator_to_array($applicationConfig) 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/unit/ServiceDefinitionTest.php: -------------------------------------------------------------------------------- 1 | self::class, 17 | 'singleton' => false, 18 | 'arguments' => ['argument1', 'argument2'], 19 | 'methods' => ['setSomething' => ['value']], 20 | ]; 21 | 22 | $serviceDefinition = new ServiceDefinition('service_name', $config); 23 | 24 | $this->assertSame('service_name', $serviceDefinition->getName()); 25 | $this->assertSame(self::class, $serviceDefinition->getClass()); 26 | $this->assertFalse($serviceDefinition->isFactory()); 27 | $this->assertFalse($serviceDefinition->isSingleton()); 28 | $this->assertSame(['argument1', 'argument2'], $serviceDefinition->getArguments()); 29 | $this->assertSame(['setSomething' => ['value']], $serviceDefinition->getMethods()); 30 | } 31 | 32 | public function testClassDefaultsToKey(): void 33 | { 34 | $serviceDefinition = new ServiceDefinition('service_name', []); 35 | 36 | $this->assertSame('service_name', $serviceDefinition->getClass()); 37 | } 38 | 39 | public function testSingletonDefaultsToFalse(): void 40 | { 41 | $serviceDefinition = new ServiceDefinition('service_name', []); 42 | 43 | $this->assertFalse($serviceDefinition->isSingleton()); 44 | } 45 | 46 | public function testSingletonDefaultCanBeSetToToTrue(): void 47 | { 48 | $serviceDefinition = new ServiceDefinition('service_name', [], true); 49 | 50 | $this->assertTrue($serviceDefinition->isSingleton()); 51 | } 52 | 53 | public function testArgumentsDefaultToAnEmptyList(): void 54 | { 55 | $serviceDefinition = new ServiceDefinition('service_name', []); 56 | 57 | $this->assertSame([], $serviceDefinition->getArguments()); 58 | } 59 | 60 | public function testMethodsDefaultToAnEmptyList(): void 61 | { 62 | $serviceDefinition = new ServiceDefinition('service_name', []); 63 | 64 | $this->assertSame([], $serviceDefinition->getMethods()); 65 | } 66 | 67 | public function testServiceFactoryDefinition(): void 68 | { 69 | $serviceDefinition = new ServiceDefinition('service_name', ['factory' => self::class]); 70 | 71 | $this->assertTrue($serviceDefinition->isFactory()); 72 | $this->assertFalse($serviceDefinition->isAlias()); 73 | $this->assertSame(self::class, $serviceDefinition->getClass()); 74 | } 75 | 76 | public function testServiceAliasDefinition(): void 77 | { 78 | $serviceDefinition = new ServiceDefinition('service_name', ['service' => self::class]); 79 | 80 | $this->assertTrue($serviceDefinition->isAlias()); 81 | $this->assertFalse($serviceDefinition->isFactory()); 82 | $this->assertSame(self::class, $serviceDefinition->getClass()); 83 | } 84 | 85 | public function testItThrowIfClassAndFactoryAreDefined(): void 86 | { 87 | $this->expectException(InvalidConfigException::class); 88 | 89 | new ServiceDefinition('service_name', ['class' => self::class, 'factory' => self::class]); 90 | } 91 | 92 | public function testItThrowIfClassAndServiceAreDefined(): void 93 | { 94 | $this->expectException(InvalidConfigException::class); 95 | 96 | new ServiceDefinition('service_name', ['class' => self::class, 'service' => self::class]); 97 | } 98 | 99 | public function testItThrowIfFactoryAndServiceAreDefined(): void 100 | { 101 | $this->expectException(InvalidConfigException::class); 102 | 103 | new ServiceDefinition('service_name', ['factory' => self::class, 'service' => self::class]); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ServiceDefinition.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | private readonly array $arguments; 30 | 31 | /** 32 | * @var array> 33 | */ 34 | private readonly array $methods; 35 | 36 | /** 37 | * @param array{ 38 | * singleton?:bool, 39 | * factory?:mixed, 40 | * service?:mixed, 41 | * arguments?:array, 42 | * methods?:array> 44 | * } $config 45 | * 46 | * @throws InvalidArgumentException 47 | * @throws InvalidConfigException 48 | */ 49 | public function __construct(string $name, array $config, bool $singletonDefault = false) 50 | { 51 | Assertion::string($name); 52 | Assertion::boolean($singletonDefault); 53 | 54 | $this->name = $name; 55 | $this->class = $this->className($name, $config); 56 | $this->isSingleton = 57 | isset($config['singleton']) && is_bool($config['singleton']) ? $config['singleton'] : $singletonDefault; 58 | $this->isFactory = isset($config['factory']); 59 | $this->isAlias = isset($config['service']); 60 | $this->arguments = isset($config['arguments']) && is_array($config['arguments']) ? $config['arguments'] : []; 61 | $this->methods = isset($config['methods']) && is_array($config['methods']) ? $config['methods'] : []; 62 | } 63 | 64 | public function getName(): string 65 | { 66 | return $this->name; 67 | } 68 | 69 | public function getClass(): string 70 | { 71 | return $this->class; 72 | } 73 | 74 | public function isSingleton(): bool 75 | { 76 | return $this->isSingleton; 77 | } 78 | 79 | public function isFactory(): bool 80 | { 81 | return $this->isFactory; 82 | } 83 | 84 | public function isAlias(): bool 85 | { 86 | return $this->isAlias; 87 | } 88 | 89 | /** 90 | * @return array 91 | */ 92 | public function getArguments(): array 93 | { 94 | return $this->arguments; 95 | } 96 | 97 | /** 98 | * @return array> 99 | */ 100 | public function getMethods(): array 101 | { 102 | return $this->methods; 103 | } 104 | 105 | /** 106 | * @param array $config 107 | * 108 | * @throws InvalidConfigException 109 | */ 110 | private function className(string $name, array $config): string 111 | { 112 | if (isset($config['class']) && isset($config['factory'])) { 113 | throw InvalidConfigException::fromNameWhenClassAndFactorySpecified($name); 114 | } 115 | 116 | if (isset($config['class']) && isset($config['service'])) { 117 | throw InvalidConfigException::fromNameWhenClassAndServiceSpecified($name); 118 | } 119 | 120 | if (isset($config['factory']) && isset($config['service'])) { 121 | throw InvalidConfigException::fromNameWhenFactoryAndServiceSpecified($name); 122 | } 123 | 124 | if (isset($config['service']) && is_string($config['service'])) { 125 | return $config['service']; 126 | } 127 | 128 | if (isset($config['class']) && is_string($config['class'])) { 129 | return $config['class']; 130 | } 131 | 132 | if (isset($config['factory']) && is_string($config['factory'])) { 133 | return $config['factory']; 134 | } 135 | 136 | return $name; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/ApplicationConfig.php: -------------------------------------------------------------------------------- 1 | 19 | * @implements IteratorAggregate 20 | */ 21 | final class ApplicationConfig implements ArrayAccess, IteratorAggregate 22 | { 23 | /** 24 | * @var non-empty-string 25 | */ 26 | private string $separator; 27 | 28 | /** 29 | * @param non-empty-string $separator 30 | * 31 | * @throws InvalidArgumentException 32 | */ 33 | public function __construct(/** 34 | * @var array 35 | */ 36 | private array $config, 37 | string $separator = '.' 38 | ) { 39 | \Assert\that($separator)->string()->notEmpty(); 40 | $this->separator = $separator; 41 | } 42 | 43 | /** 44 | * @param array $config 45 | */ 46 | public function merge(array $config): void 47 | { 48 | $this->config = array_replace_recursive($this->config, $config); 49 | } 50 | 51 | /** 52 | * @param non-empty-string $separator 53 | * 54 | * @throws InvalidArgumentException 55 | */ 56 | public function setSeparator(string $separator): void 57 | { 58 | \Assert\that($separator)->string()->notEmpty(); 59 | 60 | $this->separator = $separator; 61 | } 62 | 63 | public function getIterator(): Traversable 64 | { 65 | return new ApplicationConfigIterator($this); 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | public function getKeys(): array 72 | { 73 | return array_keys(iterator_to_array(new ApplicationConfigIterator($this))); 74 | } 75 | 76 | public function offsetExists(mixed $offset): bool 77 | { 78 | try { 79 | $this->traverseConfig($this->getPath((string) $offset)); 80 | } catch (EntryDoesNotExistException) { 81 | return false; 82 | } 83 | 84 | return true; 85 | } 86 | 87 | /** 88 | * @throws EntryDoesNotExistException 89 | */ 90 | public function offsetGet(mixed $offset): mixed 91 | { 92 | return $this->traverseConfig($this->getPath((string) $offset)); 93 | } 94 | 95 | /** 96 | * @throws ReadOnlyException 97 | */ 98 | public function offsetSet(mixed $offset, mixed $value): void 99 | { 100 | throw ReadOnlyException::fromClassName(self::class); 101 | } 102 | 103 | /** 104 | * @param mixed $offset 105 | * 106 | * @throws ReadOnlyException 107 | */ 108 | public function offsetUnset($offset): void 109 | { 110 | throw ReadOnlyException::fromClassName(self::class); 111 | } 112 | 113 | /** 114 | * @return array 115 | */ 116 | public function asArray(): array 117 | { 118 | return $this->config; 119 | } 120 | 121 | public function getSeparator(): string 122 | { 123 | return $this->separator; 124 | } 125 | 126 | /** 127 | * @return array 128 | */ 129 | private function getPath(string $offset): array 130 | { 131 | return explode($this->separator, $offset); 132 | } 133 | 134 | /** 135 | * @param array $path 136 | * 137 | * @throws EntryDoesNotExistException 138 | */ 139 | private function traverseConfig(array $path): mixed 140 | { 141 | $pointer = &$this->config; 142 | 143 | foreach ($path as $node) { 144 | if (!is_array($pointer) || !array_key_exists($node, $pointer)) { 145 | throw EntryDoesNotExistException::fromKey(implode($this->separator, $path)); 146 | } 147 | 148 | $pointer = &$pointer[$node]; 149 | } 150 | 151 | return $pointer; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 4 | 5 | return (new PhpCsFixer\Config()) 6 | ->setRules([ 7 | '@PSR12' => true, 8 | 'array_syntax' => [ 9 | 'syntax' => 'short', 10 | ], 11 | 'binary_operator_spaces' => [ 12 | 'operators' => [ 13 | '=>' => 'align', 14 | '=' => 'align', 15 | ], 16 | ], 17 | 'concat_space' => [ 18 | 'spacing' => 'one', 19 | ], 20 | 'declare_strict_types' => true, 21 | 'type_declaration_spaces' => true, 22 | 'single_line_comment_style' => [ 23 | 'comment_types' => ['hash'], 24 | ], 25 | 'lowercase_cast' => true, 26 | 'class_attributes_separation' => [ 27 | 'elements' => ['method' => 'one', 'property' => 'one', 'const' => 'one'], 28 | ], 29 | 'native_function_casing' => true, 30 | 'new_with_parentheses' => true, 31 | 'no_alias_functions' => true, 32 | 'no_blank_lines_after_class_opening' => true, 33 | 'no_blank_lines_after_phpdoc' => true, 34 | 'no_empty_comment' => true, 35 | 'no_empty_phpdoc' => true, 36 | 'no_empty_statement' => true, 37 | 'no_extra_blank_lines' => true, 38 | 'no_leading_import_slash' => true, 39 | 'no_leading_namespace_whitespace' => true, 40 | 'no_multiline_whitespace_around_double_arrow' => true, 41 | 'multiline_whitespace_before_semicolons' => false, 42 | 'no_short_bool_cast' => true, 43 | 'no_singleline_whitespace_before_semicolons' => true, 44 | 'no_spaces_around_offset' => true, 45 | 'no_trailing_comma_in_singleline' => true, 46 | 'no_unreachable_default_argument_value' => true, 47 | 'no_unused_imports' => true, 48 | 'no_useless_else' => true, 49 | 'no_useless_return' => true, 50 | 'no_whitespace_before_comma_in_array' => true, 51 | 'object_operator_without_whitespace' => true, 52 | 'ordered_imports' => true, 53 | 'phpdoc_align' => true, 54 | 'phpdoc_indent' => true, 55 | 'general_phpdoc_tag_rename' => true, 56 | 'phpdoc_inline_tag_normalizer' => true, 57 | 'phpdoc_tag_type' => true, 58 | 'phpdoc_order' => true, 59 | 'phpdoc_scalar' => true, 60 | 'phpdoc_separation' => true, 61 | 'phpdoc_single_line_var_spacing' => true, 62 | 'phpdoc_summary' => true, 63 | 'phpdoc_to_comment' => true, 64 | 'phpdoc_trim' => true, 65 | 'phpdoc_types' => true, 66 | 'self_accessor' => true, 67 | 'short_scalar_cast' => true, 68 | 'single_quote' => true, 69 | 'space_after_semicolon' => true, 70 | 'standardize_not_equals' => true, 71 | 'trailing_comma_in_multiline' => [ 72 | 'elements' => ['arrays'], 73 | ], 74 | 'trim_array_spaces' => true, 75 | 'unary_operator_spaces' => true, 76 | 'whitespace_after_comma_in_array' => true, 77 | ]) 78 | ->setRiskyAllowed(true) 79 | ->setUsingCache(true) 80 | ->setFinder($finder); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## 2.0.0 - 2025-08-12 6 | 7 | ### Changed 8 | 9 | * Minimum supported PHP version is now `8.1`. 10 | * Upgraded dependencies: 11 | * beberlei/assert to `^3.3` 12 | * symfony/yaml to `^6.4` 13 | * tomphp/exception-constructor-tools to `^2.0` 14 | 15 | ### Added 16 | 17 | * `TomPHP\ContainerConfigurator\FileReader\HJSONFileReader` for reading 18 | HJSON files (requires `laktak/hjson` to be installed). 19 | * `phpstan/phpstan` applied at max level with code cleanup 20 | 21 | ## 1.0.0 - 2015-09-31 22 | ### Added 23 | * Inflector support for Pimple. 24 | * Custom file readers with `withFileReader(string $extension, string $readerClass);`. 25 | * Custom container adapters with `withContainerAdapter(string $containerName, string $adapterName);`. 26 | 27 | ## 0.5.2 - 2016-09-30 28 | ### Added 29 | * Service aliases. 30 | * Container injection using `Configurator::container()`. 31 | 32 | ### Fixed 33 | * Non-string arguments can be provided to Pimple services. 34 | 35 | ## 0.5.1 - 2016-09-18 36 | ### Added 37 | * `TomPHP\ContainerConfigurator\FileReader\YAMLFileReader` for reading 38 | YAML files (requires `symfony/yaml` to be installed). 39 | * Service factories. 40 | 41 | ## 0.5.0 - 2016-09-13 42 | ### Added 43 | * `TomPHP\ContainerConfigurator\Configurator` as the main API. 44 | * If `class` is left out of the config for a service, then the service name 45 | is assumed to be the name of the class. 46 | * Services can be set as singleton by default. 47 | * All exceptions implement `TomPHP\ContainerConfigurator\Exception\Exception`. 48 | * Support for Pimple. 49 | 50 | ### Changed 51 | * The composer package has been renamed to `container-configurator`. 52 | * The package namespace has changed to `TomPHP\ContainerConfigurator`. 53 | * Minimum supported PHP version is now `5.6`. 54 | * Exception base-classes have been updated. 55 | * File reader classes have moved to `TomPHP\ContainerConfigurator\FileReader`. 56 | 57 | ### Removed 58 | * `TomPHP\ConfigServiceProvider\ConfigServiceProvider` 59 | * Custom configurable service providers (`TomPHP\ConfigServiceProvider\ConfigurableServiceProvider`). 60 | * Custom sub-providers. 61 | * `TomPHP\ConfigServiceProvider\Exception\RuntimeException` 62 | 63 | ## [0.4.0] - 2016-05-25 64 | ### Added 65 | * Exception thrown when no files are found when using the `fromFiles` 66 | constructor 67 | 68 | ### Changed 69 | * Config containing class names will remain as strings and not be converted to 70 | instances 71 | 72 | ## [0.3.3] - 2015-10-10 73 | ### Added 74 | * Configuring DI via the config 75 | 76 | ## [0.3.2] - 2015-09-24 77 | ### Added 78 | * Reading JSON config files via the `fromFiles` constructor 79 | * Support from braces in file globbing patterns 80 | 81 | ## [0.3.1] - 2015-09-23 82 | ### Added 83 | * Reading config files (PHP and JSON) 84 | 85 | ## [0.3.0] - 2015-09-23 86 | ### Added 87 | * `ConfigServiceProvider::fromConfig()` factory method 88 | * Sub providers 89 | 90 | ### Changed 91 | * `TomPHP\ConfigServiceProvider\InflectorConfigServiceProvider` is 92 | now a sub provider 93 | * Provider classes are marked as final 94 | 95 | ### Removed 96 | * `TomPHP\ConfigServiceProvider\Config` static factory 97 | 98 | ## [0.2.1] - 2015-09-21 99 | ### Added 100 | * Support to set up inflectors via configuration 101 | * `TomPHP\ConfigServiceProvider\Config` - a static class to enable easy setup. 102 | 103 | ## [0.2.0] - 2015-09-03 104 | ### Changed 105 | * Now depends on League Container `^2.0.2` 106 | * `TomPHP\ConfigServiceProvider\ConfigServiceProvider` now extends 107 | `League\Container\ServiceProvider\AbstractServiceProvider` 108 | 109 | ## [0.1.2] - 2014-04-12 110 | ### Added 111 | * Contributing guidelines 112 | * `composer test` to run test suite 113 | * Make sub-arrays accessible directly 114 | 115 | ### Changed 116 | * Make League Container dependency stricter (use `^` version) 117 | 118 | ## [0.1.1] - 2014-04-12 119 | ### Added 120 | * CHANGELOG.md 121 | * Homepage field to composer.json 122 | 123 | ### Fixed 124 | * Typo in README.md 125 | 126 | ## [0.1.0] - 2015-04-12 127 | ### Added 128 | * Service provider for `league/container` 129 | * Support for multiple levels of config 130 | -------------------------------------------------------------------------------- /tests/unit/ApplicationConfigTest.php: -------------------------------------------------------------------------------- 1 | config = new ApplicationConfig([ 22 | 'keyA' => 'valueA', 23 | 'group1' => [ 24 | 'keyB' => 'valueB', 25 | 'null' => null, 26 | ], 27 | ]); 28 | } 29 | 30 | public function testItProvidesAccessToSimpleScalarValues(): void 31 | { 32 | $this->assertEquals('valueA', $this->config['keyA']); 33 | } 34 | 35 | public function testItProvidesAccessToArrayValues(): void 36 | { 37 | $this->assertEquals(['keyB' => 'valueB', 'null' => null], $this->config['group1']); 38 | } 39 | 40 | public function testItProvidesToSubValuesUsingDotNotation(): void 41 | { 42 | $this->assertEquals('valueB', $this->config['group1.keyB']); 43 | } 44 | 45 | public function testItSaysIfAnEntryIsSet(): void 46 | { 47 | $this->assertArrayHasKey('group1.keyB', $this->config); 48 | } 49 | 50 | public function testItSaysIfAnEntryIsNotSet(): void 51 | { 52 | $this->assertArrayNotHasKey('bad.entry', $this->config); 53 | } 54 | 55 | public function testItSaysIfAnEntryIsSetIfItIsFalsey(): void 56 | { 57 | $this->assertArrayHasKey('group1.null', $this->config); 58 | } 59 | 60 | public function testItReturnsAllItsKeys(): void 61 | { 62 | $this->assertSame( 63 | [ 64 | 'keyA', 65 | 'group1', 66 | 'group1.keyB', 67 | 'group1.null', 68 | ], 69 | $this->config->getKeys() 70 | ); 71 | } 72 | 73 | public function testItCanBeConvertedToAnArray(): void 74 | { 75 | $this->assertEquals( 76 | [ 77 | 'keyA' => 'valueA', 78 | 'group1' => [ 79 | 'keyB' => 'valueB', 80 | 'null' => null, 81 | ], 82 | ], 83 | $this->config->asArray() 84 | ); 85 | } 86 | 87 | public function testItWorksWithADifferentSeperator(): void 88 | { 89 | $this->config = new ApplicationConfig([ 90 | 'group1' => [ 91 | 'keyA' => 'valueA', 92 | ], 93 | ], '->'); 94 | $this->assertEquals('valueA', $this->config['group1->keyA']); 95 | } 96 | 97 | public function testItThrowsForAnEmptySeparatorOnConstruction(): void 98 | { 99 | $this->expectException(InvalidArgumentException::class); 100 | 101 | $this->config = new ApplicationConfig([], ''); 102 | } 103 | 104 | public function testItCannotHaveAValueSet(): void 105 | { 106 | $this->expectException(ReadOnlyException::class); 107 | 108 | $this->config['key'] = 'value'; 109 | } 110 | 111 | public function testItCannotHaveAValueRemoved(): void 112 | { 113 | $this->expectException(ReadOnlyException::class); 114 | 115 | unset($this->config['keyA']); 116 | } 117 | 118 | public function testItMergesInNewConfig(): void 119 | { 120 | $applicationConfig = new ApplicationConfig([ 121 | 'group' => [ 122 | 'keyA' => 'valueA', 123 | 'keyB' => 'valueX', 124 | ], 125 | ]); 126 | 127 | $applicationConfig->merge(['group' => ['keyB' => 'valueB']]); 128 | 129 | $this->assertSame('valueA', $applicationConfig['group.keyA']); 130 | $this->assertSame('valueB', $applicationConfig['group.keyB']); 131 | } 132 | 133 | public function testItUpdatesTheSeparator(): void 134 | { 135 | $applicationConfig = new ApplicationConfig([ 136 | 'group' => [ 137 | 'keyA' => 'valueA', 138 | ], 139 | ]); 140 | 141 | $applicationConfig->setSeparator('/'); 142 | 143 | $this->assertSame('valueA', $applicationConfig['group/keyA']); 144 | } 145 | 146 | public function testItThrowsForAnEmptySeparatorWhenSettingSeparator(): void 147 | { 148 | $this->expectException(InvalidArgumentException::class); 149 | 150 | $this->config = new ApplicationConfig([]); 151 | $this->config->setSeparator(''); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/League/ServiceServiceProvider.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private readonly array $provides; 23 | 24 | public function __construct(private readonly \TomPHP\ContainerConfigurator\ServiceConfig $serviceConfig) 25 | { 26 | $this->provides = $this->serviceConfig->getKeys(); 27 | } 28 | 29 | public function provides(string $id): bool 30 | { 31 | return in_array($id, $this->provides); 32 | } 33 | 34 | public function register(): void 35 | { 36 | foreach ($this->serviceConfig as $config) { 37 | $this->registerService($config); 38 | } 39 | } 40 | 41 | /** 42 | * @throws NotClassDefinitionException 43 | */ 44 | private function registerService(ServiceDefinition $serviceDefinition): void 45 | { 46 | if ($serviceDefinition->isFactory()) { 47 | $service = $this->getContainer()->add( 48 | $serviceDefinition->getName(), 49 | $this->createFactoryFactory($serviceDefinition) 50 | ); 51 | 52 | $service->setShared($serviceDefinition->isSingleton()); 53 | 54 | return; 55 | } 56 | 57 | if ($serviceDefinition->isAlias()) { 58 | $this->getContainer()->add( 59 | $serviceDefinition->getName(), 60 | $this->createAliasFactory($serviceDefinition) 61 | ); 62 | 63 | return; 64 | } 65 | 66 | $service = $this->getContainer()->add( 67 | $serviceDefinition->getName(), 68 | $serviceDefinition->getClass() 69 | ); 70 | 71 | $service->setShared($serviceDefinition->isSingleton()); 72 | 73 | if (!$service instanceof Definition) { 74 | throw NotClassDefinitionException::fromServiceName($serviceDefinition->getName()); 75 | } 76 | 77 | $service->addArguments($this->injectContainer($serviceDefinition->getArguments())); 78 | $this->addMethodCalls($service, $serviceDefinition); 79 | } 80 | 81 | private function addMethodCalls(Definition $definition, ServiceDefinition $serviceDefinition): void 82 | { 83 | foreach ($serviceDefinition->getMethods() as $method => $args) { 84 | $definition->addMethodCall($method, $this->injectContainer($args)); 85 | } 86 | } 87 | 88 | /** 89 | * @return \Closure 90 | */ 91 | private function createAliasFactory(ServiceDefinition $serviceDefinition) 92 | { 93 | return fn () => $this->getContainer()->get($serviceDefinition->getClass()); 94 | } 95 | 96 | /** 97 | * @return \Closure 98 | */ 99 | private function createFactoryFactory(ServiceDefinition $serviceDefinition) 100 | { 101 | return function () use ($serviceDefinition) { 102 | $className = $serviceDefinition->getClass(); 103 | $factory = new $className(); 104 | if (!is_callable($factory)) { 105 | throw NotFactoryException::fromClassName($className); 106 | } 107 | 108 | return $factory(...$this->resolveArguments($serviceDefinition->getArguments())); 109 | }; 110 | } 111 | 112 | /** 113 | * @param array $arguments 114 | * 115 | * @return array 116 | */ 117 | private function injectContainer(array $arguments): array 118 | { 119 | return array_map( 120 | fn ($argument): mixed => ($argument === Configurator::container()) 121 | ? $this->container 122 | : $argument, 123 | $arguments 124 | ); 125 | } 126 | 127 | /** 128 | * @param array $arguments 129 | * 130 | * @return array 131 | */ 132 | private function resolveArguments(array $arguments): array 133 | { 134 | return array_map( 135 | function ($argument) { 136 | if ($argument === Configurator::container()) { 137 | return $this->container; 138 | } 139 | 140 | if ((is_string($argument) || is_int($argument) || $argument instanceof \Stringable) 141 | && $this->container?->has((string) $argument) 142 | ) { 143 | return $this->container->get((string) $argument); 144 | } 145 | 146 | return $argument; 147 | }, 148 | $arguments 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/acceptance/SupportsApplicationConfig.php: -------------------------------------------------------------------------------- 1 | 'valueA']; 14 | 15 | Configurator::apply() 16 | ->configFromArray($config) 17 | ->to($this->container); 18 | 19 | $this->assertEquals('valueA', $this->container->get('config.keyA')); 20 | $this->assertIsArray($this->container->get('config')); 21 | $this->assertArrayHasKey('keyA', $this->container->get('config')); 22 | } 23 | 24 | public function testItCascadeAddsConfigToTheContainer(): void 25 | { 26 | Configurator::apply() 27 | ->configFromArray(['keyA' => 'valueA', 'keyB' => 'valueX']) 28 | ->configFromArray(['keyB' => 'valueB']) 29 | ->to($this->container); 30 | 31 | $this->assertEquals('valueA', $this->container->get('config.keyA')); 32 | } 33 | 34 | public function testItAddsGroupedConfigToTheContainer(): void 35 | { 36 | Configurator::apply() 37 | ->configFromArray(['group1' => ['keyA' => 'valueA']]) 38 | ->to($this->container); 39 | 40 | $this->assertEquals(['keyA' => 'valueA'], $this->container->get('config.group1')); 41 | $this->assertEquals('valueA', $this->container->get('config.group1.keyA')); 42 | } 43 | 44 | public function testItAddsConfigToTheContainerWithAnAlternativeSeparator(): void 45 | { 46 | Configurator::apply() 47 | ->configFromArray(['keyA' => 'valueA']) 48 | ->withSetting(Configurator::SETTING_SEPARATOR, '/') 49 | ->to($this->container); 50 | 51 | $this->assertEquals('valueA', $this->container->get('config/keyA')); 52 | } 53 | 54 | public function testItAddsConfigToTheContainerWithAnAlternativePrefix(): void 55 | { 56 | Configurator::apply() 57 | ->configFromArray(['keyA' => 'valueA']) 58 | ->withSetting(Configurator::SETTING_PREFIX, 'settings') 59 | ->to($this->container); 60 | 61 | $this->assertEquals('valueA', $this->container->get('settings.keyA')); 62 | $this->assertIsArray($this->container->get('settings')); 63 | $this->assertArrayHasKey('keyA', $this->container->get('settings')); 64 | } 65 | 66 | public function testItAddsConfigToTheContainerWithNoPrefix(): void 67 | { 68 | Configurator::apply() 69 | ->configFromArray(['keyA' => 'valueA']) 70 | ->withSetting(Configurator::SETTING_PREFIX, '') 71 | ->to($this->container); 72 | 73 | $this->assertEquals('valueA', $this->container->get('keyA')); 74 | } 75 | 76 | public function testItAddsMultipleConfiguratorsToTheContainer(): void 77 | { 78 | Configurator::apply() 79 | ->configFromArray(['keyA' => 'valueA']) 80 | ->withSetting(Configurator::SETTING_PREFIX, 'a') 81 | ->to($this->container); 82 | 83 | Configurator::apply() 84 | ->configFromArray(['keyB' => 'valueB']) 85 | ->withSetting(Configurator::SETTING_PREFIX, 'b') 86 | ->to($this->container); 87 | 88 | $this->assertEquals('valueA', $this->container->get('a.keyA')); 89 | $this->assertEquals('valueB', $this->container->get('b.keyB')); 90 | } 91 | 92 | public function testItAddsMultipleConfiguratorsToTheContainerWithoutPrefixes(): void 93 | { 94 | Configurator::apply() 95 | ->configFromArray(['keyA' => 'valueA']) 96 | ->withSetting(Configurator::SETTING_PREFIX, '') 97 | ->to($this->container); 98 | 99 | Configurator::apply() 100 | ->configFromArray(['keyB' => 'valueB']) 101 | ->withSetting(Configurator::SETTING_PREFIX, '') 102 | ->to($this->container); 103 | 104 | $this->assertEquals('valueA', $this->container->get('keyA')); 105 | $this->assertEquals('valueB', $this->container->get('keyB')); 106 | } 107 | 108 | public function testItAddsMultipleConfiguratorsToTheContainerWithAndWithoutPrefixes(): void 109 | { 110 | Configurator::apply() 111 | ->configFromArray(['keyA' => 'valueA']) 112 | ->withSetting(Configurator::SETTING_PREFIX, '') 113 | ->to($this->container); 114 | 115 | Configurator::apply() 116 | ->configFromArray(['keyB' => 'valueB']) 117 | ->withSetting(Configurator::SETTING_PREFIX, '') 118 | ->to($this->container); 119 | 120 | Configurator::apply() 121 | ->configFromArray(['keyC' => 'valueC']) 122 | ->withSetting(Configurator::SETTING_PREFIX, 'c') 123 | ->to($this->container); 124 | 125 | Configurator::apply() 126 | ->configFromArray(['keyD' => 'valueD']) 127 | ->withSetting(Configurator::SETTING_PREFIX, 'd') 128 | ->to($this->container); 129 | 130 | $this->assertEquals('valueA', $this->container->get('keyA')); 131 | $this->assertEquals('valueB', $this->container->get('keyB')); 132 | $this->assertEquals('valueC', $this->container->get('c.keyC')); 133 | $this->assertEquals('valueD', $this->container->get('d.keyD')); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Pimple/PimpleContainerAdapter.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | private array $inflectors = []; 33 | 34 | /** 35 | * @param Container $container 36 | */ 37 | public function setContainer(object $container): void 38 | { 39 | $this->container = $container; 40 | } 41 | 42 | public function addApplicationConfig(ApplicationConfig $applicationConfig, string $prefix = 'config'): void 43 | { 44 | Assertion::string($prefix); 45 | 46 | if ($prefix !== '' && $prefix !== '0') { 47 | $this->container[$prefix] = $applicationConfig->asArray(); 48 | $prefix .= $applicationConfig->getSeparator(); 49 | } 50 | 51 | foreach ($applicationConfig as $key => $value) { 52 | // @phpstan-ignore-next-line 53 | if (!is_string($key) && !is_int($key) && !($key instanceof \Stringable)) { 54 | continue; 55 | } 56 | 57 | $this->container[$prefix . $key] = $value; 58 | } 59 | } 60 | 61 | public function addServiceConfig(ServiceConfig $serviceConfig): void 62 | { 63 | foreach ($serviceConfig as $definition) { 64 | $this->addServiceToContainer($definition); 65 | } 66 | } 67 | 68 | public function addInflectorConfig(InflectorConfig $inflectorConfig): void 69 | { 70 | foreach ($inflectorConfig as $definition) { 71 | $this->inflectors[$definition->getInterface()] = $this->createInflector($definition); 72 | } 73 | } 74 | 75 | private function addServiceToContainer(ServiceDefinition $serviceDefinition): void 76 | { 77 | $factory = $this->createFactory($serviceDefinition); 78 | 79 | if (!$serviceDefinition->isSingleton()) { 80 | $factory = $this->container->factory($factory); 81 | } 82 | 83 | $this->container[$serviceDefinition->getName()] = $factory; 84 | } 85 | 86 | private function createFactory(ServiceDefinition $serviceDefinition): callable 87 | { 88 | if ($serviceDefinition->isFactory()) { 89 | return $this->applyInflectors($this->createFactoryFactory($serviceDefinition)); 90 | } 91 | 92 | if ($serviceDefinition->isAlias()) { 93 | return $this->createAliasFactory($serviceDefinition); 94 | } 95 | 96 | return $this->applyInflectors($this->createInstanceFactory($serviceDefinition)); 97 | } 98 | 99 | /** 100 | * @return Closure 101 | */ 102 | private function createFactoryFactory(ServiceDefinition $serviceDefinition) 103 | { 104 | return function () use ($serviceDefinition) { 105 | $className = $serviceDefinition->getClass(); 106 | $factory = new $className(); 107 | if (!is_callable($factory)) { 108 | throw NotFactoryException::fromClassName($className); 109 | } 110 | 111 | return $factory(...$this->resolveArguments($serviceDefinition->getArguments())); 112 | }; 113 | } 114 | 115 | /** 116 | * @return Closure 117 | */ 118 | private function createAliasFactory(ServiceDefinition $serviceDefinition) 119 | { 120 | return fn () => $this->container[$serviceDefinition->getClass()]; 121 | } 122 | 123 | /** 124 | * @return Closure 125 | */ 126 | private function createInstanceFactory(ServiceDefinition $serviceDefinition) 127 | { 128 | return function () use ($serviceDefinition): object { 129 | $className = $serviceDefinition->getClass(); 130 | $instance = new $className(...$this->resolveArguments($serviceDefinition->getArguments())); 131 | 132 | foreach ($serviceDefinition->getMethods() as $name => $args) { 133 | $instance->$name(...$this->resolveArguments($args)); 134 | } 135 | 136 | return $instance; 137 | }; 138 | } 139 | 140 | private function createInflector(InflectorDefinition $inflectorDefinition): callable 141 | { 142 | return function ($subject) use ($inflectorDefinition): void { 143 | foreach ($inflectorDefinition->getMethods() as $method => $arguments) { 144 | $subject->$method(...$this->resolveArguments($arguments)); 145 | } 146 | }; 147 | } 148 | 149 | private function applyInflectors(Closure $factory): callable 150 | { 151 | return function () use ($factory) { 152 | $instance = $factory(); 153 | 154 | foreach ($this->inflectors as $interface => $inflector) { 155 | if ($instance instanceof $interface) { 156 | $inflector($instance); 157 | } 158 | } 159 | 160 | return $instance; 161 | }; 162 | } 163 | 164 | /** 165 | * @param array $arguments 166 | * 167 | * @return array 168 | */ 169 | private function resolveArguments(array $arguments): array 170 | { 171 | return array_map( 172 | function ($argument) { 173 | if (!is_string($argument)) { 174 | return $argument; 175 | } 176 | 177 | if ($argument === Configurator::container()) { 178 | return $this->container; 179 | } 180 | 181 | return $this->container[$argument] ?? $argument; 182 | }, 183 | $arguments 184 | ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Configurator.php: -------------------------------------------------------------------------------- 1 | FileReader\JSONFileReader::class, 26 | '.hjson' => FileReader\HJSONFileReader::class, 27 | '.php' => FileReader\PHPFileReader::class, 28 | '.yaml' => FileReader\YAMLFileReader::class, 29 | '.yml' => FileReader\YAMLFileReader::class, 30 | ]; 31 | 32 | public const CONTAINER_ADAPTERS = [ 33 | \League\Container\Container::class => League\LeagueContainerAdapter::class, 34 | \Pimple\Container::class => Pimple\PimpleContainerAdapter::class, 35 | ]; 36 | 37 | private \TomPHP\ContainerConfigurator\ApplicationConfig $applicationConfig; 38 | 39 | private ?\TomPHP\ContainerConfigurator\FileReader\ReaderFactory $readerFactory = null; 40 | 41 | /** 42 | * @var array 43 | */ 44 | private array $settings = [ 45 | self::SETTING_PREFIX => 'config', 46 | self::SETTING_SEPARATOR => '.', 47 | self::SETTING_SERVICES_KEY => 'di.services', 48 | self::SETTING_INFLECTORS_KEY => 'di.inflectors', 49 | self::SETTING_DEFAULT_SINGLETON_SERVICES => false, 50 | ]; 51 | 52 | /** 53 | * @var array 54 | */ 55 | private array $fileReaders = self::FILE_READERS; 56 | 57 | /** 58 | * @var array 59 | */ 60 | private array $containerAdapters = self::CONTAINER_ADAPTERS; 61 | 62 | private static ?string $containerIdentifier = null; 63 | 64 | public static function apply(): self 65 | { 66 | return new self(); 67 | } 68 | 69 | private function __construct() 70 | { 71 | $this->applicationConfig = new ApplicationConfig([]); 72 | } 73 | 74 | public static function container(): string 75 | { 76 | if (self::$containerIdentifier === null 77 | || self::$containerIdentifier === '' 78 | || self::$containerIdentifier === '0' 79 | ) { 80 | self::$containerIdentifier = uniqid(self::class . '::CONTAINER_ID::'); 81 | } 82 | 83 | return self::$containerIdentifier; 84 | } 85 | 86 | /** 87 | * @param array $config 88 | */ 89 | public function configFromArray(array $config): self 90 | { 91 | $this->applicationConfig->merge($config); 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * @throws InvalidArgumentException 98 | * 99 | * @return $this 100 | */ 101 | public function configFromFile(string $filename): self 102 | { 103 | Assertion::file($filename); 104 | 105 | $this->readFileAndMergeConfig($filename); 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @throws NoMatchingFilesException 112 | * @throws InvalidArgumentException 113 | * 114 | * @return $this 115 | */ 116 | public function configFromFiles(string $pattern): self 117 | { 118 | Assertion::string($pattern); 119 | 120 | $fileLocator = new FileReader\FileLocator(); 121 | 122 | $files = $fileLocator->locate($pattern); 123 | 124 | if ($files === []) { 125 | throw NoMatchingFilesException::fromPattern($pattern); 126 | } 127 | 128 | foreach ($files as $file) { 129 | $this->readFileAndMergeConfig($file); 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * @throws UnknownSettingException 137 | * @throws InvalidArgumentException 138 | * 139 | * @return $this 140 | */ 141 | public function withSetting(string $name, mixed $value): self 142 | { 143 | Assertion::string($name); 144 | Assertion::scalar($value); 145 | 146 | if (!array_key_exists($name, $this->settings)) { 147 | throw UnknownSettingException::fromSetting($name, array_keys($this->settings)); 148 | } 149 | 150 | $this->settings[$name] = $value; 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * @return $this 157 | */ 158 | public function withFileReader(string $extension, string $className): self 159 | { 160 | $this->fileReaders[$extension] = $className; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * @return $this 167 | */ 168 | public function withContainerAdapter(string $containerName, string $adapterName): self 169 | { 170 | $this->containerAdapters[$containerName] = $adapterName; 171 | 172 | return $this; 173 | } 174 | 175 | public function to(object $container): void 176 | { 177 | if (is_string($this->settings[self::SETTING_SEPARATOR]) 178 | && (isset($this->settings[self::SETTING_SEPARATOR]) 179 | && ($this->settings[self::SETTING_SEPARATOR] !== '' 180 | && $this->settings[self::SETTING_SEPARATOR] !== '0')) 181 | ) { 182 | $this->applicationConfig->setSeparator($this->settings[self::SETTING_SEPARATOR]); 183 | } 184 | 185 | $containerAdapterFactory = new ContainerAdapterFactory($this->containerAdapters); 186 | 187 | $containerAdapter = $containerAdapterFactory->create($container); 188 | 189 | if (!is_string($this->settings[self::SETTING_PREFIX])) { 190 | throw new \InvalidArgumentException('The SETTING_PREFIX must be a string.'); 191 | } 192 | 193 | $containerAdapter->addApplicationConfig($this->applicationConfig, $this->settings[self::SETTING_PREFIX]); 194 | 195 | if (isset($this->applicationConfig[$this->settings[self::SETTING_SERVICES_KEY]])) { 196 | $containerAdapter->addServiceConfig(new ServiceConfig( 197 | $this->applicationConfig[$this->settings[self::SETTING_SERVICES_KEY]], // @phpstan-ignore-line 198 | (bool) $this->settings[self::SETTING_DEFAULT_SINGLETON_SERVICES] 199 | )); 200 | } 201 | 202 | if (isset($this->applicationConfig[$this->settings[self::SETTING_INFLECTORS_KEY]])) { 203 | $containerAdapter->addInflectorConfig(new InflectorConfig( 204 | $this->applicationConfig[$this->settings[self::SETTING_INFLECTORS_KEY]] // @phpstan-ignore-line 205 | )); 206 | } 207 | } 208 | 209 | private function readFileAndMergeConfig(string $filename): void 210 | { 211 | $fileReader = $this->getReaderFor($filename); 212 | $config = $fileReader->read($filename); 213 | if (!is_array($config)) { 214 | throw new \InvalidArgumentException(sprintf("Configuration file '%s' did not return an array.", $filename)); 215 | } 216 | 217 | $this->applicationConfig->merge($config); 218 | } 219 | 220 | private function getReaderFor(string $filename): FileReader\FileReader 221 | { 222 | if (!$this->readerFactory instanceof \TomPHP\ContainerConfigurator\FileReader\ReaderFactory) { 223 | $this->readerFactory = new FileReader\ReaderFactory($this->fileReaders); 224 | } 225 | 226 | return $this->readerFactory->create($filename); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Container Configurator 2 | 3 | [![Build Status](https://api.travis-ci.org/tomphp/container-configurator.svg)](https://api.travis-ci.org/tomphp/container-configurator) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tomphp/container-configurator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tomphp/container-configurator/?branch=master) 5 | [![Latest Stable Version](https://poser.pugx.org/tomphp/container-configurator/v/stable)](https://packagist.org/packages/tomphp/container-configurator) 6 | [![Total Downloads](https://poser.pugx.org/tomphp/container-configurator/downloads)](https://packagist.org/packages/tomphp/container-configurator) 7 | [![Latest Unstable Version](https://poser.pugx.org/tomphp/container-configurator/v/unstable)](https://packagist.org/packages/tomphp/container-configurator) 8 | [![License](https://poser.pugx.org/tomphp/container-configurator/license)](https://packagist.org/packages/tomphp/container-configurator) 9 | 10 | This package enables you to configure your application and the Dependency 11 | Injection Container (DIC) via config arrays or files. Currently, supported 12 | containers are: 13 | 14 | * [League Of Extraordinary Packages' Container](https://github.com/thephpleague/container) 15 | * [Pimple](http://pimple.sensiolabs.org/) 16 | 17 | ## Installation 18 | 19 | Installation can be done easily using composer: 20 | 21 | ``` 22 | $ composer require tomphp/container-configurator 23 | ``` 24 | 25 | ## Example Usage 26 | 27 | ```php 28 | [ 35 | 'name' => 'example_db', 36 | 'username' => 'dbuser', 37 | 'password' => 'dbpass', 38 | ], 39 | 'di' => [ 40 | 'services' => [ 41 | 'database_connection' => [ 42 | 'class' => DatabaseConnection::class, 43 | 'arguments' => [ 44 | 'config.db.name', 45 | 'config.db.username', 46 | 'config.db.password', 47 | ], 48 | ], 49 | ], 50 | ], 51 | ]; 52 | 53 | $container = new Container(); 54 | Configurator::apply()->configFromArray($config)->to($container); 55 | 56 | $db = $container->get('database_connection'); 57 | ``` 58 | 59 | ## Reading Files From Disk 60 | 61 | Instead of providing the config as an array, you can also provide a list of 62 | file pattern matches to the `fromFiles` function. 63 | 64 | ```php 65 | Configurator::apply() 66 | ->configFromFile('config_dir/config.global.php') 67 | ->configFromFiles('json_dir/*.json') 68 | ->configFromFiles('config_dir/*.local.php') 69 | ->to($container); 70 | ``` 71 | 72 | `configFromFile(string $filename)` reads config in from a single file. 73 | 74 | `configFromFiles(string $pattern)` reads config from multiple files using 75 | globbing patterns. 76 | 77 | ### Merging 78 | 79 | The reader matches files in the order they are specified. As files are 80 | read their config is merged in; overwriting any matching keys. 81 | 82 | ### Supported Formats 83 | 84 | Currently `.php` and `.json` files are supported out of the box. PHP 85 | config files **must** return a PHP array. 86 | 87 | `.yaml` and `.yml` files can be read when the package `symfony/yaml` is 88 | available. Run 89 | 90 | ``` 91 | composer require symfony/yaml 92 | ``` 93 | 94 | to install it. 95 | 96 | ## Application Configuration 97 | 98 | All values in the config array are made accessible via the DIC with the keys 99 | separated by a separator (default: `.`) and prefixed with constant string (default: 100 | `config`). 101 | 102 | #### Example 103 | 104 | ```php 105 | $config = [ 106 | 'db' => [ 107 | 'name' => 'example_db', 108 | 'username' => 'dbuser', 109 | 'password' => 'dbpass', 110 | ], 111 | ]; 112 | 113 | $container = new Container(); 114 | Configurator::apply()->configFromArray($config)->to($container); 115 | 116 | var_dump($container->get('config.db.name')); 117 | /* 118 | * OUTPUT: 119 | * string(10) "example_db" 120 | */ 121 | ``` 122 | 123 | ### Accessing A Whole Sub-Array 124 | 125 | Whole sub-arrays are also made available for cases where you want them instead 126 | of individual values. 127 | 128 | #### Example 129 | 130 | ```php 131 | $config = [ 132 | 'db' => [ 133 | 'name' => 'example_db', 134 | 'username' => 'dbuser', 135 | 'password' => 'dbpass', 136 | ], 137 | ]; 138 | 139 | $container = new Container(); 140 | Configurator::apply()->configFromArray($config)->to($container); 141 | 142 | var_dump($container->get('config.db')); 143 | /* 144 | * OUTPUT: 145 | * array(3) { 146 | * ["name"]=> 147 | * string(10) "example_db" 148 | * ["username"]=> 149 | * string(6) "dbuser" 150 | * ["password"]=> 151 | * string(6) "dbpass" 152 | * } 153 | */ 154 | ``` 155 | 156 | ## Configuring Services 157 | 158 | Another feature is the ability to add services to your container via the 159 | config. By default, this is done by adding a `services` key under a `di` key in 160 | the config in the following format: 161 | 162 | ```php 163 | $config = [ 164 | 'di' => [ 165 | 'services' => [ 166 | 'logger' => [ 167 | 'class' => Logger::class, 168 | 'singleton' => true, 169 | 'arguments' => [ 170 | StdoutLogger::class, 171 | ], 172 | 'methods' => [ 173 | 'setLogLevel' => [ 'info' ], 174 | ], 175 | ], 176 | StdoutLogger::class => [], 177 | ], 178 | ], 179 | ]; 180 | 181 | $container = new Container(); 182 | Configurator::apply()->configFromArray($config)->to($container); 183 | 184 | $logger = $container->get('logger')); 185 | ``` 186 | 187 | ### Service Aliases 188 | 189 | You can create an alias to another service by using the `service` keyword 190 | instead of `class`: 191 | 192 | ```php 193 | $config = [ 194 | 'database' => [ /* ... */ ], 195 | 'di' => [ 196 | 'services' => [ 197 | DatabaseConnection::class => [ 198 | 'service' => MySQLDatabaseConnection::class, 199 | ], 200 | MySQLDatabaseConnection::class => [ 201 | 'arguments' => [ 202 | 'config.database.host', 203 | 'config.database.username', 204 | 'config.database.password', 205 | 'config.database.dbname', 206 | ], 207 | ], 208 | ], 209 | ], 210 | ]; 211 | ``` 212 | 213 | ### Service Factories 214 | 215 | If you require some addition additional logic when creating a service, you can 216 | define a Service Factory. A service factory is simply an invokable class which 217 | can take a list of arguments and returns the service instance. 218 | 219 | Services are added to the container by using the `factory` key instead of the 220 | `class` key. 221 | 222 | #### Example Config 223 | ```php 224 | $appConfig = [ 225 | 'db' => [ 226 | 'host' => 'localhost', 227 | 'database' => 'example_db', 228 | 'username' => 'example_user', 229 | 'password' => 'example_password', 230 | ], 231 | 'di' => [ 232 | 'services' => [ 233 | 'database' => [ 234 | 'factory' => MySQLPDOFactory::class, 235 | 'singleton' => true, 236 | 'arguments' => [ 237 | 'config.db.host', 238 | 'config.db.database', 239 | 'config.db.username', 240 | 'config.db.password', 241 | ], 242 | ], 243 | ], 244 | ], 245 | ]; 246 | ``` 247 | 248 | #### Example Service Factory 249 | ```php 250 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 259 | 260 | return $pdo; 261 | } 262 | } 263 | ``` 264 | 265 | ### Injecting The Container 266 | 267 | In the rare case that you want to inject the container in as a dependency to 268 | one of your services, you can use `Configurator::container()` as the name 269 | of the injected dependency. This will only work in PHP config files, it's not 270 | available with YAML or JSON. 271 | 272 | ```php 273 | $config = [ 274 | 'di' => [ 275 | 'services' => [ 276 | ContainerAwareService::class => [ 277 | 'arguments' => [Configurator::container()], 278 | ], 279 | ], 280 | ], 281 | ]; 282 | ``` 283 | 284 | ### Configuring Inflectors 285 | 286 | It is also possible to set up 287 | [Inflectors](http://container.thephpleague.com/inflectors/) by adding an 288 | `inflectors` key to the `di` section of the config. 289 | 290 | ```php 291 | $appConfig = [ 292 | 'di' => [ 293 | 'inflectors' => [ 294 | LoggerAwareInterface::class => [ 295 | 'setLogger' => ['Some\Logger'] 296 | ], 297 | ], 298 | ], 299 | ]; 300 | ``` 301 | 302 | ## Extra Settings 303 | 304 | The behaviour of the `Configurator` can be adjusted by using the 305 | `withSetting(string $name, $value` method: 306 | 307 | ```php 308 | Configurator::apply() 309 | ->configFromFiles('*.cfg.php'), 310 | ->withSetting(Configurator::SETTING_PREFIX, 'settings') 311 | ->withSetting(Configurator::SETTING_SEPARATOR, '/') 312 | ->to($container); 313 | ``` 314 | 315 | Available settings are: 316 | 317 | | Name | Description | Default | 318 | |------------------------------------|-------------------------------------------------|-----------------| 319 | | SETTING_PREFIX | Sets prefix name for config value keys. | `config` | 320 | | SETTING_SEPARATOR | Sets the separator for config key. | `.` | 321 | | SETTING_SERVICES_KEY | Where the config for the services is. | `di.services` | 322 | | SETTING_INFLECTORS_KEY | Where the config for the inflectors is. | `di.inflectors` | 323 | | SETTING_DEFAULT_SINGLETON_SERVICES | Sets whether services are singleton by default. | `false` | 324 | 325 | ## Advanced Customisation 326 | 327 | ### Adding A Custom File Reader 328 | 329 | You can create your own custom file reader by implementing the 330 | `TomPHP\ContainerConfigurator\FileReader\FileReader` interface. Once you have 331 | created it, you can use the 332 | `withFileReader(string $extension, string $readerClassName)` method to enable 333 | the it. 334 | 335 | **IMPORTANT**: `withFileReader()` must be called before calling 336 | `configFromFile()` or `configFromFiles()`! 337 | 338 | ```php 339 | Configurator::apply() 340 | ->withFileReader('.xml', MyCustomXMLFileReader::class) 341 | ->configFromFile('config.xml'), 342 | ->to($container); 343 | ``` 344 | 345 | ### Adding A Custom Container Adapter 346 | 347 | You can create your own container adapter so that you can configure other 348 | containers. This is done by implementing the 349 | `TomPHP\ContainerConfigurator\FileReader\ContainerAdapter` interface. Once you 350 | have created your adapter, you can use the 351 | `withContainerAdapter(string $containerName, string $adapterName)` method to 352 | enable the it: 353 | 354 | ```php 355 | Configurator::apply() 356 | ->withContainerAdapter(MyContainer::class, MyContainerAdapter::class) 357 | ->configFromArray($appConfig), 358 | ->to($container); 359 | ``` 360 | -------------------------------------------------------------------------------- /tests/acceptance/SupportsServiceConfig.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'services' => [ 19 | 'example_class' => [ 20 | 'class' => ExampleClass::class, 21 | ], 22 | ], 23 | ], 24 | ]; 25 | 26 | Configurator::apply() 27 | ->configFromArray($config) 28 | ->to($this->container); 29 | 30 | $this->assertInstanceOf(ExampleClass::class, $this->container->get('example_class')); 31 | } 32 | 33 | public function testItAddsServicesToTheContainerForADifferentConfigKey(): void 34 | { 35 | $config = [ 36 | 'di' => [ 37 | 'example_class' => [ 38 | 'class' => ExampleClass::class, 39 | ], 40 | ], 41 | ]; 42 | 43 | Configurator::apply() 44 | ->configFromArray($config) 45 | ->withSetting(Configurator::SETTING_SERVICES_KEY, 'di') 46 | ->to($this->container); 47 | 48 | $this->assertInstanceOf(ExampleClass::class, $this->container->get('example_class')); 49 | } 50 | 51 | public function testItCreatesUniqueServiceInstancesByDefault(): void 52 | { 53 | $config = [ 54 | 'di' => [ 55 | 'services' => [ 56 | 'example_class' => [ 57 | 'class' => ExampleClass::class, 58 | 'singleton' => false, 59 | ], 60 | ], 61 | ], 62 | ]; 63 | 64 | Configurator::apply() 65 | ->configFromArray($config) 66 | ->to($this->container); 67 | 68 | $instance1 = $this->container->get('example_class'); 69 | $instance2 = $this->container->get('example_class'); 70 | 71 | $this->assertNotSame($instance1, $instance2); 72 | } 73 | 74 | public function testItCanCreateSingletonServiceInstances(): void 75 | { 76 | $config = [ 77 | 'di' => [ 78 | 'services' => [ 79 | 'example_class' => [ 80 | 'class' => ExampleClass::class, 81 | 'singleton' => true, 82 | ], 83 | ], 84 | ], 85 | ]; 86 | 87 | Configurator::apply() 88 | ->configFromArray($config) 89 | ->to($this->container); 90 | 91 | $instance1 = $this->container->get('example_class'); 92 | $instance2 = $this->container->get('example_class'); 93 | 94 | $this->assertSame($instance1, $instance2); 95 | } 96 | 97 | public function testItCanCreateSingletonServiceInstancesByDefault(): void 98 | { 99 | $config = [ 100 | 'di' => [ 101 | 'services' => [ 102 | 'example_class' => [ 103 | 'class' => ExampleClass::class, 104 | ], 105 | ], 106 | ], 107 | ]; 108 | 109 | Configurator::apply() 110 | ->configFromArray($config) 111 | ->withSetting(Configurator::SETTING_DEFAULT_SINGLETON_SERVICES, true) 112 | ->to($this->container); 113 | 114 | $instance1 = $this->container->get('example_class'); 115 | $instance2 = $this->container->get('example_class'); 116 | 117 | $this->assertSame($instance1, $instance2); 118 | } 119 | 120 | public function testItCanCreateUniqueServiceInstancesWhenSingletonIsDefault(): void 121 | { 122 | $config = [ 123 | 'di' => [ 124 | 'services' => [ 125 | 'example_class' => [ 126 | 'class' => ExampleClass::class, 127 | 'singleton' => false, 128 | ], 129 | ], 130 | ], 131 | ]; 132 | 133 | Configurator::apply() 134 | ->configFromArray($config) 135 | ->withSetting(Configurator::SETTING_DEFAULT_SINGLETON_SERVICES, true) 136 | ->to($this->container); 137 | 138 | $instance1 = $this->container->get('example_class'); 139 | $instance2 = $this->container->get('example_class'); 140 | 141 | $this->assertNotSame($instance1, $instance2); 142 | } 143 | 144 | public function testItAddsConstructorArguments(): void 145 | { 146 | $config = [ 147 | 'di' => [ 148 | 'services' => [ 149 | 'example_class' => [ 150 | 'class' => ExampleClassWithArgs::class, 151 | 'arguments' => [ 152 | 'arg1', 153 | 'arg2', 154 | ], 155 | ], 156 | ], 157 | ], 158 | ]; 159 | 160 | Configurator::apply() 161 | ->configFromArray($config) 162 | ->to($this->container); 163 | 164 | $instance = $this->container->get('example_class'); 165 | 166 | $this->assertEquals(['arg1', 'arg2'], $instance->getConstructorArgs()); 167 | } 168 | 169 | public function testItResolvesConstructorArgumentsIfTheyAreServiceNames(): void 170 | { 171 | $config = [ 172 | 'arg1' => 'value1', 173 | 'arg2' => 'value2', 174 | 'di' => [ 175 | 'services' => [ 176 | 'example_class' => [ 177 | 'class' => ExampleClassWithArgs::class, 178 | 'arguments' => [ 179 | 'config.arg1', 180 | 'config.arg2', 181 | ], 182 | ], 183 | ], 184 | ], 185 | ]; 186 | 187 | Configurator::apply() 188 | ->configFromArray($config) 189 | ->to($this->container); 190 | 191 | $instance = $this->container->get('example_class'); 192 | 193 | $this->assertEquals(['value1', 'value2'], $instance->getConstructorArgs()); 194 | } 195 | 196 | public function testItUsesTheStringIfConstructorArgumentsAreClassNames(): void 197 | { 198 | $config = [ 199 | 'di' => [ 200 | 'services' => [ 201 | 'example_class' => [ 202 | 'class' => ExampleClassWithArgs::class, 203 | 'arguments' => [ 204 | ExampleClass::class, 205 | 'arg2', 206 | ], 207 | ], 208 | ], 209 | ], 210 | ]; 211 | 212 | Configurator::apply() 213 | ->configFromArray($config) 214 | ->to($this->container); 215 | 216 | $instance = $this->container->get('example_class'); 217 | 218 | $this->assertEquals([ExampleClass::class, 'arg2'], $instance->getConstructorArgs()); 219 | } 220 | 221 | public function testItUsesComplexConstructorArguments(): void 222 | { 223 | $config = [ 224 | 'di' => [ 225 | 'services' => [ 226 | 'example_class' => [ 227 | 'class' => ExampleClassWithArgs::class, 228 | 'arguments' => [ 229 | ['example_array'], 230 | new \stdClass(), 231 | ], 232 | ], 233 | ], 234 | ], 235 | ]; 236 | 237 | Configurator::apply() 238 | ->configFromArray($config) 239 | ->to($this->container); 240 | 241 | $instance = $this->container->get('example_class'); 242 | 243 | $this->assertEquals([['example_array'], new \stdClass()], $instance->getConstructorArgs()); 244 | } 245 | 246 | public function testItCallsSetterMethods(): void 247 | { 248 | $config = [ 249 | 'di' => [ 250 | 'services' => [ 251 | 'example_class' => [ 252 | 'class' => ExampleClass::class, 253 | 'methods' => [ 254 | 'setValue' => ['the value'], 255 | ], 256 | ], 257 | ], 258 | ], 259 | ]; 260 | 261 | Configurator::apply() 262 | ->configFromArray($config) 263 | ->to($this->container); 264 | 265 | $instance = $this->container->get('example_class'); 266 | 267 | $this->assertEquals('the value', $instance->getValue()); 268 | } 269 | 270 | public function testItResolvesSetterMethodArgumentsIfTheyAreServiceNames(): void 271 | { 272 | $config = [ 273 | 'arg' => 'value', 274 | 'di' => [ 275 | 'services' => [ 276 | 'example_class' => [ 277 | 'class' => ExampleClass::class, 278 | 'methods' => [ 279 | 'setValue' => ['config.arg'], 280 | ], 281 | ], 282 | ], 283 | ], 284 | ]; 285 | 286 | Configurator::apply() 287 | ->configFromArray($config) 288 | ->to($this->container); 289 | 290 | $instance = $this->container->get('example_class'); 291 | 292 | $this->assertEquals('value', $instance->getValue()); 293 | } 294 | 295 | public function testItUsesTheStringIffSetterMethodArgumentsAreClassNames(): void 296 | { 297 | $config = [ 298 | 'di' => [ 299 | 'services' => [ 300 | 'example_class' => [ 301 | 'class' => ExampleClass::class, 302 | 'methods' => [ 303 | 'setValue' => [ExampleClass::class], 304 | ], 305 | ], 306 | ], 307 | ], 308 | ]; 309 | 310 | Configurator::apply() 311 | ->configFromArray($config) 312 | ->to($this->container); 313 | 314 | $instance = $this->container->get('example_class'); 315 | 316 | $this->assertSame(ExampleClass::class, $instance->getValue()); 317 | } 318 | 319 | public function testIsCreatesAServiceThroughAFactoryClass(): void 320 | { 321 | $config = [ 322 | 'class_name' => ExampleClassWithArgs::class, 323 | 'di' => [ 324 | 'services' => [ 325 | 'example_service' => [ 326 | 'factory' => ExampleFactory::class, 327 | 'arguments' => [ 328 | 'config.class_name', 329 | 'example_argument', 330 | ], 331 | ], 332 | ], 333 | ], 334 | ]; 335 | 336 | Configurator::apply() 337 | ->configFromArray($config) 338 | ->to($this->container); 339 | 340 | $instance = $this->container->get('example_service'); 341 | 342 | $this->assertInstanceOf(ExampleClassWithArgs::class, $instance); 343 | $this->assertSame(['example_argument'], $instance->getConstructorArgs()); 344 | } 345 | 346 | public function testItCanCreateAServiceAlias(): void 347 | { 348 | $config = [ 349 | 'di' => [ 350 | 'services' => [ 351 | 'example_class' => [ 352 | 'class' => ExampleClass::class, 353 | 'singleton' => true, 354 | ], 355 | 'example_alias' => [ 356 | 'service' => 'example_class', 357 | ], 358 | ], 359 | ], 360 | ]; 361 | 362 | Configurator::apply() 363 | ->configFromArray($config) 364 | ->to($this->container); 365 | 366 | $this->assertSame($this->container->get('example_class'), $this->container->get('example_alias')); 367 | } 368 | 369 | public function testItInjectsTheContainerAsAConstructorDependency(): void 370 | { 371 | $config = [ 372 | 'di' => [ 373 | 'services' => [ 374 | 'example_service' => [ 375 | 'class' => ExampleClassWithArgs::class, 376 | 'arguments' => [Configurator::container()], 377 | ], 378 | ], 379 | ], 380 | ]; 381 | 382 | Configurator::apply() 383 | ->configFromArray($config) 384 | ->to($this->container); 385 | 386 | $instance = $this->container->get('example_service'); 387 | 388 | $this->assertSame([$this->container], $instance->getConstructorArgs()); 389 | } 390 | 391 | public function testItInjectsTheContainerAsAMethodDependency(): void 392 | { 393 | $config = [ 394 | 'di' => [ 395 | 'services' => [ 396 | 'example_service' => [ 397 | 'class' => ExampleClass::class, 398 | 'methods' => [ 399 | 'setValue' => [Configurator::container()], 400 | ], 401 | ], 402 | ], 403 | ], 404 | ]; 405 | 406 | Configurator::apply() 407 | ->configFromArray($config) 408 | ->to($this->container); 409 | 410 | $instance = $this->container->get('example_service'); 411 | 412 | $this->assertSame($this->container, $instance->getValue()); 413 | } 414 | 415 | public function testItInjectsTheContainerAsFactoryDependency(): void 416 | { 417 | $config = [ 418 | 'class_name' => ExampleClassWithArgs::class, 419 | 'di' => [ 420 | 'services' => [ 421 | 'example_service' => [ 422 | 'factory' => ExampleFactory::class, 423 | 'arguments' => [ 424 | 'config.class_name', 425 | Configurator::container(), 426 | ], 427 | ], 428 | ], 429 | ], 430 | ]; 431 | 432 | Configurator::apply() 433 | ->configFromArray($config) 434 | ->to($this->container); 435 | 436 | $instance = $this->container->get('example_service'); 437 | 438 | $this->assertSame([$this->container], $instance->getConstructorArgs()); 439 | } 440 | } 441 | --------------------------------------------------------------------------------