Skip to content

资源控制器

资源控制器是 DuxLite 框架的核心特性,通过 #[Resource] 注解和 Resources 基类,快速实现标准化的 RESTful API。资源控制器必须配合资源路由使用,提供完整的 CRUD 操作、自动验证、权限控制和事件钩子。

设计理念

DuxLite 资源控制器采用约定优于配置的 RESTful API 开发设计:

  • 标准化 CRUD:自动提供完整的增删改查操作
  • 路由注解驱动:使用 #[Resource] 注解自动生成路由
  • 自动验证:集成数据验证和格式化机制
  • 权限自动集成:自动配置权限检查中间件
  • 事件钩子系统:支持操作前后的业务扩展
  • 数据转换:灵活的输入输出数据转换机制

核心特性

  • Resource 注解:自动生成 RESTful 路由和权限
  • Action 注解:扩展自定义操作
  • 数据验证:自动验证和格式化输入数据
  • 数据转换:灵活的数据输入输出转换
  • 权限控制:自动集成权限检查
  • 事件钩子:支持创建、编辑、删除等操作的钩子
  • 软删除支持:内置软删除功能

Resource 注解详解

基础用法

php
use Core\Resources\Action\Resources;
use Core\Resources\Attribute\Resource;
use App\Models\User;

#[Resource(
    app: 'admin',           // 必需:路由应用名
    route: '/admin/users',  // 必需:资源路由前缀
    name: 'users',          // 必需:资源名称,用于生成路由名和权限
)]
class UserController extends Resources
{
    protected string $model = User::class;
    protected string $key = 'id';
    protected string $label = '用户管理';
}

自动生成的路由

上述配置会自动生成以下标准 RESTful 路由:

HTTP方法路由路径控制器方法路由名称功能描述
GET/admin/usersmany()admin.users.list获取用户列表
GET/admin/users/{id}one()admin.users.show获取单个用户
POST/admin/userscreate()admin.users.create创建新用户
PUT/admin/users/{id}edit()admin.users.edit完整更新用户
PATCH/admin/users/{id}store()admin.users.store部分更新用户
DELETE/admin/users/{id}del()admin.users.delete删除单个用户
DELETE/admin/usersdelMany()admin.users.deleteMany批量删除用户

Resource 注解参数

php
#[Resource(
    app: 'admin',                              // 路由应用名
    route: '/admin/users',                     // 资源路由前缀
    name: 'users',                            // 资源名称
    actions: ['list', 'show', 'create'],     // 可选:限制启用的操作
    middleware: [CustomMiddleware::class],    // 可选:自定义中间件
    softDelete: true                          // 可选:启用软删除功能
)]
class UserController extends Resources { }

参数详解:

参数类型必需说明
appstring路由应用名,必须已在应用中注册
routestring资源路由前缀,所有操作都会基于此路径
namestring资源名称,用于生成路由名和权限标识
actionsarray|false启用的操作列表,默认全部,false 禁用所有
middlewarearray自定义中间件列表
softDeletebool是否启用软删除,默认 false

限制操作范围

php
// 只读资源:只允许查询操作
#[Resource(
    app: 'api',
    route: '/api/reports',
    name: 'reports',
    actions: ['list', 'show']  // 只生成查询相关路由
)]
class ReportController extends Resources
{
    // 自动生成:
    // GET /api/reports     -> api.reports.list
    // GET /api/reports/{id} -> api.reports.show
    // 不生成增删改操作
}

// 禁用所有默认操作,只使用自定义操作
#[Resource(
    app: 'api',
    route: '/api/tools',
    name: 'tools',
    actions: false  // 禁用所有默认操作
)]
class ToolController extends Resources
{
    // 不生成任何默认路由,只能使用 Action 注解自定义操作
}

软删除支持

php
#[Resource(
    app: 'admin',
    route: '/admin/posts',
    name: 'posts',
    softDelete: true  // 启用软删除
)]
class PostController extends Resources
{
    protected string $model = Post::class;
}

启用软删除后会额外生成:

HTTP方法路由路径控制器方法路由名称功能描述
DELETE/admin/posts/{id}/trashtrash()admin.posts.trash彻底删除
PUT/admin/posts/{id}/restorerestore()admin.posts.restore恢复删除

