Completed
Push — master ( a9c3eb...3387e9 )
by Abdelrahman
02:59 queued 01:56
created

Tenantable::scopeWithoutAnyTenants()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Tenants\Traits;
6
7
use Illuminate\Support\Arr;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Builder;
10
use Illuminate\Database\Eloquent\Collection;
11
use Illuminate\Support\Collection as BaseCollection;
12
use Illuminate\Database\Eloquent\Relations\MorphToMany;
13
use Illuminate\Database\Eloquent\ModelNotFoundException;
14
use Rinvex\Tenants\Exceptions\ModelNotFoundForTenantException;
15
16
trait Tenantable
17
{
18
    /**
19
     * Register a saved model event with the dispatcher.
20
     *
21
     * @param \Closure|string $callback
22
     *
23
     * @return void
24
     */
25
    abstract public static function saved($callback);
26
27
    /**
28
     * Register a deleted model event with the dispatcher.
29
     *
30
     * @param \Closure|string $callback
31
     *
32
     * @return void
33
     */
34
    abstract public static function deleted($callback);
35
36
    /**
37
     * Define a polymorphic many-to-many relationship.
38
     *
39
     * @param string $related
40
     * @param string $name
41
     * @param string $table
0 ignored issues
show
Documentation introduced by
Should the type for parameter $table not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
42
     * @param string $foreignPivotKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $foreignPivotKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
43
     * @param string $relatedPivotKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $relatedPivotKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
44
     * @param string $parentKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $parentKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
45
     * @param string $relatedKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $relatedKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
46
     * @param bool   $inverse
47
     *
48
     * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
49
     */
50
    abstract public function morphToMany(
51
        $related,
52
        $name,
53
        $table = null,
54
        $foreignPivotKey = null,
55
        $relatedPivotKey = null,
56
        $parentKey = null,
57
        $relatedKey = null,
58
        $inverse = false
59
    );
60
61
    /**
62
     * Get all attached tenants to the model.
63
     *
64
     * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
65
     */
66
    public function tenants(): MorphToMany
67
    {
68
        return $this->morphToMany(config('rinvex.tenants.models.tenant'), 'tenantable', config('rinvex.tenants.tables.tenantables'), 'tenantable_id', 'tenant_id')
69
                    ->withTimestamps();
70
    }
71
72
    /**
73
     * Attach the given tenant(s) to the model.
74
     *
75
     * @param mixed $tenants
76
     *
77
     * @return void
78
     */
79
    public function setTenantsAttribute($tenants): void
80
    {
81
        static::saved(function (self $model) use ($tenants) {
82
            $model->syncTenants($tenants);
83
        });
84
    }
85
86
    /**
87
     * Boot the tenantable trait for the model.
88
     *
89
     * @return void
90
     */
91
    public static function bootTenantable()
92
    {
93
        if ($tenant = config('rinvex.tenants.active')) {
94
            static::addGlobalScope('tenantable', function (Builder $builder) use ($tenant) {
95
                $builder->whereHas('tenants', function (Builder $builder) use ($tenant) {
96
                    $key = $tenant instanceof Model ? $tenant->getKeyName() : (is_int($tenant) ? 'id' : 'slug');
97
                    $value = $tenant instanceof Model ? $tenant->{$key} : $tenant;
98
                    $builder->where($key, $value);
99
                });
100
            });
101
102
            static::saved(function (self $model) use ($tenant) {
103
                $model->attachTenants($tenant);
104
            });
105
        }
106
107
        static::deleted(function (self $model) {
108
            $model->tenants()->detach();
109
        });
110
    }
111
112
    /**
113
     * Returns a new query builder without any of the tenant scopes applied.
114
     *
115
     * @return \Illuminate\Database\Eloquent\Builder
116
     */
117
    public static function forAllTenants()
118
    {
119
        return (new static())->newQuery()->withoutGlobalScopes(['tenantable']);
0 ignored issues
show
Bug introduced by
It seems like newQuery() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
120
    }
121
122
    /**
123
     * Override the default findOrFail method so that we can re-throw
124
     * a more useful exception. Otherwise it can be very confusing
125
     * why queries don't work because of tenant scoping issues.
126
     *
127
     * @param mixed $id
128
     * @param array $columns
129
     *
130
     * @throws \Rinvex\Tenants\Exceptions\ModelNotFoundForTenantException
131
     *
132
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
133
     */
134
    public static function findOrFail($id, $columns = ['*'])
135
    {
136
        try {
137
            return static::query()->findOrFail($id, $columns);
138
        } catch (ModelNotFoundException $exception) {
139
            // If it DOES exist, just not for this tenant, throw a nicer exception
140
            if (! is_null(static::forAllTenants()->find($id, $columns))) {
141
                throw (new ModelNotFoundForTenantException())->setModel(static::class, [$id]);
142
            }
143
144
            throw $exception;
145
        }
146
    }
147
148
    /**
149
     * Scope query with all the given tenants.
150
     *
151
     * @param \Illuminate\Database\Eloquent\Builder $builder
152
     * @param mixed                                 $tenants
153
     *
154
     * @return \Illuminate\Database\Eloquent\Builder
155
     */
156
    public function scopeWithAllTenants(Builder $builder, $tenants): Builder
157
    {
158
        $tenants = $this->prepareTenantIds($tenants);
159
160
        collect($tenants)->each(function ($tenant) use ($builder) {
161
            $builder->whereHas('tenants', function (Builder $builder) use ($tenant) {
162
                return $builder->where('id', $tenant);
163
            });
164
        });
165
166
        return $builder;
167
    }
168
169
    /**
170
     * Scope query with any of the given tenants.
171
     *
172
     * @param \Illuminate\Database\Eloquent\Builder $builder
173
     * @param mixed                                 $tenants
174
     *
175
     * @return \Illuminate\Database\Eloquent\Builder
176
     */
177
    public function scopeWithAnyTenants(Builder $builder, $tenants): Builder
178
    {
179
        $tenants = $this->prepareTenantIds($tenants);
180
181
        return $builder->whereHas('tenants', function (Builder $builder) use ($tenants) {
182
            $builder->whereIn('id', $tenants);
183
        });
184
    }
185
186
    /**
187
     * Scope query with any of the given tenants.
188
     *
189
     * @param \Illuminate\Database\Eloquent\Builder $builder
190
     * @param mixed                                 $tenants
191
     *
192
     * @return \Illuminate\Database\Eloquent\Builder
193
     */
194
    public function scopeWithTenants(Builder $builder, $tenants): Builder
195
    {
196
        return static::scopeWithAnyTenants($builder, $tenants);
197
    }
198
199
    /**
200
     * Scope query without any of the given tenants.
201
     *
202
     * @param \Illuminate\Database\Eloquent\Builder $builder
203
     * @param mixed                                 $tenants
204
     *
205
     * @return \Illuminate\Database\Eloquent\Builder
206
     */
207
    public function scopeWithoutTenants(Builder $builder, $tenants): Builder
208
    {
209
        $tenants = $this->prepareTenantIds($tenants);
210
211
        return $builder->whereDoesntHave('tenants', function (Builder $builder) use ($tenants) {
212
            $builder->whereIn('id', $tenants);
213
        });
214
    }
215
216
    /**
217
     * Scope query without any tenants.
218
     *
219
     * @param \Illuminate\Database\Eloquent\Builder $builder
220
     *
221
     * @return \Illuminate\Database\Eloquent\Builder
222
     */
223
    public function scopeWithoutAnyTenants(Builder $builder): Builder
224
    {
225
        return $builder->doesntHave('tenants');
226
    }
227
228
    /**
229
     * Determine if the model has any of the given tenants.
230
     *
231
     * @param mixed $tenants
232
     *
233
     * @return bool
234
     */
235
    public function hasTenants($tenants): bool
236
    {
237
        $tenants = $this->prepareTenantIds($tenants);
238
239
        return ! $this->tenants->pluck('id')->intersect($tenants)->isEmpty();
0 ignored issues
show
Bug introduced by
The property tenants does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
240
    }
241
242
    /**
243
     * Determine if the model has any the given tenants.
244
     *
245
     * @param mixed $tenants
246
     *
247
     * @return bool
248
     */
249
    public function hasAnyTenants($tenants): bool
250
    {
251
        return static::hasTenants($tenants);
252
    }
253
254
    /**
255
     * Determine if the model has all of the given tenants.
256
     *
257
     * @param mixed $tenants
258
     *
259
     * @return bool
260
     */
261
    public function hasAllTenants($tenants): bool
262
    {
263
        $tenants = $this->prepareTenantIds($tenants);
264
265
        return collect($tenants)->diff($this->tenants->pluck('id'))->isEmpty();
266
    }
267
268
    /**
269
     * Sync model tenants.
270
     *
271
     * @param mixed $tenants
272
     * @param bool  $detaching
273
     *
274
     * @return $this
275
     */
276
    public function syncTenants($tenants, bool $detaching = true)
277
    {
278
        // Find tenants
279
        $tenants = $this->prepareTenantIds($tenants);
280
281
        // Sync model tenants
282
        $this->tenants()->sync($tenants, $detaching);
283
284
        return $this;
285
    }
286
287
    /**
288
     * Attach model tenants.
289
     *
290
     * @param mixed $tenants
291
     *
292
     * @return $this
293
     */
294
    public function attachTenants($tenants)
295
    {
296
        return $this->syncTenants($tenants, false);
297
    }
298
299
    /**
300
     * Detach model tenants.
301
     *
302
     * @param mixed $tenants
303
     *
304
     * @return $this
305
     */
306
    public function detachTenants($tenants = null)
307
    {
308
        $tenants = ! is_null($tenants) ? $this->prepareTenantIds($tenants) : null;
309
310
        // Sync model tenants
311
        $this->tenants()->detach($tenants);
312
313
        return $this;
314
    }
315
316
    /**
317
     * Prepare tenant IDs.
318
     *
319
     * @param mixed $tenants
320
     *
321
     * @return array
322
     */
323
    protected function prepareTenantIds($tenants): array
324
    {
325
        // Convert collection to plain array
326
        if ($tenants instanceof BaseCollection && is_string($tenants->first())) {
327
            $tenants = $tenants->toArray();
328
        }
329
330
        // Find tenants by their ids
331
        if (is_numeric($tenants) || (is_array($tenants) && is_numeric(Arr::first($tenants)))) {
0 ignored issues
show
Documentation introduced by
$tenants is of type array, but the function expects a object<Illuminate\Support\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
332
            return array_map('intval', (array) $tenants);
333
        }
334
335
        // Find tenants by their slugs
336
        if (is_string($tenants) || (is_array($tenants) && is_string(Arr::first($tenants)))) {
0 ignored issues
show
Documentation introduced by
$tenants is of type array, but the function expects a object<Illuminate\Support\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
337
            $tenants = app('rinvex.tenants.tenant')->whereIn('slug', $tenants)->get()->pluck('id');
338
        }
339
340
        if ($tenants instanceof Model) {
341
            return [$tenants->getKey()];
342
        }
343
344
        if ($tenants instanceof Collection) {
345
            return $tenants->modelKeys();
346
        }
347
348
        if ($tenants instanceof BaseCollection) {
349
            return $tenants->toArray();
350
        }
351
352
        return (array) $tenants;
353
    }
354
}
355