Skip to content

Commit eca3506

Browse files
FIX exclusion exceptions (#26)
1 parent 69a1800 commit eca3506

File tree

13 files changed

+614
-144
lines changed

13 files changed

+614
-144
lines changed

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2023 MAIZE SRL <[email protected]>
3+
Copyright (c) 2024 MAIZE SRL <[email protected]>
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
[![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)
1414
[![Total Downloads](https://img.shields.io/packagist/dt/maize-tech/laravel-excludable.svg?style=flat-square)](https://packagist.org/packages/maize-tech/laravel-excludable)
1515

16-
Easily exclude model entities from eloquent queries.
16+
Easily exclude model entities from eloquent queries.
1717

1818
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.
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.
2020

2121
An example usage could be an application with a multi tenant scenario and a set of global entities.
2222
While those entities should be accessible by all tenants, some of them might want to hide a subset of those entities for their users.
@@ -57,6 +57,17 @@ return [
5757
*/
5858

5959
'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,
6071
];
6172

6273
```
@@ -143,14 +154,24 @@ Article::excludeAllModels([1,2,3]); // passing the model keys
143154
Article::query()->count(); // returns 3
144155
```
145156

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+
146167
### Include all model entities
147168

148-
To re-include all entities of a specific model you can use the `includeAllModels` method:
169+
To re-include all entities of a specific model you can use the `includeAllModels` method:
149170

150171
``` php
151172
use App\Models\Article;
152173

153-
Article::includeAllModels()
174+
Article::includeAllModels();
154175

155176
Article::query()->count(); // returns 0
156177
```
@@ -180,7 +201,7 @@ The package automatically throws two separate events when excluding an entity:
180201
- `excluding` which is thrown before the entity is actually excluded.
181202
This could be useful, for example, with an observer which listens to this event and does some sort of 'validation' to the related entity.
182203
If the given validation does not succeed, you can just return `false`, and the entity will not be excluded;
183-
- `excluded` which is thrown right after the entity has been marked as excluded.
204+
- `excluded` which is thrown right after the entity has been marked as excluded.
184205

185206
## Testing
186207

config/excludable.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,15 @@
1212
*/
1313

1414
'exclusion_model' => 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,
1526
];

src/Excludable.php

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ trait Excludable
2525
public static function bootExcludable(): void
2626
{
2727
static::addGlobalScope(new ExclusionScope);
28+
29+
static::deleting(
30+
fn (Model $model) => Config::getExclusionModel()->where([
31+
'excludable_type' => $model->getMorphClass(),
32+
'excludable_id' => $model->getKey(),
33+
])->delete()
34+
);
2835
}
2936

3037
public function exclusions(): MorphManyWildcard
@@ -49,34 +56,73 @@ public function excluded(): bool
4956
return $this->exclusions()->count() === 1;
5057
}
5158

52-
public function addToExclusion(): bool
59+
public static function hasExclusionWildcard(): bool
5360
{
54-
if ($this->excluded()) {
55-
return true;
56-
}
57-
58-
if ($this->fireModelEvent('excluding') === false) {
59-
return false;
60-
}
61-
62-
$exclusion = $this->exclusion()->firstOrCreate([
63-
'type' => Exclusion::TYPE_EXCLUDE,
64-
'excludable_type' => $this->getMorphClass(),
65-
'excludable_id' => $this->getKey(),
66-
]);
61+
return Config::getExclusionModel()
62+
->query()
63+
->where('excludable_type', app(static::class)->getMorphClass())
64+
->where('excludable_id', '*')
65+
->exists();
66+
}
6767

68-
if ($exclusion->wasRecentlyCreated) {
69-
$this->fireModelEvent('excluded', false);
70-
}
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+
}
7198

72-
return true;
99+
return true;
100+
});
73101
}
74102

75103
public function removeFromExclusion(): bool
76104
{
77-
$this->exclusion()->delete();
105+
return DB::transaction(function () {
106+
if (! $this->excluded()) {
107+
return false;
108+
}
109+
110+
$this->exclusion()
111+
->where('excludable_id', '!=', '*')
112+
->delete();
78113

79-
return true;
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+
});
80126
}
81127

82128
public static function excludeAllModels(array|Model $exceptions = []): void