Action 注解详解

基础用法

使用 #[Action] 注解为资源控制器添加自定义操作:

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

    #[Action(
        methods: ['GET'],           // 必需:HTTP方法
        route: '/export',          // 必需:操作路径
        name: 'export',           // 可选:操作名称
        auth: true,               // 可选:是否需要认证
        can: true                 // 可选:是否需要权限检查
    )]
    public function export(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // 导出用户数据
        // 完整路径:/admin/users/export
        // 路由名称:admin.users.export
        // 权限检查:admin.users.export

        $users = User::all();
        $csvData = $this->generateCsv($users);

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

    #[Action(['POST'], '/import', name: 'import')]
    public function import(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // 导入用户数据
        // 完整路径:/admin/users/import
        // 路由名称:admin.users.import

        $uploadedFile = $request->getUploadedFiles()['file'];
        $result = $this->processImport($uploadedFile);

        return send($response, '导入成功', $result);
    }

    #[Action(['POST'], '/reset-password/{id}', name: 'resetPassword')]
    public function resetPassword(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // 重置用户密码
        // 完整路径:/admin/users/reset-password/{id}
        // 路由名称:admin.users.resetPassword

        $userId = $args['id'];
        $user = User::find($userId);

        if (!$user) {
            throw new ExceptionBusiness('用户不存在', 404);
        }

        $newPassword = $this->generateRandomPassword();
        $user->password = password_hash($newPassword, PASSWORD_DEFAULT);
        $user->save();

        return send($response, '密码重置成功', [
            'new_password' => $newPassword
        ]);
    }
}

Action 注解参数

参数类型必需说明
methodsarray|stringHTTP方法,如 ['GET']'POST'
routestring操作路径,相对于资源路径
namestring操作名称,默认使用方法名
authbool是否需要认证,默认 true
canbool是否需要权限检查,默认 true

高级 Action 示例

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

    // 支持多种 HTTP 方法
    #[Action(['GET', 'POST'], '/search', name: 'search')]
    public function search(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        $method = $request->getMethod();

        if ($method === 'GET') {
            // 显示搜索表单
            return send($response, '搜索表单', [
                'form_fields' => $this->getSearchFields()
            ]);
        } else {
            // 处理搜索请求
            $criteria = $request->getParsedBody();
            $results = $this->performSearch($criteria);
            return send($response, '搜索结果', $results);
        }
    }

    // 无需认证的公开操作
    #[Action(['GET'], '/categories', name: 'categories', auth: false)]
    public function getCategories(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // 获取产品分类(公开接口)
        $categories = Category::where('status', 1)->get();
        return send($response, '分类列表', $categories->toArray());
    }

    // 需要认证但无需权限检查
    #[Action(['POST'], '/favorite/{id}', name: 'favorite', can: false)]
    public function favorite(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // 用户收藏产品(只需登录,无需特殊权限)
        $productId = $args['id'];
        $auth = $request->getAttribute('auth');
        $userId = $auth['id'];

        UserFavorite::updateOrCreate([
            'user_id' => $userId,
            'product_id' => $productId
        ]);

        return send($response, '收藏成功');
    }

    // 带参数的复杂路径
    #[Action(['PUT'], '/category/{categoryId}/products/{id}/status', name: 'updateStatus')]
    public function updateProductStatus(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // 更新特定分类下产品的状态
        // 路径:/api/products/category/{categoryId}/products/{id}/status

        $categoryId = $args['categoryId'];
        $productId = $args['id'];
        $data = $request->getParsedBody();

        $product = Product::where('id', $productId)
            ->where('category_id', $categoryId)
            ->firstOrFail();

        $product->status = $data['status'];
        $product->save();

        return send($response, '状态更新成功', $product->toArray());
    }
}

数据验证和格式化

