支持Laravel.io的持续发展 →

用户级别、枚举和策略,我的天啊!

2023年1月13日 17分钟阅读

用户不平等。

我听到了。有些用户比其他用户地位高。他们可以做其他用户不能做的事情。他们能看到其他用户看不到的东西。有时他们甚至可以除下级用户!

这并不是一部奇怪的抽象恐怖电影剧本,也不是号召普通用户拿起断头台斩杀上层阶级的宣言。它只是应用程序构建的方式,实际上并没有什么不妥。事实上,我们今天就要做到这一点。对我来说,“我们要做到”意味着我会向你展示如何做,而你只要坐下来,拿一杯咖啡或茶,继续阅读。让我们开始吧!

目标

我们将建立一个应用程序,其中用户可以有3个级别:管理员、贡献者和成员。为此,我们将使用PHP 8.1中引入的新Enum类。之后,我们将设置一些路由来编辑用户级别和删除其他用户,最后我们将使用策略将我们的用户分为“允许”和“不允许”。

以下是更详细的步骤

  • 创建一个用户页面,其中列出用户
  • 将级别添加到我们的User模型中
  • 使编辑用户级别成为可能
  • 使用用户级别,通过策略允许或拒绝访问功能
  • 使删除用户成为可能

在这个过程中,我们将了解PHP的新枚举、模型转换以及使用Laravel策略进行授权。

我创建了一个github仓库,与这篇博客文章伴随,每个部分都有一个请求。这样你可以看到每个部分需要编辑什么代码。现在,我们开始吧,好吗?

显示用户

让我们开始吧。首先,创建一个新的 Laravel 项目并安装 Breeze。如果您需要帮助,请查看 Laravel 基础训练

我们将添加一个“用户”页面,其中列出所有用户。我们将以该页面为基础,并稍后添加更多功能。我们的用户页面将看起来像这样

```

// ... other routes here ...

+ Route::get('/users', [UserController::class, 'index'])
+     ->middleware(['auth'])
+     ->name('users.index');

现在,我们已经引用了 UserController,但这个类还不存在,我相信您的 IDE 已经提醒过您了。使用以下命令创建该控制器:`php artisan make:controller UserController` 并创建一个名为 `index` 的方法,该方法返回视图 `users.index`。它看起来像这样:

  class UserController extends Controller
  {
+     public function index()
+     {
+         return view('users.index');
+     }
  }

```

  use App\Http\Controllers\ProfileController;
+ use App\Http\Controllers\UserController;
  use Illuminate\Support\Facades\Route;

```

{{--resources/views/users/index.blade.php--}}
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Users') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                    {{ __("Users overview coming here!") }}
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

```

...
  <!-- Navigation Links -->
  <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
      <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
          {{ __('Dashboard') }}
      </x-nav-link>
+     <x-nav-link :href="route('users.index')" :active="request()->routeIs('users.index')">
+         {{ __('Users') }}
+     </x-nav-link>
  </div>

...

<!-- Responsive Navigation Menu -->
    <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
        <div class="pt-2 pb-3 space-y-1">
            <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                {{ __('Dashboard') }}
            </x-responsive-nav-link>
+           <x-responsive-nav-link :href="route('users.index')" :active="request()->routeIs('users.index')">
+               {{ __('Users') }}
+           </x-responsive-nav-link>
        </div>

重新加载页面,现在我们应该能验证我们的用户视图是否正常工作。应该看起来像这样

让我们快速美化一下。在 `users/index.blade.php` 中

<div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
-             <div class="p-6 text-gray-900">
-                   {{ __("Users overview coming here!") }}
-               </div>
+             <table class="w-full table-auto">
+                   <thead class="font-bold bg-gray-50 border-b-2">
+                   <tr>
+                       <td class="p-4">{{__('ID')}}</td>
+                       <td class="p-4">{{__('Name')}}</td>
+                       <td class="p-4">{{__('Email')}}</td>
+                       <td class="p-4">{{__('Level')}}</td>
+                       <td class="p-4">{{__('Actions')}}</td>
+                   </tr>
+                   </thead>
+                   <tbody>
+                       <tr class="border">
+                           <td class="p-4">1</td>
+                           <td class="p-4">Name here</td>
+                            <td class="p-4">Email here</td>
+                           <td class="p-4">Level here</td>
+                           <td class="p-4">Actions here</td>
+                       </tr>
+                   </tbody>
+               </table>
            </div>
        </div>
    </div>

现在,页面应该显示一个表格

看起来好多了,不是吗?我们现在要做的就是显示实际的用户在表格中。首先,让我们调整我们的视图,使其可以使用用户数组来显示每一行中的用户:

