From 887b4b905fed3a139294246cced7bcb67c268b92 Mon Sep 17 00:00:00 2001 From: Oleksii Bulba Date: Sun, 8 Jan 2023 17:34:44 +0400 Subject: [PATCH 1/3] Chore: Enhancement - Added an exception message to ServiceNotRegisteredException exception; - Added a property `serviceId` to ServiceNotRegisteredException; - Added .gitattributes files to avoid unused files in vendor folder; Signed-off-by: Oleksii Bulba --- .gitattributes | 10 ++++++++++ src/Exception/ServiceNotRegisteredException.php | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7706dfe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpcs.xml export-ignore +/phpmd.xml export-ignore +/phpunit.xml export-ignore diff --git a/src/Exception/ServiceNotRegisteredException.php b/src/Exception/ServiceNotRegisteredException.php index aa22fb0..d11b7bd 100755 --- a/src/Exception/ServiceNotRegisteredException.php +++ b/src/Exception/ServiceNotRegisteredException.php @@ -7,4 +7,17 @@ class ServiceNotRegisteredException extends \RuntimeException implements NotFoundExceptionInterface { + private string $serviceId; + + public function __construct(string $serviceId, int $code = 0, ?\Throwable $previous = null) + { + $this->serviceId = $serviceId; + + parent::__construct(sprintf('Service "%s" not registered.', $this->serviceId), $code, $previous); + } + + public function getServiceId(): string + { + return $this->serviceId; + } } From 52ffbb2ef270563fcb985523bf15cd3c777e4b4b Mon Sep 17 00:00:00 2001 From: Oleksii Bulba Date: Sun, 8 Jan 2023 17:55:52 +0400 Subject: [PATCH 2/3] Chore: Enhancement - Normalized composer.json file; - Added phpmd, phpcs and phpunit to dev dependencies in composer.json file; - Added ci.yaml and static.yaml GitHub actions; - Added composer.json conflict with micro/autowire<1.1 - Removed micro/autowire from required and added it to suggest section; Signed-off-by: Oleksii Bulba --- .github/workflows/.editorconfig | 2 + .github/workflows/ci.yaml | 44 ++++++++++++++++++++++ .github/workflows/static.yaml | 65 +++++++++++++++++++++++++++++++++ composer.json | 40 +++++++++++++++----- 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/.editorconfig create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/static.yaml diff --git a/.github/workflows/.editorconfig b/.github/workflows/.editorconfig new file mode 100644 index 0000000..dd7e381 --- /dev/null +++ b/.github/workflows/.editorconfig @@ -0,0 +1,2 @@ +[{*.yaml,*.yml}] +indent_size = 2 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..2676020 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,44 @@ +name: Plugin CI +on: + push: + branches: [ 'master' ] + pull_request: + +jobs: + tests: + name: "Tests ${{ matrix.php-version }} deps ${{ matrix.dependency-versions }}" + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + # normal, highest, non-dev installs + php-version: [ '8.1', '8.2' ] + dependency-versions: [ 'highest' ] + include: + # testing lowest PHP version with the lowest dependencies + - php-version: '8.1' + dependency-versions: 'lowest' + + # testing dev versions with the highest PHP + - php-version: '8.2' + dependency-versions: 'highest' + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Composer install" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "${{ matrix.dependency-versions }}" + composer-options: "--prefer-dist --no-progress" + + - name: "Run tests" + run: "./vendor/bin/phpunit" diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml new file mode 100644 index 0000000..a17c349 --- /dev/null +++ b/.github/workflows/static.yaml @@ -0,0 +1,65 @@ +on: [pull_request] +name: Static analysis + +jobs: + composer-validate: + name: Composer validate & normalize + runs-on: ubuntu-22.04 + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + tools: composer-normalize + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Validate + run: composer validate --strict + + - name: Normalize + run: composer-normalize --dry-run + + phpcs: + name: PHP_CodeSniffer + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + tools: squizlabs/php_codesniffer:^3.7 + + - name: Download dependencies + uses: ramsey/composer-install@v2 + + - name: PHP_CodeSniffer + run: ./vendor/bin/phpcs --standard=phpcs.xml ./src/ + + phpmd: + name: Mess Detector + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + tools: phpmd/phpmd:^2.13 + + - name: Download dependencies + uses: ramsey/composer-install@v2 + + - name: Mess Detector + run: ./vendor/bin/phpmd ./src/ github ./phpmd.xml diff --git a/composer.json b/composer.json index 5178b06..b801f79 100755 --- a/composer.json +++ b/composer.json @@ -1,14 +1,8 @@ { "name": "micro/dependency-injection", "description": "Dependency injection service container", - "type": "library", "license": "MIT", - "version": "1.1", - "autoload": { - "psr-4": { - "Micro\\Component\\DependencyInjection\\": "src/" - } - }, + "type": "library", "authors": [ { "name": "Stanislau.Komar", @@ -16,8 +10,34 @@ } ], "require": { - "php": ">=8.0", - "psr/container": "^2.0", - "micro/autowire": "^1" + "php": "^8.1 || ^8.2", + "psr/container": "^2.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.29", + "phpmd/phpmd": "^2.13", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.7" + }, + "conflict": { + "micro/autowire": "<1.1" + }, + "suggest": { + "micro/autowire": "Autowire helper for dependency injection" + }, + "autoload": { + "psr-4": { + "Micro\\Component\\DependencyInjection\\": "src/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } } } From dc893c38cf2637675b65c8e3a5edf5db485b5a13 Mon Sep 17 00:00:00 2001 From: Oleksii Bulba Date: Mon, 9 Jan 2023 01:21:34 +0400 Subject: [PATCH 3/3] Chore: Enhancement - Fixed tests after removing AutowiredContainer; - Enhanced unit tests; - Added arguments to decorated closure: actual decorated service and current container; - Dropped support for php 8.1; - PHP Code Sniffer fixes; - Updated example in README.md; Signed-off-by: Oleksii Bulba --- .github/workflows/ci.yaml | 4 +- README.md | 52 ++++++-- composer.json | 2 +- phpcs.xml | 7 +- src/Container.php | 40 ++----- src/ContainerDecoratorInterface.php | 2 +- src/ContainerRegistryInterface.php | 1 - .../ServiceNotRegisteredException.php | 3 +- .../ServiceRegistrationException.php | 4 +- tests/unit/ContainerTest.php | 112 +++++++++--------- 10 files changed, 119 insertions(+), 108 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2676020..08a0740 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,11 +13,11 @@ jobs: fail-fast: false matrix: # normal, highest, non-dev installs - php-version: [ '8.1', '8.2' ] + php-version: [ '8.2' ] dependency-versions: [ 'highest' ] include: # testing lowest PHP version with the lowest dependencies - - php-version: '8.1' + - php-version: '8.2' dependency-versions: 'lowest' # testing dev versions with the highest PHP diff --git a/README.md b/README.md index ded1663..56ec097 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### Requirements -PHP >= 8.0.0 +PHP >= 8.2 ### How to use the library @@ -34,7 +34,7 @@ include 'vendor/autoload.php'; ```php use \Micro\Component\DependencyInjection\Container; -class Logger { +class Logger implements LoggerInterface { } class Mailer { @@ -43,10 +43,14 @@ class Mailer { $container = new Container(); -$container->register(Logger::class, function(Container $container) { +$container->register(LoggerInterface::class, function(Container $container) { return new Logger(); }); +$container->register('logger.doctrine', function(Container $container) { + return new Logger('doctrine-channel'); +}); + $container->register(Mailer::class, function(Container $container) { return new Mailer($container->get(Logger::class)); }); @@ -70,13 +74,11 @@ class HelloWorldFacade implements HelloWorldFacadeInterface } } - -class HelloWorldDecorator implements HelloWorldFacadeInterface +class NiceHelloWorldDecorator implements HelloWorldFacadeInterface { public function __construct( - private readonly HelloWorldFacadeInterface $decoratedService, - ) - { + private readonly HelloWorldFacadeInterface $decoratedService + ) { } public function hello(string $name): string @@ -87,25 +89,51 @@ class HelloWorldDecorator implements HelloWorldFacadeInterface } } +class HelloWorldLoggerAwareDecorator implements HelloWorldFacadeInterface +{ + public function __construct( + private readonly HelloWorldFacadeInterface $decoratedService, + private readonly LoggerInterface $logger + ) { + } + + public function hello(string $name): string + { + $result = $this->decoratedService->hello($name); + + $this->logger->info->info($result); + + return $result; + } +} + $container = new Container(); $container->register(HelloWorldFacadeInterface::class, function () { return new HelloWorldFacade(); }); +$container->register(HelloWorldFacadeInterface::class, function ( + HelloWorldFacadeInterface $decorated +) { + return new NiceHelloWorldDecorator($decorated); +}); + $container->decorate(HelloWorldFacadeInterface::class, function( - HelloWorldFacadeInterface $serviceForDecoration + HelloWorldFacadeInterface $decorated, + Container $container ) { - return new HelloWorldLoggerAwareDecorator($serviceForDecoration); + return new HelloWorldLoggerAwareDecorator( + $decorated, + $container->get(LoggerInterface::class) + ); }); echo $container->get(HelloWorldFacadeInterface::class)->hello('Stas'); // Output: Hello, Stas. I'm glad to see you - ``` - ### Sample code for: - [PSR-11](https://www.php-fig.org/psr/psr-11/) diff --git a/composer.json b/composer.json index b801f79..a6274b8 100755 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": "^8.1 || ^8.2", + "php": "^8.2", "psr/container": "^2.0" }, "require-dev": { diff --git a/phpcs.xml b/phpcs.xml index 727ba01..6581b2d 100755 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,7 +14,12 @@ - + + + + + + diff --git a/src/Container.php b/src/Container.php index 5d12fb7..6133f6c 100755 --- a/src/Container.php +++ b/src/Container.php @@ -4,7 +4,6 @@ use Micro\Component\DependencyInjection\Exception\ServiceNotRegisteredException; use Micro\Component\DependencyInjection\Exception\ServiceRegistrationException; -use \Closure; use Psr\Container\ContainerInterface; class Container implements ContainerInterface, ContainerRegistryInterface, ContainerDecoratorInterface @@ -12,25 +11,18 @@ class Container implements ContainerInterface, ContainerRegistryInterface, Conta /** * @var array */ - private array $services; + private array $services = []; /** - * @var array + * @var array */ - private array $servicesRaw; + private array $servicesRaw = []; /** - * @var array> + * @var array> */ private array $decorators = []; - public function __construct( - ) - { - $this->services = []; - $this->servicesRaw = []; - } - /** * @template T * @@ -54,7 +46,7 @@ public function has(string $id): bool /** * {@inheritDoc} */ - public function register(string $id, Closure $service): void + public function register(string $id, \Closure $service): void { if($this->has($id)) { throw new ServiceRegistrationException(sprintf('Service "%s" already registered', $id)); @@ -66,13 +58,13 @@ public function register(string $id, Closure $service): void /** * {@inheritDoc} */ - public function decorate(string $id, Closure $service, int $priority = 0): void + public function decorate(string $id, \Closure $service, int $priority = 0): void { if(!array_key_exists($id, $this->decorators)) { $this->decorators[$id] = []; } - $this->decorators[$id][] = [$service, $priority]; + $this->decorators[$id][$priority] = $service; } /** @@ -93,7 +85,6 @@ private function lookup(string $id): object /** * @param string $serviceId - * @return object */ protected function initializeService(string $serviceId): void { @@ -101,8 +92,8 @@ protected function initializeService(string $serviceId): void throw new ServiceNotRegisteredException($serviceId); } - $raw = $this->servicesRaw[$serviceId]; - $service = $raw($this); + $raw = $this->servicesRaw[$serviceId]; + $service = $raw($this); $this->services[$serviceId] = $service; if(!array_key_exists($serviceId, $this->decorators)) { @@ -111,19 +102,10 @@ protected function initializeService(string $serviceId): void $decorators = $this->decorators[$serviceId]; - usort($decorators, function(array $left, array $right): int { - $l = $left[1]; - $r = $right[1]; - if($l === $r) { - return 0; - } - - return $left[1] > $right[1] ? 1 : -1; - }); + ksort($decorators); - /** @var array $decorator */ foreach ($decorators as $decorator) { - $this->services[$serviceId] = $decorator[0](); + $this->services[$serviceId] = $decorator($this->services[$serviceId], $this); } } } diff --git a/src/ContainerDecoratorInterface.php b/src/ContainerDecoratorInterface.php index 65767ea..e95089c 100644 --- a/src/ContainerDecoratorInterface.php +++ b/src/ContainerDecoratorInterface.php @@ -12,4 +12,4 @@ interface ContainerDecoratorInterface * @return void */ public function decorate(string $id, \Closure $service, int $priority = 0): void; -} \ No newline at end of file +} diff --git a/src/ContainerRegistryInterface.php b/src/ContainerRegistryInterface.php index 33e4dac..44d76c8 100755 --- a/src/ContainerRegistryInterface.php +++ b/src/ContainerRegistryInterface.php @@ -2,7 +2,6 @@ namespace Micro\Component\DependencyInjection; - interface ContainerRegistryInterface { /** diff --git a/src/Exception/ServiceNotRegisteredException.php b/src/Exception/ServiceNotRegisteredException.php index d11b7bd..b6dca6f 100755 --- a/src/Exception/ServiceNotRegisteredException.php +++ b/src/Exception/ServiceNotRegisteredException.php @@ -4,8 +4,7 @@ use Psr\Container\NotFoundExceptionInterface; -class ServiceNotRegisteredException extends \RuntimeException - implements NotFoundExceptionInterface +class ServiceNotRegisteredException extends \RuntimeException implements NotFoundExceptionInterface { private string $serviceId; diff --git a/src/Exception/ServiceRegistrationException.php b/src/Exception/ServiceRegistrationException.php index 73c2a8f..79a2f04 100755 --- a/src/Exception/ServiceRegistrationException.php +++ b/src/Exception/ServiceRegistrationException.php @@ -4,8 +4,6 @@ use Psr\Container\ContainerExceptionInterface; -class ServiceRegistrationException extends \RuntimeException - implements ContainerExceptionInterface +class ServiceRegistrationException extends \RuntimeException implements ContainerExceptionInterface { - } diff --git a/tests/unit/ContainerTest.php b/tests/unit/ContainerTest.php index c35223b..dacca27 100755 --- a/tests/unit/ContainerTest.php +++ b/tests/unit/ContainerTest.php @@ -14,17 +14,16 @@ public function testContainerResolveDependencies(): void { $container = new Container(); - $container->register( - 'test', function ( Container $container ) { - return new class { - public string $name = 'success'; - }; - } - ); + $container->register('test', function () { + return new NamedService('success'); + }); + /** @var NamedInterface $service */ $service = $container->get('test'); $this->assertIsObject($service); - $this->assertEquals('success', $service->name); + $this->assertInstanceOf(NamedInterface::class, $service); + $this->assertInstanceOf(NamedService::class, $service); + $this->assertEquals('success', $service->getName()); } public function testRegisterTwoServicesWithEqualAliasesException(): void @@ -32,18 +31,8 @@ public function testRegisterTwoServicesWithEqualAliasesException(): void $this->expectException(ServiceRegistrationException::class); $container = new Container(); - $container->register( - 'test', function ( Container $container ) { - return new class { - }; - } - ); - $container->register( - 'test', function ( Container $container ) { - return new class { - }; - } - ); + $container->register('test', function () { return new class {}; }); + $container->register('test', function () { return new class {}; }); } public function testContainerUnresolvedException(): void @@ -51,57 +40,68 @@ public function testContainerUnresolvedException(): void $this->expectException(ServiceNotRegisteredException::class); $container = new Container(); - $container->register( - 'test', function ( Container $container ) { - return new class { - public string $name = 'success'; - }; - } - ); + $container->register('test', function () { + return new NamedService('success'); + }); $container->get('test2'); } public function testDecorateService(): void { - $container = new ContainerAutowire(new Container()); + $container = new Container(); - $container->register('test', function ($container) { - return new class { - public function getA(): string { return 'D'; } - }; + $container->register('test', function () { + return new NamedService('D'); }); - - $container->decorate('test', function (Container $container) { - return new class($container->get('test')) { - public function __construct(private readonly object $decorated) {} - public function getA(): string { - return 'C' . $this->decorated->getA(); - } - }; + $container->decorate('test', function (NamedInterface $decorated) { + return new NamedServiceDecorator($decorated, 'C'); }); - $container->decorate('test', function (Container $container) { - return new class($container->get('test')) { - public function __construct(private readonly object $decorated) {} - public function getA(): string { - return 'A' . $this->decorated->getA(); - } - }; + $container->decorate('test', function (NamedInterface $decorated) { + return new NamedServiceDecorator($decorated, 'A'); }, 10); - $container->decorate('test', function (Container $container) { - return new class($container->get('test')) { - public function __construct(private readonly object $decorated) {} - public function getA(): string { - return 'B' . $this->decorated->getA(); - } - }; + $container->decorate('test', function (NamedInterface $decorated) { + return new NamedServiceDecorator($decorated, 'B'); }, 5); - + /** @var NamedInterface $result */ $result = $container->get('test'); - $this->assertEquals('ABCD', $result->getA()); + $this->assertInstanceOf(NamedServiceDecorator::class, $result); + $this->assertInstanceOf(NamedInterface::class, $result); + $this->assertEquals('ABCD', $result->getName()); + } +} + +interface NamedInterface +{ + public function getName(): string; +} + +readonly class NamedService implements NamedInterface +{ + public function __construct(private string $name) + { + } + + public function getName(): string + { + return $this->name; + } +} + +readonly class NamedServiceDecorator implements NamedInterface +{ + public function __construct( + private object $decorated, + private string $name + ) { + } + + public function getName(): string + { + return $this->name.$this->decorated->getName(); } }