src/Queries/HasExclusionQuery.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Maize\Excludable\Queries;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
use Illuminate\Database\Query\Builder as QueryBuilder;
7+
use Maize\Excludable\Models\Exclusion;
8+
use Maize\Excludable\Support\Config;
9+
10+
class HasExclusionQuery
11+
{
12+
public function __invoke(Builder $builder, $not = false): Builder
13+
{
14+
$model = $builder->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+
}

src/Scopes/ExclusionScope.php

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
use Illuminate\Database\Eloquent\Builder;
66
use Illuminate\Database\Eloquent\Model;
77
use Illuminate\Database\Eloquent\Scope;
8-
use Illuminate\Database\Query\Builder as QueryBuilder;
9-
use Maize\Excludable\Models\Exclusion;
108
use Maize\Excludable\Support\Config;
119

1210
class ExclusionScope implements Scope
@@ -28,26 +26,8 @@ public function extend(Builder $builder): void
2826
protected function addWhereHasExclusion(Builder $builder): void
2927
{
3028
$builder->macro('whereHasExclusion', function (Builder $builder, $not = false) {
31-
$model = $builder->getModel();
32-
$exclusionModel = Config::getExclusionModel();
33-
3429
return $builder->where(
35-
fn (Builder $query) => $query
36-
->whereHas(
37-
relation: 'exclusion',
38-
operator: $not ? '<' : '>='
39-
)
40-
->whereIn(
41-
column: $model->getQualifiedKeyName(),
42-
values: fn (QueryBuilder $query) => $query
43-
->select($exclusionModel->qualifyColumn('excludable_id'))
44-
->from($exclusionModel->getTable())
45-
->where($exclusionModel->qualifyColumn('type'), Exclusion::TYPE_INCLUDE)
46-
->where($exclusionModel->qualifyColumn('excludable_type'), $model->getMorphClass())
47-
->whereColumn($exclusionModel->qualifyColumn('excludable_id'), $model->getQualifiedKeyName()),
48-
boolean: $not ? 'or' : 'and',
49-
not: ! $not
50-
)
30+
fn (Builder $query) => Config::getHasExclusionQuery()($query, $not)
5131
);
5232
});
5333
}

src/Support/Config.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Maize\Excludable\Support;
44

55
use Maize\Excludable\Models\Exclusion;
6+
use Maize\Excludable\Queries\HasExclusionQuery;
67

78
class Config
89
{
@@ -13,4 +14,12 @@ public static function getExclusionModel(): Exclusion
1314

1415
return new $model;
1516
}
17+
18+
public static function getHasExclusionQuery(): HasExclusionQuery
19+
{
20+
/** @var string $query */
21+
$query = config('excludable.has_exclusion_query') ?? HasExclusionQuery::class;
22+
23+
return new $query();
24+
}
1625
}

src/Support/HasWildcardRelationships.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
trait HasWildcardRelationships
99
{
10-
public function morphOneWildcard(string|Model $related, string $name, string $type = null, string $id = null, string $localKey = null): MorphOneWildcard
10+
public function morphOneWildcard(string|Model $related, string $name, ?string $type = null, ?string $id = null, ?string $localKey = null): MorphOneWildcard
1111
{
1212
$instance = $this->newRelatedInstance($related);
1313

@@ -25,7 +25,7 @@ protected function newMorphOneWildcard(Builder $query, Model $parent, string $ty
2525
return new MorphOneWildcard($query, $parent, $type, $id, $localKey);
2626
}
2727

28-
public function morphManyWildcard(string|Model $related, string $name, string $type = null, string $id = null, string $localKey = null): MorphManyWildcard
28+
public function morphManyWildcard(string|Model $related, string $name, ?string $type = null, ?string $id = null, ?string $localKey = null): MorphManyWildcard
2929
{
3030
$instance = $this->newRelatedInstance($related);
3131

tests/Events/ArticleExcludedEvent.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66

77
class ArticleExcludedEvent
88
{
9-
public Article $article;
10-
11-
public function __construct(Article $article)
12-
{
13-
$this->article = $article;
9+
public function __construct(
10+
public Article $article
11+
) {
1412
}
1513
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Maize\Excludable\Tests\Events;
4+
5+
use Maize\Excludable\Tests\Models\Article;
6+
7+
class ArticleExcludingEvent
8+
{
9+
public function __construct(
10+
public Article $article
11+
) {
12+
}
13+
}

0 commit comments

Comments
 (0)