<tbody>
+ @foreach($users as $user)
  <tr class="border">
-     <td class="p-4">1</td>
-     <td class="p-4">Name here</td>
-     <td class="p-4">Email here</td>
+     <td class="p-4">{{$user->id}}</td>
+     <td class="p-4">{{$user->name}}</td>
+     <td class="p-4">{{$user->email}}</td>
      <td class="p-4">Level here</td>
      <td class="p-4">Actions here</td>
  </tr>
+ @endforeach
</tbody>

```

+ use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index()
    {
-       return view('users.index');
+       return view('users.index')->with('users', User::getAllUsers());
    }
}

```

public static function getAllUsers()
{
    return User::all(); // should use pagination, but ok for now
}

```

php artisan tinker // use 'sail artisan tinker' if you're using sail!

User::factory()->count(10)->create() // create 10 users using UserFactory

好了,现在我们已经有了足够的数据,我们可以进行下一步:添加用户级别。

提升用户级别

```

<?php

namespace App\Enums;

enum UserLevel: int
{
    case Member = 0;
    case Contributor = 1;
    case Administrator = 2;
}

在这种情况下,我们有 3 个不同的级别:成员、贡献者和管理员。这些对应于一个整数,这就是为什么它被称为 Backup Enum。这样,我们可以在编码时使用每个值的可读名称,但只有整数值将被保存到数据库中。太棒了!

现在,让我们打开用户模型并附加用户级别。在 `app/Models/User` 中

+ use App\Enums\UserLevel;

  ...

  protected $fillable = [
      'name',
      'email',
      'password',
+     'level',
    ];

...

  protected $casts = [
      'email_verified_at' => 'datetime',
+     'level' => UserLevel::class,
  ];

通过向 casts 数组中添加 level 属性,我们通知 Laravel 将数据库中的整数转换为具有所有功能的枚举类型。如果我们没有添加它,我们的模型中的 level 属性将是一个整数。

有很多天我会继续编码其他特性或跳转到前端的东西,而完全忘记添加迁移。今天并不是那样的日子。

使用 php artisan make:migration add_level_to_users 创建迁移并添加以下内容:

/**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function(Blueprint $table) {
+           $table->integer('level')->default(0);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function(Blueprint $table) {
+           $table->dropColumn('level');
        });
    }

我们在 users 表中添加了一个列,数据类型为 integer。默认值是 0,但没有简单的方法知道它对应的实际级别……但是正是出于这个原因我们才使用枚举,所以让我们更改那一行。

+ use App\Enums\UserLevel;

...

public function up()
{
  Schema::table('users', function (Blueprint $table) {
-   $table->integer('level')->default(0);
+   $table->integer('level')->default(UserLevel::Member->value);
  });
}

这样,我们会员级别的整数值可以改变,而不会破坏功能;用户默认仍然是会员。现在别忘了运行 php artisan migratesail artisan migrate

最后但同样重要的是,让我们更新我们的视图以反映正确的用户级别。我在没有任何原因的情况下把它弄得更花哨,并使其变成了一个徽章。在 views/users/index.blade.php

+ @php use App\Enums\UserLevel; @endphp

- <td class="p-4">Level here</td>
+ <td class="p-4">
+   <span @class([
+           'px-2 py-1 font-semibold text-sm rounded-lg',
+           'text-indigo-700 bg-indigo-100' => UserLevel::Member === $user->level,
+           'text-sky-700 bg-sky-100' => UserLevel::Contributor === $user->level,
+           'text-teal-700 bg-teal-100' => UserLevel::Administrator === $user->level,
+           ])>
+     {{__($user->level->name)}}
+   </span>
+ </td>

别忘了再次导入 UserLevel 类!

为了提高可读性,可以进行一个可能的更改,为用户模型中的每个用户级别添加方法,如 isAdmin()、isContributor() 和 isMember()。您可以在想的时候添加这些方法。

这是一个小挑战:为了检查一切看起来都很好,我们需要具有不同级别的用户。使用 Tinker,将您的用户设置为管理员,并添加 3 个新的贡献者。这可能是一个更新您关于 Eloquent Factories 知识的好机会。当您至少有一个用户的三个用户级别时,您可以查看我的彩色徽章在行动。它们看起来是这样的:

编辑用户

好了,每个用户现在都有一个级别。如果我们想通过提升他们一级来提升用户怎么办?现在我们一直在使用 Tinker,但它实际上更像是一个本地测试工具。我们可以在用户视图中添加一个 edit 页面,包含一个下拉菜单,以便可以编辑用户级别。让我们快速设置一下,通过创建一个新的控制器,添加一个 create 和一个 edit 路由,并创建一个新的视图。让我们从控制器开始

php artisan make:controller UserLevelController

您可能会惊讶地看到一个新的 UserLevelController 而不是重用 UserController。我这样做是为了让一切 'Cruddy by design'。如果您不知道我在说什么,请查看 Adam Wathan 在 Laracon 2017 的讲话

现在,打开我们新的控制器并添加以下方法

public function edit(User $user)
{
  return view('userlevels.edit')->with('user', $user);
}

public function update(Request $request, User $user)
{
  $validated = $request->validate([
      'level' => ['required', new Enum(UserLevel::class)]
      ]);

  $user->level = $validated['level'];
  $user->save();

  return redirect(route('users.index'));
}

new Enum(UserLevel::class) 验证规则将检查该级别是否可以转换为 UserLevel 的实例。有关更多信息,请参阅 Laravel 验证文档

此外,请确保正确导入 app\Enums\UserLevelIlluminate\Validation\Rules\Enum,否则您的 IDE 将会抱怨,更重要的是,代码将无法正常工作。

现在,让我们在 web.php 中添加我们的路由。

// ! don't forget to import the UserLevelController !
Route::get('/users/{user}/edit', [UserLevelController::class, 'edit'])
    ->middleware(['auth'])
    ->name('userlevels.edit');

Route::put('/users/{user}', [UserLevelController::class, 'update'])
    ->middleware(['auth'])
    ->name('userlevels.update');

最后,让我们添加我们的视图。它相当简单:我们显示用户名称,并添加一个选择输入来选择正确的用户级别。在 resources/views 中创建一个新的 userlevels 目录,并添加一个 edit.blade.php 文件,内容如下

@php use App\Enums\UserLevel; @endphp
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Change User Level') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
            <div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                <div class="max-w-xl">
                    <header>
                        <h2 class="text-lg font-medium text-gray-900">
                            {{ $user->name }}
                        </h2>

                        <p class="mt-1 text-sm text-gray-600">
                            {{ __("Update the user level of $user->name.") }}
                        </p>
                    </header>

                    <form method="post" action="{{ route('userlevels.update', $user) }}" class="mt-6 space-y-6">
                        @csrf
                        @method('put')

                        <div>
                            <x-input-label for="level" :value="__('User Level')"/>

                            <select name="level" id="level"
                                    class="w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
                                @foreach(UserLevel::cases() as $levelOption)
                                    <option value="{{$levelOption}}" @if ($levelOption == $user->level) selected="selected" @endif>
                                        {{$levelOption->name}}
                                    </option>
                                @endforeach
                            </select>

                            <x-input-error :messages="$errors->get('level')" class="mt-2"/>
                        </div>

                        <div class="flex items-center gap-4">
                            <x-primary-button>{{ __('Save') }}</x-primary-button>
                        </div>
                    </form>
                </div>
            </div>

        </div>
    </div>
</x-app-layout>

让我们试试!转到用户概览……嗯,我们最好添加一种方式可以访问我们新的路由。在 users/index.blade.php 中将会有一个小改动:

- <td class="p-4">Actions here</td>
+ <td class="p-4">
+     <a href="{{route('userlevels.edit', $user)}}" class="px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest">Edit</a>
+ </td>

<%= partial "shared/posts/cta", locals: { title: "Fly.io ❤️ Laravel", text: "Fly.io 是运行 Laravel Livewire 应用靠近用户的好方法。在 Fly 上几分钟内全球部署!", link_url: "https://fly.io/docs/laravel", link_text: "部署您的 Laravel 应用!  <span class='opacity:50'>→</span>", } %>

保护用户

目前,每个用户都可以更改其他任何用户用户的级别。这并不是我们想要的,对吧?

我们只想让一些用户能够更改其他用户的用户级别。幸运的是,我们已经在三个方面对用户进行了划分:管理员、贡献者和成员。所以,让我们说只有管理员可以更改用户级别。

为此,我们需要制定一项政策:运行 php artisan make:policy UserPolicy 创建一个政策。我们将使用控制器助手来验证我们的编辑和更新方法,例如在 UserLevelController 中。

public function edit(User $user)
    {
+       $this->authorize('updateLevel', $user);
        return view('userlevels.edit')->with('user', $user);
    }

    public function update(Request $request, User $user)
    {
+       $this->authorize('updateLevel', $user);

        $validated = $request->validate([
            'level' => ['required', new Enum(UserLevel::class)]
            ]);

        $user->level = $validated['level'];
        $user->save();

        return redirect(route('users.index'));
    }

那么,它是如何工作的?$this->authorize 检查登录的用户是否有权限更新 $userupdateLevel

幕后,Laravel 将查找可以用于 $user 模型的策略。如果找到了正确名称的策略(Model 名字 + Policy.php),则会在 app/Policiesapp/Models/Policies 文件夹中查找。在我们的例子中,这将是我们选择的 UserPolicy。这意味着 Laravel 会自动将我们的 UserPolicy 与我们想要对 User 模型执行的操作链接起来。

在 UserPolicy 中,Laravel 会用两个参数运行 updateLevel 方法:登录用户和该用户试图更改的模型。在我们的案例中,这将又是另一个用户。如果在方法中返回 true,则允许操作,否则会禁止。让我们快速测试一下我们的 updateLevel 方法,在 UserPolicy 中添加以下内容:

/**
 * @param User $loggedInUser the user that's trying to update the level of $model
 * @param User $model the user whose level is being updated by the $loggedInUser
 * @return bool
 */
public function updateLevel(User $loggedInUser, User $model)
{
  return false;
}

现在,没有人可以更改任何用户的用户级别。如果您在应用程序上尝试,您会看到403未授权的消息。这很好,我们知道我们的政策正在起作用!现在,让我们更新该方法,只允许管理员更新用户级别。

public function updateLevel(User $loggedInUser, User $model)
{
- return false;
+ // don't forget to import the UserLevel enum!
+ return UserLevel::Administrator == $loggedInUser->level;
}

好的,看起来很好!嗯,实际上并不是很好:当用户不能更改用户级别时,“编辑级别”按钮仍然会显示。这并不友好,显示了他们不允许点击的按钮。我们可以轻松地修复它,如下所示在 users/index.blade.php 中。

<td class="p-4">
+ @can('updateLevel', $user)
  <a href="{{route('userlevels.edit', $user)}}" class="px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest">Edit</a>
+ @endcan
</td>

这将只在登录用户被允许点击时显示按钮。更好。

还有一个小问题需要修复。作为一位非常出色的开发者,您已经想到了所有事情,并已经注意到了这个问题,但我会在这里重复一遍:如果唯一的管理员移除了他的管理员头衔,就不会有人再能更改用户级别了……让我们在 UserPolicy 中也更新它!

public function updateLevel(User $loggedInUser, User $model)
{
  // don't forget to import the UserLevel enum!
- return UserLevel::Administrator == $loggedInUser->level;
+ if (UserLevel::Administrator == $loggedInUser->level)
+ {
+   // when deleting an Admin, check if there will be admins left
+   if (UserLevel::Administrator == $model->level) return User::getNumberOfAdmins() > 1;
+   else return true;
+ }
+ else return false;
}

我们正在使用一个用户模型中尚不存在的新方法:getNumberOfAdmins。看起来是这样的。

// Add this in the User model:
public static function getNumberOfAdmins()
{
  // using ->count() is a much quicker database operation than using ->get() and counting in PHP.
  return User::where('level', UserLevel::Administrator)->count();
}

现在,当管理员登录时,他们可以编辑所有人的用户级别,但有一个例外:如果他们是唯一的管理员,他们不能更改自己的级别。看起来不错!

销毁用户

这是一个残酷的标题。毁掉他们!毁掉他们 所有人

无论如何...

我将这留给您。这里的目的是与级别编辑几乎相同,只有一个区别:非管理员可以删除自己。您可能对此感到惊讶,但确实非常容易。祝你好运!

我为您创建了一个包含所有删除功能的单独拉取请求,您可以在这里找到它:此处

这就是最终的成果应该的样子:

就这样,深入探讨了策略以及如何使用它们。我们还探索了一些 PHP 8.1 的功能:枚举!我真的很喜欢它们,因为它们使代码更具可读性。

像往常一样,感谢您的阅读!

Johannes

上次更新 1 年前。

driesvints, ktanaug21, claudio1994-oliveira, ceans, elkdev, hazratbilal98 点赞了这篇文章

6
喜欢这篇文章吗? 让作者知道并为他们鼓掌!

你可能还喜欢的文章

March 11th 2024

如何使用 Larastan 将你的 Laravel 应用从 0 到 9

在 Laravel 应用运行前找到错误是可能的,这要归功于 Larastan,它...

阅读文章
July 19th 2024

无需 traits 标准化 API 响应

我注意到的大多数用于 API 响应的库都是使用 traits 实现的,...

阅读文章
July 17th 2024

在 Laravel 项目中使用 Discord 通知收集反馈

如何在 Laravel 项目中创建反馈模块并在收到消息时收到 Discord 通知...

阅读文章

我们感谢这些 令人难以置信的公司 为我们提供支持

您的标志在这里?

Laravel.io

Laravel 解决方案门户、知识共享和社区建设。

© 2024 Laravel.io - 版权所有。