博客

领略 Laravel 和 Filament 后台的魔力

Jun 14, 2022
Roger
Admin panel

本教程将通过创建一个后台面板的过程,带你一起领略 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.css 和 app.js 文件:

npm install
npm run dev

文档: Laravel Mix

新项目状态检查 安装了Laravel, Breeze 和 Filament 之后,我们便有了真实网页应用的骨架。

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

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

composer require doctrine/dbal

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

现在,我们开始施法吧:

php artisan make:filament-resource User --generate

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

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

现在想象一下,像这样为整个应用自动生成资源。你可以在几分钟内完成后台面板 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(),
'),

它会把所有日期作为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 文件下的用户模型。

在类声明中实现 FilamentUser 接口 引入 Filament\Models\Contracts\FilamentUser 添加 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'),
];
}
}