Completed
Push — master ( d6408a...ec80e9 )
by Chris
28s queued 10s
created

HasPermissions   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 438
Duplicated Lines 23.52 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 103
loc 438
rs 5.04
c 0
b 0
f 0
wmc 57
lcom 1
cbo 6

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getPermissionClass() 0 8 2
A permissions() 0 10 1
A bootHasPermissions() 10 10 3
A hasPermissionViaRole() 0 4 1
A scopePermission() 0 19 2
A convertToPermissionModels() 0 16 4
A hasPermissionTo() 0 24 5
A hasUncachedPermissionTo() 0 4 1
A checkPermissionTo() 0 8 2
A hasAnyPermission() 12 12 3
A hasAllPermissions() 12 12 3
A hasDirectPermission() 0 18 4
A getPermissionsViaRoles() 0 7 1
A getAllPermissions() 0 11 2
B givePermissionTo() 45 45 5
A syncPermissions() 0 6 1
A revokePermissionTo() 0 10 1
A getPermissionNames() 0 4 1
A getStoredPermission() 0 21 4
A ensureModelSharesGuard() 0 6 2
A getGuardNames() 0 4 1
A getDefaultGuardName() 0 4 1
A forgetCachedPermissions() 0 4 1
A hasAllDirectPermissions() 12 12 3
A hasAnyDirectPermission() 12 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like HasPermissions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasPermissions, and based on these observations, apply Extract Interface, too.

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 (Builder $query) use ($permissions, $rolesWithPermissions) {
69
            $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) {
70
                $subQuery->whereIn(config('permission.table_names.permissions').'.id', \array_column($permissions, 'id'));
71
            });
72
            if (count($rolesWithPermissions) > 0) {
73
                $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) {
74
                    $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($rolesWithPermissions, 'id'));
75
                });
76
            }
77
        });
78
    }
79
80
    /**
81
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
82
     *
83
     * @return array
84
     */
85
    protected function convertToPermissionModels($permissions): array
86
    {
87
        if ($permissions instanceof Collection) {
88
            $permissions = $permissions->all();
89
        }
90
91
        $permissions = is_array($permissions) ? $permissions : [$permissions];
92
93
        return array_map(function ($permission) {
94
            if ($permission instanceof Permission) {
95
                return $permission;
96
            }
97
98
            return $this->getPermissionClass()->findByName($permission, $this->getDefaultGuardName());
99
        }, $permissions);
100
    }
101
102
    /**
103
     * Determine if the model may perform the given permission.
104
     *
105
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
106
     * @param string|null $guardName
107
     *
108
     * @return bool
109
     * @throws PermissionDoesNotExist
110
     */
111
    public function hasPermissionTo($permission, $guardName = null): bool
112
    {
113
        $permissionClass = $this->getPermissionClass();
114
115
        if (is_string($permission)) {
116
            $permission = $permissionClass->findByName(
117
                $permission,
118
                $guardName ?? $this->getDefaultGuardName()
119
            );
120
        }
121
122
        if (is_int($permission)) {
123
            $permission = $permissionClass->findById(
124
                $permission,
125
                $guardName ?? $this->getDefaultGuardName()
126
            );
127
        }
128
129
        if (! $permission instanceof Permission) {
130
            throw new PermissionDoesNotExist;
131
        }
132
133
        return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
134
    }
135
136
    /**
137
     * @deprecated since 2.35.0
138
     * @alias of hasPermissionTo()
139
     */
140
    public function hasUncachedPermissionTo($permission, $guardName = null): bool
141
    {
142
        return $this->hasPermissionTo($permission, $guardName);
143
    }
144
145
    /**
146
     * An alias to hasPermissionTo(), but avoids throwing an exception.
147
     *
148
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
149
     * @param string|null $guardName
150
     *
151
     * @return bool
152
     */
153
    public function checkPermissionTo($permission, $guardName = null): bool
154
    {
155
        try {
156
            return $this->hasPermissionTo($permission, $guardName);
157
        } catch (PermissionDoesNotExist $e) {
158
            return false;
159
        }
160
    }
161
162
    /**
163
     * Determine if the model has any of the given permissions.
164
     *
165
     * @param array ...$permissions
166
     *
167
     * @return bool
168
     * @throws \Exception
169
     */
170 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...
171
    {
172
        $permissions = collect($permissions)->flatten();
173
174
        foreach ($permissions as $permission) {
175
            if ($this->checkPermissionTo($permission)) {
176
                return true;
177
            }
178
        }
179
180
        return false;
181
    }
182
183
    /**
184
     * Determine if the model has all of the given permissions.
185
     *
186
     * @param array ...$permissions
187
     *
188
     * @return bool
189
     * @throws \Exception
190
     */
191 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...
192
    {
193
        $permissions = collect($permissions)->flatten();
194
195
        foreach ($permissions as $permission) {
196
            if (! $this->hasPermissionTo($permission)) {
197
                return false;
198
            }
199
        }
200
201
        return true;
202
    }
203
204
    /**
205
     * Determine if the model has, via roles, the given permission.
206
     *
207
     * @param \Spatie\Permission\Contracts\Permission $permission
208
     *
209
     * @return bool
210
     */
211
    protected function hasPermissionViaRole(Permission $permission): bool
212
    {
213
        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...
214
    }
215
216
    /**
217
     * Determine if the model has the given permission.
218
     *
219
     * @param string|int|\Spatie\Permission\Contracts\Permission $permission
220
     *
221
     * @return bool
222
     * @throws PermissionDoesNotExist
223
     */
224
    public function hasDirectPermission($permission): bool
225
    {
226
        $permissionClass = $this->getPermissionClass();
227
228
        if (is_string($permission)) {
229
            $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName());
230
        }
