Repository: yiisoft/di Branch: master Commit: 10ffafe94415 Files: 105 Total size: 227.9 KB Directory structure: gitextract_8vkzrttm/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ ├── dependabot.yml │ └── workflows/ │ ├── bc.yml │ ├── bechmark.yml │ ├── build.yml │ ├── composer-require-checker.yml │ ├── mutation.yml │ ├── rector-cs.yml │ └── static.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── .phpstorm.meta.php ├── .phpunit-watcher.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── benchmarks.md ├── composer.json ├── docs/ │ └── internals.md ├── infection.json.dist ├── phpbench.json ├── phpcs.xml.dist ├── phpunit.xml.dist ├── psalm.xml ├── rector.php ├── src/ │ ├── BuildingException.php │ ├── CompositeContainer.php │ ├── CompositeNotFoundException.php │ ├── Container.php │ ├── ContainerConfig.php │ ├── ContainerConfigInterface.php │ ├── ExtensibleService.php │ ├── Helpers/ │ │ ├── DefinitionNormalizer.php │ │ └── DefinitionParser.php │ ├── NotFoundException.php │ ├── Reference/ │ │ └── TagReference.php │ ├── ServiceProviderInterface.php │ └── StateResetter.php ├── tests/ │ ├── Benchmark/ │ │ ├── ContainerBench.php │ │ └── ContainerMethodHasBench.php │ ├── Support/ │ │ ├── A.php │ │ ├── B.php │ │ ├── Car.php │ │ ├── CarExtensionProvider.php │ │ ├── CarFactory.php │ │ ├── CarProvider.php │ │ ├── ColorInterface.php │ │ ├── ColorPink.php │ │ ├── ColorRed.php │ │ ├── ConstructorTestClass.php │ │ ├── ContainerInterfaceExtensionProvider.php │ │ ├── Cycle/ │ │ │ ├── Chicken.php │ │ │ └── Egg.php │ │ ├── EngineFactory.php │ │ ├── EngineInterface.php │ │ ├── EngineMarkOne.php │ │ ├── EngineMarkTwo.php │ │ ├── EngineStorage.php │ │ ├── Garage.php │ │ ├── GearBox.php │ │ ├── InvokableCarFactory.php │ │ ├── MethodTestClass.php │ │ ├── NonPsrContainer.php │ │ ├── NullCarExtensionProvider.php │ │ ├── NullableConcreteDependency.php │ │ ├── OptionalConcreteDependency.php │ │ ├── PropertyTestClass.php │ │ ├── SportCar.php │ │ ├── StaticFactory.php │ │ ├── TreeItem.php │ │ ├── UnionTypeInConstructorFirstTypeInParamResolvable.php │ │ ├── UnionTypeInConstructorParamNotResolvable.php │ │ ├── UnionTypeInConstructorSecondParamNotResolvable.php │ │ ├── UnionTypeInConstructorSecondTypeInParamResolvable.php │ │ └── VariadicConstructor.php │ └── Unit/ │ ├── BuildingExceptionTest.php │ ├── CompositeContainerTest.php │ ├── CompositePsrContainerOverLeagueTest.php │ ├── CompositePsrContainerOverYiisoftTest.php │ ├── CompositePsrContainerTestAbstract.php │ ├── Container/ │ │ └── DependencyFromDelegate/ │ │ ├── Car.php │ │ ├── DependencyFromDelegateTest.php │ │ ├── Engine.php │ │ └── EngineInterface.php │ ├── ContainerTest.php │ ├── Helpers/ │ │ └── DefinitionParserTest.php │ ├── LeaguePsrContainerTest.php │ ├── NotFoundExceptionTest.php │ ├── PsrContainerTestAbstract.php │ ├── Reference/ │ │ └── TagReference/ │ │ ├── Resolve/ │ │ │ ├── A.php │ │ │ ├── B.php │ │ │ ├── Main.php │ │ │ └── TagReferenceResolveTest.php │ │ └── TagReferenceTest.php │ ├── ServiceProviderTest.php │ ├── StateResetterTest.php │ └── YiisoftPsrContainerTest.php └── tools/ ├── .gitignore ├── infection/ │ └── composer.json └── psalm/ └── composer.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.php] ij_php_space_before_short_closure_left_parenthesis = true ij_php_space_after_type_cast = true [*.yml] indent_size = 2 ================================================ FILE: .gitattributes ================================================ # Autodetect text files * text=auto eol=lf # ...Unless the name matches the following overriding patterns # Definitively text files *.php text *.css text *.js text *.txt text *.md text *.xml text *.json text *.bat text *.sql text *.yml text # Ensure those won't be messed up with *.png binary *.jpg binary *.gif binary *.ttf binary # Ignore some meta files when creating an archive of this repository /.github export-ignore /.editorconfig export-ignore /.gitattributes export-ignore /.gitignore export-ignore /phpunit.xml.dist export-ignore /tests export-ignore /docs export-ignore # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ /CHANGELOG.md merge=union ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Yii Contributor Code of Conduct ## Our Pledge As contributors and maintainers of this project, and in order to keep Yii community open and welcoming, we ask to respect all community members. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Core team members are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Core team members have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing a project or community include using an official e-mail address, posting via an official social media account, within project GitHub, official forum or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting core team members. All complaints will be reviewed and investigated promptly and fairly. All core team members are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Core team members will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from core team members, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Prerequisites - [Yii goal and values](https://github.com/yiisoft/docs/blob/master/001-yii-values.md) - [Namespaces](https://github.com/yiisoft/docs/blob/master/004-namespaces.md) - [Git commit messages](https://github.com/yiisoft/docs/blob/master/006-git-commit-messages.md) - [Exceptions](https://github.com/yiisoft/docs/blob/master/007-exceptions.md) - [Interfaces](https://github.com/yiisoft/docs/blob/master/008-interfaces.md) # Getting started Since Yii 3 consists of many packages, we have a [special development tool](https://github.com/yiisoft/docs/blob/master/005-development-tool.md). 1. [Clone the repository](https://github.com/yiisoft/yii-dev-tool). 2. [Set up your own fork](https://github.com/yiisoft/yii-dev-tool#using-your-own-fork). 3. Now you are ready. Fork any package listed in `packages.php` and do `./yii-dev install username/package`. If you don't have any particular package in mind to start with: - [Check roadmap](https://github.com/yiisoft/docs/blob/master/003-roadmap.md). - Check package issues at github. Usually there are some. - Ask @samdark. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ | Q | A | ------------- | --- | Is bugfix? | ✔️/❌ | New feature? | ✔️/❌ | Breaks BC? | ✔️/❌ | Fixed issues | comma-separated list of tickets # fixed by the PR, if any ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy Please use the [security issue form](https://www.yiiframework.com/security) to report to us any security issue you find in Yii. DO NOT use the issue tracker or discuss it in the public forum as it will cause more damage than help. Please note that as a non-commercial OpenSource project we are not able to pay bounties at the moment. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # Maintain dependencies for GitHub Actions. - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" # Too noisy. See https://github.community/t/increase-if-necessary-for-github-actions-in-dependabot/179581 open-pull-requests-limit: 0 # Maintain dependencies for Composer - package-ecosystem: "composer" directory: "/" schedule: interval: "daily" versioning-strategy: increase-if-necessary ================================================ FILE: .github/workflows/bc.yml ================================================ on: pull_request: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'phpunit.xml.dist' - 'psalm.xml' push: branches: ['master'] paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'phpunit.xml.dist' - 'psalm.xml' name: backwards compatibility jobs: roave_bc_check: uses: yiisoft/actions/.github/workflows/bc.yml@master with: os: >- ['ubuntu-latest'] php: >- ['8.4'] ================================================ FILE: .github/workflows/bechmark.yml ================================================ on: pull_request: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'psalm.xml' push: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'psalm.xml' name: bechmark jobs: phpbench: uses: yiisoft/actions/.github/workflows/phpbench.yml@master with: os: >- ['ubuntu-latest', 'windows-latest'] php: >- ['8.1'] ================================================ FILE: .github/workflows/build.yml ================================================ on: pull_request: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'psalm.xml' push: branches: ['master'] paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'psalm.xml' name: build jobs: phpunit: uses: yiisoft/actions/.github/workflows/phpunit.yml@master secrets: codecovToken: ${{ secrets.CODECOV_TOKEN }} with: os: >- ['ubuntu-latest', 'windows-latest'] php: >- ['8.1', '8.2', '8.3', '8.4', '8.5'] ================================================ FILE: .github/workflows/composer-require-checker.yml ================================================ on: pull_request: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'phpunit.xml.dist' - 'psalm.xml' push: branches: ['master'] paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'phpunit.xml.dist' - 'psalm.xml' name: Composer require checker jobs: composer-require-checker: uses: yiisoft/actions/.github/workflows/composer-require-checker.yml@master with: os: >- ['ubuntu-latest'] php: >- ['8.1', '8.2', '8.3', '8.4', '8.5'] ================================================ FILE: .github/workflows/mutation.yml ================================================ on: pull_request: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'psalm.xml' push: branches: ['master'] paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'psalm.xml' name: mutation test jobs: mutation: uses: yiisoft/actions/.github/workflows/infection.yml@master with: os: >- ['ubuntu-latest'] php: >- ['8.5'] infection-args: "--ignore-msi-with-no-mutations" secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} ================================================ FILE: .github/workflows/rector-cs.yml ================================================ name: Rector + PHP CS Fixer on: pull_request_target: paths: - 'src/**' - 'tests/**' - '.github/workflows/rector-cs.yml' - 'composer.json' - 'rector.php' - '.php-cs-fixer.dist.php' permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: rector: uses: yiisoft/actions/.github/workflows/rector-cs.yml@master secrets: token: ${{ secrets.YIISOFT_GITHUB_TOKEN }} with: repository: ${{ github.event.pull_request.head.repo.full_name }} php: '8.1' ================================================ FILE: .github/workflows/static.yml ================================================ on: pull_request: paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'phpunit.xml.dist' push: branches: ['master'] paths-ignore: - 'docs/**' - 'README.md' - 'CHANGELOG.md' - '.gitignore' - '.gitattributes' - 'infection.json.dist' - 'phpunit.xml.dist' name: static analysis jobs: psalm: uses: yiisoft/actions/.github/workflows/psalm.yml@master with: os: >- ['ubuntu-latest'] php: >- ['8.1', '8.2', '8.3', '8.4'] ================================================ FILE: .gitignore ================================================ # phpstorm project files .idea # netbeans project files nbproject # zend studio for eclipse project files .buildpath .project .settings # windows thumbnail cache Thumbs.db # composer vendor dir /vendor /composer.lock # composer itself is not needed composer.phar # Mac DS_Store Files .DS_Store # PhpUnit /phpunit.phar /phpunit.xml /.phpunit.cache # Static analysis analysis.txt # Code coverage HTML /coverage # PHP CS Fixer /.php-cs-fixer.cache /.php-cs-fixer.php ================================================ FILE: .php-cs-fixer.dist.php ================================================ in([ __DIR__ . '/src', __DIR__ . '/tests', ]); return (new Config()) ->setRiskyAllowed(true) ->setParallelConfig(ParallelConfigFactory::detect()) ->setRules([ '@PER-CS3.0' => true, 'no_unused_imports' => true, 'ordered_class_elements' => true, 'class_attributes_separation' => ['elements' => ['method' => 'one']], 'declare_strict_types' => true, 'native_function_invocation' => true, 'native_constant_invocation' => true, 'fully_qualified_strict_types' => [ 'import_symbols' => true ], 'global_namespace_import' => [ 'import_classes' => true, 'import_constants' => true, 'import_functions' => true, ], ]) ->setFinder($finder); ================================================ FILE: .phpstorm.meta.php ================================================ '@', ]) ); } ================================================ FILE: .phpunit-watcher.yml ================================================ watch: directories: - src - tests fileMask: '*.php' notifications: passingTests: false failingTests: false phpunit: binaryPath: vendor/bin/phpunit timeout: 180 ================================================ FILE: CHANGELOG.md ================================================ # Yii Dependency Injection Change Log ## 1.4.2 under development - Enh #397: Explicitly import functions in "use" section (@mspirkov) ## 1.4.1 December 01, 2025 - Enh #393: Add PHP 8.5 support (@vjik) ## 1.4.0 May 30, 2025 - New #380: Add `TagReference::id()` method (@vjik) - Chg #390: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@vjik) - Enh #324: Make `BuildingException` and `NotFoundException` friendly (@np25071984) - Enh #384: Make `$config` parameter in `Container` constructor optional (@np25071984) - Enh #387: Improve container performance (@samdark) - Bug #390: Explicitly mark nullable parameters (@vjik) ## 1.3.0 October 14, 2024 - Enh #353: Add shortcut for tag reference #333 (@xepozz) - Enh #356: Improve usage `NotFoundException` for cases with definitions (@vjik) - Enh #364: Minor refactoring to improve performance of container (@samdark) - Enh #375: Raise minimum PHP version to `^8.1` and refactor code (@vjik) - Enh #376: Add default value `true` for parameter of `ContainerConfig::withStrictMode()` and `ContainerConfig::withValidate()` methods (@vjik) ## 1.2.1 December 23, 2022 - Chg #316: Fix exception messages (@xepozz) - Bug #317: Fix delegated container (@xepozz) ## 1.2.0 November 05, 2022 - Chg #310: Adopt to `yiisoft/definition` version `^3.0` (@vjik) - Enh #308: Raise minimum PHP version to `^8.0` and refactor code (@xepozz, @vjik) ## 1.1.0 June 24, 2022 - Chg #263: Raise minimal required version of `psr/container` to `^1.1|^2.0` (@xepozz, @vjik) ## 1.0.3 June 17, 2022 - Enh #302: Improve performance collecting tags (samdark) - Enh #303: Add support for `yiisoft/definitions` version `^2.0` (@vjik) ## 1.0.2 February 14, 2022 - Bug #297: Fix method name `TagHelper::extractTagFromAlias` (@rustamwin) ## 1.0.1 December 21, 2021 - Bug #293: Fix `ExtensibleService` normalization bug (@yiiliveext) ## 1.0.0 December 03, 2021 - Initial release. ================================================ FILE: LICENSE.md ================================================ Copyright © 2008 by Yii Software () All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Yii Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================

Yii

Yii Dependency Injection


