From 578390343e656f0b81fbca2bb7bfff1b2c04e898 Mon Sep 17 00:00:00 2001 From: bidi Date: Mon, 2 Nov 2026 17:23:11 +0200 Subject: [PATCH 1/5] updated composer, code audit, added docs Signed-off-by: bidi --- .github/workflows/codecov.yml | 8 +- .github/workflows/static-analysis.yml | 6 +- .laminas-ci.json | 5 +- CHANGELOG.md | 565 ++++++++++++++++-- README.md | 35 +- SECURITY.md | 1 + composer.json | 12 +- docs/book/v6/cache.md | 53 ++ docs/book/v6/configuration.md | 12 + docs/book/v6/factories.md | 52 ++ docs/book/v6/factories/abstract.md | 48 ++ docs/book/v6/factories/repository.md | 68 +++ docs/book/v6/factories/service.md | 122 ++++ docs/book/v6/installation.md | 15 + docs/book/v6/overview.md | 39 ++ docs/book/v6/upgrading.md | 113 ++++ docs/book/v6/usage.md | 16 + mkdocs.yml | 15 +- phpstan.neon | 2 +- phpunit.xml | 23 +- src/Annotation/Entity.php | 11 +- src/Annotation/Inject.php | 11 +- src/Annotation/Service.php | 3 +- src/ConfigProvider.php | 6 + src/Exception/ExceptionInterface.php | 3 - src/Exception/RuntimeException.php | 11 + src/Factory/AbstractAnnotatedFactory.php | 25 +- src/Factory/AnnotatedRepositoryFactory.php | 17 +- .../AnnotatedServiceAbstractFactory.php | 33 +- src/Factory/AnnotatedServiceFactory.php | 61 +- test/AbstractAnnotatedFactoryTest.php | 73 +++ test/AnnotatedRepositoryFactoryTest.php | 126 ++-- test/AnnotatedServiceAbstractFactoryTest.php | 93 +++ test/AnnotatedServiceFactoryTest.php | 253 +++++--- test/AnnotationTest.php | 66 ++ test/ConfigProviderTest.php | 6 +- test/Fixture/AnnotatedService.php | 22 + test/Fixture/ClassStringService.php | 17 + test/Fixture/CommentOnlyService.php | 12 + test/Fixture/ConfigService.php | 18 + test/Fixture/Dependency.php | 9 + test/Fixture/DottedConfigService.php | 17 + test/Fixture/ExampleEntity.php | 9 + test/Fixture/ExampleRepository.php | 16 + test/Fixture/ExposedAnnotatedFactory.php | 20 + test/Fixture/MissingServiceService.php | 17 + test/Fixture/NoConstructorService.php | 9 + test/Fixture/NullKeyService.php | 17 + test/Fixture/OptionalParamsService.php | 12 + test/Fixture/PlainClass.php | 9 + test/Fixture/RepositoryWithoutAnnotation.php | 14 + test/Fixture/RequiredParamsWithoutInject.php | 12 + test/Fixture/ServiceInterface.php | 9 + test/Fixture/SetterInjectedService.php | 42 ++ test/Fixture/UnbracedInjectService.php | 18 + test/Fixture/UnknownAnnotationClass.php | 12 + test/TestClass.php | 9 - 57 files changed, 1984 insertions(+), 344 deletions(-) create mode 100644 docs/book/v6/cache.md create mode 100644 docs/book/v6/configuration.md create mode 100644 docs/book/v6/factories.md create mode 100644 docs/book/v6/factories/abstract.md create mode 100644 docs/book/v6/factories/repository.md create mode 100644 docs/book/v6/factories/service.md create mode 100644 docs/book/v6/installation.md create mode 100644 docs/book/v6/overview.md create mode 100644 docs/book/v6/upgrading.md create mode 100644 docs/book/v6/usage.md create mode 100644 test/AbstractAnnotatedFactoryTest.php create mode 100644 test/AnnotatedServiceAbstractFactoryTest.php create mode 100644 test/AnnotationTest.php create mode 100644 test/Fixture/AnnotatedService.php create mode 100644 test/Fixture/ClassStringService.php create mode 100644 test/Fixture/CommentOnlyService.php create mode 100644 test/Fixture/ConfigService.php create mode 100644 test/Fixture/Dependency.php create mode 100644 test/Fixture/DottedConfigService.php create mode 100644 test/Fixture/ExampleEntity.php create mode 100644 test/Fixture/ExampleRepository.php create mode 100644 test/Fixture/ExposedAnnotatedFactory.php create mode 100644 test/Fixture/MissingServiceService.php create mode 100644 test/Fixture/NoConstructorService.php create mode 100644 test/Fixture/NullKeyService.php create mode 100644 test/Fixture/OptionalParamsService.php create mode 100644 test/Fixture/PlainClass.php create mode 100644 test/Fixture/RepositoryWithoutAnnotation.php create mode 100644 test/Fixture/RequiredParamsWithoutInject.php create mode 100644 test/Fixture/ServiceInterface.php create mode 100644 test/Fixture/SetterInjectedService.php create mode 100644 test/Fixture/UnbracedInjectService.php create mode 100644 test/Fixture/UnknownAnnotationClass.php delete mode 100644 test/TestClass.php diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index c0fbc53..9652006 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,14 +15,14 @@ jobs: - ubuntu-latest php: - - "8.2" - "8.3" - "8.4" - "8.5" + - "8.6" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -36,7 +36,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v3 + uses: actions/cache@v6 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} @@ -50,6 +50,6 @@ jobs: run: vendor/bin/phpunit --colors=always --coverage-clover clover.xml - name: Send code coverage report to Codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 9976515..2763b9c 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -15,14 +15,14 @@ jobs: - ubuntu-latest php: - - "8.2" - "8.3" - "8.4" - "8.5" + - "8.6" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -36,7 +36,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.laminas-ci.json b/.laminas-ci.json index 29216fa..efdd72e 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,3 +1,6 @@ { - "backwardCompatibilityCheck": true + "ignore_php_platform_requirements": { + "8.6": true + }, + "backwardCompatibilityCheck": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index 44badec..7be796a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,221 +1,660 @@ -## 4.0.0 - 2022-09-19 +# Changelog + +All notable changes to this project are documented in this file. + +Version `5.x` is a separate, attribute-based line of this package (see `5.0.0`). +Version `6.0.0` continues the annotation-based API of `4.x`. + +## 6.0.0 - Unreleased ### Changed -* Added new feature to inject doctrine repositories + +- Minimum PHP version is now 8.3. Supported versions: 8.3, 8.4, 8.5 and 8.6. +- `AnnotatedRepositoryFactory` now throws a `RuntimeException` when the entity manager returns a repository that is not an instance of the requested class. Set `repositoryClass` on the entity to the registered repository. +- `AnnotatedServiceFactory::__invoke()` and `AnnotatedServiceFactory::createObject()` now return `object` instead of `mixed`. +- `AbstractAnnotatedFactory::CACHE_SERVICE` is now a typed constant (`public const string`). +- `Inject` accepts a single unbraced service, e.g. `@Inject("config")`. +- `Entity::getName()` is typed as `class-string`. +- `AnnotatedServiceAbstractFactory::canCreate()` returns `false` for classes whose annotations cannot be read, and the abstract factory reuses a single `AnnotatedServiceFactory`. +- Array and generic types are specified in docblocks, so the code passes PHPStan level 8. +- Development tooling: PHPUnit 12, PHPStan level 8, GitHub Actions run PHP 8.3 to 8.6 with updated `actions/checkout`, `actions/cache` and `codecov/codecov-action`, and the Laminas backward-compatibility check is disabled for this major version. +- Documentation for 6.0 is available in `docs/book/v6`, including an upgrade guide for 4.x and 5.x, and a page for the abstract factory. +- The cache documentation describes a PSR-6 cache pool (`Psr\Cache\CacheItemPoolInterface`) instead of `doctrine/cache` drivers. +- The README and docs show the `@Service` annotation in a docblock (`/**`). ### Added -* AnnotatedRepositoryFactory.php + +- `RuntimeException::repositoryMismatch()`. +- `psr/cache` and `psr/container` as direct requirements, and a `suggest` entry for a PSR-6 cache implementation. +- A test suite with fixtures, a real annotation reader and a real service manager, covering the factories, the abstract factory, the annotation reader selection, the annotations and the exceptions. ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- PHP 8.2 support. +- `doctrine/cache` as a requirement. It is still installed through `doctrine/orm`, but your own code should require it if it uses its classes. ### Fixed -* Nothing -## 3.2.0 - 2022-04-29 +- The constructor of a service was called a second time while injecting setters. Services with `readonly` promoted properties could not be created. +- A dotted dependency such as `config.key` failed when the value was `null`. It is now injected as `null`. +- A dotted dependency longer than the data (e.g. `config.a.b` where `config['a']` is a string) returned an intermediate value. It now throws an `InvalidArgumentException`. +- `@Inject("config")` without curly braces failed with a creation error. +- `has()` and `get()` on the container threw an `AnnotationException` for unrelated classes that contain annotations unknown to the annotation reader, when the abstract factory was registered. +- Static methods annotated with `@Inject` are no longer called during setter injection. + +## 5.2.0 - 2024-10-23 + +### Changed + +- Version 5 entered maintenance mode (Issue #52). +- `laminas/laminas-coding-standard` updated to `^3.0`. +- Code linting. +- `SECURITY.md` targets the latest releases, and the site description in `mkdocs.yml` was updated. + +### Added + +- PHP 8.4 support. +- `.laminas-ci.json`. + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + +## 5.1.2 - 2024-05-03 + +### Changed + +- Documentation build workflow and `mkdocs.yml`. + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + +## 5.1.1 - 2024-04-24 + +### Changed + +- Documentation build workflow, `mkdocs.yml` and `README.md`. + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + +## 5.1.0 - 2024-04-24 + +### Changed + +- Continuous integration uses the GitHub workflows provided by Laminas. +- `doctrine/orm` constraint changed from `^2.0 || ^3.0` to `^2.9 || ^3.0`. +- Code linting. + +### Added + +- Documentation for versions 4 and 5 (Issue #37), and the documentation build GitHub Action. + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- The `Build static` badge in `README.md`. + +## 5.0.0 - 2024-03-08 + +### Changed + +- Annotations were replaced with PHP attributes (Issue #32): `Dot\AnnotatedServices\Attribute\Inject`, `Dot\AnnotatedServices\Attribute\Entity`, `AttributedServiceFactory` and `AttributedRepositoryFactory`. +- The package no longer depends on `doctrine/annotations`, `doctrine/cache` and `laminas/laminas-servicemanager`. +- `Dot\AnnotatedServices\ConfigProvider` no longer registers an abstract factory. + +### Added + +- `psr/container` `^1.0 || ^2.0` as a requirement. +- `SECURITY.md`. + +### Deprecated + +- Nothing + +### Removed + +- PHP 8.1 support. +- `AnnotatedServiceAbstractFactory` and the `@Service` annotation. +- Annotation caching, and `@Inject` on setters. + +### Fixed + +- Nothing + +## 4.4.0 - 2025-11-03 + +### Changed + +- Updated the PHP versions in the continuous integration workflows (Issue #60). +- Updated README, `SECURITY.md` and documentation. + +### Added + +- PHP 8.5 support. + +### Deprecated + +- Nothing + +### Removed + +- The version 5 documentation, which belongs to the `5.0` branch. + +### Fixed + +- Nothing + +## 4.3.0 - 2025-03-04 + +### Changed + +- Replaced Psalm with PHPStan for static analysis (Issue #58). + +### Added + +- `phpstan/phpstan` and `phpstan/phpstan-phpunit`, `phpstan.neon` and a static analysis GitHub workflow. +- Unit tests for `AnnotatedRepositoryFactory` and `AnnotatedServiceFactory` were extended. + +### Deprecated + +- Nothing + +### Removed + +- `vimeo/psalm` and `psalm.xml`. + +### Fixed + +- Nothing + +## 4.2.1 - 2025-01-27 + +### Changed + +- Psalm updated to 6.x (Issue #56). +- Codecov runs on PHP 8.4 as well. +- Updated README and documentation, and reordered the packages in `composer.json`. + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + +## 4.2.0 - 2024-10-23 + +### Changed + +- Version 4 entered active mode (Issue #52). +- `doctrine/orm` constraint changed from `^2.17.3` to `^2.20`. +- `laminas/laminas-coding-standard` updated to `^3.0`. +- Code linting. + +### Added + +- PHP 8.4 support. +- Documentation for version 4 (Issue #37), and the documentation build GitHub Action. + +### Deprecated + +- Nothing + +### Removed + +- PHP 8.1 support. + +### Fixed + +- Nothing + +## 4.1.7 - 2024-02-23 + +### Changed + +- Updated dependencies: `laminas/laminas-servicemanager` `^3.22.1`, `doctrine/annotations` `^1.14.3`, `doctrine/cache` `^1.12.1 || ^2.1.1`, `doctrine/orm` `^2.17.3`, `phpunit/phpunit` `^10.5.9` and `vimeo/psalm` `^5.20`. +- `doctrine/cache` versions in `composer.json` widened to stay backward compatible. +- Annotations are read with `Doctrine\Common\Annotations\PsrCachedReader`. `AbstractAnnotatedFactory::CACHE_SERVICE` is now `Psr\Cache\CacheItemPoolInterface::class`, and the service registered at that key must be a PSR-6 cache pool. + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- The call to `AnnotationRegistry::registerLoader()`. + +### Fixed + +- Deprecations of `doctrine/annotations` v2. + +## 4.1.4 - 2023-11-29 + +### Changed + +- README update. + +### Added + +- PHP 8.3 support. + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + +## 4.1.3 - 2023-08-29 + +### Changed + +- README update. + +### Added + +- Codecov integration (Issue #21), and the Codecov and static analysis badges. + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + +## 4.1.2 - 2023-07-25 + +### Changed + +- Nothing + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- `AbstractAnnotatedFactory::CACHE_SERVICE` was restored to public. + +## 4.1.1 - 2023-07-20 ### Changed -* Added PHP 8.0 and 8.1 compatibility + +- Nothing ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- The `annotationReader` property of `AbstractAnnotatedFactory` is initialized to `null`. + +## 4.1.0 - 2023-07-18 + +### Changed + +- Minimum PHP version is now 8.1. +- `phpunit/phpunit` updated to `^10.2` and `laminas/laminas-coding-standard` to `^2.5`. +- Added missing return types, and solved code consistency issues. +- Removed the `suggest` section from `composer.json`. +- README update. + +### Added + +- PHP 8.2 support. +- Psalm static analysis, and code style check. + +### Deprecated + +- Nothing + +### Removed + +- PHP 7.4 and 8.0 support. + +### Fixed + +- Nothing + +## 4.0.0 - 2022-09-19 + +### Changed + +- Added new feature to inject doctrine repositories + +### Added + +- AnnotatedRepositoryFactory.php + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing ## 3.2.1 - 2022-07-01 ### Changed -* Downgraded package doctrine/cache to v1 to fix conflict + +- Downgraded package doctrine/cache to v1 to fix conflict ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Conflict for doctrine/common v2 (requires doctrine/cache v1) +- Conflict for doctrine/common v2 (requires doctrine/cache v1) ## 3.2.0 - 2022-04-29 ### Changed -* Added PHP 8.0 and 8.1 compatibility + +- Added PHP 8.0 and 8.1 compatibility ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 3.1.0 - 2020-11-02 ### Changed -* Composer 2 compatibility. -* Update composer.json to require version 2 of laminas/laminas-dependency-plugin to be Composer 2 compatible. + +- Composer 2 compatibility. +- Update composer.json to require version 2 of laminas/laminas-dependency-plugin to be Composer 2 compatible. ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing ## 3.0.2 - 2020-04-23 Updated keywords ### Added -* Everything + +- Everything ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing ## 3.0.1 - 2020-04-22 Updated phpunit and php_codesniffer package versions ### Added -* Everything + +- Everything ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing ## 3.0.0 - 2020-04-12 Updated packages and php versions ### Added -* Everything + +- Everything ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing ## 2.0.0 - 2020-01-30 ### Changed -* Laminas and Mezzio migration. + +- Laminas and Mezzio migration. ### Added -* Nothing + +- Support ### Deprecated -* Nothing + +- Nothing ### Removed -* Support + +- Nothing ### Fixed -* Nothing +- Nothing ## 1.1.1 - 2018-05-03 ### Changed -* updated required dependencies + +- updated required dependencies ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 1.1.0 - 2017-03-15 ### Changed -* updated factories to use PSR11 container + +- updated factories to use PSR11 container ### Added -* documentation in readme file + +- documentation in readme file ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 1.0.1 - 2017-03-10 +### Changed + +- Nothing + ### Added -* Updated php file headers doc blocks, updated package description + +- Updated php file headers doc blocks, updated package description ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 1.0.0 - 2017-03-07 Initial tagged release ### Added -* Everything + +- Everything ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing diff --git a/README.md b/README.md index 04b81e2..d5b23d4 100644 --- a/README.md +++ b/README.md @@ -14,23 +14,24 @@ Documentation is available at: https://docs.dotkernel.org/dot-annotated-services | Branch | PSR-11 | Docblock Comments | OSS Lifecycle | PHP Version | |--------|----------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| 6.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | | 5.0 | 1 \|\| 2 | Attributes | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F5.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/5.2.0) | | 4.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | | 3.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | ## Badges -![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) +![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/blob/4.0/LICENSE.md) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/blob/6.0/LICENSE.md) -[![Build Static](https://github.com/dotkernel/dot-annotated-services/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0)](https://github.com/dotkernel/dot-annotated-services/actions/workflows/continuous-integration.yml) +[![Build Static](https://github.com/dotkernel/dot-annotated-services/actions/workflows/continuous-integration.yml/badge.svg?branch=6.0)](https://github.com/dotkernel/dot-annotated-services/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/dotkernel/dot-annotated-services/graph/badge.svg?token=ZBZDEA3LY8)](https://codecov.io/gh/dotkernel/dot-annotated-services) -[![PHPStan](https://github.com/dotkernel/dot-annotated-services/actions/workflows/static-analysis.yml/badge.svg?branch=4.0)](https://github.com/dotkernel/dot-annotated-services/actions/workflows/static-analysis.yml) +[![PHPStan](https://github.com/dotkernel/dot-annotated-services/actions/workflows/static-analysis.yml/badge.svg?branch=6.0)](https://github.com/dotkernel/dot-annotated-services/actions/workflows/static-analysis.yml) ## Installation @@ -113,7 +114,7 @@ The next step is to add the `@Entity` annotation in the repository class. The `name` field has to be the fully qualified class name. -Every repository should extend `Doctrine\ORM\EntityRepository`. +Every repository should extend `Doctrine\ORM\EntityRepository`, and must be the repository class of the entity. ```php use Doctrine\ORM\EntityRepository; @@ -132,11 +133,12 @@ class ExampleRepository extends EntityRepository Using this approach, no service manager configuration is required. It uses the registered abstract factory to create annotated services. To tell the abstract factory which services are to be created, you need to annotate the service class with the `@Service` annotation. +The annotation must be in a docblock, which starts with `/**`. ```php use Dot\AnnotatedServices\Annotation\Service; -/* +/** * @Service */ class ServiceClass @@ -149,11 +151,10 @@ And that's it, you don't need to configure the service manager with this class, ## Cache annotations -This package is built on top of `doctrine/annotation` and `doctrine/cache`. -To cache annotations, you should register a service factory at key `AbstractAnnotatedFactory::CACHE_SERVICE` that should return a valid `Doctrine\Common\Cache\Cache` cache driver. -See [Cache Drivers](https://github.com/doctrine/cache/tree/master/lib/Doctrine/Common/Cache) for available implementations offered by doctrine. +This package is built on top of `doctrine/annotations`. +To cache annotations, you should register a service factory at key `AbstractAnnotatedFactory::CACHE_SERVICE` that should return a valid PSR-6 cache pool, an implementation of `Psr\Cache\CacheItemPoolInterface`. -Below, we give an example, as defined in our frontend and admin starter applications: +Below, we give an example, using [symfony/cache](https://github.com/symfony/cache): ```php return [ @@ -161,7 +162,7 @@ return [ 'dependencies' => [ 'factories' => [ // used by dot-annotated-services to cache annotations - // needs to return a cache instance from Doctrine\Common\Cache + // needs to return a PSR-6 cache pool AbstractAnnotatedFactory::CACHE_SERVICE => AnnotationsCacheFactory::class, ] ], @@ -171,15 +172,19 @@ return [ ```php namespace Frontend\App\Factory; -use Doctrine\Common\Cache\FilesystemCache; +use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; class AnnotationsCacheFactory { - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container): CacheItemPoolInterface { //change this to suite your caching needs - return new FilesystemCache($container->get('config')['annotations_cache_dir']); + return new FilesystemAdapter( + namespace: 'annotations', + directory: $container->get('config')['annotations_cache_dir'], + ); } } ``` diff --git a/SECURITY.md b/SECURITY.md index 5889225..b54a862 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,7 @@ | Version | Supported | PHP Version | |---------|---------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| 6.0 | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | | 5.0 | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F5.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/5.2.0) | | 4.0 | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | | 3.0 | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | diff --git a/composer.json b/composer.json index 1f70e67..adf0d2b 100644 --- a/composer.json +++ b/composer.json @@ -20,17 +20,21 @@ "service-manager" ], "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0", "doctrine/annotations": "^1.14.3", - "doctrine/cache": "^1.12.1 || ^2.1.1", "doctrine/orm" : "^2.20", - "laminas/laminas-servicemanager": "^3.22.1" + "laminas/laminas-servicemanager": "^3.22.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.1 || ^2.0" + }, + "suggest": { + "symfony/cache": "A PSR-6 pool to cache parsed annotations" }, "require-dev": { "laminas/laminas-coding-standard": "^3.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.9" + "phpunit/phpunit": "^12.0" }, "autoload": { "psr-4": { diff --git a/docs/book/v6/cache.md b/docs/book/v6/cache.md new file mode 100644 index 0000000..a889565 --- /dev/null +++ b/docs/book/v6/cache.md @@ -0,0 +1,53 @@ +# Caching the annotations + +`dot-annotated-services` reads class annotations using [doctrine/annotations](https://github.com/doctrine/annotations) and can cache them in any [PSR-6](https://www.php-fig.org/psr/psr-6/) cache pool. + +## Configuration + +To cache annotations, register a service factory at key `Dot\AnnotatedServices\Factory\AbstractAnnotatedFactory::CACHE_SERVICE` (which is `Psr\Cache\CacheItemPoolInterface::class`). +It must return an implementation of `Psr\Cache\CacheItemPoolInterface`. + +Without this service, annotations are parsed on every request. + +If the `config` service has the key `debug` set to `true`, the cache checks whether the annotated files changed and refreshes stale entries. +Keep it `false` in production. + +See below an example on how you can configure `dot-annotated-services` to cache annotations. +You can add this configuration value to your application's config file: + +```php +'annotations_cache_dir' => __DIR__ . '/../../data/cache/annotations', +'dependencies' => [ + 'factories' => [ + Dot\AnnotatedServices\Factory\AbstractAnnotatedFactory::CACHE_SERVICE => YourApp\Factory\AnnotationsCacheFactory::class, + ], +], +``` + +where `AnnotationsCacheFactory` is a custom factory that needs to return a PSR-6 cache pool. +The example below uses [symfony/cache](https://github.com/symfony/cache), but any PSR-6 implementation works: + +```php +get('config')['annotations_cache_dir'], + ); + } +} +``` + +> Cache drivers from `doctrine/cache`, such as `FilesystemCache`, are not PSR-6 pools and cannot be used directly. diff --git a/docs/book/v6/configuration.md b/docs/book/v6/configuration.md new file mode 100644 index 0000000..9ee1bcb --- /dev/null +++ b/docs/book/v6/configuration.md @@ -0,0 +1,12 @@ +# Configuration + +After installation, register `dot-annotated-services` in your project by adding the below line to your configuration aggregator (usually: `config/config.php`): + +```php +Dot\AnnotatedServices\ConfigProvider::class, +``` + +This registers `AnnotatedServiceAbstractFactory`, which creates every class marked with the `@Service` annotation without further configuration. +`AnnotatedServiceFactory` and `AnnotatedRepositoryFactory` are registered per class under `factories` in your own `ConfigProvider`. + +See [Factories](factories.md) for details and [Cache](cache.md) to cache the parsed annotations. diff --git a/docs/book/v6/factories.md b/docs/book/v6/factories.md new file mode 100644 index 0000000..2f1295b --- /dev/null +++ b/docs/book/v6/factories.md @@ -0,0 +1,52 @@ +# Factories + +`dot-annotated-services` is based on three reusable factories - `AnnotatedRepositoryFactory`, `AnnotatedServiceFactory` and `AnnotatedServiceAbstractFactory` - able to inject any dependency into a class. + +## AnnotatedRepositoryFactory + +Injects entity repositories into a class. + +The repository must extend `Doctrine\ORM\EntityRepository` and carry the `@Entity` annotation. +The repository class returned by the entity manager must be the registered class: set the repository class on the entity (`#[ORM\Entity(repositoryClass: ...)]`) so that `EntityManager::getRepository()` returns an instance of it. + +### Exceptions thrown + +- `Dot\AnnotatedServices\Exception\RuntimeException` if repository does not exist +- `Dot\AnnotatedServices\Exception\RuntimeException` if repository does not extend `Doctrine\ORM\EntityRepository` +- `Dot\AnnotatedServices\Exception\RuntimeException` if repository does not have `@Entity` annotation +- `Dot\AnnotatedServices\Exception\RuntimeException` if the entity manager returns a repository that is not an instance of the requested class +- `Psr\Container\NotFoundExceptionInterface` if `Doctrine\ORM\EntityManagerInterface` does not exist in the service container +- `Psr\Container\ContainerExceptionInterface` if service manager is unable to provide an instance of `Doctrine\ORM\EntityManagerInterface` + +## AnnotatedServiceFactory + +Injects class dependencies into classes. + +A constructor with required parameters must carry the `@Inject` annotation. +A constructor without required parameters can omit it. + +If a dependency is specified using the dot notation, `AnnotatedServiceFactory` will try to load a service having that specific alias. +If it does not find one, it will try to load the dependency as a config tree, checking each segment if it's available in the service container. + +If a dependency is not registered in the service container but is the name of an existing class, `AnnotatedServiceFactory` creates it with `new`, without constructor arguments. + +You can use the `@Inject` annotation on public setters too. +They are called, in declaration order, after the object is constructed and injected with the configured dependencies. +The constructor is only called once, and static methods are ignored. + +### Exceptions thrown + +- `Dot\AnnotatedServices\Exception\RuntimeException` if service does not exist +- `Dot\AnnotatedServices\Exception\RuntimeException` if service does not have `@Inject` annotation on its constructor, and the constructor has required parameters +- `Dot\AnnotatedServices\Exception\RuntimeException` if a dependency is neither a service nor a class +- `Dot\AnnotatedServices\Exception\InvalidArgumentException` if a key of a dotted dependency does not exist, including when the path is longer than the data +- `ReflectionException` on failure of creating a ReflectionClass of the dependency +- `Psr\Container\NotFoundExceptionInterface` if a dependency does not exist in the service container +- `Psr\Container\ContainerExceptionInterface` if service manager is unable to provide an instance of a dependency + +## AnnotatedServiceAbstractFactory + +Creates every class annotated with `@Service`, without any service manager configuration. +See [Create services without a factory](factories/abstract.md). + +It never throws when asked whether it can create a class (for example through `$container->has()`): classes it cannot read, such as classes with unknown annotations, are reported as not creatable. diff --git a/docs/book/v6/factories/abstract.md b/docs/book/v6/factories/abstract.md new file mode 100644 index 0000000..399e669 --- /dev/null +++ b/docs/book/v6/factories/abstract.md @@ -0,0 +1,48 @@ +# Create services without a factory + +Using this approach, no service manager configuration is required. +The abstract factory registered by `Dot\AnnotatedServices\ConfigProvider` creates every class marked with the `@Service` annotation. + +## Prepare class + +Add the `@Service` annotation to the class docblock. +Configure the dependencies with `@Inject`, as described in [Inject class dependencies](service.md). + +```php + The annotation must be in a docblock, which starts with `/**`. +> A comment starting with `/*` is ignored, and the class is not created. + +And that's it, you don't need to configure the service manager with this class, creation will happen automatically. + +## Behavior + +- Only class names are handled. Interfaces, unknown names and classes without `@Service` are left to the other factories of the container. +- Classes that contain annotations unknown to the annotation reader are reported as not creatable, instead of throwing an exception from `$container->has()`. +- Because the abstract factory is asked about every name the container cannot otherwise resolve, [cache the annotations](../cache.md) in production. diff --git a/docs/book/v6/factories/repository.md b/docs/book/v6/factories/repository.md new file mode 100644 index 0000000..1d68a41 --- /dev/null +++ b/docs/book/v6/factories/repository.md @@ -0,0 +1,68 @@ +# Inject entity repositories + +## Prepare repository + +`dot-annotated-services` determines the entity a repository is related to by looking at the `@Entity` annotation, added to the repository class. + +```php + $this->getDependencies(), + ]; + } + + public function getDependencies(): array + { + return [ + 'factories' => [ + YourApp\Repository\ExampleRepository::class => Dot\AnnotatedServices\Factory\AnnotatedRepositoryFactory::class, + ], + ]; + } +} +``` diff --git a/docs/book/v6/factories/service.md b/docs/book/v6/factories/service.md new file mode 100644 index 0000000..b484d82 --- /dev/null +++ b/docs/book/v6/factories/service.md @@ -0,0 +1,122 @@ +# Inject class dependencies + +## Prepare class + +`dot-annotated-services` determines the dependencies by looking at the `@Inject` annotation, added to the constructor of a class. +Dependencies are specified as a list of service names or FQCNs. + +```php +helper = $helper; + } +``` + +The constructor is never called a second time, so classes with `readonly` promoted properties work. +Static methods are ignored. + +## Register class + +Open the ConfigProvider of the module where your class resides. + +Add a new entry under `factories`, where the key is your class FQCN and the value is `Dot\AnnotatedServices\Factory\AnnotatedServiceFactory::class`. + +See the below example for a better understanding of the file structure. + +```php + $this->getDependencies(), + ]; + } + + public function getDependencies(): array + { + return [ + 'factories' => [ + YourApp\Service\Example::class => Dot\AnnotatedServices\Factory\AnnotatedServiceFactory::class, + ], + ]; + } +} +``` + +> You can use only the fully qualified class name as the service key. diff --git a/docs/book/v6/installation.md b/docs/book/v6/installation.md new file mode 100644 index 0000000..abc9cac --- /dev/null +++ b/docs/book/v6/installation.md @@ -0,0 +1,15 @@ +# Installation + +`dot-annotated-services` requires PHP `8.3` or newer. + +Install `dotkernel/dot-annotated-services` by executing the following Composer command: + +```shell +composer require dotkernel/dot-annotated-services +``` + +To [cache the parsed annotations](cache.md), also install any PSR-6 cache implementation, for example: + +```shell +composer require symfony/cache +``` diff --git a/docs/book/v6/overview.md b/docs/book/v6/overview.md new file mode 100644 index 0000000..38d2f01 --- /dev/null +++ b/docs/book/v6/overview.md @@ -0,0 +1,39 @@ +# Overview + +`dot-annotated-services` is Dotkernel's dependency injection service using annotations. + +By providing reusable factories for service and repository injection, it reduces code complexity in projects. + +Version `6.0` continues the annotation-based API of version `4.x`. +Version `5.x` is a separate package line that uses PHP attributes instead of annotations. +See the [upgrade guide](upgrading.md) if you are coming from `4.x` or `5.x`. + +## Requirements + +- PHP `8.3`, `8.4`, `8.5` or `8.6` +- PSR-11 version `1` +- `doctrine/annotations` `^1.14.3` +- `doctrine/orm` `^2.20` + +## Version History + +| Branch | PSR-11 | Docblock Comments | OSS Lifecycle | PHP Version | +|--------|----------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| 6.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | +| 5.0 | 1 \|\| 2 | Attributes | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F5.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/5.2.0) | +| 4.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | +| 3.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | + +## Badges + +![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) + +[![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/issues) +[![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/network) +[![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/stargazers) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-annotated-services)](https://github.com/dotkernel/dot-annotated-services/blob/6.0/LICENSE.md) + +[![Build Static](https://github.com/dotkernel/dot-annotated-services/actions/workflows/continuous-integration.yml/badge.svg?branch=6.0)](https://github.com/dotkernel/dot-annotated-services/actions/workflows/continuous-integration.yml) +[![codecov](https://codecov.io/gh/dotkernel/dot-annotated-services/graph/badge.svg?token=ZBZDEA3LY8)](https://codecov.io/gh/dotkernel/dot-annotated-services) +[![PHPStan](https://github.com/dotkernel/dot-annotated-services/actions/workflows/static-analysis.yml/badge.svg?branch=6.0)](https://github.com/dotkernel/dot-annotated-services/actions/workflows/static-analysis.yml) diff --git a/docs/book/v6/upgrading.md b/docs/book/v6/upgrading.md new file mode 100644 index 0000000..4475937 --- /dev/null +++ b/docs/book/v6/upgrading.md @@ -0,0 +1,113 @@ +# Upgrading to 6.0 + +Version `6.0` continues the annotation-based API of `4.x`. +The `5.x` releases are a separate, attribute-based package line. +Follow the section that matches the version you are coming from. + +## From 4.x + +### Requirements + +- PHP `8.3` or newer is required. PHP `8.2` is no longer supported. +- `doctrine/cache` is no longer a requirement of this package. If your application uses its classes, require it yourself. +- `psr/cache` and `psr/container` are now declared as direct requirements. + +### Changes that can break your code + +| Change | Symptom | Fix | +| --- | --- | --- | +| The constructor is no longer called a second time when it carries `@Inject` | Code that relied on the constructor running twice no longer does | Remove that reliance. Classes with `readonly` promoted properties now work | +| `AnnotatedRepositoryFactory` throws if the entity manager returns a repository that is not an instance of the registered class | `Dot\AnnotatedServices\Exception\RuntimeException: Entity manager returned a repository of type ...` | Set `repositoryClass` on the entity to the registered repository, or register the class the entity manager returns | +| A dotted dependency whose path is longer than the data now throws | `InvalidArgumentException` where `config.a.b` used to inject the value of `config.a` | Fix the key path | +| `AnnotatedServiceAbstractFactory::canCreate()` returns `false` for classes it cannot read | `has()` returns `false` instead of throwing `AnnotationException` | None, unless your code caught that exception | +| `AnnotatedServiceFactory::__invoke()` and `createObject()` return `object` instead of `mixed` | Fatal error in subclasses that override them with an incompatible return type | Return an `object` from the override | +| `AbstractAnnotatedFactory::CACHE_SERVICE` is a typed constant (`string`) | Fatal error in subclasses that redeclare it untyped | Declare it as `public const string` | + +### Behavior that now works + +- `@Inject("config")` without curly braces is accepted. +- A dotted key whose value is `null` is injected as `null`. It used to throw. +- `has()` on the container no longer throws for classes with unknown annotations. + +### Documentation changes + +- The cache documentation of `4.x` described `doctrine/cache` drivers. The package has always required a PSR-6 cache pool. See [Cache](cache.md). +- `@Service` must be written in a docblock (`/**`), not in a `/*` comment. + +## From 5.x + +`5.x` uses PHP attributes. `6.0` uses annotations, and adds an abstract factory, setter injection and an annotation cache. +Migrating from `5.x` to `6.0` means moving back from attributes to annotations. + +### Classes + +| 5.x | 6.0 | +| --- | --- | +| `Dot\AnnotatedServices\Attribute\Inject` | `Dot\AnnotatedServices\Annotation\Inject` | +| `Dot\AnnotatedServices\Attribute\Entity` | `Dot\AnnotatedServices\Annotation\Entity` | +| `Dot\AnnotatedServices\Factory\AttributedServiceFactory` | `Dot\AnnotatedServices\Factory\AnnotatedServiceFactory` | +| `Dot\AnnotatedServices\Factory\AttributedRepositoryFactory` | `Dot\AnnotatedServices\Factory\AnnotatedRepositoryFactory` | + +### Syntax + +Before, on `5.x`: + +```php +use Dot\AnnotatedServices\Attribute\Entity; +use Dot\AnnotatedServices\Attribute\Inject; + +#[Entity(name: Example::class)] +class ExampleRepository extends EntityRepository +{ +} + +class ExampleService +{ + #[Inject(ExampleRepository::class, 'config')] + public function __construct( + protected ExampleRepository $repository, + protected array $config, + ) { + } +} +``` + +After, on `6.0`: + +```php +use Dot\AnnotatedServices\Annotation\Entity; +use Dot\AnnotatedServices\Annotation\Inject; + +/** + * @Entity(name="App\Entity\Example") + */ +class ExampleRepository extends EntityRepository +{ +} + +class ExampleService +{ + /** + * @Inject({ + * ExampleRepository::class, + * "config" + * }) + */ + public function __construct( + protected ExampleRepository $repository, + protected array $config, + ) { + } +} +``` + +### Registration + +Replace the factory class names in your `ConfigProvider`, and add `Dot\AnnotatedServices\ConfigProvider::class` to your configuration aggregator if you want to use `@Service`. + +### Differences to be aware of + +- `6.0` supports PSR-11 version `1` only. `5.x` also supported version `2`. +- `6.0` has no check for services that inject themselves. A service listed in its own `@Inject` is not reported as a recursion. +- `6.0` additionally supports `@Service` (see [Create services without a factory](factories/abstract.md)), `@Inject` on setters, and caching the parsed annotations (see [Cache](cache.md)). +- `6.0` requires `doctrine/annotations` and a PSR-6 pool if you want to cache annotations. diff --git a/docs/book/v6/usage.md b/docs/book/v6/usage.md new file mode 100644 index 0000000..94d0d18 --- /dev/null +++ b/docs/book/v6/usage.md @@ -0,0 +1,16 @@ +# Usage + +Version `6.x` of `dot-annotated-services` uses [doctrine/annotations](https://github.com/doctrine/annotations) to parse and inject dependencies. +It continues the annotation-based API of version `4.x`. +Version `5.x` is a separate package line that uses PHP attributes instead. + +> Annotations must be written in a docblock, which starts with `/**`. +> A comment starting with `/*` is ignored. + +You can use it to: + +- [Inject class dependencies](factories/service.md) +- [Inject entity repositories](factories/repository.md) +- [Create services without writing a factory](factories/abstract.md) + +Coming from `4.x` or `5.x`? Read the [upgrade guide](upgrading.md). diff --git a/mkdocs.yml b/mkdocs.yml index ae5f3d8..2bb8687 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,11 +2,24 @@ docs_dir: docs/book site_dir: docs/html extra: project: Packages - current_version: v4 + current_version: v6 versions: + - v6 - v4 nav: - Home: index.md + - v6: + - Overview: v6/overview.md + - Installation: v6/installation.md + - Configuration: v6/configuration.md + - Usage: v6/usage.md + - Factories: v6/factories.md + - Cache: v6/cache.md + - Upgrading: v6/upgrading.md + - Reference: + - "Inject class dependencies": v6/factories/service.md + - "Inject entity repositories": v6/factories/repository.md + - "Create services without a factory": v6/factories/abstract.md - v4: - Overview: v4/overview.md - Installation: v4/installation.md diff --git a/phpstan.neon b/phpstan.neon index 349be25..08d6acd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon parameters: - level: 5 + level: 8 paths: - src - test diff --git a/phpunit.xml b/phpunit.xml index 65d03b8..b2a6f47 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,14 +1,13 @@ - - - - ./test - - - - - - ./src - - + + + + ./test + + + + + ./src + + diff --git a/src/Annotation/Entity.php b/src/Annotation/Entity.php index c8c84f9..9a93856 100644 --- a/src/Annotation/Entity.php +++ b/src/Annotation/Entity.php @@ -16,14 +16,23 @@ */ class Entity { - /** @Required */ + /** + * @Required + * @var class-string + */ private string $name; + /** + * @param class-string $name + */ public function __construct(string $name) { $this->name = $name; } + /** + * @return class-string + */ public function getName(): string { return $this->name; diff --git a/src/Annotation/Inject.php b/src/Annotation/Inject.php index cb13367..cee96fd 100644 --- a/src/Annotation/Inject.php +++ b/src/Annotation/Inject.php @@ -7,19 +7,28 @@ use Doctrine\Common\Annotations\Annotation; use Doctrine\Common\Annotations\Annotation\Target; +use function array_values; + /** * @Annotation * @Target({"METHOD"}) */ class Inject { + /** @var list */ private array $services = []; + /** + * @param array{value?: string|list} $values + */ public function __construct(array $values) { - $this->services = $values['value'] ?? []; + $this->services = array_values((array) ($values['value'] ?? [])); } + /** + * @return list + */ public function getServices(): array { return $this->services; diff --git a/src/Annotation/Service.php b/src/Annotation/Service.php index b378408..cb89427 100644 --- a/src/Annotation/Service.php +++ b/src/Annotation/Service.php @@ -5,12 +5,13 @@ namespace Dot\AnnotatedServices\Annotation; use Doctrine\Common\Annotations\Annotation; +use Doctrine\Common\Annotations\Annotation\Target; /** * Annotates a class as a service which can be created and injected by this library * * @Annotation - * @Annotation\Target({"CLASS"}) + * @Target({"CLASS"}) */ class Service { diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 51284a4..415cee1 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -8,6 +8,9 @@ class ConfigProvider { + /** + * @return array{dependencies: array{abstract_factories: list}} + */ public function __invoke(): array { return [ @@ -15,6 +18,9 @@ public function __invoke(): array ]; } + /** + * @return array{abstract_factories: list} + */ public function getDependenciesConfig(): array { return [ diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 0571bb7..700c73b 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -4,9 +4,6 @@ namespace Dot\AnnotatedServices\Exception; -/** - * Interface ExceptionInterface - */ interface ExceptionInterface { } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 72ea6c2..fccf439 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -31,6 +31,17 @@ public static function annotationNotFound(string $annotation, string $class, str )); } + public static function repositoryMismatch(string $requestedName, string $actualClass): self + { + return new self(sprintf( + 'Entity manager returned a repository of type "%s", but "%s" was requested. ' + . 'Set "%s" as the repositoryClass of the entity.', + $actualClass, + $requestedName, + $requestedName + )); + } + public static function invalidAnnotation(string $requestedName): self { return new self(sprintf( diff --git a/src/Factory/AbstractAnnotatedFactory.php b/src/Factory/AbstractAnnotatedFactory.php index bc4d523..b00ae5b 100644 --- a/src/Factory/AbstractAnnotatedFactory.php +++ b/src/Factory/AbstractAnnotatedFactory.php @@ -14,8 +14,10 @@ abstract class AbstractAnnotatedFactory { - public const CACHE_SERVICE = CacheItemPoolInterface::class; + public const string CACHE_SERVICE = CacheItemPoolInterface::class; + protected ?Reader $annotationReader = null; + public function setAnnotationReader(Reader $annotationReader): void { $this->annotationReader = $annotationReader; @@ -33,17 +35,16 @@ protected function createAnnotationReader(ContainerInterface $container): Reader if (! $container->has(self::CACHE_SERVICE)) { return $this->annotationReader = new AnnotationReader(); - } else { - /** @var CacheItemPoolInterface $cache */ - $cache = $container->get(self::CACHE_SERVICE); - $debug = false; - if ($container->has('config')) { - $config = $container->get('config'); - if (isset($config['debug'])) { - $debug = (bool) $config['debug']; - } - } - return $this->annotationReader = new PsrCachedReader(new AnnotationReader(), $cache, $debug); } + + /** @var CacheItemPoolInterface $cache */ + $cache = $container->get(self::CACHE_SERVICE); + $debug = false; + if ($container->has('config')) { + $config = $container->get('config'); + $debug = isset($config['debug']) && (bool) $config['debug']; + } + + return $this->annotationReader = new PsrCachedReader(new AnnotationReader(), $cache, $debug); } } diff --git a/src/Factory/AnnotatedRepositoryFactory.php b/src/Factory/AnnotatedRepositoryFactory.php index 45fec3e..90b801b 100644 --- a/src/Factory/AnnotatedRepositoryFactory.php +++ b/src/Factory/AnnotatedRepositoryFactory.php @@ -19,6 +19,7 @@ class AnnotatedRepositoryFactory extends AbstractAnnotatedFactory { /** + * @return EntityRepository * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException @@ -30,9 +31,11 @@ public function __invoke(ContainerInterface $container, string $requestedName): } /** + * @return EntityRepository * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException + * @throws RuntimeException */ public function createObject(ContainerInterface $container, string $requestedName): EntityRepository { @@ -45,13 +48,19 @@ public function createObject(ContainerInterface $container, string $requestedNam throw RuntimeException::doesNotExtend(EntityRepository::class); } - $annotationReader = $this->createAnnotationReader($container); - $entity = $annotationReader->getClassAnnotation($reflectionClass, Entity::class); - if (! $entity) { + $entity = $this->createAnnotationReader($container)->getClassAnnotation($reflectionClass, Entity::class); + if (! $entity instanceof Entity) { throw RuntimeException::annotationNotFound(Entity::class, $requestedName, static::class); } + /** @var EntityManagerInterface $entityManager */ $entityManager = $container->get(EntityManagerInterface::class); - return $entityManager->getRepository($entity->getName()); + $repository = $entityManager->getRepository($entity->getName()); + + if (! $repository instanceof $requestedName) { + throw RuntimeException::repositoryMismatch($requestedName, $repository::class); + } + + return $repository; } } diff --git a/src/Factory/AnnotatedServiceAbstractFactory.php b/src/Factory/AnnotatedServiceAbstractFactory.php index c53b97b..127d444 100644 --- a/src/Factory/AnnotatedServiceAbstractFactory.php +++ b/src/Factory/AnnotatedServiceAbstractFactory.php @@ -4,47 +4,50 @@ namespace Dot\AnnotatedServices\Factory; +use Doctrine\Common\Annotations\AnnotationException; use Dot\AnnotatedServices\Annotation\Service; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Override; use Psr\Container\ContainerInterface; use ReflectionClass; use function class_exists; +use function is_string; class AnnotatedServiceAbstractFactory extends AbstractAnnotatedFactory implements AbstractFactoryInterface { + private ?AnnotatedServiceFactory $serviceFactory = null; + /** * @param string $requestedName */ + #[Override] public function canCreate(ContainerInterface $container, $requestedName): bool { - if ($requestedName === null) { + if (! is_string($requestedName) || ! class_exists($requestedName)) { return false; } - if (! class_exists($requestedName)) { + try { + return $this->createAnnotationReader($container) + ->getClassAnnotation(new ReflectionClass($requestedName), Service::class) !== null; + } catch (AnnotationException) { return false; } - - $annotationReader = $this->createAnnotationReader($container); - $refClass = new ReflectionClass($requestedName); - - $service = $annotationReader->getClassAnnotation($refClass, Service::class); - if ($service === null) { - return false; - } - - return true; } /** * @param string $requestedName + * @param array|null $options */ + #[Override] public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): object { - $factory = new AnnotatedServiceFactory(); - $factory->setAnnotationReader($this->createAnnotationReader($container)); + if ($this->serviceFactory === null) { + $this->serviceFactory = new AnnotatedServiceFactory(); + $this->serviceFactory->setAnnotationReader($this->createAnnotationReader($container)); + } - return $factory->createObject($container, $requestedName); + return $this->serviceFactory->createObject($container, $requestedName); } } diff --git a/src/Factory/AnnotatedServiceFactory.php b/src/Factory/AnnotatedServiceFactory.php index a75c2ea..bf1ba08 100644 --- a/src/Factory/AnnotatedServiceFactory.php +++ b/src/Factory/AnnotatedServiceFactory.php @@ -15,6 +15,7 @@ use ReflectionException; use ReflectionMethod; +use function array_key_exists; use function array_shift; use function class_exists; use function count; @@ -28,8 +29,9 @@ class AnnotatedServiceFactory extends AbstractAnnotatedFactory * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException + * @throws RuntimeException */ - public function __invoke(ContainerInterface $container, string $requestedName): mixed + public function __invoke(ContainerInterface $container, string $requestedName): object { return $this->createObject($container, $requestedName); } @@ -38,15 +40,14 @@ public function __invoke(ContainerInterface $container, string $requestedName): * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException + * @throws RuntimeException */ - public function createObject(ContainerInterface $container, string $requestedName): mixed + public function createObject(ContainerInterface $container, string $requestedName): object { if (! class_exists($requestedName)) { throw RuntimeException::classNotFound($requestedName); } - $service = null; - $annotationReader = $this->createAnnotationReader($container); $refClass = $this->getReflectionClass($requestedName); $constructor = $refClass->getConstructor(); @@ -55,7 +56,7 @@ public function createObject(ContainerInterface $container, string $requestedNam $service = new $requestedName(); } else { $inject = $annotationReader->getMethodAnnotation($constructor, Inject::class); - if ($inject === null && $constructor->getNumberOfRequiredParameters() > 0) { + if (! $inject instanceof Inject && $constructor->getNumberOfRequiredParameters() > 0) { throw RuntimeException::annotationNotFound( Inject::class, $requestedName, @@ -63,27 +64,30 @@ public function createObject(ContainerInterface $container, string $requestedNam ); } - $services = []; - if ($inject) { - $services = $this->getServicesToInject($container, $inject); - } + $services = $inject instanceof Inject ? $this->getServicesToInject($container, $inject) : []; $service = new $requestedName(...$services); } - $methods = $refClass->getMethods(ReflectionMethod::IS_PUBLIC); - foreach ($methods as $method) { + foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + // The constructor was already called above with its injected services + if ($method->isConstructor() || $method->isStatic()) { + continue; + } + $inject = $annotationReader->getMethodAnnotation($method, Inject::class); - if ($inject) { - $services = $this->getServicesToInject($container, $inject); - $method->invoke($service, ...$services); + if (! $inject instanceof Inject) { + continue; } + + $method->invoke($service, ...$this->getServicesToInject($container, $inject)); } return $service; } /** + * @return list * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ @@ -108,30 +112,47 @@ protected function getServicesToInject(ContainerInterface $container, Inject $in throw RuntimeException::classNotFound($serviceKey); } - $services[] = empty($parts) ? $service : $this->readKeysFromArray($parts, $service); + $services[] = $parts === [] ? $service : $this->readKeysFromArray($parts, $service); } return $services; } + /** + * @param non-empty-list $keys + * @throws InvalidArgumentException + */ protected function readKeysFromArray(array $keys, mixed $array): mixed { $key = array_shift($keys); - // When one of the provided keys is not found, throw an exception - if (! isset($array[$key])) { + + if (is_array($array)) { + $exists = array_key_exists($key, $array); + } elseif ($array instanceof ArrayAccess) { + $exists = $array->offsetExists($key); + } else { + $exists = false; + } + + // Also thrown when the path is longer than the data, e.g. "config.a.b" where "a" is a scalar + if (! $exists) { throw new InvalidArgumentException(sprintf( 'The key "%s" provided in the dotted notation could not be found in the array service', $key )); } + $value = $array[$key]; - if (! empty($keys) && (is_array($value) || $value instanceof ArrayAccess)) { - $value = $this->readKeysFromArray($keys, $value); + if ($keys === []) { + return $value; } - return $value; + + return $this->readKeysFromArray($keys, $value); } /** + * @param class-string $requestedName + * @return ReflectionClass * @throws ReflectionException */ protected function getReflectionClass(string $requestedName): ReflectionClass diff --git a/test/AbstractAnnotatedFactoryTest.php b/test/AbstractAnnotatedFactoryTest.php new file mode 100644 index 0000000..0346d73 --- /dev/null +++ b/test/AbstractAnnotatedFactoryTest.php @@ -0,0 +1,73 @@ +subject = new ExposedAnnotatedFactory(); + } + + /** + * @param array $services + */ + private function container(array $services = []): ServiceManager + { + return new ServiceManager(['services' => $services]); + } + + public function testUsesPlainReaderWhenNoCacheIsRegistered(): void + { + $this->assertInstanceOf(AnnotationReader::class, $this->subject->reader($this->container())); + } + + public function testUsesCachedReaderWhenACachePoolIsRegistered(): void + { + $container = $this->container([ + AbstractAnnotatedFactory::CACHE_SERVICE => $this->createStub(CacheItemPoolInterface::class), + ]); + + $this->assertInstanceOf(PsrCachedReader::class, $this->subject->reader($container)); + } + + public function testUsesCachedReaderWhenCacheAndConfigAreRegistered(): void + { + $container = $this->container([ + AbstractAnnotatedFactory::CACHE_SERVICE => $this->createStub(CacheItemPoolInterface::class), + 'config' => ['debug' => true], + ]); + + $this->assertInstanceOf(PsrCachedReader::class, $this->subject->reader($container)); + } + + public function testReusesTheReaderOnceCreated(): void + { + $container = $this->container(); + + $this->assertSame($this->subject->reader($container), $this->subject->reader($container)); + } + + public function testExplicitlySetReaderTakesPrecedence(): void + { + $reader = $this->createStub(Reader::class); + $this->subject->setAnnotationReader($reader); + + $this->assertSame($reader, $this->subject->reader($this->container())); + } +} diff --git a/test/AnnotatedRepositoryFactoryTest.php b/test/AnnotatedRepositoryFactoryTest.php index 0b498c1..a55a191 100644 --- a/test/AnnotatedRepositoryFactoryTest.php +++ b/test/AnnotatedRepositoryFactoryTest.php @@ -4,118 +4,90 @@ namespace DotTest\AnnotatedServices; -use Doctrine\Common\Annotations\Reader; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Dot\AnnotatedServices\Annotation\Entity; use Dot\AnnotatedServices\Exception\RuntimeException; -use Dot\AnnotatedServices\Factory\AnnotatedRepositoryFactory as Subject; -//use DotTest\AnnotatedServices\TestClass; -use PHPUnit\Framework\MockObject\Exception; -use PHPUnit\Framework\MockObject\MockObject; +use Dot\AnnotatedServices\Factory\AnnotatedRepositoryFactory; +use DotTest\AnnotatedServices\Fixture\ExampleEntity; +use DotTest\AnnotatedServices\Fixture\ExampleRepository; +use DotTest\AnnotatedServices\Fixture\PlainClass; +use DotTest\AnnotatedServices\Fixture\RepositoryWithoutAnnotation; +use Laminas\ServiceManager\ServiceManager; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\ContainerInterface; -use Psr\Container\NotFoundExceptionInterface; -use ReflectionException; - -use function get_class; +#[CoversClass(AnnotatedRepositoryFactory::class)] class AnnotatedRepositoryFactoryTest extends TestCase { - private MockObject&ContainerInterface $container; - private MockObject&Subject $subject; - private MockObject&Reader $annotationReader; - - /** - * @throws Exception - */ - public function setUp(): void + private AnnotatedRepositoryFactory $subject; + + protected function setUp(): void { - $this->container = $this->createMock(ContainerInterface::class); - $this->annotationReader = $this->createMock(Reader::class); - $this->subject = $this->createPartialMock(Subject::class, ['createAnnotationReader']); + $this->subject = new AnnotatedRepositoryFactory(); } - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - * @throws ReflectionException - */ - public function testThrowsExceptionClassNotFound() + private function container(EntityManagerInterface $entityManager): ServiceManager { - $requestedName = 'TestRepository'; - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage(RuntimeException::classNotFound($requestedName)->getMessage()); - - $this->subject->__invoke($this->container, $requestedName); + return new ServiceManager(['services' => [EntityManagerInterface::class => $entityManager]]); } - /** - * @throws ContainerExceptionInterface - * @throws ReflectionException - * @throws NotFoundExceptionInterface - */ - public function testThrowsExceptionClassNotExtendsEntityRepository() + public function testThrowsExceptionClassNotFound(): void { - $requestedName = TestClass::class; + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(RuntimeException::classNotFound('TestRepository')->getMessage()); - $this->getMockBuilder($requestedName)->getMock(); + ($this->subject)($this->container($this->createStub(EntityManagerInterface::class)), 'TestRepository'); + } + public function testThrowsExceptionClassDoesNotExtendEntityRepository(): void + { $this->expectException(RuntimeException::class); $this->expectExceptionMessage(RuntimeException::doesNotExtend(EntityRepository::class)->getMessage()); - $this->subject->__invoke($this->container, $requestedName); + + ($this->subject)($this->container($this->createStub(EntityManagerInterface::class)), PlainClass::class); } - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - * @throws Exception - * @throws ReflectionException - */ - public function testCreateObjectThrowsExceptionAnnotationNotFound() + public function testThrowsExceptionWhenEntityAnnotationIsMissing(): void { - $repository = $this->createMock(EntityRepository::class); - $this->annotationReader->method('getClassAnnotation')->willReturn(null); - - $this->subject->method('createAnnotationReader')->willReturn($this->annotationReader); - $this->expectException(RuntimeException::class); $this->expectExceptionMessage(RuntimeException::annotationNotFound( Entity::class, - $repository::class, - get_class($this->subject) + RepositoryWithoutAnnotation::class, + AnnotatedRepositoryFactory::class )->getMessage()); - $this->subject->__invoke($this->container, $repository::class); + ($this->subject)( + $this->container($this->createStub(EntityManagerInterface::class)), + RepositoryWithoutAnnotation::class + ); } - /** - * @throws ContainerExceptionInterface - * @throws Exception - * @throws NotFoundExceptionInterface - * @throws ReflectionException - */ - public function testCreateObjectReturnsEntityRepository() + public function testReturnsRepositoryFromEntityManager(): void { - $repository = $this->createMock(EntityRepository::class); - $annotation = new Entity('test'); + $repository = $this->createStub(ExampleRepository::class); $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('getRepository') + ->with(ExampleEntity::class) + ->willReturn($repository); - $entityManager->method('getRepository')->willReturn($repository); - - $this->annotationReader->method('getClassAnnotation')->willReturn($annotation); + $result = ($this->subject)($this->container($entityManager), ExampleRepository::class); - $this->container->method('get') - ->with(EntityManagerInterface::class) - ->willReturn($entityManager); + $this->assertSame($repository, $result); + } - $this->subject - ->method('createAnnotationReader') - ->willReturn($this->annotationReader); + public function testThrowsExceptionWhenEntityManagerReturnsADifferentRepositoryClass(): void + { + $repository = $this->createStub(EntityRepository::class); + $entityManager = $this->createStub(EntityManagerInterface::class); + $entityManager->method('getRepository')->willReturn($repository); - $object = $this->subject->__invoke($this->container, $repository::class); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + RuntimeException::repositoryMismatch(ExampleRepository::class, $repository::class)->getMessage() + ); - $this->assertContainsOnlyInstancesOf(EntityRepository::class, [$object]); + ($this->subject)($this->container($entityManager), ExampleRepository::class); } } diff --git a/test/AnnotatedServiceAbstractFactoryTest.php b/test/AnnotatedServiceAbstractFactoryTest.php new file mode 100644 index 0000000..96c0703 --- /dev/null +++ b/test/AnnotatedServiceAbstractFactoryTest.php @@ -0,0 +1,93 @@ +subject = new AnnotatedServiceAbstractFactory(); + } + + /** + * @param array $services + */ + private function container(array $services = []): ServiceManager + { + $config = (new ConfigProvider())()['dependencies']; + $config['services'] = $services; + $config['abstract_factories'] = [AnnotatedServiceAbstractFactory::class]; + + return new ServiceManager($config); + } + + /** + * @return array + */ + public static function notCreatableProvider(): array + { + return [ + 'class without @Service' => [PlainClass::class], + 'interface' => [ServiceInterface::class], + 'unknown name' => ['This\Class\Does\Not\Exist'], + '/* */ comment is not a docblock' => [CommentOnlyService::class], + 'docblock with unknown annotation' => [UnknownAnnotationClass::class], + ]; + } + + public function testCanCreateClassAnnotatedWithService(): void + { + $this->assertTrue($this->subject->canCreate($this->container(), AnnotatedService::class)); + } + + #[DataProvider('notCreatableProvider')] + public function testCannotCreateOtherNames(string $requestedName): void + { + $this->assertFalse($this->subject->canCreate($this->container(), $requestedName)); + } + + #[DataProvider('notCreatableProvider')] + public function testContainerReportsOtherNamesAsMissingWithoutThrowing(string $requestedName): void + { + $this->assertFalse($this->container()->has($requestedName)); + } + + public function testCreatesAnnotatedServiceThroughTheContainer(): void + { + $config = ['debug' => true]; + $container = $this->container(['config' => $config]); + + $this->assertTrue($container->has(AnnotatedService::class)); + + $service = $container->get(AnnotatedService::class); + + $this->assertInstanceOf(AnnotatedService::class, $service); + $this->assertSame($config, $service->config); + } + + public function testInvokeBuildsService(): void + { + $config = ['debug' => false]; + $service = ($this->subject)($this->container(['config' => $config]), AnnotatedService::class); + + $this->assertInstanceOf(AnnotatedService::class, $service); + $this->assertSame($config, $service->config); + } +} diff --git a/test/AnnotatedServiceFactoryTest.php b/test/AnnotatedServiceFactoryTest.php index 8f56ec4..9c3e2b9 100644 --- a/test/AnnotatedServiceFactoryTest.php +++ b/test/AnnotatedServiceFactoryTest.php @@ -4,139 +4,200 @@ namespace DotTest\AnnotatedServices; -use Doctrine\Common\Annotations\Reader; use Dot\AnnotatedServices\Annotation\Inject; +use Dot\AnnotatedServices\Exception\ExceptionInterface; +use Dot\AnnotatedServices\Exception\InvalidArgumentException; use Dot\AnnotatedServices\Exception\RuntimeException; -use Dot\AnnotatedServices\Factory\AnnotatedServiceFactory as Subject; -use PHPUnit\Framework\MockObject\Exception; -use PHPUnit\Framework\MockObject\MockObject; +use Dot\AnnotatedServices\Factory\AnnotatedServiceFactory; +use DotTest\AnnotatedServices\Fixture\ClassStringService; +use DotTest\AnnotatedServices\Fixture\ConfigService; +use DotTest\AnnotatedServices\Fixture\Dependency; +use DotTest\AnnotatedServices\Fixture\DottedConfigService; +use DotTest\AnnotatedServices\Fixture\MissingServiceService; +use DotTest\AnnotatedServices\Fixture\NoConstructorService; +use DotTest\AnnotatedServices\Fixture\NullKeyService; +use DotTest\AnnotatedServices\Fixture\OptionalParamsService; +use DotTest\AnnotatedServices\Fixture\RequiredParamsWithoutInject; +use DotTest\AnnotatedServices\Fixture\SetterInjectedService; +use DotTest\AnnotatedServices\Fixture\UnbracedInjectService; +use Laminas\ServiceManager\ServiceManager; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\ContainerInterface; -use Psr\Container\NotFoundExceptionInterface; -use ReflectionClass; -use ReflectionException; -use ReflectionMethod; - -use function get_class; +#[CoversClass(AnnotatedServiceFactory::class)] class AnnotatedServiceFactoryTest extends TestCase { - private MockObject&ContainerInterface $container; - private MockObject&Subject $subject; - private MockObject&Reader $annotationReader; + private AnnotatedServiceFactory $subject; - /** - * @throws Exception - */ - public function setUp(): void + protected function setUp(): void { - $this->container = $this->createMock(ContainerInterface::class); - $this->annotationReader = $this->createMock(Reader::class); - $this->subject = $this->createPartialMock(Subject::class, [ - 'createAnnotationReader', - 'getReflectionClass', - ]); + $this->subject = new AnnotatedServiceFactory(); + SetterInjectedService::$constructed = 0; + SetterInjectedService::$staticCalls = 0; } /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - * @throws ReflectionException + * @param array $services */ - public function testThrowsExceptionClassNotFound() + private function container(array $services = []): ServiceManager { - $requestedName = 'TestService'; + return new ServiceManager(['services' => $services]); + } + public function testThrowsExceptionClassNotFound(): void + { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage(RuntimeException::classNotFound($requestedName)->getMessage()); + $this->expectExceptionMessage(RuntimeException::classNotFound('TestService')->getMessage()); - $this->subject->__invoke($this->container, $requestedName); + ($this->subject)($this->container(), 'TestService'); } - /** - * @throws ContainerExceptionInterface - * @throws Exception - * @throws NotFoundExceptionInterface - * @throws ReflectionException - */ - public function testReturnServiceWithNoDependencies() + public function testCreatesServiceWithoutConstructor(): void { - $requestedName = TestClass::class; - $this->getMockBuilder($requestedName)->allowMockingUnknownTypes()->getMock(); - $refClass = $this->createMock(ReflectionClass::class); + $service = ($this->subject)($this->container(), NoConstructorService::class); - $refClass->method('getConstructor')->willReturn(null); - $refClass->method('getMethods')->willReturn([]); + $this->assertInstanceOf(NoConstructorService::class, $service); + } - $this->annotationReader->method('getMethodAnnotation')->willReturn(null); - $this->subject - ->method('createAnnotationReader') - ->willReturn($this->annotationReader); - $this->subject->method('getReflectionClass')->willReturn($refClass); + public function testThrowsExceptionWhenInjectIsMissingAndConstructorRequiresParameters(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(RuntimeException::annotationNotFound( + Inject::class, + RequiredParamsWithoutInject::class, + AnnotatedServiceFactory::class + )->getMessage()); + + ($this->subject)($this->container(), RequiredParamsWithoutInject::class); + } - $object = $this->subject->__invoke($this->container, $requestedName); + public function testCreatesServiceWithoutInjectWhenAllParametersAreOptional(): void + { + $service = ($this->subject)($this->container(), OptionalParamsService::class); - $this->assertSame($requestedName, $object::class); + $this->assertInstanceOf(OptionalParamsService::class, $service); + $this->assertSame('default', $service->name); } - /** - * @throws ContainerExceptionInterface - * @throws Exception - * @throws NotFoundExceptionInterface - * @throws ReflectionException - */ - public function testThrowsExceptionAnnotationNotFound() + public function testInjectsServiceFromContainer(): void { - $requestedName = TestClass::class; - $this->getMockBuilder($requestedName)->allowMockingUnknownTypes()->getMock(); - $refClass = $this->createMock(ReflectionClass::class); - $refConstructor = $this->createMock(ReflectionMethod::class); + $config = ['debug' => true]; + $service = ($this->subject)($this->container(['config' => $config]), ConfigService::class); - $refClass->method('getConstructor')->willReturn($refConstructor); - $refConstructor->method('getNumberOfRequiredParameters')->willReturn(100); + $this->assertInstanceOf(ConfigService::class, $service); + $this->assertSame($config, $service->config); + } - $this->annotationReader->method('getMethodAnnotation')->willReturn(null); - $this->subject - ->method('createAnnotationReader') - ->willReturn($this->annotationReader); - $this->subject->method('getReflectionClass')->willReturn($refClass); + public function testInjectsServiceFromUnbracedAnnotation(): void + { + $config = ['debug' => true]; + $service = ($this->subject)($this->container(['config' => $config]), UnbracedInjectService::class); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage(RuntimeException::annotationNotFound( - Inject::class, - $requestedName, - get_class($this->subject), - )->getMessage()); + $this->assertInstanceOf(UnbracedInjectService::class, $service); + $this->assertSame($config, $service->config); + } + + public function testInstantiatesClassStringWhenNotInContainer(): void + { + $service = ($this->subject)($this->container(), ClassStringService::class); - $this->subject->__invoke($this->container, $requestedName); + $this->assertInstanceOf(ClassStringService::class, $service); } - /** - * @throws ContainerExceptionInterface - * @throws Exception - * @throws NotFoundExceptionInterface - * @throws ReflectionException - */ - public function testReturnService() + public function testPrefersContainerServiceOverClassInstantiation(): void { - $requestedName = TestClass::class; - $this->getMockBuilder($requestedName)->allowMockingUnknownTypes()->getMock(); - $refClass = $this->createMock(ReflectionClass::class); - $refConstructor = $this->createMock(ReflectionMethod::class); + $dependency = new Dependency(); + $service = ($this->subject)( + $this->container([Dependency::class => $dependency]), + ClassStringService::class + ); + + $this->assertInstanceOf(ClassStringService::class, $service); + $this->assertSame($dependency, $service->dependency); + } - $refClass->method('getConstructor')->willReturn($refConstructor); - $refClass->method('getMethods')->willReturn([]); - $refConstructor->method('getNumberOfRequiredParameters')->willReturn(1); + public function testInjectsNestedConfigValueUsingDotNotation(): void + { + $service = ($this->subject)( + $this->container(['config' => ['a' => ['b' => 'value']]]), + DottedConfigService::class + ); + + $this->assertInstanceOf(DottedConfigService::class, $service); + $this->assertSame('value', $service->value); + } + + public function testInjectsNullValueUsingDotNotation(): void + { + $service = ($this->subject)($this->container(['config' => ['k' => null]]), NullKeyService::class); - $inject = new Inject(['test']); - $this->annotationReader->method('getMethodAnnotation')->willReturn($inject); + $this->assertInstanceOf(NullKeyService::class, $service); + $this->assertNull($service->value); + } - $this->subject->method('createAnnotationReader')->willReturn($this->annotationReader); - $this->subject->method('getReflectionClass')->willReturn($refClass); + public function testServiceRegisteredWithDottedNameWinsOverConfigPath(): void + { + $service = ($this->subject)( + $this->container([ + 'config.a.b' => 'from-service', + 'config' => ['a' => ['b' => 'from-config']], + ]), + DottedConfigService::class + ); + + $this->assertInstanceOf(DottedConfigService::class, $service); + $this->assertSame('from-service', $service->value); + } - $service = $this->subject->__invoke($this->container, $requestedName); + public function testThrowsExceptionWhenDottedKeyIsMissing(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The key "b" provided in the dotted notation could not be found'); + + ($this->subject)($this->container(['config' => ['a' => []]]), DottedConfigService::class); + } + + public function testThrowsExceptionWhenDottedPathIsLongerThanTheData(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The key "b" provided in the dotted notation could not be found'); + + ($this->subject)($this->container(['config' => ['a' => 'scalar']]), DottedConfigService::class); + } - $this->assertSame($requestedName, $service::class); + public function testThrowsExceptionWhenInjectedServiceDoesNotExist(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(RuntimeException::classNotFound('missing_service')->getMessage()); + + ($this->subject)($this->container(), MissingServiceService::class); + } + + public function testCallsConstructorOnlyOnceAndInjectsSetters(): void + { + $config = ['debug' => true]; + $service = ($this->subject)($this->container(['config' => $config]), SetterInjectedService::class); + + $this->assertInstanceOf(SetterInjectedService::class, $service); + $this->assertSame(1, SetterInjectedService::$constructed); + $this->assertSame($config, $service->config); + $this->assertInstanceOf(Dependency::class, $service->dependency); + } + + public function testDoesNotCallAnnotatedStaticMethods(): void + { + ($this->subject)($this->container(['config' => []]), SetterInjectedService::class); + + $this->assertSame(0, SetterInjectedService::$staticCalls); + } + + public function testThrownExceptionsImplementTheExceptionInterface(): void + { + try { + ($this->subject)($this->container(), 'TestService'); + $this->fail('Exception expected'); + } catch (ExceptionInterface $exception) { + $this->assertInstanceOf(RuntimeException::class, $exception); + } } } diff --git a/test/AnnotationTest.php b/test/AnnotationTest.php new file mode 100644 index 0000000..a3b5e9d --- /dev/null +++ b/test/AnnotationTest.php @@ -0,0 +1,66 @@ + ['a', 'b']]); + + $this->assertSame(['a', 'b'], $inject->getServices()); + } + + public function testInjectAcceptsASingleUnbracedService(): void + { + $inject = new Inject(['value' => 'config']); + + $this->assertSame(['config'], $inject->getServices()); + } + + public function testInjectWithoutValueHasNoServices(): void + { + $this->assertSame([], (new Inject([]))->getServices()); + } + + public function testEntityExposesItsName(): void + { + $this->assertSame(self::class, (new Entity(self::class))->getName()); + } + + public function testExceptionsImplementTheExceptionInterface(): void + { + $this->assertContains(ExceptionInterface::class, class_implements(RuntimeException::class)); + $this->assertContains(ExceptionInterface::class, class_implements(InvalidArgumentException::class)); + } + + public function testRepositoryMismatchMessageNamesBothClasses(): void + { + $message = RuntimeException::repositoryMismatch('App\Repo', EntityRepository::class)->getMessage(); + + $this->assertStringContainsString('App\Repo', $message); + $this->assertStringContainsString(EntityRepository::class, $message); + } + + public function testInvalidAnnotationMessageNamesTheService(): void + { + $this->assertStringContainsString('"Foo"', RuntimeException::invalidAnnotation('Foo')->getMessage()); + } +} diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index 536a1c8..3ec3947 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -6,13 +6,16 @@ use Dot\AnnotatedServices\ConfigProvider; use Dot\AnnotatedServices\Factory\AnnotatedServiceAbstractFactory; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +#[CoversClass(ConfigProvider::class)] class ConfigProviderTest extends TestCase { + /** @var array */ protected array $config; - protected function setup(): void + protected function setUp(): void { $this->config = (new ConfigProvider())(); } @@ -24,6 +27,7 @@ public function testHasDependencies(): void public function testDependenciesHasFactories(): void { + $this->assertIsArray($this->config['dependencies']); $this->assertArrayHasKey('abstract_factories', $this->config['dependencies']); $this->assertContainsEquals( AnnotatedServiceAbstractFactory::class, diff --git a/test/Fixture/AnnotatedService.php b/test/Fixture/AnnotatedService.php new file mode 100644 index 0000000..35344d2 --- /dev/null +++ b/test/Fixture/AnnotatedService.php @@ -0,0 +1,22 @@ + $config + */ + public function __construct(public readonly array $config) + { + } +} diff --git a/test/Fixture/ClassStringService.php b/test/Fixture/ClassStringService.php new file mode 100644 index 0000000..88e8ffe --- /dev/null +++ b/test/Fixture/ClassStringService.php @@ -0,0 +1,17 @@ + $config + */ + public function __construct(public readonly array $config) + { + } +} diff --git a/test/Fixture/Dependency.php b/test/Fixture/Dependency.php new file mode 100644 index 0000000..46010ae --- /dev/null +++ b/test/Fixture/Dependency.php @@ -0,0 +1,9 @@ + + */ +class ExampleRepository extends EntityRepository +{ +} diff --git a/test/Fixture/ExposedAnnotatedFactory.php b/test/Fixture/ExposedAnnotatedFactory.php new file mode 100644 index 0000000..8929e02 --- /dev/null +++ b/test/Fixture/ExposedAnnotatedFactory.php @@ -0,0 +1,20 @@ +createAnnotationReader($container); + } +} diff --git a/test/Fixture/MissingServiceService.php b/test/Fixture/MissingServiceService.php new file mode 100644 index 0000000..fd507a7 --- /dev/null +++ b/test/Fixture/MissingServiceService.php @@ -0,0 +1,17 @@ + + */ +class RepositoryWithoutAnnotation extends EntityRepository +{ +} diff --git a/test/Fixture/RequiredParamsWithoutInject.php b/test/Fixture/RequiredParamsWithoutInject.php new file mode 100644 index 0000000..67ad1ca --- /dev/null +++ b/test/Fixture/RequiredParamsWithoutInject.php @@ -0,0 +1,12 @@ + $config + */ + public function __construct(public readonly array $config) + { + self::$constructed++; + } + + /** + * @Inject({Dependency::class}) + */ + public function setDependency(Dependency $dependency): void + { + $this->dependency = $dependency; + } + + /** + * @Inject({Dependency::class}) + */ + public static function staticSetter(Dependency $dependency): void + { + self::$staticCalls++; + } +} diff --git a/test/Fixture/UnbracedInjectService.php b/test/Fixture/UnbracedInjectService.php new file mode 100644 index 0000000..a8c209d --- /dev/null +++ b/test/Fixture/UnbracedInjectService.php @@ -0,0 +1,18 @@ + $config + */ + public function __construct(public readonly array $config) + { + } +} diff --git a/test/Fixture/UnknownAnnotationClass.php b/test/Fixture/UnknownAnnotationClass.php new file mode 100644 index 0000000..4f358fa --- /dev/null +++ b/test/Fixture/UnknownAnnotationClass.php @@ -0,0 +1,12 @@ + Date: Tue, 3 Nov 2026 12:59:36 +0200 Subject: [PATCH 2/5] updated composer Signed-off-by: bidi --- .laminas-ci.json | 2 +- CHANGELOG.md | 5 ++++- README.md | 2 +- composer.json | 6 +++--- docs/book/v6/overview.md | 9 +++++---- docs/book/v6/upgrading.md | 7 +++++-- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.laminas-ci.json b/.laminas-ci.json index efdd72e..e827682 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -2,5 +2,5 @@ "ignore_php_platform_requirements": { "8.6": true }, - "backwardCompatibilityCheck": false + "backwardCompatibilityCheck": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be796a..c644428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Version `6.0.0` continues the annotation-based API of `4.x`. ### Changed - Minimum PHP version is now 8.3. Supported versions: 8.3, 8.4, 8.5 and 8.6. +- `doctrine/orm` is now required as `^3.0` (was `^2.20`) and `doctrine/annotations` as `^2.0` (was `^1.14.3`). +- `laminas/laminas-servicemanager` constraint widened from `^3.22.1` to `^3.22.1 || ^4.0`, and PSR-11 version 2 is supported (`psr/container` `^1.1 || ^2.0`). - `AnnotatedRepositoryFactory` now throws a `RuntimeException` when the entity manager returns a repository that is not an instance of the requested class. Set `repositoryClass` on the entity to the registered repository. - `AnnotatedServiceFactory::__invoke()` and `AnnotatedServiceFactory::createObject()` now return `object` instead of `mixed`. - `AbstractAnnotatedFactory::CACHE_SERVICE` is now a typed constant (`public const string`). @@ -35,7 +37,8 @@ Version `6.0.0` continues the annotation-based API of `4.x`. ### Removed - PHP 8.2 support. -- `doctrine/cache` as a requirement. It is still installed through `doctrine/orm`, but your own code should require it if it uses its classes. +- `doctrine/cache` as a requirement. `doctrine/orm` 3.x does not install it either, so your own code should require it if it uses its classes. +- Support for Doctrine ORM 2.x and `doctrine/annotations` 1.x. ### Fixed diff --git a/README.md b/README.md index d5b23d4..0d8d0c8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Documentation is available at: https://docs.dotkernel.org/dot-annotated-services | Branch | PSR-11 | Docblock Comments | OSS Lifecycle | PHP Version | |--------|----------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| 6.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | +| 6.0 | 1 \|\| 2 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | | 5.0 | 1 \|\| 2 | Attributes | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F5.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/5.2.0) | | 4.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | | 3.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | diff --git a/composer.json b/composer.json index adf0d2b..93d76ea 100644 --- a/composer.json +++ b/composer.json @@ -21,9 +21,9 @@ ], "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0", - "doctrine/annotations": "^1.14.3", - "doctrine/orm" : "^2.20", - "laminas/laminas-servicemanager": "^3.22.1", + "doctrine/annotations": "^2.0", + "doctrine/orm" : "^3.0", + "laminas/laminas-servicemanager": "^3.22.1 || ^4.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.1 || ^2.0" }, diff --git a/docs/book/v6/overview.md b/docs/book/v6/overview.md index 38d2f01..8af0b60 100644 --- a/docs/book/v6/overview.md +++ b/docs/book/v6/overview.md @@ -11,15 +11,16 @@ See the [upgrade guide](upgrading.md) if you are coming from `4.x` or `5.x`. ## Requirements - PHP `8.3`, `8.4`, `8.5` or `8.6` -- PSR-11 version `1` -- `doctrine/annotations` `^1.14.3` -- `doctrine/orm` `^2.20` +- PSR-11 version `1` or `2` +- `laminas/laminas-servicemanager` `^3.22.1` or `^4.0` +- `doctrine/annotations` `^2.0` +- `doctrine/orm` `^3.0` ## Version History | Branch | PSR-11 | Docblock Comments | OSS Lifecycle | PHP Version | |--------|----------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| 6.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | +| 6.0 | 1 \|\| 2 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/6.0.0) | | 5.0 | 1 \|\| 2 | Attributes | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F5.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/5.2.0) | | 4.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | | 3.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | diff --git a/docs/book/v6/upgrading.md b/docs/book/v6/upgrading.md index 4475937..fa9b12d 100644 --- a/docs/book/v6/upgrading.md +++ b/docs/book/v6/upgrading.md @@ -9,7 +9,9 @@ Follow the section that matches the version you are coming from. ### Requirements - PHP `8.3` or newer is required. PHP `8.2` is no longer supported. -- `doctrine/cache` is no longer a requirement of this package. If your application uses its classes, require it yourself. +- `doctrine/orm` `^3.0` and `doctrine/annotations` `^2.0` are required. Applications still on Doctrine ORM `2.x` or `doctrine/annotations` `1.x` must upgrade those first, or stay on `4.x`. +- `doctrine/cache` is no longer a requirement of this package, and `doctrine/orm` `3.x` does not install it. If your application uses its classes, require it yourself. +- `laminas/laminas-servicemanager` `^3.22.1 || ^4.0` is accepted (`4.x` is new in `6.0`), and so is PSR-11 version `2`. - `psr/cache` and `psr/container` are now declared as direct requirements. ### Changes that can break your code @@ -107,7 +109,8 @@ Replace the factory class names in your `ConfigProvider`, and add `Dot\Annotated ### Differences to be aware of -- `6.0` supports PSR-11 version `1` only. `5.x` also supported version `2`. +- `6.0` requires Doctrine ORM `3.x`. `5.x` also supported `2.x`. +- `6.0` requires `laminas/laminas-servicemanager` (`^3.22.1` or `^4.0`), because it builds on its factory interfaces. `5.x` only needed `psr/container`. - `6.0` has no check for services that inject themselves. A service listed in its own `@Inject` is not reported as a recursion. - `6.0` additionally supports `@Service` (see [Create services without a factory](factories/abstract.md)), `@Inject` on setters, and caching the parsed annotations (see [Cache](cache.md)). - `6.0` requires `doctrine/annotations` and a PSR-6 pool if you want to cache annotations. From acd35698b0b19cee56f8e2e89c02bc270716cf67 Mon Sep 17 00:00:00 2001 From: bidi Date: Tue, 3 Nov 2026 21:20:40 +0200 Subject: [PATCH 3/5] updated phpunit - temp change for deprecations Signed-off-by: bidi --- CHANGELOG.md | 1 + phpunit.xml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c644428..c29069c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Version `6.0.0` continues the annotation-based API of `4.x`. - Documentation for 6.0 is available in `docs/book/v6`, including an upgrade guide for 4.x and 5.x, and a page for the abstract factory. - The cache documentation describes a PSR-6 cache pool (`Psr\Cache\CacheItemPoolInterface`) instead of `doctrine/cache` drivers. - The README and docs show the `@Service` annotation in a docblock (`/**`). +- Temporary: `phpunit.xml` sets `ignoreIndirectDeprecations="true"` on ``, because `laminas/laminas-servicemanager` calls `spl_object_hash()`, which is deprecated in PHP 8.6. Remove it once a fixed release of `laminas/laminas-servicemanager` is available. ### Added diff --git a/phpunit.xml b/phpunit.xml index b2a6f47..51393e4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,7 +5,8 @@ ./test - + + ./src From 235957d0ab81b3f3fa82f0166f3efeedfda59800 Mon Sep 17 00:00:00 2001 From: bidi Date: Wed, 4 Nov 2026 14:32:09 +0200 Subject: [PATCH 4/5] updated readme, docs, tests Signed-off-by: bidi --- README.md | 5 + docs/book/v5/overview.md | 45 ++++++ docs/book/v6/overview.md | 5 + mkdocs.yml | 3 + test/AnnotatedServiceFactoryTest.php | 140 ++++++++++++++++++ test/Fixture/InheritedSetterParent.php | 20 +++ test/Fixture/InheritedSetterService.php | 9 ++ .../MissingSetterDependencyService.php | 17 +++ test/Fixture/MultiInjectService.php | 23 +++ test/Fixture/SetterEdgeCasesService.php | 31 ++++ test/Fixture/SetterInjectedService.php | 11 ++ test/Fixture/ThrowingConstructorService.php | 15 ++ test/Fixture/UnknownRootKeyService.php | 17 +++ 13 files changed, 341 insertions(+) create mode 100644 docs/book/v5/overview.md create mode 100644 test/Fixture/InheritedSetterParent.php create mode 100644 test/Fixture/InheritedSetterService.php create mode 100644 test/Fixture/MissingSetterDependencyService.php create mode 100644 test/Fixture/MultiInjectService.php create mode 100644 test/Fixture/SetterEdgeCasesService.php create mode 100644 test/Fixture/ThrowingConstructorService.php create mode 100644 test/Fixture/UnknownRootKeyService.php diff --git a/README.md b/README.md index 0d8d0c8..8a59b3f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ Documentation is available at: https://docs.dotkernel.org/dot-annotated-services | 4.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | | 3.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | +Versions `4.x` and `6.0` are the proper releases of this package. They are annotation-based. + +Version `5.x` is discontinued. It used PHP attributes, which are now provided by a separate package: [dot-dependency-injection](https://github.com/dotkernel/dot-dependency-injection) ([documentation](https://docs.dotkernel.org/dot-dependency-injection/)). +Use it if you want attributes instead of annotations. See [Version 5 is discontinued](https://github.com/dotkernel/dot-annotated-services/blob/6.0/docs/book/v5/overview.md) for migration notes. + ## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) diff --git a/docs/book/v5/overview.md b/docs/book/v5/overview.md new file mode 100644 index 0000000..c858ab5 --- /dev/null +++ b/docs/book/v5/overview.md @@ -0,0 +1,45 @@ +# Version 5 is discontinued + +Version `5.x` of `dot-annotated-services` is discontinued and receives no further releases. +The last release was `5.2.0`. + +Version `5.x` was a separate line of this package that replaced annotations with PHP attributes. +That functionality now lives in its own package, [dot-dependency-injection](https://github.com/dotkernel/dot-dependency-injection), which is the supported alternative for attribute-based dependency injection. + +## Which package to use + +| You want | Use | +| --- | --- | +| PHP attributes | [dot-dependency-injection](https://docs.dotkernel.org/dot-dependency-injection/) | +| Annotations (`doctrine/annotations`) | `dot-annotated-services` `6.0`, or `4.x` if you have to stay on Doctrine ORM `2.x` | + +Versions `4.x` and `6.0` are the proper releases of `dot-annotated-services`. +They are annotation-based and are documented in [version 6](../v6/overview.md) and [version 4](../v4/overview.md). + +## Migrating from 5.x to dot-dependency-injection + +The attributes and factories keep their names and syntax. +Only the package and the namespaces change. + +Replace the package: + +```shell +composer remove dotkernel/dot-annotated-services +composer require dotkernel/dot-dependency-injection +``` + +Replace the namespaces in your code and configuration: + +| 5.x (`dot-annotated-services`) | `dot-dependency-injection` | +| --- | --- | +| `Dot\AnnotatedServices\Attribute\Inject` | `Dot\DependencyInjection\Attribute\Inject` | +| `Dot\AnnotatedServices\Attribute\Entity` | `Dot\DependencyInjection\Attribute\Entity` | +| `Dot\AnnotatedServices\Factory\AttributedServiceFactory` | `Dot\DependencyInjection\Factory\AttributedServiceFactory` | +| `Dot\AnnotatedServices\Factory\AttributedRepositoryFactory` | `Dot\DependencyInjection\Factory\AttributedRepositoryFactory` | +| `Dot\AnnotatedServices\ConfigProvider` | `Dot\DependencyInjection\ConfigProvider` | + +Check the requirements, including the supported PHP versions, in the [dot-dependency-injection documentation](https://docs.dotkernel.org/dot-dependency-injection/). + +## Migrating from 5.x to annotations + +If you prefer annotations, follow the [upgrade guide](../v6/upgrading.md#from-5x) for `6.0`. diff --git a/docs/book/v6/overview.md b/docs/book/v6/overview.md index 8af0b60..28fabf9 100644 --- a/docs/book/v6/overview.md +++ b/docs/book/v6/overview.md @@ -25,6 +25,11 @@ See the [upgrade guide](upgrading.md) if you are coming from `4.x` or `5.x`. | 4.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F4.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/4.4.0) | | 3.0 | 1 | Annotations | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F3.0%2FOSSMETADATA) | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-annotated-services/3.2.1) | +Versions `4.x` and `6.0` are the proper releases of this package. They are annotation-based. + +Version `5.x` is discontinued. It used PHP attributes, which are now provided by a separate package: [dot-dependency-injection](https://github.com/dotkernel/dot-dependency-injection) ([documentation](https://docs.dotkernel.org/dot-dependency-injection/)). +Use it if you want attributes instead of annotations. See [Version 5 is discontinued](../v5/overview.md) for migration notes. + ## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fgithub.com%2Fdotkernel%2Fdot-annotated-services%2Fblob%2F6.0%2FOSSMETADATA) diff --git a/mkdocs.yml b/mkdocs.yml index 2bb8687..2591285 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ extra: current_version: v6 versions: - v6 + - v5 - v4 nav: - Home: index.md @@ -20,6 +21,8 @@ nav: - "Inject class dependencies": v6/factories/service.md - "Inject entity repositories": v6/factories/repository.md - "Create services without a factory": v6/factories/abstract.md + - v5: + - Discontinued: v5/overview.md - v4: - Overview: v4/overview.md - Installation: v4/installation.md diff --git a/test/AnnotatedServiceFactoryTest.php b/test/AnnotatedServiceFactoryTest.php index 9c3e2b9..b81a104 100644 --- a/test/AnnotatedServiceFactoryTest.php +++ b/test/AnnotatedServiceFactoryTest.php @@ -4,6 +4,8 @@ namespace DotTest\AnnotatedServices; +use ArrayObject; +use DomainException; use Dot\AnnotatedServices\Annotation\Inject; use Dot\AnnotatedServices\Exception\ExceptionInterface; use Dot\AnnotatedServices\Exception\InvalidArgumentException; @@ -13,16 +15,25 @@ use DotTest\AnnotatedServices\Fixture\ConfigService; use DotTest\AnnotatedServices\Fixture\Dependency; use DotTest\AnnotatedServices\Fixture\DottedConfigService; +use DotTest\AnnotatedServices\Fixture\InheritedSetterService; use DotTest\AnnotatedServices\Fixture\MissingServiceService; +use DotTest\AnnotatedServices\Fixture\MissingSetterDependencyService; +use DotTest\AnnotatedServices\Fixture\MultiInjectService; use DotTest\AnnotatedServices\Fixture\NoConstructorService; use DotTest\AnnotatedServices\Fixture\NullKeyService; use DotTest\AnnotatedServices\Fixture\OptionalParamsService; use DotTest\AnnotatedServices\Fixture\RequiredParamsWithoutInject; +use DotTest\AnnotatedServices\Fixture\SetterEdgeCasesService; use DotTest\AnnotatedServices\Fixture\SetterInjectedService; +use DotTest\AnnotatedServices\Fixture\ThrowingConstructorService; use DotTest\AnnotatedServices\Fixture\UnbracedInjectService; +use DotTest\AnnotatedServices\Fixture\UnknownRootKeyService; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\ServiceManager; +use LogicException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use stdClass; #[CoversClass(AnnotatedServiceFactory::class)] class AnnotatedServiceFactoryTest extends TestCase @@ -34,6 +45,7 @@ protected function setUp(): void $this->subject = new AnnotatedServiceFactory(); SetterInjectedService::$constructed = 0; SetterInjectedService::$staticCalls = 0; + SetterInjectedService::$plainCalls = 0; } /** @@ -191,6 +203,134 @@ public function testDoesNotCallAnnotatedStaticMethods(): void $this->assertSame(0, SetterInjectedService::$staticCalls); } + public function testDoesNotCallPublicMethodsWithoutInject(): void + { + $service = ($this->subject)($this->container(['config' => []]), SetterInjectedService::class); + + $this->assertInstanceOf(SetterInjectedService::class, $service); + $this->assertSame(0, SetterInjectedService::$plainCalls); + $this->assertSame('value', $service->process('value')); + } + + public function testInjectsValueFromArrayAccessObjectUsingDotNotation(): void + { + $config = new ArrayObject(['a' => new ArrayObject(['b' => 'value'])]); + $service = ($this->subject)($this->container(['config' => $config]), DottedConfigService::class); + + $this->assertInstanceOf(DottedConfigService::class, $service); + $this->assertSame('value', $service->value); + } + + public function testThrowsExceptionWhenKeyIsMissingFromArrayAccessObject(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The key "b" provided in the dotted notation could not be found'); + + ($this->subject)( + $this->container(['config' => ['a' => new ArrayObject([])]]), + DottedConfigService::class + ); + } + + public function testThrowsExceptionWhenDottedPathReachesAnObjectWithoutArrayAccess(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The key "b" provided in the dotted notation could not be found'); + + ($this->subject)( + $this->container(['config' => ['a' => new stdClass()]]), + DottedConfigService::class + ); + } + + public function testInjectsMixedServicesInTheDeclaredOrder(): void + { + $config = ['debug' => true]; + $service = ($this->subject)( + $this->container([ + 'config' => $config, + 'settings' => ['mode' => 'fast'], + ]), + MultiInjectService::class + ); + + $this->assertInstanceOf(MultiInjectService::class, $service); + // Typed constructor parameters make a wrong injection order a TypeError, so the types prove the order + $this->assertSame($config, $service->config); + $this->assertSame('fast', $service->mode); + } + + public function testThrowsExceptionWhenFirstSegmentOfDottedKeyIsNotAService(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(RuntimeException::classNotFound('unknown_root')->getMessage()); + + ($this->subject)($this->container(), UnknownRootKeyService::class); + } + + public function testInjectsSetterWithMultipleArguments(): void + { + $service = ($this->subject)( + $this->container(['settings' => ['mode' => 'fast']]), + SetterEdgeCasesService::class + ); + + $this->assertInstanceOf(SetterEdgeCasesService::class, $service); + $this->assertInstanceOf(Dependency::class, $service->dependency); + $this->assertSame('fast', $service->mode); + } + + public function testDoesNotCallNonPublicMethodsWithInject(): void + { + $service = ($this->subject)( + $this->container(['settings' => ['mode' => 'fast']]), + SetterEdgeCasesService::class + ); + + $this->assertInstanceOf(SetterEdgeCasesService::class, $service); + $this->assertFalse($service->protectedCalled); + } + + public function testThrowsExceptionWhenSetterDependencyDoesNotExist(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(RuntimeException::classNotFound('missing_setter_service')->getMessage()); + + ($this->subject)($this->container(), MissingSetterDependencyService::class); + } + + public function testInjectsSetterInheritedFromParentClass(): void + { + $service = ($this->subject)($this->container(), InheritedSetterService::class); + + $this->assertInstanceOf(InheritedSetterService::class, $service); + $this->assertInstanceOf(Dependency::class, $service->dependency); + } + + public function testContainerExceptionsAreNotWrapped(): void + { + $container = new ServiceManager([ + 'factories' => [ + 'config' => static function (): never { + throw new LogicException('boom'); + }, + ], + ]); + + $this->expectException(ServiceNotCreatedException::class); + $this->expectExceptionMessage('boom'); + + ($this->subject)($container, ConfigService::class); + } + + public function testConstructorExceptionsAreNotWrapped(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('constructor failed'); + + ($this->subject)($this->container(), ThrowingConstructorService::class); + } + public function testThrownExceptionsImplementTheExceptionInterface(): void { try { diff --git a/test/Fixture/InheritedSetterParent.php b/test/Fixture/InheritedSetterParent.php new file mode 100644 index 0000000..3e2d74f --- /dev/null +++ b/test/Fixture/InheritedSetterParent.php @@ -0,0 +1,20 @@ +dependency = $dependency; + } +} diff --git a/test/Fixture/InheritedSetterService.php b/test/Fixture/InheritedSetterService.php new file mode 100644 index 0000000..f391101 --- /dev/null +++ b/test/Fixture/InheritedSetterService.php @@ -0,0 +1,9 @@ + $config + */ + public function __construct( + public readonly array $config, + public readonly Dependency $dependency, + public readonly string $mode, + ) { + } +} diff --git a/test/Fixture/SetterEdgeCasesService.php b/test/Fixture/SetterEdgeCasesService.php new file mode 100644 index 0000000..4154bfa --- /dev/null +++ b/test/Fixture/SetterEdgeCasesService.php @@ -0,0 +1,31 @@ +dependency = $dependency; + $this->mode = $mode; + } + + /** + * @Inject({Dependency::class}) + */ + protected function protectedSetter(Dependency $dependency): void + { + $this->protectedCalled = true; + } +} diff --git a/test/Fixture/SetterInjectedService.php b/test/Fixture/SetterInjectedService.php index 1300606..37ffa78 100644 --- a/test/Fixture/SetterInjectedService.php +++ b/test/Fixture/SetterInjectedService.php @@ -10,6 +10,7 @@ class SetterInjectedService { public static int $constructed = 0; public static int $staticCalls = 0; + public static int $plainCalls = 0; public ?Dependency $dependency = null; @@ -32,6 +33,16 @@ public function setDependency(Dependency $dependency): void $this->dependency = $dependency; } + /** + * Public method that carries no injection annotation: the factory must never call it. + */ + public function process(string $input): string + { + self::$plainCalls++; + + return $input; + } + /** * @Inject({Dependency::class}) */ diff --git a/test/Fixture/ThrowingConstructorService.php b/test/Fixture/ThrowingConstructorService.php new file mode 100644 index 0000000..1a44cfb --- /dev/null +++ b/test/Fixture/ThrowingConstructorService.php @@ -0,0 +1,15 @@ + Date: Thu, 5 Nov 2026 14:00:05 +0200 Subject: [PATCH 5/5] removed laminas servicemanager Signed-off-by: bidi --- CHANGELOG.md | 12 +-- README.md | 32 +------ composer.json | 1 - docs/book/v6/configuration.md | 12 +-- docs/book/v6/factories.md | 13 +-- docs/book/v6/factories/abstract.md | 48 ---------- docs/book/v6/overview.md | 1 - docs/book/v6/upgrading.md | 39 ++++++-- docs/book/v6/usage.md | 1 - mkdocs.yml | 1 - phpunit.xml | 3 +- src/Annotation/Service.php | 18 ---- src/ConfigProvider.php | 24 ++--- .../AnnotatedServiceAbstractFactory.php | 53 ----------- test/AbstractAnnotatedFactoryTest.php | 6 +- test/AnnotatedRepositoryFactoryTest.php | 6 +- test/AnnotatedServiceAbstractFactoryTest.php | 93 ------------------- test/AnnotatedServiceFactoryTest.php | 21 ++--- test/ConfigProviderTest.php | 23 +---- test/Fixture/AnnotatedService.php | 22 ----- test/Fixture/ArrayContainer.php | 45 +++++++++ test/Fixture/CommentOnlyService.php | 12 --- test/Fixture/ContainerFailureException.php | 12 +++ test/Fixture/ServiceInterface.php | 9 -- test/Fixture/ServiceNotFoundException.php | 12 +++ test/Fixture/UnknownAnnotationClass.php | 12 --- 26 files changed, 142 insertions(+), 389 deletions(-) delete mode 100644 docs/book/v6/factories/abstract.md delete mode 100644 src/Annotation/Service.php delete mode 100644 src/Factory/AnnotatedServiceAbstractFactory.php delete mode 100644 test/AnnotatedServiceAbstractFactoryTest.php delete mode 100644 test/Fixture/AnnotatedService.php create mode 100644 test/Fixture/ArrayContainer.php delete mode 100644 test/Fixture/CommentOnlyService.php create mode 100644 test/Fixture/ContainerFailureException.php delete mode 100644 test/Fixture/ServiceInterface.php create mode 100644 test/Fixture/ServiceNotFoundException.php delete mode 100644 test/Fixture/UnknownAnnotationClass.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c29069c..f93636f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,25 +11,23 @@ Version `6.0.0` continues the annotation-based API of `4.x`. - Minimum PHP version is now 8.3. Supported versions: 8.3, 8.4, 8.5 and 8.6. - `doctrine/orm` is now required as `^3.0` (was `^2.20`) and `doctrine/annotations` as `^2.0` (was `^1.14.3`). -- `laminas/laminas-servicemanager` constraint widened from `^3.22.1` to `^3.22.1 || ^4.0`, and PSR-11 version 2 is supported (`psr/container` `^1.1 || ^2.0`). +- `laminas/laminas-servicemanager` is no longer a requirement. The factories only depend on `psr/container`, which is now allowed as `^1.1 || ^2.0` (PSR-11 versions 1 and 2). +- `Dot\AnnotatedServices\ConfigProvider` returns an empty configuration. It no longer registers an abstract factory. - `AnnotatedRepositoryFactory` now throws a `RuntimeException` when the entity manager returns a repository that is not an instance of the requested class. Set `repositoryClass` on the entity to the registered repository. - `AnnotatedServiceFactory::__invoke()` and `AnnotatedServiceFactory::createObject()` now return `object` instead of `mixed`. - `AbstractAnnotatedFactory::CACHE_SERVICE` is now a typed constant (`public const string`). - `Inject` accepts a single unbraced service, e.g. `@Inject("config")`. - `Entity::getName()` is typed as `class-string`. -- `AnnotatedServiceAbstractFactory::canCreate()` returns `false` for classes whose annotations cannot be read, and the abstract factory reuses a single `AnnotatedServiceFactory`. - Array and generic types are specified in docblocks, so the code passes PHPStan level 8. - Development tooling: PHPUnit 12, PHPStan level 8, GitHub Actions run PHP 8.3 to 8.6 with updated `actions/checkout`, `actions/cache` and `codecov/codecov-action`, and the Laminas backward-compatibility check is disabled for this major version. -- Documentation for 6.0 is available in `docs/book/v6`, including an upgrade guide for 4.x and 5.x, and a page for the abstract factory. +- Documentation for 6.0 is available in `docs/book/v6`, including an upgrade guide for 4.x and 5.x. - The cache documentation describes a PSR-6 cache pool (`Psr\Cache\CacheItemPoolInterface`) instead of `doctrine/cache` drivers. -- The README and docs show the `@Service` annotation in a docblock (`/**`). -- Temporary: `phpunit.xml` sets `ignoreIndirectDeprecations="true"` on ``, because `laminas/laminas-servicemanager` calls `spl_object_hash()`, which is deprecated in PHP 8.6. Remove it once a fixed release of `laminas/laminas-servicemanager` is available. ### Added - `RuntimeException::repositoryMismatch()`. - `psr/cache` and `psr/container` as direct requirements, and a `suggest` entry for a PSR-6 cache implementation. -- A test suite with fixtures, a real annotation reader and a real service manager, covering the factories, the abstract factory, the annotation reader selection, the annotations and the exceptions. +- A test suite with fixtures, a real annotation reader and a minimal PSR-11 container, covering the factories, the annotation reader selection, the annotations and the exceptions. ### Deprecated @@ -40,6 +38,7 @@ Version `6.0.0` continues the annotation-based API of `4.x`. - PHP 8.2 support. - `doctrine/cache` as a requirement. `doctrine/orm` 3.x does not install it either, so your own code should require it if it uses its classes. - Support for Doctrine ORM 2.x and `doctrine/annotations` 1.x. +- `AnnotatedServiceAbstractFactory` and the `@Service` annotation, together with the `laminas/laminas-servicemanager` dependency they needed. Register classes with `AnnotatedServiceFactory` instead (see the upgrade guide). ### Fixed @@ -47,7 +46,6 @@ Version `6.0.0` continues the annotation-based API of `4.x`. - A dotted dependency such as `config.key` failed when the value was `null`. It is now injected as `null`. - A dotted dependency longer than the data (e.g. `config.a.b` where `config['a']` is a string) returned an intermediate value. It now throws an `InvalidArgumentException`. - `@Inject("config")` without curly braces failed with a creation error. -- `has()` and `get()` on the container threw an `AnnotationException` for unrelated classes that contain annotations unknown to the annotation reader, when the abstract factory was registered. - Static methods annotated with `@Inject` are no longer called during setter injection. ## 5.2.0 - 2024-10-23 diff --git a/README.md b/README.md index 8a59b3f..821a0c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # dot-annotated-services -Dotkernel component used to create services through [Laminas Service Manager](https://github.com/laminas/laminas-servicemanager) and inject them with dependencies just using method annotations. -It can also create services without the need to write factories. +Dotkernel component used to create services through any PSR-11 container, such as [Laminas Service Manager](https://github.com/laminas/laminas-servicemanager), and inject them with dependencies just using method annotations. Annotation parsing can be cached to improve performance. This package can clean up your code by getting rid of all the factories you write, sometimes just to inject a dependency or two. @@ -46,13 +45,11 @@ Run the following command in your project directory composer require dotkernel/dot-annotated-services ``` -After installing, add the `ConfigProvider` class to your configuration aggregate. - ## Usage ### Using the AnnotatedServiceFactory -You can register services in the service manager using the `AnnotatedServiceFactory` as below. +You can register services in the container using the `AnnotatedServiceFactory` as below. ```php return [ @@ -85,9 +82,9 @@ public function __construct( ``` The annotation `@Inject` is telling the factory to inject the services between curly braces. -Valid service names should be provided, as registered in the service manager. +Valid service names should be provided, as registered in the container. -To inject an array value from the service manager, you can use dot notation as below. +To inject an array value from the container, you can use dot notation as below. ```php use Dot\AnnotatedServices\Annotation\Inject; @@ -133,27 +130,6 @@ class ExampleRepository extends EntityRepository } ``` -### Using the abstract factory - -Using this approach, no service manager configuration is required. It uses the registered abstract factory to create annotated services. - -To tell the abstract factory which services are to be created, you need to annotate the service class with the `@Service` annotation. -The annotation must be in a docblock, which starts with `/**`. - -```php -use Dot\AnnotatedServices\Annotation\Service; - -/** - * @Service - */ -class ServiceClass -{ - // configure injections as described in the previous section -} -``` - -And that's it, you don't need to configure the service manager with this class, creation will happen automatically. - ## Cache annotations This package is built on top of `doctrine/annotations`. diff --git a/composer.json b/composer.json index 93d76ea..e1fa4e3 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,6 @@ "php": "~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0", "doctrine/annotations": "^2.0", "doctrine/orm" : "^3.0", - "laminas/laminas-servicemanager": "^3.22.1 || ^4.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.1 || ^2.0" }, diff --git a/docs/book/v6/configuration.md b/docs/book/v6/configuration.md index 9ee1bcb..8de419a 100644 --- a/docs/book/v6/configuration.md +++ b/docs/book/v6/configuration.md @@ -1,12 +1,12 @@ # Configuration -After installation, register `dot-annotated-services` in your project by adding the below line to your configuration aggregator (usually: `config/config.php`): +`dot-annotated-services` needs no global registration. +The factories are registered per class, under `factories` in your own `ConfigProvider`. +See [Inject class dependencies](factories/service.md) and [Inject entity repositories](factories/repository.md). -```php -Dot\AnnotatedServices\ConfigProvider::class, -``` +`Dot\AnnotatedServices\ConfigProvider` is still provided, but it returns an empty configuration. +You can keep it in your configuration aggregator (usually: `config/config.php`) if it is already there, or leave it out. -This registers `AnnotatedServiceAbstractFactory`, which creates every class marked with the `@Service` annotation without further configuration. -`AnnotatedServiceFactory` and `AnnotatedRepositoryFactory` are registered per class under `factories` in your own `ConfigProvider`. +The factories only depend on `psr/container`, so they work with any PSR-11 container that accepts a factory callable per service key, such as Laminas Service Manager. See [Factories](factories.md) for details and [Cache](cache.md) to cache the parsed annotations. diff --git a/docs/book/v6/factories.md b/docs/book/v6/factories.md index 2f1295b..4d9d0c6 100644 --- a/docs/book/v6/factories.md +++ b/docs/book/v6/factories.md @@ -1,6 +1,6 @@ # Factories -`dot-annotated-services` is based on three reusable factories - `AnnotatedRepositoryFactory`, `AnnotatedServiceFactory` and `AnnotatedServiceAbstractFactory` - able to inject any dependency into a class. +`dot-annotated-services` is based on two reusable factories - `AnnotatedRepositoryFactory` and `AnnotatedServiceFactory` - able to inject any dependency into a class. ## AnnotatedRepositoryFactory @@ -16,7 +16,7 @@ The repository class returned by the entity manager must be the registered class - `Dot\AnnotatedServices\Exception\RuntimeException` if repository does not have `@Entity` annotation - `Dot\AnnotatedServices\Exception\RuntimeException` if the entity manager returns a repository that is not an instance of the requested class - `Psr\Container\NotFoundExceptionInterface` if `Doctrine\ORM\EntityManagerInterface` does not exist in the service container -- `Psr\Container\ContainerExceptionInterface` if service manager is unable to provide an instance of `Doctrine\ORM\EntityManagerInterface` +- `Psr\Container\ContainerExceptionInterface` if the container is unable to provide an instance of `Doctrine\ORM\EntityManagerInterface` ## AnnotatedServiceFactory @@ -42,11 +42,4 @@ The constructor is only called once, and static methods are ignored. - `Dot\AnnotatedServices\Exception\InvalidArgumentException` if a key of a dotted dependency does not exist, including when the path is longer than the data - `ReflectionException` on failure of creating a ReflectionClass of the dependency - `Psr\Container\NotFoundExceptionInterface` if a dependency does not exist in the service container -- `Psr\Container\ContainerExceptionInterface` if service manager is unable to provide an instance of a dependency - -## AnnotatedServiceAbstractFactory - -Creates every class annotated with `@Service`, without any service manager configuration. -See [Create services without a factory](factories/abstract.md). - -It never throws when asked whether it can create a class (for example through `$container->has()`): classes it cannot read, such as classes with unknown annotations, are reported as not creatable. +- `Psr\Container\ContainerExceptionInterface` if the container is unable to provide an instance of a dependency diff --git a/docs/book/v6/factories/abstract.md b/docs/book/v6/factories/abstract.md deleted file mode 100644 index 399e669..0000000 --- a/docs/book/v6/factories/abstract.md +++ /dev/null @@ -1,48 +0,0 @@ -# Create services without a factory - -Using this approach, no service manager configuration is required. -The abstract factory registered by `Dot\AnnotatedServices\ConfigProvider` creates every class marked with the `@Service` annotation. - -## Prepare class - -Add the `@Service` annotation to the class docblock. -Configure the dependencies with `@Inject`, as described in [Inject class dependencies](service.md). - -```php - The annotation must be in a docblock, which starts with `/**`. -> A comment starting with `/*` is ignored, and the class is not created. - -And that's it, you don't need to configure the service manager with this class, creation will happen automatically. - -## Behavior - -- Only class names are handled. Interfaces, unknown names and classes without `@Service` are left to the other factories of the container. -- Classes that contain annotations unknown to the annotation reader are reported as not creatable, instead of throwing an exception from `$container->has()`. -- Because the abstract factory is asked about every name the container cannot otherwise resolve, [cache the annotations](../cache.md) in production. diff --git a/docs/book/v6/overview.md b/docs/book/v6/overview.md index 28fabf9..c2f055a 100644 --- a/docs/book/v6/overview.md +++ b/docs/book/v6/overview.md @@ -12,7 +12,6 @@ See the [upgrade guide](upgrading.md) if you are coming from `4.x` or `5.x`. - PHP `8.3`, `8.4`, `8.5` or `8.6` - PSR-11 version `1` or `2` -- `laminas/laminas-servicemanager` `^3.22.1` or `^4.0` - `doctrine/annotations` `^2.0` - `doctrine/orm` `^3.0` diff --git a/docs/book/v6/upgrading.md b/docs/book/v6/upgrading.md index fa9b12d..efadec3 100644 --- a/docs/book/v6/upgrading.md +++ b/docs/book/v6/upgrading.md @@ -11,17 +11,18 @@ Follow the section that matches the version you are coming from. - PHP `8.3` or newer is required. PHP `8.2` is no longer supported. - `doctrine/orm` `^3.0` and `doctrine/annotations` `^2.0` are required. Applications still on Doctrine ORM `2.x` or `doctrine/annotations` `1.x` must upgrade those first, or stay on `4.x`. - `doctrine/cache` is no longer a requirement of this package, and `doctrine/orm` `3.x` does not install it. If your application uses its classes, require it yourself. -- `laminas/laminas-servicemanager` `^3.22.1 || ^4.0` is accepted (`4.x` is new in `6.0`), and so is PSR-11 version `2`. +- `laminas/laminas-servicemanager` is no longer a requirement. The factories only depend on `psr/container`, version `1.1` or `2`. If your application uses Laminas Service Manager, require it yourself. - `psr/cache` and `psr/container` are now declared as direct requirements. ### Changes that can break your code | Change | Symptom | Fix | | --- | --- | --- | +| `AnnotatedServiceAbstractFactory` and the `@Service` annotation are removed | Class not found, or the service is no longer created automatically | Register the class under `factories` with `AnnotatedServiceFactory`, and remove `@Service` and the abstract factory registration. See below | +| `Dot\AnnotatedServices\ConfigProvider` no longer registers the abstract factory | `abstract_factories` is no longer filled by this package | None, unless you read that configuration key | | The constructor is no longer called a second time when it carries `@Inject` | Code that relied on the constructor running twice no longer does | Remove that reliance. Classes with `readonly` promoted properties now work | | `AnnotatedRepositoryFactory` throws if the entity manager returns a repository that is not an instance of the registered class | `Dot\AnnotatedServices\Exception\RuntimeException: Entity manager returned a repository of type ...` | Set `repositoryClass` on the entity to the registered repository, or register the class the entity manager returns | | A dotted dependency whose path is longer than the data now throws | `InvalidArgumentException` where `config.a.b` used to inject the value of `config.a` | Fix the key path | -| `AnnotatedServiceAbstractFactory::canCreate()` returns `false` for classes it cannot read | `has()` returns `false` instead of throwing `AnnotationException` | None, unless your code caught that exception | | `AnnotatedServiceFactory::__invoke()` and `createObject()` return `object` instead of `mixed` | Fatal error in subclasses that override them with an incompatible return type | Return an `object` from the override | | `AbstractAnnotatedFactory::CACHE_SERVICE` is a typed constant (`string`) | Fatal error in subclasses that redeclare it untyped | Declare it as `public const string` | @@ -29,16 +30,39 @@ Follow the section that matches the version you are coming from. - `@Inject("config")` without curly braces is accepted. - A dotted key whose value is `null` is injected as `null`. It used to throw. -- `has()` on the container no longer throws for classes with unknown annotations. + +### Replacing `@Service` + +Before, on `4.x`: + +```php +/** + * @Service + */ +class ExampleService +{ +} +``` + +After, on `6.0`: remove the annotation and register the class with the factory in your `ConfigProvider`: + +```php +'dependencies' => [ + 'factories' => [ + ExampleService::class => Dot\AnnotatedServices\Factory\AnnotatedServiceFactory::class, + ], +], +``` + +Also remove `Dot\AnnotatedServices\Factory\AnnotatedServiceAbstractFactory` from `abstract_factories` if you registered it yourself. ### Documentation changes - The cache documentation of `4.x` described `doctrine/cache` drivers. The package has always required a PSR-6 cache pool. See [Cache](cache.md). -- `@Service` must be written in a docblock (`/**`), not in a `/*` comment. ## From 5.x -`5.x` uses PHP attributes. `6.0` uses annotations, and adds an abstract factory, setter injection and an annotation cache. +`5.x` uses PHP attributes. `6.0` uses annotations, and adds setter injection and an annotation cache. Migrating from `5.x` to `6.0` means moving back from attributes to annotations. ### Classes @@ -105,12 +129,11 @@ class ExampleService ### Registration -Replace the factory class names in your `ConfigProvider`, and add `Dot\AnnotatedServices\ConfigProvider::class` to your configuration aggregator if you want to use `@Service`. +Replace the factory class names in your `ConfigProvider`. Nothing needs to be added to your configuration aggregator. ### Differences to be aware of - `6.0` requires Doctrine ORM `3.x`. `5.x` also supported `2.x`. -- `6.0` requires `laminas/laminas-servicemanager` (`^3.22.1` or `^4.0`), because it builds on its factory interfaces. `5.x` only needed `psr/container`. - `6.0` has no check for services that inject themselves. A service listed in its own `@Inject` is not reported as a recursion. -- `6.0` additionally supports `@Service` (see [Create services without a factory](factories/abstract.md)), `@Inject` on setters, and caching the parsed annotations (see [Cache](cache.md)). +- `6.0` additionally supports `@Inject` on setters, and caching the parsed annotations (see [Cache](cache.md)). - `6.0` requires `doctrine/annotations` and a PSR-6 pool if you want to cache annotations. diff --git a/docs/book/v6/usage.md b/docs/book/v6/usage.md index 94d0d18..ff8b253 100644 --- a/docs/book/v6/usage.md +++ b/docs/book/v6/usage.md @@ -11,6 +11,5 @@ You can use it to: - [Inject class dependencies](factories/service.md) - [Inject entity repositories](factories/repository.md) -- [Create services without writing a factory](factories/abstract.md) Coming from `4.x` or `5.x`? Read the [upgrade guide](upgrading.md). diff --git a/mkdocs.yml b/mkdocs.yml index 2591285..d9bbc7c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,6 @@ nav: - Reference: - "Inject class dependencies": v6/factories/service.md - "Inject entity repositories": v6/factories/repository.md - - "Create services without a factory": v6/factories/abstract.md - v5: - Discontinued: v5/overview.md - v4: diff --git a/phpunit.xml b/phpunit.xml index 51393e4..b2a6f47 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,8 +5,7 @@ ./test - - + ./src diff --git a/src/Annotation/Service.php b/src/Annotation/Service.php deleted file mode 100644 index cb89427..0000000 --- a/src/Annotation/Service.php +++ /dev/null @@ -1,18 +0,0 @@ -}} + * @return array */ public function __invoke(): array { - return [ - 'dependencies' => $this->getDependenciesConfig(), - ]; - } - - /** - * @return array{abstract_factories: list} - */ - public function getDependenciesConfig(): array - { - return [ - 'abstract_factories' => [ - AnnotatedServiceAbstractFactory::class, - ], - ]; + return []; } } diff --git a/src/Factory/AnnotatedServiceAbstractFactory.php b/src/Factory/AnnotatedServiceAbstractFactory.php deleted file mode 100644 index 127d444..0000000 --- a/src/Factory/AnnotatedServiceAbstractFactory.php +++ /dev/null @@ -1,53 +0,0 @@ -createAnnotationReader($container) - ->getClassAnnotation(new ReflectionClass($requestedName), Service::class) !== null; - } catch (AnnotationException) { - return false; - } - } - - /** - * @param string $requestedName - * @param array|null $options - */ - #[Override] - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): object - { - if ($this->serviceFactory === null) { - $this->serviceFactory = new AnnotatedServiceFactory(); - $this->serviceFactory->setAnnotationReader($this->createAnnotationReader($container)); - } - - return $this->serviceFactory->createObject($container, $requestedName); - } -} diff --git a/test/AbstractAnnotatedFactoryTest.php b/test/AbstractAnnotatedFactoryTest.php index 0346d73..6eeab1f 100644 --- a/test/AbstractAnnotatedFactoryTest.php +++ b/test/AbstractAnnotatedFactoryTest.php @@ -8,8 +8,8 @@ use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Dot\AnnotatedServices\Factory\AbstractAnnotatedFactory; +use DotTest\AnnotatedServices\Fixture\ArrayContainer; use DotTest\AnnotatedServices\Fixture\ExposedAnnotatedFactory; -use Laminas\ServiceManager\ServiceManager; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; @@ -27,9 +27,9 @@ protected function setUp(): void /** * @param array $services */ - private function container(array $services = []): ServiceManager + private function container(array $services = []): ArrayContainer { - return new ServiceManager(['services' => $services]); + return new ArrayContainer($services); } public function testUsesPlainReaderWhenNoCacheIsRegistered(): void diff --git a/test/AnnotatedRepositoryFactoryTest.php b/test/AnnotatedRepositoryFactoryTest.php index a55a191..fe95b23 100644 --- a/test/AnnotatedRepositoryFactoryTest.php +++ b/test/AnnotatedRepositoryFactoryTest.php @@ -9,11 +9,11 @@ use Dot\AnnotatedServices\Annotation\Entity; use Dot\AnnotatedServices\Exception\RuntimeException; use Dot\AnnotatedServices\Factory\AnnotatedRepositoryFactory; +use DotTest\AnnotatedServices\Fixture\ArrayContainer; use DotTest\AnnotatedServices\Fixture\ExampleEntity; use DotTest\AnnotatedServices\Fixture\ExampleRepository; use DotTest\AnnotatedServices\Fixture\PlainClass; use DotTest\AnnotatedServices\Fixture\RepositoryWithoutAnnotation; -use Laminas\ServiceManager\ServiceManager; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -27,9 +27,9 @@ protected function setUp(): void $this->subject = new AnnotatedRepositoryFactory(); } - private function container(EntityManagerInterface $entityManager): ServiceManager + private function container(EntityManagerInterface $entityManager): ArrayContainer { - return new ServiceManager(['services' => [EntityManagerInterface::class => $entityManager]]); + return new ArrayContainer([EntityManagerInterface::class => $entityManager]); } public function testThrowsExceptionClassNotFound(): void diff --git a/test/AnnotatedServiceAbstractFactoryTest.php b/test/AnnotatedServiceAbstractFactoryTest.php deleted file mode 100644 index 96c0703..0000000 --- a/test/AnnotatedServiceAbstractFactoryTest.php +++ /dev/null @@ -1,93 +0,0 @@ -subject = new AnnotatedServiceAbstractFactory(); - } - - /** - * @param array $services - */ - private function container(array $services = []): ServiceManager - { - $config = (new ConfigProvider())()['dependencies']; - $config['services'] = $services; - $config['abstract_factories'] = [AnnotatedServiceAbstractFactory::class]; - - return new ServiceManager($config); - } - - /** - * @return array - */ - public static function notCreatableProvider(): array - { - return [ - 'class without @Service' => [PlainClass::class], - 'interface' => [ServiceInterface::class], - 'unknown name' => ['This\Class\Does\Not\Exist'], - '/* */ comment is not a docblock' => [CommentOnlyService::class], - 'docblock with unknown annotation' => [UnknownAnnotationClass::class], - ]; - } - - public function testCanCreateClassAnnotatedWithService(): void - { - $this->assertTrue($this->subject->canCreate($this->container(), AnnotatedService::class)); - } - - #[DataProvider('notCreatableProvider')] - public function testCannotCreateOtherNames(string $requestedName): void - { - $this->assertFalse($this->subject->canCreate($this->container(), $requestedName)); - } - - #[DataProvider('notCreatableProvider')] - public function testContainerReportsOtherNamesAsMissingWithoutThrowing(string $requestedName): void - { - $this->assertFalse($this->container()->has($requestedName)); - } - - public function testCreatesAnnotatedServiceThroughTheContainer(): void - { - $config = ['debug' => true]; - $container = $this->container(['config' => $config]); - - $this->assertTrue($container->has(AnnotatedService::class)); - - $service = $container->get(AnnotatedService::class); - - $this->assertInstanceOf(AnnotatedService::class, $service); - $this->assertSame($config, $service->config); - } - - public function testInvokeBuildsService(): void - { - $config = ['debug' => false]; - $service = ($this->subject)($this->container(['config' => $config]), AnnotatedService::class); - - $this->assertInstanceOf(AnnotatedService::class, $service); - $this->assertSame($config, $service->config); - } -} diff --git a/test/AnnotatedServiceFactoryTest.php b/test/AnnotatedServiceFactoryTest.php index b81a104..e2768a1 100644 --- a/test/AnnotatedServiceFactoryTest.php +++ b/test/AnnotatedServiceFactoryTest.php @@ -11,8 +11,10 @@ use Dot\AnnotatedServices\Exception\InvalidArgumentException; use Dot\AnnotatedServices\Exception\RuntimeException; use Dot\AnnotatedServices\Factory\AnnotatedServiceFactory; +use DotTest\AnnotatedServices\Fixture\ArrayContainer; use DotTest\AnnotatedServices\Fixture\ClassStringService; use DotTest\AnnotatedServices\Fixture\ConfigService; +use DotTest\AnnotatedServices\Fixture\ContainerFailureException; use DotTest\AnnotatedServices\Fixture\Dependency; use DotTest\AnnotatedServices\Fixture\DottedConfigService; use DotTest\AnnotatedServices\Fixture\InheritedSetterService; @@ -28,9 +30,6 @@ use DotTest\AnnotatedServices\Fixture\ThrowingConstructorService; use DotTest\AnnotatedServices\Fixture\UnbracedInjectService; use DotTest\AnnotatedServices\Fixture\UnknownRootKeyService; -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\ServiceManager; -use LogicException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use stdClass; @@ -51,9 +50,9 @@ protected function setUp(): void /** * @param array $services */ - private function container(array $services = []): ServiceManager + private function container(array $services = []): ArrayContainer { - return new ServiceManager(['services' => $services]); + return new ArrayContainer($services); } public function testThrowsExceptionClassNotFound(): void @@ -309,15 +308,13 @@ public function testInjectsSetterInheritedFromParentClass(): void public function testContainerExceptionsAreNotWrapped(): void { - $container = new ServiceManager([ - 'factories' => [ - 'config' => static function (): never { - throw new LogicException('boom'); - }, - ], + $container = new ArrayContainer([], [ + 'config' => static function (): never { + throw new ContainerFailureException('boom'); + }, ]); - $this->expectException(ServiceNotCreatedException::class); + $this->expectException(ContainerFailureException::class); $this->expectExceptionMessage('boom'); ($this->subject)($container, ConfigService::class); diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index 3ec3947..5293637 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -5,33 +5,14 @@ namespace DotTest\AnnotatedServices; use Dot\AnnotatedServices\ConfigProvider; -use Dot\AnnotatedServices\Factory\AnnotatedServiceAbstractFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; #[CoversClass(ConfigProvider::class)] class ConfigProviderTest extends TestCase { - /** @var array */ - protected array $config; - - protected function setUp(): void - { - $this->config = (new ConfigProvider())(); - } - - public function testHasDependencies(): void - { - $this->assertArrayHasKey('dependencies', $this->config); - } - - public function testDependenciesHasFactories(): void + public function testRegistersNothingGlobally(): void { - $this->assertIsArray($this->config['dependencies']); - $this->assertArrayHasKey('abstract_factories', $this->config['dependencies']); - $this->assertContainsEquals( - AnnotatedServiceAbstractFactory::class, - $this->config['dependencies']['abstract_factories'] - ); + $this->assertSame([], (new ConfigProvider())()); } } diff --git a/test/Fixture/AnnotatedService.php b/test/Fixture/AnnotatedService.php deleted file mode 100644 index 35344d2..0000000 --- a/test/Fixture/AnnotatedService.php +++ /dev/null @@ -1,22 +0,0 @@ - $config - */ - public function __construct(public readonly array $config) - { - } -} diff --git a/test/Fixture/ArrayContainer.php b/test/Fixture/ArrayContainer.php new file mode 100644 index 0000000..a25e0c3 --- /dev/null +++ b/test/Fixture/ArrayContainer.php @@ -0,0 +1,45 @@ + $services Ready-made services + * @param array $factories Called on every get() + */ + public function __construct( + private readonly array $services = [], + private readonly array $factories = [], + ) { + } + + public function get(string $id): mixed + { + if (array_key_exists($id, $this->services)) { + return $this->services[$id]; + } + + if (isset($this->factories[$id])) { + return ($this->factories[$id])(); + } + + throw new ServiceNotFoundException(sprintf('Service "%s" was not found', $id)); + } + + public function has(string $id): bool + { + return array_key_exists($id, $this->services) || isset($this->factories[$id]); + } +} diff --git a/test/Fixture/CommentOnlyService.php b/test/Fixture/CommentOnlyService.php deleted file mode 100644 index aa2e8e3..0000000 --- a/test/Fixture/CommentOnlyService.php +++ /dev/null @@ -1,12 +0,0 @@ -