231
232
        if (is_int($permission)) {
233
            $permission = $permissionClass->findById($permission, $this->getDefaultGuardName());
234
        }
235
236
        if (! $permission instanceof Permission) {
237
            throw new PermissionDoesNotExist;
238
        }
239
240
        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...
241
    }
242
243
    /**
244
     * Return all the permissions the model has via roles.
245
     */
246
    public function getPermissionsViaRoles(): Collection
247
    {
248
        return $this->loadMissing('roles', 'roles.permissions')
0 ignored issues
show
Bug introduced by
It seems like loadMissing() 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...
249
            ->roles->flatMap(function ($role) {
250
                return $role->permissions;
251
            })->sort()->values();
252
    }
253
254
    /**
255
     * Return all the permissions the model has, both directly and via roles.
256
     */
257
    public function getAllPermissions(): Collection
258
    {
259
        /** @var Collection $permissions */
260
        $permissions = $this->permissions;
261
262
        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...
263
            $permissions = $permissions->merge($this->getPermissionsViaRoles());
264
        }
265
266
        return $permissions->sort()->values();
267
    }
268
269
    /**
270
     * Grant the given permission(s) to a role.
271
     *
272
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
273
     *
274
     * @return $this
275
     */
276 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...
277
    {
278
        $permissions = collect($permissions)
279
            ->flatten()
280
            ->map(function ($permission) {
281
                if (empty($permission)) {
282
                    return false;
283
                }
284
285
                return $this->getStoredPermission($permission);
286
            })
287
            ->filter(function ($permission) {
288
                return $permission instanceof Permission;
289
            })
290
            ->each(function ($permission) {
291
                $this->ensureModelSharesGuard($permission);
292
            })
293
            ->map->id
294
            ->all();
295
296
        $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...
297
298
        if ($model->exists) {
299
            $this->permissions()->sync($permissions, false);
300
            $model->load('permissions');
301
        } else {
302
            $class = \get_class($model);
303
304
            $class::saved(
305
                function ($object) use ($permissions, $model) {
306
                    static $modelLastFiredOn;
307
                    if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) {
308
                        return;
309
                    }
310
                    $object->permissions()->sync($permissions, false);
311
                    $object->load('permissions');
312
                    $modelLastFiredOn = $object;
313
                }
314
            );
315
        }
316
317
        $this->forgetCachedPermissions();
318
319
        return $this;
320
    }
321
322
    /**
323
     * Remove all current permissions and set the given ones.
324
     *
325
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
326
     *
327
     * @return $this
328
     */
329
    public function syncPermissions(...$permissions)
330
    {
331
        $this->permissions()->detach();
332
333
        return $this->givePermissionTo($permissions);
334
    }
335
336
    /**
337
     * Revoke the given permission.
338
     *
339
     * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission
340
     *
341
     * @return $this
342
     */
343
    public function revokePermissionTo($permission)
344
    {
345
        $this->permissions()->detach($this->getStoredPermission($permission));
346
347
        $this->forgetCachedPermissions();
348
349
        $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...
350
351
        return $this;
352
    }
353
354
    public function getPermissionNames(): Collection
355
    {
356
        return $this->permissions->pluck('name');
357
    }
358
359
    /**
360
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
361
     *
362
     * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection
363
     */
364
    protected function getStoredPermission($permissions)
365
    {
366
        $permissionClass = $this->getPermissionClass();
367
368
        if (is_numeric($permissions)) {
369
            return $permissionClass->findById($permissions, $this->getDefaultGuardName());
370
        }
371
372
        if (is_string($permissions)) {
373
            return $permissionClass->findByName($permissions, $this->getDefaultGuardName());
374
        }
375
376
        if (is_array($permissions)) {
377
            return $permissionClass
378
                ->whereIn('name', $permissions)
379
                ->whereIn('guard_name', $this->getGuardNames())
380
                ->get();
381
        }
382
383
        return $permissions;
384
    }
385
386
    /**
387
     * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission
388
     *
389
     * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch
390
     */
391
    protected function ensureModelSharesGuard($roleOrPermission)
392
    {
393
        if (! $this->getGuardNames()->contains($roleOrPermission->guard_name)) {
394
            throw GuardDoesNotMatch::create($roleOrPermission->guard_name, $this->getGuardNames());
395
        }
396
    }
397
398
    protected function getGuardNames(): Collection
399
    {
400
        return Guard::getNames($this);
401
    }
402
403
    protected function getDefaultGuardName(): string
404
    {
405
        return Guard::getDefaultName($this);
406
    }
407
408
    /**
409
     * Forget the cached permissions.
410
     */
411
    public function forgetCachedPermissions()
412
    {
413
        app(PermissionRegistrar::class)->forgetCachedPermissions();
414
    }
415
416
    /**
417
     * Check if the model has All of the requested Direct permissions.
418
     * @param array ...$permissions
419
     * @return bool
420
     */
421 View Code Duplication
    public function hasAllDirectPermissions(...$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...
422
    {
423
        $permissions = collect($permissions)->flatten();
424
425
        foreach ($permissions as $permission) {
426
            if (! $this->hasDirectPermission($permission)) {
427
                return false;
428
            }
429
        }
430
431
        return true;
432
    }
433
434
    /**
435
     * Check if the model has Any of the requested Direct permissions.
436
     * @param array ...$permissions
437
     * @return bool
438
     */
439 View Code Duplication
    public function hasAnyDirectPermission(...$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...
440
    {
441
        $permissions = collect($permissions)->flatten();
442
443
        foreach ($permissions as $permission) {
444
            if ($this->hasDirectPermission($permission)) {
445
                return true;
446
            }
447
        }
448
449
        return false;
450
    }
451
}
452