Laravel Jetstream:使用 Spatie 权限模型实现 CRUD 权限管理
Laravel Jetstream 是一个入门工具包,它不仅可以帮助你使用 Auth 脚手架,还可以帮助你使用 Teams 或双因素身份验证等额外功能。 但是我看到很多人在安装后都在为定制 Jetstream 以及添加更多功能而苦苦挣扎。 因此,在本文中,让我们在 Jetstream 之上添加一个带有角色/权限的简单 CRUD。
安装 Jetstream
我们将创建一个新的 Laravel 项目来管理待办事项列表。 Laravel + Jetstream 有多种安装方式,我将使用 Laravel 安装程序。
你可以这样做:
laravel new project
cd project
composer require laravel/jetstream
php artisan jetstream:install livewire
注意:Jetstream 有两个选项 - Livewire 和 Inertia。 在本文中,我将使用 Livewire 堆栈,但这并不重要。 Livewire/Inertia 用于脚手架代码,但在安装 Jetstream 之后,你可以用纯 Laravel + Blade MVC 代码继续写代码,而无需任何那些额外的工具,
所以,安装说明在上面,但在这里我会介绍一个更短的安装技巧。 Laravel 安装程序允许你通过一个命令完成所有操作:
laravel new project --jet --stack=livewire
脚手架安装完成后,我们需要运行以下命令:
cd project
php artisan migrate
npm install && npm run dev
如果一切顺利,我们应该会看到默认主页,右上角有登录/注册链接。
当你点击注册时,你将跳转到注册表单。
填写完成后,用户注册成功并登陆仪表板。
如果你在屏幕上看到相同的内容,那么恭喜您,安装部分已成功完成。
任务列表:数据库结构和非 Jetstream 部分
接下来,我们需要为我们的数据库任务表创建后端结构。 在这里,我们将暂时远离 Jetstream。 你不必依赖入门工具包来编写更多代码,这是开发人员应该了解的有关 Jetstream 的主要内容之一。 你可以像通常在没有 Jetstream 的情况下那样写代码,也许只是重用它的一些组件。
此时我们需要做的是:
- 创建 Task 模型
- 创建数据库迁移
- 创建 TaskController 和 实现 CRUD 操作
- 为新的控制器添加路由
- 创建表单请求类进行验证
这些操作与入门套件无关,无论是 Jetstream、Breeze 还是其他任何工具。
然后,我们需要在 Jetstream 中加入以下操作:
- 将 「Tasks」 菜单项添加到 Jetstream 导航
- 为 CRUD 操作创建 Blade 视图
这两件事将取决于 Jestream 的前端结构,我们将在下文一一讲解。
首先,非 Jestream 部分:
php artisan make:model Task -mcrR
这些附加标志将生成相关文件:
-m
将会生成迁移类-cr
将会生成带有资源方法(index, create, store, show, edit, update, destroy)的控制器-R
将会生成两个表单请求类:StoreTaskRequest 和 UpdateTaskRequest
接下来,对于这个简单的示例,我们只用一个字段填充迁移。
database/migrations/2022_04_19_131435_create_tasks_table.php:
return new class extends Migration
{
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
我故意不显示down()
方法,因为自 2017 年 Taylor Otwell 允许不创建 down 方法 之后我就再没有创建过该方法。
随后执行 php artisan migrate
命令,这样就会创建这个表。
然后,在模型中,我添加了一个 fillable 字段:
app/Models/Task.php
class Task extends Model
{
protected $fillable = ['name'];
}
我来再来看看控制器的 CRUD 代码。
app/Http/Controllers/TaskController.php:
use App\Http\Requests\StoreTaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use App\Models\Task;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::all();
return view('tasks.index', compact('tasks'));
}
public function create()
{
return view('tasks.create');
}
public function store(StoreTaskRequest $request)
{
Task::create($request->validated());
return redirect()->route('tasks.index');
}
public function edit(Task $task)
{
return view('tasks.edit', compact('task'));
}
public function update(UpdateTaskRequest $request, Task $task)
{
$task->update($request->validated());
return redirect()->route('tasks.index');
}
public function destroy(Task $task)
{
$task->delete();
return redirect()->route('tasks.index');
}
}
最后,我们使用 auth
中间件将该控制器分配给路由。
routes/web.php:
Route::get('/', function () {
return view('welcome');
});
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified'
])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
// This is our new line
Route::resource('tasks', \App\Http\Controllers\TaskController::class);
});
这是我们第一次从 Jetstream 中看到一些东西:生成的仪表板路由带有默认中间件,如 auth:sanctum
等。 我们在这里的任务只是将我们的路由添加到该分组中。
任务列表:带有 Jetstream 布局的新页面
我们现在有了控制器但是还没有视图。我们将指定视图为 resources/views/tasks/index.blade.php
,让我们来创建该文件。
如果你希望它具有相同的 Jetstream 设计,你可以使用现有的 Dashboard 文件并替换它的部分。
resources/views/dashboard.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</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-xl sm:rounded-lg">
<x-jet-welcome />
</div>
</div>
</div>
</x-app-layout>
我们在 IDE 中执行 File -> Save as…,并将其保存为 resources/views/tasks/index.blade.php
,然后将 header 替换为 「Tasks list」和将 <x-jet-welcome />
替换为静态文本 「Coming soon」。
resources/views/tasks/index.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Tasks list') }}
</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-xl sm:rounded-lg">
Coming soon.
</div>
</div>
</div>
</x-app-layout>
现在如果我们在浏览器中输入 /tasks
,我们将会看到:
如果你还不熟悉 x-app-layout
或 x-slot
语法,请阅读 使用 Blade 组件的布局. 。
如果你还不熟悉 __()
方法,请阅读 Laravel 中的翻译.。
最后,我们在顶部的两个位置添加菜单,其中包含 Jetstream 组件「x-jet-nav-link」和「x-jet-responsive-nav-link」。我们只需复制粘贴仪表盘链接并更改路由。
resources/views/navigation-menu.blade.php:
<x-jet-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-jet-nav-link>
<x-jet-nav-link href="{{ route('tasks.index') }}" :active="request()->routeIs('tasks.*')">
{{ __('Tasks') }}
</x-jet-nav-link>
<x-jet-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-jet-responsive-nav-link>
<x-jet-responsive-nav-link href="{{ route('tasks.index') }}" :active="request()->routeIs('tasks.*')">
{{ __('Tasks') }}
</x-jet-responsive-nav-link>
任务列表:带有 Tailwind CSS 的 Jetstream 表格
现在,让我们用实际的表格替换我们的「Coming soon」文本。
Laravel Jetstream 视觉设计基于 Tailwind CSS 框架,因此我们应该继续将它用于自定义页面。
你可以通过多种来源获取基于 Tailwind 的表格的组件,我选择了免费的 Flowbite here 。我将从那里复制粘贴代码,并使用任务列表的@forelse
循环显示详细信息。
resources/views/tasks/index.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Tasks list') }}
</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-xl sm:rounded-lg">
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Task name
</th>
<th scope="col" class="px-6 py-3">
</th>
</tr>
</thead>
<tbody>
@forelse ($tasks as $task)
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ $task->name }}
</td>
<td class="px-6 py-4">
<a href="{{ route('tasks.edit', $task) }}"
class="font-medium text-blue-600 dark:text-blue-500 hover:underline">Edit</a>
</td>
</tr>
@empty
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td colspan="2"
class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ __('No tasks found') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</x-app-layout>
如果我在数据库中手动创建一些随机任务,它应该如下所示:
完整的 CRUD:按钮和表单
在表格上方,我们放置一个按钮来添加新记录。 Jetstream 为各种 UI 元素(包括按钮)提供了一组 Blade 组件。 要发布它们,我们需要运行:
php artisan vendor:publish --tag=jetstream-views
然后,在 resources/views/vendor/jetstream/components
里面会有很多元素,它们是通过 Jetstream 的服务提供者自动启用的。
如果我们打开resources/views/vendor/jetstream/components/button.blade.php
,我们会看到:
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center ...']) }}>
{{ $slot }}
</button>
items-center
后面列出了很多其他 Tailwind 类,这正是我们需要的,但只是以链接的形式,而不是按钮。 所以我只是将所有这些类复制粘贴到我将创建的新组件中:
resources/views/components/link.blade.php:
<a {{ $attributes->merge(['class' => 'inline-flex items-center ...']) }}>
{{ $slot }}
</a>
然后,在 resources/views/tasks/index.blade
中我们可以这样做:
<div class="relative ...">
<x-link href="{{ route('tasks.create') }}" class="m-4">Add new task</x-link>
<table class="...">
我们添加了 <x-link>
并将其样式化为按钮,并添加了 m-4
类作为外边距。
请注意,你需要再次运行 npm run dev
,或在后台使用 npm run watch
,Tailwind 将重新编译 Blade 文件中使用的所有类,例如我们的示例中的 m-4
。
结果如下:
当我们点击该链接时,将会进入 /tasks/create
URL,它将会调用 TaskController@create
方法并且加载 tasks/create.blade.php
视图。
对于该表单,我将重新使用 Jetstream 中注册页面中的 HTML 代码和类,该页面位于resources/views/auth/register.blade.php
。 经过一些复制粘贴后,这是我们在新创建的文件中的新表单。
resources/views/tasks/create.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Add New Task') }}
</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-xl sm:rounded-lg">
<div class="relative overflow-x-auto shadow-md sm:rounded-lg px-4 py-4">
<x-jet-validation-errors class="mb-4" />
<form method="POST" action="{{ route('tasks.store') }}">
@csrf
<div>
<x-jet-label for="name" value="{{ __('Name') }}" />
<x-jet-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
</div>
<div class="flex mt-4">
<x-jet-button>
{{ __('Save Task') }}
</x-jet-button>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
如你所见,我们使用相同的布局,只是为填充添加了一些 px-4 py-4 CSS 类。
此外,重要的是要注意两个 Jetstream 组件:
x-jet-validation-errors
将列出会话中的所有表单验证错误x-jet-button
与之前看到的提交按钮相同,作为我们的x-link
组件的 「灵感」
展示效果是这样的:
保存数据应该是可行的,因为我们已经为此创建了控制器代码,记得吗? 我们需要添加验证规则。
app/Http/Requests/StoreTaskRequest.php:
class StoreTaskRequest extends FormRequest
{
public function authorize()
{
// default is "false", we need to change to "true"
return true;
}
public function rules()
{
return [
'name' => 'required'
];
}
}
我们还为更新表单生成了表单请求类,因此在 app/Http/Requests/UpdateTaskRequest.php
中,我们需要为 authorize()
和 rules()
方法添加相同的代码。 是否使用单独的表单请求类是有争议的,尤其是在规则相同的情况下,但我个人的偏好是避免使用相同的类,因为你永远不知道规则什么时候会开始不同。
编辑表单几乎与创建表单相同。 因此,我们可以打开create.blade.php
,执行File -> Save as...
并以最少的更改保存它:
resources/views/tasks/edit.blade.php
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit Task') }}
</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-xl sm:rounded-lg">
<div class="relative overflow-x-auto shadow-md sm:rounded-lg px-4 py-4">
<x-jet-validation-errors class="mb-4" />
<form method="POST" action="{{ route('tasks.update', $task) }}">
@csrf
@method('PUT')
<div>
<x-jet-label for="name" value="{{ __('Name') }}" />
<x-jet-input id="name" class="block mt-1 w-full" type="text" name="name" :value="$task->name" required autofocus autocomplete="name" />
</div>
<div class="flex mt-4">
<x-jet-button>
{{ __('Save Task') }}
</x-jet-button>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
最后,我们需要构建删除按钮。我们将使用一个名为 x-jet-danger-button
的新 Jetstream 组件。有了它,让我们也替换「Edit」链接,使其看起来更像一个按钮。
resources/views/tasks/index.blade.php:
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ $task->name }}
</td>
<td class="px-6 py-4">
<x-link href="{{ route('tasks.edit', $task) }}">Edit</x-link>
<form method="POST" action="{{ route('tasks.destroy', $task) }}" class="inline-block">
@csrf
@method('DELETE')
<x-jet-danger-button
type="submit"
onclick="return confirm('Are you sure?')">Delete</x-jet-danger-button>
</form>
</td>
结果如下
角色与权限
对于这一部分,我们将使用著名的、流行的、由 Spatie 实现的 Laravel Permission 软件包。
正如你将看到的,它的用法独立于 Jetstream,它与你在任何其他 Laravel 项目中使用它是一样的。换句话说,Jetstream 为你提供登录/注册表单和配置文件管理的基本身份验证,但无论你在上面添加什么,大部分都不「关心」Jetstream。
举个简单的例子,我们允许任何用户查看任务,但只有管理员用户可以添加/编辑/删除任务。
因此,正如 文档 所说,我们运行:
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
然后,我们需要在 User 模型中启用权限。
app/Models/User.php:
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
默认情况下,任何注册用户都不会拥有任何角色或权限。 我们将创建一个单独的 Seedr 类,以创建具有权限的管理员。
php artisan make:seeder AdminUserSeeder
database/seeders/AdminUserSeeder.php:
use App\Models\User;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class AdminUserSeeder extends Seeder
{
public function run()
{
$adminRole = Role::create(['name' => 'Administrator']);
$permission = Permission::create(['name' => 'manage tasks']);
$permission->assignRole($adminRole);
$adminUser = User::factory()->create([
'email' => 'admin@admin.com',
'password' => bcrypt('SecurePassword')
]);
$adminUser->assignRole('Administrator');
}
}
现在,我们需要检查谁有权管理任务。 你可以通过权限名称或角色名称检查它,这是你的个人喜好。
首先,在 Blade 文件中,查看三个 @can
… @endcan
代码块。
blade 文件如下
resources/views/tasks/index.blade.php:
@can('manage tasks')
<x-link href="{{ route('tasks.create') }}" class="m-4">Add new task</x-link>
@endcan
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Task name
</th>
@can('manage tasks')
<th scope="col" class="px-6 py-3">
</th>
@endcan
</tr>
</thead>
<tbody>
@forelse ($tasks as $task)
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ $task->name }}
</td>
@can('manage tasks')
<td class="px-6 py-4">
<x-link href="{{ route('tasks.edit', $task) }}">Edit</x-link>
<form method="POST" action="{{ route('tasks.destroy', $task) }}" class="inline-block">
@csrf
@method('DELETE')
<x-jet-danger-button
type="submit"
onclick="return confirm('Are you sure?')">Delete</x-jet-danger-button>
</form>
</td>
@endcan
</tr>
@empty
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td colspan="2"
class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ __('No tasks found') }}
</td>
</tr>
@endforelse
</tbody>
</table>
然后,我们还需要保护后端路由,因此请查看 Controller 中的 $this->authorize()
的登录检查。
app/Http/Controllers/TaskController.php:
class TaskController extends Controller
{
public function index()
{
$tasks = Task::all();
return view('tasks.index', compact('tasks'));
}
public function create()
{
$this->authorize('manage tasks');
return view('tasks.create');
}
public function store(StoreTaskRequest $request)
{
$this->authorize('manage tasks');
Task::create($request->validated());
return redirect()->route('tasks.index');
}
public function edit(Task $task)
{
$this->authorize('manage tasks');
return view('tasks.edit', compact('task'));
}
public function update(UpdateTaskRequest $request, Task $task)
{
$this->authorize('manage tasks');
$task->update($request->validated());
return redirect()->route('tasks.index');
}
public function destroy(Task $task)
{
$this->authorize('manage tasks');
$task->delete();
return redirect()->route('tasks.index');
}
}
结论
就是这样,我们已经在 Jetstream 之上构建了具有角色/权限的 CRUD。 我的总体目标是向你展示 Jetstream 只是 starter 套件,但随后你可以编写任何你想要的自定义代码,大多数情况下可以忽略 Jetstream。
也就是说,对于按钮、链接等 UI 元素,你可以重复使用有用的 Blade 组件。
查看更多关于 Jetstream in the official documentation.
此演示项目的代码仓库地址是 public here.
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。