阅读时间:1 分钟
0 字

资源控制器

资源控制器是 DuxLite 的核心特性,通过 #[Resource] 注解和 Resources 基类,快速实现标准化的 CRUD 操作。

应用注册

在使用资源控制器之前,必须先在应用中注册资源:

php
// App.php
use Core\App\AppExtend;
use Core\Bootstrap;
use Core\Resources\Resource;
use Core\Auth\AuthMiddleware;
use Core\Permission\PermissionMiddleware;

class App extends AppExtend
{
    public function init(Bootstrap $app): void
    {
        // 初始化资源
        \Core\App::resource()->set(
            "admin",
            (new Resource(
                'admin',
                '/admin'
            ))->addAuthMiddleware(
                new AuthMiddleware("admin"),
                new PermissionMiddleware("admin", \App\Models\User::class)
            )
        );
    }
}

配置文件注册:

toml
# config/app.toml
registers = [
    "App\\App"
]

基本概念

设计理念

  • 标准化 CRUD:自动提供完整的增删改查操作
  • 注解驱动:使用 #[Resource] 注解自动生成功能
  • 事件钩子:支持操作前后的业务扩展
  • 数据验证:集成验证和格式化机制

基础用法

php
use Core\Resources\Attribute\Resource;

#[Resource(
    app: 'admin',
    route: '/admin/users',
    name: 'users'
)]
class UserController extends Resources
{
    protected string $model = User::class;
}

查询功能

列表查询

使用 queryMany() 方法定制列表查询:

php
class UserController extends Resources
{
    /**
     * 列表查询定制
     */
    public function queryMany(Builder $query, ServerRequestInterface $request, array $args): void
    {
        $params = $request->getQueryParams();

        // 搜索功能
        if (!empty($params['keyword'])) {
            $query->where(function($q) use ($params) {
                $q->where('name', 'like', "%{$params['keyword']}%")
                  ->orWhere('email', 'like', "%{$params['keyword']}%");
            });
        }

        // 状态筛选
        if (isset($params['status']) && $params['status'] !== '') {
            $query->where('status', $params['status']);
        }

        // 时间范围筛选
        if (!empty($params['start_date'])) {
            $query->where('created_at', '>=', $params['start_date']);
        }
        
        if (!empty($params['end_date'])) {
            $query->where('created_at', '<=', $params['end_date']);
        }
    }
}

详情查询

使用 queryOne() 方法定制详情查询:

php
class UserController extends Resources
{
    /**
     * 详情查询定制
     */
    public function queryOne(Builder $query, ServerRequestInterface $request, array $args): void
    {
        // 预加载关联数据
        $query->with(['profile', 'roles.permissions']);
    }
}

通用查询条件

使用 query() 方法添加通用条件:

php
class UserController extends Resources
{
    /**
     * 通用查询条件(适用于所有操作)
     */
    public function query(Builder $query): void
    {
        // 只查询未删除的记录
        $query->where('deleted_at', null);
        
        // 按创建时间倒序
        $query->orderBy('created_at', 'desc');
    }
}

数据转换

使用 transform() 方法格式化输出数据:

php
class UserController extends Resources
{
    /**
     * 数据转换 - 输出格式化
     */
    public function transform(object $item): array
    {
        return [
            'id' => $item->id,
            'name' => $item->name,
            'email' => $item->email,
            'status' => $item->status,
            'status_text' => $item->status ? '正常' : '禁用',
            'avatar' => $item->avatar ? url('/storage/' . $item->avatar) : null,
            'created_at' => $item->created_at->format('Y-m-d H:i:s'),
            'updated_at' => $item->updated_at->format('Y-m-d H:i:s'),
        ];
    }
}

创建功能

数据验证

使用 validator() 方法定义验证规则。更多验证规则和使用方法请参考:数据验证

