Initial commit, cloned from lumax/dependency-injection-component

This commit is contained in:
Daniel Winning 2025-04-26 15:03:52 +01:00
commit 5ee5de9ef1
19 changed files with 3440 additions and 0 deletions

5
.gitattributes vendored Normal file
View file

@ -0,0 +1,5 @@
/tests/ export-ignore
Jenkinsfile export-ignore
phpunit.xml export-ignore
.gitignore export-ignore
.gitattributes export-ignore

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/.idea
/vendor
/node_modules
/coverage
/.phpunit.result.cache

35
CHANGELOG.md Normal file
View file

@ -0,0 +1,35 @@
# Luma | Dependency Injection Component Change Log
## [1.3.0] - 2024-05-05
### Added
- Add support for configuration parameters as service arguments
### Changed
- N/A
### Deprecated
- N/A
### Removed
- N/A
### Fixed
- N/A
### Security
- Updated dependencies
---
## [1.2.2] - 2024-03-02
- Minor housekeeping; `package.json` cleanup, `composer.json` cleanup
- Update build pipelines
## [1.2.1] - 2024-02-23
- Update build pipelines
## [1.2.0] - 2024-02-22
- Added CHANGELOG
- Added automated build pipeline
- `DependencyManager` now throws a RuntimeException if `loadDependenciesFromFile` is called with an invalid filetype (such as JSON).
- Increased test coverage to 100%

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# Luma | Dependency Injection Component
<div>
<!-- Version Badge -->
<img src="https://img.shields.io/badge/Version-1.3.0-blue" alt="Version 1.3.0">
<!-- PHP Coverage Badge -->
<img src="https://img.shields.io/badge/PHP Coverage-96.36%25-green" alt="PHP Coverage 96.36%">
<!-- License Badge -->
<img src="https://img.shields.io/badge/License-GPL--3.0--or--later-34ad9b" alt="License GPL--3.0--or--later">
</div>
A PHP package for managing dependencies and dependency injection.
---
## Installation
You can install this package via [Composer](https://getcomposer.org/):
```bash
composer require lumax/dependency-injection-component
```
## Usage
### DependencyContainer
The `DependencyContainer` class provides a simple way to manage and retrieve dependencies. You can add and retrieve
dependencies as follows:
```php
use Loom\DependencyInjectionComponent\DependencyContainer;
// Create a container
$container = new DependencyContainer();
// Add a dependency
$container->add(MyDependency::class, new MyDependency());
// Retrieve a dependency
$dependency = $container->get(MyDependency::class);
```
### DependencyManager
The `DependencyManager` class allows you to load dependencies from a YAML configuration file and register them in a
`DependencyContainer`. Here's an example of how to use it:
```php
use Loom\DependencyInjectionComponent\DependencyContainer;
use Loom\DependencyInjectionComponent\DependencyManager;
// Create a container
$container = new DependencyContainer();
// Create a manager and load dependencies from a YAML file
$manager = new DependencyManager($container);
$manager->loadDependenciesFromFile('path/to/dependencies.yaml');
```
In your YAML configuration file (`dependencies.yaml`), you can define services and their arguments for injection.
### Setting Up Your Services/Dependencies Definitions
Here's an example of a `dependencies.yaml` file that demonstrates how to define services and their arguments for injection:
```yaml
services:
myService:
class: 'Namespace\MyService'
arguments:
- 'argument1'
- 'argument2'
- '@anotherService' # Inject another service
```
Here's a breakdown of the elements in the dependencies.yaml file:
- `services`: This section defines the services and their configurations.
- `alias`: Your chosen alias for the service - `myService`.
- `class`: The fully qualified class name of the service class.
- `arguments`: An array of constructor arguments. Use "@" to reference other services.
Once you've set up your `dependencies.yaml` file with the desired services and configurations, you can load and manage
these dependencies using the Dependency Injection Package.
## License
This package is open-source software licensed under the [GNU General Public License, version 3.0 (GPL-3.0)](https://opensource.org/licenses/GPL-3.0).

35
composer.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "loomlabs/loom.di-component",
"description": "A Dependency Injection Package",
"type": "library",
"minimum-stability": "stable",
"require": {
"symfony/yaml": "^6.3",
"psr/container": "^2.0",
"lumax/framework-component": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^10.3"
},
"autoload": {
"psr-4": {
"Loom\\DependencyInjectionComponent\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Loom\\DependencyInjectionComponent\\Tests\\": "tests/Unit/"
}
},
"scripts": {
"test": "php -d xdebug.mode=coverage ./vendor/bin/phpunit --testdox --colors=always --coverage-html coverage --coverage-clover coverage/coverage.xml --testdox-html coverage/testdox.html && npx badger --phpunit ./coverage/coverage.xml && npx badger --version ./composer.json && npx badger --license ./composer.json"
},
"authors": [
{
"name": "Daniel Winning",
"email": "daniel@winningsoftware.co.uk"
}
],
"version": "1.3.0",
"license": "GPL-3.0-or-later"
}

2779
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

14
package-lock.json generated Normal file
View file

@ -0,0 +1,14 @@
{
"name": "dependency-injection",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@dannyxcii/badger": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@dannyxcii/badger/-/badger-0.4.1.tgz",
"integrity": "sha512-fK39595AbeKiajI6cxI+Odov+/r8aWXLItNELoxFhf59uzTEI6lL7JUm2t7GyBi0OZieO+7me1zWdRkIONdFOQ==",
"dev": true
}
}
}

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"description": "A PHP package for managing dependencies and dependency injection.",
"repository": {
"type": "git",
"url": "git+https://github.com/DanielWinning/dependency-injection-component.git"
},
"author": "Daniel Winning <daniel@winningsoftware.co.uk>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/DanielWinning/dependency-injection-component/issues"
},
"homepage": "https://github.com/DanielWinning/dependency-injection-component#readme",
"devDependencies": {
"@dannyxcii/badger": "^0.4.1"
}
}

