Повышение эффективности приложения Laravel с помощью ролей и разрешений Spatie

RedDeveloper
09.04.2023 13:22
Повышение эффективности приложения Laravel с помощью ролей и разрешений Spatie

Повышение эффективности приложения Laravel с помощью ролей и разрешений Spatie .

Приступаем к установке и прочим действиям

Установите пакет разрешений Spatie с помощью этих команд :

//Install the package
 composer require spatie/laravel-permission

//Register the provider in the config/app.php
'providers' => [
    // ...
    Spatie\Permission\PermissionServiceProvider::class,
];

//This will generate the necessary migrations for the package and the config file 
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"


php artisan optimize:clear
# or
php artisan config:clear


php artisan migrate // will migrate the neccessary tables required for this package

Модель пользователя должна иметь признак HasRole:

use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;

    // ...
}

Теперь мы создадим несколько разрешений в таблице разрешений.

Imp: Убедитесь, что сначала добавили " MODULE Name " с точечным обозначением и OPERATION Name, как показано ниже:

Имя модуля: категории

Имя операции: create

Конечный результат: categories.create

Конечный результат categoriescreate

Это поможет отобразить настройки "добавления прав доступа к ролям" по категориям следующим образом:

Это поможет отобразить настройки "добавления прав доступа к ролям" по категориям следующим

Теперь создадим RoleController с помощью команды " php artisan make:controller RoleController ". Нам не нужно создавать модель для ролей или разрешений, так как они уже есть в нашем пакете и мы можем использовать их с помощью команды "use Spatie\Permission\Models\Role ". Давайте создадим несколько методов для нашего контроллера ролей.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use Spatie\Permission\Models\Role; <--- ROLE MODEL
use Spatie\Permission\Models\Permission; <---- PERMISSION MODEL

class RoleController extends Controller
{
    public function allRoles()
    {
    
    }

    public function create()
    {
    
    }

    public function store(Request $request)
    {
    
    }

    public function edit($id)
    {
    
    }

    public function update(Request $request, $id)
    {

    }

    public function delete($id)
    {
    
    }

}

Давайте посмотрим на страницу создания ролей и как отобразить ее следующим образом:

Давайте посмотрим на страницу создания ролей и как отобразить ее следующим образом

Метод создания :

public function create()
{
    \DB::statement("SET SQL_MODE=''");;
    $role_permission = Permission::select('name','id')->groupBy('name')->get();

    $custom_permission = array();

    foreach($role_permission as $per){

        $key = substr($per->name, 0, strpos($per->name, "."));

        if (str_starts_with($per->name, $key)){
         
            $custom_permission[$key][] = $per;
        }

    }
 
    return view('admin.roles.create')->with('permissions',$custom_permission);
}

Посмотрите, как мы создадим этот массив:

  1. $role_permission = Permission::select('name','id')->groupBy('name')->get();:Это выбирает только столбцы name и id из таблицы разрешений, группирует результаты по столбцу name и, наконец, получает все результаты:

Пример :-

Пример -

2. $custom_permission = array();: Это инициализирует пустой массив $custom_permission, который будет использоваться для хранения пользовательских разрешений позже в коде.

   foreach($role_permission as $per){

        $key = substr($per->name, 0, strpos($per->name, "."));

        if (str_starts_with($per->name, $key)){
            $custom_permission[$key][] = $per;
        }

    }

3. $key = substr($per->name, 0, strpos($per->name, "."));: Эта строка извлекает подстроку из начала поля name текущего элемента $per до первого появления символа точки ("."). Это делается с помощью функций substr() и strpos() в PHP, а полученная подстрока сохраняется как $key .

4. if (str_starts_with($per->name, $key)){ ... }: Затем проверяется, начинается ли строка столбца 'name' с извлеченного $key, используя функцию str_starts_with(). например, вот так :

if categories.create starts with categories which is true 
   (categories == categories.create) ==> true
Some defination of str_starts_with.

str_starts_with($string, $substring) 
Parameters:

$string: This parameter refers to the string whose starting string needs to be checked.
$substring: This parameter refers to the string that needs to be checked.

Return Type: If the string begins with the substring then str_starts_with() will return TRUE otherwise it will return FALSE.  

5. Если да, то происходит добавление записи в массив $custom_permission, используя $key в качестве ключа массива, который представляет собой категории, а $per в качестве объекта.

Таким образом, это будет выглядеть следующим образом:

Таким образом это будет выглядеть следующим образом

Это используется как механизм группировки для объединения разрешений с похожими префиксами в массив $custom_permission.

Мы можем получить доступ к массиву $custom_permission и использовать его для отображения пользовательских разрешений, сгруппированных по их префиксам.

Теперь отобразим это в представлении

Create.blade.php:

<div class="ml-4 mt-16 w-9/12">
  <form action="{{route('roles.store')}}" method="POST">
      @csrf
  
      <h1 class="text-3xl mt-4 mb-8"> Create Role </h1>

      <div class="mb-6">
          <label for="text" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Role Name</label>
          <input type="text" value="{{old('name')}}" name="name" id="email" class="bg-gray-50 w-80 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 " placeholder="User, Editor, Author ... " >
          
          @foreach ($errors->get('name') as $error)
              <p class="text-red-600">{{$error}}</p>
          @endforeach
      </div>

      <table class="permissionTable border rounded-md bg-white overflow-hidden shadow-lg my-4 p-4">
          <th class="px-4 py-4">
              {{__('Section')}}
          </th>

          <th class="px-4 py-4">
              <label>
                  <input class="grand_selectall" type="checkbox">
                  {{__('Select All') }}
              </label>
          </th>

          <th class="px-4 py-4">
              {{__("Available permissions")}}
          </th>



          <tbody>
          @foreach($permissions as $key => $group)
              <tr class="py-8">
                  <td class="p-6">
                      <b>{{ ucfirst($key) }}</b>
                  </td>
                  <td class="p-6" width="30%">
                      <label>
                          <input class="selectall" type="checkbox">
                          {{__('Select All') }}
                      </label>
                  </td>
                  <td class="p-6">

                      @forelse($group as $permission)

                      <label style="width: 30%" class="">
                          <input name="permissions[]" class="permissioncheckbox" class="rounded-md border" type="checkbox" value="{{ $permission->id }}">
                          {{$permission->name}} &nbsp;&nbsp;
                      </label>

                      @empty
                          {{ __("No permission in this group !") }}
                      @endforelse

                  </td>

              </tr>
          @endforeach
          </tbody>
      </table>


      <button type="submit" class="text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 ">
          Create Role
      </button>

  </form>
</div>

Теперь для того, чтобы функция select all работала, вставьте этот код в тег script в файле create.blade.php:


$(".permissionTable").on('click', '.selectall', function () {

    if ($(this).is(':checked')) {
        $(this).closest('tr').find('[type=checkbox]').prop('checked', true);

    } else {
        $(this).closest('tr').find('[type=checkbox]').prop('checked', false);

    }

    calcu_allchkbox();

});

$(".permissionTable").on('click', '.grand_selectall', function () {
    if ($(this).is(':checked')) {
        $('.selectall').prop('checked', true);
        $('.permissioncheckbox').prop('checked', true);
    } else {
        $('.selectall').prop('checked', false);
        $('.permissioncheckbox').prop('checked', false);
    }
});

$(function () {

    calcu_allchkbox();
    selectall();

});

function selectall(){

    $('.selectall').each(function (i) {

        var allchecked = new Array();

        $(this).closest('tr').find('.permissioncheckbox').each(function (index) {
            if ($(this).is(":checked")) {
                allchecked.push(1);
            } else {
                allchecked.push(0);
            }
        });

        if ($.inArray(0, allchecked) != -1) {
            $(this).prop('checked', false);
        } else {
            $(this).prop('checked', true);
        }

    });
}

function calcu_allchkbox(){

    var allchecked = new Array();

    $('.selectall').each(function (i) {


        $(this).closest('tr').find('.permissioncheckbox').each(function (index) {
            if ($(this).is(":checked")) {
                allchecked.push(1);
            } else {
                allchecked.push(0);
            }
        });


    });

    if ($.inArray(0, allchecked) != -1) {
        $('.grand_selectall').prop('checked', false);
    } else {
        $('.grand_selectall').prop('checked', true);
    }

}



$('.permissionTable').on('click', '.permissioncheckbox', function () {

    var allchecked = new Array;

    $(this).closest('tr').find('.permissioncheckbox').each(function (index) {
        if ($(this).is(":checked")) {
            allchecked.push(1);
        } else {
            allchecked.push(0);
        }
    });

    if ($.inArray(0, allchecked) != -1) {
        $(this).closest('tr').find('.selectall').prop('checked', false);
    } else {
        $(this).closest('tr').find('.selectall').prop('checked', true);

    }

    calcu_allchkbox();

});

Store method :

Теперь мы создадим метод роли и прикрепим разрешения к ролям с помощью метода givePermissionTo() .

public function store(Request $request)
{
    $request->validate([

        'name' => 'required',
    ]);

    $role = Role::create([
        'name' => $request->name,
    ]);

    if ($request->permissions){

        foreach ($request->permissions as $key => $value) {
            $role->givePermissionTo($value);
        }
    }

    return redirect()->route('roles.all');
}

