Laravel 项目开发规范
1. 建立开发规范之目的
对于框架设计而言,灵活是件好事,能提供给开发者不同的选项,能让框架适用更多的场景。
但对于团队开发来说,大部分时候,更多的选项反而是累赘。因为每个人都可能写出不一样的代码,这无疑增加了项目维护的难度,影响效率。如果是在一个中大型的商业项目开发中,团队中有着几个甚至十几个开发者,没有规范的情况下,开发者会根据各自的喜好去选择,有时甚至出现一个开发者尝试多个选项的可能,就会造成整个团队产出的代码可读性极低,代码结构混乱,也为后面的项目代码的维护带来了难度。
建立开发规范的目的在于通过统一的代码风格,保证代码的易读性、可维护性。开发规范一旦统一,所有团队成员严格遵守,你会发现,其他成员写的代码就如你自己写的一样,编码愉悦感提高了,整个项目代码阅读起来更加流畅,工作效率自然也会因此提高,同时代码的健壮性也得到了保障。
2. 必须遵循的开发原则
- DRY ———— 「Don't Repeat Yourself」, 不写重复的逻辑代码。
- KISS ———— 「Keep it Simple, Stupid」, 提倡简单易读的代码,不写高深、晦涩难懂的代码,不过度设计。
- 约定大于配置 ———— 「Convention Over Configuration」,优选选择框架及社区提倡的做法,不过度配置。
- Restful ———— 利用「资源化概念」和标准的 HTTP 动词来组织程序。
- MVC - Model, View, Controller ,以 MVC 为核心,严格控制 Controller 的可读性和代码行数。
除了这些原则外,还有不要强制,但非常有用的设计原则,如 SOLID 设计原则(S:单一职责原则、O:开/闭原则、L:里氏替换原则、I:接口隔离原则、D:依赖倒置原则)、组合优于继承等。
3. 项目目录结构规范
laravel-app/
├── app/
│ ├── Console/ # Artisan 命令
│ │ ├── Development/ # 存放开发专用命令
│ │ ├── LongPulling/ # 存放死循环执行的命令(可选)
│ │ ├── OneTime/ # 存放一次性命令
│ │ ├── Schedule/ # 存放计划任务
│ │ └── . # 根目录存放一般命令(在非常复杂的项目中,应该再按照业务逻辑进行分组)
│ ├── Exceptions/ # 异常处理
│ ├── Http/
│ │ ├── Controllers/ # 控制器
│ │ ├── Middleware/ # 中间件
│ │ └── Requests/ # 表单请求验证
│ ├── Models/ # Eloquent 模型
│ │ ├── Auth/ # 示例:存放用户、角色、权限相关的模型
│ │ ├── Order/ # 示例:存放订单相关的模型
│ │ └── Payment/ # 示例:存放支付渠道相关的的模型
│ ├── Providers/ # 服务提供者
│ └── Services/ # 业务逻辑处理
│ ├── Auth/ # 示例:存放登录、授权相关的业务逻辑文件
│ └── Payment/ # 示例:存放支付相关的业务逻辑文件
├── bootstrap/ # 应用启动脚本
├── config/ # 配置文件
├── database/
│ ├── migrations/ # 数据库迁移
│ │ ├── Auth/ # 示例:存放用户、角色、权限相关的迁移文件
│ │ └── Payment/ # 示例:存放支付方式、订单相关的迁移文件
│ └── seeders/ # 数据填充
├── public/ # 公开静态文件
├── resources/
│ ├── views/ # Blade 视图
│ │ ├── layouts/ # 示例:页面布局的视图文件
│ │ ├── common/ # 示例:存放页面通用元素的视图文件
│ │ ├── pages/ # 示例:简单的页面存放文件夹,如:about、contact 等的视图文件
│ │ └── resources/ # 示例:对应 Restful 路由的资源路径名称,以 URI photos/create 为例,对应 create.blade.php 文件,存放在文件夹 photos 下。
│ │ ├── Auth/ # 示例:存放用户、角色、权限相关的视图文件
│ │ └── Payment/ # 示例:存放支付方式、订单相关的视图文件
│ └── lang/ # 多语言文件
├── routes/ # 路由定义
├── storage/ # 应用存储(日志、缓存)
└── tests/ # 测试代码
├── Feature/ # 示例:存放功能测试、集成测试的测试代码
└── Unit/ # 示例:存放单元测试代码,
├── API/ # 示例:存放 API 接口的测试代码
├── Web/ # 示例:存放 web 接口的测试代码
├── Admin/ # 示例:存放管理后台接口的测试代码
├── Command/ # 示例:存放命令行接口的测试代码
├── Job/ # 示例:存放任务接口的测试代码
├── Fixtures/ # 示例:测试中使用到样例数据,必须使用子目录存储,绝不直接放置于此目录下
├── Job/ # 示例:存放任务接口的测试代码
└── Fixtures/ # 示例:测试中使用到样例数据,必须使用子目录存储,4. 代码规范
4.1 代码风格
代码风格建议遵循 PSR-12 规范。
PHPStorm 安装代码风格检测插件 php-cs-fixer,安装教程。
4.1.1 变量
使用有意义且可读的变量名,变量名遵循驼峰命名规范。
// 坏代码示例 $date = date('y-m-d'); // 好代码示例 $currentDate = date('y-m-d'); // 从变量名可知,$date 是一个日期,但不知是当天日期还是某个特定时间的日期,相比于 $date,$currentDate更能体现其具体使用场景。不要添加不需要的上下文。如果你的 类或对象 已经有明确的含义,不要在变量中再次重复它。
// 坏代码 class Car { public $carMake; public $carModel; public $carColor; //... } // 好代码 class Car { public $make; public $model; public $color; //... }
4.1.2 分支判断
避免嵌套太深(最多不超过三层)并尽早返回。
# 坏代码示例 function fibonacci(int $n) { if ($n < 50) { if ($n !== 0) { if ($n !== 1) { return fibonacci($n - 1) + fibonacci($n - 2); } return 1; } return 0; } return 'Not supported'; } # 好代码示例 function fibonacci(int $n): int { if ($n === 0 || $n === 1) { return $n; } if ($n >= 50) { throw new Exception('Not supported'); } return fibonacci($n - 1) + fibonacci($n - 2); }
4.1.3 函数
函数名应该语义化。
// 坏代码示例 class Email { //... public function handle(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 这是什么? 消息的句柄? 我们现在正在写入文件吗? $message->handle(); // 好代码示例 class Email { //... public function send(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 清晰明了 $message->send();函数参数最多不应超过3个。
- 一旦一个方法拥有三个以上的参数将会导致组合爆炸,在进行测试时需要使用每个单独的参数测试大量不同的情况。
- 零参数是理想的情况。 一两个参数是可以的,三个应该避免。除此之外的任何东西都应该合并
- 通常,如果您有两个以上参数这说明你的函数做的事情太多了。如果不是,大多数情况下一个更高级别的对象就足以作为一个参数。
一个函数应该只做一件事,同时不要使用标志位作为函数参数。
// 坏代码,函数做了两件事,创建临时文件或者创建正式文件 function createFile(string $name, bool $temp = false): void { if ($temp) { touch('./temp/' . $name); } else { touch($name); } } // 好代码:应该基于职责做拆分,拆分为两个函数 function createFile(string $name): void { touch($name); } function createTempFile(string $name): void { touch('./temp/' . $name); }避免副作用。避免副作用意味着,尽量不要再函数内使用全局变量或者使用引用传递的参数。
// 坏代码示例 // 通过以下函数引用的全局变量。 // 如果我们有另外一个使用这个名字函数,现在它将是一个数组,它可能会破坏它。 $name = 'Ryan McDermott'; function splitIntoFirstAndLastName(): void { global $name; $name = explode(' ', $name); } splitIntoFirstAndLastName(); var_dump($name); // ['Ryan', 'McDermott'];// 好代码示例 function splitIntoFirstAndLastName(string $name): array { return explode(' ', $name); } $name = 'Ryan McDermott'; $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott'];封装条件,且函数命名时避免使用否定条件。
// 坏代码示例 if ($article->state === 'published') { // ... }// 好代码示例 if ($article->isPublished()) { // ... } private function isPublished() { return $article->state === 'published'; }// 坏代码示例 function isDOMNodeNotPresent(DOMNode $node): bool { // ... } if (! isDOMNodeNotPresent($node)) { // ... }// 好代码示例 function isDOMNodePresent(DOMNode $node): bool { // ... } if (isDOMNodePresent($node)) { // ... }
4.1.4 类与对象
- public 方法和属性对于更改是最危险的,因为一些外部代码可能很容易依赖它们,而您无法控制哪些代码依赖它们。 类中的修改对类的所有用户都是危险的。
- protected 修饰符与 public 一样危险,因为它们在任何子类的范围内都可用。 这实际上意味着 public 和 protected 之间的区别仅在于访问机制,但封装保证保持不变。类中的修改对所有后代类都是危险的。
- private 修饰符保证代码 仅在单个类的边界内修改是危险的(您可以安全地修改并且不会有 叠叠乐效应).
4.2 配置信息与环境变量
- 因 .env 不会被纳入版本控制器中,所以本地 .env 文件里添加变量时必须同步到 .env.example 文件中,以免影响其他项目参与者的工作。
所有程序配置信息必须通过 config() 来读取,所有的 .env 配置信息必须通过 env() 来读取,不在配置文件以外的范围使用 env()。原因在于:
- 定义分明,config() 是配置信息,env() 只是用来区分不同环境。
- 统一放置于 config 中还可以利用框架的配置信息缓存功能(php artisan config:cahce)来提高运行效率,当配置信息被缓存后,.env 文件将不会被加载,所以env() 函数将读取不到 .env 文件中的内容。
- 代码健壮性, config() 在 env() 之上多出来一个抽象层,会使代码更加健壮,更加灵活。
4.3 路由
- 不在路由定义文件中书写「闭包路由」或者其他业务逻辑代码,因为路由缓存 并不会作用在基于闭包的路由。
- 路由定义文件中要保持干净整洁,绝不放置除路由配置以外的其他程序逻辑。
路由定义的方式存在多种,推荐使用控制器路由的 Laravel 8+ 推荐写法(使用命名空间或自动导入),推荐使用下面示例代码的第二种写法或第三种写法。
// 第一种写法:指向控制器方法 Route::get('/user', 'UserController@index')->name('user.index'); // 第二种写法:Laravel 8+ 推荐写法(使用命名空间或自动导入) use App\Http\Controllers\UserController; Route::get('/user', [UserController::class, 'index'])->name('user.index'); // 第三种写法:基于第二种写法,并且同时有多个路由用到相同的 Controller use App\Http\Controllers\OrderController; Route::controller(OrderController::class)->group(function () { Route::get('/orders/{id}', 'show'); Route::post('/orders', 'store'); });优先使用 Restful 路由,配合资源控制器路由一起使用,Restful 路由的 uri 应该使用复数形式,例如 uri 应该声明为 /photos/{photo},而不是 /photo/{photo}。
例如声明以下的一个资源路由,将会注册如下表格所示的路由。
use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class);HTTP请求方式 URI 操作 路由名称 GET /photos index photos.index GET /photos/create create photos.creat POST /photos store photos.store GET /photos/{photo} show photos.show GET /photos/{photo}/edit edit photos.edit PUT/PATCH /photos/{photo} update photos.update DELETE /photos/{photo} destroy photos.destroy 不难想到,有时可能需要定义一个嵌套的资源型路由。例如,照片资源可能被添加了多个评论。那么可以在路由中使用 . 符号来声明资源型控制器。这将注册下表格所示的路由。
use App\Http\Controllers\PhotoCommentController; Route::resource('photos.comments', PhotoCommentController::class);HTTP请求方式 URI 操作 路由名称 GET /photos/{photo}/comments index photos.comments.index GET /photos/{photo}/comments/create create photos.comments.creat POST /photos/{photo}/comments store photos.comments.store GET /photos/{photo}/comments/{comment} show photos.comments.show GET /photos/{photo}/comments/{comment}/edit edit photos.comments.edit PUT/PATCH /photos/{photo}/comments/{comment} update photos.comments.update DELETE /photos/{photo}/comments/{comment} destroy photos.comments.destroy 注意:如果您需要向资源控制器添加超出默认资源路由集的其他路由,则应在调用 Route::resource 方法之前定义这些路由;否则,由 resource 方法定义的路由可能会无意中优先于您的补充路由。
4.4 控制器
- 优先使用Restful 资源控制器。
控制器命名规范,必须使用资源的复数形式,如:
- 类名:PhotosController
- 文件名:PhotosController.php
- 在控制器内不能声明私有方法,控制器内所有的方法都是外部可访问的表示路由动作的公有方法,并且控制器里的所有方法,都应该被使用到,否则应该删除,也绝对不在控制器里批量注释掉代码,无用的逻辑代码就必须清除掉。。
- 控制器内不应包含业务逻辑,业务逻辑的实现应该在业务逻辑层去实现。
- 不应该为「方法」书写很明显的注释,这要求方法取名要足够合理,不需要过多注释。应该为一些复杂的逻辑代码块书写注释,主要介绍产品逻辑 - 为什么要这么做。
表单数据校验应该放到控制器层(如果是非复杂表单的字段检验,可直接在方法内进行校验,否则需单独写一个表单验证类),验证通过后再将请求转发给业务逻辑层。
/** * 存储一篇新的博客文章。 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]); // 博客文章验证通过... }
4.5 表单验证
- 使用表单请求 - FormRequest 类 来处理控制器里的表单验证。
验证类命名规则,FormRequest表验证类命名必须按照控制器方法来命名,且必须添加模型的前缀,类名称的 Request 后缀也是必须的,这方便了编辑器开始打开文件。如:
- UserCreateRequest
- UserUpdateRequest
4.6 业务逻辑层(Service 层或 Logic 层)
- Controller 层只接收和转发请求,Model 层只负责模型定义以及数据关联逻辑。由业务逻辑层负责处理业务逻辑。
- Service 中的方法命名参考 Laravel Model 中的方法命名。
所有的 Service 类都必须存放于 app/Services 目录中,且应避免直接将 Service 类放置于 app/Services 目录下,应该考虑通过业务逻辑,将其归类于子目录中。
Auth —— 存放登录、授权相关的 Service Payment —— 存放支付相关的 Service Book —— 存放课程相关的 Service- Service 类必须是无状态的,无状态意味着无论是控制器方法中、命令行、单元测试中,都可调用。
4.7 模型
- 所有的数据模型文件,都必须存放在:app/Models/ 文件夹中。且所有的 Eloquent 数据模型都必须继承统一的基类 App\Models\Model,此基类存放位置为 /app/Models/Model.php。
命名规范
- 数据模型类名必须为「单数」,如:App\Models\Photo
- 类文件名必须为「单数」,如:App\Models\Photo.php
- 数据库表名字必须为「复数」,如:photos,users...
- 数据库表迁移名字必须为「复数」,如:2014_08_08_234417_create_photos_table.php
- 数据填充文件名必须为「复数」,如:PhotosTableSeeder.php
- 数据库字段名必须是遵循蛇形命名法,如:view_count, is_vip
- 数据库表主键名必须为id
- 数据库表外键必须为「resource_id」,如:user_id, post_id
- 目录分层:为模型文件按业务逻辑做分层。
- 尽量避免使用 Laravel 的 模型事件。使用模型事件的问题在于,其职能很难界定,所有的业务逻辑都能写到模型事件中。
4.8 视图
- 避免在 resources/views 目录下直接放置视图文件。页面布局文件必须放在 resources/views/layouts 目录下,页面组件必须放在 resources/views/common 目录下,简单页面(如404页面)放在 resources/views/pages 目录下。对应 Restful 路由的资源路径名称,例如以URI photos/create 为例,对应的 create.blade.php 文件,应存放在 resources/views/photos 目录下。
- 局部视图文件必须使用 <kdb>_</kdb> 前缀来命名,如:photos/_upload_form.blade.php
为了和 Restful 路由器和资源控制器保持一致,视图命名也必须使用资源视图的命名方式。例如, 一个完整的 photos 资源对应的视图文件为以下:
├── photos │ ├── _form.blade.php │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php
5. Git 工作流规范
6. 服务器部署规范
6.1 Composer 依赖安装规范
生产环境或预发布环境进行项目部署时,必须使用 --no-dev 来安装项目运行必需依赖。如安装 predis 扩展:
composer require predis/predis --no-dev本地环境或测试环境安装开发专用扩展时,必须使用 --dev 来安装开发依赖。如在本地安装 phpunit 扩展:
composer require phpunit/phpunit --dev
6.2 Composer 依赖版本更新
绝对不能在生产服务器执行,composer udpate,原因如下:
- composer udpate 会根据 composer.json 文件的版本约束拉取最新兼容版本,可能导致依赖包升级到未测试的版本,引入不兼容的变更或bug。同时,这将导致开发环境与生产环境的依赖版本可能因此不一致,当系统出现问题时,排查问题变得困难。
- 由于依赖包的升级可能导致项目运行报错。
- 执行composer udpate后,由于依赖已全局更新,回滚到旧版本可能需要重新部署整个项目。
- 正确做法是,在开发环境更新依赖(即在本地开发环境执行composer udpate),测试项目依赖更新后的所有功能是否正常。测试无影响后,提交新的 composer.lock 文件到版本控制,确保依赖版本锁定。在生产环境执行composer install,这将严格安装 composer.lock 中记录的版本,保证环境一致性,减少因部署的环境不一致而导致的项目运行问题。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。