├── .github
├── FUNDING.yml
├── SECURITY.md
├── workflows
│ ├── bc-check.yml
│ ├── phpstan.yml
│ ├── pint.yml
│ ├── infection.yml
│ └── run-tests.yml
└── CONTRIBUTING.md
├── .editorconfig
├── tests
├── Boilerplate
│ ├── BoilerplateInterface.php
│ ├── Services
│ │ ├── TestServiceInterface.php
│ │ ├── TestService.php
│ │ └── Users
│ │ │ └── UserService.php
│ ├── Repositories
│ │ ├── TestRepositoryInterface.php
│ │ ├── TestRepository.php
│ │ └── Users
│ │ │ └── UserRepository.php
│ ├── Models
│ │ └── TestModel.php
│ ├── Builder
│ │ └── Best
│ │ │ └── BestBuilder.php
│ ├── Builders
│ │ └── Best
│ │ │ └── BestBuilder.php
│ ├── BoilerplateServiceWithWrongContext.php
│ ├── BoilerplateServiceWithConstructorClass.php
│ ├── BoilerplateServiceResolvesGlobalInMethod.php
│ ├── Domain
│ │ └── Best
│ │ │ └── BestDomain.php
│ ├── BoilerplateService.php
│ ├── BoilerplateServiceWithVariadicConstructor.php
│ ├── BoilerplateServiceWithConstructorPrimitive.php
│ ├── ParameterOrderBoilerplate.php
│ ├── BoilerplateServiceWithConstructor.php
│ ├── BoilerplateDependenciesAssignedOldWay.php
│ ├── BoilerplateWithBootedCallProxyWrongParams.php
│ └── BoilerplateServiceResolvesContextualInMethod.php
├── TestCase.php
├── MethodBindingTest.php
├── ContainerCallTest.php
├── ForwardingTest.php
└── ContainerTest.php
├── .gitignore
├── src
├── Exceptions
│ └── InstanceInteractionException.php
├── LecServiceProvider.php
├── Helpers
│ └── helpers.php
├── Call.php
├── Core
│ ├── Forwarding.php
│ ├── MethodBinder.php
│ └── CallProxy.php
├── Traits
│ └── InteractsWithContainer.php
└── Overrides
│ ├── BoundMethod.php
│ └── Container.php
├── pint.json
├── phpstan.neon
├── LICENSE.md
├── phpunit.xml.dist
├── infection.json5
├── composer.json
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://paypal.com/donate/?hosted_button_id=KHLEL8PFS4AXJ"]
2 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | If you discover any security related issues, please email contact@observer.name instead of using the issue tracker.
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 4
6 | indent_style = space
7 | end_of_line = lf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/tests/Boilerplate/BoilerplateInterface.php:
--------------------------------------------------------------------------------
1 | boilerplateService;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/Boilerplate/BoilerplateServiceWithConstructorClass.php:
--------------------------------------------------------------------------------
1 | boilerplateService;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/Boilerplate/BoilerplateServiceResolvesGlobalInMethod.php:
--------------------------------------------------------------------------------
1 | name('laravel-enhanced-container');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Boilerplate/BoilerplateService.php:
--------------------------------------------------------------------------------
1 | boilerplates = $boilerplates;
15 | }
16 |
17 | public function test(): mixed
18 | {
19 | return $this->boilerplates;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel",
3 | "rules": {
4 | "phpdoc_separation": true,
5 | "concat_space": {
6 | "spacing": "one"
7 | },
8 | "class_attributes_separation": {
9 | "elements": {
10 | "const": "only_if_meta"
11 | }
12 | },
13 | "binary_operator_spaces": {
14 | "default": "single_space",
15 | "operators": {"=>": null, "=": "align_single_space_minimal"}
16 | },
17 | "declare_strict_types": true,
18 | "php_unit_method_casing": false
19 | },
20 | "exclude": [
21 | "src/Overrides"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/tests/Boilerplate/Repositories/TestRepository.php:
--------------------------------------------------------------------------------
1 | param;
17 | }
18 |
19 | public function getNextParam(): string
20 | {
21 | return $this->nextParam;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('testing');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Boilerplate/Services/TestService.php:
--------------------------------------------------------------------------------
1 | $class,
13 | 'dependencies' => $parameters,
14 | 'context' => $context,
15 | ]);
16 | }
17 | }
18 |
19 | if (! function_exists('bind')) {
20 |
21 | function bind(object|string $abstract): MethodBinder
22 | {
23 | return app(MethodBinder::class, ['abstract' => $abstract]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Boilerplate/ParameterOrderBoilerplate.php:
--------------------------------------------------------------------------------
1 | param) {
21 | return $count + 1;
22 | }
23 |
24 | return $count;
25 | }
26 |
27 | public function getParam(): bool
28 | {
29 | return $this->param;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/bc-check.yml:
--------------------------------------------------------------------------------
1 | name: bc-check
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | backwards-compatibility-check:
13 | name: "Backwards Compatibility Check"
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | - name: "Install PHP"
20 | uses: shivammathur/setup-php@v2
21 | with:
22 | php-version: "8.3"
23 | - name: "Install dependencies"
24 | run: "composer install"
25 | - name: "Install BC check"
26 | run: "composer require --dev roave/backward-compatibility-check"
27 | - name: "Check for BC breaks"
28 | run: "vendor/bin/roave-backward-compatibility-check"
29 |
--------------------------------------------------------------------------------
/tests/Boilerplate/BoilerplateDependenciesAssignedOldWay.php:
--------------------------------------------------------------------------------
1 | bootCallProxies();
20 | }
21 |
22 | public function getProxy(): object
23 | {
24 | return $this->proxy;
25 | }
26 |
27 | public function getProxiedClass(): object
28 | {
29 | return $this->proxy->boilerplateService;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Boilerplate/Repositories/Users/UserRepository.php:
--------------------------------------------------------------------------------
1 | bootCallProxies();
19 | }
20 |
21 | public function getProxy(): object
22 | {
23 | return $this->proxy;
24 | }
25 |
26 | public function getProxiedClass(): object
27 | {
28 | return $this->proxy->boilerplateService;
29 | }
30 |
31 | public function getOriginal(): object
32 | {
33 | return $this->boilerplateService;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/infection.yml:
--------------------------------------------------------------------------------
1 | name: infection
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | infection:
11 | name: "Running Infection"
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Setup PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: '8.3'
20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, fileinfo, sodium
21 | coverage: xdebug
22 |
23 | - name: Cache composer dependencies
24 | uses: actions/cache@v2
25 | with:
26 | path: vendor
27 | key: composer-${{ hashFiles('composer.lock') }}
28 |
29 | - name: Run composer install
30 | run: composer install -n --prefer-dist
31 |
32 | - name: Run infection
33 | run: ./vendor/bin/infection --show-mutations --min-msi=100 --min-covered-msi=100
34 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/nunomaduro/larastan/extension.neon
3 |
4 | parameters:
5 |
6 | paths:
7 | - src
8 |
9 | # The level 9 is the highest level
10 | level: max
11 |
12 | phpVersion: 80000
13 |
14 | excludePaths:
15 | - ./src/Overrides
16 |
17 | ignoreErrors:
18 | - '#Parameter \#2 \$callback of static method Illuminate\\Container\\BoundMethod\:\:call\(\) expects \(callable\(\)\: mixed\)\|string, array\{object, string\} given\.#'
19 | - '#Property MichaelRubel\\EnhancedContainer\\Exceptions\\InstanceInteractionException\:\:\$message has no type specified\.#'
20 | - '#(.*)getDependencies\(\) expects class\-string, string given\.#'
21 | - '#(.*)resolvePassedClass\(\) expects string, object\|string given\.#'
22 | - '#\(class\-string\) does not accept string\.#'
23 |
24 | checkMissingIterableValueType: false
25 |
26 | reportUnmatchedIgnoredErrors: false
27 |
28 | checkOctaneCompatibility: true
29 |
--------------------------------------------------------------------------------
/tests/Boilerplate/Services/Users/UserService.php:
--------------------------------------------------------------------------------
1 |
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 | ./src/Overrides
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tests/Boilerplate/BoilerplateServiceResolvesContextualInMethod.php:
--------------------------------------------------------------------------------
1 | boilerplate;
17 | }
18 |
19 | public function methodHasContextual(): CallProxy
20 | {
21 | return call(BoilerplateInterface::class, [], static::class);
22 | }
23 |
24 | public function methodHasContextual2(): CallProxy
25 | {
26 | return call(BoilerplateInterface::class, [], static::class);
27 | }
28 |
29 | public function methodHasContextual3(): CallProxy
30 | {
31 | return call(BoilerplateInterface::class, [], TestRepository::class);
32 | }
33 |
34 | public function methodHasGlobal(): CallProxy
35 | {
36 | return call(BoilerplateInterface::class);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/infection.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "vendor/infection/infection/resources/schema.json",
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | "logs": {
9 | "text": "php://stderr",
10 | "github": true
11 | },
12 | // "logs": {
13 | // "text": "infection.log"
14 | // },
15 | "mutators": {
16 | "@default": {
17 | "ignore": [
18 | "Illuminate\\Container\\Container",
19 | "Illuminate\\Container\\BoundMethod"
20 | ]
21 | },
22 | "PublicVisibility": {
23 | "ignore": [
24 | "MichaelRubel\\EnhancedContainer\\Traits\\BootsCallProxies::bootCallProxies"
25 | ]
26 | },
27 | "ProtectedVisibility": {
28 | "ignore": [
29 | "MichaelRubel\\EnhancedContainer\\Traits\\InteractsWithContainer"
30 | ]
31 | },
32 | "Throw_": {
33 | "ignore": [
34 | "MichaelRubel\\EnhancedContainer\\Core\\CallProxy::handleMissing"
35 | ]
36 | },
37 | "ArrayItemRemoval": {
38 | "ignore": [
39 | "MichaelRubel\\EnhancedContainer\\Core\\CallProxy::containerCall::124"
40 | ]
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Core/Forwarding.php:
--------------------------------------------------------------------------------
1 | pendingClass = $this->resolve($class);
30 |
31 | return $this;
32 | }
33 |
34 | /**
35 | * Define the forwarding for the pending class set previously.
36 | */
37 | public function to(string $destination): static
38 | {
39 | app()->bind(
40 | abstract: $this->pendingClass . static::CONTAINER_KEY,
41 | concrete: $this->resolve($destination)
42 | );
43 |
44 | return $this;
45 | }
46 |
47 | /**
48 | * Extract an implementation from the interface if passed.
49 | */
50 | protected function resolve(string $class): string
51 | {
52 | return ! interface_exists($class) ? $class : app($class)::class;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | fail-fast: true
14 | matrix:
15 | os: [ubuntu-latest]
16 | php: [8.3, 8.4]
17 | laravel: ['11.*']
18 | testbench: ['9.3']
19 | stability: [prefer-lowest, prefer-stable]
20 |
21 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
22 |
23 | steps:
24 | - name: Checkout code
25 | uses: actions/checkout@v3
26 |
27 | - name: Setup PHP
28 | uses: shivammathur/setup-php@v2
29 | with:
30 | php-version: ${{ matrix.php }}
31 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, fileinfo, sodium
32 | coverage: pcov
33 |
34 | - name: Setup problem matchers
35 | run: |
36 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
37 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
38 |
39 | - name: Install dependencies
40 | run: |
41 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:^2.64.1" --no-interaction --no-update
42 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction
43 |
44 | - name: Execute tests
45 | run: vendor/bin/phpunit --coverage-clover build/clover.xml
46 |
--------------------------------------------------------------------------------
/src/Core/MethodBinder.php:
--------------------------------------------------------------------------------
1 | abstract = $this->convertToNamespace($abstract);
24 | }
25 |
26 | /**
27 | * Method binding.
28 | */
29 | public function method(?string $method = null, ?\Closure $override = null): ?static
30 | {
31 | $this->resolve();
32 |
33 | if (is_null($method) || is_null($override)) {
34 | return $this;
35 | }
36 |
37 | return $this->{$method}($override);
38 | }
39 |
40 | /**
41 | * Try to resolve an implementation for this particular abstract type.
42 | */
43 | protected function resolve(): mixed
44 | {
45 | if (app()->bound($this->abstract)) {
46 | $concrete = app($this->abstract);
47 |
48 | $this->abstract = $this->convertToNamespace($concrete);
49 | }
50 |
51 | return $this->abstract;
52 | }
53 |
54 | /**
55 | * Bind the method to the container.
56 | */
57 | public function __call(string $method, array $parameters): void
58 | {
59 | app()->bindMethod([$this->abstract, $method], current($parameters));
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "michael-rubel/laravel-enhanced-container",
3 | "description": "This package provides DX tweaks for Service Container in Laravel.",
4 | "keywords": [
5 | "michael-rubel",
6 | "laravel",
7 | "laravel-enhanced-container",
8 | "lec"
9 | ],
10 | "homepage": "https://github.com/michael-rubel/laravel-enhanced-container",
11 | "license": "MIT",
12 | "authors": [
13 | {
14 | "name": "Michael Rubel",
15 | "email": "contact@observer.name",
16 | "role": "Maintainer"
17 | }
18 | ],
19 | "require": {
20 | "php": "^8.3|^8.4",
21 | "illuminate/contracts": "^11.21",
22 | "spatie/laravel-package-tools": "^1.9"
23 | },
24 | "require-dev": {
25 | "brianium/paratest": "^7.4",
26 | "infection/infection": "^0.27.3",
27 | "laravel/pint": "^1.0",
28 | "nunomaduro/collision": "^8.0",
29 | "nunomaduro/larastan": "^2.0",
30 | "orchestra/testbench": "^9.3",
31 | "phpunit/phpunit": "^10.5"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "MichaelRubel\\EnhancedContainer\\": "src"
36 | },
37 | "exclude-from-classmap": [
38 | "vendor/laravel/framework/src/Illuminate/Container/Container.php",
39 | "vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php"
40 | ],
41 | "files": [
42 | "src/Helpers/helpers.php",
43 | "src/Overrides/Container.php",
44 | "src/Overrides/BoundMethod.php"
45 | ]
46 | },
47 | "autoload-dev": {
48 | "psr-4": {
49 | "MichaelRubel\\EnhancedContainer\\Tests\\": "tests"
50 | }
51 | },
52 | "scripts": {
53 | "test": "./vendor/bin/testbench package:test --no-coverage",
54 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage"
55 | },
56 | "config": {
57 | "sort-packages": true,
58 | "allow-plugins": {
59 | "infection/extension-installer": true
60 | }
61 | },
62 | "extra": {
63 | "laravel": {
64 | "providers": [
65 | "MichaelRubel\\EnhancedContainer\\LecServiceProvider"
66 | ]
67 | }
68 | },
69 | "minimum-stability": "dev",
70 | "prefer-stable": true
71 | }
72 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are **welcome** and will be fully **credited**.
4 |
5 | Please read and understand the contribution guide before creating an issue or pull request.
6 |
7 | ## Etiquette
8 |
9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code
10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be
11 | extremely unfair for them to suffer abuse or anger for their hard work.
12 |
13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
14 | world that developers are civilized and selfless people.
15 |
16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
18 |
19 | ## Viability
20 |
21 | When requesting or submitting new features, first consider whether it might be useful to others. Open
22 | source projects are used by many developers, who may have entirely different needs to your own. Think about
23 | whether or not your feature is likely to be used by other users of the project.
24 |
25 | ## Procedure
26 |
27 | Before filing an issue:
28 |
29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
30 | - Check to make sure your feature suggestion isn't already present within the project.
31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
32 | - Check the pull requests tab to ensure that the feature isn't already in progress.
33 |
34 | Before submitting a pull request:
35 |
36 | - Check the codebase to ensure that your feature doesn't already exist.
37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
38 |
39 | ## Requirements
40 |
41 | If the project maintainer has any additional requirements, you will find them listed here.
42 |
43 | - # **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).
44 |
45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
46 |
47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
48 |
49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
50 |
51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
52 |
53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
54 |
55 | **Happy coding**!
56 |
--------------------------------------------------------------------------------
/src/Traits/InteractsWithContainer.php:
--------------------------------------------------------------------------------
1 | getClassForResolution($class, $context);
21 | $dependencies = $this->getDependencies($class, $dependencies);
22 |
23 | return app($class, $dependencies);
24 | }
25 |
26 | /**
27 | * Get the class for resolution.
28 | */
29 | protected function getClassForResolution(string $class, ?string $context = null): string
30 | {
31 | return isset(app()->contextual[$context])
32 | ? $this->getContextualConcrete($class, $context)
33 | : $class;
34 | }
35 |
36 | /**
37 | * Try to get the contextual concrete.
38 | */
39 | protected function getContextualConcrete(string $class, ?string $context = null): string
40 | {
41 | return app()->contextual[$context][$class] ?? $class;
42 | }
43 |
44 | /**
45 | * Try to get binding concrete.
46 | *
47 | *
48 | * @throws \ReflectionException
49 | */
50 | protected function getBindingConcrete(string $class): string
51 | {
52 | return (
53 | new \ReflectionFunction(
54 | app()->getBindings()[$class]['concrete']
55 | )
56 | )->getStaticVariables()['concrete'];
57 | }
58 |
59 | /**
60 | * Resolve class dependencies.
61 | *
62 | *
63 | * @throws \ReflectionException
64 | */
65 | protected function getDependencies(string $class, array $dependencies = []): array
66 | {
67 | if (! Arr::isAssoc($dependencies)) {
68 | if (! class_exists($class) && ! interface_exists($class)) {
69 | $class = $this->getBindingConcrete($class);
70 | }
71 |
72 | /** @var class-string $class */
73 | $constructor = (new \ReflectionClass($class))->getConstructor();
74 |
75 | if ($constructor) {
76 | $dependencies = $this->makeContainerParameters(
77 | $constructor->getParameters(),
78 | $dependencies
79 | );
80 | }
81 | }
82 |
83 | return $dependencies;
84 | }
85 |
86 | /**
87 | * @throws \ReflectionException
88 | */
89 | protected function getParameters(object $class, string $method, array $parameters): array
90 | {
91 | if (empty($parameters) || Arr::isAssoc($parameters)) {
92 | return $parameters;
93 | }
94 |
95 | return $this->makeContainerParameters(
96 | (new \ReflectionMethod($class, $method))->getParameters(),
97 | $parameters
98 | );
99 | }
100 |
101 | /**
102 | * Combine parameters to make it container-readable.
103 | */
104 | protected function makeContainerParameters(array $reflectionParameters, array $methodParameters): array
105 | {
106 | return collect($this->sliceParameters($reflectionParameters, $methodParameters))
107 | ->map->getName()
108 | ->combine($this->sliceParameters($methodParameters, $reflectionParameters))
109 | ->all();
110 | }
111 |
112 | /**
113 | * Slice an array to align the parameters.
114 | */
115 | protected function sliceParameters(array $parameters, array $countable): array
116 | {
117 | return array_slice($parameters, 0, count($countable));
118 | }
119 |
120 | /**
121 | * Convert the object to its namespace.
122 | */
123 | protected function convertToNamespace(object|string $object): string
124 | {
125 | return is_string($object) ? $object : $object::class;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Laravel Enhanced Container
4 | [](https://packagist.org/packages/michael-rubel/laravel-enhanced-container)
5 | [](https://packagist.org/packages/michael-rubel/laravel-enhanced-container)
6 | [](https://scrutinizer-ci.com/g/michael-rubel/laravel-enhanced-container/?branch=main)
7 | [](https://scrutinizer-ci.com/g/michael-rubel/laravel-enhanced-container/?branch=main)
8 | [](https://github.com/michael-rubel/laravel-enhanced-container/actions)
9 | [](https://github.com/michael-rubel/laravel-enhanced-container/actions)
10 |
11 | This package provides additional features for Laravel's Service Container.
12 |
13 | ## #StandWithUkraine
14 | [](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md)
15 |
16 | ## Contents
17 | * [Installation](#installation)
18 | * [Usage](#usage)
19 | + [Resolve contextual binding outside of constructor](#resolve-contextual-binding-outside-of-constructor)
20 | + [Method binding](#method-binding)
21 | + [Method forwarding](#method-forwarding)
22 |
23 | ## Installation
24 |
25 | Install the package via composer (Laravel 11):
26 | ```bash
27 | composer require michael-rubel/laravel-enhanced-container
28 | ```
29 |
30 | If you need the package for older versions of Laravel, check [changelog](https://github.com/michael-rubel/laravel-enhanced-container/releases).
31 |
32 | ## Usage
33 |
34 | ### Resolve contextual binding outside of constructor
35 |
36 | ```php
37 | call(ServiceInterface::class, context: static::class);
38 |
39 | // The `call` method automatically resolves the implementation from the interface you passed.
40 | // If you pass the context, the proxy tries to resolve contextual binding instead of global binding first.
41 | ```
42 |
43 | [🔝 back to contents](#contents)
44 |
45 | ### Method binding
46 | This feature makes it possible to override the behavior of methods accessed using the `call`.
47 |
48 | Assuming that is your function in the service class:
49 | ```php
50 | class Service
51 | {
52 | public function yourMethod(int $count): int
53 | {
54 | return $count;
55 | }
56 | }
57 | ```
58 |
59 | Bind the service to an interface:
60 | ```php
61 | $this->app->bind(ServiceInterface::class, Service::class);
62 | ```
63 |
64 | Call your service method through container:
65 | ```php
66 | call(ServiceInterface::class)->yourMethod(100);
67 | ```
68 |
69 | For example in feature tests:
70 | ```php
71 | $this->app->bind(ApiGatewayContract::class, InternalApiGateway::class);
72 |
73 | bind(ApiGatewayContract::class)->method('performRequest', function ($service, $app, $params) {
74 | // Note: you can access `$params` passed to the method call.
75 |
76 | return true;
77 | });
78 |
79 | $apiGateway = call(ApiGatewayContract::class);
80 | $request = $apiGateway->performRequest();
81 | $this->assertTrue($request);
82 | ```
83 |
84 | Note: if you rely on interfaces, the proxy will automatically resolve bound implementation for you.
85 |
86 | #### Note for package creators
87 | If you want to use method binding in your own package, you need to make sure the [`LecServiceProvider`](https://github.com/michael-rubel/laravel-enhanced-container/blob/main/src/LecServiceProvider.php) registered before you use this feature.
88 | ```php
89 | $this->app->register(LecServiceProvider::class);
90 | ```
91 |
92 | [🔝 back to contents](#contents)
93 |
94 | ### Method forwarding
95 | This feature automatically forwards the method if it doesn't exist in your class to the second one defined in the forwarding configuration.
96 |
97 | You can define forwarding in your ServiceProvider:
98 | ```php
99 | use MichaelRubel\EnhancedContainer\Core\Forwarding;
100 |
101 | Forwarding::enable()
102 | ->from(Service::class)
103 | ->to(Repository::class);
104 | ```
105 |
106 | You can as well use chained forwarding:
107 | ```php
108 | Forwarding::enable()
109 | ->from(Service::class)
110 | ->to(Repository::class)
111 | ->from(Repository::class)
112 | ->to(Model::class);
113 | ```
114 |
115 | #### Important notes
116 | - Pay attention to which internal instance you're now working on in `CallProxy` when using forwarding. The instance may change without your awareness. If you interact with the same methods/properties on a different instance, the `InstanceInteractionException` will be thrown.
117 | - If you use `PHPStan/Larastan` you'll need to add the `@method` docblock to the service to make it static-analyzable, otherwise it will return an error that the method doesn't exist in the class.
118 |
119 | [🔝 back to contents](#contents)
120 |
121 | ## Testing
122 |
123 | ```bash
124 | composer test
125 | ```
126 |
127 | ## License
128 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
129 |
--------------------------------------------------------------------------------
/src/Core/CallProxy.php:
--------------------------------------------------------------------------------
1 | instance = $this->getInstance($class, $dependencies, $context);
45 | }
46 |
47 | /**
48 | * Gets the internal property by name.
49 | */
50 | public function getInternal(string $property): mixed
51 | {
52 | return $this->{$property};
53 | }
54 |
55 | /**
56 | * Sets the internal instance to previous one.
57 | */
58 | public function setPrevious(): static
59 | {
60 | if ($this->previous) {
61 | $oldInstance = $this->instance;
62 |
63 | $this->instance = $this->previous;
64 |
65 | $this->previous = $oldInstance;
66 | }
67 |
68 | return $this;
69 | }
70 |
71 | /**
72 | * Disables the forwarding on the proxy level.
73 | */
74 | public function disableForwarding(): static
75 | {
76 | $this->forwarding = false;
77 |
78 | return $this;
79 | }
80 |
81 | /**
82 | * Enables the forwarding on the proxy level.
83 | */
84 | public function enableForwarding(): static
85 | {
86 | $this->forwarding = true;
87 |
88 | return $this;
89 | }
90 |
91 | /**
92 | * Perform the container call.
93 | */
94 | protected function containerCall(object $service, string $method, array $parameters): mixed
95 | {
96 | try {
97 | return BoundMethod::call(
98 | Container::getInstance(), [$service, $method], $this->getParameters($service, $method, $parameters)
99 | );
100 | } catch (\ReflectionException) {
101 | return $this->forwardCallTo($service, $method, $parameters);
102 | }
103 | }
104 |
105 | /**
106 | * Find the forwarding instance if bound.
107 | */
108 | protected function findForwardingInstance(): void
109 | {
110 | $clue = $this->instance::class . Forwarding::CONTAINER_KEY;
111 |
112 | if ($this->forwarding && app()->bound($clue)) {
113 | $newInstance = app($clue);
114 |
115 | $this->previous = $this->instance;
116 | $this->instance = $newInstance;
117 | }
118 | }
119 |
120 | /**
121 | * Save the interaction with proxy.
122 | */
123 | protected function interact(string $name, string $type): void
124 | {
125 | $this->interactions[$name] = $type;
126 | }
127 |
128 | /**
129 | * Check the proxy has previous interaction
130 | * with the same method or property.
131 | */
132 | protected function hasPreviousInteraction(string $name): bool
133 | {
134 | return $this->previous && isset($this->interactions[$name]);
135 | }
136 |
137 | /**
138 | * Handle the missing by error message.
139 | */
140 | protected function handleMissing(\Closure $callback, string $by): mixed
141 | {
142 | try {
143 | return $callback();
144 | } catch (\Error|\ErrorException $e) {
145 | if (Str::contains($e->getMessage(), $by)) {
146 | $this->findForwardingInstance();
147 |
148 | return $callback();
149 | }
150 |
151 | throw $e;
152 | }
153 | }
154 |
155 | /**
156 | * Pass the call through container.
157 | */
158 | public function __call(string $method, array $parameters): mixed
159 | {
160 | if (! method_exists($this->instance, $method)) {
161 | if ($this->hasPreviousInteraction($method)) {
162 | throw new InstanceInteractionException;
163 | }
164 |
165 | $this->findForwardingInstance();
166 | }
167 |
168 | $this->interact($method, Call::METHOD);
169 |
170 | return $this->handleMissing(
171 | fn () => $this->containerCall($this->instance, $method, $parameters),
172 | by: 'Call to undefined method'
173 | );
174 | }
175 |
176 | /**
177 | * Get the instance's property.
178 | */
179 | public function __get(string $name): mixed
180 | {
181 | if (! property_exists($this->instance, $name)) {
182 | if ($this->hasPreviousInteraction($name)) {
183 | throw new InstanceInteractionException;
184 | }
185 |
186 | $this->findForwardingInstance();
187 | }
188 |
189 | $this->interact($name, Call::GET);
190 |
191 | return $this->handleMissing(
192 | fn () => $this->instance->{$name},
193 | by: 'Undefined property'
194 | );
195 | }
196 |
197 | /**
198 | * Set the instance's property.
199 | */
200 | public function __set(string $name, mixed $value): void
201 | {
202 | $this->interact($name, Call::SET);
203 |
204 | $this->instance->{$name} = $value;
205 | }
206 |
207 | /**
208 | * Check the property is set.
209 | */
210 | public function __isset(string $name): bool
211 | {
212 | $this->interact($name, Call::ISSET);
213 |
214 | return isset($this->instance->{$name});
215 | }
216 |
217 | /**
218 | * Unset the property.
219 | */
220 | public function __unset(string $name): void
221 | {
222 | $this->interact($name, Call::UNSET);
223 |
224 | unset($this->instance->{$name});
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/tests/MethodBindingTest.php:
--------------------------------------------------------------------------------
1 | method()->test(fn () => 'overridden');
17 |
18 | $call = call(BoilerplateService::class)->test('test', 1);
19 |
20 | $this->assertEquals('overridden', $call);
21 | }
22 |
23 | /** @test */
24 | public function testCanOverrideMethodAsObject()
25 | {
26 | bind(new BoilerplateService)->method()->test(fn () => collect('illuminate'));
27 |
28 | $call = call(BoilerplateService::class)->test('test', 1);
29 |
30 | $this->assertEquals(collect('illuminate'), $call);
31 | }
32 |
33 | /** @test */
34 | public function testCanOverrideMethodUsingService()
35 | {
36 | bind(new BoilerplateService)->method()->yourMethod(
37 | fn ($service, $app) => $service->yourMethod(100) + 1
38 | );
39 |
40 | $call = call(BoilerplateService::class)->yourMethod(100);
41 |
42 | $this->assertEquals(101, $call);
43 | }
44 |
45 | /** @test */
46 | public function testCanOverrideMethodUsingInterface()
47 | {
48 | $this->app->bind(BoilerplateInterface::class, BoilerplateService::class);
49 |
50 | bind(BoilerplateInterface::class)->method()->yourMethod(
51 | fn ($service, $app) => $service->yourMethod(100) + 1
52 | );
53 |
54 | $call = call(BoilerplateService::class)->yourMethod(100);
55 |
56 | $this->assertEquals(101, $call);
57 | }
58 |
59 | /** @test */
60 | public function testCanOverrideMethodWithInterfaceAlternativeSyntax()
61 | {
62 | $this->app->bind(BoilerplateInterface::class, BoilerplateService::class);
63 |
64 | bind(BoilerplateInterface::class)->method('yourMethod',
65 | fn ($service, $app) => $service->yourMethod(100) + 1
66 | );
67 |
68 | $call = call(BoilerplateService::class)->yourMethod(100);
69 |
70 | $this->assertEquals(101, $call);
71 | }
72 |
73 | /** @test */
74 | public function testBindMethodReturnsItselfIfOnlyMethodPassed()
75 | {
76 | bind(BoilerplateService::class)->method();
77 |
78 | $call = call(BoilerplateService::class)->yourMethod(100);
79 |
80 | $this->assertEquals(100, $call);
81 | }
82 |
83 | /** @test */
84 | public function testBindMethodReturnsItselfIfOnlyMethodPassedWithString()
85 | {
86 | bind(BoilerplateService::class)->method('yourMethod');
87 |
88 | $call = call(BoilerplateService::class)->yourMethod(100);
89 |
90 | $this->assertEquals(100, $call);
91 | }
92 |
93 | /** @test */
94 | public function testCanOverrideMethodUsingAnotherSyntax()
95 | {
96 | bind(BoilerplateService::class)->method('yourMethod', function ($service, $app, $params) {
97 | return $service->yourMethod($params['count']) + 1;
98 | });
99 |
100 | $call = call(BoilerplateService::class)->yourMethod(100);
101 |
102 | $this->assertEquals(101, $call);
103 | }
104 |
105 | /** @test */
106 | public function testCanOverrideMethodWithParameters()
107 | {
108 | bind(BoilerplateService::class)->method()->yourMethod(
109 | fn ($service, $app, $params) => $service->yourMethod($params['count']) + 1
110 | );
111 |
112 | $call = call(BoilerplateService::class)->yourMethod(100);
113 |
114 | $this->assertEquals(101, $call);
115 | }
116 |
117 | /** @test */
118 | public function testCanOverrideMethodWithParametersAndAddCondition()
119 | {
120 | bind(BoilerplateService::class)->method('yourMethod', function ($service, $app, $params) {
121 | if ($params['count'] === 100) {
122 | return $service->yourMethod($params['count']) + 1;
123 | }
124 |
125 | return false;
126 | });
127 |
128 | $call = call(BoilerplateService::class)->yourMethod(100);
129 | $this->assertSame(101, $call);
130 |
131 | $call = call(BoilerplateService::class)->yourMethod(200);
132 | $this->assertFalse($call);
133 |
134 | bind(BoilerplateService::class)->method()->yourMethod(function ($service, $app, $params) {
135 | return $params['count'] + 1;
136 | });
137 | $this->assertSame(2, call(BoilerplateService::class)->yourMethod(1));
138 | }
139 |
140 | /** @test */
141 | public function testCanOverrideMethodWhenReusingCallProxyInstance()
142 | {
143 | $callProxy = call(BoilerplateService::class);
144 |
145 | bind(BoilerplateService::class)->method()->yourMethod(
146 | fn ($service, $app) => $service->yourMethod(100) + 1
147 | );
148 |
149 | $test = $callProxy->yourMethod(100);
150 |
151 | $this->assertEquals(101, $test);
152 |
153 | bind(BoilerplateService::class)->method()->yourMethod(
154 | fn ($service, $app) => true
155 | );
156 |
157 | $test = $callProxy->yourMethod();
158 |
159 | $this->assertTrue($test);
160 | }
161 |
162 | /** @test */
163 | public function testCanResolveStringsInMethodBinding()
164 | {
165 | $this->app->bind('test', BoilerplateService::class);
166 | bind('test')->method('test', fn () => 'works');
167 |
168 | $this->assertEquals('works', call(BoilerplateService::class)->test());
169 | }
170 |
171 | /** @test */
172 | public function testTryingToResolveStringsDoesNotThrowExceptionIfNotBound()
173 | {
174 | bind('test')->method('test', fn () => 'works');
175 |
176 | $this->assertTrue(
177 | app()->hasMethodBinding('test@test')
178 | );
179 | }
180 |
181 | /** @test */
182 | public function testReturnsSelfWhenClosureIsNull()
183 | {
184 | $instance = bind('test')->method();
185 | $this->assertInstanceOf(MethodBinder::class, $instance);
186 |
187 | $instance = bind('test')->method(null, fn () => true);
188 | $this->assertInstanceOf(MethodBinder::class, $instance);
189 |
190 | $instance = bind('test')->method('test');
191 | $this->assertInstanceOf(MethodBinder::class, $instance);
192 | }
193 |
194 | /** @test */
195 | public function testBindingBuilderExtension()
196 | {
197 | $this->app->bind(BoilerplateInterface::class, BoilerplateService::class);
198 |
199 | $binder = new TestMethodBinder(BoilerplateInterface::class);
200 | $binder->method('test', fn () => 'test');
201 |
202 | $this->assertFalse(
203 | app()->hasMethodBinding(BoilerplateInterface::class . '@test')
204 | );
205 | }
206 | }
207 |
208 | class TestMethodBinder extends MethodBinder
209 | {
210 | public function __construct(object|string $abstract)
211 | {
212 | $this->abstract = $this->convertToNamespace($abstract);
213 | }
214 |
215 | public function method(?string $method = null, ?\Closure $override = null): ?static
216 | {
217 | $this->resolve();
218 |
219 | return $this->{$method}($override);
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/Overrides/BoundMethod.php:
--------------------------------------------------------------------------------
1 | make($segments[0]), $method],
68 | $parameters
69 | );
70 | }
71 |
72 | /**
73 | * Call a method that has been bound to the container.
74 | *
75 | * @param \Illuminate\Container\Container $container
76 | * @param callable $callback
77 | * @param mixed $default
78 | * @return mixed
79 | */
80 | protected static function callBoundMethod($container, $callback, $default)
81 | {
82 | if (! is_array($callback)) {
83 | return Util::unwrapIfClosure($default);
84 | }
85 |
86 | // Here we need to turn the array callable into a Class@method string we can use to
87 | // examine the container and see if there are any method bindings for this given
88 | // method. If there are, we can call this method binding callback immediately.
89 | $method = static::normalizeMethod($callback);
90 |
91 | if ($container->hasMethodBinding($method)) {
92 | $reflectDefault = new ReflectionFunction($default);
93 |
94 | return $container->callMethodBinding(
95 | $method,
96 | $callback[0],
97 | $reflectDefault->getStaticVariables()['parameters']
98 | );
99 | }
100 |
101 | return Util::unwrapIfClosure($default);
102 | }
103 |
104 | /**
105 | * Normalize the given callback into a Class@method string.
106 | *
107 | * @param callable $callback
108 | * @return string
109 | */
110 | protected static function normalizeMethod($callback)
111 | {
112 | $class = is_string($callback[0]) ? $callback[0] : get_class($callback[0]);
113 |
114 | return "{$class}@{$callback[1]}";
115 | }
116 |
117 | /**
118 | * Get all dependencies for a given method.
119 | *
120 | * @param \Illuminate\Container\Container $container
121 | * @param callable|string $callback
122 | * @param array $parameters
123 | * @return array
124 | *
125 | * @throws \ReflectionException
126 | */
127 | protected static function getMethodDependencies($container, $callback, array $parameters = [])
128 | {
129 | $dependencies = [];
130 |
131 | foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
132 | static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
133 | }
134 |
135 | return array_merge($dependencies, array_values($parameters));
136 | }
137 |
138 | /**
139 | * Get the proper reflection instance for the given callback.
140 | *
141 | * @param callable|string $callback
142 | * @return \ReflectionFunctionAbstract
143 | *
144 | * @throws \ReflectionException
145 | */
146 | protected static function getCallReflector($callback)
147 | {
148 | if (is_string($callback) && str_contains($callback, '::')) {
149 | $callback = explode('::', $callback);
150 | } elseif (is_object($callback) && ! $callback instanceof Closure) {
151 | $callback = [$callback, '__invoke'];
152 | }
153 |
154 | return is_array($callback)
155 | ? new ReflectionMethod($callback[0], $callback[1])
156 | : new ReflectionFunction($callback);
157 | }
158 |
159 | /**
160 | * Get the dependency for the given call parameter.
161 | *
162 | * @param \Illuminate\Container\Container $container
163 | * @param \ReflectionParameter $parameter
164 | * @param array $parameters
165 | * @param array $dependencies
166 | * @return void
167 | *
168 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
169 | */
170 | protected static function addDependencyForCallParameter(
171 | $container,
172 | $parameter,
173 | array &$parameters,
174 | &$dependencies
175 | ) {
176 | $pendingDependencies = [];
177 |
178 | if (array_key_exists($paramName = $parameter->getName(), $parameters)) {
179 | $pendingDependencies[] = $parameters[$paramName];
180 |
181 | unset($parameters[$paramName]);
182 | } elseif ($attribute = Util::getContextualAttributeFromDependency($parameter)) {
183 | $pendingDependencies[] = $container->resolveFromAttribute($attribute);
184 | } elseif (! is_null($className = Util::getParameterClassName($parameter))) {
185 | if (array_key_exists($className, $parameters)) {
186 | $pendingDependencies[] = $parameters[$className];
187 |
188 | unset($parameters[$className]);
189 | } elseif ($parameter->isVariadic()) {
190 | $variadicDependencies = $container->make($className);
191 |
192 | $pendingDependencies = array_merge($pendingDependencies, is_array($variadicDependencies)
193 | ? $variadicDependencies
194 | : [$variadicDependencies]);
195 | } else {
196 | $pendingDependencies[] = $container->make($className);
197 | }
198 | } elseif ($parameter->isDefaultValueAvailable()) {
199 | $pendingDependencies[] = $parameter->getDefaultValue();
200 | } elseif (! $parameter->isOptional() && ! array_key_exists($paramName, $parameters)) {
201 | $message = "Unable to resolve dependency [{$parameter}] in class {$parameter->getDeclaringClass()->getName()}";
202 |
203 | throw new BindingResolutionException($message);
204 | }
205 |
206 | foreach ($pendingDependencies as $dependency) {
207 | $container->fireAfterResolvingAttributeCallbacks($parameter->getAttributes(), $dependency);
208 | }
209 |
210 | $dependencies = array_merge($dependencies, $pendingDependencies);
211 | }
212 |
213 | /**
214 | * Determine if the given string is in Class@method syntax.
215 | *
216 | * @param mixed $callback
217 | * @return bool
218 | */
219 | protected static function isCallableWithAtSign($callback)
220 | {
221 | return is_string($callback) && str_contains($callback, '@');
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/tests/ContainerCallTest.php:
--------------------------------------------------------------------------------
1 | test('test', 1);
30 |
31 | $this->assertTrue($test);
32 | }
33 |
34 | /** @test */
35 | public function testCanCallTheServiceAsObject()
36 | {
37 | $test = call(new BoilerplateService)->test('test', 1);
38 |
39 | $this->assertTrue($test);
40 | }
41 |
42 | /** @test */
43 | public function testCanCallTheServiceUsingInterface()
44 | {
45 | app()->singleton(BoilerplateInterface::class, BoilerplateService::class);
46 |
47 | $test = call(BoilerplateInterface::class)->test('test', 1);
48 |
49 | $this->assertTrue($test);
50 | }
51 |
52 | /** @test */
53 | public function testCanCallTheServiceWithoutParameters()
54 | {
55 | $test = call(BoilerplateService::class)->test();
56 |
57 | $this->assertTrue($test);
58 | }
59 |
60 | /** @test */
61 | public function testCanCallTheServiceWithRequiredConstructorParams()
62 | {
63 | $call = call(BoilerplateServiceWithConstructor::class, [true])->yourMethod(100);
64 |
65 | $this->assertEquals(101, $call);
66 | }
67 |
68 | /** @test */
69 | public function testCanCallTheServiceWithRequiredConstructorNamedParams()
70 | {
71 | $call = call(BoilerplateServiceWithConstructor::class, ['param' => true])->yourMethod(100);
72 |
73 | $this->assertEquals(101, $call);
74 | }
75 |
76 | /** @test */
77 | public function testCanCallReusingCallProxyInstance()
78 | {
79 | $callProxy = call(BoilerplateServiceWithConstructor::class, ['param' => true]);
80 |
81 | $test = $callProxy->yourMethod(100);
82 | $this->assertEquals(101, $test);
83 |
84 | $test = $callProxy->test();
85 | $this->assertTrue($test);
86 | }
87 |
88 | /** @test */
89 | public function testCanGetAndSetPropertiesThroughCallProxy()
90 | {
91 | $callProxy = call(BoilerplateService::class);
92 |
93 | $test = $callProxy->testProperty;
94 | $this->assertTrue($test);
95 |
96 | $test = $callProxy->testProperty = false;
97 | $this->assertFalse($test);
98 | }
99 |
100 | /** @test */
101 | public function testMethodDoesntExist()
102 | {
103 | $this->expectException(\Error::class);
104 |
105 | $object = resolve(UserService::class);
106 | call($object)->doesntExistMethod();
107 | }
108 |
109 | /** @test */
110 | public function testFailsToCallMethodWithWrongParameters()
111 | {
112 | $this->expectException(BindingResolutionException::class);
113 |
114 | $object = resolve(UserService::class);
115 |
116 | call($object)->testMethodWithParam();
117 | }
118 |
119 | /** @test */
120 | public function testCanCallMethodWithTwoParamsWhenOnlyOneExists()
121 | {
122 | $object = resolve(UserService::class);
123 |
124 | $test = call($object)->testMethodWithParam(123, true);
125 |
126 | $this->assertTrue($test);
127 | }
128 |
129 | /** @test */
130 | public function testFailsToCallMethodWithWrongParametersMultiple()
131 | {
132 | $this->expectException(BindingResolutionException::class);
133 |
134 | $object = resolve(UserService::class);
135 | call($object)->testMethodWithMultipleParams(123, true);
136 | }
137 |
138 | /** @test */
139 | public function testFailsToCallMethodWithWrongTypes()
140 | {
141 | $this->expectException(\TypeError::class);
142 |
143 | $object = resolve(UserService::class);
144 |
145 | call($object)->testMethodWithMultipleParams(123, true, 123, false);
146 | }
147 |
148 | /** @test */
149 | public function testFailsToCallMethodWithThreeParamsUsingFourParams()
150 | {
151 | $object = resolve(UserService::class);
152 |
153 | $test = call($object)->testMethodWithMultipleParams([], true, 123, false);
154 |
155 | $this->assertTrue($test);
156 | }
157 |
158 | /** @test */
159 | public function testSupportsNamedParameters()
160 | {
161 | $response = call(ParameterOrderBoilerplate::class)->handle(
162 | third: 'Third',
163 | second: 'Second',
164 | first: 'First'
165 | );
166 |
167 | $this->assertSame('FirstSecondThird', $response);
168 | }
169 |
170 | /** @test */
171 | public function testSupportsStringBindingsWithDependencies()
172 | {
173 | $this->app->bind('test', BoilerplateService::class);
174 |
175 | $response = call('test', ['dependency']);
176 |
177 | $this->assertInstanceOf(BoilerplateService::class, $response->getInternal(Call::INSTANCE));
178 | }
179 |
180 | /** @test */
181 | public function testArrayParams()
182 | {
183 | $this->app->bind('test', BoilerplateServiceWithConstructorPrimitive::class);
184 |
185 | $response = call('test', [
186 | 'param' => false,
187 | 'nextParam' => 'testString',
188 | ]);
189 |
190 | $this->assertFalse($response->getParam());
191 | $this->assertStringContainsString('testString', $response->getNextParam());
192 | }
193 |
194 | /** @test */
195 | public function testCallProxyResolvesContextualBinding()
196 | {
197 | // bind the service globally
198 | $this->app->singleton(BoilerplateInterface::class, BoilerplateService::class);
199 |
200 | $call = call(BoilerplateInterface::class);
201 |
202 | $this->assertInstanceOf(BoilerplateService::class, $call->getInternal(Call::INSTANCE));
203 |
204 | // set contextual
205 | $this->app->when(BoilerplateServiceResolvesContextualInMethod::class)
206 | ->needs(BoilerplateInterface::class)
207 | ->give(TestService::class);
208 |
209 | $this->app->when(TestRepository::class)
210 | ->needs(BoilerplateInterface::class)
211 | ->give(UserService::class);
212 |
213 | $service = call(BoilerplateServiceResolvesContextualInMethod::class);
214 |
215 | $this->assertInstanceOf(TestService::class, $service->constructorHasContextual());
216 | $this->assertInstanceOf(TestService::class, $service->methodHasContextual()->getInternal(Call::INSTANCE));
217 | $this->assertInstanceOf(TestService::class, $service->methodHasContextual2()->getInternal(Call::INSTANCE));
218 | $this->assertInstanceOf(UserService::class, $service->methodHasContextual3()->getInternal(Call::INSTANCE));
219 | $this->assertInstanceOf(BoilerplateService::class, $service->methodHasGlobal()->getInternal(Call::INSTANCE));
220 |
221 | // ensure global still available for other classes
222 | $service = call(BoilerplateServiceResolvesGlobalInMethod::class);
223 | $this->assertInstanceOf(BoilerplateService::class, $service->getsGlobalBinding()->getInternal(Call::INSTANCE));
224 | }
225 |
226 | /** @test */
227 | public function testCanCheckIssetProperty()
228 | {
229 | $proxy = call(UserService::class);
230 | $this->assertTrue(isset($proxy->existingProperty));
231 | }
232 |
233 | /** @test */
234 | public function testCanUnsetProperty()
235 | {
236 | $proxy = call(UserService::class);
237 | $this->assertFalse($proxy->existingProperty);
238 | unset($proxy->existingProperty);
239 | $this->expectException(\Error::class);
240 | $this->assertNull($proxy->existingProperty);
241 | }
242 |
243 | /** @test */
244 | public function testIssetPropertyWithForwarding()
245 | {
246 | Forwarding::enable()
247 | ->from(UserService::class)
248 | ->to(UserRepository::class);
249 |
250 | $proxy = call(UserService::class);
251 | $this->assertTrue(isset($proxy->existingProperty));
252 | $this->assertTrue($proxy->testProperty);
253 | $this->expectException(InstanceInteractionException::class);
254 | $proxy->existingProperty;
255 | }
256 |
257 | /** @test */
258 | public function testUnsetPropertyWithForwarding()
259 | {
260 | Forwarding::enable()
261 | ->from(UserService::class)
262 | ->to(UserRepository::class);
263 |
264 | $proxy = call(UserService::class);
265 | unset($proxy->existingProperty);
266 | $this->assertTrue($proxy->testProperty);
267 | $this->expectException(InstanceInteractionException::class);
268 | $proxy->existingProperty;
269 | }
270 |
271 | /** @test */
272 | public function testCanOverrideMethodsInCallProxy()
273 | {
274 | Forwarding::enable()
275 | ->from(UserService::class)
276 | ->to(UserRepository::class);
277 |
278 | $call = new TestCallProxy(UserService::class);
279 | $this->assertTrue($call->existingMethod());
280 | $call->nonExistingMethod();
281 | }
282 |
283 | /** @test */
284 | public function testParsesNonAssociativeArraysWhenResolvingDependencies()
285 | {
286 | $repo = app(TestRepository::class);
287 | $call = call(UserService::class, [0 => app(TestRepository::class), 1 => true]);
288 | $this->assertEquals($call->testRepository, $repo);
289 | $this->assertTrue($call->existingProperty);
290 | }
291 | }
292 |
293 | class TestCallProxy extends CallProxy
294 | {
295 | protected function containerCall(object $service, string $method, array $parameters): mixed
296 | {
297 | return parent::containerCall($service, $method, $parameters);
298 | }
299 |
300 | public function __call(string $method, array $parameters): mixed
301 | {
302 | if (! method_exists($this->instance, $method)) {
303 | if ($this->hasPreviousInteraction($method)) {
304 | throw new InstanceInteractionException;
305 | }
306 |
307 | $this->findForwardingInstance();
308 | }
309 |
310 | $this->interact($method, Call::METHOD);
311 |
312 | return $this->handleMissing(
313 | fn () => $this->containerCall($this->instance, $method, $parameters),
314 | by: 'Call to undefined method'
315 | );
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/tests/ForwardingTest.php:
--------------------------------------------------------------------------------
1 | testMethod();
26 | $this->assertFalse($call);
27 |
28 | $this->expectException(\Error::class);
29 | call(TestService::class)->nonExistingMethod();
30 | }
31 |
32 | /** @test */
33 | public function testCanGetAndSetPropertyWithoutForwarding()
34 | {
35 | $proxy = call(TestService::class);
36 | $proxy->test = true;
37 | $this->assertTrue($proxy->test);
38 |
39 | $proxy = call(TestService::class);
40 | $proxy->test = false;
41 | $this->assertFalse($proxy->test);
42 | }
43 |
44 | /** @test */
45 | public function testServiceForwardsToRepositoryWhenMethodDoesntExist()
46 | {
47 | Forwarding::enable()
48 | ->from(TestService::class)
49 | ->to(TestRepository::class);
50 |
51 | $call = call(TestService::class)->nonExistingMethod();
52 |
53 | $this->assertTrue($call);
54 | }
55 |
56 | /** @test */
57 | public function testCanGetAndSetPropertiesWithForwarding()
58 | {
59 | Forwarding::enable()
60 | ->from(UserService::class)
61 | ->to(UserRepository::class);
62 |
63 | $callProxy = call(UserService::class);
64 | $test = $callProxy->testProperty;
65 | $this->assertTrue($test);
66 | $this->assertInstanceOf(UserRepository::class, $callProxy->getInternal(Call::INSTANCE));
67 |
68 | $callProxy->testProperty = false;
69 | $test = $callProxy->testProperty;
70 | $this->assertFalse($test);
71 | $this->assertInstanceOf(UserRepository::class, $callProxy->getInternal(Call::INSTANCE));
72 | }
73 |
74 | /** @test */
75 | public function testSetsNonExistingProperty()
76 | {
77 | $callProxy = call(UserService::class);
78 | $callProxy->testProperty = true;
79 | $this->assertTrue($callProxy->testProperty);
80 | $this->assertInstanceOf(UserService::class, $callProxy->getInternal(Call::INSTANCE));
81 | }
82 |
83 | /** @test */
84 | public function testCanPassAnObjectWithMethodForwarding()
85 | {
86 | Forwarding::enable()
87 | ->from(UserService::class)
88 | ->to(UserRepository::class);
89 |
90 | $object = resolve(UserService::class);
91 | $test = call($object)->testMethod();
92 |
93 | $this->assertTrue($test);
94 | }
95 |
96 | /** @test */
97 | public function testMethodDoesntExist()
98 | {
99 | Forwarding::enable()
100 | ->from(UserService::class)
101 | ->to(UserRepository::class);
102 |
103 | $this->expectException(\Error::class);
104 |
105 | $object = resolve(UserService::class);
106 | call($object)->doesntExistMethod();
107 | }
108 |
109 | /** @test */
110 | public function testFailsToCallMethodInRepo()
111 | {
112 | Forwarding::enable()
113 | ->from(UserService::class)
114 | ->to(UserRepository::class);
115 |
116 | $this->expectException(\TypeError::class);
117 |
118 | $object = resolve(UserService::class);
119 | call($object)->testMethodMultipleParamsInRepo([], '123test');
120 | }
121 |
122 | /** @test */
123 | public function testCanCallMethodWithMultipleParamsInRepo()
124 | {
125 | Forwarding::enable()
126 | ->from(UserService::class)
127 | ->to(UserRepository::class);
128 |
129 | $object = resolve(UserService::class);
130 | $test = call($object)->testMethodMultipleParamsInRepo([], 123);
131 |
132 | $this->assertTrue($test);
133 | }
134 |
135 | /** @test */
136 | public function testCanCallRepoDirectlyWithMethodForwarding()
137 | {
138 | Forwarding::enable()
139 | ->from(UserService::class)
140 | ->to(UserRepository::class);
141 |
142 | $object = resolve(UserRepository::class);
143 | $test = call($object)->testMethodMultipleParamsInRepo([], 123);
144 |
145 | $this->assertTrue($test);
146 | }
147 |
148 | /** @test */
149 | public function testCanCallRepoDirectlyWithoutForwarding()
150 | {
151 | $object = resolve(UserRepository::class);
152 | $test = call($object)->testMethodMultipleParamsInRepo([], 123);
153 |
154 | $this->assertTrue($test);
155 | }
156 |
157 | /** @test */
158 | public function testContainerCallRedirectsManuallyIfCannotFindTheMethod()
159 | {
160 | Forwarding::enable()
161 | ->from(TestService::class)
162 | ->to(TestModel::class);
163 |
164 | $this->expectException(QueryException::class);
165 |
166 | // TestService redirects to the model.
167 | // The container cannot call it, so we're forwarding the method manually.
168 | call(TestService::class)->find(1);
169 |
170 | // The test throws the exception, and it's excepted since we don't have
171 | // any DB connection, but it says the `find` method actually works.
172 | }
173 |
174 | /** @test */
175 | public function testReflectionExceptionIsThrownWhenManualForwardingIsDisabled()
176 | {
177 | $this->expectException(\BadMethodCallException::class);
178 |
179 | call(TestService::class)->find(1);
180 | }
181 |
182 | /** @test */
183 | public function testChainedForwarding()
184 | {
185 | // Define a chained forwarding.
186 | Forwarding::enable()
187 | ->from(UserService::class)
188 | ->to(UserRepository::class)
189 | ->from(UserRepository::class)
190 | ->to(TestModel::class)
191 | ->from(TestModel::class)
192 | ->to(BestBuilder::class);
193 |
194 | // Make the service through CallProxy.
195 | $proxy = call(UserService::class);
196 |
197 | // Call method directly on the service.
198 | $test = $proxy->existingMethod();
199 | $this->assertTrue($test);
200 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
201 |
202 | // Call method that exists in the repository
203 | // assigned to the service in forwarding above.
204 | $test = $proxy->nonExistingMethod();
205 | $this->assertTrue($test);
206 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::INSTANCE));
207 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::PREVIOUS));
208 |
209 | // Call the method that only exists on the model,
210 | // i.e. on the third element in the forwarding chain.
211 | $this->assertTrue($proxy->nonExistingInRepositoryMethod());
212 | $this->assertInstanceOf(TestModel::class, $proxy->getInternal(Call::INSTANCE));
213 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::PREVIOUS));
214 |
215 | // Call the method that only exists on the builder,
216 | // i.e. on the fourth element in the forwarding chain.
217 | $this->assertTrue($proxy->builderMethod());
218 | $this->assertInstanceOf(BestBuilder::class, $proxy->getInternal(Call::INSTANCE));
219 | $this->assertInstanceOf(TestModel::class, $proxy->getInternal(Call::PREVIOUS));
220 | $this->expectException(QueryException::class);
221 | $proxy->builder->find(1);
222 | }
223 |
224 | /** @test */
225 | public function testChainedForwardingWithMissingMethods()
226 | {
227 | Forwarding::enable()
228 | ->from(UserService::class)
229 | ->to(UserRepository::class)
230 | ->from(UserRepository::class)
231 | ->to(TestModel::class);
232 |
233 | $proxy = call(UserService::class);
234 |
235 | $test = $proxy->existingMethod();
236 | $this->assertTrue($test);
237 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
238 |
239 | $test = $proxy->nonExistingInRepositoryMethod();
240 | $this->assertTrue($test);
241 | $this->assertInstanceOf(TestModel::class, $proxy->getInternal(Call::INSTANCE));
242 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::PREVIOUS));
243 | }
244 |
245 | /** @test */
246 | public function testChainedForwardingWithMissingProperties()
247 | {
248 | Forwarding::enable()
249 | ->from(UserService::class)
250 | ->to(UserRepository::class)
251 | ->from(UserRepository::class)
252 | ->to(TestModel::class);
253 |
254 | $proxy = call(UserService::class);
255 |
256 | $test = $proxy->existingProperty;
257 | $this->assertFalse($test);
258 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
259 |
260 | $test = $proxy->nonInServiceExistingProperty;
261 | $this->assertTrue($test);
262 | $this->assertInstanceOf(TestModel::class, $proxy->getInternal(Call::INSTANCE));
263 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::PREVIOUS));
264 | }
265 |
266 | /** @test */
267 | public function testStatesForProperties()
268 | {
269 | // Define a chained forwarding.
270 | Forwarding::enable()
271 | ->from(UserService::class)
272 | ->to(UserRepository::class);
273 |
274 | // Make the service through CallProxy.
275 | $proxy = call(UserService::class);
276 |
277 | // Set property to base instance.
278 | $proxy->existingProperty = true;
279 | $this->assertSame(Call::SET, $proxy->getInternal(Call::INTERACTIONS)['existingProperty']);
280 | $this->assertTrue($proxy->existingProperty);
281 | $this->assertSame(Call::GET, $proxy->getInternal(Call::INTERACTIONS)['existingProperty']);
282 |
283 | $proxy->nonExistingProperty = true;
284 | $this->assertSame(Call::SET, $proxy->getInternal(Call::INTERACTIONS)['nonExistingProperty']);
285 | $this->assertTrue($proxy->nonExistingProperty);
286 | $this->assertSame(Call::GET, $proxy->getInternal(Call::INTERACTIONS)['nonExistingProperty']);
287 |
288 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
289 | }
290 |
291 | /** @test */
292 | public function testFailsWhenTryingToNonExistingCallMethodAndInteractedPreviously()
293 | {
294 | Forwarding::enable()
295 | ->from(UserService::class)
296 | ->to(UserRepository::class);
297 |
298 | $proxy = call(UserService::class);
299 |
300 | $proxy->nonExistingProperty = true;
301 | $this->assertTrue($proxy->nonExistingProperty);
302 |
303 | $proxy->nonExistingMethod();
304 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::INSTANCE));
305 |
306 | $this->expectException(InstanceInteractionException::class);
307 | $this->assertTrue($proxy->nonExistingProperty);
308 | }
309 |
310 | /** @test */
311 | public function testStateMachineForMethods()
312 | {
313 | // Define a chained forwarding.
314 | Forwarding::enable()
315 | ->from(UserService::class)
316 | ->to(UserRepository::class);
317 |
318 | // Make the service through CallProxy.
319 | $proxy = call(UserService::class);
320 |
321 | // Set property to base instance.
322 | $this->assertTrue($proxy->existingMethod());
323 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
324 | $this->assertSame(Call::METHOD, $proxy->getInternal(Call::INTERACTIONS)['existingMethod']);
325 |
326 | // Swaps instance.
327 | $this->assertTrue($proxy->nonExistingMethod());
328 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::INSTANCE));
329 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::PREVIOUS));
330 | $this->assertTrue($proxy->methodInRepository());
331 |
332 | // Should throw an exception because we previously changed the state.
333 | $this->expectException(InstanceInteractionException::class);
334 | $proxy->existingMethod();
335 | }
336 |
337 | /** @test */
338 | public function tesForwardingResolvesInterfaces()
339 | {
340 | $this->app->bind(TestServiceInterface::class, TestService::class);
341 | $this->app->bind(TestRepositoryInterface::class, TestRepository::class);
342 |
343 | Forwarding::enable()
344 | ->from(TestServiceInterface::class)
345 | ->to(TestRepositoryInterface::class);
346 |
347 | $proxy = call(TestService::class);
348 |
349 | $this->assertTrue($proxy->existingMethod());
350 | $this->assertInstanceOf(TestService::class, $proxy->getInternal(Call::INSTANCE));
351 | $this->assertSame(Call::METHOD, $proxy->getInternal(Call::INTERACTIONS)['existingMethod']);
352 |
353 | $this->assertTrue($proxy->nonExistingMethod());
354 | $this->assertInstanceOf(TestRepository::class, $proxy->getInternal(Call::INSTANCE));
355 | $this->assertInstanceOf(TestService::class, $proxy->getInternal(Call::PREVIOUS));
356 | $this->assertTrue($proxy->methodInRepository());
357 | }
358 |
359 | /** @test */
360 | public function testCanSetPrevious()
361 | {
362 | // Define a chained forwarding.
363 | Forwarding::enable()
364 | ->from(UserService::class)
365 | ->to(UserRepository::class);
366 |
367 | // Make the service through CallProxy.
368 | $proxy = call(UserService::class);
369 |
370 | // Set property to base instance.
371 | $this->assertTrue($proxy->existingMethod());
372 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
373 | $this->assertNull($proxy->getInternal(Call::PREVIOUS));
374 |
375 | // Swaps instance.
376 | $this->assertTrue($proxy->nonExistingMethod());
377 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::INSTANCE));
378 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::PREVIOUS));
379 |
380 | // Instance swapped, but we want to set previous.
381 | $proxy->setPrevious();
382 | $this->assertInstanceOf(UserService::class, $proxy->getInternal(Call::INSTANCE));
383 | $this->assertInstanceOf(UserRepository::class, $proxy->getInternal(Call::PREVIOUS));
384 | }
385 |
386 | /** @test */
387 | public function testCanDisableForwardingOnProxyLevel()
388 | {
389 | Forwarding::enable()
390 | ->from(UserService::class)
391 | ->to(UserRepository::class);
392 |
393 | $proxy = call(UserService::class);
394 |
395 | // Doesn't swap the instance because we manually turned out forwarding on this instance.
396 | $proxy->disableForwarding();
397 |
398 | $this->expectException(\Error::class);
399 | $proxy->nonExistingMethod();
400 | }
401 |
402 | /** @test */
403 | public function testCanEnableDisabledForwardingOnProxyLevel()
404 | {
405 | Forwarding::enable()
406 | ->from(UserService::class)
407 | ->to(UserRepository::class);
408 |
409 | $proxy = call(UserService::class);
410 |
411 | $proxy->disableForwarding();
412 | $this->assertFalse($proxy->getInternal(Call::FORWARDING));
413 |
414 | $proxy->enableForwarding();
415 | $this->assertTrue($proxy->getInternal(Call::FORWARDING));
416 | }
417 |
418 | /** @test */
419 | public function testCanForwardingExtension()
420 | {
421 | $forwarding = new TestForwarding;
422 | $forwarding->from(UserService::class)->to(UserRepository::class);
423 |
424 | $proxy = call(UserService::class);
425 | $this->assertTrue($proxy->getInternal(Call::FORWARDING));
426 | }
427 | }
428 |
429 | class TestForwarding extends Forwarding
430 | {
431 | public function to(string $destination): static
432 | {
433 | app()->bind(
434 | abstract: $this->pendingClass . static::CONTAINER_KEY,
435 | concrete: $this->resolve($destination)
436 | );
437 |
438 | return $this;
439 | }
440 | }
441 |
--------------------------------------------------------------------------------
/tests/ContainerTest.php:
--------------------------------------------------------------------------------
1 | assertSame($container, Container::getInstance());
27 |
28 | Container::setInstance(null);
29 |
30 | $container2 = Container::getInstance();
31 |
32 | $this->assertInstanceOf(Container::class, $container2);
33 | $this->assertNotSame($container, $container2);
34 | }
35 |
36 | public function testClosureResolution()
37 | {
38 | $container = new Container;
39 | $container->bind('name', function () {
40 | return 'Taylor';
41 | });
42 | $this->assertSame('Taylor', $container->make('name'));
43 | }
44 |
45 | public function testBindIfDoesntRegisterIfServiceAlreadyRegistered()
46 | {
47 | $container = new Container;
48 | $container->bind('name', function () {
49 | return 'Taylor';
50 | });
51 | $container->bindIf('name', function () {
52 | return 'Dayle';
53 | });
54 |
55 | $this->assertSame('Taylor', $container->make('name'));
56 | }
57 |
58 | public function testBindIfDoesRegisterIfServiceNotRegisteredYet()
59 | {
60 | $container = new Container;
61 | $container->bind('surname', function () {
62 | return 'Taylor';
63 | });
64 | $container->bindIf('name', function () {
65 | return 'Dayle';
66 | });
67 |
68 | $this->assertSame('Dayle', $container->make('name'));
69 | }
70 |
71 | public function testSingletonIfDoesntRegisterIfBindingAlreadyRegistered()
72 | {
73 | $container = new Container;
74 | $container->singleton('class', function () {
75 | return new stdClass;
76 | });
77 | $firstInstantiation = $container->make('class');
78 | $container->singletonIf('class', function () {
79 | return new ContainerConcreteStub;
80 | });
81 | $secondInstantiation = $container->make('class');
82 | $this->assertSame($firstInstantiation, $secondInstantiation);
83 | }
84 |
85 | public function testSingletonIfDoesRegisterIfBindingNotRegisteredYet()
86 | {
87 | $container = new Container;
88 | $container->singleton('class', function () {
89 | return new stdClass;
90 | });
91 | $container->singletonIf('otherClass', function () {
92 | return new ContainerConcreteStub;
93 | });
94 | $firstInstantiation = $container->make('otherClass');
95 | $secondInstantiation = $container->make('otherClass');
96 | $this->assertSame($firstInstantiation, $secondInstantiation);
97 | }
98 |
99 | public function testSharedClosureResolution()
100 | {
101 | $container = new Container;
102 | $container->singleton('class', function () {
103 | return new stdClass;
104 | });
105 | $firstInstantiation = $container->make('class');
106 | $secondInstantiation = $container->make('class');
107 | $this->assertSame($firstInstantiation, $secondInstantiation);
108 | }
109 |
110 | public function testScopedClosureResolution()
111 | {
112 | $container = new Container;
113 | $container->scoped('class', function () {
114 | return new stdClass;
115 | });
116 | $firstInstantiation = $container->make('class');
117 | $secondInstantiation = $container->make('class');
118 | $this->assertSame($firstInstantiation, $secondInstantiation);
119 | }
120 |
121 | public function testScopedIf()
122 | {
123 | $container = new Container;
124 | $container->scopedIf('class', function () {
125 | return 'foo';
126 | });
127 | $this->assertSame('foo', $container->make('class'));
128 | $container->scopedIf('class', function () {
129 | return 'bar';
130 | });
131 | $this->assertSame('foo', $container->make('class'));
132 | $this->assertNotSame('bar', $container->make('class'));
133 | }
134 |
135 | public function testScopedClosureResets()
136 | {
137 | $container = new Container;
138 | $container->scoped('class', function () {
139 | return new stdClass;
140 | });
141 | $firstInstantiation = $container->make('class');
142 |
143 | $container->forgetScopedInstances();
144 |
145 | $secondInstantiation = $container->make('class');
146 | $this->assertNotSame($firstInstantiation, $secondInstantiation);
147 | }
148 |
149 | public function testAutoConcreteResolution()
150 | {
151 | $container = new Container;
152 | $this->assertInstanceOf(ContainerConcreteStub::class, $container->make(ContainerConcreteStub::class));
153 | }
154 |
155 | public function testSharedConcreteResolution()
156 | {
157 | $container = new Container;
158 | $container->singleton(ContainerConcreteStub::class);
159 |
160 | $var1 = $container->make(ContainerConcreteStub::class);
161 | $var2 = $container->make(ContainerConcreteStub::class);
162 | $this->assertSame($var1, $var2);
163 | }
164 |
165 | public function testScopedConcreteResolutionResets()
166 | {
167 | $container = new Container;
168 | $container->scoped(ContainerConcreteStub::class);
169 |
170 | $var1 = $container->make(ContainerConcreteStub::class);
171 |
172 | $container->forgetScopedInstances();
173 |
174 | $var2 = $container->make(ContainerConcreteStub::class);
175 |
176 | $this->assertNotSame($var1, $var2);
177 | }
178 |
179 | public function testBindFailsLoudlyWithInvalidArgument()
180 | {
181 | $this->expectException(TypeError::class);
182 | $container = new Container;
183 |
184 | $concrete = new ContainerConcreteStub;
185 | $container->bind(ContainerConcreteStub::class, $concrete);
186 | }
187 |
188 | public function testAbstractToConcreteResolution()
189 | {
190 | $container = new Container;
191 | $container->bind(IContainerContractStub::class, ContainerImplementationStub::class);
192 | $class = $container->make(ContainerDependentStub::class);
193 | $this->assertInstanceOf(ContainerImplementationStub::class, $class->impl);
194 | }
195 |
196 | public function testNestedDependencyResolution()
197 | {
198 | $container = new Container;
199 | $container->bind(IContainerContractStub::class, ContainerImplementationStub::class);
200 | $class = $container->make(ContainerNestedDependentStub::class);
201 | $this->assertInstanceOf(ContainerDependentStub::class, $class->inner);
202 | $this->assertInstanceOf(ContainerImplementationStub::class, $class->inner->impl);
203 | }
204 |
205 | public function testContainerIsPassedToResolvers()
206 | {
207 | $container = new Container;
208 | $container->bind('something', function ($c) {
209 | return $c;
210 | });
211 | $c = $container->make('something');
212 | $this->assertSame($c, $container);
213 | }
214 |
215 | public function testArrayAccess()
216 | {
217 | $container = new Container;
218 | $this->assertFalse(isset($container['something']));
219 | $container['something'] = function () {
220 | return 'foo';
221 | };
222 | $this->assertTrue(isset($container['something']));
223 | $this->assertNotEmpty($container['something']);
224 | $this->assertSame('foo', $container['something']);
225 | unset($container['something']);
226 | $this->assertFalse(isset($container['something']));
227 |
228 | //test offsetSet when it's not instanceof Closure
229 | $container = new Container;
230 | $container['something'] = 'text';
231 | $this->assertTrue(isset($container['something']));
232 | $this->assertNotEmpty($container['something']);
233 | $this->assertSame('text', $container['something']);
234 | unset($container['something']);
235 | $this->assertFalse(isset($container['something']));
236 | }
237 |
238 | public function testAliases()
239 | {
240 | $container = new Container;
241 | $container['foo'] = 'bar';
242 | $container->alias('foo', 'baz');
243 | $container->alias('baz', 'bat');
244 | $this->assertSame('bar', $container->make('foo'));
245 | $this->assertSame('bar', $container->make('baz'));
246 | $this->assertSame('bar', $container->make('bat'));
247 | }
248 |
249 | public function testAliasesWithArrayOfParameters()
250 | {
251 | $container = new Container;
252 | $container->bind('foo', function ($app, $config) {
253 | return $config;
254 | });
255 | $container->alias('foo', 'baz');
256 | $this->assertEquals([1, 2, 3], $container->make('baz', [1, 2, 3]));
257 | }
258 |
259 | public function testBindingsCanBeOverridden()
260 | {
261 | $container = new Container;
262 | $container['foo'] = 'bar';
263 | $container['foo'] = 'baz';
264 | $this->assertSame('baz', $container['foo']);
265 | }
266 |
267 | public function testBindingAnInstanceReturnsTheInstance()
268 | {
269 | $container = new Container;
270 |
271 | $bound = new stdClass;
272 | $resolved = $container->instance('foo', $bound);
273 |
274 | $this->assertSame($bound, $resolved);
275 | }
276 |
277 | public function testBindingAnInstanceAsShared()
278 | {
279 | $container = new Container;
280 | $bound = new stdClass;
281 | $container->instance('foo', $bound);
282 | $object = $container->make('foo');
283 | $this->assertSame($bound, $object);
284 | }
285 |
286 | public function testResolutionOfDefaultParameters()
287 | {
288 | $container = new Container;
289 | $instance = $container->make(ContainerDefaultValueStub::class);
290 | $this->assertInstanceOf(ContainerConcreteStub::class, $instance->stub);
291 | $this->assertSame('taylor', $instance->default);
292 | }
293 |
294 | public function testBound()
295 | {
296 | $container = new Container;
297 | $container->bind(ContainerConcreteStub::class, function () {
298 | //
299 | });
300 | $this->assertTrue($container->bound(ContainerConcreteStub::class));
301 | $this->assertFalse($container->bound(IContainerContractStub::class));
302 |
303 | $container = new Container;
304 | $container->bind(IContainerContractStub::class, ContainerConcreteStub::class);
305 | $this->assertTrue($container->bound(IContainerContractStub::class));
306 | $this->assertFalse($container->bound(ContainerConcreteStub::class));
307 | }
308 |
309 | public function testUnsetRemoveBoundInstances()
310 | {
311 | $container = new Container;
312 | $container->instance('object', new stdClass);
313 | unset($container['object']);
314 |
315 | $this->assertFalse($container->bound('object'));
316 | }
317 |
318 | public function testBoundInstanceAndAliasCheckViaArrayAccess()
319 | {
320 | $container = new Container;
321 | $container->instance('object', new stdClass);
322 | $container->alias('object', 'alias');
323 |
324 | $this->assertTrue(isset($container['object']));
325 | $this->assertTrue(isset($container['alias']));
326 | }
327 |
328 | public function testReboundListeners()
329 | {
330 | unset($_SERVER['__test.rebind']);
331 |
332 | $container = new Container;
333 | $container->bind('foo', function () {
334 | //
335 | });
336 | $container->rebinding('foo', function () {
337 | $_SERVER['__test.rebind'] = true;
338 | });
339 | $container->bind('foo', function () {
340 | //
341 | });
342 |
343 | $this->assertTrue($_SERVER['__test.rebind']);
344 | }
345 |
346 | public function testReboundListenersOnInstances()
347 | {
348 | unset($_SERVER['__test.rebind']);
349 |
350 | $container = new Container;
351 | $container->instance('foo', function () {
352 | //
353 | });
354 | $container->rebinding('foo', function () {
355 | $_SERVER['__test.rebind'] = true;
356 | });
357 | $container->instance('foo', function () {
358 | //
359 | });
360 |
361 | $this->assertTrue($_SERVER['__test.rebind']);
362 | }
363 |
364 | public function testReboundListenersOnInstancesOnlyFiresIfWasAlreadyBound()
365 | {
366 | $_SERVER['__test.rebind'] = false;
367 |
368 | $container = new Container;
369 | $container->rebinding('foo', function () {
370 | $_SERVER['__test.rebind'] = true;
371 | });
372 | $container->instance('foo', function () {
373 | //
374 | });
375 |
376 | $this->assertFalse($_SERVER['__test.rebind']);
377 | }
378 |
379 | public function testInternalClassWithDefaultParameters()
380 | {
381 | $this->expectException(BindingResolutionException::class);
382 | $this->expectExceptionMessage('Unresolvable dependency resolving [Parameter #0 [ $first ]] in class MichaelRubel\EnhancedContainer\Tests\ContainerMixedPrimitiveStub');
383 |
384 | $container = new Container;
385 | $container->make(ContainerMixedPrimitiveStub::class, []);
386 | }
387 |
388 | public function testBindingResolutionExceptionMessage()
389 | {
390 | $this->expectException(BindingResolutionException::class);
391 | $this->expectExceptionMessage('Target [MichaelRubel\EnhancedContainer\Tests\IContainerContractStub] is not instantiable.');
392 |
393 | $container = new Container;
394 | $container->make(IContainerContractStub::class, []);
395 | }
396 |
397 | public function testBindingResolutionExceptionMessageIncludesBuildStack()
398 | {
399 | $this->expectException(BindingResolutionException::class);
400 | $this->expectExceptionMessage('Target [MichaelRubel\EnhancedContainer\Tests\IContainerContractStub] is not instantiable while building [MichaelRubel\EnhancedContainer\Tests\ContainerDependentStub].');
401 |
402 | $container = new Container;
403 | $container->make(ContainerDependentStub::class, []);
404 | }
405 |
406 | public function testBindingResolutionExceptionMessageWhenClassDoesNotExist()
407 | {
408 | $this->expectException(BindingResolutionException::class);
409 | $this->expectExceptionMessage('Target class [Foo\Bar\Baz\DummyClass] does not exist.');
410 |
411 | $container = new Container;
412 | $container->build('Foo\Bar\Baz\DummyClass');
413 | }
414 |
415 | public function testForgetInstanceForgetsInstance()
416 | {
417 | $container = new Container;
418 | $containerConcreteStub = new ContainerConcreteStub;
419 | $container->instance(ContainerConcreteStub::class, $containerConcreteStub);
420 | $this->assertTrue($container->isShared(ContainerConcreteStub::class));
421 | $container->forgetInstance(ContainerConcreteStub::class);
422 | $this->assertFalse($container->isShared(ContainerConcreteStub::class));
423 | }
424 |
425 | public function testForgetInstancesForgetsAllInstances()
426 | {
427 | $container = new Container;
428 | $containerConcreteStub1 = new ContainerConcreteStub;
429 | $containerConcreteStub2 = new ContainerConcreteStub;
430 | $containerConcreteStub3 = new ContainerConcreteStub;
431 | $container->instance('Instance1', $containerConcreteStub1);
432 | $container->instance('Instance2', $containerConcreteStub2);
433 | $container->instance('Instance3', $containerConcreteStub3);
434 | $this->assertTrue($container->isShared('Instance1'));
435 | $this->assertTrue($container->isShared('Instance2'));
436 | $this->assertTrue($container->isShared('Instance3'));
437 | $container->forgetInstances();
438 | $this->assertFalse($container->isShared('Instance1'));
439 | $this->assertFalse($container->isShared('Instance2'));
440 | $this->assertFalse($container->isShared('Instance3'));
441 | }
442 |
443 | public function testContainerFlushFlushesAllBindingsAliasesAndResolvedInstances()
444 | {
445 | $container = new Container;
446 | $container->bind('ConcreteStub', function () {
447 | return new ContainerConcreteStub;
448 | }, true);
449 | $container->alias('ConcreteStub', 'ContainerConcreteStub');
450 | $container->make('ConcreteStub');
451 | $this->assertTrue($container->resolved('ConcreteStub'));
452 | $this->assertTrue($container->isAlias('ContainerConcreteStub'));
453 | $this->assertArrayHasKey('ConcreteStub', $container->getBindings());
454 | $this->assertTrue($container->isShared('ConcreteStub'));
455 | $container->flush();
456 | $this->assertFalse($container->resolved('ConcreteStub'));
457 | $this->assertFalse($container->isAlias('ContainerConcreteStub'));
458 | $this->assertEmpty($container->getBindings());
459 | $this->assertFalse($container->isShared('ConcreteStub'));
460 | }
461 |
462 | public function testResolvedResolvesAliasToBindingNameBeforeChecking()
463 | {
464 | $container = new Container;
465 | $container->bind('ConcreteStub', function () {
466 | return new ContainerConcreteStub;
467 | }, true);
468 | $container->alias('ConcreteStub', 'foo');
469 |
470 | $this->assertFalse($container->resolved('ConcreteStub'));
471 | $this->assertFalse($container->resolved('foo'));
472 |
473 | $container->make('ConcreteStub');
474 |
475 | $this->assertTrue($container->resolved('ConcreteStub'));
476 | $this->assertTrue($container->resolved('foo'));
477 | }
478 |
479 | public function testGetAlias()
480 | {
481 | $container = new Container;
482 | $container->alias('ConcreteStub', 'foo');
483 | $this->assertSame('ConcreteStub', $container->getAlias('foo'));
484 | }
485 |
486 | public function testGetAliasRecursive()
487 | {
488 | $container = new Container;
489 | $container->alias('ConcreteStub', 'foo');
490 | $container->alias('foo', 'bar');
491 | $container->alias('bar', 'baz');
492 | $this->assertSame('ConcreteStub', $container->getAlias('baz'));
493 | $this->assertTrue($container->isAlias('baz'));
494 | $this->assertTrue($container->isAlias('bar'));
495 | $this->assertTrue($container->isAlias('foo'));
496 | }
497 |
498 | public function testItThrowsExceptionWhenAbstractIsSameAsAlias()
499 | {
500 | $this->expectException('LogicException');
501 | $this->expectExceptionMessage('[name] is aliased to itself.');
502 |
503 | $container = new Container;
504 | $container->alias('name', 'name');
505 | }
506 |
507 | public function testContainerGetFactory()
508 | {
509 | $container = new Container;
510 | $container->bind('name', function () {
511 | return 'Taylor';
512 | });
513 |
514 | $factory = $container->factory('name');
515 | $this->assertEquals($container->make('name'), $factory());
516 | }
517 |
518 | public function testMakeWithMethodIsAnAliasForMakeMethod()
519 | {
520 | $mock = $this->getMockBuilder(Container::class)
521 | ->onlyMethods(['make'])
522 | ->getMock();
523 |
524 | $mock->expects($this->once())
525 | ->method('make')
526 | ->with(ContainerDefaultValueStub::class, ['default' => 'laurence'])
527 | ->willReturn(new stdClass);
528 |
529 | $result = $mock->makeWith(ContainerDefaultValueStub::class, ['default' => 'laurence']);
530 |
531 | $this->assertInstanceOf(stdClass::class, $result);
532 | }
533 |
534 | public function testResolvingWithArrayOfParameters()
535 | {
536 | $container = new Container;
537 | $instance = $container->make(ContainerDefaultValueStub::class, ['default' => 'adam']);
538 | $this->assertSame('adam', $instance->default);
539 |
540 | $instance = $container->make(ContainerDefaultValueStub::class);
541 | $this->assertSame('taylor', $instance->default);
542 |
543 | $container->bind('foo', function ($app, $config) {
544 | return $config;
545 | });
546 |
547 | $this->assertEquals([1, 2, 3], $container->make('foo', [1, 2, 3]));
548 | }
549 |
550 | public function testResolvingWithArrayOfMixedParameters()
551 | {
552 | $container = new Container;
553 | $instance = $container->make(ContainerMixedPrimitiveStub::class, ['first' => 1, 'last' => 2, 'third' => 3]);
554 | $this->assertSame(1, $instance->first);
555 | $this->assertInstanceOf(ContainerConcreteStub::class, $instance->stub);
556 | $this->assertSame(2, $instance->last);
557 | $this->assertFalse(isset($instance->third));
558 | }
559 |
560 | public function testResolvingWithUsingAnInterface()
561 | {
562 | $container = new Container;
563 | $container->bind(IContainerContractStub::class, ContainerInjectVariableStubWithInterfaceImplementation::class);
564 | $instance = $container->make(IContainerContractStub::class, ['something' => 'laurence']);
565 | $this->assertSame('laurence', $instance->something);
566 | }
567 |
568 | public function testNestedParameterOverride()
569 | {
570 | $container = new Container;
571 | $container->bind('foo', function ($app, $config) {
572 | return $app->make('bar', ['name' => 'Taylor']);
573 | });
574 | $container->bind('bar', function ($app, $config) {
575 | return $config;
576 | });
577 |
578 | $this->assertEquals(['name' => 'Taylor'], $container->make('foo', ['something']));
579 | }
580 |
581 | public function testNestedParametersAreResetForFreshMake()
582 | {
583 | $container = new Container;
584 |
585 | $container->bind('foo', function ($app, $config) {
586 | return $app->make('bar');
587 | });
588 |
589 | $container->bind('bar', function ($app, $config) {
590 | return $config;
591 | });
592 |
593 | $this->assertEquals([], $container->make('foo', ['something']));
594 | }
595 |
596 | public function testSingletonBindingsNotRespectedWithMakeParameters()
597 | {
598 | $container = new Container;
599 |
600 | $container->singleton('foo', function ($app, $config) {
601 | return $config;
602 | });
603 |
604 | $this->assertEquals(['name' => 'taylor'], $container->make('foo', ['name' => 'taylor']));
605 | $this->assertEquals(['name' => 'abigail'], $container->make('foo', ['name' => 'abigail']));
606 | }
607 |
608 | public function testCanBuildWithoutParameterStackWithNoConstructors()
609 | {
610 | $container = new Container;
611 | $this->assertInstanceOf(ContainerConcreteStub::class, $container->build(ContainerConcreteStub::class));
612 | }
613 |
614 | public function testCanBuildWithoutParameterStackWithConstructors()
615 | {
616 | $container = new Container;
617 | $container->bind(IContainerContractStub::class, ContainerImplementationStub::class);
618 | $this->assertInstanceOf(ContainerDependentStub::class, $container->build(ContainerDependentStub::class));
619 | }
620 |
621 | public function testContainerKnowsEntry()
622 | {
623 | $container = new Container;
624 | $container->bind(IContainerContractStub::class, ContainerImplementationStub::class);
625 | $this->assertTrue($container->has(IContainerContractStub::class));
626 | }
627 |
628 | public function testContainerCanBindAnyWord()
629 | {
630 | $container = new Container;
631 | $container->bind('Taylor', stdClass::class);
632 | $this->assertInstanceOf(stdClass::class, $container->get('Taylor'));
633 | }
634 |
635 | public function testContainerCanDynamicallySetService()
636 | {
637 | $container = new Container;
638 | $this->assertFalse(isset($container['name']));
639 | $container['name'] = 'Taylor';
640 | $this->assertTrue(isset($container['name']));
641 | $this->assertSame('Taylor', $container['name']);
642 | }
643 |
644 | public function testUnknownEntryThrowsException()
645 | {
646 | $this->expectException(EntryNotFoundException::class);
647 |
648 | $container = new Container;
649 | $container->get('Taylor');
650 | }
651 |
652 | public function testBoundEntriesThrowsContainerExceptionWhenNotResolvable()
653 | {
654 | $this->expectException(ContainerExceptionInterface::class);
655 |
656 | $container = new Container;
657 | $container->bind('Taylor', IContainerContractStub::class);
658 |
659 | $container->get('Taylor');
660 | }
661 |
662 | public function testContainerCanResolveClasses()
663 | {
664 | $container = new Container;
665 | $class = $container->get(ContainerConcreteStub::class);
666 |
667 | $this->assertInstanceOf(ContainerConcreteStub::class, $class);
668 | }
669 |
670 | // public function testContainerCanCatchCircularDependency()
671 | // {
672 | // $this->expectException(\Illuminate\Contracts\Container\CircularDependencyException::class);
673 |
674 | // $container = new Container;
675 | // $container->get(CircularAStub::class);
676 | // }
677 |
678 | public function testGetMethodBindings()
679 | {
680 | $container = new Container;
681 | $methodBindings = $container->getMethodBindings();
682 |
683 | $this->assertIsArray($methodBindings);
684 | }
685 |
686 | public function testMakeOr()
687 | {
688 | $container = new Container;
689 | $callback = function ($app) {
690 | $app->bind('test', fn () => true);
691 |
692 | return false;
693 | };
694 |
695 | $var1 = $container->makeOr('test', $callback);
696 | $this->assertFalse($var1);
697 |
698 | $var2 = $container->makeOr('test', $callback);
699 | $this->assertTrue($var2);
700 | }
701 |
702 | public function testMakeOrParametrized()
703 | {
704 | $container = new Container;
705 | $callback = function ($app) {
706 | $app->bind('test', fn () => true);
707 |
708 | return false;
709 | };
710 |
711 | $var1 = $container->makeOr(ContainerInjectVariableStub::class, $callback);
712 | $this->assertFalse($var1);
713 |
714 | $var2 = $container->makeOr(ContainerInjectVariableStub::class, ['something' => 'laravel'], $callback);
715 | $this->assertSame('laravel', $var2->something);
716 | }
717 |
718 | public function testMakeOrResolutionWithoutCallback()
719 | {
720 | $container = new Container;
721 | $var = $container->makeOr(ContainerInjectVariableStub::class, ['something' => 'laravel']);
722 | $this->assertSame('laravel', $var->something);
723 | }
724 |
725 | public function testMakeOrThrowsExceptionWhenCannotResolveAbstractOrExecuteCallback()
726 | {
727 | $this->expectException(BindingResolutionException::class);
728 | $container = new Container;
729 | $container->makeOr(ContainerInjectVariableStub::class);
730 | }
731 |
732 | public function testRemember()
733 | {
734 | $container = new Container;
735 | $callback = function ($app) {
736 | $app->bind('executed', fn () => 'once');
737 |
738 | return true;
739 | };
740 |
741 | $var1 = $container->remember('test', $callback);
742 | $this->assertTrue($var1);
743 |
744 | $var2 = $container->remember('test', $callback);
745 | $this->assertTrue($var2);
746 |
747 | $this->assertSame('once', $container->make('executed'));
748 | }
749 |
750 | public function testResolveVariadicPrimitive()
751 | {
752 | $container = new Container;
753 | $parent = $container->make(VariadicPrimitive::class);
754 |
755 | $this->assertSame($parent->params, []);
756 | }
757 | }
758 |
759 | class CircularAStub
760 | {
761 | public function __construct(CircularBStub $b)
762 | {
763 | //
764 | }
765 | }
766 |
767 | class CircularBStub
768 | {
769 | public function __construct(CircularCStub $c)
770 | {
771 | //
772 | }
773 | }
774 |
775 | class CircularCStub
776 | {
777 | public function __construct(CircularAStub $a)
778 | {
779 | //
780 | }
781 | }
782 |
783 | class ContainerConcreteStub
784 | {
785 | //
786 | }
787 |
788 | interface IContainerContractStub
789 | {
790 | //
791 | }
792 |
793 | class ContainerImplementationStub implements IContainerContractStub
794 | {
795 | //
796 | }
797 |
798 | class ContainerImplementationStubTwo implements IContainerContractStub
799 | {
800 | //
801 | }
802 |
803 | class ContainerDependentStub
804 | {
805 | public $impl;
806 |
807 | public function __construct(IContainerContractStub $impl)
808 | {
809 | $this->impl = $impl;
810 | }
811 | }
812 |
813 | class ContainerNestedDependentStub
814 | {
815 | public $inner;
816 |
817 | public function __construct(ContainerDependentStub $inner)
818 | {
819 | $this->inner = $inner;
820 | }
821 | }
822 |
823 | class ContainerDefaultValueStub
824 | {
825 | public $stub;
826 | public $default;
827 |
828 | public function __construct(ContainerConcreteStub $stub, $default = 'taylor')
829 | {
830 | $this->stub = $stub;
831 | $this->default = $default;
832 | }
833 | }
834 |
835 | class ContainerMixedPrimitiveStub
836 | {
837 | public $first;
838 | public $last;
839 | public $stub;
840 |
841 | public function __construct($first, ContainerConcreteStub $stub, $last)
842 | {
843 | $this->stub = $stub;
844 | $this->last = $last;
845 | $this->first = $first;
846 | }
847 | }
848 |
849 | class ContainerInjectVariableStub
850 | {
851 | public $something;
852 |
853 | public function __construct(ContainerConcreteStub $concrete, $something)
854 | {
855 | $this->something = $something;
856 | }
857 | }
858 |
859 | class ContainerInjectVariableStubWithInterfaceImplementation implements IContainerContractStub
860 | {
861 | public $something;
862 |
863 | public function __construct(ContainerConcreteStub $concrete, $something)
864 | {
865 | $this->something = $something;
866 | }
867 | }
868 |
869 | class VariadicPrimitive
870 | {
871 | /**
872 | * @var array
873 | */
874 | public $params;
875 |
876 | public function __construct(...$params)
877 | {
878 | $this->params = $params;
879 | }
880 | }
881 |
--------------------------------------------------------------------------------
/src/Overrides/Container.php:
--------------------------------------------------------------------------------
1 | getAlias($c);
188 | }
189 |
190 | return new ContextualBindingBuilder($this, $aliases);
191 | }
192 |
193 | /**
194 | * Define a contextual binding based on an attribute.
195 | *
196 | * @param string $attribute
197 | * @param \Closure $handler
198 | * @return void
199 | */
200 | public function whenHasAttribute(string $attribute, Closure $handler)
201 | {
202 | $this->contextualAttributes[$attribute] = $handler;
203 | }
204 |
205 | /**
206 | * Determine if the given abstract type has been bound.
207 | *
208 | * @param string $abstract
209 | * @return bool
210 | */
211 | public function bound($abstract)
212 | {
213 | return isset($this->bindings[$abstract]) ||
214 | isset($this->instances[$abstract]) ||
215 | $this->isAlias($abstract);
216 | }
217 |
218 | /**
219 | * {@inheritdoc}
220 | *
221 | * @return bool
222 | */
223 | public function has(string $id): bool
224 | {
225 | return $this->bound($id);
226 | }
227 |
228 | /**
229 | * Determine if the given abstract type has been resolved.
230 | *
231 | * @param string $abstract
232 | * @return bool
233 | */
234 | public function resolved($abstract)
235 | {
236 | if ($this->isAlias($abstract)) {
237 | $abstract = $this->getAlias($abstract);
238 | }
239 |
240 | return isset($this->resolved[$abstract]) ||
241 | isset($this->instances[$abstract]);
242 | }
243 |
244 | /**
245 | * Determine if a given type is shared.
246 | *
247 | * @param string $abstract
248 | * @return bool
249 | */
250 | public function isShared($abstract)
251 | {
252 | return isset($this->instances[$abstract]) ||
253 | (isset($this->bindings[$abstract]['shared']) &&
254 | $this->bindings[$abstract]['shared'] === true);
255 | }
256 |
257 | /**
258 | * Determine if a given string is an alias.
259 | *
260 | * @param string $name
261 | * @return bool
262 | */
263 | public function isAlias($name)
264 | {
265 | return isset($this->aliases[$name]);
266 | }
267 |
268 | /**
269 | * Register a binding with the container.
270 | *
271 | * @param string $abstract
272 | * @param \Closure|string|null $concrete
273 | * @param bool $shared
274 | * @return void
275 | *
276 | * @throws \TypeError
277 | */
278 | public function bind($abstract, $concrete = null, $shared = false)
279 | {
280 | $this->dropStaleInstances($abstract);
281 |
282 | // If no concrete type was given, we will simply set the concrete type to the
283 | // abstract type. After that, the concrete type to be registered as shared
284 | // without being forced to state their classes in both of the parameters.
285 | if (is_null($concrete)) {
286 | $concrete = $abstract;
287 | }
288 |
289 | // If the factory is not a Closure, it means it is just a class name which is
290 | // bound into this container to the abstract type and we will just wrap it
291 | // up inside its own Closure to give us more convenience when extending.
292 | if (! $concrete instanceof Closure) {
293 | if (! is_string($concrete)) {
294 | throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
295 | }
296 |
297 | $concrete = $this->getClosure($abstract, $concrete);
298 | }
299 |
300 | $this->bindings[$abstract] = compact('concrete', 'shared');
301 |
302 | // If the abstract type was already resolved in this container we'll fire the
303 | // rebound listener so that any objects which have already gotten resolved
304 | // can have their copy of the object updated via the listener callbacks.
305 | if ($this->resolved($abstract)) {
306 | $this->rebound($abstract);
307 | }
308 | }
309 |
310 | /**
311 | * Get the Closure to be used when building a type.
312 | *
313 | * @param string $abstract
314 | * @param string $concrete
315 | * @return \Closure
316 | */
317 | protected function getClosure($abstract, $concrete)
318 | {
319 | return function ($container, $parameters = []) use ($abstract, $concrete) {
320 | if ($abstract == $concrete) {
321 | return $container->build($concrete);
322 | }
323 |
324 | return $container->resolve(
325 | $concrete, $parameters, $raiseEvents = false
326 | );
327 | };
328 | }
329 |
330 | /**
331 | * Determine if the container has a method binding.
332 | *
333 | * @param string $method
334 | * @return bool
335 | */
336 | public function hasMethodBinding($method)
337 | {
338 | return isset($this->methodBindings[$method]);
339 | }
340 |
341 | /**
342 | * Bind a callback to resolve with Container::call.
343 | *
344 | * @param array|string $method
345 | * @param \Closure $callback
346 | * @return void
347 | */
348 | public function bindMethod($method, $callback)
349 | {
350 | $this->methodBindings[$this->parseBindMethod($method)] = $callback;
351 | }
352 |
353 | /**
354 | * Get the method to be bound in class@method format.
355 | *
356 | * @param array|string $method
357 | * @return string
358 | */
359 | protected function parseBindMethod($method)
360 | {
361 | if (is_array($method)) {
362 | return $method[0].'@'.$method[1];
363 | }
364 |
365 | return $method;
366 | }
367 |
368 | /**
369 | * Get the method binding for the given method.
370 | *
371 | * @param string $method
372 | * @param mixed $instance
373 | * @param array $parameters
374 | *
375 | * @return mixed
376 | */
377 | public function callMethodBinding($method, $instance, $parameters)
378 | {
379 | return call_user_func($this->methodBindings[$method], $instance, $this, $parameters);
380 | }
381 |
382 | /**
383 | * Add a contextual binding to the container.
384 | *
385 | * @param string $concrete
386 | * @param string $abstract
387 | * @param \Closure|string $implementation
388 | * @return void
389 | */
390 | public function addContextualBinding($concrete, $abstract, $implementation)
391 | {
392 | $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
393 | }
394 |
395 | /**
396 | * Register a binding if it hasn't already been registered.
397 | *
398 | * @param string $abstract
399 | * @param \Closure|string|null $concrete
400 | * @param bool $shared
401 | * @return void
402 | */
403 | public function bindIf($abstract, $concrete = null, $shared = false)
404 | {
405 | if (! $this->bound($abstract)) {
406 | $this->bind($abstract, $concrete, $shared);
407 | }
408 | }
409 |
410 | /**
411 | * Register a shared binding in the container.
412 | *
413 | * @param string $abstract
414 | * @param \Closure|string|null $concrete
415 | * @return void
416 | */
417 | public function singleton($abstract, $concrete = null)
418 | {
419 | $this->bind($abstract, $concrete, true);
420 | }
421 |
422 | /**
423 | * Register a shared binding if it hasn't already been registered.
424 | *
425 | * @param string $abstract
426 | * @param \Closure|string|null $concrete
427 | * @return void
428 | */
429 | public function singletonIf($abstract, $concrete = null)
430 | {
431 | if (! $this->bound($abstract)) {
432 | $this->singleton($abstract, $concrete);
433 | }
434 | }
435 |
436 | /**
437 | * Register a scoped binding in the container.
438 | *
439 | * @param string $abstract
440 | * @param \Closure|string|null $concrete
441 | * @return void
442 | */
443 | public function scoped($abstract, $concrete = null)
444 | {
445 | $this->scopedInstances[] = $abstract;
446 |
447 | $this->singleton($abstract, $concrete);
448 | }
449 |
450 | /**
451 | * Register a scoped binding if it hasn't already been registered.
452 | *
453 | * @param string $abstract
454 | * @param \Closure|string|null $concrete
455 | * @return void
456 | */
457 | public function scopedIf($abstract, $concrete = null)
458 | {
459 | if (! $this->bound($abstract)) {
460 | $this->scoped($abstract, $concrete);
461 | }
462 | }
463 |
464 | /**
465 | * "Extend" an abstract type in the container.
466 | *
467 | * @param string $abstract
468 | * @param \Closure $closure
469 | * @return void
470 | *
471 | * @throws \InvalidArgumentException
472 | */
473 | public function extend($abstract, Closure $closure)
474 | {
475 | $abstract = $this->getAlias($abstract);
476 |
477 | if (isset($this->instances[$abstract])) {
478 | $this->instances[$abstract] = $closure($this->instances[$abstract], $this);
479 |
480 | $this->rebound($abstract);
481 | } else {
482 | $this->extenders[$abstract][] = $closure;
483 |
484 | if ($this->resolved($abstract)) {
485 | $this->rebound($abstract);
486 | }
487 | }
488 | }
489 |
490 | /**
491 | * Register an existing instance as shared in the container.
492 | *
493 | * @param string $abstract
494 | * @param mixed $instance
495 | * @return mixed
496 | */
497 | public function instance($abstract, $instance)
498 | {
499 | $this->removeAbstractAlias($abstract);
500 |
501 | $isBound = $this->bound($abstract);
502 |
503 | unset($this->aliases[$abstract]);
504 |
505 | // We'll check to determine if this type has been bound before, and if it has
506 | // we will fire the rebound callbacks registered with the container and it
507 | // can be updated with consuming classes that have gotten resolved here.
508 | $this->instances[$abstract] = $instance;
509 |
510 | if ($isBound) {
511 | $this->rebound($abstract);
512 | }
513 |
514 | return $instance;
515 | }
516 |
517 | /**
518 | * Remove an alias from the contextual binding alias cache.
519 | *
520 | * @param string $searched
521 | * @return void
522 | */
523 | protected function removeAbstractAlias($searched)
524 | {
525 | if (! isset($this->aliases[$searched])) {
526 | return;
527 | }
528 |
529 | foreach ($this->abstractAliases as $abstract => $aliases) {
530 | foreach ($aliases as $index => $alias) {
531 | if ($alias == $searched) {
532 | unset($this->abstractAliases[$abstract][$index]);
533 | }
534 | }
535 | }
536 | }
537 |
538 | /**
539 | * Assign a set of tags to a given binding.
540 | *
541 | * @param array|string $abstracts
542 | * @param array|mixed ...$tags
543 | * @return void
544 | */
545 | public function tag($abstracts, $tags)
546 | {
547 | $tags = is_array($tags) ? $tags : array_slice(func_get_args(), 1);
548 |
549 | foreach ($tags as $tag) {
550 | if (! isset($this->tags[$tag])) {
551 | $this->tags[$tag] = [];
552 | }
553 |
554 | foreach ((array) $abstracts as $abstract) {
555 | $this->tags[$tag][] = $abstract;
556 | }
557 | }
558 | }
559 |
560 | /**
561 | * Resolve all of the bindings for a given tag.
562 | *
563 | * @param string $tag
564 | * @return iterable
565 | */
566 | public function tagged($tag)
567 | {
568 | if (! isset($this->tags[$tag])) {
569 | return [];
570 | }
571 |
572 | return new RewindableGenerator(function () use ($tag) {
573 | foreach ($this->tags[$tag] as $abstract) {
574 | yield $this->make($abstract);
575 | }
576 | }, count($this->tags[$tag]));
577 | }
578 |
579 | /**
580 | * Alias a type to a different name.
581 | *
582 | * @param string $abstract
583 | * @param string $alias
584 | * @return void
585 | *
586 | * @throws \LogicException
587 | */
588 | public function alias($abstract, $alias)
589 | {
590 | if ($alias === $abstract) {
591 | throw new LogicException("[{$abstract}] is aliased to itself.");
592 | }
593 |
594 | $this->aliases[$alias] = $abstract;
595 |
596 | $this->abstractAliases[$abstract][] = $alias;
597 | }
598 |
599 | /**
600 | * Bind a new callback to an abstract's rebind event.
601 | *
602 | * @param string $abstract
603 | * @param \Closure $callback
604 | * @return mixed
605 | */
606 | public function rebinding($abstract, Closure $callback)
607 | {
608 | $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback;
609 |
610 | if ($this->bound($abstract)) {
611 | return $this->make($abstract);
612 | }
613 | }
614 |
615 | /**
616 | * Refresh an instance on the given target and method.
617 | *
618 | * @param string $abstract
619 | * @param mixed $target
620 | * @param string $method
621 | * @return mixed
622 | */
623 | public function refresh($abstract, $target, $method)
624 | {
625 | return $this->rebinding($abstract, function ($app, $instance) use ($target, $method) {
626 | $target->{$method}($instance);
627 | });
628 | }
629 |
630 | /**
631 | * Fire the "rebound" callbacks for the given abstract type.
632 | *
633 | * @param string $abstract
634 | * @return void
635 | */
636 | protected function rebound($abstract)
637 | {
638 | if (! $callbacks = $this->getReboundCallbacks($abstract)) {
639 | return;
640 | }
641 |
642 | $instance = $this->make($abstract);
643 |
644 | foreach ($callbacks as $callback) {
645 | $callback($this, $instance);
646 | }
647 | }
648 |
649 | /**
650 | * Get the rebound callbacks for a given type.
651 | *
652 | * @param string $abstract
653 | * @return array
654 | */
655 | protected function getReboundCallbacks($abstract)
656 | {
657 | return $this->reboundCallbacks[$abstract] ?? [];
658 | }
659 |
660 | /**
661 | * Wrap the given closure such that its dependencies will be injected when executed.
662 | *
663 | * @param \Closure $callback
664 | * @param array $parameters
665 | * @return \Closure
666 | */
667 | public function wrap(Closure $callback, array $parameters = [])
668 | {
669 | return fn () => $this->call($callback, $parameters);
670 | }
671 |
672 | /**
673 | * Call the given Closure / class@method and inject its dependencies.
674 | *
675 | * @param callable|string $callback
676 | * @param array $parameters
677 | * @param string|null $defaultMethod
678 | * @return mixed
679 | *
680 | * @throws \InvalidArgumentException
681 | */
682 | public function call($callback, array $parameters = [], $defaultMethod = null)
683 | {
684 | $pushedToBuildStack = false;
685 |
686 | if (($className = $this->getClassForCallable($callback)) && ! in_array(
687 | $className,
688 | $this->buildStack,
689 | true
690 | )) {
691 | $this->buildStack[] = $className;
692 |
693 | $pushedToBuildStack = true;
694 | }
695 |
696 | $result = BoundMethod::call($this, $callback, $parameters, $defaultMethod);
697 |
698 | if ($pushedToBuildStack) {
699 | array_pop($this->buildStack);
700 | }
701 |
702 | return $result;
703 | }
704 |
705 | /**
706 | * Get the class name for the given callback, if one can be determined.
707 | *
708 | * @param callable|string $callback
709 | * @return string|false
710 | */
711 | protected function getClassForCallable($callback)
712 | {
713 | if (is_callable($callback) &&
714 | ! ($reflector = new ReflectionFunction($callback(...)))->isAnonymous()) {
715 | return $reflector->getClosureScopeClass()->name ?? false;
716 | }
717 |
718 | return false;
719 | }
720 |
721 | /**
722 | * Get a closure to resolve the given type from the container.
723 | *
724 | * @param string $abstract
725 | * @return \Closure
726 | */
727 | public function factory($abstract)
728 | {
729 | return fn () => $this->make($abstract);
730 | }
731 |
732 | /**
733 | * An alias function name for make().
734 | *
735 | * @param string|callable $abstract
736 | * @param array $parameters
737 | * @return mixed
738 | *
739 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
740 | */
741 | public function makeWith($abstract, array $parameters = [])
742 | {
743 | return $this->make($abstract, $parameters);
744 | }
745 |
746 | /**
747 | * Resolve the given type from the container.
748 | *
749 | * @param string $abstract
750 | * @param array $parameters
751 | * @return mixed
752 | *
753 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
754 | */
755 | public function make($abstract, array $parameters = [])
756 | {
757 | return $this->resolve($abstract, $parameters);
758 | }
759 |
760 | /**
761 | * Resolve the given type from the container or execute the callback.
762 | *
763 | * @param string $abstract
764 | * @param mixed|array $parameters
765 | * @param \Closure|null $callback
766 | * @return mixed
767 | *
768 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
769 | */
770 | public function makeOr($abstract, $parameters = [], ?Closure $callback = null)
771 | {
772 | try {
773 | if ($parameters instanceof Closure) {
774 | $callback = $parameters;
775 |
776 | return $this->make($abstract);
777 | }
778 |
779 | return $this->make($abstract, $parameters);
780 | } catch (BindingResolutionException $e) {
781 | return $callback instanceof Closure ? $callback($this) : throw $e;
782 | }
783 | }
784 |
785 | /**
786 | * Bind the given type to the container with the results of the callback.
787 | *
788 | * @param string $abstract
789 | * @param \Closure|null $callback
790 | * @param string $type
791 | * @return mixed
792 | */
793 | public function remember($abstract, ?Closure $callback = null, $type = 'scoped')
794 | {
795 | return $this->makeOr($abstract, function () use ($abstract, $callback, $type) {
796 | $result = $callback($this);
797 |
798 | $this->{$type}($abstract, fn () => $result);
799 |
800 | return $result;
801 | });
802 | }
803 |
804 | /**
805 | * {@inheritdoc}
806 | *
807 | * @return mixed
808 | */
809 | public function get(string $id)
810 | {
811 | try {
812 | return $this->resolve($id);
813 | } catch (Exception $e) {
814 | if ($this->has($id) || $e instanceof CircularDependencyException) {
815 | throw $e;
816 | }
817 |
818 | throw new EntryNotFoundException($id, is_int($e->getCode()) ? $e->getCode() : 0, $e);
819 | }
820 | }
821 |
822 | /**
823 | * Resolve the given type from the container.
824 | *
825 | * @param string|callable $abstract
826 | * @param array $parameters
827 | * @param bool $raiseEvents
828 | * @return mixed
829 | *
830 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
831 | * @throws \Illuminate\Contracts\Container\CircularDependencyException
832 | */
833 | protected function resolve($abstract, $parameters = [], $raiseEvents = true)
834 | {
835 | $abstract = $this->getAlias($abstract);
836 |
837 | // First we'll fire any event handlers which handle the "before" resolving of
838 | // specific types. This gives some hooks the chance to add various extends
839 | // calls to change the resolution of objects that they're interested in.
840 | if ($raiseEvents) {
841 | $this->fireBeforeResolvingCallbacks($abstract, $parameters);
842 | }
843 |
844 | $concrete = $this->getContextualConcrete($abstract);
845 |
846 | $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
847 |
848 | // If an instance of the type is currently being managed as a singleton we'll
849 | // just return an existing instance instead of instantiating new instances
850 | // so the developer can keep using the same objects instance every time.
851 | if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
852 | return $this->instances[$abstract];
853 | }
854 |
855 | $this->with[] = $parameters;
856 |
857 | if (is_null($concrete)) {
858 | $concrete = $this->getConcrete($abstract);
859 | }
860 |
861 | // We're ready to instantiate an instance of the concrete type registered for
862 | // the binding. This will instantiate the types, as well as resolve any of
863 | // its "nested" dependencies recursively until all have gotten resolved.
864 | $object = $this->isBuildable($concrete, $abstract)
865 | ? $this->build($concrete)
866 | : $this->make($concrete);
867 |
868 | // If we defined any extenders for this type, we'll need to spin through them
869 | // and apply them to the object being built. This allows for the extension
870 | // of services, such as changing configuration or decorating the object.
871 | foreach ($this->getExtenders($abstract) as $extender) {
872 | $object = $extender($object, $this);
873 | }
874 |
875 | // If the requested type is registered as a singleton we'll want to cache off
876 | // the instances in "memory" so we can return it later without creating an
877 | // entirely new instance of an object on each subsequent request for it.
878 | if ($this->isShared($abstract) && ! $needsContextualBuild) {
879 | $this->instances[$abstract] = $object;
880 | }
881 |
882 | if ($raiseEvents) {
883 | $this->fireResolvingCallbacks($abstract, $object);
884 | }
885 |
886 | // Before returning, we will also set the resolved flag to "true" and pop off
887 | // the parameter overrides for this build. After those two things are done
888 | // we will be ready to return back the fully constructed class instance.
889 | if (! $needsContextualBuild) {
890 | $this->resolved[$abstract] = true;
891 | }
892 |
893 | array_pop($this->with);
894 |
895 | return $object;
896 | }
897 |
898 | /**
899 | * Get the concrete type for a given abstract.
900 | *
901 | * @param string|callable $abstract
902 | * @return mixed
903 | */
904 | protected function getConcrete($abstract)
905 | {
906 | // If we don't have a registered resolver or concrete for the type, we'll just
907 | // assume each type is a concrete name and will attempt to resolve it as is
908 | // since the container should be able to resolve concretes automatically.
909 | if (isset($this->bindings[$abstract])) {
910 | return $this->bindings[$abstract]['concrete'];
911 | }
912 |
913 | return $abstract;
914 | }
915 |
916 | /**
917 | * Get the contextual concrete binding for the given abstract.
918 | *
919 | * @param string|callable $abstract
920 | * @return \Closure|string|array|null
921 | */
922 | protected function getContextualConcrete($abstract)
923 | {
924 | if (! is_null($binding = $this->findInContextualBindings($abstract))) {
925 | return $binding;
926 | }
927 |
928 | // Next we need to see if a contextual binding might be bound under an alias of the
929 | // given abstract type. So, we will need to check if any aliases exist with this
930 | // type and then spin through them and check for contextual bindings on these.
931 | if (empty($this->abstractAliases[$abstract])) {
932 | return;
933 | }
934 |
935 | foreach ($this->abstractAliases[$abstract] as $alias) {
936 | if (! is_null($binding = $this->findInContextualBindings($alias))) {
937 | return $binding;
938 | }
939 | }
940 | }
941 |
942 | /**
943 | * Find the concrete binding for the given abstract in the contextual binding array.
944 | *
945 | * @param string|callable $abstract
946 | * @return \Closure|string|null
947 | */
948 | protected function findInContextualBindings($abstract)
949 | {
950 | return $this->contextual[end($this->buildStack)][$abstract] ?? null;
951 | }
952 |
953 | /**
954 | * Determine if the given concrete is buildable.
955 | *
956 | * @param mixed $concrete
957 | * @param string $abstract
958 | * @return bool
959 | */
960 | protected function isBuildable($concrete, $abstract)
961 | {
962 | return $concrete === $abstract || $concrete instanceof Closure;
963 | }
964 |
965 | /**
966 | * Instantiate a concrete instance of the given type.
967 | *
968 | * @param \Closure|string $concrete
969 | * @return mixed
970 | *
971 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
972 | * @throws \Illuminate\Contracts\Container\CircularDependencyException
973 | */
974 | public function build($concrete)
975 | {
976 | // If the concrete type is actually a Closure, we will just execute it and
977 | // hand back the results of the functions, which allows functions to be
978 | // used as resolvers for more fine-tuned resolution of these objects.
979 | if ($concrete instanceof Closure) {
980 | $this->buildStack[] = spl_object_hash($concrete);
981 |
982 | try {
983 | return $concrete($this, $this->getLastParameterOverride());
984 | } finally {
985 | array_pop($this->buildStack);
986 | }
987 | }
988 |
989 | try {
990 | $reflector = new ReflectionClass($concrete);
991 | } catch (ReflectionException $e) {
992 | throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
993 | }
994 |
995 | // If the type is not instantiable, the developer is attempting to resolve
996 | // an abstract type such as an Interface or Abstract Class and there is
997 | // no binding registered for the abstractions so we need to bail out.
998 | if (! $reflector->isInstantiable()) {
999 | return $this->notInstantiable($concrete);
1000 | }
1001 |
1002 | $this->buildStack[] = $concrete;
1003 |
1004 | $constructor = $reflector->getConstructor();
1005 |
1006 | // If there are no constructors, that means there are no dependencies then
1007 | // we can just resolve the instances of the objects right away, without
1008 | // resolving any other types or dependencies out of these containers.
1009 | if (is_null($constructor)) {
1010 | array_pop($this->buildStack);
1011 |
1012 | $this->fireAfterResolvingAttributeCallbacks(
1013 | $reflector->getAttributes(), $instance = new $concrete
1014 | );
1015 |
1016 | return $instance;
1017 | }
1018 |
1019 | $dependencies = $constructor->getParameters();
1020 |
1021 | // Once we have all the constructor's parameters we can create each of the
1022 | // dependency instances and then use the reflection instances to make a
1023 | // new instance of this class, injecting the created dependencies in.
1024 | try {
1025 | $instances = $this->resolveDependencies($dependencies);
1026 | } catch (BindingResolutionException $e) {
1027 | array_pop($this->buildStack);
1028 |
1029 | throw $e;
1030 | }
1031 |
1032 | array_pop($this->buildStack);
1033 |
1034 | $this->fireAfterResolvingAttributeCallbacks(
1035 | $reflector->getAttributes(), $instance = $reflector->newInstanceArgs($instances)
1036 | );
1037 |
1038 | return $instance;
1039 | }
1040 |
1041 | /**
1042 | * Resolve all of the dependencies from the ReflectionParameters.
1043 | *
1044 | * @param \ReflectionParameter[] $dependencies
1045 | * @return array
1046 | *
1047 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
1048 | */
1049 | protected function resolveDependencies(array $dependencies)
1050 | {
1051 | $results = [];
1052 |
1053 | foreach ($dependencies as $dependency) {
1054 | // If the dependency has an override for this particular build we will use
1055 | // that instead as the value. Otherwise, we will continue with this run
1056 | // of resolutions and let reflection attempt to determine the result.
1057 | if ($this->hasParameterOverride($dependency)) {
1058 | $results[] = $this->getParameterOverride($dependency);
1059 |
1060 | continue;
1061 | }
1062 |
1063 | $result = null;
1064 |
1065 | if (! is_null($attribute = Util::getContextualAttributeFromDependency($dependency))) {
1066 | $result = $this->resolveFromAttribute($attribute);
1067 | }
1068 |
1069 | // If the class is null, it means the dependency is a string or some other
1070 | // primitive type which we can not resolve since it is not a class and
1071 | // we will just bomb out with an error since we have no-where to go.
1072 | $result ??= is_null(Util::getParameterClassName($dependency))
1073 | ? $this->resolvePrimitive($dependency)
1074 | : $this->resolveClass($dependency);
1075 |
1076 | $this->fireAfterResolvingAttributeCallbacks($dependency->getAttributes(), $result);
1077 |
1078 | if ($dependency->isVariadic()) {
1079 | $results = array_merge($results, $result);
1080 | } else {
1081 | $results[] = $result;
1082 | }
1083 | }
1084 |
1085 | return $results;
1086 | }
1087 |
1088 | /**
1089 | * Determine if the given dependency has a parameter override.
1090 | *
1091 | * @param \ReflectionParameter $dependency
1092 | * @return bool
1093 | */
1094 | protected function hasParameterOverride($dependency)
1095 | {
1096 | return array_key_exists(
1097 | $dependency->name, $this->getLastParameterOverride()
1098 | );
1099 | }
1100 |
1101 | /**
1102 | * Get a parameter override for a dependency.
1103 | *
1104 | * @param \ReflectionParameter $dependency
1105 | * @return mixed
1106 | */
1107 | protected function getParameterOverride($dependency)
1108 | {
1109 | return $this->getLastParameterOverride()[$dependency->name];
1110 | }
1111 |
1112 | /**
1113 | * Get the last parameter override.
1114 | *
1115 | * @return array
1116 | */
1117 | protected function getLastParameterOverride()
1118 | {
1119 | return count($this->with) ? end($this->with) : [];
1120 | }
1121 |
1122 | /**
1123 | * Resolve a non-class hinted primitive dependency.
1124 | *
1125 | * @param \ReflectionParameter $parameter
1126 | * @return mixed
1127 | *
1128 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
1129 | */
1130 | protected function resolvePrimitive(ReflectionParameter $parameter)
1131 | {
1132 | if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->getName()))) {
1133 | return Util::unwrapIfClosure($concrete, $this);
1134 | }
1135 |
1136 | if ($parameter->isDefaultValueAvailable()) {
1137 | return $parameter->getDefaultValue();
1138 | }
1139 |
1140 | if ($parameter->isVariadic()) {
1141 | return [];
1142 | }
1143 |
1144 | if ($parameter->hasType() && $parameter->allowsNull()) {
1145 | return null;
1146 | }
1147 |
1148 | $this->unresolvablePrimitive($parameter);
1149 | }
1150 |
1151 | /**
1152 | * Resolve a class based dependency from the container.
1153 | *
1154 | * @param \ReflectionParameter $parameter
1155 | * @return mixed
1156 | *
1157 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
1158 | */
1159 | protected function resolveClass(ReflectionParameter $parameter)
1160 | {
1161 | try {
1162 | return $parameter->isVariadic()
1163 | ? $this->resolveVariadicClass($parameter)
1164 | : $this->make(Util::getParameterClassName($parameter));
1165 | }
1166 |
1167 | // If we can not resolve the class instance, we will check to see if the value
1168 | // is optional, and if it is we will return the optional parameter value as
1169 | // the value of the dependency, similarly to how we do this with scalars.
1170 | catch (BindingResolutionException $e) {
1171 | if ($parameter->isDefaultValueAvailable()) {
1172 | array_pop($this->with);
1173 |
1174 | return $parameter->getDefaultValue();
1175 | }
1176 |
1177 | if ($parameter->isVariadic()) {
1178 | array_pop($this->with);
1179 |
1180 | return [];
1181 | }
1182 |
1183 | throw $e;
1184 | }
1185 | }
1186 |
1187 | /**
1188 | * Resolve a class based variadic dependency from the container.
1189 | *
1190 | * @param \ReflectionParameter $parameter
1191 | * @return mixed
1192 | */
1193 | protected function resolveVariadicClass(ReflectionParameter $parameter)
1194 | {
1195 | $className = Util::getParameterClassName($parameter);
1196 |
1197 | $abstract = $this->getAlias($className);
1198 |
1199 | if (! is_array($concrete = $this->getContextualConcrete($abstract))) {
1200 | return $this->make($className);
1201 | }
1202 |
1203 | return array_map(fn ($abstract) => $this->resolve($abstract), $concrete);
1204 | }
1205 |
1206 | /**
1207 | * Resolve a dependency based on an attribute.
1208 | *
1209 | * @param \ReflectionAttribute $attribute
1210 | * @return mixed
1211 | */
1212 | public function resolveFromAttribute(ReflectionAttribute $attribute)
1213 | {
1214 | $handler = $this->contextualAttributes[$attribute->getName()] ?? null;
1215 |
1216 | $instance = $attribute->newInstance();
1217 |
1218 | if (is_null($handler) && method_exists($instance, 'resolve')) {
1219 | $handler = $instance->resolve(...);
1220 | }
1221 |
1222 | if (is_null($handler)) {
1223 | throw new BindingResolutionException("Contextual binding attribute [{$attribute->getName()}] has no registered handler.");
1224 | }
1225 |
1226 | return $handler($instance, $this);
1227 | }
1228 |
1229 | /**
1230 | * Throw an exception that the concrete is not instantiable.
1231 | *
1232 | * @param string $concrete
1233 | * @return void
1234 | *
1235 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
1236 | */
1237 | protected function notInstantiable($concrete)
1238 | {
1239 | if (! empty($this->buildStack)) {
1240 | $previous = implode(', ', $this->buildStack);
1241 |
1242 | $message = "Target [$concrete] is not instantiable while building [$previous].";
1243 | } else {
1244 | $message = "Target [$concrete] is not instantiable.";
1245 | }
1246 |
1247 | throw new BindingResolutionException($message);
1248 | }
1249 |
1250 | /**
1251 | * Throw an exception for an unresolvable primitive.
1252 | *
1253 | * @param \ReflectionParameter $parameter
1254 | * @return void
1255 | *
1256 | * @throws \Illuminate\Contracts\Container\BindingResolutionException
1257 | */
1258 | protected function unresolvablePrimitive(ReflectionParameter $parameter)
1259 | {
1260 | $message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";
1261 |
1262 | throw new BindingResolutionException($message);
1263 | }
1264 |
1265 | /**
1266 | * Register a new before resolving callback for all types.
1267 | *
1268 | * @param \Closure|string $abstract
1269 | * @param \Closure|null $callback
1270 | * @return void
1271 | */
1272 | public function beforeResolving($abstract, ?Closure $callback = null)
1273 | {
1274 | if (is_string($abstract)) {
1275 | $abstract = $this->getAlias($abstract);
1276 | }
1277 |
1278 | if ($abstract instanceof Closure && is_null($callback)) {
1279 | $this->globalBeforeResolvingCallbacks[] = $abstract;
1280 | } else {
1281 | $this->beforeResolvingCallbacks[$abstract][] = $callback;
1282 | }
1283 | }
1284 |
1285 | /**
1286 | * Register a new resolving callback.
1287 | *
1288 | * @param \Closure|string $abstract
1289 | * @param \Closure|null $callback
1290 | * @return void
1291 | */
1292 | public function resolving($abstract, ?Closure $callback = null)
1293 | {
1294 | if (is_string($abstract)) {
1295 | $abstract = $this->getAlias($abstract);
1296 | }
1297 |
1298 | if (is_null($callback) && $abstract instanceof Closure) {
1299 | $this->globalResolvingCallbacks[] = $abstract;
1300 | } else {
1301 | $this->resolvingCallbacks[$abstract][] = $callback;
1302 | }
1303 | }
1304 |
1305 | /**
1306 | * Register a new after resolving callback for all types.
1307 | *
1308 | * @param \Closure|string $abstract
1309 | * @param \Closure|null $callback
1310 | * @return void
1311 | */
1312 | public function afterResolving($abstract, ?Closure $callback = null)
1313 | {
1314 | if (is_string($abstract)) {
1315 | $abstract = $this->getAlias($abstract);
1316 | }
1317 |
1318 | if ($abstract instanceof Closure && is_null($callback)) {
1319 | $this->globalAfterResolvingCallbacks[] = $abstract;
1320 | } else {
1321 | $this->afterResolvingCallbacks[$abstract][] = $callback;
1322 | }
1323 | }
1324 |
1325 | /**
1326 | * Register a new after resolving attribute callback for all types.
1327 | *
1328 | * @param string $attribute
1329 | * @param \Closure $callback
1330 | * @return void
1331 | */
1332 | public function afterResolvingAttribute(string $attribute, \Closure $callback)
1333 | {
1334 | $this->afterResolvingAttributeCallbacks[$attribute][] = $callback;
1335 | }
1336 |
1337 | /**
1338 | * Fire all of the before resolving callbacks.
1339 | *
1340 | * @param string $abstract
1341 | * @param array $parameters
1342 | * @return void
1343 | */
1344 | protected function fireBeforeResolvingCallbacks($abstract, $parameters = [])
1345 | {
1346 | $this->fireBeforeCallbackArray($abstract, $parameters, $this->globalBeforeResolvingCallbacks);
1347 |
1348 | foreach ($this->beforeResolvingCallbacks as $type => $callbacks) {
1349 | if ($type === $abstract || is_subclass_of($abstract, $type)) {
1350 | $this->fireBeforeCallbackArray($abstract, $parameters, $callbacks);
1351 | }
1352 | }
1353 | }
1354 |
1355 | /**
1356 | * Fire an array of callbacks with an object.
1357 | *
1358 | * @param string $abstract
1359 | * @param array $parameters
1360 | * @param array $callbacks
1361 | * @return void
1362 | */
1363 | protected function fireBeforeCallbackArray($abstract, $parameters, array $callbacks)
1364 | {
1365 | foreach ($callbacks as $callback) {
1366 | $callback($abstract, $parameters, $this);
1367 | }
1368 | }
1369 |
1370 | /**
1371 | * Fire all of the resolving callbacks.
1372 | *
1373 | * @param string $abstract
1374 | * @param mixed $object
1375 | * @return void
1376 | */
1377 | protected function fireResolvingCallbacks($abstract, $object)
1378 | {
1379 | $this->fireCallbackArray($object, $this->globalResolvingCallbacks);
1380 |
1381 | $this->fireCallbackArray(
1382 | $object, $this->getCallbacksForType($abstract, $object, $this->resolvingCallbacks)
1383 | );
1384 |
1385 | $this->fireAfterResolvingCallbacks($abstract, $object);
1386 | }
1387 |
1388 | /**
1389 | * Fire all of the after resolving callbacks.
1390 | *
1391 | * @param string $abstract
1392 | * @param mixed $object
1393 | * @return void
1394 | */
1395 | protected function fireAfterResolvingCallbacks($abstract, $object)
1396 | {
1397 | $this->fireCallbackArray($object, $this->globalAfterResolvingCallbacks);
1398 |
1399 | $this->fireCallbackArray(
1400 | $object, $this->getCallbacksForType($abstract, $object, $this->afterResolvingCallbacks)
1401 | );
1402 | }
1403 |
1404 | /**
1405 | * Fire all of the after resolving attribute callbacks.
1406 | *
1407 | * @param \ReflectionAttribute[] $attributes
1408 | * @param mixed $object
1409 | * @return void
1410 | */
1411 | public function fireAfterResolvingAttributeCallbacks(array $attributes, $object)
1412 | {
1413 | foreach ($attributes as $attribute) {
1414 | if (is_a($attribute->getName(), ContextualAttribute::class, true)) {
1415 | $instance = $attribute->newInstance();
1416 |
1417 | if (method_exists($instance, 'after')) {
1418 | $instance->after($instance, $object, $this);
1419 | }
1420 | }
1421 |
1422 | $callbacks = $this->getCallbacksForType(
1423 | $attribute->getName(), $object, $this->afterResolvingAttributeCallbacks
1424 | );
1425 |
1426 | foreach ($callbacks as $callback) {
1427 | $callback($attribute->newInstance(), $object, $this);
1428 | }
1429 | }
1430 | }
1431 |
1432 | /**
1433 | * Get all callbacks for a given type.
1434 | *
1435 | * @param string $abstract
1436 | * @param object $object
1437 | * @param array $callbacksPerType
1438 | * @return array
1439 | */
1440 | protected function getCallbacksForType($abstract, $object, array $callbacksPerType)
1441 | {
1442 | $results = [];
1443 |
1444 | foreach ($callbacksPerType as $type => $callbacks) {
1445 | if ($type === $abstract || $object instanceof $type) {
1446 | $results = array_merge($results, $callbacks);
1447 | }
1448 | }
1449 |
1450 | return $results;
1451 | }
1452 |
1453 | /**
1454 | * Fire an array of callbacks with an object.
1455 | *
1456 | * @param mixed $object
1457 | * @param array $callbacks
1458 | * @return void
1459 | */
1460 | protected function fireCallbackArray($object, array $callbacks)
1461 | {
1462 | foreach ($callbacks as $callback) {
1463 | $callback($object, $this);
1464 | }
1465 | }
1466 |
1467 | /**
1468 | * Get the container's bindings.
1469 | *
1470 | * @return array
1471 | */
1472 | public function getBindings()
1473 | {
1474 | return $this->bindings;
1475 | }
1476 |
1477 | /**
1478 | * Get the container's method bindings.
1479 | *
1480 | * @return array
1481 | */
1482 | public function getMethodBindings()
1483 | {
1484 | return $this->methodBindings;
1485 | }
1486 |
1487 | /**
1488 | * Get the alias for an abstract if available.
1489 | *
1490 | * @param string $abstract
1491 | * @return string
1492 | */
1493 | public function getAlias($abstract)
1494 | {
1495 | return isset($this->aliases[$abstract])
1496 | ? $this->getAlias($this->aliases[$abstract])
1497 | : $abstract;
1498 | }
1499 |
1500 | /**
1501 | * Get the extender callbacks for a given type.
1502 | *
1503 | * @param string $abstract
1504 | * @return array
1505 | */
1506 | protected function getExtenders($abstract)
1507 | {
1508 | return $this->extenders[$this->getAlias($abstract)] ?? [];
1509 | }
1510 |
1511 | /**
1512 | * Remove all of the extender callbacks for a given type.
1513 | *
1514 | * @param string $abstract
1515 | * @return void
1516 | */
1517 | public function forgetExtenders($abstract)
1518 | {
1519 | unset($this->extenders[$this->getAlias($abstract)]);
1520 | }
1521 |
1522 | /**
1523 | * Drop all of the stale instances and aliases.
1524 | *
1525 | * @param string $abstract
1526 | * @return void
1527 | */
1528 | protected function dropStaleInstances($abstract)
1529 | {
1530 | unset($this->instances[$abstract], $this->aliases[$abstract]);
1531 | }
1532 |
1533 | /**
1534 | * Remove a resolved instance from the instance cache.
1535 | *
1536 | * @param string $abstract
1537 | * @return void
1538 | */
1539 | public function forgetInstance($abstract)
1540 | {
1541 | unset($this->instances[$abstract]);
1542 | }
1543 |
1544 | /**
1545 | * Clear all of the instances from the container.
1546 | *
1547 | * @return void
1548 | */
1549 | public function forgetInstances()
1550 | {
1551 | $this->instances = [];
1552 | }
1553 |
1554 | /**
1555 | * Clear all of the scoped instances from the container.
1556 | *
1557 | * @return void
1558 | */
1559 | public function forgetScopedInstances()
1560 | {
1561 | foreach ($this->scopedInstances as $scoped) {
1562 | unset($this->instances[$scoped]);
1563 | }
1564 | }
1565 |
1566 | /**
1567 | * Flush the container of all bindings and resolved instances.
1568 | *
1569 | * @return void
1570 | */
1571 | public function flush()
1572 | {
1573 | $this->aliases = [];
1574 | $this->resolved = [];
1575 | $this->bindings = [];
1576 | $this->instances = [];
1577 | $this->methodBindings = [];
1578 | $this->abstractAliases = [];
1579 | $this->scopedInstances = [];
1580 | }
1581 |
1582 | /**
1583 | * Get the globally available instance of the container.
1584 | *
1585 | * @return static
1586 | */
1587 | public static function getInstance()
1588 | {
1589 | return static::$instance ??= new static;
1590 | }
1591 |
1592 | /**
1593 | * Set the shared instance of the container.
1594 | *
1595 | * @param \Illuminate\Contracts\Container\Container|null $container
1596 | * @return \Illuminate\Contracts\Container\Container|static
1597 | */
1598 | public static function setInstance(?ContainerContract $container = null)
1599 | {
1600 | return static::$instance = $container;
1601 | }
1602 |
1603 | /**
1604 | * Determine if a given offset exists.
1605 | *
1606 | * @param string $key
1607 | * @return bool
1608 | */
1609 | public function offsetExists($key): bool
1610 | {
1611 | return $this->bound($key);
1612 | }
1613 |
1614 | /**
1615 | * Get the value at a given offset.
1616 | *
1617 | * @param string $key
1618 | * @return mixed
1619 | */
1620 | public function offsetGet($key): mixed
1621 | {
1622 | return $this->make($key);
1623 | }
1624 |
1625 | /**
1626 | * Set the value at a given offset.
1627 | *
1628 | * @param string $key
1629 | * @param mixed $value
1630 | * @return void
1631 | */
1632 | public function offsetSet($key, $value): void
1633 | {
1634 | $this->bind($key, $value instanceof Closure ? $value : fn () => $value);
1635 | }
1636 |
1637 | /**
1638 | * Unset the value at a given offset.
1639 | *
1640 | * @param string $key
1641 | * @return void
1642 | */
1643 | public function offsetUnset($key): void
1644 | {
1645 | unset($this->bindings[$key], $this->instances[$key], $this->resolved[$key]);
1646 | }
1647 |
1648 | /**
1649 | * Dynamically access container services.
1650 | *
1651 | * @param string $key
1652 | * @return mixed
1653 | */
1654 | public function __get($key)
1655 | {
1656 | return $this[$key];
1657 | }
1658 |
1659 | /**
1660 | * Dynamically set container services.
1661 | *
1662 | * @param string $key
1663 | * @param mixed $value
1664 | * @return void
1665 | */
1666 | public function __set($key, $value)
1667 | {
1668 | $this[$key] = $value;
1669 | }
1670 | }
1671 |
--------------------------------------------------------------------------------