Completed
Pull Request — master (#644)
by Chris
01:25
created

HasPermissions::hasPermissionViaRole()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
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
13
trait HasPermissions
14
{
15
    public static function bootHasPermissions()
16
    {
17
        static::deleting(function ($model) {
18
            if (method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting()) {
19
                return;
20
            }
21
22
            $model->permissions()->detach();
23
        });
24
    }
25
26
    /**
27
     * A model may have multiple direct permissions.
28
     */
29
    public function permissions(): MorphToMany
30
    {
31
        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...
32
            config('permission.models.permission'),
33
            'model',
34
            config('permission.table_names.model_has_permissions'),
35
            'model_id',
36
            'permission_id'
37
        );
38
    }
39
40
    /**
41
     * Scope the model query to certain permissions only.
42
     *
43
     * @param \Illuminate\Database\Eloquent\Builder $query
44
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
45
     *
46
     * @return \Illuminate\Database\Eloquent\Builder
47
     */
48
    public function scopePermission(Builder $query, $permissions): Builder
49
    {
50
        $permissions = $this->convertToPermissionModels($permissions);
51
52
        $rolesWithPermissions = array_unique(array_reduce($permissions, function ($result, $permission) {
53
            return array_merge($result, $permission->roles->all());
54
        }, []));
55
56
        return $query->
57
        where(function ($query) use ($permissions, $rolesWithPermissions) {
58
            $query->whereHas('permissions', function ($query) use ($permissions) {
59
                $query->where(function ($query) use ($permissions) {
60
                    foreach ($permissions as $permission) {
61
                        $query->orWhere(config('permission.table_names.permissions').'.id', $permission->id);
62
                    }
63
                });
64
            });
65
            if (count($rolesWithPermissions) > 0) {
66
                $query->orWhereHas('roles', function ($query) use ($rolesWithPermissions) {
67
                    $query->where(function ($query) use ($rolesWithPermissions) {
68
                        foreach ($rolesWithPermissions as $role) {
69
                            $query->orWhere(config('permission.table_names.roles').'.id', $role->id);
70
                        }
71
                    });
72
                });
73
            }
74
        });
75
    }
76
77
    /**
78
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
79
     *
80
     * @return array
81
     */
82
    protected function convertToPermissionModels($permissions): array
83
    {
84
        if ($permissions instanceof Collection) {
85
            $permissions = $permissions->all();
86
        }
87
88
        $permissions = array_wrap($permissions);
89
90
        return array_map(function ($permission) {
91
            if ($permission instanceof Permission) {
92
                return $permission;
93
            }
94
95
            return app(Permission::class)->findByName($permission, $this->getDefaultGuardName());
96
        }, $permissions);
97
    }
98
99
    /**
100
     * Determine if the model may perform the given permission.
101
     *
102
     * @param string|\Spatie\Permission\Contracts\Permission $permission
103
     * @param string|null $guardName
104
     *
105
     * @return bool
106
     */
107
    public function hasPermissionTo($permission, $guardName = null): bool
108
    {
109 View Code Duplication
        if (is_string($permission)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
110
            $permission = app(Permission::class)->findByName(
111
                $permission,
112
                $guardName ?? $this->getDefaultGuardName()
113
            );
114
        }
115
116
        if (is_int($permission)) {
117
            $permission = app(Permission::class)->findById($permission, $this->getDefaultGuardName());
118
        }
119
120
        return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
121
    }
122
123
    /**
124
     * Determine if the model has any of the given permissions.
125
     *
126
     * @param array ...$permissions
127
     *
128
     * @return bool
129
     */
130
    public function hasAnyPermission(...$permissions): bool
131
    {
132
        if (is_array($permissions[0])) {
133
            $permissions = $permissions[0];
134
        }
135
136
        foreach ($permissions as $permission) {
137
            if ($this->hasPermissionTo($permission)) {
138
                return true;
139
            }
140
        }
141
142
        return false;
143
    }
144
145
    /**
146
     * Determine if the model has, via roles, the given permission.
147
     *
148
     * @param \Spatie\Permission\Contracts\Permission $permission
149
     *
150
     * @return bool
151
     */
152
    protected function hasPermissionViaRole(Permission $permission): bool
153
    {
154
        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...
155
    }
156
157
    /**
158
     * Determine if the model has the given permission.
159
     *
160
     * @param string|\Spatie\Permission\Contracts\Permission $permission
161
     *
162
     * @return bool
163
     */
164
    public function hasDirectPermission($permission): bool
165
    {
166 View Code Duplication
        if (is_string($permission)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
167
            $permission = app(Permission::class)->findByName($permission, $this->getDefaultGuardName());
168
            if (! $permission) {
169
                return false;
170
            }
171
        }
172
173
        if (is_int($permission)) {
174
            $permission = app(Permission::class)->findById($permission, $this->getDefaultGuardName());
175
            if (! $permission) {
176
                return false;
177
            }
178
        }
179
180
        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...
181
    }
182
183
    /**
184
     * Return all the permissions the model has via roles.
185
     */
186
    public function getPermissionsViaRoles(): Collection
187
    {
188
        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...
189
            ->roles->flatMap(function ($role) {
190
                return $role->permissions;
191
            })->sort()->values();
192
    }
193
194
    /**
195
     * Return all the permissions the model has, both directly and via roles.
196
     */
197
    public function getAllPermissions(): Collection
198
    {
199
        return $this->permissions
200
            ->merge($this->getPermissionsViaRoles())
201
            ->sort()
202
            ->values();
203
    }
204
205
    /**
206
     * Grant the given permission(s) to a role.
207
     *
208
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
209
     *
210
     * @return $this
211
     */
212 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...
213
    {
214
        $permissions = collect($permissions)
215
            ->flatten()
216
            ->map(function ($permission) {
217
                return $this->getStoredPermission($permission);
218
            })
219
            ->each(function ($permission) {
220
                $this->ensureModelSharesGuard($permission);
221
            })
222
            ->all();
223
224
        $this->permissions()->saveMany($permissions);
225
226
        $this->forgetCachedPermissions();
227
228
        return $this;
229
    }
230
231
    /**
232
     * Remove all current permissions and set the given ones.
233
     *
234
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
235
     *
236
     * @return $this
237
     */
238
    public function syncPermissions(...$permissions)
239
    {
240
        $this->permissions()->detach();
241
242
        return $this->givePermissionTo($permissions);
243
    }
244
245
    /**
246
     * Revoke the given permission.
247
     *
248
     * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission
249
     *
250
     * @return $this
251
     */
252
    public function revokePermissionTo($permission)
253
    {
254
        $this->permissions()->detach($this->getStoredPermission($permission));
255
256
        $this->forgetCachedPermissions();
257
258
        return $this;
259
    }
260
261
    /**
262
     * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
263
     *
264
     * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection
265
     */
266
    protected function getStoredPermission($permissions)
267
    {
268
        if (is_numeric($permissions)) {
269
            return app(Permission::class)->findById($permissions, $this->getDefaultGuardName());
270
        }
271
272
        if (is_string($permissions)) {
273
            return app(Permission::class)->findByName($permissions, $this->getDefaultGuardName());
274
        }
275
276
        if (is_array($permissions)) {
277
            return app(Permission::class)
278
                ->whereIn('name', $permissions)
279
                ->whereIn('guard_name', $this->getGuardNames())
280
                ->get();
281
        }
282
283
        return $permissions;
284
    }
285
286
    /**
287
     * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission
288
     *
289
     * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch
290
     */
291
    protected function ensureModelSharesGuard($roleOrPermission)
292
    {
293
        if (! $this->getGuardNames()->contains($roleOrPermission->guard_name)) {
294
            throw GuardDoesNotMatch::create($roleOrPermission->guard_name, $this->getGuardNames());
295
        }
296
    }
297
298
    protected function getGuardNames(): Collection
299
    {
300
        return Guard::getNames($this);
301
    }
302
303
    protected function getDefaultGuardName(): string
304
    {
305
        return Guard::getDefaultName($this);
306
    }
307
308
    /**
309
     * Forget the cached permissions.
310
     */
311
    public function forgetCachedPermissions()
312
    {
313
        app(PermissionRegistrar::class)->forgetCachedPermissions();
314
    }
315
}
316