验证规则定义

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

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

        $rules = [
            'username' => [
                ['required', '用户名不能为空'],
                ['lengthMin', 3, '用户名至少3个字符'],
                ['lengthMax', 20, '用户名不能超过20个字符'],
                ['regex', '/^[a-zA-Z0-9_]+$/', '用户名只能包含字母、数字和下划线']
            ],
            'email' => [
                ['required', '邮箱不能为空'],
                ['email', '邮箱格式无效']
            ]
        ];

        // 创建时需要密码
        if ($action === 'create') {
            $rules['password'] = [
                ['required', '密码不能为空'],
                ['lengthMin', 8, '密码至少8个字符'],
                ['regex', '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/', '密码必须包含大小写字母和数字']
            ];
        }

        // 更新时密码可选
        if ($action === 'edit' && !empty($data['password'])) {
            $rules['password'] = [
                ['lengthMin', 8, '密码至少8个字符'],
                ['regex', '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/', '密码必须包含大小写字母和数字']
            ];
        }

        return $rules;
    }

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

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

        // 处理个人资料JSON字段
        if (isset($data->profile)) {
            $formatted['profile'] = json_encode($data->profile);
        }

        return $formatted;
    }
}

数据转换

php
class UserController extends Resources
{
    /**
     * 数据转换 - 输出格式化
     */
    public function transform(object $item): array
    {
        return [
            'id' => $item->id,
            'username' => $item->username,
            'email' => $item->email,
            'phone' => $item->phone,
            'status' => $item->status,
            'status_text' => $this->getStatusText($item->status),
            'avatar' => $item->avatar ? url('/storage/' . $item->avatar) : null,
            'profile' => $item->profile ? json_decode($item->profile, true) : null,
            'role' => $item->role->name ?? null,
            'permissions' => $item->permissions->pluck('name')->toArray(),
            'last_login_at' => $item->last_login_at?->format('Y-m-d H:i:s'),
            'created_at' => $item->created_at->format('Y-m-d H:i:s'),
            'updated_at' => $item->updated_at->format('Y-m-d H:i:s'),
        ];
    }

    private function getStatusText(int $status): string
    {
        return match($status) {
            0 => '禁用',
            1 => '正常',
            2 => '待审核',
            default => '未知'
        };
    }
}

查询和过滤

查询定制

php
class UserController extends Resources
{
    /**
     * 通用查询条件(适用于所有操作)
     */
    public function query(Builder $query): void
    {
        $query->where('deleted_at', null);
    }

    /**
     * 列表查询定制
     */
    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('username', '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']);
        }

        // 角色筛选
        if (!empty($params['role'])) {
            $query->whereHas('roles', function($q) use ($params) {
                $q->where('name', $params['role']);
            });
        }
    }

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

分页配置

php
class UserController extends Resources
{
    // 分页设置
    protected array $pagination = [
        'status' => true,        // 是否启用分页
        'pageSize' => 20,       // 默认每页数量
    ];

    // 多条数据字段控制
    public array $includesMany = ['id', 'username', 'email', 'status', 'created_at'];
    public array $excludesMany = ['password', 'remember_token'];

    // 单条数据字段控制
    public array $includesOne = ['id', 'username', 'email', 'phone', 'status', 'profile'];
    public array $excludesOne = ['password', 'remember_token'];
}

事件钩子系统

操作钩子

php
class UserController extends Resources
{
    /**
     * 创建前钩子
     */
    public function createBefore(Data $data, mixed $model): void
    {
        // 设置创建者
        $auth = App::auth();
        $model->created_by = $auth['id'];

        // 业务逻辑检查
        if ($this->emailExists($data->email)) {
            throw new ExceptionBusiness('邮箱已被使用');
        }
    }

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

        // 分配默认角色
        $defaultRole = Role::where('name', 'user')->first();
        if ($defaultRole) {
            $model->roles()->attach($defaultRole->id);
        }

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

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

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

    /**
     * 编辑后钩子
     */
    public function editAfter(Data $data, mixed $model): void
    {
        // 记录操作日志
        OperationLog::create([
            'user_id' => App::auth()['id'],
            'action' => 'edit_user',
            'target_id' => $model->id,
            'changes' => $model->getChanges(),
            'created_at' => now()
        ]);

        // 清除相关缓存
        Cache::tags(['users'])->flush();
    }

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

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

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

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

软删除钩子

php
class PostController extends Resources
{
    /**
     * 彻底删除前钩子
     */
    public function trashBefore(mixed $model): void
    {
        // 检查是否可以彻底删除
        if ($model->comments()->exists()) {
            throw new ExceptionBusiness('文章还有评论,无法彻底删除');
        }
    }