php
class UserController extends Resources
{
    /**
     * 数据验证规则
     */
    public function validator(array $data, ServerRequestInterface $request, array $args): array
    {
        $action = $request->getAttribute('action', '');

        $rules = [
            'name' => [
                ['required', '姓名不能为空'],
                ['lengthMin', 2, '姓名至少2个字符'],
                ['lengthMax', 50, '姓名不能超过50个字符']
            ],
            'email' => [
                ['required', '邮箱不能为空'],
                ['email', '邮箱格式无效']
            ]
        ];

        // 创建时需要密码
        if ($action === 'create') {
            $rules['password'] = [
                ['required', '密码不能为空'],
                ['lengthMin', 8, '密码至少8个字符']
            ];
        }

        return $rules;
    }
}

数据格式化

使用 format() 方法格式化入库数据:

php
class UserController extends Resources
{
    /**
     * 数据格式化
     */
    public function format(Data $data, ServerRequestInterface $request, array $args): array
    {
        $formatted = [
            'name' => $data->name,
            'email' => strtolower($data->email),
            'status' => (int) ($data->status ?? 1),
        ];

        // 密码加密
        if (isset($data->password)) {
            $formatted['password'] = password_hash($data->password, PASSWORD_DEFAULT);
        }

        return $formatted;
    }
}

创建钩子

使用创建前后钩子处理业务逻辑:

php
class UserController extends Resources
{
    /**
     * 创建前钩子
     */
    public function createBefore(Data $data, mixed $model): void
    {
        // 检查邮箱唯一性
        if (User::where('email', $data->email)->exists()) {
            throw new \Core\Handlers\ExceptionBusiness('邮箱已存在');
        }

        // 设置创建者
        $auth = \Core\App::auth();
        $model->created_by = $auth['id'];
    }

    /**
     * 创建后钩子
     */
    public function createAfter(Data $data, mixed $model): void
    {
        // 创建用户配置文件
        $model->profile()->create([
            'nickname' => $data->name,
            'avatar' => 'default.png'
        ]);

        // 发送欢迎邮件
        $this->sendWelcomeEmail($model);
    }

    private function sendWelcomeEmail($user): void
    {
        // 发送邮件逻辑
    }
}

修改功能

编辑钩子

使用编辑前后钩子处理更新逻辑:

php
class UserController extends Resources
{
    /**
     * 编辑前钩子
     */
    public function editBefore(Data $data, mixed $model): void
    {
        // 权限检查
        $auth = \Core\App::auth();
        if ($model->id !== $auth['id'] && !$this->isAdmin($auth)) {
            throw new \Core\Handlers\ExceptionBusiness('无权限编辑此用户', 403);
        }

        // 邮箱唯一性检查(排除自己)
        if (isset($data->email)) {
            $exists = User::where('email', $data->email)
                ->where('id', '!=', $model->id)
                ->exists();
            if ($exists) {
                throw new \Core\Handlers\ExceptionBusiness('邮箱已被其他用户使用');
            }
        }
    }

    /**
     * 编辑后钩子
     */
    public function editAfter(Data $data, mixed $model): void
    {
        // 记录操作日志
        \Core\App::log()->info('用户信息已更新', [
            'user_id' => $model->id,
            'changes' => $model->getChanges()
        ]);

        // 清除相关缓存
        \Core\App::cache()->delete("user_{$model->id}");
    }

    private function isAdmin(array $auth): bool
    {
        return ($auth['role'] ?? '') === 'admin';
    }
}

删除功能

删除钩子

使用删除前后钩子处理删除逻辑:

php
class UserController extends Resources
{
    /**
     * 删除前钩子
     */
    public function delBefore(mixed $model): void
    {
        // 检查是否可以删除
        if ($model->posts()->exists()) {
            throw new \Core\Handlers\ExceptionBusiness('该用户还有关联文章,无法删除');
        }

        // 不能删除管理员
        if ($model->hasRole('admin')) {
            throw new \Core\Handlers\ExceptionBusiness('不能删除管理员用户');
        }
    }

