├── .coveralls.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── _laravel_ide_helper.php ├── composer.json ├── phpunit.xml ├── src ├── LocalClassScopeServiceProvider.php └── ScopedMacro.php └── tests ├── AgeScope.php ├── Person.php ├── Test.php └── _ide_helper_models.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: build/logs/clover.xml 2 | json_path: build/logs/coveralls-upload.json 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | php: [8.2, 8.3, 8.4] 12 | lib: 13 | - laravel: ^13.0.x-dev 14 | - laravel: ^12.0 15 | - laravel: ^11.0 16 | exclude: 17 | - php: 8.2 18 | lib: 19 | laravel: ^13.0.x-dev 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | coverage: xdebug 29 | 30 | - name: Adjust package versions 31 | run: | 32 | composer require "laravel/framework:${{ matrix.lib.laravel }}" --dev 33 | - run: mkdir -p build/logs 34 | - run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml 35 | 36 | - name: Upload Coverage 37 | uses: nick-invision/retry@v2 38 | env: 39 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | COVERALLS_PARALLEL: 'true' 41 | COVERALLS_FLAG_NAME: 'laravel:${{ matrix.lib.laravel }}' 42 | with: 43 | timeout_minutes: 1 44 | max_attempts: 3 45 | command: | 46 | composer global require php-coveralls/php-coveralls 47 | php-coveralls --coverage_clover=build/logs/clover.xml -v 48 | 49 | coverage-aggregation: 50 | needs: build 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Aggregate Coverage 54 | uses: coverallsapp/github-action@master 55 | with: 56 | github-token: ${{ secrets.GITHUB_TOKEN }} 57 | parallel-finished: true 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | .php_cs.cache 4 | /.phpunit.cache/ 5 | composer.lock 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mpyw 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Local Class Scope [![Build Status](https://github.com/mpyw/laravel-local-class-scope/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/mpyw/laravel-local-class-scope/actions) [![Coverage Status](https://coveralls.io/repos/github/mpyw/laravel-local-class-scope/badge.svg?branch=master)](https://coveralls.io/github/mpyw/laravel-local-class-scope?branch=master) 2 | 3 | A tiny macro that reuse a global scope class as a local scope. 4 | 5 | The idea is from: [[Proposal] Local query scopes as classes · Issue #636 · laravel/ideas](https://github.com/laravel/ideas/issues/636) 6 | 7 | ## Requirements 8 | 9 | - PHP: `^8.2` 10 | - Laravel: `^11.0 || ^12.0` 11 | 12 | > [!NOTE] 13 | > Older versions have outdated dependency requirements. If you cannot prepare the latest environment, please refer to past releases. 14 | 15 | ## Installing 16 | 17 | ```bash 18 | composer require mpyw/laravel-local-class-scope 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Simple Scope 24 | 25 | ```php 26 | class ActiveScope implements Scope 27 | { 28 | public function apply(Builder $query, Model $model): void 29 | { 30 | $query->where('active', true); 31 | } 32 | } 33 | ``` 34 | 35 | ```php 36 | User::scoped(ActiveScope::class)->get(); 37 | ``` 38 | 39 | ```php 40 | User::scoped(new ActiveScope())->get(); 41 | ``` 42 | 43 | ### Scope that takes arguments 44 | 45 | ```php 46 | class AgeScope implements Scope 47 | { 48 | protected $parameters; 49 | 50 | public function __construct(...$parameters) 51 | { 52 | $this->parameters = $parameters; 53 | } 54 | 55 | public function apply(Builder $query, Model $model): void 56 | { 57 | $query->where('age', ...$this->parameters); 58 | } 59 | } 60 | ``` 61 | 62 | ```php 63 | User::scoped(AgeScope::class, '>', 18)->get(); 64 | ``` 65 | 66 | ```php 67 | User::scoped(new AgeScope('>', 18))->get(); 68 | ``` 69 | 70 | ### Combination 71 | 72 | ```php 73 | User::scoped(ActiveScope::class)->scoped(AgeScope::class, '>', 18)->get(); 74 | ``` 75 | 76 | ### Re-define as a local method scope 77 | 78 | ```php 79 | class User extends Model 80 | { 81 | public function scopeActive(Builder $query): Builder 82 | { 83 | return $this->scoped(ActiveScope::class); 84 | } 85 | } 86 | ``` 87 | 88 | ### Share local method re-definition via trait 89 | 90 | ```php 91 | trait ScopesActive 92 | { 93 | public function scopeActive(Builder $query): Builder 94 | { 95 | return $this->scoped(ActiveScope::class); 96 | } 97 | } 98 | ``` 99 | 100 | ```php 101 | class User extends Model 102 | { 103 | use ScopesActive; 104 | } 105 | ``` 106 | 107 | ```php 108 | class Admin extends Model 109 | { 110 | use ScopesActive; 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /_laravel_ide_helper.php: -------------------------------------------------------------------------------- 1 | =9.0", 31 | "phpunit/phpunit": ">=11.0" 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Mpyw\\LaravelLocalClassScope\\LocalClassScopeServiceProvider" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | ./src 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/LocalClassScopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | query = $query; 27 | } 28 | 29 | /** 30 | * Apply Scope to Eloquent\Builder. 31 | * 32 | * @param \Illuminate\Database\Eloquent\Scope|string $scope 33 | * @param mixed ...$parameters 34 | * @return \Illuminate\Database\Eloquent\Builder 35 | */ 36 | public function __invoke($scope, ...$parameters): Builder 37 | { 38 | if (is_string($scope) && class_exists($scope)) { 39 | $scope = new $scope(...$parameters); 40 | } 41 | if (!$scope instanceof Scope) { 42 | throw new InvalidArgumentException('$scope must be an instance of Scope'); 43 | } 44 | 45 | $scope->apply($this->query, $this->query->getModel()); 46 | 47 | return $this->query; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/AgeScope.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 24 | } 25 | 26 | /** 27 | * Apply the scope to a given Eloquent query builder. 28 | * 29 | * @param Builder $builder 30 | * @param Model $model 31 | */ 32 | public function apply(Builder $builder, Model $model): void 33 | { 34 | $builder->where('age', ...$this->parameters); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Person.php: -------------------------------------------------------------------------------- 1 | scoped(AgeScope::class, '>=', 18); 25 | 26 | $this->assertSame('select * from "people" where "age" >= ?', $query->toSql()); 27 | $this->assertSame([18], $query->getBindings()); 28 | } 29 | 30 | public function testInstance(): void 31 | { 32 | $query = Person::query()->scoped(new AgeScope(18)); 33 | 34 | $this->assertSame('select * from "people" where "age" = ?', $query->toSql()); 35 | $this->assertSame([18], $query->getBindings()); 36 | } 37 | 38 | public function testCallFromModel(): void 39 | { 40 | $query = Person::scoped(AgeScope::class, '<', 18); 41 | 42 | $this->assertSame('select * from "people" where "age" < ?', $query->toSql()); 43 | $this->assertSame([18], $query->getBindings()); 44 | } 45 | 46 | public function testInvalidScope(): void 47 | { 48 | $this->expectException(InvalidArgumentException::class); 49 | $this->expectExceptionMessage('$scope must be an instance of Scope'); 50 | 51 | Person::query()->scoped(new Person()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/_ide_helper_models.php: -------------------------------------------------------------------------------- 1 |