[![Latest Stable Version](https://poser.pugx.org/yiisoft/di/v)](https://packagist.org/packages/yiisoft/di) [![Total Downloads](https://poser.pugx.org/yiisoft/di/downloads)](https://packagist.org/packages/yiisoft/di) [![Build status](https://github.com/yiisoft/di/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/di/actions/workflows/build.yml) [![Code coverage](https://codecov.io/gh/yiisoft/di/graph/badge.svg?token=P8W1UTwgQt)](https://codecov.io/gh/yiisoft/di) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fdi%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/di/master) [![static analysis](https://github.com/yiisoft/di/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/di/actions?query=workflow%3A%22static+analysis%22) [![type-coverage](https://shepherd.dev/github/yiisoft/di/coverage.svg)](https://shepherd.dev/github/yiisoft/di) [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) container that's able to instantiate and configure classes resolving dependencies. ## Features - [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible. - Supports property injection, constructor injection, and method injection. - Detects circular references. - Accepts array definitions. You can use it with mergeable configs. - Provides optional autoload fallback for classes without explicit definition. - Allows delegated lookup and has a composite container. - Supports aliasing. - Supports service providers. - Has state resetter for long-running workers serving many requests, such as [RoadRunner](https://roadrunner.dev/) or [Swoole](https://www.swoole.co.uk/). - Supports container delegates. - Does auto-wiring. > [!NOTE] > The container contains only shared instances. If you need a factory, use the dedicated [yiisoft/factory](https://github.com/yiisoft/factory) package. ## Requirements - PHP 8.1 - 8.5. - `Multibyte String` PHP extension. ## Installation You could install the package with composer: ```shell composer require yiisoft/di ``` ## Using the container Usage of the DI container is simple: You first initialize it with an array of *definitions*. The array keys are usually interface names. It will then use these definitions to create an object whenever the application requests that type. This happens, for example, when fetching a type directly from the container somewhere in the application. But objects are also created implicitly if a definition has a dependency on another definition. Usually one uses a single container for the whole application. It's often configured either in the entry script such as `index.php` or a configuration file: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withDefinitions($definitions); $container = new Container($config); ``` You could store the definitions in a `.php` file that returns an array: ```php return [ // resolve EngineMarkOne dependencies automatically EngineInterface::class => EngineMarkOne::class, // full definition MyServiceInterface::class => [ 'class' => MyService::class, // call the constructor, pass named argument "amount" '__construct()' => [ 'amount' => 42, 'db' => Reference::to(SecondaryConnection::class), // instance of another dependency ], // set a public property '$name' => 'Alex', // call a public method 'setDiscount()' => [10], ], // closure for complicated cases AnotherServiceInterface::class => static function(ConnectionInterface $db) { return new AnotherService($db); }, // factory MyObjectInterface::class => fn () => MyFactory::create('args'), // static call MyObjectInterface2::class => [MyFactory::class, 'create'], // direct instance MyInterface::class => new MyClass(), ]; ``` You can define an object in several ways: - In the simple case, an interface definition maps an id to a particular class. - A full definition describes how to instantiate a class in more detail: - `class` has the name of the class to instantiate. - `__construct()` holds an array of constructor arguments. - The rest of the config is property values (prefixed with `$`) and method calls, postfixed with `()`. They're set/called in the order they appear in the array. - Closures are useful if instantiation is tricky and can be better done in code. When using these, arguments are auto-wired by type. `ContainerInterface` could be used to get current container instance. - If it's even more complicated, it's a good idea to move such a code into a factory and reference it as a static call. - While it's usually not a good idea, you can also set an already instantiated object into the container. See [yiisoft/definitions](https://github.com/yiisoft/definitions) for more information. After you configure the container, you can obtain a service via `get()`: ```php /** @var \Yiisoft\Di\Container $container */ $object = $container->get('interface_name'); ``` Note, however, that it's bad practice using a container directly. It's much better to rely on auto-wiring as provided by the Injector available from the [yiisoft/injector](https://github.com/yiisoft/injector) package. ## Using aliases The DI container supports aliases via the `Yiisoft\Definitions\Reference` class. This way you can retrieve objects by a more handy name: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, 'engine_one' => EngineInterface::class, ]); $container = new Container($config); $object = $container->get('engine_one'); ``` ## Using class aliases for specific configuration To define another instance of a class with specific configuration, you can use native PHP `class_alias()`: ```php class_alias(Yiisoft\Db\Pgsql\Connection::class, 'MyPgSql'); $config = ContainerConfig::create() ->withDefinitions([ MyPgSql::class => [ ... ] ]); $container = new Container($config); $object = $container->get(MyPgSql::class); ``` It could be then conveniently used by type-hinting: ```php final class MyService { public function __construct(MyPgSql $myPgSql) { // ... } } ``` ## Composite containers A composite container combines many containers in a single container. When using this approach, you should fetch objects only from the composite container. ```php use Yiisoft\Di\CompositeContainer; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $composite = new CompositeContainer(); $carConfig = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, CarInterface::class => Car::class ]); $carContainer = new Container($carConfig); $bikeConfig = ContainerConfig::create() ->withDefinitions([ BikeInterface::class => Bike::class ]); $bikeContainer = new Container($bikeConfig); $composite->attach($carContainer); $composite->attach($bikeContainer); // Returns an instance of a `Car` class. $car = $composite->get(CarInterface::class); // Returns an instance of a `Bike` class. $bike = $composite->get(BikeInterface::class); ``` Note that containers attached earlier override dependencies of containers attached later. ```php use Yiisoft\Di\CompositeContainer; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $carConfig = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, CarInterface::class => Car::class ]); $carContainer = new Container($carConfig); $composite = new CompositeContainer(); $composite->attach($carContainer); // Returns an instance of a `Car` class. $car = $composite->get(CarInterface::class); // Returns an instance of a `EngineMarkOne` class. $engine = $car->getEngine(); $engineConfig = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkTwo::class, ]); $engineContainer = new Container($engineConfig); $composite = new CompositeContainer(); $composite->attach($engineContainer); $composite->attach($carContainer); // Returns an instance of a `Car` class. $car = $composite->get(CarInterface::class); // Returns an instance of a `EngineMarkTwo` class. $engine = $composite->get(EngineInterface::class); ``` ## Using service providers A service provider is a special class that's responsible for providing complex services or groups of dependencies for the container and extensions of existing services. A provider should extend from `Yiisoft\Di\ServiceProviderInterface` and must contain a `getDefinitions()` and `getExtensions()` methods. It should only provide services for the container and therefore should only contain code related to this task. It should *never* implement any business logic or other functionality such as environment bootstrap or applying changes to a database. The `getExtensions()` method allows implementing the decorator pattern by wrapping existing services with additional functionality. A typical service provider could look like: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ServiceProviderInterface; class CarFactoryProvider extends ServiceProviderInterface { public function getDefinitions(): array { return [ CarFactory::class => [ 'class' => CarFactory::class, '$color' => 'red', ], EngineInterface::class => SolarEngine::class, WheelInterface::class => [ 'class' => Wheel::class, '$color' => 'black', ], CarInterface::class => [ 'class' => BMW::class, '$model' => 'X5', ], ]; } public function getExtensions(): array { return [ // Note that Garage should already be defined in a container Garage::class => function(ContainerInterface $container, Garage $garage) { $car = $container ->get(CarFactory::class) ->create(); $garage->setCar($car); return $garage; } ]; } } ``` Here you created a service provider responsible for bootstrapping of a car factory with all its dependencies. An extension is callable that returns a modified service object. In this case you get existing `Garage` service and put a car into the garage by calling the method `setCar()`. Thus, before applying this provider, you had an empty garage and with the help of the extension you fill it. To add this service provider to a container, you can pass either its class or a configuration array in the extra config: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withProviders([CarFactoryProvider::class]); $container = new Container($config); ``` When you add a service provider, DI calls its `getDefinitions()` and `getExtensions()` methods *immediately* and both services and their extensions get registered into the container. ### Using service providers for decorator pattern Service provider extensions are a powerful feature that allows implementing the decorator pattern. This lets you wrap existing services with additional functionality without modifying their original implementation. Here's an example of using the decorator pattern to add logging to an existing mailer service: ```php use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Yiisoft\Di\ServiceProviderInterface; interface MailerInterface { public function send(string $to, string $subject, string $body): void; } class Mailer implements MailerInterface { public function send(string $to, string $subject, string $body): void { // Original mailer implementation // Sends email via SMTP or external service } } class LoggingMailerDecorator implements MailerInterface { public function __construct( private MailerInterface $mailer, private LoggerInterface $logger ) { } public function send(string $to, string $subject, string $body): void { $this->logger->info("Sending email to {$to}"); $this->mailer->send($to, $subject, $body); $this->logger->info("Email sent to {$to}"); } } class MailerDecoratorProvider implements ServiceProviderInterface { public function getDefinitions(): array { return []; } public function getExtensions(): array { return [ MailerInterface::class => static function (ContainerInterface $container, MailerInterface $mailer) { // Wrap the original mailer with logging decorator return new LoggingMailerDecorator($mailer, $container->get(LoggerInterface::class)); } ]; } } ``` In this example, the extension receives the original `MailerInterface` instance and wraps it with `LoggingMailerDecorator`, which adds logging before and after sending emails. The decorator pattern allows you to add cross-cutting concerns like logging, caching, or monitoring without changing the original service implementation. ## Container tags You can tag services in the following way: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withDefinitions([ BlueCarService::class => [ 'class' => BlueCarService::class, 'tags' => ['car'], ], RedCarService::class => [ 'definition' => fn () => new RedCarService(), 'tags' => ['car'], ], ]); $container = new Container($config); ``` Another way to tag services is setting tags via container constructor: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withDefinitions([ BlueCarService::class => [ 'class' => BlueCarService::class, ], RedCarService::class => fn () => new RedCarService(), ]) ->withTags([ // "car" tag has references to both blue and red cars 'car' => [BlueCarService::class, RedCarService::class] ]); $container = new Container($config); ``` ### Getting tagged services You can get tagged services from the container in the following way: ```php $container->get(\Yiisoft\Di\Reference\TagReference::id('car')); ``` The result is an array that has two instances: `BlueCarService` and `RedCarService`. ### Using tagged services in configuration Use `TagReference` to get tagged services in configuration: ```php [ Garage::class => [ '__construct()' => [ \Yiisoft\Di\Reference\TagReference::to('car'), ], ], ], ``` ## Resetting services state Despite stateful services isn't a great practice, these are often inevitable. When you build long-running applications with tools like [Swoole](https://www.swoole.co.uk/) or [RoadRunner](https://roadrunner.dev/) you should reset the state of such services every request. For this purpose you can use `StateResetter` with resetters callbacks: ```php $resetter = new StateResetter($container); $resetter->setResetters([ MyServiceInterface::class => function () { $this->reset(); // a method of MyServiceInterface }, ]); ``` The callback has access to the private and protected properties of the service instance, so you can set the initial state of the service efficiently without creating a new instance. You should trigger the reset itself after each request-response cycle. For RoadRunner, it would look like the following: ```php while ($request = $psr7->acceptRequest()) { $response = $application->handle($request); $psr7->respond($response); $application->afterEmit($response); $container ->get(\Yiisoft\Di\StateResetter::class) ->reset(); gc_collect_cycles(); } ``` ### Setting resetters in definitions You define the reset state for each service by providing "reset" callback in the following way: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => function () { $this->number = 42; }, ], ]); $container = new Container($config); ``` Note: resetters from definitions work only if you don't set `StateResetter` in definition or service providers. ### Configuring `StateResetter` manually To manually add resetters or in case you use Yii DI composite container with a third party container that doesn't support state reset natively, you could configure state resetter separately. The following example is PHP-DI: ```php MyServiceInterface::class => function () { // ... }, StateResetter::class => function (ContainerInterface $container) { $resetter = new StateResetter($container); $resetter->setResetters([ MyServiceInterface::class => function () { $this->reset(); // a method of MyServiceInterface }, ]); return $resetter; } ``` ## Specifying metadata for non-array definitions To specify some metadata, such as in cases of "resetting services state" or "container tags," for non-array definitions, you could use the following syntax: ```php LogTarget::class => [ 'definition' => static function (LoggerInterface $logger) use ($params) { $target = ... return $target; }, 'reset' => function () use ($params) { ... }, ], ``` Now you've explicitly moved the definition itself to "definition" key. ## Delegates Each delegate is a callable returning a container instance that's used in case DI can't find a service in a primary container: ```php function (ContainerInterface $container): ContainerInterface { } ``` To configure delegates use extra config: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withDelegates([ function (ContainerInterface $container): ContainerInterface { // ... } ]); $container = new Container($config); ``` ## Tuning for production By default, the container validates definitions right when they're set. In the production environment, it makes sense to turn it off: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withValidate(false); $container = new Container($config); ``` ## Strict mode Container may work in a strict mode, that's when you should define everything in the container explicitly. To turn it on, use the following code: ```php use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; $config = ContainerConfig::create() ->withStrictMode(true); $container = new Container($config); ``` ## Documentation - [Internals](docs/internals.md) If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). ## License The Yii Dependency Injection is free software. It is released under the terms of the BSD License. Please see [`LICENSE`](./LICENSE.md) for more information. Maintained by [Yii Software](https://www.yiiframework.com/). ## Support the project [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) ## Follow updates [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) ================================================ FILE: benchmarks.md ================================================ DI benchmark report =================== ### suite: 1343bd6191c6668ccf8fb4cf1a51a7b61159c825, date: 2020-04-06, stime: 20:48:28 benchmark | subject | set | revs | its | mem_peak | best | mean | mode | worst | stdev | rstdev | diff --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- ContainerBench | benchSequentialLookups | 0 | 1000 | 5 | 1,460,456b | 615.235μs | 625.586μs | 621.205μs | 636.719μs | 7.948μs | 1.27% | 1.00x ContainerBench | benchSequentialLookups | 1 | 1000 | 5 | 1,467,608b | 634.765μs | 645.507μs | 642.453μs | 661.132μs | 8.691μs | 1.35% | 1.03x ContainerBench | benchSequentialLookups | 2 | 1000 | 5 | 1,467,752b | 632.813μs | 638.086μs | 636.211μs | 646.484μs | 4.605μs | 0.72% | 1.02x ContainerBench | benchRandomLookups | 0 | 1000 | 5 | 1,460,456b | 609.375μs | 625.000μs | 633.784μs | 639.649μs | 12.628μs | 2.02% | 1.00x ContainerBench | benchRandomLookups | 1 | 1000 | 5 | 1,467,608b | 644.531μs | 650.391μs | 652.084μs | 657.227μs | 4.744μs | 0.73% | 1.04x ContainerBench | benchRandomLookups | 2 | 1000 | 5 | 1,467,752b | 638.672μs | 649.219μs | 650.291μs | 658.203μs | 6.250μs | 0.96% | 1.04x ContainerBench | benchRandomLookupsComposite | 0 | 1000 | 5 | 24,786,136b | 699.219μs | 708.398μs | 711.499μs | 715.820μs | 5.943μs | 0.84% | 1.13x ContainerBench | benchRandomLookupsComposite | 1 | 1000 | 5 | 24,780,496b | 743.164μs | 753.125μs | 748.496μs | 772.461μs | 10.268μs | 1.36% | 1.20x ContainerBench | benchRandomLookupsComposite | 2 | 1000 | 5 | 24,780,640b | 739.257μs | 747.071μs | 744.033μs | 755.860μs | 6.020μs | 0.81% | 1.20x ================================================ FILE: composer.json ================================================ { "name": "yiisoft/di", "type": "library", "description": "Yii DI container", "keywords": [ "di", "dependency", "injection", "container", "injector", "autowiring", "psr-11" ], "homepage": "https://www.yiiframework.com/", "license": "BSD-3-Clause", "support": { "issues": "https://github.com/yiisoft/di/issues?state=open", "source": "https://github.com/yiisoft/di", "forum": "https://www.yiiframework.com/forum/", "wiki": "https://www.yiiframework.com/wiki/", "irc": "ircs://irc.libera.chat:6697/yii", "chat": "https://t.me/yii3en" }, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/yiisoft" }, { "type": "github", "url": "https://github.com/sponsors/yiisoft" } ], "require": { "php": "8.1 - 8.5", "ext-mbstring": "*", "psr/container": "^1.1 || ^2.0", "yiisoft/definitions": "^3.0", "yiisoft/friendly-exception": "^1.1.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.92.5", "bamarni/composer-bin-plugin": "^1.8.3", "league/container": "^5.1.0", "maglnet/composer-require-checker": "^4.7.1", "phpbench/phpbench": "^1.4.1", "phpunit/phpunit": "^10.5.46", "rector/rector": "^2.0.17", "spatie/phpunit-watcher": "^1.24", "yiisoft/injector": "^1.2.1", "yiisoft/test-support": "^3.1" }, "suggest": { "yiisoft/injector": "^1.0", "phpbench/phpbench": "To run benchmarks." }, "provide": { "psr/container-implementation": "1.0.0" }, "autoload": { "psr-4": { "Yiisoft\\Di\\": "src" } }, "autoload-dev": { "psr-4": { "Yiisoft\\Di\\Tests\\": "tests" } }, "scripts": { "test": "phpunit --testdox --no-interaction", "test-watch": "phpunit-watcher watch" }, "scripts-descriptions": { "test": "Run all tests" }, "extra": { "bamarni-bin": { "bin-links": true, "target-directory": "tools", "forward-command": true } }, "config": { "sort-packages": true, "allow-plugins": { "bamarni/composer-bin-plugin": true, "composer/package-versions-deprecated": true } } } ================================================ FILE: docs/internals.md ================================================ # Internals ## Further reading - [Martin Fowler's article](https://martinfowler.com/articles/injection.html). ## Benchmarks To run benchmarks execute the next command ```shell ./vendor/bin/phpbench run ``` Result example ```text \Yiisoft\Di\Tests\Benchmark\ContainerBench benchConstructStupid....................I4 [μ Mo]/r: 438.566 435.190 (μs) [μSD μRSD]/r: 9.080μs 2.07% benchConstructSmart.....................I4 [μ Mo]/r: 470.958 468.942 (μs) [μSD μRSD]/r: 2.848μs 0.60% benchSequentialLookups # 0..............R5 I4 [μ Mo]/r: 2,837.000 2,821.636 (μs) [μSD μRSD]/r: 34.123μs 1.20% benchSequentialLookups # 1..............R1 I0 [μ Mo]/r: 12,253.600 12,278.859 (μs) [μSD μRSD]/r: 69.087μs 0.56% benchRandomLookups # 0..................R5 I4 [μ Mo]/r: 3,142.200 3,111.290 (μs) [μSD μRSD]/r: 87.639μs 2.79% benchRandomLookups # 1..................R1 I2 [μ Mo]/r: 13,298.800 13,337.170 (μs) [μSD μRSD]/r: 103.891μs 0.78% benchRandomLookupsComposite # 0.........R1 I3 [μ Mo]/r: 3,351.600 3,389.104 (μs) [μSD μRSD]/r: 72.516μs 2.16% benchRandomLookupsComposite # 1.........R1 I4 [μ Mo]/r: 13,528.200 13,502.881 (μs) [μSD μRSD]/r: 99.997μs 0.74% \Yiisoft\Di\Tests\Benchmark\ContainerMethodHasBench benchPredefinedExisting.................R1 I4 [μ Mo]/r: 0.115 0.114 (μs) [μSD μRSD]/r: 0.001μs 1.31% benchUndefinedExisting..................R5 I4 [μ Mo]/r: 0.436 0.432 (μs) [μSD μRSD]/r: 0.008μs 1.89% benchUndefinedNonexistent...............R5 I4 [μ Mo]/r: 0.946 0.942 (μs) [μSD μRSD]/r: 0.006μs 0.59% 8 subjects, 55 iterations, 5,006 revs, 0 rejects, 0 failures, 0 warnings (best [mean mode] worst) = 0.113 [4,483.856 4,486.051] 0.117 (μs) ⅀T: 246,612.096μs μSD/r 43.563μs μRSD/r: 1.336% ``` > **Warning!** > > These summary statistics can be misleading. > You should always verify the individual subject statistics before drawing any conclusions. > **Legend** > > - μ: Mean time taken by all iterations in variant. > - Mo: Mode of all iterations in variant. > - μSD: μ standard deviation. > - μRSD: μ relative standard deviation. > - best: Maximum time of all iterations (minimal of all iterations). > - mean: Mean time taken by all iterations. > - mode: Mode of all iterations. > - worst: Minimum time of all iterations (minimal of all iterations). ### Command examples - Default report for all benchmarks that outputs the result to `CSV-file` ```shell ./vendor/bin/phpbench run --report=default --progress=dots --output=csv_file ``` Generated MD-file example ```text >DI benchmark report >=================== > >### suite: 1343b1dc0589cb4e985036d14b3e12cb430a975b, date: 2020-02-21, stime: 16:02:45 > >benchmark | subject | set | revs | iter | mem_peak | time_rev | comp_z_value | comp_deviation > --- | --- | --- | --- | --- | --- | --- | --- | --- >ContainerBench | benchConstructStupid | 0 | 1000 | 0 | 1,416,784b | 210.938μs | -1.48σ | -1.1% >ContainerBench | benchConstructStupid | 0 | 1000 | 1 | 1,416,784b | 213.867μs | +0.37σ | +0.27% >ContainerBench | benchConstructStupid | 0 | 1000 | 2 | 1,416,784b | 212.890μs | -0.25σ | -0.18% >ContainerBench | benchConstructStupid | 0 | 1000 | 3 | 1,416,784b | 215.820μs | +1.60σ | +1.19% >ContainerBench | benchConstructStupid | 0 | 1000 | 4 | 1,416,784b | 212.891μs | -0.25σ | -0.18% >ContainerBench | benchConstructSmart | 0 | 1000 | 0 | 1,426,280b | 232.422μs | -1.03σ | -0.5% >ContainerBench | benchConstructSmart | 0 | 1000 | 1 | 1,426,280b | 232.422μs | -1.03σ | -0.5% >ContainerBench | benchConstructSmart | 0 | 1000 | 2 | 1,426,280b | 233.398μs | -0.17σ | -0.08% >ContainerBench | benchConstructSmart | 0 | 1000 | 3 | 1,426,280b | 234.375μs | +0.69σ | +0.33% >ContainerBench | benchConstructSmart | 0 | 1000 | 4 | 1,426,280b | 235.351μs | +1.54σ | +0.75% >`... skipped` | `...` | `...` | `...` | `...` | `...` | `...` | `...` | `...` >ContainerMethodHasBench | benchPredefinedExisting | 0 | 1000 | 0 | 1,216,144b | 81.055μs | -0.91σ | -1.19% >ContainerMethodHasBench | benchPredefinedExisting | 0 | 1000 | 1 | 1,216,144b | 83.985μs | +1.83σ | +2.38% >ContainerMethodHasBench | benchPredefinedExisting | 0 | 1000 | 2 | 1,216,144b | 82.032μs | 0.00σ | 0.00% >ContainerMethodHasBench | benchPredefinedExisting | 0 | 1000 | 3 | 1,216,144b | 82.031μs | 0.00σ | 0.00% >ContainerMethodHasBench | benchPredefinedExisting | 0 | 1000 | 4 | 1,216,144b | 81.055μs | -0.91σ | -1.19% >`... skipped` | `...` | `...` | `...` | `...` | `...` | `...` | `...` | `...` ``` > **Legend** > > - benchmark: Benchmark class. > - subject: Benchmark class method. > - set: Set of data (provided by ParamProvider). > - revs: Number of revolutions (represent the number of times that the code is executed). > - iter: Number of iteration. > - mem_peak: (mean) Peak memory used by iteration as retrieved by memory_get_peak_usage. > - time_rev: Mean time taken by all iterations in variant. > - comp_z_value: Z-score. > - comp_deviation: Relative deviation (margin of error). - Aggregate report for the `lookup` group that outputs the result to `console` and `CSV-file` ```shell ./vendor/bin/phpbench run --report=aggregate --progress=dots --output=csv_file --output=console --group=lookup ``` > **Notice** > > Available groups: `construct` `lookup` `has` Generated MD-file example ```text > DI benchmark report > =================== > >### suite: 1343b1d2654a3819c72a96d236302b70a504dac7, date: 2020-02-21, stime: 13:27:32 > >benchmark | subject | set | revs | its | mem_peak | best | mean | mode | worst | stdev | rstdev | diff > --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- >ContainerBench | benchSequentialLookups | 0 | 1000 | 5 | 1,454,024b | 168.945μs | 170.117μs | 169.782μs | 171.875μs | 0.957μs | 0.56% | 1.00x >ContainerBench | benchSequentialLookups | 1 | 1000 | 5 | 1,445,296b | 3,347.656μs | 3,384.961μs | 3,390.411μs | 3,414.062μs | 21.823μs | 0.64% | 19.90x >ContainerBench | benchSequentialLookups | 2 | 1000 | 5 | 1,445,568b | 3,420.898μs | 3,488.477μs | 3,447.260μs | 3,657.227μs | 85.705μs | 2.46% | 20.51x >ContainerBench | benchRandomLookups | 0 | 1000 | 5 | 1,454,024b | 169.922μs | 171.875μs | 171.871μs | 173.828μs | 1.381μs | 0.80% | 1.01x >ContainerBench | benchRandomLookups | 1 | 1000 | 5 | 1,445,296b | 3,353.515μs | 3,389.844μs | 3,377.299μs | 3,446.289μs | 31.598μs | 0.93% | 19.93x >ContainerBench | benchRandomLookups | 2 | 1000 | 5 | 1,445,568b | 3,445.313μs | 3,587.696μs | 3,517.823μs | 3,749.023μs | 115.850μs | 3.23% | 21.09x >ContainerBench | benchRandomLookupsComposite | 0 | 1000 | 5 | 1,454,032b | 297.852μs | 299.610μs | 298.855μs | 302.734μs | 1.680μs | 0.56% | 1.76x >ContainerBench | benchRandomLookupsComposite | 1 | 1000 | 5 | 1,445,880b | 3,684.570μs | 3,708.984μs | 3,695.731μs | 3,762.695μs | 28.297μs | 0.76% | 21.80x >ContainerBench | benchRandomLookupsComposite | 2 | 1000 | 5 | 1,446,152b | 3,668.946μs | 3,721.680μs | 3,727.407μs | 3,765.625μs | 30.881μs | 0.83% | 21.88x ``` > **Legend** > > * benchmark: Benchmark class. > * subject: Benchmark class method. > * set: Set of data (provided by ParamProvider). > * revs: Number of revolutions (represent the number of times that the code is executed). > * its: Number of iterations (one measurement for each iteration). > * mem_peak: (mean) Peak memory used by each iteration as retrieved by memory_get_peak_usage. > * best: Maximum time of all iterations in variant. > * mean: Mean time taken by all iterations in variant. > * mode: Mode of all iterations in variant. > * worst: Minimum time of all iterations in variant. > * stdev: Standard deviation. > * rstdev: The relative standard deviation. > * diff: Difference between variants in a single group. ## Unit testing The package is tested with [PHPUnit](https://phpunit.de/). To run tests: ```shell ./vendor/bin/phpunit ``` ## Mutation testing The package tests are checked with [Infection](https://infection.github.io/) mutation framework with [Infection Static Analysis Plugin](https://github.com/Roave/infection-static-analysis-plugin). To run it: ```shell ./vendor/bin/roave-infection-static-analysis-plugin ``` ## Static analysis The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: ```shell ./vendor/bin/psalm ``` ## Code style Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or use either newest or any specific version of PHP: ```shell ./vendor/bin/rector ``` ## Dependencies Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker) to detect transitive [Composer](https://getcomposer.org/) dependencies. To run the checker, execute the following command: ```shell ./vendor/bin/composer-require-checker ``` ================================================ FILE: infection.json.dist ================================================ { "source": { "directories": [ "src" ] }, "logs": { "text": "php:\/\/stderr", "stryker": { "report": "master" } }, "mutators": { "@default": true } } ================================================ FILE: phpbench.json ================================================ { "runner.bootstrap": "vendor/autoload.php", "runner.path": "tests/Benchmark", "runner.retry_threshold": 3, "report.outputs": { "csv_file": { "extends": "delimited", "delimiter": ",", "file": "benchmarks.csv" } } } ================================================ FILE: phpcs.xml.dist ================================================ ./ ./vendor/* ================================================ FILE: phpunit.xml.dist ================================================ ./tests/Unit ./src ================================================ FILE: psalm.xml ================================================ ================================================ FILE: rector.php ================================================ withPaths([ __DIR__ . '/src', __DIR__ . '/tests', ]) ->withPhpSets(php81: true) ->withRules([ InlineConstructorDefaultToPropertyRector::class, ]) ->withSkip([ ClosureToArrowFunctionRector::class, ArrayToFirstClassCallableRector::class => [ __DIR__ . '/tests/Unit/Helpers/DefinitionParserTest.php', ], ]); ================================================ FILE: src/BuildingException.php ================================================ getMessage() === '' ? $error::class : $error->getMessage(), implode('" -> "', $buildStack === [] ? [$id] : $buildStack), ); parent::__construct($message, 0, $previous); } public function getName(): string { return sprintf('Unable to build "%s" object.', $this->id); } public function getSolution(): ?string { $solution = <<id); } } ================================================ FILE: src/CompositeContainer.php ================================================ $id * @psalm-return ($id is class-string ? T : mixed) */ public function get($id) { /** @psalm-suppress TypeDoesNotContainType */ if (!is_string($id)) { throw new InvalidArgumentException( sprintf( 'ID must be a string, %s given.', get_debug_type($id), ), ); } if ($id === StateResetter::class) { $resetters = []; foreach ($this->containers as $container) { if ($container->has(StateResetter::class)) { $resetters[] = $container->get(StateResetter::class); } } $stateResetter = new StateResetter($this); $stateResetter->setResetters($resetters); return $stateResetter; } if (TagReference::isTagAlias($id)) { $tags = []; foreach ($this->containers as $container) { if (!$container instanceof Container) { continue; } if ($container->has($id)) { /** @psalm-suppress MixedArgument `Container::get()` always return array for tag */ array_unshift($tags, $container->get($id)); } } /** @psalm-suppress MixedArgument `Container::get()` always return array for tag */ return array_merge(...$tags); } foreach ($this->containers as $container) { if ($container->has($id)) { /** @psalm-suppress MixedReturnStatement */ return $container->get($id); } } // Collect details from containers $exceptions = []; foreach ($this->containers as $container) { $hasException = false; try { $container->get($id); } catch (Throwable $t) { $hasException = true; $exceptions[] = [$t, $container]; } finally { if (!$hasException) { $exceptions[] = [ new RuntimeException( 'Container "has()" returned false, but no exception was thrown from "get()".', ), $container, ]; } } } throw new CompositeNotFoundException($exceptions); } public function has($id): bool { /** @psalm-suppress TypeDoesNotContainType */ if (!is_string($id)) { throw new InvalidArgumentException( sprintf( 'ID must be a string, %s given.', get_debug_type($id), ), ); } if ($id === StateResetter::class) { return true; } if (TagReference::isTagAlias($id)) { foreach ($this->containers as $container) { if (!$container instanceof Container) { continue; } if ($container->has($id)) { return true; } } return false; } foreach ($this->containers as $container) { if ($container->has($id)) { return true; } } return false; } /** * Attaches a container to the composite container. */ public function attach(ContainerInterface $container): void { $this->containers[] = $container; } /** * Removes a container from the list of containers. */ public function detach(ContainerInterface $container): void { foreach ($this->containers as $i => $c) { if ($container === $c) { unset($this->containers[$i]); } } } } ================================================ FILE: src/CompositeNotFoundException.php ================================================ $exceptions */ public function __construct(array $exceptions) { $message = ''; foreach ($exceptions as $i => [$exception, $container]) { $containerClass = $container::class; $containerId = spl_object_id($container); $number = $i + 1; $message .= "\n $number. Container $containerClass #$containerId: {$exception->getMessage()}"; } parent::__construct(sprintf('No definition or class found or resolvable in composite container:%s', $message)); } } ================================================ FILE: src/Container.php ================================================ */ private array $instances = []; private CompositeContainer $delegates; /** * @var array Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. * @psalm-var array> */ private array $tags; /** * @var Closure[] * @psalm-var array */ private array $resetters = []; private bool $useResettersFromMeta = true; /** * @param ?ContainerConfigInterface $config Container configuration. * * @throws InvalidConfigException If configuration is not valid. */ public function __construct(?ContainerConfigInterface $config = null) { $config ??= ContainerConfig::create(); $this->definitions = new DefinitionStorage( [ ContainerInterface::class => $this, StateResetter::class => StateResetter::class, ], $config->useStrictMode(), ); $this->validate = $config->shouldValidate(); $this->setTags($config->getTags()); $this->addDefinitions($config->getDefinitions()); $this->addProviders($config->getProviders()); $this->setDelegates($config->getDelegates()); } /** * Returns a value indicating whether the container has the definition of the specified name. * * @param string $id Class name, interface name or alias name. * * @return bool Whether the container is able to provide instance of class specified. * * @see addDefinition() */ public function has(string $id): bool { try { if ($this->definitions->has($id)) { return true; } } catch (CircularReferenceException) { return true; } if (TagReference::isTagAlias($id)) { $tag = TagReference::extractTagFromAlias($id); return isset($this->tags[$tag]); } return false; } /** * Returns an instance by either interface name or alias. * * The same instance of the class will be returned each time this method is called. * * @param string $id The interface or an alias name that was previously registered. * * @throws CircularReferenceException * @throws InvalidConfigException * @throws NotFoundExceptionInterface * @throws NotInstantiableException * @throws BuildingException * * @return mixed An instance of the requested interface. * * @psalm-template T * @psalm-param string|class-string $id * @psalm-return ($id is class-string ? T : mixed) * * @psalm-suppress MixedReturnStatement `mixed` is a correct return type for this method. */ public function get(string $id) { // Fast path: check if instance exists. if (array_key_exists($id, $this->instances)) { if ($id === StateResetter::class) { return $this->prepareStateResetter(); } return $this->instances[$id]; } try { $this->instances[$id] = $this->build($id); } catch (NotFoundException $exception) { // Fast path: if the exception ID matches the requested ID, no need to modify stack. if ($exception->getId() === $id) { // Try delegates before giving up. try { if ($this->delegates->has($id)) { return $this->delegates->get($id); } } catch (Throwable $e) { throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e); } throw $exception; } // Add current ID to build stack for better error reporting. $buildStack = $exception->getBuildStack(); array_unshift($buildStack, $id); throw new NotFoundException($exception->getId(), $buildStack); } catch (NotFoundExceptionInterface $exception) { // Try delegates before giving up try { if ($this->delegates->has($id)) { return $this->delegates->get($id); } } catch (Throwable $e) { throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e); } throw new NotFoundException($id, [$id], previous: $exception); } catch (ContainerExceptionInterface $e) { if (!$e instanceof InvalidConfigException) { throw $e; } throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e); } catch (Throwable $e) { throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e); } // Handle StateResetter for newly built instances. if ($id === StateResetter::class) { return $this->prepareStateResetter(); } return $this->instances[$id]; } private function prepareStateResetter(): StateResetter { $delegatesResetter = null; if ($this->delegates->has(StateResetter::class)) { $delegatesResetter = $this->delegates->get(StateResetter::class); } /** @var StateResetter $mainResetter */ $mainResetter = $this->instances[StateResetter::class]; if ($this->useResettersFromMeta) { /** @var StateResetter[] $resetters */ $resetters = []; foreach ($this->resetters as $serviceId => $callback) { if (isset($this->instances[$serviceId])) { $resetters[$serviceId] = $callback; } } if ($delegatesResetter !== null) { $resetters[] = $delegatesResetter; } $mainResetter->setResetters($resetters); } elseif ($delegatesResetter !== null) { $resetter = new StateResetter($this->get(ContainerInterface::class)); $resetter->setResetters([$mainResetter, $delegatesResetter]); return $resetter; } return $mainResetter; } /** * Sets a definition to the container. Definition may be defined multiple ways. * * @param string $id ID to set definition for. * @param mixed $definition Definition to set. * * @throws InvalidConfigException * * @see DefinitionNormalizer::normalize() */ private function addDefinition(string $id, mixed $definition): void { [$definition, $meta] = DefinitionParser::parse($definition); if ($this->validate) { $this->validateDefinition($definition, $id); // Only validate meta if it's not empty. if ($meta !== []) { $this->validateMeta($meta); } } /** * @psalm-var array{reset?:Closure,tags?:string[]} $meta */ // Process meta only if it has tags or reset callback. if (isset($meta[self::META_TAGS])) { $this->setDefinitionTags($id, $meta[self::META_TAGS]); } if (isset($meta[self::META_RESET])) { $this->setDefinitionResetter($id, $meta[self::META_RESET]); } unset($this->instances[$id]); $this->addDefinitionToStorage($id, $definition); } /** * Sets multiple definitions at once. * * @param array $config Definitions indexed by their IDs. * * @throws InvalidConfigException */ private function addDefinitions(array $config): void { foreach ($config as $id => $definition) { if ($this->validate && !is_string($id)) { throw new InvalidConfigException( sprintf( 'Key must be a string. %s given.', get_debug_type($id), ), ); } /** @var string $id */ $this->addDefinition($id, $definition); } } /** * Set container delegates. * * Each delegate must be a callable in format `function (ContainerInterface $container): ContainerInterface`. * The container instance returned is used in case a service can't be found in primary container. * * @throws InvalidConfigException */ private function setDelegates(array $delegates): void { $this->delegates = new CompositeContainer(); $container = $this->get(ContainerInterface::class); foreach ($delegates as $delegate) { if (!$delegate instanceof Closure) { throw new InvalidConfigException( 'Delegate must be callable in format "function (ContainerInterface $container): ContainerInterface".', ); } $delegate = $delegate($container); if (!$delegate instanceof ContainerInterface) { throw new InvalidConfigException( 'Delegate callable must return an object that implements ContainerInterface.', ); } $this->delegates->attach($delegate); } $this->definitions->setDelegateContainer($this->delegates); } /** * @param mixed $definition Definition to validate. * @param string|null $id ID of the definition to validate. * * @throws InvalidConfigException */ private function validateDefinition(mixed $definition, ?string $id = null): void { // Skip validation for common simple cases. if ($definition instanceof ContainerInterface || $definition instanceof Closure) { return; } if (is_array($definition)) { if (isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) { $class = $definition['class']; $constructorArguments = $definition['__construct()']; /** * @var array $methodsAndProperties Is always array for prepared array definition data. * @see DefinitionParser::parse() * @psalm-var array $methodsAndProperties */ $methodsAndProperties = $definition['methodsAndProperties']; $definition = array_merge( $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class], [ArrayDefinition::CONSTRUCTOR => $constructorArguments], // extract only value from parsed definition method array_map(static fn(array $data): mixed => $data[2], $methodsAndProperties), ); } } elseif ($definition instanceof ExtensibleService) { throw new InvalidConfigException( 'Invalid definition. ExtensibleService is only allowed in provider extensions.', ); } DefinitionValidator::validate($definition, $id); } /** * @throws InvalidConfigException */ private function validateMeta(array $meta): void { foreach ($meta as $key => $value) { if (!in_array($key, self::ALLOWED_META, true)) { throw new InvalidConfigException( sprintf( 'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?', $key, $key, $key, ), ); } if ($key === self::META_TAGS) { $this->validateDefinitionTags($value); } if ($key === self::META_RESET) { $this->validateDefinitionReset($value); } } } /** * @throws InvalidConfigException */ private function validateDefinitionTags(mixed $tags): void { if (!is_array($tags)) { throw new InvalidConfigException( sprintf( 'Invalid definition: tags should be array of strings, %s given.', get_debug_type($tags), ), ); } foreach ($tags as $tag) { if (!is_string($tag)) { throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.'); } } } /** * @throws InvalidConfigException */ private function validateDefinitionReset(mixed $reset): void { if (!$reset instanceof Closure) { throw new InvalidConfigException( sprintf( 'Invalid definition: "reset" should be closure, %s given.', get_debug_type($reset), ), ); } } /** * @throws InvalidConfigException */ private function setTags(array $tags): void { if ($this->validate) { foreach ($tags as $tag => $services) { if (!is_string($tag)) { throw new InvalidConfigException( sprintf( 'Invalid tags configuration: tag should be string, %s given.', $tag, ), ); } if (!is_array($services)) { throw new InvalidConfigException( sprintf( 'Invalid tags configuration: tag should contain array of service IDs, %s given.', get_debug_type($services), ), ); } foreach ($services as $service) { if (!is_string($service)) { throw new InvalidConfigException( sprintf( 'Invalid tags configuration: service should be defined as class string, %s given.', get_debug_type($service), ), ); } } } } /** @psalm-var array> $tags */ $this->tags = $tags; } /** * @psalm-param string[] $tags */ private function setDefinitionTags(string $id, array $tags): void { foreach ($tags as $tag) { if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) { $this->tags[$tag][] = $id; } } } private function setDefinitionResetter(string $id, Closure $resetter): void { $this->resetters[$id] = $resetter; } /** * Add definition to storage. * * @param string $id ID to set definition for. * @param mixed|object $definition Definition to set. * * @see $definitions */ private function addDefinitionToStorage(string $id, mixed $definition): void { $this->definitions->set($id, $definition); if ($id === StateResetter::class) { $this->useResettersFromMeta = false; } } /** * Creates new instance by either interface name or alias. * * @param string $id The interface or the alias name that was previously registered. * * @throws InvalidConfigException * @throws NotFoundExceptionInterface * @throws CircularReferenceException * * @return mixed|object New-built instance of the specified class. * * @internal */ private function build(string $id): mixed { // Fast path: check for circular reference first as it's the most critical. if (isset($this->building[$id])) { if ($id === ContainerInterface::class) { return $this; } throw new CircularReferenceException( sprintf( 'Circular reference to "%s" detected while building: %s.', $id, implode(', ', array_keys($this->building)), ), ); } // Less common case: tag alias. if (TagReference::isTagAlias($id)) { return $this->getTaggedServices($id); } // Check if the definition exists. if (!$this->definitions->has($id)) { throw new NotFoundException($id, $this->definitions->getBuildStack()); } $this->building[$id] = 1; try { $normalizedDefinition = DefinitionNormalizer::normalize($this->definitions->get($id), $id); $object = $normalizedDefinition->resolve($this->get(ContainerInterface::class)); } finally { unset($this->building[$id]); } return $object; } private function getTaggedServices(string $tagAlias): array { $tag = TagReference::extractTagFromAlias($tagAlias); $services = []; if (isset($this->tags[$tag])) { foreach ($this->tags[$tag] as $service) { $services[] = $this->get($service); } } return $services; } /** * @throws CircularReferenceException * @throws InvalidConfigException */ private function addProviders(array $providers): void { $extensions = []; foreach ($providers as $provider) { $providerInstance = $this->buildProvider($provider); $extensions[] = $providerInstance->getExtensions(); $this->addDefinitions($providerInstance->getDefinitions()); } foreach ($extensions as $providerExtensions) { foreach ($providerExtensions as $id => $extension) { if (!is_string($id)) { throw new InvalidConfigException( sprintf('Extension key must be a service ID as string, %s given.', $id), ); } if ($id === ContainerInterface::class) { throw new InvalidConfigException('ContainerInterface extensions are not allowed.'); } if (!$this->definitions->has($id)) { throw new InvalidConfigException("Extended service \"$id\" doesn't exist."); } if (!is_callable($extension)) { throw new InvalidConfigException( sprintf( 'Extension of service should be callable, %s given.', get_debug_type($extension), ), ); } $definition = $this->definitions->get($id); if (!$definition instanceof ExtensibleService) { $definition = new ExtensibleService($definition, $id); $this->addDefinitionToStorage($id, $definition); } $definition->addExtension($extension); } } } /** * Builds service provider by definition. * * @param mixed $provider Class name or instance of provider. * * @throws InvalidConfigException If provider argument is not valid. * * @return ServiceProviderInterface Instance of service provider. */ private function buildProvider(mixed $provider): ServiceProviderInterface { if ($this->validate && !(is_string($provider) || $provider instanceof ServiceProviderInterface)) { throw new InvalidConfigException( sprintf( 'Service provider should be a class name or an instance of %s. %s given.', ServiceProviderInterface::class, get_debug_type($provider), ), ); } /** * @psalm-suppress MixedMethodCall Service provider defined as class string * should container public constructor, otherwise throws error. */ $providerInstance = is_object($provider) ? $provider : new $provider(); if (!$providerInstance instanceof ServiceProviderInterface) { throw new InvalidConfigException( sprintf( 'Service provider should be an instance of %s. %s given.', ServiceProviderInterface::class, get_debug_type($providerInstance), ), ); } return $providerInstance; } } ================================================ FILE: src/ContainerConfig.php ================================================ definitions = $definitions; return $new; } public function getDefinitions(): array { return $this->definitions; } /** * @param array $providers Service providers to get definitions from. */ public function withProviders(array $providers): self { $new = clone $this; $new->providers = $providers; return $new; } public function getProviders(): array { return $this->providers; } /** * @param array $tags Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. */ public function withTags(array $tags): self { $new = clone $this; $new->tags = $tags; return $new; } public function getTags(): array { return $this->tags; } /** * @param bool $validate Whether definitions should be validated immediately. */ public function withValidate(bool $validate = true): self { $new = clone $this; $new->validate = $validate; return $new; } public function shouldValidate(): bool { return $this->validate; } /** * @param array $delegates Container delegates. Each delegate is a callable in format * `function (ContainerInterface $container): ContainerInterface`. The container instance returned is used * in case a service can't be found in primary container. */ public function withDelegates(array $delegates): self { $new = clone $this; $new->delegates = $delegates; return $new; } public function getDelegates(): array { return $this->delegates; } /** * @param bool $useStrictMode If the automatic addition of definition when class exists and can be resolved * is disabled. */ public function withStrictMode(bool $useStrictMode = true): self { $new = clone $this; $new->useStrictMode = $useStrictMode; return $new; } public function useStrictMode(): bool { return $this->useStrictMode; } } ================================================ FILE: src/ContainerConfigInterface.php ================================================ ['service1', 'service2']]`. */ public function getTags(): array; /** * @return bool Whether definitions should be validated immediately. */ public function shouldValidate(): bool; /** * @return array Container delegates. Each delegate is a callable in format * `function (ContainerInterface $container): ContainerInterface`. The container instance returned is used * in case a service can't be found in primary container. */ public function getDelegates(): array; /** * @return bool If the automatic addition of definition when class exists and can be resolved is disabled. */ public function useStrictMode(): bool; } ================================================ FILE: src/ExtensibleService.php ================================================ withAnotherOption(42); * } * ``` */ final class ExtensibleService implements DefinitionInterface { /** * @var callable[] */ private array $extensions = []; /** * @param mixed $definition Definition to allow registering extensions for. */ public function __construct( private readonly mixed $definition, private readonly string $id, ) {} /** * Add an extension. * * An extension is callable that returns a modified service object: * * ```php * static function (ContainerInterface $container, $service) { * return $service->withAnotherOption(42); * } * ``` * * @param callable $closure An extension to register. */ public function addExtension(callable $closure): void { $this->extensions[] = $closure; } public function resolve(ContainerInterface $container): mixed { $service = DefinitionNormalizer::normalize($this->definition, $this->id) ->resolve($container); foreach ($this->extensions as $extension) { $result = $extension($container->get(ContainerInterface::class), $service); if ($result === null) { continue; } $service = $result; } return $service; } } ================================================ FILE: src/Helpers/DefinitionNormalizer.php ================================================ [ * 'definition' => [ * '__class' => BigEngine::class, * 'setNumber()' => [42], * ], * 'tags' => ['a', 'b'], * 'reset' => function () { * $this->number = 42; * }, * ] * ``` * * 2) Mixed in array definition: * * ```php * Engine::class => [ * '__class' => BigEngine::class, * 'setNumber()' => [42], * 'tags' => ['a', 'b'], * 'reset' => function () { * $this->number = 42; * }, * ] * ``` */ final class DefinitionParser { public const IS_PREPARED_ARRAY_DEFINITION_DATA = 'isPreparedArrayDefinitionData'; private const DEFINITION_META = 'definition'; /** * @param mixed $definition Definition to parse. * * @return array Definition parsed into an array of a special structure. * @psalm-return array{mixed,array} */ public static function parse(mixed $definition): array { if (!is_array($definition)) { return [$definition, []]; } // Dedicated definition if (isset($definition[self::DEFINITION_META])) { $newDefinition = $definition[self::DEFINITION_META]; unset($definition[self::DEFINITION_META]); return [$newDefinition, $definition]; } // Callable definition if (is_callable($definition, true)) { return [$definition, []]; } // Array definition $meta = []; $class = null; $constructorArguments = []; $methodsAndProperties = []; foreach ($definition as $key => $value) { if (is_string($key)) { // Class if ($key === ArrayDefinition::CLASS_NAME) { $class = $value; continue; } // Constructor arguments if ($key === ArrayDefinition::CONSTRUCTOR) { $constructorArguments = $value; continue; } // Methods and properties if (count($methodArray = explode('()', $key, 2)) === 2) { $methodsAndProperties[$key] = [ArrayDefinition::TYPE_METHOD, $methodArray[0], $value]; continue; } if (count($propertyArray = explode('$', $key, 2)) === 2) { $methodsAndProperties[$key] = [ArrayDefinition::TYPE_PROPERTY, $propertyArray[1], $value]; continue; } } $meta[$key] = $value; } return [ [ 'class' => $class, '__construct()' => $constructorArguments, 'methodsAndProperties' => $methodsAndProperties, self::IS_PREPARED_ARRAY_DEFINITION_DATA => true, ], $meta, ]; } } ================================================ FILE: src/NotFoundException.php ================================================ buildStack)) { $message = sprintf('No definition or class found or resolvable for "%s".', $id); } elseif ($this->buildStack === [$id]) { $message = sprintf('No definition or class found or resolvable for "%s" while building it.', $id); } else { $message = sprintf( 'No definition or class found or resolvable for "%s" while building "%s".', end($this->buildStack), implode('" -> "', $buildStack), ); } parent::__construct($message, previous: $previous); } public function getId(): string { return $this->id; } /** * @return string[] */ public function getBuildStack(): array { return $this->buildStack; } public function getName(): string { return sprintf('No definition or class found for "%s" ID.', $this->id); } public function getSolution(): ?string { $solution = <<id); } } ================================================ FILE: src/Reference/TagReference.php ================================================ ['class' => Car::class], * 'car-factory' => CarFactory::class, * EngineInterface::class => EngineMarkOne::class, * ]; * } * } * ``` */ interface ServiceProviderInterface { /** * Returns definitions for the container. * * This method: * * - Should only return definitions for the Container preventing any side effects. * - Should be idempotent. * * @return array Definitions for the container. Each array key is the name of the service (usually it is * an interface name), and a corresponding value is a service definition. */ public function getDefinitions(): array; /** * Returns an array of service extensions. * * An extension is callable that returns a modified service object: * * ```php * static function (ContainerInterface $container, $service) { * return $service->withAnotherOption(42); * } * ``` * * @return array Extensions for the container services. Each array key is the name of the service to be modified * and a corresponding value is callable doing the job. */ public function getExtensions(): array; } ================================================ FILE: src/StateResetter.php ================================================ resetters as $resetter) { if ($resetter instanceof self) { $resetter->reset(); continue; } $resetter($this->container); } } /** * @param Closure[]|self[] $resetters Array of reset callbacks. Each callback has access to the private and * protected properties of the service instance, so you can set the initial state of the service efficiently * without creating a new instance. */ public function setResetters(array $resetters): void { $this->resetters = []; foreach ($resetters as $serviceId => $callback) { if (is_int($serviceId)) { if (!$callback instanceof self) { throw new InvalidArgumentException(sprintf( 'State resetter object should be instance of "%s", "%s" given.', self::class, get_debug_type($callback), )); } $this->resetters[] = $callback; continue; } if (!$callback instanceof Closure) { throw new InvalidArgumentException( 'Callback for state resetter should be closure in format ' . '`function (ContainerInterface $container): void`. ' . 'Got "' . get_debug_type($callback) . '".', ); } $instance = $this->container->get($serviceId); if (!is_object($instance)) { throw new InvalidArgumentException( 'State resetter supports resetting objects only. Container returned ' . get_debug_type($instance) . '.', ); } /** @var Closure */ $this->resetters[] = $callback->bindTo($instance, $instance::class); } } } ================================================ FILE: tests/Benchmark/ContainerBench.php ================================================ PropertyTestClass::class], [ 'serviceClass' => NullableConcreteDependency::class, 'otherDefinitions' => [ EngineInterface::class => EngineMarkOne::class, Car::class => Car::class, EngineMarkOne::class => EngineMarkOne::class, ], ], [ 'serviceClass' => NullableConcreteDependency::class, 'otherDefinitions' => [ EngineInterface::class => EngineMarkTwo::class, ], ], ]; } /** * Load the bulk of the definitions. * These all refer to a service that is not yet defined but must be defined in the benchmark. */ public function before(): void { $definitions3 = []; $definitions2 = []; $definitions3['service'] = PropertyTestClass::class; for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $this->indexes[] = $i; $definitions2["second$i"] = Reference::to('service'); $definitions3["third$i"] = Reference::to('service'); } $this->randomIndexes = $this->indexes; shuffle($this->randomIndexes); $this->composite = new CompositeContainer(); // Attach the dummy containers multiple times, to see what would happen if there are lots of them. $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions2), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions3), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions2), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions3), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions2), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions3), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions2), ), ); $this->composite->attach( new Container( ContainerConfig::create() ->withDefinitions($definitions3), ), ); } /** * @Groups({"construct"}) * * @throws InvalidConfigException * @throws NotInstantiableException */ public function benchConstruct(): void { $definitions = []; for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $definitions["service$i"] = PropertyTestClass::class; } $container = new Container( ContainerConfig::create() ->withDefinitions($definitions), ); } /** * @Groups({"lookup"}) * @ParamProviders({"provideDefinitions"}) */ public function benchSequentialLookups($params): void { $definitions = []; for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $definitions["service$i"] = $params['serviceClass']; } if (isset($params['otherDefinitions'])) { $definitions = array_merge($definitions, $params['otherDefinitions']); } $container = new Container( ContainerConfig::create() ->withDefinitions($definitions), ); for ($i = 0; $i < self::SERVICE_COUNT / 2; $i++) { // Do array lookup. $index = $this->indexes[$i]; $container->get("service$index"); } } /** * @Groups({"lookup"}) * @ParamProviders({"provideDefinitions"}) */ public function benchRandomLookups($params): void { $definitions = []; for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $definitions["service$i"] = $params['serviceClass']; } if (isset($params['otherDefinitions'])) { $definitions = array_merge($definitions, $params['otherDefinitions']); } $container = new Container( ContainerConfig::create() ->withDefinitions($definitions), ); for ($i = 0; $i < self::SERVICE_COUNT / 2; $i++) { // Do array lookup. $index = $this->randomIndexes[$i]; $container->get("service$index"); } } /** * @Groups({"lookup"}) * @ParamProviders({"provideDefinitions"}) */ public function benchRandomLookupsComposite($params): void { $definitions = []; for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $definitions["service$i"] = $params['serviceClass']; } if (isset($params['otherDefinitions'])) { $definitions = array_merge($definitions, $params['otherDefinitions']); } $container = new Container( ContainerConfig::create() ->withDefinitions($definitions), ); $this->composite->attach($container); for ($i = 0; $i < self::SERVICE_COUNT / 2; $i++) { // Do array lookup. $index = $this->randomIndexes[$i]; $this->composite->get("service$index"); } } } ================================================ FILE: tests/Benchmark/ContainerMethodHasBench.php ================================================ container = new Container( ContainerConfig::create() ->withDefinitions($definitions), ); } public function benchPredefinedExisting(): void { for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $this->container->has("service$i"); } } public function benchUndefinedExisting(): void { for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $this->container->has(GearBox::class); } } public function benchUndefinedNonexistent(): void { for ($i = 0; $i < self::SERVICE_COUNT; $i++) { $this->container->has('NonexistentNamespace\NonexistentClass'); } } } ================================================ FILE: tests/Support/A.php ================================================ color = $color; return $this; } public function getColor(): ColorInterface { return $this->color; } public function getEngine(): EngineInterface { return $this->engine; } public function getEngineName(): string { return $this->engine->getName(); } public function getMoreEngines(): array { return $this->moreEngines; } } ================================================ FILE: tests/Support/CarExtensionProvider.php ================================================ static function (ContainerInterface $container, Car $car) { $car->setColor(new ColorRed()); return $car; }, EngineInterface::class => static fn(ContainerInterface $container, EngineInterface $engine) => $container->get(EngineMarkTwo::class), ]; } } ================================================ FILE: tests/Support/CarFactory.php ================================================ createByName($name)); } public function createWithColor(ColorInterface $color): Car { $car = new Car(EngineFactory::createDefault()); return $car->setColor($color); } } ================================================ FILE: tests/Support/CarProvider.php ================================================ Car::class, EngineInterface::class => EngineMarkOne::class, ]; } public function getExtensions(): array { return [ Car::class => static function (ContainerInterface $container, Car $car) { $car->setColor(new ColorPink()); return $car; }, 'sport_car' => static function (ContainerInterface $container, SportCar $car) { $car->setColor(new ColorPink()); return $car; }, ]; } } ================================================ FILE: tests/Support/ColorInterface.php ================================================ allParameters = func_get_args(); } /** * @return mixed */ public function getParameter() { return $this->parameter; } public function getAllParameters(): array { return $this->allParameters; } } ================================================ FILE: tests/Support/ContainerInterfaceExtensionProvider.php ================================================ static fn(ContainerInterface $container, ContainerInterface $extended) => $container, ]; } } ================================================ FILE: tests/Support/Cycle/Chicken.php ================================================ container->get(EngineMarkOne::class); } if ($name === EngineMarkTwo::NAME) { return $this->container->get(EngineMarkTwo::class); } throw new Exception('unknown engine name: ' . $name); } public static function createDefault(): EngineInterface { return new EngineMarkOne(); } } ================================================ FILE: tests/Support/EngineInterface.php ================================================ number = $value; } public function getNumber(): int { return $this->number; } } ================================================ FILE: tests/Support/EngineMarkTwo.php ================================================ number = $value; } public function getNumber(): int { return $this->number; } } ================================================ FILE: tests/Support/EngineStorage.php ================================================ engines = $engines; } public function getEngines(): array { return $this->engines; } } ================================================ FILE: tests/Support/Garage.php ================================================ car; } } ================================================ FILE: tests/Support/GearBox.php ================================================ get('engine'); return new Car($engine); } } ================================================ FILE: tests/Support/MethodTestClass.php ================================================ value; } public function setValue(mixed $value): void { $this->value = $value; } } ================================================ FILE: tests/Support/NonPsrContainer.php ================================================ static fn(ContainerInterface $container, Car $car) => null, ]; } } ================================================ FILE: tests/Support/NullableConcreteDependency.php ================================================ car; } } ================================================ FILE: tests/Support/PropertyTestClass.php ================================================ color = $color; return $this; } public function getColor(): ColorInterface { return $this->color; } public function getEngine(): EngineInterface { return $this->engine; } public function getEngineName(): string { return $this->engine->getName(); } public function getMaxSpeed(): int { return $this->maxSpeed; } } ================================================ FILE: tests/Support/StaticFactory.php ================================================ parameters = $parameters; } public function getFirst() { return $this->first; } public function getEngine(): EngineInterface { return $this->engine; } public function getParameters(): array { return $this->parameters; } } ================================================ FILE: tests/Unit/BuildingExceptionTest.php ================================================ assertSame('Caught unhandled error "i am angry" while building "test".', $exception->getMessage()); $this->assertSame('Unable to build "test" object.', $exception->getName()); $this->assertSame( <<getSolution(), ); } public function testEmptyMessage(): void { $exception = new BuildingException('test', new RuntimeException()); $this->assertSame('Caught unhandled error "RuntimeException" while building "test".', $exception->getMessage()); } public function testBuildStack(): void { $exception = new BuildingException('test', new RuntimeException('i am angry'), ['a', 'b', 'test']); $this->assertSame('Caught unhandled error "i am angry" while building "a" -> "b" -> "test".', $exception->getMessage()); } public function testCode(): void { $exception = new BuildingException('test', new RuntimeException()); $this->assertSame(0, $exception->getCode()); } } ================================================ FILE: tests/Unit/CompositeContainerTest.php ================================================ expectException(InvalidArgumentException::class); $this->expectExceptionMessageMatches( '/^ID must be a string, (integer|int) given\.$/', ); $container->get(42); } public function testTagsWithYiiAndNotYiiContainers(): void { $compositeContainer = new CompositeContainer(); $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine'], ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, 'tags' => ['engine'], ], ]); $firstContainer = new Container($config); $secondContainer = new \League\Container\Container(); $compositeContainer->attach($firstContainer); $compositeContainer->attach($secondContainer); $engines = $compositeContainer->get('tag@engine'); $this->assertIsArray($engines); $this->assertCount(2, $engines); $this->assertInstanceOf(EngineMarkOne::class, $engines[0]); $this->assertInstanceOf(EngineMarkTwo::class, $engines[1]); } public function testNonPsrContainer(): void { $compositeContainer = new CompositeContainer(); $compositeContainer->attach(new NonPsrContainer()); $this->expectException(CompositeNotFoundException::class); $this->expectExceptionMessageMatches( '/No definition or class found or resolvable in composite container/', ); $this->expectExceptionMessageMatches( '/Container "has\(\)" returned false, but no exception was thrown from "get\(\)"\./', ); $compositeContainer->get('test'); } public function testHasNoString(): void { $container = new CompositeContainer(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('ID must be a string, bool given.'); $container->has(true); } #[TestWith([true, 'engine'])] #[TestWith([false, 'other'])] public function testHasTag(bool $expected, string $tag): void { $container = new CompositeContainer(); $container->attach( new Container( ContainerConfig::create()->withTags(['engine' => []]), ), ); assertSame($expected, $container->has('tag@' . $tag)); } public function testHasTagWithoutYiiContainer(): void { $container = new CompositeContainer(); $container->attach(new SimpleContainer()); assertFalse($container->has('tag@engine')); } } ================================================ FILE: tests/Unit/CompositePsrContainerOverLeagueTest.php ================================================ setupContainer(new Container(), $definitions); return $this->createCompositeContainer($container); } public function setupContainer(ContainerInterface $container, iterable $definitions = []): ContainerInterface { foreach ($definitions as $id => $definition) { $container->add($id, $definition); } return $container; } public function testNotFoundException(): void { $compositeContainer = new CompositeContainer(); $container1 = new Container(); $container1Id = spl_object_id($container1); $container2 = new Container(); $container2Id = spl_object_id($container2); $compositeContainer->attach($container1); $compositeContainer->attach($container2); $this->expectException(CompositeNotFoundException::class); $this->expectExceptionMessage("No definition or class found or resolvable in composite container:\n 1. Container League\Container\Container #$container1Id: Alias (test) is not being managed by the container or delegates\n 2. Container League\Container\Container #$container2Id: Alias (test) is not being managed by the container or delegates"); $compositeContainer->get('test'); } } ================================================ FILE: tests/Unit/CompositePsrContainerOverYiisoftTest.php ================================================ withDefinitions($definitions); $container = new Container($config); return $this->createCompositeContainer($container); } public function testResetterInCompositeContainerWithExternalResetter(): void { $composite = $this->createContainer([ StateResetter::class => function (ContainerInterface $container) { $resetter = new StateResetter($container); $resetter->setResetters([ 'engineMarkOne' => function () { $this->number = 42; }, ]); return $resetter; }, 'engineMarkOne' => function () { $engine = new EngineMarkOne(); $engine->setNumber(42); return $engine; }, ]); $config = ContainerConfig::create() ->withDefinitions([ 'engineMarkTwo' => ['class' => EngineMarkTwo::class, 'setNumber()' => [43], 'reset' => function () { $this->number = 43; }, ], ]); $secondContainer = new Container($config); $composite->attach($secondContainer); $engineMarkOne = $composite->get('engineMarkOne'); $engineMarkTwo = $composite->get('engineMarkTwo'); $this->assertSame( 42, $composite ->get('engineMarkOne') ->getNumber(), ); $this->assertSame( 43, $composite ->get('engineMarkTwo') ->getNumber(), ); $engineMarkOne->setNumber(45); $engineMarkTwo->setNumber(46); $this->assertSame( 45, $composite ->get('engineMarkOne') ->getNumber(), ); $this->assertSame( 46, $composite ->get('engineMarkTwo') ->getNumber(), ); $composite ->get(StateResetter::class) ->reset(); $this->assertSame($engineMarkOne, $composite->get('engineMarkOne')); $this->assertSame($engineMarkTwo, $composite->get('engineMarkTwo')); $this->assertSame( 42, $composite ->get('engineMarkOne') ->getNumber(), ); $this->assertSame( 43, $composite ->get('engineMarkTwo') ->getNumber(), ); } public function testNotFoundException(): void { $compositeContainer = new CompositeContainer(); $container1 = new Container(); $container1Id = spl_object_id($container1); $container2 = new Container(); $container2Id = spl_object_id($container2); $compositeContainer->attach($container1); $compositeContainer->attach($container2); $this->expectException(CompositeNotFoundException::class); $this->expectExceptionMessage("No definition or class found or resolvable in composite container:\n 1. Container Yiisoft\Di\Container #$container1Id: No definition or class found or resolvable for \"test\" while building it.\n 2. Container Yiisoft\Di\Container #$container2Id: No definition or class found or resolvable for \"test\" while building it."); $compositeContainer->get('test'); } } ================================================ FILE: tests/Unit/CompositePsrContainerTestAbstract.php ================================================ attach($attachedContainer); return $compositeContainer; } public function testAttach(): void { $compositeContainer = new CompositeContainer(); $config = ContainerConfig::create() ->withDefinitions([ 'test' => EngineMarkOne::class, ]); $container = new Container($config); $compositeContainer->attach($container); $this->assertTrue($compositeContainer->has('test')); $this->assertInstanceOf(EngineMarkOne::class, $compositeContainer->get('test')); } public function testDetach(): void { $compositeContainer = new CompositeContainer(); $config = ContainerConfig::create() ->withDefinitions([ 'test' => EngineMarkOne::class, ]); $container = new Container($config); $compositeContainer->attach($container); $this->assertInstanceOf(EngineMarkOne::class, $compositeContainer->get('test')); $compositeContainer->detach($container); $this->expectException(NotFoundExceptionInterface::class); $this->assertInstanceOf(EngineMarkOne::class, $compositeContainer->get('test')); } public function testHasDefinition(): void { $compositeContainer = $this->createContainer([EngineInterface::class => EngineMarkOne::class]); $this->assertTrue($compositeContainer->has(EngineInterface::class)); $config = ContainerConfig::create() ->withDefinitions([ 'test' => EngineMarkTwo::class, ]); $container = new Container($config); $compositeContainer->attach($container); $this->assertTrue($compositeContainer->has('test')); } public function testGetPriority(): void { $compositeContainer = $this->createContainer([EngineInterface::class => EngineMarkOne::class]); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkTwo::class, ]); $container = new Container($config); $compositeContainer->attach($container); $this->assertInstanceOf(EngineMarkOne::class, $compositeContainer->get(EngineInterface::class)); $config = ContainerConfig::create() ->withDefinitions([EngineInterface::class => EngineMarkOne::class]); $containerOne = new Container($config); $config = ContainerConfig::create() ->withDefinitions([EngineInterface::class => EngineMarkTwo::class]); $containerTwo = new Container($config); $compositeContainer = new CompositeContainer(); $compositeContainer->attach($containerOne); $compositeContainer->attach($containerTwo); $this->assertInstanceOf(EngineMarkOne::class, $compositeContainer->get(EngineInterface::class)); } public function testTags(): void { $compositeContainer = new CompositeContainer(); $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine'], ], ]); $firstContainer = new Container($config); $config = ContainerConfig::create() ->withDefinitions([ EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, 'tags' => ['engine'], ], ]); $secondContainer = new Container($config); $compositeContainer->attach($firstContainer); $compositeContainer->attach($secondContainer); $engines = $compositeContainer->get('tag@engine'); $this->assertIsArray($engines); $this->assertCount(2, $engines); $this->assertSame(EngineMarkOne::class, $engines[1]::class); $this->assertSame(EngineMarkTwo::class, $engines[0]::class); } public function testDelegateLookup(): void { $compositeContainer = new CompositeContainer(); $firstContainer = new Container(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ]); $secondContainer = new Container($config); $compositeContainer->attach($firstContainer); $compositeContainer->attach($secondContainer); $car = $compositeContainer->get(Car::class); $this->assertInstanceOf(Car::class, $car); } public function testDelegateLookupUnionTypes(): void { $compositeContainer = new CompositeContainer(); $firstContainer = new Container(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ]); $secondContainer = new Container($config); $compositeContainer->attach($firstContainer); $compositeContainer->attach($secondContainer); $car = $compositeContainer->get(UnionTypeInConstructorParamNotResolvable::class); $this->assertInstanceOf(UnionTypeInConstructorParamNotResolvable::class, $car); } } ================================================ FILE: tests/Unit/Container/DependencyFromDelegate/Car.php ================================================ withDefinitions([ ContainerInterface::class => new SimpleContainer(), Car::class => Car::class, ]) ->withDelegates([ static fn() => new SimpleContainer([ Car::class => new Car(new Engine()), ]), ]), ); $car = $container->get(Car::class); assertInstanceOf(Car::class, $car); assertInstanceOf(Engine::class, $car->engine); } public function testNotFoundInDelegate(): void { $container = new Container( ContainerConfig::create() ->withDefinitions([ ContainerInterface::class => new SimpleContainer(), 'car' => Car::class, ]) ->withDelegates([ static fn() => new Container( ContainerConfig::create() ->withDefinitions([ 'car' => Car::class, ]), ), ]), ); $exception = null; try { $container->get('car'); } catch (Throwable $exception) { } assertInstanceOf(BuildingException::class, $exception); assertSame( sprintf( 'Caught unhandled error "No definition or class found or resolvable for "%2$s" while building "%1$s" -> "%3$s" -> "%2$s"." while building "%1$s".', 'car', EngineInterface::class, Car::class, ), $exception->getMessage(), ); $previous = $exception->getPrevious(); assertInstanceOf(NotFoundException::class, $previous); $this->assertSame( sprintf( 'No definition or class found or resolvable for "%2$s" while building "%1$s" -> "%3$s" -> "%2$s".', 'car', EngineInterface::class, Car::class, ), $previous->getMessage(), ); } } ================================================ FILE: tests/Unit/Container/DependencyFromDelegate/Engine.php ================================================ expectNotToPerformAssertions(); new Container(); } public function testSettingScalars(): void { $this->expectException(InvalidConfigException::class); $config = ContainerConfig::create() ->withDefinitions([ 'scalar' => 123, ]); $container = new Container($config); $container->get('scalar'); } public function testIntegerKeys(): void { $this->expectException(InvalidConfigException::class); $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class, EngineMarkTwo::class, ]); $container = new Container($config); $container->get(Car::class); } public function testNullableClassDependency(): void { $container = new Container(); $this->expectException(NotFoundException::class); $container->get(NullableConcreteDependency::class); } public function testOptionalResolvableClassDependency(): void { $container = new Container( ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ]), ); $this->assertTrue($container->has(OptionalConcreteDependency::class)); $service = $container->get(OptionalConcreteDependency::class); $this->assertInstanceOf(Car::class, $service->getCar()); } public function testOptionalNotResolvableClassDependency(): void { $container = new Container(); $this->assertTrue($container->has(OptionalConcreteDependency::class)); $service = $container->get(OptionalConcreteDependency::class); $this->assertNull($service->getCar()); } public function testOptionalCircularClassDependency(): void { $config = ContainerConfig::create() ->withDefinitions([ A::class => A::class, B::class => B::class, ]); $container = new Container($config); $a = $container->get(A::class); $this->assertInstanceOf(B::class, $a->b); $this->assertNull($a->b->a); } public static function dataHas(): array { return [ [false, 'non_existing'], [false, ColorInterface::class], [true, Car::class], [true, EngineMarkOne::class], [true, EngineInterface::class], [true, EngineStorage::class], [true, Chicken::class], [true, TreeItem::class], ]; } #[DataProvider('dataHas')] public function testHas(bool $expected, $id): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ]); $container = new Container($config); $this->assertSame($expected, $container->has($id)); } public static function dataUnionTypes(): array { return [ [UnionTypeInConstructorSecondTypeInParamResolvable::class], [UnionTypeInConstructorFirstTypeInParamResolvable::class], ]; } #[DataProvider('dataUnionTypes')] public function testUnionTypes(string $class): void { $container = new Container(); $this->assertTrue($container->has($class)); } public function testClassExistsButIsNotResolvable(): void { $container = new Container(); $this->assertFalse($container->has('non_existing')); $this->assertFalse($container->has(Car::class)); $this->assertFalse($container->has(SportCar::class)); $this->assertFalse($container->has(NullableConcreteDependency::class)); $this->assertFalse($container->has(ColorInterface::class)); } public static function dataClassExistButIsNotResolvableWithUnionTypes(): array { return [ [UnionTypeInConstructorParamNotResolvable::class], [UnionTypeInConstructorSecondParamNotResolvable::class], ]; } #[DataProvider('dataClassExistButIsNotResolvableWithUnionTypes')] public function testClassExistButIsNotResolvableWithUnionTypes(string $class): void { $container = new Container(); $this->assertFalse($container->has($class)); } public function testWithoutDefinition(): void { $container = new Container(); $hasEngine = $container->has(EngineMarkOne::class); $this->assertTrue($hasEngine); $engine = $container->get(EngineMarkOne::class); $this->assertInstanceOf(EngineMarkOne::class, $engine); } public function testCircularClassDependencyWithoutDefinition(): void { $container = new Container(); $this->expectException(CircularReferenceException::class); $container->get(Chicken::class); } public function testTrivialDefinition(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => EngineMarkOne::class, ]); $container = new Container($config); $one = $container->get(EngineMarkOne::class); $two = $container->get(EngineMarkOne::class); $this->assertInstanceOf(EngineMarkOne::class, $one); $this->assertSame($one, $two); } public function testCircularClassDependency(): void { $config = ContainerConfig::create() ->withDefinitions([ Chicken::class => Chicken::class, Egg::class => Egg::class, ]); $container = new Container($config); $this->expectException(CircularReferenceException::class); $container->get(Chicken::class); } public function testClassSimple(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine' => EngineMarkOne::class, ]); $container = new Container($config); $this->assertInstanceOf(EngineMarkOne::class, $container->get('engine')); } public function testSetAll(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine1' => EngineMarkOne::class, 'engine2' => EngineMarkTwo::class, ]); $container = new Container($config); $this->assertInstanceOf(EngineMarkOne::class, $container->get('engine1')); $this->assertInstanceOf(EngineMarkTwo::class, $container->get('engine2')); } public function testClassConstructor(): void { $config = ContainerConfig::create() ->withDefinitions([ 'constructor_test' => [ 'class' => ConstructorTestClass::class, '__construct()' => [42], ], ]); $container = new Container($config); /** @var ConstructorTestClass $object */ $object = $container->get('constructor_test'); $this->assertSame(42, $object->getParameter()); } // See https://github.com/yiisoft/di/issues/157#issuecomment-701458616 public function testIntegerIndexedConstructorArguments(): void { $config = ContainerConfig::create() ->withDefinitions([ 'items' => [ 'class' => ArrayIterator::class, '__construct()' => [ [], ArrayIterator::STD_PROP_LIST, ], ], ]); $container = new Container($config); $items = $container->get('items'); $this->assertInstanceOf(ArrayIterator::class, $items); $this->assertSame(ArrayIterator::STD_PROP_LIST, $items->getFlags()); } public function testExcessiveConstructorParametersIgnored(): void { $config = ContainerConfig::create() ->withDefinitions([ 'constructor_test' => [ 'class' => ConstructorTestClass::class, '__construct()' => [ 'parameter' => 42, 'surplus1' => 43, ], ], ]); $container = new Container($config); /** @var ConstructorTestClass $object */ $object = $container->get('constructor_test'); $this->assertSame([42], $object->getAllParameters()); } public function testVariadicConstructorParameters(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, 'stringIndexed' => [ 'class' => VariadicConstructor::class, '__construct()' => [ 'first' => 1, 'parameters' => [42, 43, 44], ], ], 'integerIndexed' => [ 'class' => VariadicConstructor::class, '__construct()' => [1, new EngineMarkOne(), 42, 43, 44], ], ]); $container = new Container($config); $object = $container->get('stringIndexed'); $this->assertSame(1, $object->getFirst()); $this->assertSame([42, 43, 44], $object->getParameters()); $this->assertInstanceOf(EngineMarkOne::class, $object->getEngine()); $object = $container->get('integerIndexed'); $this->assertSame(1, $object->getFirst()); $this->assertInstanceOf(EngineMarkOne::class, $object->getEngine()); $this->assertSame([42, 43, 44], $object->getParameters()); } public function testMixedIndexedConstructorParametersAreNotAllowed(): void { $config = ContainerConfig::create() ->withDefinitions([ 'test' => [ 'class' => VariadicConstructor::class, '__construct()' => [ 'parameters' => 42, 43, ], ], ]); $container = new Container($config); $this->expectException(BuildingException::class); $this->expectExceptionMessage( 'Caught unhandled error "Arguments indexed both by name and by position are not allowed in the same array." while building "test".', ); $container->get('test'); } public function testClassProperties(): void { $config = ContainerConfig::create() ->withDefinitions([ 'property_test' => [ 'class' => PropertyTestClass::class, '$property' => 42, ], ]); $container = new Container($config); /** @var PropertyTestClass $object */ $object = $container->get('property_test'); $this->assertSame(42, $object->property); } public function testClassMethods(): void { $config = ContainerConfig::create() ->withDefinitions([ 'method_test' => [ 'class' => MethodTestClass::class, 'setValue()' => [42], ], ]); $container = new Container($config); /** @var MethodTestClass $object */ $object = $container->get('method_test'); $this->assertSame(42, $object->getValue()); } public function testClosureInConstructor(): void { $color = static fn() => new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ConstructorTestClass::class => [ 'class' => ConstructorTestClass::class, '__construct()' => [$color], ], ]); $container = new Container($config); $testClass = $container->get(ConstructorTestClass::class); $this->assertSame($color, $testClass->getParameter()); } public function testDynamicClosureInConstruct(): void { $config = ContainerConfig::create() ->withDefinitions([ 'car' => [ 'class' => Car::class, '__construct()' => [ DynamicReference::to(static fn(EngineInterface $engine) => $engine), ], ], EngineInterface::class => EngineMarkTwo::class, ]); $container = new Container($config); $car = $container->get('car'); $engine = $container->get(EngineInterface::class); $this->assertSame($engine, $car->getEngine()); } public function testKeepClosureDefinition(): void { $engine = new EngineMarkOne(); $closure = static fn(EngineInterface $engine) => $engine; $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => $engine, 'closure' => DynamicReference::to($closure), 'engine' => $closure, ]); $container = new Container($config); $closure = $container->get('closure'); $this->assertSame($closure, $container->get('closure')); $this->assertSame($engine, $container->get('engine')); } public function testClosureInProperty(): void { $color = static fn() => new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ PropertyTestClass::class => [ 'class' => PropertyTestClass::class, '$property' => $color, ], ]); $container = new Container($config); $testClass = $container->get(PropertyTestClass::class); $this->assertSame($color, $testClass->property); } public function testDynamicClosureInProperty(): void { $color = new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ColorInterface::class => $color, 'car' => [ 'class' => Car::class, '$color' => DynamicReference::to(fn() => $color), ], ]); $container = new Container($config); $car = $container->get('car'); $this->assertSame($color, $car->getColor()); } public function testClosureInMethodCall(): void { $color = static fn() => new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, MethodTestClass::class => [ 'class' => MethodTestClass::class, 'setValue()' => [$color], ], ]); $container = new Container($config); $testClass = $container->get(MethodTestClass::class); $this->assertSame($color, $testClass->getValue()); } public function testDynamicClosureInMethodCall(): void { $color = new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ColorInterface::class => $color, 'car' => [ 'class' => Car::class, 'setColor()' => [DynamicReference::to(fn() => $color)], ], ]); $container = new Container($config); $car = $container->get('car'); $this->assertSame($color, $car->getColor()); } public function testAlias(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => Reference::to('engine'), 'engine' => Reference::to('engine-mark-one'), 'engine-mark-one' => EngineMarkOne::class, ]); $container = new Container($config); $engine1 = $container->get('engine-mark-one'); $engine2 = $container->get('engine'); $engine3 = $container->get(EngineInterface::class); $this->assertInstanceOf(EngineMarkOne::class, $engine1); $this->assertSame($engine1, $engine2); $this->assertSame($engine2, $engine3); } public function testCircularAlias(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine-1' => Reference::to('engine-2'), 'engine-2' => Reference::to('engine-3'), 'engine-3' => Reference::to('engine-1'), ]); $container = new Container($config); $this->expectException(CircularReferenceException::class); $container->get('engine-1'); } public function testUndefinedDependencies(): void { $config = ContainerConfig::create() ->withDefinitions([ 'car' => Car::class, ]); $container = new Container($config); $this->expectException(NotFoundException::class); $container->get('car'); } public function testDependencies(): void { $config = ContainerConfig::create() ->withDefinitions([ 'car' => Car::class, EngineInterface::class => EngineMarkTwo::class, ]); $container = new Container($config); /** @var Car $car */ $car = $container->get('car'); $this->assertEquals(EngineMarkTwo::NAME, $car->getEngineName()); } public function testCircularReference(): void { $config = ContainerConfig::create() ->withDefinitions([ TreeItem::class => TreeItem::class, ]); $container = new Container($config); $this->expectException(CircularReferenceException::class); $container->get(TreeItem::class); } /** * @link https://github.com/yiisoft/di/pull/189 */ public function testFalsePositiveCircularReferenceWithClassID(): void { $this->expectNotToPerformAssertions(); $container = new Container(); // Build an object $container->get(ColorPink::class); // set definition to container (fn(string $id, $definition) => $this->addDefinition($id, $definition))->call( $container, ColorPink::class, ColorPink::class, ); try { // Build an object $container->get(ColorPink::class); } catch (CircularReferenceException) { $this->fail('Circular reference detected false positively.'); } } /** * @link https://github.com/yiisoft/di/pull/189 */ public function testFalsePositiveCircularReferenceWithStringID(): void { $this->expectNotToPerformAssertions(); $container = new Container(); try { // Build an object $container->get('test'); } catch (NotFoundException) { // It is expected } // set definition to container (fn(string $id, $definition) => $this->addDefinition($id, $definition))->call( $container, 'test', ColorPink::class, ); try { // Build an object $container->get('test'); } catch (CircularReferenceException) { $this->fail('Circular reference detected false positively.'); } } public function testCallable(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, 'test' => fn(ContainerInterface $container) => $container->get(EngineInterface::class), ]); $container = new Container($config); $object = $container->get('test'); $this->assertInstanceOf(EngineMarkOne::class, $object); } public function testCallableWithInjector(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, 'car' => fn(CarFactory $factory, Injector $injector) => $injector->invoke($factory->create(...)), ]); $container = new Container($config); $engine = $container->get(EngineInterface::class); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $this->assertSame($engine, $car->getEngine()); } public function testCallableWithArgs(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine1' => fn(EngineFactory $factory) => $factory->createByName(EngineMarkOne::NAME), 'engine2' => fn(EngineFactory $factory) => $factory->createByName(EngineMarkTwo::NAME), ]); $container = new Container($config); $engine1 = $container->get('engine1'); $this->assertInstanceOf(EngineMarkOne::class, $engine1); $this->assertSame(EngineMarkOne::NUMBER, $engine1->getNumber()); $engine2 = $container->get('engine2'); $this->assertInstanceOf(EngineMarkTwo::class, $engine2); $this->assertSame(EngineMarkTwo::NUMBER, $engine2->getNumber()); } public function testCallableWithDependencies(): void { $config = ContainerConfig::create() ->withDefinitions([ 'car1' => fn(CarFactory $carFactory, EngineFactory $engineFactory) => $carFactory->createByEngineName( $engineFactory, EngineMarkOne::NAME, ), 'car2' => fn(CarFactory $carFactory, EngineFactory $engineFactory) => $carFactory->createByEngineName( $engineFactory, EngineMarkTwo::NAME, ), ]); $container = new Container($config); $car1 = $container->get('car1'); $this->assertInstanceOf(Car::class, $car1); $this->assertInstanceOf(EngineMarkOne::class, $car1->getEngine()); $car2 = $container->get('car2'); $this->assertInstanceOf(Car::class, $car2); $this->assertInstanceOf(EngineMarkTwo::class, $car2->getEngine()); } public function testObject(): void { $engine = new EngineMarkOne(); $config = ContainerConfig::create() ->withDefinitions([ 'engine' => $engine, ]); $container = new Container($config); $object = $container->get('engine'); $this->assertSame($engine, $object); } public function testArrayStaticCall(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, 'car' => CarFactory::create(...), ]); $container = new Container($config); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $this->assertInstanceOf(EngineMarkOne::class, $car->getEngine()); } public function testArrayDynamicCall(): void { $config = ContainerConfig::create() ->withDefinitions([ ColorInterface::class => ColorPink::class, 'car' => [CarFactory::class, 'createWithColor'], ]); $container = new Container($config); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $this->assertInstanceOf(ColorPink::class, $car->getColor()); } public function testArrayDynamicCallWithObject(): void { $config = ContainerConfig::create() ->withDefinitions([ ColorInterface::class => ColorPink::class, 'car' => [new CarFactory(), 'createWithColor'], ]); $container = new Container($config); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $this->assertInstanceOf(ColorPink::class, $car->getColor()); } public function testInvokeable(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine' => EngineMarkOne::class, 'invokeable' => new InvokableCarFactory(), ]); $container = new Container($config); $object = $container->get('invokeable'); $this->assertInstanceOf(Car::class, $object); } public function testReference(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine' => EngineMarkOne::class, 'color' => ColorPink::class, 'car' => [ 'class' => Car::class, '__construct()' => [ Reference::to('engine'), ], '$color' => Reference::to('color'), ], ]); $container = new Container($config); $object = $container->get('car'); $this->assertInstanceOf(Car::class, $object); $this->assertInstanceOf(ColorPink::class, $object->color); } public function testReferencesInArrayInDependencies(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine1' => EngineMarkOne::class, 'engine2' => EngineMarkTwo::class, 'engine3' => EngineMarkTwo::class, 'engine4' => EngineMarkTwo::class, 'car' => [ 'class' => Car::class, '__construct()' => [ Reference::to('engine1'), [ 'engine2' => Reference::to('engine2'), 'more' => [ 'engine3' => Reference::to('engine3'), 'more' => [ 'engine4' => Reference::to('engine4'), ], ], ], ], ], ]); $container = new Container($config); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $moreEngines = $car->getMoreEngines(); $this->assertSame($container->get('engine2'), $moreEngines['engine2']); $this->assertSame($container->get('engine3'), $moreEngines['more']['engine3']); $this->assertSame($container->get('engine4'), $moreEngines['more']['more']['engine4']); } public function testReferencesInProperties(): void { $color = new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ColorInterface::class => $color, 'car' => [ 'class' => Car::class, '$color' => Reference::to(ColorInterface::class), ], ]); $container = new Container($config); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $this->assertSame($color, $car->getColor()); } public function testReferencesInMethodCall(): void { $color = new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, ColorInterface::class => $color, 'car' => [ 'class' => Car::class, 'setColor()' => [Reference::to(ColorInterface::class)], ], ]); $container = new Container($config); $car = $container->get('car'); $this->assertInstanceOf(Car::class, $car); $this->assertSame($color, $car->getColor()); } public function testCallableArrayValueInConstructor(): void { $array = [ [EngineMarkTwo::class, 'getNumber'], ]; $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, Car::class => [ 'class' => Car::class, '__construct()' => [ Reference::to(EngineInterface::class), $array, ], ], ]); $container = new Container($config); /** @var Car $object */ $object = $container->get(Car::class); $this->assertSame($array, $object->getMoreEngines()); } public function testSameInstance(): void { $config = ContainerConfig::create() ->withDefinitions([ 'engine' => EngineMarkOne::class, ]); $container = new Container($config); $one = $container->get('engine'); $two = $container->get('engine'); $this->assertSame($one, $two); } public function testGetByClassIndirectly(): void { $number = 42; $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, EngineMarkOne::class => [ 'setNumber()' => [$number], ], ]); $container = new Container($config); $engine = $container->get(EngineInterface::class); $this->assertInstanceOf(EngineMarkOne::class, $engine); $this->assertSame($number, $engine->getNumber()); } public function testThrowingNotFoundException(): void { $this->expectException(NotFoundException::class); $container = new Container(); $container->get('non_existing'); } public function testContainerInContainer(): void { $config = ContainerConfig::create() ->withDefinitions([ 'container' => static fn(ContainerInterface $container) => $container, ]); $container = new Container($config); $this->assertSame($container, $container->get('container')); $this->assertSame($container, $container->get(ContainerInterface::class)); } public function testTagsInArrayDefinition(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine'], ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, 'tags' => ['engine'], ], ]); $container = new Container($config); $engines = $container->get('tag@engine'); $this->assertIsArray($engines); $this->assertSame(EngineMarkOne::class, $engines[0]::class); $this->assertSame(EngineMarkTwo::class, $engines[1]::class); } public function testTagsInClosureDefinition(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'definition' => fn() => new EngineMarkOne(), 'tags' => ['engine'], ], EngineMarkTwo::class => [ 'definition' => fn() => new EngineMarkTwo(), 'tags' => ['engine'], ], ]); $container = new Container($config); $engines = $container->get('tag@engine'); $this->assertIsArray($engines); $this->assertSame(EngineMarkOne::class, $engines[0]::class); $this->assertSame(EngineMarkTwo::class, $engines[1]::class); } public function testTagsMultiple(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine', 'mark_one'], ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, 'tags' => ['engine'], ], ]); $container = new Container($config); $engines = $container->get('tag@engine'); $markOneEngines = $container->get('tag@mark_one'); $this->assertIsArray($engines); $this->assertSame(EngineMarkOne::class, $engines[0]::class); $this->assertSame(EngineMarkTwo::class, $engines[1]::class); $this->assertIsArray($markOneEngines); $this->assertSame(EngineMarkOne::class, $markOneEngines[0]::class); $this->assertCount(1, $markOneEngines); } public function testTagsEmpty(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, ], ]); $container = new Container($config); $engines = $container->get('tag@engine'); $this->assertIsArray($engines); $this->assertCount(0, $engines); } public function testTagsWithExternalDefinition(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine'], ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, ], ]) ->withTags(['engine' => [EngineMarkTwo::class]]); $container = new Container($config); $engines = $container->get('tag@engine'); $this->assertIsArray($engines); $this->assertCount(2, $engines); $this->assertSame(EngineMarkOne::class, $engines[1]::class); $this->assertSame(EngineMarkTwo::class, $engines[0]::class); } public function testTagsWithExternalDefinitionMerge(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine'], ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, 'tags' => ['engine'], ], ]) ->withTags(['mark_two' => [EngineMarkTwo::class]]); $container = new Container($config); $engines = $container->get('tag@engine'); $markTwoEngines = $container->get('tag@mark_two'); $this->assertIsArray($engines); $this->assertCount(2, $engines); $this->assertSame(EngineMarkOne::class, $engines[0]::class); $this->assertSame(EngineMarkTwo::class, $engines[1]::class); $this->assertIsArray($markTwoEngines); $this->assertCount(1, $markTwoEngines); $this->assertSame(EngineMarkTwo::class, $markTwoEngines[0]::class); } public function testTagsAsArrayInConstructor(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'tags' => ['engine'], ], EngineMarkTwo::class => [ 'class' => EngineMarkTwo::class, 'tags' => ['engine'], ], Car::class => [ '__construct()' => ['moreEngines' => Reference::to('tag@engine')], ], ]); $container = new Container($config); $engines = $container ->get(Car::class) ->getMoreEngines(); $this->assertIsArray($engines); $this->assertCount(2, $engines); $this->assertSame(EngineMarkOne::class, $engines[0]::class); $this->assertSame(EngineMarkTwo::class, $engines[1]::class); } public static function dataResetter(): array { return [ 'strict-mode' => [true], 'non-strict-mode' => [false], ]; } #[DataProvider('dataResetter')] public function testResetter(bool $strictMode): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => function () { $this->number = 42; }, ], ]) ->withStrictMode($strictMode); $container = new Container($config); $engine = $container->get(EngineInterface::class); $this->assertSame( 42, $container ->get(EngineInterface::class) ->getNumber(), ); $engine->setNumber(45); $this->assertSame( 45, $container ->get(EngineInterface::class) ->getNumber(), ); $container ->get(StateResetter::class) ->reset(); $this->assertSame($engine, $container->get(EngineInterface::class)); $this->assertSame(42, $engine->getNumber()); } public function testResetterInDelegates(): void { $config = ContainerConfig::create() ->withDelegates([ static function (ContainerInterface $container) { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => function () { $this->number = 42; }, ], ]); return new Container($config); }, ]); $container = new Container($config); $engine = $container->get(EngineInterface::class); $this->assertSame( 42, $container ->get(EngineInterface::class) ->getNumber(), ); $engine->setNumber(45); $this->assertSame( 45, $container ->get(EngineInterface::class) ->getNumber(), ); $container ->get(StateResetter::class) ->reset(); $this->assertSame($engine, $container->get(EngineInterface::class)); $this->assertSame(42, $engine->getNumber()); } public function testNewContainerDefinitionInDelegates(): void { $firstContainer = null; $secondContainer = null; $config = ContainerConfig::create() ->withDefinitions([ ContainerInterface::class => new Container(), ]) ->withDelegates([ function (ContainerInterface $container) use (&$firstContainer): ContainerInterface { $firstContainer = $container; return new Container(); }, function (ContainerInterface $container) use (&$secondContainer): ContainerInterface { $secondContainer = $container; return new Container(); }, ]); $originalContainer = new Container($config); $container = $originalContainer->get(ContainerInterface::class); $this->assertNotSame($container, $originalContainer); $this->assertSame($container, $firstContainer); $this->assertSame($container, $secondContainer); } public function testResetterInDelegatesWithCustomResetter(): void { $config = ContainerConfig::create() ->withDelegates([ static function (ContainerInterface $container) { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => function () { $this->number = 42; }, ], ]); return new Container($config); }, ]) ->withDefinitions([ Car::class => [ 'class' => Car::class, 'setColor()' => [new ColorPink()], ], StateResetter::class => [ 'class' => StateResetter::class, 'setResetters()' => [ [ Car::class => function () { $this->color = new ColorPink(); }, ], ], ], ]); $container = new Container($config); $engine = $container->get(EngineInterface::class); $this->assertSame( 42, $container ->get(EngineInterface::class) ->getNumber(), ); $car = $container->get(Car::class); $this->assertInstanceOf( ColorPink::class, $container ->get(Car::class) ->getColor(), ); $engine->setNumber(45); $this->assertSame( 45, $container ->get(EngineInterface::class) ->getNumber(), ); $car->setColor(new ColorRed()); $this->assertInstanceOf( ColorRed::class, $container ->get(Car::class) ->getColor(), ); $container ->get(StateResetter::class) ->reset(); $this->assertSame($engine, $container->get(EngineInterface::class)); $this->assertSame(42, $engine->getNumber()); $this->assertSame($car, $container->get(Car::class)); $this->assertInstanceOf( ColorPink::class, $container ->get(Car::class) ->getColor(), ); } public static function dataResetterInProviderDefinitions(): array { return [ 'strict-mode' => [true], 'non-strict-mode' => [false], ]; } #[DataProvider('dataResetterInProviderDefinitions')] public function testResetterInProviderDefinitions(bool $strictMode): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], ], ]) ->withProviders([ new class implements ServiceProviderInterface { public function getDefinitions(): array { return [ StateResetter::class => static function (ContainerInterface $container) { $resetter = new StateResetter($container); $resetter->setResetters([ EngineInterface::class => function () { $this->number = 42; }, ]); return $resetter; }, ]; } public function getExtensions(): array { return []; } }, ]) ->withStrictMode($strictMode); $container = new Container($config); $engine = $container->get(EngineInterface::class); $engine->setNumber(45); $container ->get(StateResetter::class) ->reset(); $this->assertSame($engine, $container->get(EngineInterface::class)); $this->assertSame(42, $engine->getNumber()); } public function testResetterInProviderExtensions(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], ], ]) ->withProviders([ new class implements ServiceProviderInterface { public function getDefinitions(): array { return []; } public function getExtensions(): array { return [ StateResetter::class => static function ( ContainerInterface $container, StateResetter $resetter, ) { $resetter->setResetters([ EngineInterface::class => function () { $this->number = 42; }, ]); return $resetter; }, ]; } }, ]); $container = new Container($config); $engine = $container->get(EngineInterface::class); $engine->setNumber(45); $container ->get(StateResetter::class) ->reset(); $this->assertSame($engine, $container->get(EngineInterface::class)); $this->assertSame(42, $engine->getNumber()); } public function testNestedResetter(): void { $color = new ColorPink(); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => function () { $this->number = 42; }, ], ColorInterface::class => $color, Car::class => [ 'class' => Car::class, 'setColor()' => [DynamicReference::to(fn() => $color)], 'reset' => function (ContainerInterface $container) { $this->color = $container->get(ColorInterface::class); }, ], ]); $container = new Container($config); $engine = $container->get(EngineInterface::class); $car = $container->get(Car::class); $this->assertSame($engine, $car->getEngine()); $this->assertInstanceOf(EngineMarkOne::class, $car->getEngine()); $engine->setNumber(45); $car->setColor(new ColorRed()); $this->assertSame( 45, $container ->get(Car::class) ->getEngine() ->getNumber(), ); $this->assertSame( 'red', $container ->get(Car::class) ->getColor() ->getColor(), ); $container ->get(StateResetter::class) ->reset(); $this->assertSame($engine, $container->get(EngineInterface::class)); $this->assertSame($car, $container->get(Car::class)); $this->assertSame( 42, $car ->getEngine() ->getNumber(), ); $this->assertSame($color, $car->getColor()); } public function testResetterInCompositeContainer(): void { $composite = new CompositeContainer(); $config = ContainerConfig::create() ->withDefinitions([ 'engineMarkOne' => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => function () { $this->number = 42; }, ], ]); $firstContainer = new Container($config); $config = ContainerConfig::create() ->withDefinitions([ 'engineMarkTwo' => [ 'class' => EngineMarkTwo::class, 'setNumber()' => [43], 'reset' => function () { $this->number = 43; }, ], ]); $secondContainer = new Container($config); $composite->attach($firstContainer); $composite->attach($secondContainer); $engineMarkOne = $composite->get('engineMarkOne'); $engineMarkTwo = $composite->get('engineMarkTwo'); $this->assertSame( 42, $composite ->get('engineMarkOne') ->getNumber(), ); $this->assertSame( 43, $composite ->get('engineMarkTwo') ->getNumber(), ); $engineMarkOne->setNumber(45); $engineMarkTwo->setNumber(46); $this->assertSame( 45, $composite ->get('engineMarkOne') ->getNumber(), ); $this->assertSame( 46, $composite ->get('engineMarkTwo') ->getNumber(), ); $composite ->get(StateResetter::class) ->reset(); $this->assertSame($engineMarkOne, $composite->get('engineMarkOne')); $this->assertSame($engineMarkTwo, $composite->get('engineMarkTwo')); $this->assertSame( 42, $composite ->get('engineMarkOne') ->getNumber(), ); $this->assertSame( 43, $composite ->get('engineMarkTwo') ->getNumber(), ); } public function testCircularReferenceExceptionWhileResolvingProviders(): void { $provider = new class implements ServiceProviderInterface { public function getDefinitions(): array { return [ // wrapping container with proxy class ContainerInterface::class => static fn(ContainerInterface $container) => $container, ]; } public function getExtensions(): array { return []; } }; $this->expectException(BuildingException::class); $this->expectExceptionMessage( 'Caught unhandled error "RuntimeException" while building "Yiisoft\Di\Tests\Support\B".', ); $config = ContainerConfig::create() ->withDefinitions([ B::class => function () { throw new RuntimeException(); }, ]) ->withProviders([$provider]); $container = new Container($config); $container->get(B::class); } public function testDifferentContainerWithProviders(): void { $provider = new class implements ServiceProviderInterface { public function getDefinitions(): array { return [ ContainerInterface::class => static fn(ContainerInterface $container) => new Container(), ]; } public function getExtensions(): array { return []; } }; $config = ContainerConfig::create() ->withProviders([$provider]); $originalContainer = new Container($config); $container = $originalContainer->get(ContainerInterface::class); $this->assertNotSame($originalContainer, $container); } public function testErrorOnMethodTypo(): void { $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition: metadata "setId" is not allowed. Did you mean "setId()" or "$setId"?', ); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'setId' => [42], ], ]); new Container($config); } public function testErrorOnPropertyTypo(): void { $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition: metadata "dev" is not allowed. Did you mean "dev()" or "$dev"?', ); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'dev' => true, ], ]); new Container($config); } public function testErrorOnDisallowMeta(): void { $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition: metadata "dev" is not allowed. Did you mean "dev()" or "$dev"?', ); $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => [ 'class' => EngineMarkOne::class, 'tags' => ['a', 'b'], 'dev' => 42, ], ]); new Container($config); } public function testDelegateLookup(): void { $delegate = static function (ContainerInterface $container) { $config = ContainerConfig::create() ->withDefinitions([ EngineInterface::class => EngineMarkOne::class, SportCar::class => ['__construct()' => ['maxSpeed' => 300]], ]); return new Container($config); }; $config = ContainerConfig::create() ->withDefinitions([ Garage::class => Garage::class, EngineInterface::class => EngineMarkTwo::class, ]) ->withValidate(true) ->withDelegates([$delegate]); $container = new Container($config); $garage = $container->get(Garage::class); $this->assertInstanceOf(Garage::class, $garage); $this->assertInstanceOf( EngineMarkOne::class, $garage ->getCar() ->getEngine(), ); } public function testNonClosureDelegate(): void { $config = ContainerConfig::create() ->withDelegates([42]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Delegate must be callable in format "function (ContainerInterface $container): ContainerInterface".', ); new Container($config); } public function testNonContainerDelegate(): void { $config = ContainerConfig::create() ->withDelegates([ static fn(ContainerInterface $container) => 42, ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Delegate callable must return an object that implements ContainerInterface.', ); new Container($config); } public function testExtensibleServiceDefinition(): void { $config = ContainerConfig::create() ->withDefinitions([ 'test' => new ExtensibleService([], 'test'), ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition. ExtensibleService is only allowed in provider extensions.', ); new Container($config); } public function testWrongTag(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'tags' => ['engine', 42], ], ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid tag. Expected a string, got 42.', ); new Container($config); } public function testNumberProvider(): void { $config = ContainerConfig::create() ->withProviders([42]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessageMatches( '/^Service provider should be a class name or an instance of ' . preg_quote(ServiceProviderInterface::class, '/') . '\. (integer|int) given\.$/', ); new Container($config); } public function testNonServiceProviderInterfaceProvider(): void { $config = ContainerConfig::create() ->withProviders([stdClass::class]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Service provider should be an instance of ' . ServiceProviderInterface::class . '.' . ' stdClass given.', ); new Container($config); } public function testStrictModeDisabled(): void { $config = ContainerConfig::create() ->withStrictMode(false); $container = new Container($config); $this->assertTrue($container->has(EngineMarkOne::class)); $engine = $container->get(EngineMarkOne::class); $this->assertInstanceOf(EngineMarkOne::class, $engine); } public function testStrictModeEnabled(): void { $config = ContainerConfig::create() ->withStrictMode(true); $container = new Container($config); $this->assertFalse($container->has(EngineMarkOne::class)); $this->expectException(NotFoundExceptionInterface::class); $container->get(EngineMarkOne::class); } public function testIntegerKeyInExtensions(): void { $config = ContainerConfig::create() ->withProviders([ new class implements ServiceProviderInterface { public function getDefinitions(): array { return []; } public function getExtensions(): array { return [ 23 => static fn(ContainerInterface $container, StateResetter $resetter) => $resetter, ]; } }, ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage('Extension key must be a service ID as string, 23 given.'); new Container($config); } public function testNonCallableExtension(): void { $config = ContainerConfig::create() ->withProviders([ new class implements ServiceProviderInterface { public function getDefinitions(): array { return []; } public function getExtensions(): array { return [ ColorPink::class => [], ]; } }, ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage('Extension of service should be callable, array given.'); new Container($config); } public function testNonArrayReset(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'reset' => 42, ], ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition: "reset" should be closure, int given.', ); new Container($config); } public function testNonArrayTags(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => [42], 'tags' => 'hello', ], ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition: tags should be array of strings, string given.', ); new Container($config); } public function testNonArrayArguments(): void { $config = ContainerConfig::create() ->withDefinitions([ EngineMarkOne::class => [ 'class' => EngineMarkOne::class, 'setNumber()' => 42, ], ]); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage( 'Invalid definition: incorrect method "setNumber()" arguments. Expected array, got "int". Probably you should wrap them into square brackets.', ); $container = new Container($config); } public static function dataInvalidTags(): array { return [ [ '/^Invalid tags configuration: tag should be string, 42 given\.$/', [42 => [EngineMarkTwo::class]], ], [ '/^Invalid tags configuration: tag should contain array of service IDs, (integer|int) given\.$/', ['engine' => 42], ], [ '/^Invalid tags configuration: service should be defined as class string, (integer|int) given\.$/', ['engine' => [42]], ], ]; } #[DataProvider('dataInvalidTags')] public function testInvalidTags(string $message, array $tags): void { $config = ContainerConfig::create() ->withTags($tags); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessageMatches($message); new Container($config); } public static function dataNotFoundExceptionMessageWithDefinitions(): array { return [ 'without-definition' => [[]], 'with-definition' => [[SportCar::class => SportCar::class]], ]; } #[DataProvider('dataNotFoundExceptionMessageWithDefinitions')] public function testNotFoundExceptionMessageWithDefinitions(array $definitions): void { $config = ContainerConfig::create()->withDefinitions($definitions); $container = new Container($config); $this->expectException(NotFoundException::class); $this->expectExceptionMessage( 'No definition or class found or resolvable for "' . EngineInterface::class . '" while building "' . SportCar::class . '" -> "' . EngineInterface::class . '".', ); $container->get(SportCar::class); } public function testNotFoundExceptionWithNotYiiContainer(): void { $config = ContainerConfig::create() ->withDefinitions([ ContainerInterface::class => new SimpleContainer(), SportCar::class => SportCar::class, ]); $container = new Container($config); $exception = null; try { $container->get(SportCar::class); } catch (Throwable $e) { $exception = $e; } $this->assertInstanceOf(NotFoundException::class, $exception); $this->assertSame( 'No definition or class found or resolvable for "' . SportCar::class . '" while building it.', $exception->getMessage(), ); $this->assertInstanceOf( \Yiisoft\Test\Support\Container\Exception\NotFoundException::class, $exception->getPrevious(), ); } public function testExceptionOnGetInDelegate(): void { $container = new Container( ContainerConfig::create()->withDelegates([ static fn() => new SimpleContainer( factory: static fn() => throw new RuntimeException('Error in delegate'), ), ]), ); $exception = null; try { $container->get('identifier'); } catch (Throwable $exception) { } $this->assertInstanceOf(BuildingException::class, $exception); $this->assertSame( 'Caught unhandled error "Error in delegate" while building "identifier".', $exception->getMessage(), ); $previous = $exception->getPrevious(); $this->assertInstanceOf(RuntimeException::class, $previous); $this->assertSame('Error in delegate', $previous->getMessage()); } public function testExceptionOnHasInDelegate(): void { $container = new Container( ContainerConfig::create()->withDelegates([ static fn() => new SimpleContainer( hasCallback: static fn() => throw new RuntimeException('Error in delegate'), ), ]), ); $exception = null; try { $container->get('identifier'); } catch (Throwable $exception) { } $this->assertInstanceOf(BuildingException::class, $exception); $this->assertSame( 'Caught unhandled error "Error in delegate" while building "identifier".', $exception->getMessage(), ); $previous = $exception->getPrevious(); $this->assertInstanceOf(RuntimeException::class, $previous); $this->assertSame('Error in delegate', $previous->getMessage()); } public function testGetStateResetterTwice(): void { $container = new Container(); $resetter1 = $container->get(StateResetter::class); $resetter2 = $container->get(StateResetter::class); assertInstanceOf(StateResetter::class, $resetter1); assertSame($resetter1, $resetter2); } } ================================================ FILE: tests/Unit/Helpers/DefinitionParserTest.php ================================================ new EngineMarkOne(); $definition = [ 'definition' => $fn, 'tags' => ['one', 'two'], ]; [$definition, $meta] = DefinitionParser::parse($definition); $this->assertSame($fn, $definition); $this->assertSame(['tags' => ['one', 'two']], $meta); } public function testParseArrayCallableDefinition(): void { $definition = [ 'definition' => [StaticFactory::class, 'create'], 'tags' => ['one', 'two'], ]; [$definition, $meta] = DefinitionParser::parse($definition); $this->assertSame([StaticFactory::class, 'create'], $definition); $this->assertSame(['tags' => ['one', 'two']], $meta); } public function testParseArrayDefinition(): void { $definition = [ 'class' => EngineMarkOne::class, '__construct()' => [42], 'tags' => ['one', 'two'], ]; [$definition, $meta] = DefinitionParser::parse($definition); $this->assertSame([ 'class' => EngineMarkOne::class, '__construct()' => [42], 'methodsAndProperties' => [], DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA => true, ], $definition); $this->assertSame(['tags' => ['one', 'two']], $meta); } } ================================================ FILE: tests/Unit/LeaguePsrContainerTest.php ================================================ setupContainer(new Container(), $definitions); } public function setupContainer(ContainerInterface $container, iterable $definitions = []): ContainerInterface { foreach ($definitions as $id => $definition) { $container->add($id, $definition); } return $container; } } ================================================ FILE: tests/Unit/NotFoundExceptionTest.php ================================================ assertSame('test', $exception->getId()); $this->assertSame('No definition or class found for "test" ID.', $exception->getName()); $this->assertSame( <<getSolution(), ); } public function testMessage(): void { $exception = new NotFoundException('test'); $this->assertSame('No definition or class found or resolvable for "test".', $exception->getMessage()); } public function testBuildStack(): void { $exception = new NotFoundException('test', ['a', 'b', 'test']); $this->assertSame( 'No definition or class found or resolvable for "test" while building "a" -> "b" -> "test".', $exception->getMessage(), ); } public function testCode(): void { $exception = new NotFoundException('test'); $this->assertSame(0, $exception->getCode()); } } ================================================ FILE: tests/Unit/PsrContainerTestAbstract.php ================================================ createContainer(); $this->expectException(ContainerExceptionInterface::class); $container->get(Chicken::class); } public function testSimpleDefinition(): void { $container = $this->createContainer([ EngineInterface::class => EngineMarkOne::class, ]); $one = $container->get(EngineInterface::class); $this->assertInstanceOf(EngineMarkOne::class, $one); } public function testClassSimple(): void { $container = $this->createContainer(['engine' => EngineMarkOne::class]); $this->assertInstanceOf(EngineMarkOne::class, $container->get('engine')); } public function testSetAll(): void { $container = $this->createContainer([ 'engine1' => EngineMarkOne::class, 'engine2' => EngineMarkTwo::class, ]); $this->assertInstanceOf(EngineMarkOne::class, $container->get('engine1')); $this->assertInstanceOf(EngineMarkTwo::class, $container->get('engine2')); } public function testObject(): void { $container = $this->createContainer([ 'engine' => new EngineMarkOne(), ]); $object = $container->get('engine'); $this->assertInstanceOf(EngineMarkOne::class, $object); } public function testThrowingNotFoundException(): void { $this->expectException(NotFoundExceptionInterface::class); $container = $this->createContainer(); $container->get('non_existing'); } } ================================================ FILE: tests/Unit/Reference/TagReference/Resolve/A.php ================================================ withDefinitions([ Main::class => [ '$data' => TagReference::to('letters'), ], ]) ->withTags([ 'letters' => [A::class, B::class], ]), ); $main = $container->get(Main::class); $this->assertCount(2, $main->data); $this->assertInstanceOf(A::class, $main->data[0]); $this->assertInstanceOf(B::class, $main->data[1]); } } ================================================ FILE: tests/Unit/Reference/TagReference/TagReferenceTest.php ================================================ expectException(Error::class); new TagReference(); } public function testConstructor(): void { $reflection = new ReflectionClass(TagReference::class); $reflectionMethod = $reflection->getConstructor(); $this->assertTrue($reflectionMethod->isPrivate()); $reflectionMethod->invoke($reflection->newInstanceWithoutConstructor()); } public function testAliases(): void { $this->assertFalse(TagReference::isTagAlias('test')); $this->assertFalse(TagReference::isTagAlias('tag#test')); $this->assertTrue(TagReference::isTagAlias('tag@test')); } public function testExtractTag(): void { $this->assertEquals('test', TagReference::extractTagFromAlias('tag@test')); } public function testExtractWrongTagDelimiter(): void { $this->expectException(InvalidArgumentException::class); TagReference::extractTagFromAlias('tag#test'); } public function testExtractWrongTagFormat(): void { $this->expectException(InvalidArgumentException::class); TagReference::extractTagFromAlias('test'); } public function testReference(): void { $reference = TagReference::to('test'); $spyContainer = new class implements ContainerInterface { public function get($id) { return $id; } public function has($id): bool { return true; } }; $result = $reference->resolve($spyContainer); $this->assertEquals('tag@test', $result); } public function testId(): void { $this->assertSame('tag@test', TagReference::id('test')); } } ================================================ FILE: tests/Unit/ServiceProviderTest.php ================================================ ensureProviderRegisterDefinitions(CarProvider::class); $this->ensureProviderRegisterExtensions(CarExtensionProvider::class); } public function testAddProviderByInstance(): void { $this->ensureProviderRegisterDefinitions(new CarProvider()); $this->ensureProviderRegisterExtensions(new CarExtensionProvider()); } public function testNotExistedExtension(): void { $this->expectException(InvalidConfigException::class); $config = ContainerConfig::create() ->withProviders([ CarProvider::class, ]); new Container($config); } public function testContainerInterfaceExtension(): void { $this->expectException(InvalidConfigException::class); $config = ContainerConfig::create() ->withProviders([ ContainerInterfaceExtensionProvider::class, ]); new Container($config); } public function testExtensionOverride(): void { $config = ContainerConfig::create() ->withDefinitions([ Car::class => Car::class, 'sport_car' => SportCar::class, ]) ->withProviders([ CarProvider::class, CarExtensionProvider::class, ]); $container = new Container($config); $this->assertInstanceOf( ColorRed::class, $container ->get(Car::class) ->getColor(), ); } public function testExtensionReturnedNull(): void { $config = ContainerConfig::create() ->withDefinitions([ Car::class => Car::class, 'sport_car' => SportCar::class, ]) ->withProviders([ CarProvider::class, NullCarExtensionProvider::class, CarExtensionProvider::class, ]); $container = new Container($config); $this->assertInstanceOf( ColorRed::class, $container ->get(Car::class) ->getColor(), ); } public function testClassMethodsWithExtensible(): void { $config = ContainerConfig::create() ->withDefinitions([ 'method_test' => [ 'class' => MethodTestClass::class, 'setValue()' => [42], ], ]) ->withProviders([ new class implements ServiceProviderInterface { public function getDefinitions(): array { return []; } public function getExtensions(): array { return [ 'method_test' => static fn(ContainerInterface $container, MethodTestClass $class) => $class, ]; } }, ]); $container = new Container($config); /** @var MethodTestClass $object */ $object = $container->get('method_test'); $this->assertSame(42, $object->getValue()); } private function ensureProviderRegisterExtensions($provider): void { $config = ContainerConfig::create() ->withDefinitions([ Car::class => Car::class, EngineInterface::class => EngineMarkOne::class, 'sport_car' => SportCar::class, ]) ->withProviders([$provider]); $container = new Container($config); $this->assertTrue($container->has(Car::class)); $this->assertTrue($container->has(EngineInterface::class)); $this->assertInstanceOf(Car::class, $container->get(Car::class)); $this->assertInstanceOf( ColorRed::class, $container ->get(Car::class) ->getColor(), ); $this->assertInstanceOf( EngineMarkTwo::class, $container ->get(Car::class) ->getEngine(), ); } private function ensureProviderRegisterDefinitions($provider): void { $container = new Container(); $this->assertFalse( $container->has(Car::class), 'Container should not have Car registered before service provider added due to autoload fallback.', ); $this->assertFalse( $container->has('car'), 'Container should not have "car" registered before service provider added.', ); $this->assertFalse( $container->has(EngineInterface::class), 'Container should not have EngineInterface registered before service provider added.', ); $config = ContainerConfig::create() ->withDefinitions([ Car::class => Car::class, 'sport_car' => SportCar::class, ]) ->withProviders([$provider]); $container = new Container($config); // ensure addProvider invoked ServiceProviderInterface::register $this->assertTrue( $container->has('car'), 'CarProvider should have registered "car" once it was added to container.', ); $this->assertTrue( $container->has(EngineInterface::class), 'CarProvider should have registered EngineInterface once it was added to container.', ); } } ================================================ FILE: tests/Unit/StateResetterTest.php ================================================ expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'State resetter object should be instance of "' . StateResetter::class . '", "stdClass" given.', ); $resetter->setResetters([ new stdClass(), ]); } public function testStateResetterObjectForService(): void { $resetter = new StateResetter(new SimpleContainer()); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'Callback for state resetter should be closure in format ' . '`function (ContainerInterface $container): void`. ' . 'Got "' . StateResetter::class . '".', ); $resetter->setResetters([ Car::class => $resetter, ]); } public function testResetNonObject(): void { $resetter = new StateResetter( new SimpleContainer([ 'value' => 42, ]), ); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessageMatches( '/^State resetter supports resetting objects only\. Container returned (integer|int)\.$/', ); $resetter->setResetters([ 'value' => static function () {}, ]); } } ================================================ FILE: tests/Unit/YiisoftPsrContainerTest.php ================================================ withDefinitions($definitions); return new Container($config); } } ================================================ FILE: tools/.gitignore ================================================ /*/vendor /*/composer.lock ================================================ FILE: tools/infection/composer.json ================================================ { "require-dev": { "infection/infection": "^0.26 || ^0.31.9" }, "config": { "allow-plugins": { "infection/extension-installer": true } } } ================================================ FILE: tools/psalm/composer.json ================================================ { "require-dev": { "vimeo/psalm": "^5.26.1 || ^6.12" } }