├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── SECURITY.md └── workflows │ ├── bc-check.yml │ ├── infection.yml │ ├── phpstan.yml │ ├── pint.yml │ └── run-tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── infection.json5 ├── phpstan.neon ├── phpunit.xml.dist ├── pint.json ├── src ├── Call.php ├── Core │ ├── CallProxy.php │ ├── Forwarding.php │ └── MethodBinder.php ├── Exceptions │ └── InstanceInteractionException.php ├── Helpers │ └── helpers.php ├── LecServiceProvider.php ├── Overrides │ ├── BoundMethod.php │ └── Container.php └── Traits │ └── InteractsWithContainer.php └── tests ├── Boilerplate ├── BoilerplateDependenciesAssignedOldWay.php ├── BoilerplateInterface.php ├── BoilerplateService.php ├── BoilerplateServiceResolvesContextualInMethod.php ├── BoilerplateServiceResolvesGlobalInMethod.php ├── BoilerplateServiceWithConstructor.php ├── BoilerplateServiceWithConstructorClass.php ├── BoilerplateServiceWithConstructorPrimitive.php ├── BoilerplateServiceWithVariadicConstructor.php ├── BoilerplateServiceWithWrongContext.php ├── BoilerplateWithBootedCallProxyWrongParams.php ├── Builder │ └── Best │ │ └── BestBuilder.php ├── Builders │ └── Best │ │ └── BestBuilder.php ├── Domain │ └── Best │ │ └── BestDomain.php ├── Models │ └── TestModel.php ├── ParameterOrderBoilerplate.php ├── Repositories │ ├── TestRepository.php │ ├── TestRepositoryInterface.php │ └── Users │ │ └── UserRepository.php └── Services │ ├── TestService.php │ ├── TestServiceInterface.php │ └── Users │ └── UserService.php ├── ContainerCallTest.php ├── ContainerTest.php ├── ForwardingTest.php ├── MethodBindingTest.php └── TestCase.php /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: phpstan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | larastan: 13 | name: "Running Larastan" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: '8.3' 22 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl 23 | coverage: none 24 | 25 | - name: Cache composer dependencies 26 | uses: actions/cache@v2 27 | with: 28 | path: vendor 29 | key: composer-${{ hashFiles('composer.lock') }} 30 | 31 | - name: Run composer install 32 | run: composer install -n --prefer-dist 33 | 34 | - name: Run phpstan 35 | run: ./vendor/bin/phpstan 36 | -------------------------------------------------------------------------------- /.github/workflows/pint.yml: -------------------------------------------------------------------------------- 1 | name: pint 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | pint: 10 | name: "Running Laravel Pint" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: '8.3' 19 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl 20 | coverage: none 21 | 22 | - name: Cache composer dependencies 23 | uses: actions/cache@v2 24 | with: 25 | path: vendor 26 | key: composer-${{ hashFiles('composer.lock') }} 27 | 28 | - name: Run composer install 29 | run: composer install -n --prefer-dist 30 | 31 | - name: Run Laravel Pint 32 | run: ./vendor/bin/pint --test -v --config pint.json 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .php_cs 3 | .php_cs.cache 4 | .phpunit.result.cache 5 | build 6 | coverage 7 | docs 8 | phpunit.xml 9 | psalm.xml 10 | testbench.yaml 11 | vendor 12 | node_modules 13 | .php-cs-fixer.cache 14 | .phpunit.cache 15 | composer.lock 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Michael Rubel 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Method binding with Service Container Call Proxy](https://github.com/michael-rubel/laravel-enhanced-container/assets/37669560/f7f2b1d0-68cb-485b-bdc5-93ea109f7e1d) 2 | 3 | # Laravel Enhanced Container 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/michael-rubel/laravel-enhanced-container.svg?style=flat-square)](https://packagist.org/packages/michael-rubel/laravel-enhanced-container) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/michael-rubel/laravel-enhanced-container.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/michael-rubel/laravel-enhanced-container) 6 | [![Code Quality](https://img.shields.io/scrutinizer/quality/g/michael-rubel/laravel-enhanced-container.svg?style=flat-square&logo=scrutinizer)](https://scrutinizer-ci.com/g/michael-rubel/laravel-enhanced-container/?branch=main) 7 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/michael-rubel/laravel-enhanced-container.svg?style=flat-square&logo=scrutinizer)](https://scrutinizer-ci.com/g/michael-rubel/laravel-enhanced-container/?branch=main) 8 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/michael-rubel/laravel-enhanced-container/run-tests.yml?branch=main&style=flat-square&label=tests&logo=github)](https://github.com/michael-rubel/laravel-enhanced-container/actions) 9 | [![PHPStan](https://img.shields.io/github/actions/workflow/status/michael-rubel/laravel-enhanced-container/phpstan.yml?branch=main&style=flat-square&label=larastan&logo=laravel)](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 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Call.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Exceptions/InstanceInteractionException.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 | -------------------------------------------------------------------------------- /src/LecServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-enhanced-container'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/BoilerplateInterface.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 | -------------------------------------------------------------------------------- /tests/Boilerplate/BoilerplateServiceResolvesGlobalInMethod.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 | -------------------------------------------------------------------------------- /tests/Boilerplate/BoilerplateServiceWithConstructorClass.php: -------------------------------------------------------------------------------- 1 | boilerplateService; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Boilerplate/BoilerplateServiceWithConstructorPrimitive.php: -------------------------------------------------------------------------------- 1 | param; 17 | } 18 | 19 | public function getNextParam(): string 20 | { 21 | return $this->nextParam; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Boilerplate/BoilerplateServiceWithVariadicConstructor.php: -------------------------------------------------------------------------------- 1 | boilerplates = $boilerplates; 15 | } 16 | 17 | public function test(): mixed 18 | { 19 | return $this->boilerplates; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Boilerplate/BoilerplateServiceWithWrongContext.php: -------------------------------------------------------------------------------- 1 | boilerplateService; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Boilerplate/BoilerplateWithBootedCallProxyWrongParams.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 | -------------------------------------------------------------------------------- /tests/Boilerplate/Builder/Best/BestBuilder.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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('testing'); 27 | } 28 | } 29 | --------------------------------------------------------------------------------