    /**
     * 彻底删除后钩子
     */
    public function trashAfter(mixed $model): void
    {
        // 删除关联文件
        if ($model->featured_image) {
            Storage::delete($model->featured_image);
        }

        // 删除所有附件
        foreach ($model->attachments as $attachment) {
            Storage::delete($attachment->file_path);
            $attachment->delete();
        }
    }

    /**
     * 恢复前钩子
     */
    public function restoreBefore(mixed $model): void
    {
        // 检查分类是否仍然存在
        if (!Category::find($model->category_id)) {
            throw new ExceptionBusiness('文章分类已被删除,无法恢复');
        }
    }

    /**
     * 恢复后钩子
     */
    public function restoreAfter(mixed $model): void
    {
        // 恢复相关数据
        $model->comments()->restore();

        // 更新统计
        $this->updateCategoryPostCount($model->category_id);
    }
}

元数据支持

列表元数据

php
class UserController extends Resources
{
    /**
     * 列表页面元数据
     */
    public function metaMany(object|array $query, array $data, ServerRequestInterface $request, array $args): array
    {
        return [
            'total_users' => User::count(),
            'active_users' => User::where('status', 1)->count(),
            'inactive_users' => User::where('status', 0)->count(),
            'new_users_today' => User::whereDate('created_at', today())->count(),
            'filters' => [
                'status_options' => [
                    ['value' => '', 'label' => '全部状态'],
                    ['value' => 1, 'label' => '正常'],
                    ['value' => 0, 'label' => '禁用'],
                    ['value' => 2, 'label' => '待审核']
                ],
                'role_options' => Role::select('id as value', 'name as label')->get()->toArray()
            ],
            'permissions' => [
                'can_create' => $this->canCreate(),
                'can_bulk_delete' => $this->canBulkDelete(),
                'can_export' => $this->canExport()
            ]
        ];
    }
}

详情元数据

php
class UserController extends Resources
{
    /**
     * 详情页面元数据
     */
    public function metaOne(mixed $data, ServerRequestInterface $request, array $args): array
    {
        return [
            'permissions' => $this->getUserPermissions($data),
            'statistics' => [
                'posts_count' => $data->posts()->count(),
                'comments_count' => $data->comments()->count(),
                'login_count' => $data->login_logs()->count(),
                'last_login' => $data->last_login_at?->format('Y-m-d H:i:s')
            ],
            'related_data' => [
                'roles' => $data->roles->map(function($role) {
                    return [
                        'id' => $role->id,
                        'name' => $role->name,
                        'display_name' => $role->display_name
                    ];
                }),
                'recent_posts' => $data->posts()
                    ->latest()
                    ->limit(5)
                    ->select('id', 'title', 'created_at')
                    ->get()
            ],
            'actions' => [
                'can_edit' => $this->canEdit($data),
                'can_delete' => $this->canDelete($data),
                'can_reset_password' => $this->canResetPassword($data)
            ]
        ];
    }
}

权限控制集成

自动权限检查

资源控制器会自动集成权限检查:

php
#[Resource(app: 'admin', route: '/admin/users', name: 'users')]
class UserController extends Resources
{
    // 自动权限检查 admin.users.*
}

跳过权限检查

php
class UserController extends Resources
{
    #[Action(['GET'], '/public-info', name: 'publicInfo', can: false)]
    public function getPublicInfo(): ResponseInterface
    {
        // 跳过权限检查的公开接口
        return send($response, '公开信息', [
            'total_users' => User::count(),
            'online_users' => $this->getOnlineUserCount()
        ]);
    }
}

完整示例

电商产品管理

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

#[Resource(
    app: 'admin',
    route: '/admin/products',
    name: 'products',
    softDelete: true,
    middleware: [AdminMiddleware::class]
)]
class ProductController extends Resources
{
    protected string $model = Product::class;
    protected string $label = '产品管理';

    // 分页配置
    protected array $pagination = [
        'status' => true,
        'pageSize' => 15,
    ];