Метод редактирования:

 public function edit($id)
    {
       
        $role = Role::with('permissions')->find($id);

        \DB::statement("SET SQL_MODE=''");
        $role_permission = Permission::select('name','id')->groupBy('name')->get();


        $custom_permission = array();

        foreach($role_permission as $per){

            $key = substr($per->name, 0, strpos($per->name, "."));

            if (str_starts_with($per->name, $key)){
                $custom_permission[$key][] = $per;
            }

        }

        return view('admin.roles.edit',compact('role'))->with('permissions',$custom_permission);
    }

Edit.blade.php:

   <div class="ml-4 mt-16 w-9/12">
        <form action="{{route('roles.store')}}" method="POST">
            @csrf
        
            <h1 class="text-3xl mt-4 mb-8"> Update Role </h1>

            <div class="mb-6">
                <label for="text" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Role Name</label>
                <input type="text" value="{{old('name',$role->name ?? '')}}" name="name" id="email" class="bg-gray-50 w-80 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 " placeholder="Wedding, Kitty kat, parties, lol deaths haha" >
                
                @foreach ($errors->get('name') as $error)
                    <p class="text-red-600">{{$error}}</p>
                @endforeach
            </div>

            <table class="permissionTable border rounded-md bg-white overflow-hidden shadow-lg my-4 p-4">
                <th class="px-4 py-4">
                    {{__('Section')}}
                </th>

                <th class="px-4 py-4">
                    <label>
                        <input class="grand_selectall" type="checkbox">
                        {{__('Select All') }}
                    </label>
                </th>

                <th class="px-4 py-4">
                    {{__("Available permissions")}}
                </th>



                <tbody>
                @foreach($permissions as $key => $group)
                    <tr class="py-8">
                        <td class="p-6">
                            <b>{{ ucfirst($key) }}</b>
                        </td>
                        <td class="p-6" width="30%">
                            <label>
                                <input class="selectall" type="checkbox">
                                {{__('Select All') }}
                            </label>
                        </td>
                        <td class="p-6">

                            @forelse($group as $permission)

                            <label style="width: 30%" class="">
                                <input  {{ $role->permissions->contains('id',$permission->id) ? "checked" : "" }} name="permissions[]" class="permissioncheckbox" class="rounded-md border" type="checkbox" value="{{ $permission->id }}">
                                {{$permission->name}} &nbsp;&nbsp;
                            </label>

                            @empty
                                {{ __("No permission in this group !") }}
                            @endforelse

                        </td>

                    </tr>
                @endforeach
                </tbody>
            </table>


            <button type="submit" class="text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 ">
                Update Role
            </button>
        </form>
    </div>

Метод обновления :

Несколько разрешений могут быть синхронизированы с ролью с помощью метода syncPermissions:

public function update(Request $request, $id)
{
    
    $role = Role::where('id',$id)->first();

    $request->validate([
        'name' => 'required'
    ]);

    $role->update([
        "name" => $request->name
    ]);

    $role->syncPermissions($request->permissions);


    return redirect()->route('admin.roles.all')->with('success','Roles Updated Successfully');
}

Метод удаления :

public function delete($id)
{
    $role = Role::where('id',$id)->first();

    if (isset($role)){
        
        $role->permissions()->detach();
        $role->delete();

        return redirect()->route('roles.all')->with('success','Roles Deleted Successfully');

    }
}

Index.blade.php:

<div class="flex flex-col mt-6">
  <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
      <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
          <div class="overflow-hidden border border-gray-200 dark:border-gray-700 md:rounded-lg">
              <table id="eventstable" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
                  <thead class="bg-gray-50 dark:bg-gray-800">
                      <tr>
                          <th scope="col" class="py-3.5 px-4 text-sm font-normal text-left rtl:text-right text-gray-500 dark:text-gray-400">
                              <div class="flex items-center gap-x-3">
                                  <input type="checkbox" class="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700">
                                  <span>Id</span>
                              </div>
                          </th>

                          <th scope="col" class="px-12 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 dark:text-gray-400">
                              <button class="flex items-center gap-x-2">
                                  <span>Role Name</span>
                              </button>
                          </th>

                          <th scope="col" class="px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 dark:text-gray-400">
                              <button class="flex items-center gap-x-2">
                                  <span>Permissions</span>
                              </button>
                          </th>


                          <th scope="col" class="relative py-3.5 px-4">
                              <span class="sr-only">Edit</span>
                          </th>
                     
                      </tr>
                  </thead>
                  <tbody class="bg-white divide-y divide-gray-200 dark:divide-gray-700 dark:bg-gray-900">
                      
                      @foreach ($roles as $role)
                          
                      <tr>
                          <td class="px-4 py-4 text-sm font-medium text-gray-700 whitespace-nowrap">
                              <div class="inline-flex items-center gap-x-3">

                                  <div class="flex items-center gap-x-2">
                                      <div>
                                          <h2 class="font-medium text-gray-800 dark:text-white ">{{$loop->iteration}}</h2>
                                      </div>
                                  </div>
                              </div>
                          </td>
                          <td class="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">{{$role->name}}</td>
                          <td class="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">
                              <div class="flex items-center flex-wrap gap-2">
                                  @foreach ($role->permissions as $permission)
                                      <div class="rounded-full bg-indigo-400 px-2 py-0.5 text-indigo-200 font-semibold">{{$permission->name}}</div>
                                  @endforeach
                              </div>
                          </td>

                          
                          <td class="px-4 py-4 text-sm whitespace-nowrap">
                              <div class="flex items-center gap-x-6">

                                  <a href="{{route('roles.edit',$role->id)}}" class="block text-gray-500 transition-colors duration-200 dark:hover:text-yellow-500 dark:text-gray-300 hover:text-yellow-500 focus:outline-none">
                                      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
                                          <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
                                      </svg>
                                  </a>

                                  <form method="POST" action="{{route('roles.delete',$role->id)}}">
                                      @csrf
                                      @method('DELETE')
                                      
                                      <button type="submit" class="text-gray-500 transition-colors duration-200 dark:hover:text-red-500 dark:text-gray-300 hover:text-red-500 focus:outline-none">
                                          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
                                              <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
                                          </svg>
                                      </button>
                                  </form>
                              </div>
                          </td>
                      </tr>

                      @endforeach
                  </tbody>
              </table>
          </div>
      </div>
  </div>