    /**
     * 删除后钩子
     */
    public function delAfter(mixed $model): void
    {
        // 清理用户相关数据
        $model->profile()->delete();
        $model->sessions()->delete();

        // 删除用户文件
        if ($model->avatar) {
            \Core\App::storage()->delete($model->avatar);
        }
    }
}

软删除功能

软删除钩子

使用软删除前后钩子处理软删除逻辑:

php
class UserController extends Resources
{
    /**
     * 软删除前钩子
     */
    public function trashBefore(mixed $model): void
    {
        // 检查是否可以彻底删除
        if ($model->orders()->exists()) {
            throw new \Core\Handlers\ExceptionBusiness('该用户还有关联订单,无法彻底删除');
        }
    }

    /**
     * 软删除后钩子
     */
    public function trashAfter(mixed $model): void
    {
        // 记录彻底删除日志
        \Core\App::log()->info('用户已被彻底删除', [
            'user_id' => $model->id
        ]);
    }

    /**
     * 恢复前钩子
     */
    public function restoreBefore(mixed $model): void
    {
        // 检查是否可以恢复
        if (User::where('email', $model->email)->where('id', '!=', $model->id)->exists()) {
            throw new \Core\Handlers\ExceptionBusiness('邮箱已被其他用户使用,无法恢复');
        }
    }

    /**
     * 恢复后钩子
     */
    public function restoreAfter(mixed $model): void
    {
        // 发送恢复通知
        \Core\App::log()->info('用户已被恢复', [
            'user_id' => $model->id
        ]);
    }
}

批量删除功能

批量删除钩子

使用批量删除前后钩子处理批量删除逻辑:

php
class UserController extends Resources
{
    /**
     * 批量删除前钩子
     */
    public function delManyBefore(array $models): void
    {
        // 检查是否可以批量删除
        foreach ($models as $model) {
            if ($model->posts()->exists()) {
                throw new \Core\Handlers\ExceptionBusiness("用户 {$model->name} 还有关联文章,无法删除");
            }
        }
    }

    /**
     * 批量删除后钩子
     */
    public function delManyAfter(array $models): void
    {
        // 记录批量删除日志
        $userIds = array_map(fn($model) => $model->id, $models);
        \Core\App::log()->info('用户批量删除成功', [
            'user_ids' => $userIds
        ]);
    }
}

扩展方法

自定义操作

使用 #[Action] 注解添加自定义操作:

php
use Core\Resources\Attribute\Action;

class UserController extends Resources
{
    /**
     * 导出用户数据
     */
    #[Action(methods: 'GET', route: '/export', name: 'export')]
    public function export(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        $users = User::all();
        $csvData = $this->generateCsv($users);

        return send($response, '导出成功', [
            'download_url' => '/downloads/users.csv'
        ]);
    }

    /**
     * 批量激活用户
     */
    #[Action(methods: 'PUT', route: '/batch-activate', name: 'batchActivate')]
    public function batchActivate(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        $data = $request->getParsedBody();
        $userIds = $data['user_ids'] ?? [];

        if (empty($userIds)) {
            throw new \Core\Handlers\ExceptionBusiness('请选择要激活的用户');
        }

        $count = User::whereIn('id', $userIds)->update(['status' => 1]);

        return send($response, '批量激活成功', [
            'affected_count' => $count
        ]);
    }

    /**
     * 修改用户密码
     */
    #[Action(methods: 'PUT', route: '/{id}/password', name: 'changePassword')]
    public function changePassword(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        $userId = (int) $args['id'];
        $data = $request->getParsedBody();

        $user = User::find($userId);
        if (!$user) {
            throw new \Core\Handlers\ExceptionNotFound('用户不存在');
        }

        // 验证当前密码
        if (!password_verify($data['current_password'], $user->password)) {
            throw new \Core\Handlers\ExceptionBusiness('当前密码错误');
        }

        // 更新密码
        $user->update([
            'password' => password_hash($data['new_password'], PASSWORD_DEFAULT)
        ]);

        return send($response, '密码修改成功');
    }