    // 字段控制
    public array $includesMany = ['id', 'name', 'price', 'stock', 'status', 'category_id', 'created_at'];
    public array $excludesMany = ['description', 'content'];
    public array $includesOne = ['id', 'name', 'price', 'stock', 'status', 'category_id', 'description', 'content', 'images'];

    /**
     * 验证规则
     */
    public function validator(array $data, ServerRequestInterface $request, array $args): array
    {
        return [
            'name' => [
                ['required', '产品名称不能为空'],
                ['lengthMin', 2, '产品名称至少2个字符'],
                ['lengthMax', 100, '产品名称不能超过100个字符']
            ],
            'price' => [
                ['required', '价格不能为空'],
                ['numeric', '价格必须是数字'],
                ['min', 0.01, '价格不能小于0.01']
            ],
            'stock' => [
                ['required', '库存不能为空'],
                ['integer', '库存必须是整数'],
                ['min', 0, '库存不能小于0']
            ],
            'category_id' => [
                ['required', '分类不能为空'],
                ['integer', '分类ID必须是整数']
            ],
            'status' => [
                ['required', '状态不能为空'],
                ['in', [0, 1], '状态值无效']
            ]
        ];
    }

    /**
     * 数据格式化
     */
    public function format(Data $data, ServerRequestInterface $request, array $args): array
    {
        $formatted = [
            'name' => $data->name,
            'price' => bc_format($data->price, 2),
            'stock' => (int) $data->stock,
            'category_id' => (int) $data->category_id,
            'status' => (int) $data->status,
        ];

        if (isset($data->description)) {
            $formatted['description'] = $data->description;
        }

        if (isset($data->content)) {
            $formatted['content'] = $data->content;
        }

        if (isset($data->images)) {
            $formatted['images'] = json_encode($data->images);
        }

        return $formatted;
    }

    /**
     * 数据转换
     */
    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,
            'stock_status' => $this->getStockStatus($item->stock),
            'status' => $item->status,
            'status_text' => $item->status ? '上架' : '下架',
            'category' => [
                'id' => $item->category->id,
                'name' => $item->category->name
            ],
            'images' => $item->images ? json_decode($item->images, true) : [],
            'sales_count' => $item->order_items()->sum('quantity'),
            'created_at' => $item->created_at->format('Y-m-d H:i:s'),
            'updated_at' => $item->updated_at->format('Y-m-d H:i:s'),
        ];
    }

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

        // 预加载关联
        $query->with(['category', 'tags']);

        // 搜索
        if (!empty($params['keyword'])) {
            $query->where('name', 'like', "%{$params['keyword']}%");
        }

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

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

        // 价格范围
        if (!empty($params['min_price'])) {
            $query->where('price', '>=', $params['min_price']);
        }
        if (!empty($params['max_price'])) {
            $query->where('price', '<=', $params['max_price']);
        }

        // 库存筛选
        if (!empty($params['low_stock'])) {
            $query->where('stock', '<', 10);
        }
    }

    /**
     * 详情查询
     */
    public function queryOne(Builder $query, ServerRequestInterface $request, array $args): void
    {
        $query->with(['category', 'order_items.order']);
    }

    /**
     * 创建前钩子
     */
    public function createBefore(Data $data, mixed $model): void
    {
        // 检查分类是否存在
        if (!Category::find($data->category_id)) {
            throw new ExceptionBusiness('产品分类不存在');
        }

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

    /**
     * 创建后钩子
     */
    public function createAfter(Data $data, mixed $model): void
    {
        // 更新分类产品数量
        $this->updateCategoryProductCount($model->category_id);

        // 记录操作日志
        $this->logOperation('create', $model);
    }

    /**
     * 删除前钩子
     */
    public function delBefore(mixed $model): void
    {
        // 检查是否有订单
        if ($model->order_items()->exists()) {
            throw new ExceptionBusiness('该产品已有订单,无法删除');
        }
    }

    /**
     * 列表元数据
     */
    public function metaMany(object|array $query, array $data, ServerRequestInterface $request, array $args): array
    {
        return [
            'statistics' => [
                'total_products' => Product::count(),
                'active_products' => Product::where('status', 1)->count(),
                'low_stock_products' => Product::where('stock', '<', 10)->count(),
                'total_value' => Product::sum('price')
            ],
            'filters' => [
                'categories' => Category::select('id as value', 'name as label')->get()->toArray(),
                'status_options' => [
                    ['value' => '', 'label' => '全部状态'],
                    ['value' => 1, 'label' => '上架'],
                    ['value' => 0, 'label' => '下架']
                ]
            ]
        ];
    }

    // 自定义操作:批量上架
    #[Action(['POST'], '/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 ExceptionBusiness('请选择要上架的产品');
        }

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

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

    // 自定义操作:库存预警
    #[Action(['GET'], '/stock-alert', name: 'stockAlert')]
    public function stockAlert(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        $lowStockProducts = Product::where('stock', '<', 10)
            ->with('category')
            ->get()
            ->map(function($product) {
                return $this->transform($product);
            });

        return send($response, '库存预警', [
            'low_stock_count' => $lowStockProducts->count(),
            'products' => $lowStockProducts
        ]);
    }

    // 辅助方法
    private function getStockStatus(int $stock): string
    {
        if ($stock <= 0) return '缺货';
        if ($stock < 10) return '库存不足';
        return '正常';
    }

    private function updateCategoryProductCount(int $categoryId): void
    {
        $count = Product::where('category_id', $categoryId)->count();
        Category::where('id', $categoryId)->update(['product_count' => $count]);
    }

    private function logOperation(string $action, mixed $model): void
    {
        OperationLog::create([
            'user_id' => App::auth()['id'],
            'action' => "product_{$action}",
            'target_type' => Product::class,
            'target_id' => $model->id,
            'data' => $model->toArray(),
            'created_at' => now()
        ]);
    }
}