22
phpunit.xml Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true"
bootstrap="vendor/autoload.php"
displayDetailsOnIncompleteTests="true"
displayDetailsOnSkippedTests="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerNotices="true"
failOnWarning="true"
failOnRisky="true">
<testsuites>
<testsuite name="Unit Tests">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View file

@ -0,0 +1,80 @@
<?php
namespace Loom\DependencyInjectionComponent;
use Loom\DependencyInjectionComponent\Exception\NotFoundException;
use Psr\Container\ContainerInterface;
class DependencyContainer implements ContainerInterface
{
private array $container = [];
/**
* @param string $key
* @param mixed $value
*
* @return void
*/
public function add(string $key, mixed $value): void
{
$this->container[$key] = $value;
}
/**
* @param $id
*
* @return mixed
*
* @throws NotFoundException
*/
public function get($id): mixed
{
if ($this->has($id)) {
return $this->container[$id];
}
if (class_exists($id)) {
return $this->resolveService($id);
}
throw new NotFoundException("Dependency not found $id");
}
/**
* @param string $id
*
* @return bool
*/
public function has(string $id): bool
{
return isset($this->container[$id]);
}
/**
* @return array
*/
public function getServices(): array
{
return $this->container;
}
/**
* Resolve a service by its fully qualified class name (FQCN).
*
* @param string $className
*
* @return mixed
*
* @throws NotFoundException
*/
private function resolveService(string $className): mixed
{
foreach ($this->container as $value) {
if ($value instanceof $className) {
return $value;
}
}
throw new NotFoundException("Dependency not found: $className");
}
}

126
src/DependencyManager.php Normal file
View file