</div>

Теперь, чтобы заставить это работать, мы можем прикрепить роли к пользователям с помощью метода assignRole:

$user->assignRole('Seller');

Итак, допустим, если вы создадите роль и прикрепите к ней права на создание категорий и просмотр категорий.

Теперь в пакете разрешений Spatie всем разрешениям будут назначены ворота, так что вы можете сделать это, чтобы запретить пользователям доступ к определенным областям.

Скажем, если вы хотите, чтобы только роли с правами на создание категорий могли просматривать только страницу/разделы создания категорий, вы можете сделать это следующим образом:

В Blade

@can('categories.create')
      <div class="ml-auto px-3 py-1  text-blue-600 bg-blue-100 rounded-md">
          <a href="{{route('categories.create')}}">Category Create</a>
      </div>
@endcan

//=============== OR FOR CHECKING MULTIPLE ABILITIES/PERMISSIONS ========== //

@canany(['categories.create', 'categories.delete'])
    <div class="actions">
        @can('categories.edit')
            <button>Edit</button>
        @endcan
        @can('categories.delete')
            <button>Delete</button>
        @endcan
    </div>
@endcanany

В контроллере:

public function createCategory()
{
    $this->authorize('categories.create');

    ================= OR =================

    if (Gate::allows('categories.create')) {
        // User is authorized, perform the action
    } else {
        abort(403); // Or redirect, or return an error response, depending on your needs
    }

    return view('admin.categories.create');
} 

При использовании Middleware:

Route::group(['middleware' => ['can:categories.create']], function () {
    //
});

При хранении/обновлении данных с помощью класса Form Request:

Requests/StoreCategories.php

 public function authorize(): bool
 {
    switch ($this->method()) {
        case 'POST':
            return $this->user()->can('categories.create');

            break;

        case 'PUT':
            return $this->user()->can('categories.edit');

            break;
    }

    return true;
}

Определение супер администратора:

Если вы хотите, чтобы роль "Super Admin" отвечала true на все разрешения, без необходимости назначать все эти разрешения роли, вы можете использовать метод Gate::before() в Laravel. Например:

use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->registerPolicies();
        // Implicitly grant "Super Admin" role all permissions
        // This works in the app by using gate-related functions like auth()->user->can() and @can()
        Gate::before(function ($user, $ability) {
            return $user->hasRole('Super Admin') ? true : null;
        });
    }
}

ПРИМЕЧАНИЕ: Правила Gate::before должны возвращать null, а не false, иначе это будет мешать нормальной работе политики.

Лучшие практики из пакета разрешений Spatie :

Роли лучше всего назначать только Пользователям, чтобы "группировать" людей по "наборам разрешений".

Разрешения лучше всего назначать ролям. Чем более гранулированными/детальными будут названия ваших разрешений (например, отдельные разрешения типа "просмотр документа" и "редактирование документа"), тем легче контролировать доступ в вашем приложении.

Пользователям редко следует давать "прямые" разрешения. Лучше всего, если пользователи наследуют разрешения через роли, к которым они приписаны.

При таком проектировании все разделы вашего приложения могут проверять наличие определенных разрешений, необходимых для доступа к определенным функциям или выполнения определенных действий. И таким образом вы всегда можете использовать родные директивы Laravel @can и can() везде в вашем приложении, что позволяет слою Gate в Laravel делать всю тяжелую работу.

Надеюсь, вы получили большое удовольствие от чтения этой статьи.
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.