mateusjunges/laravel-acl

View on GitHub
src/Concerns/HasPermissions.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Junges\ACL\Concerns;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Junges\ACL\AclRegistrar;
use Junges\ACL\Contracts\Group;
use Junges\ACL\Contracts\Permission;
use Junges\ACL\Exceptions\GuardDoesNotMatch;
use Junges\ACL\Exceptions\PermissionDoesNotExistException;
use Junges\ACL\Guard;

/**
 * @method static Builder group(mixed $group);
 * @method static Builder permission(mixed $permission);
 * @mixin Model
 */
trait HasPermissions
{
    private $permissionClass;

    public static function bootHasPermissions()
    {
        static::deleting(function ($model) {
            if (method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting()) {
                return;
            }

            $model->permissions()->detach();
        });
    }

    public function getPermissionClass()
    {
        if (! isset($this->permissionClass)) {
            $this->permissionClass = app(AclRegistrar::class)->getPermissionClass();
        }

        return $this->permissionClass;
    }

    /**
     * Return all user permissions.
     *
     * @return BelongsToMany
     */
    public function permissions(): BelongsToMany
    {
        $relation = $this->morphToMany(
            config('acl.models.permission'),
            'model',
            config('acl.tables.model_has_permissions'),
            config('acl.column_names.model_morph_key'),
            AclRegistrar::$pivotPermission
        );

        if (AclRegistrar::$teams) {
            return $relation->wherePivot(AclRegistrar::$teamsKey, getPermissionsTeamId());
        }

        return $relation;
    }

    /**
     * Scope the model query to certain permissions only.
     *
     * @param Builder $query
     * @param $permissions
     *
     * @return Builder
     */
    public function scopePermission(Builder $query, $permissions): Builder
    {
        $permissions = $this->convertToPermissionModels($permissions);

        $groupsWithPermissions = array_unique(array_reduce($permissions, function ($result, $permission) {
            return array_merge($result, $permission->groups->all());
        }, []));

        return $query->where(function ($query) use ($permissions, $groupsWithPermissions) {
            $query->whereHas('permissions', function ($query) use ($permissions) {
                $permissionClass = $this->getPermissionClass();
                $key = (new $permissionClass)->getKeyName();
                $query->whereIn(config('acl.tables.permissions').".$key", array_column($permissions, $key));
            });

            if (count($groupsWithPermissions) > 0) {
                $query->orWhereHas('groups', function ($query) use ($groupsWithPermissions) {
                    $groupClass = $this->getGroupClass();
                    $key = (new $groupClass)->getKeyName();
                    $query->whereIn(config('acl.tables.groups').".$key", array_column($groupsWithPermissions, $key));
                });
            }
        });
    }

    /**
     * Determine if a user has a permission, regardless of whether it is direct or via group.
     *
     * @param  int|string|Model  $permission
     * @return bool
     */
    public function hasPermission($permission, string $guardName = null): bool
    {
        /** @var Permission $permissionClass */
        $permissionClass = $this->getPermissionClass();

        if (is_string($permission)) {
            $permission = $permissionClass->findByName(
                $permission,
                $guardName ?? $this->getDefaultGuardName()
            );
        }

        if (is_int($permission)) {
            $permission = $permissionClass->findById(
                $permission,
                $guardName ?? $this->getDefaultGuardName(),
            );
        }

        if (! $permission instanceof Permission) {
            throw new PermissionDoesNotExistException();
        }

        return $this->hasDirectPermission($permission) || $this->hasPermissionThroughGroup($permission);
    }

    /**
     * Same as hasPermission(), but avoid throwing an exception.
     *
     * @param $permission
     * @param $guardName
     * @return bool
     * @throws \Junges\ACL\Exceptions\GuardDoesNotMatch
     */
    public function checkPermission($permission, $guardName = null): bool
    {
        try {
            return $this->hasPermission($permission, $guardName);
        } catch (PermissionDoesNotExistException $exception) {
            return false;
        }
    }

    /**
     * Retrieves all permissions a user has via groups.
     *
     * @return Collection
     */
    public function getPermissionsViaGroups(): Collection
    {
        return $this->loadMissing('groups', 'groups.permissions')
            ->groups->flatMap(fn ($group) => $group->permissions)
            ->sort()
            ->values();
    }

    /**
     * Sync user permissions on database.
     *
     * @param array $permissions
     *
     * @return $this|bool
     */
    public function syncPermissions(...$permissions)
    {
        $this->permissions()->detach();

        return $this->assignPermission($permissions);
    }

    /**
     * Revoke permissions from the user.
     *
     * @param  mixed  $permissions
     *
     * @return $this
     */
    public function revokePermission($permissions): self
    {
        $permissions = is_array($permissions) || $permissions instanceof Collection
            ? $permissions
            : func_get_args();

        foreach ($permissions as $permission) {
            $this->permissions()->detach($this->getStoredPermission($permission));
        }

        if (is_a($this, get_class(app(AclRegistrar::class)->getGroupClass()))) {
            $this->forgetCachedPermissions();
        }

        $this->load('permissions');

        return $this;
    }

    public function getPermissionNames(): Collection
    {
        return $this->permissions->pluck('name');
    }