@ -0,0 +1,126 @@
<?php
namespace Loom\DependencyInjectionComponent;
use Luma\Framework\Luma;
use Symfony\Component\Yaml\Yaml;
class DependencyManager
{
private DependencyContainer $container;
/**
* @param DependencyContainer $container
*/
public function __construct(DependencyContainer $container)
{
$this->container = $container;
}
/**
* @param string $filename
*
* @return void
*
* @throws Exception\NotFoundException
*/
public function loadDependenciesFromFile(string $filename): void
{
$loadedConfig = $this->loadConfigFile($filename);
if (isset($loadedConfig['services'])) {
$this->registerServices($loadedConfig['services']);
}
}
/**
* @param string $filename
*
* @return array
*/
private function loadConfigFile(string $filename): array
{
if (!str_ends_with($filename, '.yaml')) {
throw new \RuntimeException("Invalid dependency configuration in YAML file: $filename");
}
$loadedConfig = Yaml::parseFile($filename);
if (is_null($loadedConfig)) {
return [];
}
return $loadedConfig;
}
/**
* @param array $services
*
* @return void
*
* @throws Exception\NotFoundException
*/
private function registerServices(array $services): void
{
foreach ($services as $key => $config) {
$this->validateServiceConfig($config);
$arguments = $this->resolveServiceArguments($config['arguments'] ?? []);
$serviceInstance = $this->instantiateService($config['class'], $arguments);
$this->container->add($key, $serviceInstance);
}
}
/**
* @param array $config
*
* @return void
*/
private function validateServiceConfig(array $config): void
{
if (!isset($config['class']) || !class_exists($config['class'])) {
throw new \RuntimeException("Invalid service class in configuration");
}
}
/**
* @param array $arguments
*
* @return array
*
* @throws Exception\NotFoundException
*/
private function resolveServiceArguments(array $arguments): array
{
$resolvedArguments = [];
foreach ($arguments as $argument) {
if (is_string($argument) && str_starts_with($argument, '@')) {
$serviceAlias = ltrim($argument, '@');
$resolvedArguments[] = $this->container->get($serviceAlias);
} elseif (is_string($argument) && str_starts_with($argument, ':') && str_ends_with($argument, ':')) {
$resolvedArguments[] = Luma::getConfigParam(trim($argument, ':'));
} else {
$resolvedArguments[] = $argument;
}
}
return $resolvedArguments;
}
/**
* @param string $class
* @param array $arguments
*
* @return object
*/
private function instantiateService(string $class, array $arguments): object
{
if (empty($arguments)) {
return new $class();
} else {
return new $class(...$arguments);
}
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Loom\DependencyInjectionComponent\Exception;
use Psr\Container\NotFoundExceptionInterface;
class NotFoundException extends \Exception implements NotFoundExceptionInterface
{
public function __construct(string $message = 'Entry not found', int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Loom\DependencyInjectionComponent\Tests;
use Loom\DependencyInjectionComponent\DependencyContainer;
use Loom\DependencyInjectionComponent\Exception\NotFoundException;
use PHPUnit\Framework\TestCase;
class DependencyContainerTest extends TestCase
{
/**
* @return void
*
* @throws NotFoundException
*/
public function testAddAndGet(): void
{
$container = new DependencyContainer();
$container->add('app.lang', 'en_gb');
$this->assertEquals('en_gb', $container->get('app.lang'));
}
/**
* @return void
*/
public function testHas(): void
{
$container = new DependencyContainer();
$container->add('app.lang', 'en_gb');
$this->assertTrue($container->has('app.lang'));
$this->assertFalse($container->has('el'));
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testGetNonExistentKeyThrowsNotFoundException(): void
{
$container = new DependencyContainer();
$this->expectException(NotFoundException::class);
$container->get('app');
}
/**
* @return void
*/
public function testGetServices(): void
{
$container = new DependencyContainer();
$service = new \stdClass();
$container->add('test', $service);
$this->assertEquals(['test' => $service], $container->getServices());
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testResolveService(): void
{
$container = new DependencyContainer();
$service = new \stdClass();
$service->testString = 'Test Service';
$container->add('test', $service);
$resolvedService = $container->get('test');
$this->assertSame($service, $resolvedService);
$this->assertEquals($service->testString, $resolvedService->testString);
$resolvedService = $container->get('\stdClass');
$this->assertSame($service, $resolvedService);
$this->assertEquals($service->testString, $resolvedService->testString);
$this->expectException(NotFoundException::class);
$container->get('\Exception');
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace Loom\DependencyInjectionComponent\Tests;
use Loom\DependencyInjectionComponent\DependencyContainer;
use Loom\DependencyInjectionComponent\DependencyManager;
use Loom\DependencyInjectionComponent\Exception\NotFoundException;
use PHPUnit\Framework\TestCase;
class DependencyManagerTest extends TestCase
{
/**
* @return void
*/
public function testItCreatesAnInstanceOfDependencyManager(): void
{
$container = new DependencyContainer();
$manager = new DependencyManager($container);
$this->assertInstanceOf(DependencyManager::class, $manager);
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testItThrowsNoErrorLoadingFromYaml(): void
{
$container = new DependencyContainer();
$manager = new DependencyManager($container);
$this->expectNotToPerformAssertions();
$manager->loadDependenciesFromFile($this->getTestServiceConfigPath());
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testItReturnsExpectedDependency(): void
{
$container = new DependencyContainer();
$manager = new DependencyManager($container);
$manager->loadDependenciesFromFile($this->getTestServiceConfigPath());
$arithmeticError = $container->get('arithmetic_error');
$this->assertInstanceOf(\ArithmeticError::class, $arithmeticError);
$this->assertEquals('Always the same error message', $arithmeticError->getMessage());
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testEmptyServiceConfigReturnsEmptyArray(): void
{
$container = new DependencyContainer();
$manager = new DependencyManager($container);
$manager->loadDependenciesFromFile($this->getTestServiceConfigPath('_empty'));
$this->assertEquals([], $container->getServices());
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testInvalidServiceConfigThrowsRuntimeException(): void
{
$container = new DependencyContainer();
$manager = new DependencyManager($container);
$this->expectException(\RuntimeException::class);
$manager->loadDependenciesFromFile($this->getTestServiceConfigPath('_invalid_class'));
}
/**
* @return void
*
* @throws NotFoundException
*/
public function testInvalidFileTypeThrowsRuntimeException(): void
{
$container = new DependencyContainer();
$manager = new DependencyManager($container);
self::expectException(\RuntimeException::class);
$manager->loadDependenciesFromFile($this->getTestServiceConfigPath('', 'services.json'));
}
/**
* @param string $append
*
* @param string|null $customName
* @return string
*/
private function getTestServiceConfigPath(string $append = '', string $customName = null): string
{
return sprintf('%s/%s', dirname(__FILE__, 2), $customName ?? "services$append.yaml");
}
}

1
tests/config.yaml Normal file
View file

@ -0,0 +1 @@
test_string: 'Test string'

0
tests/services.json Normal file
View file

16
tests/services.yaml Normal file
View file

@ -0,0 +1,16 @@
services:
stdcl:
class: \stdClass
arguments: []
standard_exception:
class: \Exception
arguments:
- 'Always the same error message'
arithmetic_error:
class: \ArithmeticError
arguments:
message: 'Always the same error message'
code: 0
previous: '@standard_exception'
assertion_error:
class: \AssertionError

View file

View file

@ -0,0 +1,3 @@
services:
test:
arguments: []