编程

领略 Laravel 和 Filament 后台的魔力

3130 2022-06-12 20:45:42

本教程将通过创建一个后台面板的过程,带你一起领略 Laravel 的魔力,了解 Filament 后台项目。

如果你此前没接触过 Filament,就以此来作为第一个项目吧。同时,我也会为刚刚刚接触 Laravel 和 Livewire 的信任尽量提供一些有用信息。

我们就先从复制,粘贴开始,尽快完成一个作品吧。

PHP 工具

Laravel

Laravel 是用于创建网页应用的 PHP 框架。像其他框架一样,Laravel 刚开始也有一个陡峭的学习曲线,不过一旦你入门了,你很快就能创建一个网页应用。

Livewire

Livewire 是一个让前端界面和后端代码贴合的粘合剂。你可以借此创建响应式界面,不用刷新页面就能处理数据。你只需使用你学过的 Laravel 知识就可以使用它。

Filament Admin

Filament 就像炼金术士,将以上两个元素组合,使开发者可以花费更少的时间编写后台面板。你可能会因为发现第一天居然就能做这么多东西而万分吃惊。

准备一个空的项目框架

全新安装 Laravel

首先是从无到有创建一个全新的 Laravel 项目。你可以自己选择项目名,这里使用的是 magic-admin

composer create-project laravel/laravel magic-admin

cd magic-admin

该命令会下载最新版的 Laravel。

文档: 第一个 Laravel 项目

Laravel Breeze 用于用户认证

使用以下命令添加完整的用户认证系统:

composer require laravel/breeze --dev

注意:  --dev 标志只会在开发模式下安装这个包。难道生产环境中就不需要用户认证吗?不是的。当运行以下命令安装这个包后,包里的文件都会复制到应用下面:

php artisan breeze:install

请注意,如果刚接触 Laravel: artisan 命令是 Laravel 带来一个魔法。你可以运行 php artisan list 命令查看它能做的所有事情。

Breeze 将提供完整的用户认证,也包括注册和登录表单、退出和密码重置处理。1 分钟完成!

文档: Breeze Starter Kit

安装 Filament 后台

接下来安装 Filament 后台:

composer require filament/filament

数据库安装

无论你使用的是什么样的环境,你需要为应用准备一个数据库连接。你可以在你的 Laravel 项目的根目录的 .env 文件中找到这些设置:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

迁移文件包含了创建数据库的指令。migrate 命令用来初始化数据库,创建像 users、password_reset 这样的表格等:

php artisan migrate

不生效?如果命令报错,请再检查以下 .env 文件下的数据库连接配置。

文档: 执行迁移

编译 Javascript 和 CSS

在浏览器中加载网站前,需要先将使用的 CSS 和 Javascript 进行编译。本教程将使用默认配置,此命令将会生成 app.cssapp.js 文件:

npm install
npm run dev

文档: Laravel Mix

新项目状态检查

安装了 Laravel, Breeze 和 Filament 之后,我们便有了真实网页应用的骨架。

  1. Laravel 标准的欢迎页面位于域名根目录 /
  2. Breeze 认证页面在 /login 和  /register 下(其他还有 /dashboard )
  3. Filament 将自己放在需要登录的 /admin 之后
  4. 一旦注册登录,你就可以在 /admin 看到后台面板

Filament 的魔力

第一次使用 Filament 创建后台的时候,我是真的惊呆了。先添加这个包到你的项目中:

composer require doctrine/dbal

这能让 Filament 读取你的数据库结构。

现在,我们开始施法吧:

php artisan make:filament-resource User --generate

这一 artisan 命令会基于 Breeze 创建的 User 模型生成 Filament Resource 代码。-- generate 标志会读取现有的 users 表格在后台资源中自动生成匹配的字段。

功能齐全!小手一挥,你就可以创建、读取、更新和删除用户了。magic-admin-user-screens.jpg

现在想象一下,像这样为整个应用自动生成资源。你可以在几分钟内完成后台面板 80% 的工作量。

之所以说是 80%,因为你还需要花点时间去修补和调整自动生成的资源。这是本教程余下的时间要做的事情。涉及的魔法会随着我们的深入而减少,不过仍然有一些好玩的小诀窍。

生成假用户

我们来使用 Laravel 模型工厂为 CRUD 创建一些数据吧。这一假用户生成器也会在安装 Breeze 的时候为我们创建好。

首先,进入 tinker 模式,在这里可以快速运行 PHP 代码:

php artisan tinker

这里产生一个新命令窗口,你可以输入以下命令生成 20 个随机用户:

User::factory()->count(20)->create();

你可以输入 quit 退出 tinker 命令行。

自定义Filament资源

自动生成的 Filament 资源 有两个主要的组件:table 定义了 用户展示列表,form 定义了如何创建和编辑用户。

两者都在 app/Filament/Resources/UserResource.php 文件中定义

调整用户列表

