Completed
Pull Request — master (#926)
by
unknown
01:38
created

HasPermissions::forgetCachedPermissions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Spatie\Permission\Traits;
4
5
use Spatie\Permission\Guard;
6
use Illuminate\Support\Collection;
7
use Illuminate\Database\Eloquent\Builder;
8
use Spatie\Permission\PermissionRegistrar;
9
use Spatie\Permission\Contracts\Permission;
10
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
11
use Illuminate\Database\Eloquent\Relations\MorphToMany;
12
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
13
14
trait HasPermissions
15
{
16
    private $permissionClass;
17
18 View Code Duplication
    public static function bootHasPermissions()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
19
    {
20
        static::deleting(function ($model) {
21
            if (method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting()) {
22
                return;
23
            }
24
25
            $model->permissions()->detach();
26
        });
27
    }
28
29
    public function getPermissionClass()
30
    {
31
        if (! isset($this->permissionClass)) {
32
            $this->permissionClass = app(PermissionRegistrar::class)->getPermissionClass();
33
        }
34
35
        return $this->permissionClass;
36
    }
37
38
    /**
39
     * A model may have multiple direct permissions.
40
     */
41
    public function permissions(): MorphToMany
42
    {
43
        return $this->morphToMany(
0 ignored issues
show
Bug introduced by
It seems like morphToMany() 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...
44
            config('permission.models.permission'),
45
            'model',
46
            config('permission.table_names.model_has_permissions'),
47
            config('permission.column_names.model_morph_key'),
48
            'permission_id'
49
        );
50
    }
51
52
    /**
53
     * Scope the model query to certain permissions only.
54
     *
55
     * @param \Illuminate\Database\Eloquent\Builder $query
56
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
57
     *
58
     * @return \Illuminate\Database\Eloquent\Builder
59
     */
60
    public function scopePermission(Builder $query, $permissions): Builder
61
    {
62
        $permissions = $this->convertToPermissionModels($permissions);
63
64
        $rolesWithPermissions = array_unique(array_reduce($permissions, function ($result, $permission) {
65
            return array_merge($result, $permission->roles->all());
66
        }, []));
67
68
        return $query->where(function ($query) use ($permissions, $rolesWithPermissions) {
69
            $query->whereHas('permissions', function ($query) use ($permissions) {
70
                $query->where(function ($query) use ($permissions) {
71
                    foreach ($permissions as $permission) {
72
                        $query->orWhere(config('permission.table_names.permissions').'.id', $permission->id);
73
                    }
74
                });
75
            });
76
            if (count($rolesWithPermissions) > 0) {
77
                $query->orWhereHas('roles', function ($query) use ($rolesWithPermissions) {
78
                    $query->where(function ($query) use ($rolesWithPermissions) {
79
                        foreach ($rolesWithPermissions as $role) {
80
                            $query->orWhere(config('permission.table_names.roles').'.id', $role->id);
81
                        }
82
                    });
83
                });
84
            }
85
        });
86
    }
87
88
    /**
89
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
90
     *
91
     * @return array
92
     */
93
    protected function convertToPermissionModels($permissions): array
94
    {
95
        if ($permissions instanceof Collection) {
96
            $permissions = $permissions->all();
97
        }
98
99
        $permissions = array_wrap($permissions);
100
101
        return array_map(function ($permission) {
102
            if ($permission instanceof Permission) {
103
                return $permission;
104
            }
105
106
            return $this->getPermissionClass()->findByName($permission, $this->getDefaultGuardName());
107
        }, $permissions);
108
    }
109
110
    /**
111
     * Determine if the model may perform the given permission.
112
     *
113
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
114
     * @param string|null $guardName
115
     *
116
     * @return bool
117
     * @throws \Exception
118
     */
119
    public function hasPermissionTo($permission, $guardName = null): bool
120
    {
121
        if (! is_string($permission) && ! is_int($permission) && ! $permission instanceof Permission) {
122
            throw new PermissionDoesNotExist;
123
        }
124
125
        if (! PermissionRegistrar::$cacheIsTaggable) {
126
            return $this->hasUncachedPermissionTo($permission, $guardName);
127
        }
128
129
        return cache()
130
            ->tags($this->getCacheTags($permission))
131
            ->remember(
132
                $this->getCacheKey($permission),
133
                PermissionRegistrar::$cacheExpirationTime,
134
                function () use ($permission, $guardName) {
135
                    return $this->hasUncachedPermissionTo($permission, $guardName);
136
                }
137
            );
138
    }
139
140
    /**
141
     * Check the uncached permissions for the model.
142
     *
143
     * @param string|int|Permission $permission
144
     * @param string|null $guardName
145
     *
146
     * @return bool
147
     */
148
    public function hasUncachedPermissionTo($permission, $guardName = null): bool
149
    {
150
        $permissionClass = $this->getPermissionClass();
151
152
        if (is_string($permission)) {
153
            $permission = $permissionClass->findByName(
154
                $permission,
155
                $guardName ?? $this->getDefaultGuardName()
156
            );
157
        }
158
159
        if (is_int($permission)) {
160
            $permission = $permissionClass->findById(
161
                $permission,
162
                $guardName ?? $this->getDefaultGuardName()
163
            );
164
        }
165
166
        if (! $permission instanceof Permission) {
167
            throw new PermissionDoesNotExist;
168
        }
169
170
        return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
171
    }
172
173
    /**
174
     * An alias to hasPermissionTo(), but avoids throwing an exception.
175
     *
176
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
177
     * @param string|null $guardName
178
     *
179
     * @return bool
180
     *
181
     * @throws \Exception
182
     */
183
    public function checkPermissionTo($permission, $guardName = null): bool
184
    {
185
        try {
186
            return $this->hasPermissionTo($permission, $guardName);
187
        } catch (PermissionDoesNotExist $e) {
188
            return false;
189
        }
190
    }
191
192
    /**
193
     * Construct the key for the cache entry.
194
     *
195
     * @param null|string|int|\Spatie\Permission\Contracts\Permission $permission
196
     *
197
     * @return string
198
     */
199
    protected function getCacheKey($permission = null)
200
    {
201
        $key = PermissionRegistrar::$cacheKey.'.'.$this->getClassCacheString();
202
203
        if ($permission !== null) {
204
            $key .= $this->getPermissionCacheString($permission);
205
        }
206
207
        return $key;
208
    }
209
210
    /**
211
     * Construct the tags for the cache entry.
212
     *
213
     * @param null|string|int|\Spatie\Permission\Contracts\Permission $permission
214
     *
215
     * @return array
216
     */
217
    protected function getCacheTags($permission = null)
218
    {
219
        $tags = [
220
            PermissionRegistrar::$cacheKey,
221
            $this->getClassCacheString(),
222
        ];
223
224
        if ($permission !== null) {
225
            $tags[] = $this->getPermissionCacheString($permission);
226
        }
227
228
        return $tags;
229
    }
230
231
    /**
232
     * Get the key to cache the model by.
233
     *
234
     * @return string
235
     */
236
    private function getClassCacheString()
237
    {
238
        return str_replace('\\', '.', get_class($this)).'.'.$this->getKey();
0 ignored issues
show
Bug introduced by
It seems like getKey() 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...
239
    }
240
241
    /**
242
     * Get the key to cache the permission by.
243
     *
244
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
245
     *
246
     * @return mixed
247
     */
248
    protected function getPermissionCacheString($permission)
249
    {
250
        if ($permission instanceof Permission) {
251
            $permission = $permission[PermissionRegistrar::$cacheModelKey];
252
        }
253
254
        return str_replace('\\', '.', Permission::class).'.'.$permission;
255
    }
256
257
    /**
258
     * Determine if the model has any of the given permissions.
259
     *
260
     * @param array ...$permissions
261
     *
262
     * @return bool
263
     * @throws \Exception
264
     */
265 View Code Duplication
    public function hasAnyPermission(...$permissions): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
    {
267
        if (is_array($permissions[0])) {
268
            $permissions = $permissions[0];
269
        }
270
271
        foreach ($permissions as $permission) {
272
            if ($this->checkPermissionTo($permission)) {
273
                return true;
274
            }
275
        }
276
277
        return false;
278
    }
279
280
    /**
281
     * Determine if the model has all of the given permissions.
282
     *
283
     * @param array ...$permissions
284
     *
285
     * @return bool
286
     * @throws \Exception
287
     */
288 View Code Duplication
    public function hasAllPermissions(...$permissions): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
289
    {
290
        if (is_array($permissions[0])) {
291
            $permissions = $permissions[0];
292
        }
293
294
        foreach ($permissions as $permission) {
295
            if (! $this->hasPermissionTo($permission)) {
296
                return false;
297
            }
298
        }
299
300
        return true;
301
    }
302
303
    /**
304
     * Determine if the model has, via roles, the given permission.
305
     *
306
     * @param \Spatie\Permission\Contracts\Permission $permission
307
     *
308
     * @return bool
309
     */
310
    protected function hasPermissionViaRole(Permission $permission): bool
311
    {
312
        return $this->hasRole($permission->roles);
0 ignored issues
show
Bug introduced by
Accessing roles on the interface Spatie\Permission\Contracts\Permission suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
It seems like hasRole() 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...
313
    }
314
315
    /**
316
     * Determine if the model has the given permission.
317
     *
318
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
319
     *
320
     * @return bool
321
     */
322
    public function hasDirectPermission($permission): bool
323
    {
324
        $permissionClass = $this->getPermissionClass();
325
326
        if (is_string($permission)) {
327
            $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName());
328
            if (! $permission) {
329
                return false;
330
            }
331
        }
332
333
        if (is_int($permission)) {
334
            $permission = $permissionClass->findById($permission, $this->getDefaultGuardName());
335
            if (! $permission) {
336
                return false;
337
            }
338
        }
339
340
        if (! $permission instanceof Permission) {
341
            return false;
342
        }
343
344
        return $this->permissions->contains('id', $permission->id);
0 ignored issues
show
Bug introduced by
The property permissions 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...
Bug introduced by
Accessing id on the interface Spatie\Permission\Contracts\Permission suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
345
    }
346
347
    /**
348
     * Return all the permissions the model has via roles.
349
     */
350
    public function getPermissionsViaRoles(): Collection
351
    {
352
        return $this->load('roles', 'roles.permissions')
0 ignored issues
show
Bug introduced by
It seems like load() 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...
353
            ->roles->flatMap(function ($role) {
354
                return $role->permissions;
355
            })->sort()->values();
356
    }
357
358
    /**
359
     * Return all the permissions the model has, both directly and via roles.
360
     *
361
     * @throws \Exception
362
     */
363
    public function getAllPermissions(): Collection
364
    {
365
        if (PermissionRegistrar::$cacheIsTaggable) {
366
            return cache()->tags($this->getCacheTags())
367
                ->remember(
368
                    $this->getCacheKey(),
369
                    PermissionRegistrar::$cacheExpirationTime,
370
                    function () {
371
                        $permissions = $this->permissions;
372
373
                        if ($this->roles) {
0 ignored issues
show
Bug introduced by
The property roles 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...
374
                            $permissions = $permissions->merge($this->getPermissionsViaRoles());
375
                        }
376
377
                        return $permissions->sort()->values();
378
                    }
379
                );
380
        }
381
382
        $permissions = $this->permissions;
383
384
        if ($this->roles) {
385
            $permissions = $permissions->merge($this->getPermissionsViaRoles());
386
        }
387
388
        return $permissions->sort()->values();
389
    }
390
391
    /**
392
     * Grant the given permission(s) to a role.
393
     *
394
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
395
     *
396
     * @return $this
397
     */
398 View Code Duplication
    public function givePermissionTo(...$permissions)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
399
    {
400
        $permissions = collect($permissions)
401
            ->flatten()
402
            ->map(function ($permission) {
403
                return $this->getStoredPermission($permission);
404
            })
405
            ->filter(function ($permission) {
406
                return $permission instanceof Permission;
407
            })
408
            ->each(function ($permission) {
409
                $this->ensureModelSharesGuard($permission);
410
            })
411
            ->map->id
412
            ->all();
413
414
        $model = $this->getModel();
0 ignored issues
show
Bug introduced by
It seems like getModel() 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...
415
416
        if ($model->exists) {
417
            $this->permissions()->sync($permissions, false);
418
            $model->load('permissions');
419
        } else {
420
            $class = \get_class($model);
421
422
            $class::saved(
423
                function ($model) use ($permissions) {
424
                    $model->permissions()->sync($permissions, false);
425
                    $model->load('permissions');
426
                });
427
        }
428
429
        $this->forgetCachedPermissions();
430
431
        return $this;
432
    }
433
434
    /**
435
     * Remove all current permissions and set the given ones.
436
     *
437
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
438
     *
439
     * @return $this
440
     */
441
    public function syncPermissions(...$permissions)
442
    {
443
        $this->permissions()->detach();
444
445
        return $this->givePermissionTo($permissions);
446
    }
447
448
    /**
449
     * Revoke the given permission.
450
     *
451
     * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission
452
     *
453
     * @return $this
454
     */
455
    public function revokePermissionTo($permission)
456
    {
457
        $this->permissions()->detach($this->getStoredPermission($permission));
458
459
        $this->forgetCachedPermissions();
460
461
        $this->load('permissions');
0 ignored issues
show
Bug introduced by
It seems like load() 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...
462
463
        return $this;
464
    }
465
466
    /**
467
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
468
     *
469
     * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection
470
     */
471
    protected function getStoredPermission($permissions)
472
    {
473
        $permissionClass = $this->getPermissionClass();
474
475
        if (is_numeric($permissions)) {
476
            return $permissionClass->findById($permissions, $this->getDefaultGuardName());
477
        }
478
479
        if (is_string($permissions)) {
480
            return $permissionClass->findByName($permissions, $this->getDefaultGuardName());
481
        }
482
483
        if (is_array($permissions)) {
484
            return $permissionClass
485
                ->whereIn('name', $permissions)
486
                ->whereIn('guard_name', $this->getGuardNames())
487
                ->get();
488
        }
489
490
        return $permissions;
491
    }
492
493
    /**
494
     * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission
495
     *
496
     * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch
497
     */
498
    protected function ensureModelSharesGuard($roleOrPermission)
499
    {
500
        if (! $this->getGuardNames()->contains($roleOrPermission->guard_name)) {
501
            throw GuardDoesNotMatch::create($roleOrPermission->guard_name, $this->getGuardNames());
502
        }
503
    }
504
505
    protected function getGuardNames(): Collection
506
    {
507
        return Guard::getNames($this);
508
    }
509
510
    protected function getDefaultGuardName(): string
511
    {
512
        return Guard::getDefaultName($this);
513
    }
514
515
    /**
516
     * Forget the cached permissions.
517
     */
518
    public function forgetCachedPermissions()
519
    {
520
        app(PermissionRegistrar::class)->forgetCachedPermissions();
521
    }
522
}
523