最佳实践

1. 合理使用钩子

php
// ✅ 推荐:在钩子中处理业务逻辑
public function createAfter(Data $data, mixed $model): void
{
    // 发送通知
    $this->sendNotification($model);

    // 更新缓存
    Cache::tags(['users'])->flush();

    // 记录日志
    $this->logUserCreated($model);
}

// ❌ 避免:在钩子中进行复杂的HTTP请求
public function createAfter(Data $data, mixed $model): void
{
    // 不要在钩子中进行耗时操作
    Http::post('https://api.example.com/webhook', $model->toArray());
}

2. 数据验证

php
// ✅ 推荐:根据操作类型设置不同验证规则
public function validator(array $data, ServerRequestInterface $request, array $args): array
{
    $action = $request->getAttribute('action', '');

    $rules = $this->getBaseRules();

    if ($action === 'create') {
        $rules = array_merge($rules, $this->getCreateRules());
    } elseif ($action === 'edit') {
        $rules = array_merge($rules, $this->getEditRules());
    }

    return $rules;
}

3. 权限控制

php
// ✅ 推荐:使用 Resource 注解自动权限检查
#[Resource(app: 'admin', route: '/admin/users', name: 'users')]
class UserController extends Resources
{
    // 自动权限检查 admin.users.*
}

// ✅ 推荐:自定义操作明确权限设置
#[Action(['POST'], '/export', name: 'export', can: true)]
public function export(): ResponseInterface
{
    // 需要 admin.users.export 权限
}

4. 数据转换

php
// ✅ 推荐:在 transform 中处理数据格式
public function transform(object $item): array
{
    return [
        'id' => $item->id,
        'name' => $item->name,
        // 格式化日期
        'created_at' => $item->created_at->format('Y-m-d H:i:s'),
        // 处理关联数据
        'category_name' => $item->category->name ?? '',
        // 计算字段
        'is_active' => $item->status === 1
    ];
}

5. 查询优化

php
// ✅ 推荐:预加载关联避免N+1问题
public function queryMany(Builder $query, ServerRequestInterface $request, array $args): void
{
    $query->with(['category', 'tags']);
}

// ✅ 推荐:限制查询字段
public array $includesMany = ['id', 'name', 'price', 'status', 'created_at'];
public array $excludesMany = ['description', 'content']; // 排除大字段

通过资源控制器,您可以快速构建功能完整、安全可靠的 RESTful API,同时保持代码的简洁和可维护性。资源控制器与路由系统的完美结合,让API开发变得简单而高效。

基于 MIT 许可证发布