以下就是自动生成的表格:

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name'),
            Tables\Columns\TextColumn::make('email'),
            Tables\Columns\TextColumn::make('email_verified_at')
                ->dateTime(),
            Tables\Columns\TextColumn::make('created_at')
                ->dateTime(),
            Tables\Columns\TextColumn::make('updated_at')
                ->dateTime(),
         ])
         ->filters([
                //
         ]);
}

让列表可搜索

你看多简单!只要在你要搜索的字段中添加 searchable()

Tables\Columns\TextColumn::make('name')->searchable(),
Tables\Columns\TextColumn::make('email')->searchable(),

刷新页面后搜索框会出现在表格的顶部。很神奇吧!

文档: Filament Searchable 字段

修改 email 验证为布尔值

我不关心什么时候验证,不过想知道是否已经验证了。我们可以将 TextColumn 改为 BooleanColumn

// 修改前
Tables\Columns\TextColumn::make('email_verified_at')->dateTime(),

// 修改后
Tables\Columns\BooleanColumn::make('email_verified_at'),

它会把所有日期作为 true 在表格中显示为绿色的勾号。

文档: Boolean 字段

修改字段标签

Email verified at 标签不再适用。我们可以设置一个自定义标签:

Tables\Columns\BooleanColumn::make('email_verified_at')->label('Verified'),

文档: 设置标签

添加过滤器 Filter

我们可以为表格添加过滤器,来筛选展示的记录。接下来,我们添加一个复选框用户过滤显示验证过的用户。

// 在 table 方法中找到空的 filters 方法
->filters([
    //
]);

// 你需要在顶部引入 Builder 类
use Illuminate\Database\Eloquent\Builder;

// 然后就可以添加 filter 了
->filters([
   Tables\Filters\Filter::make('verified')
       ->query(fn (Builder $query): Builder => $query->whereNotNull('email_verified_at')),
]);

刷新后你会在搜索框旁边看到一个小的烟囱图标。点击就可以选择只显示验证用户了。

那么再添加一个过滤器只显示未验证用户吧。

->filters([
    Tables\Filters\Filter::make('verified')
        ->query(fn (Builder $query): Builder => $query->whereNotNull('email_verified_at')),
    Tables\Filters\Filter::make('unverified')
        ->query(fn (Builder $query): Builder => $query->whereNull('email_verified_at')),
]);

文档: 表格过滤器 Filters

修改显示日期

默认 datetime 字段会显示像 Dec 27, 2021 23:08:03 这样的日期。我们可以用标准的 PHP 日期格式将其改成可读性更强的格式。 

// 此处将显示'Dec 21, 2021',而不显示时间
Tables\Columns\TextColumn::make('created_at')
                    ->dateTime('M j, Y'),
Tables\Columns\TextColumn::make('updated_at')
                    ->dateTime('M j, Y'),

文档: Text Column 格式化

根据日期排序

如果我们从新到旧对日期进行排序会很方便。使用 sortable()

Tables\Columns\TextColumn::make('created_at')
                    ->dateTime('M j, Y')
                    ->sortable(),
Tables\Columns\TextColumn::make('updated_at')
                    ->dateTime('M j, Y')
                    ->sortable(),

现在字段名变成可点击的了。再次点击会换方向排序。请注意,随机生成的用户会再同一天创建,因此日期看起来是一样的。

文档: 字段排序

修改用户创建/编辑表单

自动生成的表单让我们可以开箱即用修改用户 name、email、password 字段。

表单字段同样可以在 app/Filament/Resources/UserResource.php 文件中找到。

public static function form(Form $form): Form
{
     return $form
         ->schema([
             Forms\Components\TextInput::make('name')
                 ->required()
                 ->maxLength(255),
             Forms\Components\TextInput::make('email')
                 ->email()
                 ->required()
                 ->maxLength(255),
             Forms\Components\TextInput::make('password')
                 ->password()
                 ->required()
                 ->maxLength(255),
         ]);
}

解决密码 Hash 问题

这个用户表单中包含了一个密码字段。不过,如果你输入一个新密码,它在数据库中会以明文保存。Laravel 哈希加密所有密码,因此尝试用未哈希的密码登录会登录失败。

注意: 我通常的方案是,在用户模型中创建一个 setPasswordAttribute() 修改器。这样密码一修改,就会被自动哈希。不过,这也要修改标准的认证过程,移除所有调用 Hash::make() 的地方,否则会双重哈希。

我们来看看 Filament 视角下是如何修改的:

// 需要引入此类
use Illuminate\Support\Facades\Hash;

Forms\Components\TextInput::make('password')
         ->password()
         ->required()
         ->maxLength(255)
         ->dehydrateStateUsing(fn ($state) => Hash::make($state)),

上述 dehydrate 指向了 数据修改的 Livewire 生命周期。dehydrateStateUsing 函数会在输入改变时调用,对密码进行哈希加密。

创建新用户时这样没有问题,不过如果你更新现有用户时,没有修改密码,就会出现双重哈希。简单的方法是,让密码字段只在创建用户页面显示。

// 需要引入
use Livewire\Component;

