├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── art ├── socialcard-dark.png └── socialcard-light.png ├── composer.json ├── config └── excludable.php ├── database ├── factories │ └── ArticleFactory.php └── migrations │ ├── create_articles_table.php.stub │ └── create_exclusions_table.php.stub └── src ├── Excludable.php ├── ExcludableServiceProvider.php ├── Models └── Exclusion.php ├── Queries └── HasExclusionQuery.php ├── Scopes └── ExclusionScope.php └── Support ├── Config.php ├── HasWildcardRelationships.php ├── MorphManyWildcard.php └── MorphOneWildcard.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-excludable` will be documented in this file. 4 | 5 | ## 4.1.0 - 2024-03-27 6 | 7 | ### What's Changed 8 | 9 | * Laravel 11.x Compatibility by @enricodelazzari in https://github.com/maize-tech/laravel-excludable/pull/30 10 | 11 | ## 4.0.0 - 2024-01-10 12 | 13 | ### What's Changed 14 | 15 | * Added exclusion wildcards 16 | * Added exclusion exceptions 17 | 18 | ## 3.1.0 - 2023-02-13 19 | 20 | ### What's Changed 21 | 22 | - Add support to Laravel 10.x 23 | 24 | ## 3.0.0 - 2022-02-16 25 | 26 | - add laravel 9 support 27 | - drop support to older laravel versions 28 | 29 | ## 2.0.0 - 2021-10-12 30 | 31 | - UPDATE package namespace 32 | 33 | ## 1.0.0 - 2021-05-05 34 | 35 | - first release 🚀 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 MAIZE SRL 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 |

2 | 3 | 4 | 5 | Social Card of Laravel Excludable 6 | 7 |

8 | 9 | # Laravel Excludable 10 | 11 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/maize-tech/laravel-excludable.svg?style=flat-square)](https://packagist.org/packages/maize-tech/laravel-excludable) 12 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/maize-tech/laravel-excludable/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/maize-tech/laravel-excludable/actions?query=workflow%3Arun-tests+branch%3Amain) 13 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/maize-tech/laravel-excludable/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/maize-tech/laravel-excludable/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/maize-tech/laravel-excludable.svg?style=flat-square)](https://packagist.org/packages/maize-tech/laravel-excludable) 15 | 16 | Easily exclude model entities from eloquent queries. 17 | 18 | This package allows you to define a subset of model entities that should be excluded from eloquent queries. 19 | You will be able to override the default `Exclusion` model and its associated migration, so you can eventually restrict the exclusion context by defining the entity that should effectively exclude the subset. 20 | 21 | An example usage could be an application with a multi tenant scenario and a set of global entities. 22 | While those entities should be accessible by all tenants, some of them might want to hide a subset of those entities for their users. 23 | You can find an example in the [Usage](#usage) section. 24 | 25 | ## Installation 26 | 27 | You can install the package via composer: 28 | 29 | ```bash 30 | composer require maize-tech/laravel-excludable 31 | ``` 32 | 33 | You can publish and run the migrations with: 34 | 35 | ```bash 36 | php artisan vendor:publish --provider="Maize\Excludable\ExcludableServiceProvider" --tag="excludable-migrations" 37 | php artisan migrate 38 | ``` 39 | 40 | You can publish the config file with: 41 | ```bash 42 | php artisan vendor:publish --provider="Maize\Excludable\ExcludableServiceProvider" --tag="excludable-config" 43 | ``` 44 | 45 | This is the content of the published config file: 46 | 47 | ```php 48 | return [ 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Exclusion model 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Here you may specify the fully qualified class name of the exclusion model. 56 | | 57 | */ 58 | 59 | 'exclusion_model' => Maize\Excludable\Models\Exclusion::class, 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Has exclusion query 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may specify the fully qualified class name of the exclusion query. 67 | | 68 | */ 69 | 70 | 'has_exclusion_query' => Maize\Excludable\Queries\HasExclusionQuery::class, 71 | ]; 72 | 73 | ``` 74 | 75 | ## Usage 76 | 77 | ### Basic 78 | 79 | To use the package, add the `Maize\Excludable\Excludable` trait to all models you want to make excludable. 80 | 81 | Here's an example model including the `Excludable` trait: 82 | 83 | ``` php 84 | findOrFail(1) 107 | 108 | $article->addToExclusion(); 109 | 110 | $article->excluded(); // returns true 111 | ``` 112 | 113 | That's all! 114 | 115 | The package will add the given entity to the exclusions table, so all article related queries will exclude it. 116 | 117 | ``` php 118 | use App\Models\Article; 119 | 120 | Article::findOrFail(1); // throws Symfony\Component\HttpKernel\Exception\NotFoundHttpException 121 | ``` 122 | 123 | ### Exclude all model entities 124 | 125 | To exclude all entities of a specific model you can use the `excludeAllModels` method: 126 | 127 | ``` php 128 | use App\Models\Article; 129 | 130 | Article::excludeAllModels(); 131 | 132 | Article::query()->count(); // returns 0 133 | ``` 134 | 135 | The given method will create an Exclusion entity with a wildcard, which means all newly created entities will be excluded too. 136 | 137 | You can also provide a subset of entities which should not be excluded: 138 | 139 | ``` php 140 | use App\Models\Article; 141 | 142 | $article = Article::query()->find(1); 143 | 144 | Article::excludeAllModels($article); // passing a model entity 145 | 146 | Article::query()->count(); // returns 1 147 | ``` 148 | 149 | ``` php 150 | use App\Models\Article; 151 | 152 | Article::excludeAllModels([1,2,3]); // passing the model keys 153 | 154 | Article::query()->count(); // returns 3 155 | ``` 156 | 157 | To check whether a specific model has a wildcard or not you can use the `hasExclusionWildcard` method: 158 | 159 | ``` php 160 | use App\Models\Article; 161 | 162 | Article::excludeAllModels(); 163 | 164 | $hasWildcard = Article::hasExclusionWildcard(); // returns true 165 | ``` 166 | 167 | ### Include all model entities 168 | 169 | To re-include all entities of a specific model you can use the `includeAllModels` method: 170 | 171 | ``` php 172 | use App\Models\Article; 173 | 174 | Article::includeAllModels(); 175 | 176 | Article::query()->count(); // returns 0 177 | ``` 178 | 179 | The given method will delete all Exclusion entities related to the Article model. 180 | 181 | ### Include excluded entities 182 | 183 | ``` php 184 | use App\Models\Article; 185 | 186 | Article::withExcluded()->get(); // queries all models, including those marked as excluded 187 | ``` 188 | 189 | ### Only show excluded entities 190 | 191 | ``` php 192 | use App\Models\Article; 193 | 194 | Article::onlyExcluded()->get(); // queries only excluded entities 195 | ``` 196 | 197 | ### Event handling 198 | 199 | The package automatically throws two separate events when excluding an entity: 200 | 201 | - `excluding` which is thrown before the entity is actually excluded. 202 | This could be useful, for example, with an observer which listens to this event and does some sort of 'validation' to the related entity. 203 | If the given validation does not succeed, you can just return `false`, and the entity will not be excluded; 204 | - `excluded` which is thrown right after the entity has been marked as excluded. 205 | 206 | ## Testing 207 | 208 | ```bash 209 | composer test 210 | ``` 211 | 212 | ## Changelog 213 | 214 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 215 | 216 | ## Contributing 217 | 218 | Please see [CONTRIBUTING](https://github.com/maize-tech/.github/blob/main/CONTRIBUTING.md) for details. 219 | 220 | ## Security Vulnerabilities 221 | 222 | Please review [our security policy](https://github.com/maize-tech/.github/security/policy) on how to report security vulnerabilities. 223 | 224 | ## Credits 225 | 226 | - [Enrico De Lazzari](https://github.com/enricodelazzari) 227 | - [All Contributors](../../contributors) 228 | 229 | ## License 230 | 231 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 232 | -------------------------------------------------------------------------------- /art/socialcard-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maize-tech/laravel-excludable/d8f054c417cf158a1be3588df5e499a4b16db96d/art/socialcard-dark.png -------------------------------------------------------------------------------- /art/socialcard-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maize-tech/laravel-excludable/d8f054c417cf158a1be3588df5e499a4b16db96d/art/socialcard-light.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maize-tech/laravel-excludable", 3 | "description": "Laravel Excludable", 4 | "keywords": [ 5 | "maize-tech", 6 | "laravel", 7 | "excludable" 8 | ], 9 | "homepage": "https://github.com/maize-tech/laravel-excludable", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Enrico De Lazzari", 14 | "email": "enrico.delazzari@maize.io", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "illuminate/database": "^9.0|^10.0|^11.0", 21 | "illuminate/support": "^9.0|^10.0|^11.0", 22 | "spatie/laravel-package-tools": "^1.14.1" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.0", 26 | "orchestra/testbench": "^7.0|^8.0|^9.0", 27 | "phpunit/phpunit": "^9.5|^10.5", 28 | "vimeo/psalm": "^4.20|^5.22" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Maize\\Excludable\\": "src", 33 | "Maize\\Excludable\\Database\\Factories\\": "database/factories" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Maize\\Excludable\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "format": "vendor/bin/pint", 43 | "psalm": "vendor/bin/psalm", 44 | "test": "vendor/bin/phpunit --colors=always", 45 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Maize\\Excludable\\ExcludableServiceProvider" 54 | ] 55 | } 56 | }, 57 | "minimum-stability": "dev", 58 | "prefer-stable": true 59 | } 60 | -------------------------------------------------------------------------------- /config/excludable.php: -------------------------------------------------------------------------------- 1 | Maize\Excludable\Models\Exclusion::class, 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Has exclusion query 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Here you may specify the fully qualified class name of the exclusion query. 22 | | 23 | */ 24 | 25 | 'has_exclusion_query' => Maize\Excludable\Queries\HasExclusionQuery::class, 26 | ]; 27 | -------------------------------------------------------------------------------- /database/factories/ArticleFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->timestamps(); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /database/migrations/create_exclusions_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('type'); 14 | $table->string('excludable_type'); 15 | $table->string('excludable_id'); 16 | $table->index(["excludable_type", "excludable_id"]); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('exclusions'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Excludable.php: -------------------------------------------------------------------------------- 1 | Config::getExclusionModel()->where([ 31 | 'excludable_type' => $model->getMorphClass(), 32 | 'excludable_id' => $model->getKey(), 33 | ])->delete() 34 | ); 35 | } 36 | 37 | public function exclusions(): MorphManyWildcard 38 | { 39 | return $this 40 | ->morphManyWildcard( 41 | related: Config::getExclusionModel(), 42 | name: 'excludable' 43 | ); 44 | } 45 | 46 | public function exclusion(): MorphOneWildcard 47 | { 48 | return $this 49 | ->exclusions() 50 | ->where('type', Exclusion::TYPE_EXCLUDE) 51 | ->one(); 52 | } 53 | 54 | public function excluded(): bool 55 | { 56 | return $this->exclusions()->count() === 1; 57 | } 58 | 59 | public static function hasExclusionWildcard(): bool 60 | { 61 | return Config::getExclusionModel() 62 | ->query() 63 | ->where('excludable_type', app(static::class)->getMorphClass()) 64 | ->where('excludable_id', '*') 65 | ->exists(); 66 | } 67 | 68 | public function addToExclusion(): bool 69 | { 70 | return DB::transaction(function () { 71 | if ($this->fireModelEvent('excluding') === false) { 72 | return false; 73 | } 74 | 75 | $wasRecentlyDeleted = (bool) $this->exclusions()->where([ 76 | 'type' => Exclusion::TYPE_INCLUDE, 77 | 'excludable_type' => $this->getMorphClass(), 78 | 'excludable_id' => $this->getKey(), 79 | ])->delete(); 80 | 81 | if ($wasRecentlyDeleted) { 82 | $this->fireModelEvent('excluded', false); 83 | } 84 | 85 | if ($this->excluded()) { 86 | return true; 87 | } 88 | 89 | $exclusion = $this->exclusion()->firstOrCreate([ 90 | 'type' => Exclusion::TYPE_EXCLUDE, 91 | 'excludable_type' => $this->getMorphClass(), 92 | 'excludable_id' => $this->getKey(), 93 | ]); 94 | 95 | if ($exclusion->wasRecentlyCreated) { 96 | $this->fireModelEvent('excluded', false); 97 | } 98 | 99 | return true; 100 | }); 101 | } 102 | 103 | public function removeFromExclusion(): bool 104 | { 105 | return DB::transaction(function () { 106 | if (! $this->excluded()) { 107 | return false; 108 | } 109 | 110 | $this->exclusion() 111 | ->where('excludable_id', '!=', '*') 112 | ->delete(); 113 | 114 | if (! static::hasExclusionWildcard()) { 115 | return true; 116 | } 117 | 118 | Config::getExclusionModel()->create([ 119 | 'type' => Exclusion::TYPE_INCLUDE, 120 | 'excludable_type' => $this->getMorphClass(), 121 | 'excludable_id' => $this->getKey(), 122 | ]); 123 | 124 | return true; 125 | }); 126 | } 127 | 128 | public static function excludeAllModels(array|Model $exceptions = []): void 129 | { 130 | $exceptions = collect($exceptions) 131 | ->map(fn (mixed $exception) => match (true) { 132 | is_a($exception, Model::class) => $exception->getKey(), 133 | default => $exception, 134 | }); 135 | 136 | DB::transaction(function () use ($exceptions) { 137 | $exclusionModel = Config::getExclusionModel(); 138 | 139 | $exclusionModel 140 | ->query() 141 | ->where('excludable_type', app(static::class)->getMorphClass()) 142 | ->delete(); 143 | 144 | $exclusionModel 145 | ->query() 146 | ->create([ 147 | 'type' => Exclusion::TYPE_EXCLUDE, 148 | 'excludable_type' => app(static::class)->getMorphClass(), 149 | 'excludable_id' => '*', 150 | ]); 151 | 152 | $exceptions->each( 153 | fn (mixed $exception) => $exclusionModel->query()->create([ 154 | 'type' => Exclusion::TYPE_INCLUDE, 155 | 'excludable_type' => app(static::class)->getMorphClass(), 156 | 'excludable_id' => $exception, 157 | ]) 158 | ); 159 | }); 160 | } 161 | 162 | public static function includeAllModels(): void 163 | { 164 | Config::getExclusionModel() 165 | ->query() 166 | ->where('excludable_type', app(static::class)->getMorphClass()) 167 | ->delete(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/ExcludableServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-excludable') 14 | ->hasConfigFile() 15 | ->hasMigration('create_exclusions_table'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/Exclusion.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $fillable = [ 22 | 'type', 23 | 'excludable_type', 24 | 'excludable_id', 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /src/Queries/HasExclusionQuery.php: -------------------------------------------------------------------------------- 1 | getModel(); 15 | $exclusionModel = Config::getExclusionModel(); 16 | 17 | return $builder 18 | ->whereHas( 19 | relation: 'exclusion', 20 | operator: $not ? '<' : '>=' 21 | ) 22 | ->whereIn( 23 | column: $model->getQualifiedKeyName(), 24 | values: fn (QueryBuilder $query) => $query 25 | ->select($exclusionModel->qualifyColumn('excludable_id')) 26 | ->from($exclusionModel->getTable()) 27 | ->where($exclusionModel->qualifyColumn('type'), Exclusion::TYPE_INCLUDE) 28 | ->where($exclusionModel->qualifyColumn('excludable_type'), $model->getMorphClass()) 29 | ->whereColumn($exclusionModel->qualifyColumn('excludable_id'), $model->getQualifiedKeyName()), 30 | boolean: $not ? 'or' : 'and', 31 | not: ! $not 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Scopes/ExclusionScope.php: -------------------------------------------------------------------------------- 1 | whereDoesntHaveExclusion(); 17 | } 18 | 19 | public function extend(Builder $builder): void 20 | { 21 | foreach ($this->extensions as $extension) { 22 | $this->{"add{$extension}"}($builder); 23 | } 24 | } 25 | 26 | protected function addWhereHasExclusion(Builder $builder): void 27 | { 28 | $builder->macro('whereHasExclusion', function (Builder $builder, $not = false) { 29 | return $builder->where( 30 | fn (Builder $query) => Config::getHasExclusionQuery()($query, $not) 31 | ); 32 | }); 33 | } 34 | 35 | protected function addWhereDoesntHaveExclusion(Builder $builder): void 36 | { 37 | $builder->macro('whereDoesntHaveExclusion', function (Builder $builder) { 38 | return $builder->whereHasExclusion(not: true); 39 | }); 40 | } 41 | 42 | protected function addWithExcluded(Builder $builder): void 43 | { 44 | $builder->macro('withExcluded', function (Builder $builder, $withExcluded = true) { 45 | if (! $withExcluded) { 46 | return $builder->withoutExcluded(); 47 | } 48 | 49 | return $builder->withoutGlobalScope($this); 50 | }); 51 | } 52 | 53 | protected function addWithoutExcluded(Builder $builder): void 54 | { 55 | $builder->macro('withoutExcluded', function (Builder $builder) { 56 | return $builder 57 | ->withoutGlobalScope($this) 58 | ->whereDoesntHaveExclusion(); 59 | }); 60 | } 61 | 62 | protected function addOnlyExcluded(Builder $builder): void 63 | { 64 | $builder->macro('onlyExcluded', function (Builder $builder) { 65 | return $builder 66 | ->withoutGlobalScope($this) 67 | ->whereHasExclusion(); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Support/Config.php: -------------------------------------------------------------------------------- 1 | newRelatedInstance($related); 13 | 14 | [$type, $id] = $this->getMorphs($name, $type, $id); 15 | 16 | $table = $instance->getTable(); 17 | 18 | $localKey = $localKey ?: $this->getKeyName(); 19 | 20 | return $this->newMorphOneWildcard($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); 21 | } 22 | 23 | protected function newMorphOneWildcard(Builder $query, Model $parent, string $type, string $id, string $localKey): MorphOneWildcard 24 | { 25 | return new MorphOneWildcard($query, $parent, $type, $id, $localKey); 26 | } 27 | 28 | public function morphManyWildcard(string|Model $related, string $name, ?string $type = null, ?string $id = null, ?string $localKey = null): MorphManyWildcard 29 | { 30 | $instance = $this->newRelatedInstance($related); 31 | 32 | [$type, $id] = $this->getMorphs($name, $type, $id); 33 | 34 | $table = $instance->getTable(); 35 | 36 | $localKey = $localKey ?: $this->getKeyName(); 37 | 38 | return $this->newMorphManyWildcard($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); 39 | } 40 | 41 | protected function newMorphManyWildcard(Builder $query, Model $parent, string $type, string $id, string $localKey): MorphManyWildcard 42 | { 43 | return new MorphManyWildcard($query, $parent, $type, $id, $localKey); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/MorphManyWildcard.php: -------------------------------------------------------------------------------- 1 | add('*') 14 | ->toArray(); 15 | } 16 | 17 | public function one(): MorphOneWildcard 18 | { 19 | return MorphOneWildcard::noConstraints(fn () => new MorphOneWildcard( 20 | $this->getQuery(), 21 | $this->getParent(), 22 | $this->morphType, 23 | $this->foreignKey, 24 | $this->localKey 25 | )); 26 | } 27 | 28 | public function addConstraints(): void 29 | { 30 | if (static::$constraints) { 31 | $this 32 | ->getRelationQuery() 33 | ->where($this->morphType, $this->morphClass) 34 | ->whereNotNull($this->foreignKey) 35 | ->where( 36 | fn (Builder $query) => $query 37 | ->where($this->foreignKey, (string) $this->getParentKey()) 38 | ->orWhere($this->foreignKey, '*') 39 | ); 40 | } 41 | } 42 | 43 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']): Builder 44 | { 45 | if ($query->getQuery()->from == $parentQuery->getQuery()->from) { 46 | return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns); 47 | } 48 | 49 | return $query 50 | ->select($columns) 51 | ->where($query->qualifyColumn($this->getMorphType()), $this->morphClass) 52 | ->where( 53 | fn (Builder $query) => $query 54 | ->whereColumn($this->getExistenceCompareKey(), $this->getQualifiedParentKeyName()) 55 | ->orWhere($this->getExistenceCompareKey(), '*') 56 | ); 57 | } 58 | 59 | public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']): Builder 60 | { 61 | $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()); 62 | 63 | $query->getModel()->setTable($hash); 64 | 65 | return $query 66 | ->select($columns) 67 | ->where( 68 | fn (Builder $query) => $query 69 | ->whereColumn($this->getForeignKeyName(), $this->getQualifiedParentKeyName()) 70 | ->orWhere($this->getForeignKeyName(), '*') 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Support/MorphOneWildcard.php: -------------------------------------------------------------------------------- 1 | add('*') 15 | ->toArray(); 16 | } 17 | 18 | public function addOneOfManyJoinSubQueryConstraints(JoinClause $join): void 19 | { 20 | $join 21 | ->on($this->qualifySubSelectColumn($this->morphType), '=', $this->qualifyRelatedColumn($this->morphType)) 22 | ->where( 23 | fn (Builder $query) => $query 24 | ->whereColumn($this->qualifySubSelectColumn($this->foreignKey), $this->qualifyRelatedColumn($this->foreignKey)) 25 | ->orWhere($this->qualifySubSelectColumn($this->foreignKey), '*') 26 | ); 27 | } 28 | 29 | public function addConstraints(): void 30 | { 31 | if (static::$constraints) { 32 | $query = $this->getRelationQuery(); 33 | 34 | $query 35 | ->whereNotNull($this->foreignKey) 36 | ->where( 37 | fn (Builder $query) => $query 38 | ->where($this->foreignKey, (string) $this->getParentKey()) 39 | ->orWhere($this->foreignKey, '*') 40 | ); 41 | } 42 | } 43 | 44 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']): Builder 45 | { 46 | if ($query->getQuery()->from == $parentQuery->getQuery()->from) { 47 | return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns); 48 | } 49 | 50 | return $query 51 | ->select($columns) 52 | ->where($query->qualifyColumn($this->getMorphType()), $this->morphClass) 53 | ->where( 54 | fn (Builder $query) => $query 55 | ->whereColumn($this->getExistenceCompareKey(), $this->getQualifiedParentKeyName()) 56 | ->orWhere($this->getExistenceCompareKey(), '*') 57 | ); 58 | } 59 | 60 | public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']): Builder 61 | { 62 | $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()); 63 | 64 | $query->getModel()->setTable($hash); 65 | 66 | return $query 67 | ->select($columns) 68 | ->where( 69 | fn (Builder $query) => $query 70 | ->whereColumn($this->getForeignKeyName(), $this->getQualifiedParentKeyName()) 71 | ->orWhere($this->getForeignKeyName(), '*') 72 | ); 73 | } 74 | } 75 | --------------------------------------------------------------------------------