Laravel Jetstream:使用 Spatie 权限添加 CRUD

发布于 作者

Laravel Jetstream: Add CRUD with Spatie Permission image

Laravel Jetstream 是一个入门套件,它不仅可以帮助您进行身份验证脚手架,还可以提供团队或双因素身份验证等额外功能。但我发现许多人难以在安装后自定义 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 模型
  • 创建数据库迁移
  • 创建一个 Resource TaskController 并填充 CRUD 操作
  • 向该新控制器添加路由
  • 创建 Form Request 类以进行验证

这些是与入门套件无关的操作,无论它是 Jetstream、Breeze 还是其他任何套件。

然后,我们需要将以下操作整合到 Jetstream 中

  • 将“任务”菜单项添加到 Jetstream 导航
  • 创建 CRUD 操作的 Blade 视图

因此,这两件事将取决于 Jetstream 的前端结构,我们将在下面看一下。

首先,非 Jetstream 部分

php artisan make:model Task -mcrR

这些额外的标志将生成相关文件

  • -m 将生成一个迁移类
  • -cr 将生成一个包含 Resource 方法的控制器:index、create、store、show、edit、update、destroy
  • -R 将生成两个 Form Request 类: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 批准它 后,我就没有创建该方法。

我们也运行 php artisan migrate,这样此表就会被创建。

然后,在 Model 中,我添加一个可填充字段

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 设计,您可以使用现有的仪表板文件,只需替换其中的部分。

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 中执行“文件” ->“另存为...”操作,将其保存为 resources/views/tasks/index.blade.php,然后用“任务列表”替换标题,用静态文本“即将推出”替换 <x-jet-welcome />,暂时就这样。

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 URL,我们应该看到以下内容

如果您不熟悉 x-app-layoutx-slot 之类的语法,请阅读有关 使用 Blade 组件的布局 的内容。

如果您不熟悉 __() 方法,请阅读有关 Laravel 中的翻译 的内容。

最后,我们在两个地方添加了顶部菜单,使用 Jetstream 组件 x-jet-nav-linkx-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 表格

现在,让我们用实际的表格替换“即将推出”文本。

Laravel Jetstream 的视觉设计基于 Tailwind CSS 框架,因此我们应该继续使用它来创建其他自定义页面。

您可以从各种来源获取基于 Tailwind 的表格组件,我在这里选择了 Flowbite 的免费版本。因此,我将从那里复制粘贴代码,并使用任务列表的 @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'
];
}
}

我们还为更新表单生成了 Form Request 类,因此在 app/Http/Requests/UpdateTaskRequest.php 中,我们需要添加相同代码的 authorize()rules() 方法。是否使用单独的 Form Request 类值得商榷,尤其是如果规则相同的情况下,但我个人更倾向于避免使用相同的类,因为你永远不知道规则什么时候会开始不同。

现在,编辑表单几乎与创建表单完全相同。因此,我们可以打开 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>

最后,我们需要构建删除按钮。我们将使用一个新的 Jetstream 组件,名为 x-jet-danger-button。同时,让我们将“编辑”链接替换为更像按钮的样子。

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>

视觉效果


角色和权限

对于这部分,我们将使用一个广为人知的流行包,名为 Laravel Permission,由 Spatie 开发。

正如您将看到的,其用法独立于 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;

默认情况下,任何注册用户都没有任何角色或权限。我们将创建一个单独的 seeder 类来创建具有权限的管理员。

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' => '[email protected]',
'password' => bcrypt('SecurePassword')
]);
$adminUser->assignRole('Administrator');
}
}

现在,我们需要检查谁有权限管理任务。您可以通过权限名称或角色名称进行检查,这取决于您的个人喜好。

首先,在 Blade 文件中,查看三个 @can ... @endcan 块。

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>

然后,我们还需要保护后端,因此请查看控制器中的 $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 只是一个入门套件,但之后您可以编写任何您想要的自定义代码,大多时候可以忽略 Jetstream。

也就是说,有一些有用的 Blade 组件可以重复使用,用于 UI 元素,如按钮、链接等。

您可以在 官方文档 中阅读有关 Jetstream 的更多信息。

此演示项目的代码库 在此公开

PovilasKorop photo

Laravel Daily 的课程和教程创建者

Cube

Laravel 新闻

加入 40k+ 其他开发者,绝不错过任何新技巧、教程等信息。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和即时反馈在本地和生产环境中进行调试。

Tinkerwell
No Compromises logo

No Compromises

Joel 和 Aaron,来自 No Compromises 播客的两名经验丰富的开发者,现在可以为您的 Laravel 项目雇用。 ⬧ 固定价格 7500 美元/月。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

No Compromises
Kirschbaum logo

Kirschbaum

提供创新和稳定性,确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

让您的项目充满活力,只需每月 2500 美元即可获得经验丰富的 Laravel 开发者,拥有 4-6 年的经验。获得 160 小时的专人服务和 15 天无风险试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

立即获得 Lucky - Laravel 开发的理想选择,拥有超过十年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源包,将现代无头电子商务功能的力量带入 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 职位发布平台

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,包含运行现代 SaaS 所需的所有功能。支付、漂亮的结账页面、管理面板、用户仪表盘、身份验证、现成的组件、统计、博客、文档等等。

SaaSykit: Laravel SaaS 启动套件
Rector logo

Rector

您无缝升级 Laravel、降低成本和加速创新的合作伙伴,助力企业取得成功

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务性、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 灵活、现代的数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为您的 Laravel 应用程序添加评论

阅读文章