Forms\Components\TextInput::make('password')
          ->password()
          ->required()
          ->maxLength(255)
          ->dehydrateStateUsing(fn ($state) => Hash::make($state))
          ->visible(fn (Component $livewire): bool => $livewire instanceof Pages\CreateUser),

文档: 基于页面隐藏组件 

添加 email_verified_at 字段

这样就能手动验证用户。email_verified_at 字段要么是一个日期,要么是 null。 

你可以在表单后面插入此代码:

Forms\Components\DatePicker::make('email_verified_at'),

由于 email_verified_at 在数据库中是时间戳,我选择使用了 DatePicker 而非 DateTimePicker。这样将时间设置简化为 00:00:00。

文档: 日期时间选择器

我们并需要将此属性添加到 app/Models/User.php 中的 $fillable 数组:

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

注意: 通常这一步是通过点击操作按钮调用 user()->markEmailAsVerified() 来实现,而非使用在表单中日期字段。不过,此处目的在于展示如何添加新的字段。

收尾

我们已经展示了 Laravel 和 Filament 的魔力,它们能让你用惊人的速度搭建后台面板。我们还能继续花费数小时做些微调和自定义,不过我们暂且先这样吧。接下来还有一些事情需要处理才能上线。

管理Filament导航菜单

当只有一个用户资源时可能不那么重要,不过随着项目的增长,你可能需要在导航菜单中重新组织一下这些资源。

app/Filament/Resources/UserResource.php 文件的顶部可对其进行设置:

class UserResource extends Resource
{
    protected static ?string $model = User::class;

    protected static ?string $navigationIcon = 'heroicon-o-collection';

    ...
}

我们为用户模型调整使用合适的图标。既然已涉及此,我们不妨也同时添加一个排序号,来定义该导航菜单的显示顺序:

// 修改此项
protected static ?string $navigationIcon = 'heroicon-o-users';

// 添加此项
protected static ?int $navigationSort = 1;

这样用户菜单就会排在前面。

文档: Filament 后台导航

后台管理面板安全

Laravel 环境设置的是 local 时,所有的用户都能访问后面面板。不过如果你上线到生产(production)环境,包括你自己在内的所有人都会被锁住。要让用户获得授权,需要修改 app/Models/User.php 文件下的用户模型。

  1. 在类声明中实现 FilamentUser 接口
  2. 引入 Filament\Models\Contracts\FilamentUser
  3. 添加 canAccessFilament() 方法
<?php

namespace App\Models;

use Filament\Models\Contracts\FilamentUser;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements FilamentUser
{
    // ...

    public function canAccessFilament(): bool
    {
        return $this->email == 'admin@laravel-filament.cn' && $this->hasVerifiedEmail();
    }
}

最终结果

这样,我们就安装好了 Filament 后台面板,并自定义了一个资源来管理用户。使用其他模型重复这么一个过程,你就能完成一个功能齐全的后台面板了!

当你使用关联连接资源时,你也能发现一些很有趣的功能实现。不过,本教程会先止于此。

这里是修改后完整的 UserResource.php 文件。(未包含 app/Models/User.php 模型文件)。

<?php

namespace App\Filament\Resources;

use Closure;
use Filament\Forms;
use App\Models\User;
use Filament\Tables;
use Livewire\Component;
use Filament\Resources\Form;
use Filament\Resources\Table;
use Filament\Resources\Resource;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers;

class UserResource extends Resource
{
    protected static ?string $model = User::class;

    protected static ?string $navigationIcon = 'heroicon-o-users';

    protected static ?int $navigationSort = 1;

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\TextInput::make('name')
                    ->required()
                    ->maxLength(255),
                Forms\Components\TextInput::make('email')
                    ->email()
                    ->required()
                    ->maxLength(255),
                Forms\Components\TextInput::make('password')
                    ->password()
                    ->required()
                    ->maxLength(255)
                    ->dehydrateStateUsing(fn ($state) => Hash::make($state))
                    ->visible(fn (Component $livewire): bool => $livewire instanceof Pages\CreateUser),
                Forms\Components\DatePicker::make('email_verified_at'),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('name')->searchable(),
                Tables\Columns\TextColumn::make('email')->searchable(),
                Tables\Columns\BooleanColumn::make('email_verified_at')->label('Verified'),
                Tables\Columns\TextColumn::make('created_at')
                    ->dateTime('M j, Y')
                    ->sortable(),
                Tables\Columns\TextColumn::make('updated_at')
                    ->dateTime('M j, Y')
                    ->sortable(),
            ])
            ->filters([
                Tables\Filters\Filter::make('verified')
                 ->query(fn (Builder $query): Builder => $query->whereNotNull('email_verified_at')),
                Tables\Filters\Filter::make('unverified')
                 ->query(fn (Builder $query): Builder => $query->whereNull('email_verified_at')),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListUsers::route('/'),
            'create' => Pages\CreateUser::route('/create'),
            'edit' => Pages\EditUser::route('/{record}/edit'),
        ];
    }
}