    private function generateCsv($users): string
    {
        // CSV 生成逻辑
        return 'csv_content';
    }
}

权限控制

在自定义操作中控制权限:

php
class UserController extends Resources
{
    // 需要权限检查
    #[Action(methods: 'GET', route: '/statistics', name: 'statistics', can: true)]
    public function getStatistics(...): ResponseInterface
    {
        // 需要 admin.users.statistics 权限
        return send($response, '统计数据', $stats);
    }

    // 跳过权限检查
    #[Action(methods: 'GET', route: '/public-info', name: 'publicInfo', can: false)]
    public function getPublicInfo(...): ResponseInterface
    {
        // 不需要权限,但仍需要认证
        return send($response, '公开信息', $info);
    }

    // 跳过认证和权限
    #[Action(methods: 'GET', route: '/status', name: 'status', auth: false, can: false)]
    public function getStatus(...): ResponseInterface
    {
        // 完全公开的接口
        return send($response, '服务状态正常');
    }
}

完整示例

php
use Core\Resources\Attribute\Resource;
use Core\Resources\Attribute\Action;

#[Resource(
    app: 'admin',
    route: '/admin/products',
    name: 'products'
)]
class ProductController extends Resources
{
    protected string $model = Product::class;

    /**
     * 验证规则
     */
    public function validator(array $data, ServerRequestInterface $request, array $args): array
    {
        return [
            'name' => [
                ['required', '产品名称不能为空'],
                ['lengthMin', 2, '产品名称至少2个字符']
            ],
            'price' => [
                ['required', '价格不能为空'],
                ['numeric', '价格必须是数字'],
                ['min', 0.01, '价格不能小于0.01']
            ],
            'category_id' => [
                ['required', '分类不能为空'],
                ['integer', '分类ID必须是整数']
            ]
        ];
    }

    /**
     * 数据转换
     */
    public function transform(object $item): array
    {
        return [
            'id' => $item->id,
            'name' => $item->name,
            'price' => $item->price,
            'price_formatted' => '¥' . number_format($item->price, 2),
            'stock' => $item->stock,
            'status' => $item->status,
            'status_text' => $item->status ? '上架' : '下架',
            'category' => [
                'id' => $item->category->id,
                'name' => $item->category->name
            ],
            'created_at' => $item->created_at->format('Y-m-d H:i:s')
        ];
    }

    /**
     * 列表查询
     */
    public function queryMany(Builder $query, ServerRequestInterface $request, array $args): void
    {
        $params = $request->getQueryParams();

        // 分类筛选
        if (!empty($params['category_id'])) {
            $query->where('category_id', $params['category_id']);
        }

        // 状态筛选
        if (isset($params['status']) && $params['status'] !== '') {
            $query->where('status', $params['status']);
        }

        // 预加载分类
        $query->with('category');
    }

    /**
     * 创建后处理
     */
    public function createAfter(Data $data, mixed $model): void
    {
        // 清除分类缓存
        \Core\App::cache()->delete("category_{$model->category_id}_products");
    }

    /**
     * 批量上架
     */
    #[Action(methods: 'POST', route: '/batch-publish', name: 'batchPublish')]
    public function batchPublish(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        $data = $request->getParsedBody();
        $ids = $data['ids'] ?? [];

        if (empty($ids)) {
            throw new \Core\Handlers\ExceptionBusiness('请选择要上架的产品');
        }

        $count = Product::whereIn('id', $ids)->update(['status' => 1]);

        return send($response, '批量上架成功', [
            'updated_count' => $count
        ]);
    }
}

通过资源控制器,可以快速构建功能完整的 CRUD API,同时通过钩子方法和自定义操作实现复杂的业务逻辑。