用户不平等。
我听到了。有些用户比其他用户地位高。他们可以做其他用户不能做的事情。他们能看到其他用户看不到的东西。有时他们甚至可以除下级用户!
这并不是一部奇怪的抽象恐怖电影剧本,也不是号召普通用户拿起断头台斩杀上层阶级的宣言。它只是应用程序构建的方式,实际上并没有什么不妥。事实上,我们今天就要做到这一点。对我来说,“我们要做到”意味着我会向你展示如何做,而你只要坐下来,拿一杯咖啡或茶,继续阅读。让我们开始吧!
目标
我们将建立一个应用程序,其中用户可以有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 migrate
或 sail 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\UserLevel
和 Illuminate\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
检查登录的用户是否有权限更新 $user
的 updateLevel
。
幕后,Laravel 将查找可以用于 $user
模型的策略。如果找到了正确名称的策略(Model 名字 + Policy.php
),则会在 app/Policies
和 app/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
driesvints, ktanaug21, claudio1994-oliveira, ceans, elkdev, hazratbilal98 点赞了这篇文章