资讯详情

资讯详情

Snipe-IT 控制器层架构规范解析:无 DTO/Repository 的内联 Eloquent 与 Action 拆分实践

后端企业应用【免费下载链接】snipe-itA free open source IT asset/license management system项目地址https://gitcode.com/GitHub_Trending/sn/snipe-it点击查看免费下载导读本文聚焦开源 IT 资产/许可证管理系统 Snipe-IT 的控制器层app/Http/Controllers/**架构约定。作为维护了十年、拥有 109 个控制器文件的大型 Laravel 单体应用Snipe-IT 通过.ai/rules/controllers.md这份路径作用域规则明确了两条铁律控制器内不允许引入 DTO 或 Repository 层查询须直接内联 Eloquent 完成当方法变得臃肿时必须拆解到 Action 或 Presenter 中。读完本文你将理解这套薄控制器 静态 Action 展示 Presenter Form Request 校验的 Laravel 工程化组合拳在真实项目中的落地方式并能直接对照 Snipe-IT 源码学会如何在 Controller 中做出正确的拆分决策。规则文档的定位一份路径作用域的 AI 协作规范在深入控制器写法之前先明确该文档在整个仓库中的角色。Snipe-IT 仓库根目录下的.ai/README.md说明.ai/目录存放的是面向 AI 编码助手的开发规范分为两层常驻指南Always-on合成进根目录CLAUDE.md与AGENTS.md在每次请求时都会注入助手上下文路径作用域规则Path-scoped rules.ai/rules/下的小型聚焦规则仅在编辑路径匹配的文件的按需加载。.ai/rules/index.md中维护着路径 glob 与规则文件的映射表其中一行| app/Http/Controllers/** | .ai/rules/controllers.md |即当助手要改动app/Http/Controllers/下任何文件时就会加载本文主角controllers.md。这份规则不是泛泛的编码建议而是被php artisan boost:install --guidelines见.ai/README.md驱动的、与源码强绑定的工程约束。controllers.md全文结构如下--- paths: - app/Http/Controllers/** --- # Controllers ## No DTOs or repository layer Controllers build Eloquent queries inline and pass models, collections, and arrays around. There are no DTO or repository classes — do not introduce them. Extract to an Action or a Presenter when a controller method gets heavy.这份规则定义了 Snipe-IT 控制器层的全部三条核心约定下面逐一结合源码展开。约定一拒绝 DTO 与 Repository 层查询内联 Eloquent规则原文与意图Controllers build Eloquent queries inline and pass models, collections, and arrays around. There are no DTO or repository classes — do not introduce them.核心意思是控制器方法直接使用 Eloquent 查询构造器Model::where(...)、-withCount()、-skip()-take()等拼装查询并在方法间传递模型实例、集合与数组禁止引入 DTO数据传输对象类或 Repository仓储类。这一约定与 Laravel 社区常见的Repository 模式教学形成鲜明对比。Snipe-IT 的选择有其现实考量减少间接层仓库中大多数实体的 CRUD 逻辑高度同构index/store/show/update/destroy内联查询让每个控制器方法一目了然保持查询灵活IT 资产管理领域常出现按供应商/类别/公司多维筛选、按assets_count等聚合列排序的场景直接操作查询构造器可以零成本组合条件避免为抽象而抽象项目没有多数据源切换需求Repository 层带来的测试替身与接口定义成本大于收益。源码印证Web 控制器的内联查询Web 端SuppliersController的store()/update()方法直接在方法体内new Supplier、逐字段赋值并save()没有任何仓储类参与// app/Http/Controllers/SuppliersController.php#L52-L80 public function store(ImageUploadRequest $request): RedirectResponse { $this-authorize(create, Supplier::class); $supplier new Supplier; $supplier-name request(name); $supplier-address request(address); // ... 其余字段逐个赋值 $supplier-created_by auth()-id(); $supplier $request-handleImages($supplier); if ($supplier-save()) { return redirect()-route(suppliers.index) -with(success, trans(admin/suppliers/message.create.success)); } return redirect()-back()-withInput()-withErrors($supplier-getErrors()); }注意这里返回的是RedirectResponse 翻译键trans(...)而非 DTO。这与.ai/rules/app.md中UI 字符串必须用trans()短点号翻译键、禁止硬编码英文的规范保持了一致。API 端同样内联。Api\SuppliersController::index()在方法内完成白名单校验、withCount聚合、TextSearch全文检索、条件where拼接与skip/take分页见 app/Http/Controllers/Api/SuppliersController.php返回的是 Transformer 处理后的数组// app/Http/Controllers/Api/SuppliersController.php#L134-L136 $suppliers $suppliers-skip($offset)-take($limit)-get(); return (new SuppliersTransformer)-transformSuppliers($suppliers, $total);模型、集合Collection、数组就是整个应用内部传递的数据契约全仓库搜索不到任何名为 DTO 的类。约定二方法变重时向 Action 或 Presenter 拆解Extract to an Action or a Presenter when a controller method gets heavy.重heavy的判据是什么从代码推断通常指多分支的业务规则、需要抛出多个领域异常的状态判断、或跨多个关联模型的聚合操作。此时不引入 DTO/Repository而是拆到两类专用类中——Action业务动作与Presenter展示逻辑。Action单一静态 run() 的业务动作类.ai/rules/actions.md规定了 Action 的形态An Action is a class inapp/Actions/Entity/namedVerbEntityAction, with onepublic static function run(...)and no constructor. Call it statically.即Action 位于app/Actions/实体/目录命名为动词实体Action只暴露一个public static function run(...)无构造函数禁止使用handle()、execute()、__invoke()或实例化调用。Snipe-IT 的 app/Actions/Suppliers/DestroySupplierAction.php 是教科书式范例。SuppliersController::destroy()中原本要写检查 6 类关联、逐个抛异常、清理图片、再删除这坨逻辑被完整抽进 Actionpublic static function run(Supplier $supplier): bool { $supplier-loadCount([ maintenances as maintenances_count, assets as assets_count, licenses as licenses_count, accessories as accessories_count, consumables as consumables_count, components as components_count, ]); if ($supplier-assets_count 0) { throw new ItemStillHasAssets($supplier); } // ... maintenances / licenses / accessories / consumables / components 同理 if ($supplier-image) { Storage::disk(public)-delete(suppliers/.$supplier-image); } $supplier-delete(); return true; }控制器端随之变得极薄——只负责鉴权、调用、按异常类型翻译错误信息并重定向// app/Http/Controllers/SuppliersController.php#L131-L167 public function destroy(Supplier $supplier): RedirectResponse { $this-authorize(delete, Supplier::class); try { DestroySupplierAction::run(supplier: $supplier); } catch (ItemStillHasAssets $e) { return redirect()-route(suppliers.index)-with(error, trans(general.bulk_delete_associations.assoc_assets, [asset_count (int) $supplier-assets_count, item trans(general.supplier)])); } catch (ItemStillHasComponents $e) { // ... } return redirect()-route(suppliers.index)-with(success, trans(admin/suppliers/message.delete.success)); }API 端的Api\SuppliersController::destroy()app/Http/Controllers/Api/SuppliersController.php复用同一个 Action只是把错误渲染从redirect()-with(error, ...)换成Helper::formatStandardApiResponse(error, null, ...)。一个 Action 同时服务 Web 与 API 两个入口这正是无 DTO/Repository架构下逻辑复用的核心手段。异常类本身也遵循面向领域命名——app/Exceptions/ItemStillHasAssets.php、ItemStillHasComponents.php 等把该实体仍有关联资产/组件的约束变成显式异常方便 Controller 精确捕获。app/Actions/下共有 11 个子目录Acceptances、AssetModels、Breadcrumbs、Categories、CheckoutRequests、Companies、Departments、Depreciations、Manufacturers、Permissions、StatusLabels、Suppliers全部遵循VerbEntityAction::run()这一统一形态。Presenter展示格式化与表格列配置的归属地.ai/rules/presenters.md明确了 Presenter 的职责边界Display formatting and Bootstrap-table column config belong inapp/Presenters/EntityPresenter.php, reached from the model via$model-present(). Keep this logic out of controllers, transformers, and Blade.即显示格式化与 Bootstrap-table 列配置属于app/Presenters/实体Presenter.php通过模型的$model-present()访问并明令禁止把这些逻辑放进 Controller、Transformer 或 Blade 模板。以 app/Presenters/SupplierPresenter.php 为例它承担了两类重逻辑dataTableLayout()返回整个 Bootstrap-table 的 JSON 列配置包括每列的field、sortable、searchable、visible、formatter如suppliersLinkFormatter、imageFormatter、dateDisplayFormatter以及表头翻译键。这个 200 多行的配置若塞进控制器会让index()立即变重nameUrl()、viewUrl()、formattedNameLink()等封装带权限判断的链接生成例如有view权限时输出a href...名称/a无权限时仅输出转义文本。业务规则Action与展示规则Presenter分离后Controller 只剩下鉴权 编排 响应每个方法保持 515 行内联代码可读性与可测试性同时得到保证。约定三Controller 骨架中的固定协作件虽然controllers.md未展开但其约定隐含了 Controller 与另两个规则域的协作边界。读懂这张协作图才能真正在 Snipe-IT 中写出合规的控制器。Form Request 是唯一校验入口.ai/rules/requests.md规定校验必须用 Form Request 类禁止在控制器内$request-validate()或Validator::make()。Form Request 继承App\Http\Requests\Request并在protected $rules属性中声明规则涉及文件上传时改继承ImageUploadRequest并在控制器中调用$request-handleImages($model)。上文的SuppliersController::store(ImageUploadRequest $request)就是标准用法方法签名即校验声明控制器内一行$request-handleImages($supplier)完成图片处理校验与上传逻辑都不会污染控制器。API 响应统一封装、分页禁止 paginate.ai/rules/api.md针对 API 控制器进一步细化统一响应信封所有 API 响应走Helper::formatStandardApiResponse(success, $payload, trans(...))失败用error且$payload null消息一律翻译键项目不使用 Laravel Eloquent API Resource列表分页用 offset/limitindex()通过容器解析app(api_offset_value)与app(api_limit_value)以-skip($offset)-take($limit)-get()分页禁用paginate()/simplePaginate()/cursorPaginate()Select2 接口是唯一例外selectlist()必须返回LengthAwarePaginator供SelectlistTransformer::transformSelectlist()推导 select2 的pagination.more等无限滚动参数。见 app/Http/Controllers/Api/SuppliersController.php 的selectlist()搜索、orderBy(name)、paginate(50)、逐项设置use_text/use_image后交给SelectlistTransformer。API 控制器的index()因此同时体现内联 Eloquent与重逻辑上移/外移白名单排序列、聚合withCount、TextSearch、动态条件where、offset/limit 全部内联在方法里而真正繁重的序列化交给 SuppliersTransformer。一张图看懂 Snipe-IT 控制器的请求处理链综合上述规则与源码一次标准请求在 Snipe-IT 中经过的路径可归纳为路由routes/ → 中间件鉴权/公司作用域 → Form Request校验 图片预处理 → Controller 方法authorize() 内联 Eloquent 查询 或 Action 调用 → 分支 ├─ WebView / RedirectResponse消息用 trans() 翻译键 └─ APIHelper::formatStandardApiResponse 信封 / SelectlistTransformer → 展示层Presenter$model-present()提供格式化与 datatable 列配置这条链路中Controller 始终是编排者而非逻辑仓库业务规则交给静态 Action展示规则交给 Presenter校验交给 Form Request序列化交给 Transformer唯独 DTO 与 Repository 不存在。如何在此规范下实践判定与落地清单如果你要在 Snipe-IT 中新增或重构控制器方法可按如下清单自检先问这方法是否变重了若方法出现三种以上异常分支、跨模型聚合计数、或超过约 20 行的内联逻辑考虑拆分业务动作 → Action在app/Actions/Entity/建VerbEntityAction只写一个public static function run(...)无构造函数领域约束用app/Exceptions/下的ItemStillHas*异常表达让 Controller 用try/catch精确渲染错误Web 端redirect()-with(error, trans(...))API 端formatStandardApiResponse(error, null, trans(...))展示逻辑 → PresenterHTML 片段、权限判断式链接、Bootstrap-tabledataTableLayout()列配置放进app/Presenters/EntityPresenter.php通过$model-present()访问校验 → Form Request继承App\Http\Requests\Request声明protected $rules上传场景继承ImageUploadRequest并在控制器里调用handleImages()API 分页普通列表用skip/takeapp(api_offset_value)/app(api_limit_value)select2 端点用paginate(50)交给SelectlistTransformer消息文本一律trans(admin/entity/message.xxx)翻译键不硬编码英文。遵循这套约定写出的控制器与仓库现有的 app/Http/Controllers 下 109 个控制器保持同构也才能让 AI 助手经由.ai/rules/index.md的路径匹配给出与项目风格一致的代码建议。结语Snipe-IT 用一份只有三行的路径作用域规则管住了拥有 109 个控制器、横跨 Web 与 API 的大型 Laravel 应用的架构底线不引入 DTO/Repository 的间接层控制器只做内联 Eloquent 与编排一旦变重业务拆到静态 Action、展示拆到 Presenter配合 Form Request 校验与统一 API 信封。这套重逻辑两向分流的模式让 SuppliersController.php 这样的控制器即便经过十年迭代依然薄而清晰也为同类 Laravel 单体项目提供了一份可以直接借鉴的控制器层规范蓝本。赞分享后端企业应用【免费下载链接】snipe-itA free open source IT asset/license management system项目地址https://gitcode.com/GitHub_Trending/sn/snipe-it点击查看免费下载相关推荐一次编写处处运行Quasar Framework 跨平台 Vue.js 开发全景指南一次编写处处运行Quasar Framework 跨平台 Vue.js 开发全景指南 本文以 docs/src/pages/introduction to后端企业应用CICC/bert-base-chinese核心配置详解隐藏层、注意力头与词汇表参数全解析CICC/bert base chinese核心配置详解隐藏层、注意力头与词汇表参数全解析 CICC/bert base chinese是一款专为中文优化的BSnipe-IT 模型开发规范为新 Eloquent 模型配套 Factory 与 Seeder 的完整实践Snipe IT 模型开发规范为新 Eloquent 模型配套 Factory 与 Seeder 的完整实践 导读 本文以 Snipe IT 仓库中的开发规范后端企业应用上一篇pnpm 路径感知的注册表缓存键修复共享主机下元数据混淆导致的 ERR_PNPM_TARBALL_URL_MISMATCH下一篇Slang IR Type 家族参考文档的评审与修复生成式设计文档 QA 闭环实战解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →