Laravel Jetstream:使用 Spatie 权限模型实现 CRUD 权限管理

Laravel

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-layoutx-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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://laravel-news.com/jetstream-spati...

译文地址:https://learnku.com/laravel/t/67745

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!