    public function getStoredPermission($permissions)
    {
        $permissionClass = $this->getPermissionClass();

        if (is_numeric($permissions)) {
            return $permissionClass->findById($permissions, $this->getDefaultGuardName());
        }

        if (is_string($permissions)) {
            return $permissionClass->findByName($permissions, $this->getDefaultGuardName());
        }

        if (is_array($permissions)) {
            $permissions = array_map(function ($permission) use ($permissionClass) {
                return is_a($permission, get_class($permissionClass)) ? $permission->name : $permission;
            }, $permissions);

            return $permissionClass
                ->whereIn('name', $permissions)
                ->whereIn('guard_name', $this->getGuardNames())
                ->get();
        }

        return $permissions;
    }

    public function forgetCachedPermissions()
    {
        app(AclRegistrar::class)->forgetCachedPermissions();
    }

    /**
     * Check if the model has all the requested Direct permissions.
     *
     * @param ...$permissions
     * @return bool
     */
    public function hasAllDirectPermissions(...$permissions): bool
    {
        $permissions = collect($permissions)->flatten();

        foreach ($permissions as $permission) {
            if (! $this->hasDirectPermission($permission)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if the model has any of the requested Direct permissions.
     *
     * @param ...$permissions
     * @return bool
     */
    public function hasAnyDirectPermission(...$permissions): bool
    {
        $permissions = collect($permissions)->flatten();

        foreach ($permissions as $permission) {
            if ($this->hasDirectPermission($permission)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if a user has any permission.
     *
     * @param  mixed  $permissions
     *
     * @return bool
     */
    public function hasAnyPermission($permissions): bool
    {
        $permissions = is_array($permissions) || $permissions instanceof Collection ? $permissions : func_get_args();

        foreach ($permissions as $permission) {
            if ($this->checkPermission($permission)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if the user has all specified permissions.
     *
     * @param array $permissions
     *
     * @return bool
     */
    public function hasAllPermissions(...$permissions): bool
    {
        $permissions = collect($permissions)->flatten();

        foreach ($permissions as $permission) {
            if (! $this->checkPermission($permission)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Determine if the user has a group which has the required permission.
     *
     * @param  Permission  $permission
     * @return bool
     */
    public function hasPermissionThroughGroup(Permission $permission): bool
    {
        return $this->hasGroup($permission->groups);
    }

    /**
     * Determine if a user has a permission directly associated.
     *
     * @param int|string|Model $permission
     * @param string|null $guardName
     * @return bool
     */
    public function hasDirectPermission($permission, string $guardName = null): bool
    {
        $permissionClass = $this->getPermissionClass();

        if (is_string($permission)) {
            $permission = $permissionClass->findByName($permission, $guardName ?? $this->getDefaultGuardName());
        }

        if (is_int($permission)) {
            $permission = $permissionClass->findById($permission, $guardName ?? $this->getDefaultGuardName());
        }

        if (! $permission instanceof Permission) {
            throw new PermissionDoesNotExistException();
        }

        return $this->permissions->contains($permission->getKeyName(), $permission->getKey());
    }

    /**
     * Return all the permissions a user has, both directly and via groups.
     *
     * @return Collection
     */
    public function getAllPermissions(): Collection
    {
        $permissions = $this->permissions;

        if ($this->groups) {
            $permissions = $permissions->merge($this->getPermissionsViaGroups());
        }

        return $permissions->sort()->values();
    }

    /**
     * Give the given permissions to the model.
     *
     * @param mixed $permissions
     *
     * @return self|bool
     */
    public function assignPermission($permissions): self
    {
        $permissions = collect(is_array($permissions) || $permissions instanceof Collection ? $permissions : func_get_args())
            ->flatten()
            ->reduce(function ($array, $permission) {
                if (empty($permission)) {
                    return $array;
                }

                $permission = $this->getStoredPermission($permission);

                if (! $permission instanceof Permission) {
                    return $array;
                }

                $this->ensureModelSharesGuard($permission);

                $array[$permission->getKey()] = AclRegistrar::$teams && ! is_a($this, Group::class)
                    ? [AclRegistrar::$teamsKey => getPermissionsTeamId()] : [];

                return $array;
            }, []);

        $model = $this->getModel();

        if ($model->exists) {
            $this->permissions()->sync($permissions, false);
            $model->load('permissions');
        } else {
            /** @var Model $class */
            $class = get_class($model);

            $class::saved(
                function ($object) use ($permissions, $model) {
                    if ($model->getKey() != $object->getKey()) {
                        return;
                    }
                    $model->permissions()->sync($permissions, false);
                    $model->load('permissions');
                }
            );
        }

        if (is_a($this, get_class(app(AclRegistrar::class)->getGroupClass()))) {
            $this->forgetCachedPermissions();
        }

        return $this;
    }

    protected function getGuardNames(): Collection
    {
        return Guard::getNames($this);
    }

    protected function getDefaultGuardName(): string
    {
        return Guard::getDefaultName($this);
    }

    protected function ensureModelSharesGuard($groupOrPermission)
    {
        if (! $this->getGuardNames()->contains($groupOrPermission->guard_name)) {
            throw GuardDoesNotMatch::create($groupOrPermission->guard_name, $this->getGuardNames());
        }
    }

    /**
     * Convert permission id or permission slug to permission model.
     *
     * @param $permissions
     *
     * @return array
     */
    protected function convertToPermissionModels($permissions): array
    {
        if ($permissions instanceof Collection) {
            $permissions = $permissions->all();
        }

        return array_map(function ($permission) {
            if ($permission instanceof Permission) {
                return $permission;
            }

            $method = is_string($permission) ? 'findByName' : 'findById';

            return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName());
        }, Arr::wrap($permissions));
    }
}