diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4489304..b77eca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,11 @@ jobs: php: - '8.0' - '8.1' + - '8.2' coverage: - none include: - - php: '8.2' + - php: '8.3' coverage: xdebug steps: - uses: shivammathur/setup-php@2.26.0 @@ -28,7 +29,7 @@ jobs: fetch-depth: 2 # required by Scrutinizer - run: composer install --no-progress --no-ansi --no-interaction --dev --prefer-dist - run: php test/test.php - - if: github.repository == 'mindplay-dk/unbox' && matrix.coverage == 'xdebug' + - if: matrix.coverage == 'xdebug' uses: sudo-bot/action-scrutinizer@latest with: cli-args: '--format=php-clover test/build/clover.xml' diff --git a/.gitignore b/.gitignore index 9bab9ec..613b203 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor .* !.git* +/composer.lock diff --git a/README.md b/README.md index cf70351..aefcf6c 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,9 @@ ref(string $name) : BoxedValueInterface # create a boxed referenc registerFallback(ContainerInterface $container) # register a fallack container +requires(string $requirement, string $description) # defines a Requirement +provides(string $requirement, string $description) # fulfills an abstract Requirement + createContainer() : Container # create a bootstrapped Container instance ``` @@ -524,6 +527,82 @@ for a quick development setup. Even if somebody wanted to override some of the r in e.g. your default development setup, they can of course still do that, e.g. by calling `register()` again to override components as needed. +##### Provider Requirements + +In large, modular architectures, you may have many Providers with inter-dependencies, which +can become difficult to manage at scale. + +Since Providers exist *outside* the realm of the Container, the concept of Requirements can +be used to define verifiable provider interdependencies, which will be checked at the time +when `createContainer()` is called. + +Requirements may be defined by calling `requires()`, and more than one Provider may specify the +same Requirement - possibly for different reasons, which may be described using the optional +`$description` argument. + +Requirements may be fulfilled by calling either `register()` or `provides()`. + +###### Component Requirements + +Providers may depend on the consumer to manually register a component. + +For example, the following Provider requires you to register a `PDO` connection instance: + +```php +class MyProvider implements ProviderInterface +{ + public function register(ContainerFactory $factory) + { + $factory->requires(PDO::class, "a PDO instance connected to a MySQL database"); + + // ... + } +} +``` + +Attempting to bootstrap this Provider, without manually registering the `PDO` instance, will +generate an Exception, as soon as `createContainer()` is called - which is much easier to debug +than the `NotFoundException` you would otherwise get, and which might not occur until you try +to resolve a component that actually depends on the database connection. + +###### Abstract Requirements + +Providers may have abstract Requirements - something that can't be expressed by a simple +component dependency. + +For example, the following Provider requires you to simply indicate that you've bootstrapped +a "payment gateway" - whatever that means to the Provider in question: + +```php +class MyProvider implements ProviderInterface +{ + public function register(ContainerFactory $factory) + { + $factory->requires("acme.payment-gateway", "please refer to acme's documentation"); + + // ... + } +} +``` + +Another provider needs to explicitly indicate fulfillment of this abstract Requirement: + +```php +class MyPaymentProvider implements ProviderInterface +{ + public function register(ContainerFactory $factory) + { + $factory->provides("acme.payment-gateway"); + + // ... + } +} +``` + +Note that abstract Requirements should be a last resort - component dependencies are +*generally* simpler and easier to understand. This feature exists primarily to support +complex, large-scale modular frameworks. + ### Fallback Containers You can use this feature to build layered architecture with different component life-cycles. diff --git a/UPGRADING.md b/UPGRADING.md index cb4ce90..371d194 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,10 @@ Upgrading ========= +#### 3.1.0 + +This feature-release introduces requirements (via `requires` and `provides`) and requires PHP 8.0 or later. + #### 3.0.0 This release removes backwards compatibility with the legacy package `container-interop/container-interop` diff --git a/composer.json b/composer.json index ef405b0..9d9ec8d 100644 --- a/composer.json +++ b/composer.json @@ -15,10 +15,13 @@ }, "require-dev": { "mindplay/benchpress": "^0.1", - "mindplay/testies": "^1.0", + "mindplay/testies": "^1.1.2", "php-di/php-di": "^7.0.5", "pimple/pimple": "^3.5", - "phpunit/php-code-coverage": "^9.2.5" + "phpunit/php-code-coverage": "^9 || ^10 || ^11" + }, + "scripts": { + "test": "XDEBUG_MODE=coverage php test/test.php" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 29d0a04..0000000 --- a/composer.lock +++ /dev/null @@ -1,994 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "c8c86b3335bd2bb7d08db2bfe29f8175", - "packages": [ - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - } - ], - "packages-dev": [ - { - "name": "laravel/serializable-closure", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/laravel/serializable-closure.git", - "reference": "e5a3057a5591e1cfe8183034b0203921abe2c902" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/e5a3057a5591e1cfe8183034b0203921abe2c902", - "reference": "e5a3057a5591e1cfe8183034b0203921abe2c902", - "shasum": "" - }, - "require": { - "php": "^7.3|^8.0" - }, - "require-dev": { - "nesbot/carbon": "^2.61", - "pestphp/pest": "^1.21.3", - "phpstan/phpstan": "^1.8.2", - "symfony/var-dumper": "^5.4.11" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Laravel\\SerializableClosure\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - }, - { - "name": "Nuno Maduro", - "email": "nuno@laravel.com" - } - ], - "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", - "keywords": [ - "closure", - "laravel", - "serializable" - ], - "support": { - "issues": "https://github.com/laravel/serializable-closure/issues", - "source": "https://github.com/laravel/serializable-closure" - }, - "time": "2023-07-14T13:56:28+00:00" - }, - { - "name": "mindplay/benchpress", - "version": "0.1.0", - "source": { - "type": "git", - "url": "https://github.com/mindplay-dk/benchpress.git", - "reference": "e46935bb0c220ba2ecd6d32314f875f3d8511f16" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mindplay-dk/benchpress/zipball/e46935bb0c220ba2ecd6d32314f875f3d8511f16", - "reference": "e46935bb0c220ba2ecd6d32314f875f3d8511f16", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "mindplay\\benchpress\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0+" - ], - "authors": [ - { - "name": "Rasmus Schultz", - "email": "rasmus@mindplay.dk" - } - ], - "description": "A simple benchmark suite for PHP 5.3 and up", - "support": { - "issues": "https://github.com/mindplay-dk/benchpress/issues", - "source": "https://github.com/mindplay-dk/benchpress/tree/master" - }, - "time": "2016-08-07T13:44:25+00:00" - }, - { - "name": "mindplay/testies", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/mindplay-dk/testies.git", - "reference": "6501bc0ec4599948ee83284aa926538af839de50" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mindplay-dk/testies/zipball/6501bc0ec4599948ee83284aa926538af839de50", - "reference": "6501bc0ec4599948ee83284aa926538af839de50", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^6.3.3", - "phpunit/php-code-coverage": "^9.2.5" - }, - "suggest": { - "phpunit/php-code-coverage": "^9.2.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/test.func.php" - ], - "psr-4": { - "mindplay\\testies\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Rasmus Schultz", - "email": "rasmus@mindplay.dk" - } - ], - "description": "Yeah, testies: a lightweight library for quick, simple unit-testing", - "support": { - "issues": "https://github.com/mindplay-dk/testies/issues", - "source": "https://github.com/mindplay-dk/testies/tree/1.0.0" - }, - "time": "2021-02-25T09:50:25+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.17.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" - }, - "time": "2023-08-13T19:53:39+00:00" - }, - { - "name": "php-di/invoker", - "version": "2.3.4", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/Invoker.git", - "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/33234b32dafa8eb69202f950a1fc92055ed76a86", - "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "psr/container": "^1.0|^2.0" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "mnapoli/hard-mode": "~0.3.0", - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Invoker\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Generic and extensible callable invoker", - "homepage": "https://github.com/PHP-DI/Invoker", - "keywords": [ - "callable", - "dependency", - "dependency-injection", - "injection", - "invoke", - "invoker" - ], - "support": { - "issues": "https://github.com/PHP-DI/Invoker/issues", - "source": "https://github.com/PHP-DI/Invoker/tree/2.3.4" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - } - ], - "time": "2023-09-08T09:24:21+00:00" - }, - { - "name": "php-di/php-di", - "version": "7.0.5", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "9ea40a5a6970bf1ca5cbe148bc16cbad6ca3db6c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/9ea40a5a6970bf1ca5cbe148bc16cbad6ca3db6c", - "reference": "9ea40a5a6970bf1ca5cbe148bc16cbad6ca3db6c", - "shasum": "" - }, - "require": { - "laravel/serializable-closure": "^1.0", - "php": ">=8.0", - "php-di/invoker": "^2.0", - "psr/container": "^1.1 || ^2.0" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3", - "friendsofphp/proxy-manager-lts": "^1", - "mnapoli/phpunit-easymock": "^1.3", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^4.6" - }, - "suggest": { - "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "DI\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The dependency injection container for humans", - "homepage": "https://php-di.org/", - "keywords": [ - "PSR-11", - "container", - "container-interop", - "dependency injection", - "di", - "ioc", - "psr11" - ], - "support": { - "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.5" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", - "type": "tidelift" - } - ], - "time": "2023-08-10T14:57:56+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "9.2.29", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-09-19T04:57:46+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2021-12-02T12:48:52+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" - }, - { - "name": "pimple/pimple", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/silexphp/Pimple.git", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1 || ^2.0" - }, - "require-dev": { - "symfony/phpunit-bridge": "^5.4@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Pimple": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Pimple, a simple Dependency Injection Container", - "homepage": "https://pimple.symfony.com", - "keywords": [ - "container", - "dependency injection" - ], - "support": { - "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" - }, - "time": "2021-10-28T11:13:42+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" - }, - { - "name": "sebastian/environment", - "version": "5.1.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:03:51+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-28T06:42:11+00:00" - }, - { - "name": "sebastian/version", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2021-07-28T10:34:58+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": "^8.0" - }, - "platform-dev": [], - "plugin-api-version": "2.6.0" -} diff --git a/src/Configuration.php b/src/Configuration.php index 5d67333..5432277 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -37,6 +37,18 @@ abstract class Configuration */ protected $fallbacks = []; + /** + * Check for the existence of a component with a given name. + * + * @param string $name component name + * + * @return bool true, if a component with the given name has been defined + */ + public function has(string $name): bool + { + return array_key_exists($name, $this->values) || isset($this->factory[$name]); + } + /** * Internally copy configuration state, e.g. from `ContainerFactory` to `Container` * diff --git a/src/ContainerFactory.php b/src/ContainerFactory.php index 0ea7a91..01820e6 100644 --- a/src/ContainerFactory.php +++ b/src/ContainerFactory.php @@ -11,6 +11,16 @@ */ class ContainerFactory extends Configuration { + /** + * @var string[][] map where Requirement ID => list of requirement descriptions + */ + protected $required = []; + + /** + * @var string[] map where Requirement ID => description of provided abstract requirements + */ + protected $provided = []; + public function __construct() {} @@ -62,7 +72,7 @@ public function __construct() * * @throws InvalidArgumentException */ - public function register($name, $func_or_map_or_type = null, $map = []): void + public function register(string $name, $func_or_map_or_type = null, $map = []): void { if (is_callable($func_or_map_or_type)) { // second argument is a creation function @@ -241,19 +251,63 @@ public function ref(string $name): BoxedReference } /** - * Add a packaged configuration (a "provider") to this container. - * - * @see ProviderInterface + * Add a Provider (a packaged unit of bootstrapping) to this Container. * * @param ProviderInterface $provider * * @return void + * + * @see ProviderInterface */ public function add(ProviderInterface $provider): void { $provider->register($this); } + /** + * Defines a Requirement, which will be checked when you call {@see createContainer()}. + * + * Requirements must be fulfilled by either {@see register()} or {@see provides()}. + * + * @param string $requirement Requirement name. + * @param string $description Optional description. + * Displayed in an Exception message on failure. + * + * @see provides() + */ + public function requires(string $requirement, string $description = "") + { + $this->required[$requirement][] = $description; + } + + /** + * Fulfills an abstract Requirement defined by {@see requires()}. + * + * @param string $requirement Requirement name. + * @param string $description Optional description. + * Displayed in an Exception message on failure. + * + * @throws ContainerException if the given Requirement has already been fulfilled + * + * @see requires() + */ + public function provides(string $requirement, string $description = "") + { + if (array_key_exists($requirement, $this->provided)) { + $message = "The following Requirement has already been fulfilled: {$requirement}"; + + $description = $this->provided[$requirement]; + + if ($description) { + $message .= " ($description)"; + } + + throw new ContainerException($message); + } + + $this->provided[$requirement] = $description; + } + /** * Add a fallback container to this container. * @@ -281,9 +335,24 @@ public function registerFallback(ContainerInterface $container): void * Create and bootstrap a new `Container` instance * * @return Container + * + * @throws ContainerException if any Requirements have not been fulfilled */ public function createContainer() { + $messages = []; + + foreach ($this->required as $requirement => $descriptions) { + if (! array_key_exists($requirement, $this->provided) && !$this->has($requirement)) { + $messages[] = "The following Requirement has not been fulfilled: {$requirement}" + . (array_filter($descriptions) ? " (" . implode("; ", $descriptions) . ")" : ""); + } + } + + if ($messages) { + throw new ContainerException(implode("\n", $messages)); + } + return new Container($this); } } diff --git a/src/ProviderInterface.php b/src/ProviderInterface.php index 5b92313..526d138 100644 --- a/src/ProviderInterface.php +++ b/src/ProviderInterface.php @@ -12,9 +12,9 @@ interface ProviderInterface /** * Registers services and components with a given `ContainerFactory` * - * @param ContainerFactory $container + * @param ContainerFactory $factory * * @return void */ - public function register(ContainerFactory $container); + public function register(ContainerFactory $factory); } diff --git a/test/fixtures.php b/test/fixtures.php index 6285d8c..3fa4d07 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -96,15 +96,15 @@ public function __construct(UserRepository $users) class TestProvider implements ProviderInterface { - public function register(ContainerFactory $container) + public function register(ContainerFactory $factory) { - $container->set('cache_path', '/tmp/cache'); + $factory->set('cache_path', '/tmp/cache'); - $container->register(CacheProvider::class, function ($cache_path) { + $factory->register(CacheProvider::class, function ($cache_path) { return new FileCache($cache_path); }); - $container->register(UserRepository::class, function (CacheProvider $cache) { + $factory->register(UserRepository::class, function (CacheProvider $cache) { return new UserRepository($cache); }); } diff --git a/test/test.php b/test/test.php index 854d73e..3d71535 100644 --- a/test/test.php +++ b/test/test.php @@ -609,6 +609,107 @@ function (CacheProvider $provider) { } ); +test( + 'can check abstract requirements', + function () { + $factory = new ContainerFactory(); + + $factory->requires("duck", "it really needs a duck"); + + // The same Requirement may be defined more than once: + $factory->requires("duck", "it really, really needs a duck"); + + expect( + ContainerException::class, + "should throw for missing requirement", + function () use ($factory) { + $factory->createContainer(); + }, + [ + "/requirement has not been fulfilled/i", + "/duck \(it really needs a duck; it really, really needs a duck\)/i", + ] + ); + + $factory->provides("duck", "add that missing duck"); + + ok($factory->createContainer() instanceof Container); + + expect( + ContainerException::class, + "should throw on attempt to provide a requirement twice", + function () use ($factory) { + $factory->provides("duck", "add another duck"); + }, + ["/requirement has already been fulfilled/i", "/duck \(add that missing duck\)/i"] + ); + } +); + +test( + 'can check component requirements', + function () { + $factory = new ContainerFactory(); + + // component defined using register() in TestProvider + $factory->requires(CacheProvider::class, "requires a cache"); + + // component defined using set() in TestProvider + $factory->requires("cache_path", "requires a cache path"); + + expect( + ContainerException::class, + "should throw for missing requirement", + function () use ($factory) { + $factory->createContainer(); + }, + [ + "/requirement has not been fulfilled/i", + "/CacheProvider \(requires a cache\)/i", + "/cache_path \(requires a cache path\)/i" + ] + ); + + $factory->add(new TestProvider()); + + ok($factory->createContainer() instanceof Container); + } +); + +test( + 'abstract requirements cannot fulfill component dependencies', + function () { + $factory = new ContainerFactory(); + + $factory->requires("cake", "I want cake!"); + + expect( + ContainerException::class, + "should throw for missing requirement", + function () use ($factory) { + $factory->createContainer(); + }, + "/requirement has not been fulfilled/i" + ); + + $factory->provides("cake", "I can haz cake."); + + $container = $factory->createContainer(); + + expect( + NotFoundException::class, + "should throw for missing dependency", + function () use ($container) { + $container->get("cake"); + } + ); + + $factory->set("cake", "yum"); + + eq($factory->createContainer()->get("cake"), "yum", "does not conflict with component dependencies"); + } +); + test( 'internal reflection guard clauses', function () {