Laravel 项目开发规范
文章创建于: 2017-08-10,最后修改于:2018-09-19

规范
1

1. 前言

本规范大量参考 优帆远扬 团队内部 Laravel 工程师践行的开发规范。并由本人基于公司项目环境进行一定程度的修改。

在此感谢 laravel-china 社区和 优帆远扬团队 和 社区站长 @Summer 的无私奉献。

1.1. 关于规范

1.1.1. 说明

1.1.2 优势

  • 高效编码 - 避免了过多的选择造成的『决策时间』浪费;
  • 风格统一 - 最大程度统一了开发团队成员代码书写风格和思路,代码阅读起来如出一辙;
  • 减少错误 - 减小初级工程师的犯错几率。

1.2. 开发哲学

因为篇幅原因本规范无法涉及到项目里每一块代码的编写标准,所以此处重点说明下此规范遵循的『开发哲学』,开发中请把其当做指明灯,来指引你做决策:

  • DRY –「Don’t Repeat Yourself」不写重复的逻辑代码;
  • 约定俗成 - 「Convention Over Configuration」,优先选择框架提倡的做法,不过度配置;
  • KISS - 「Keep it Simple, Stupid」提倡简单易读的代码,不写高深、晦涩难懂的代码,不过度设计;
  • 主厨精选 - 让有经验的人来为你选择方案,不独创方案;
  • 官方提倡 - 优先选择官方推崇的方案。

1.3. 关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须(Must) - 只能这样子做,请无条件遵循,没有别的选项;
  • 绝不(Must Not)- 严令禁止,在任何情况下都不能这样做;
  • 应该(Should) - 强烈建议这样做,但是不强求;
  • 不应该(Should Not) - 强烈建议不这样做,但是不强求;
  • 可以(May) - 选择性高一点,在这个文档内,此词语使用较少;

参考:RFC 2119

1.4. 关于执行

在这份规范里,有些内容里会解释『这样做的理由』,这样做的目的是为了达成共识。

请不要以此『理由』的准确性来怀疑规范的权威性,规范就是规范,可以讨论改正,但在执行的时候 必须 严格遵守。

请把『团队项目开发』想象就是在行军打仗,对于规范要绝对服从。要有大局观,做到团结一致,把个人的喜好放一边,把整个团队的执行效率放在第一位。

2. 项目规范

2.1. Laravel 版本选择

以下是 Laravel 的版本路线图:

版本 发布日期 版本类型 维护周期
Laravel 5.1 2015 年 6 月 LTS 长久支持 Bug 修复 2017 年 6 月份,安全修复 2018 年 6 月份
Laravel 5.2 2015 年 12 月 一般发行 提供 6 个月的 Bug 修复支持,一年的安全修复支持
Laravel 5.3 2016 年 8 月 一般发行 提供 6 个月的 Bug 修复支持,一年的安全修复支持
Laravel 5.4 2017 年 1 月 一般发行 提供 6 个月的 Bug 修复支持,一年的安全修复支持
Laravel 5.5 2017 年 8 月 LTS 长久支持 Bug 修复 2019 年 8 月份,安全修复 2020 年 8 月份

选择 Laravel 版本时,应该 优先考虑 LTS 版本,因为安全性和稳定性考虑,商业项目开发中 不应该 使用最新版本的 『Laravel 一般发行版』。

扩展阅读:如何选择 Laravel 框架版本。

请使用以下命令来创建指定版本的 Laravel 项目:

composer create-project laravel/laravel project-name --prefer-dist "5.5.*"

2.2. 环境说明

一般情况下,一个项目 应该 有以下三个基本的项目环境:

  • Local - 开发环境
  • Staging - 线上测试环境
  • Production - 线上生产环境

2.2.1. 服务器系统

Laravel 社区提倡使用 Ubuntu 系统,开发环境 Homestead 中默认也是使用 Ubuntu 系统。所以服务器系统 应该 优先考虑 Ubuntu,并且是 LTS 支持的系统,如 Ubuntu 14.04.5 LTS 或者 Ubuntu 16.04.2 LTS 。

2.2.2. PHP 7

PHP 版本 应该 优先考虑 PHP 7,不止因为其运行高效,还因为随着 PHP 7 的广泛应用,PHP 7 以下的版本将会很快停止维护。

2.2.3. MySQL 5.7

数据库软件 应该 优先选择 MySQL,因为其使用率最高。MySQL 5.7 与 PHP 7 一样,已经是大势所趋,选择版本时 应该 优先考虑 MySQL 5.7。

2.2.4. 其他软件

应该 优先选择 流行 稳定 版本。线上环境 绝不 使用 Beta 或者其他不稳定发行版。

2.2.5. Local 开发环境

所有项目 应该 使用 Homestead 作为应用程序运行的环境。请阅读 为什么必须使用 Homestead 来开发 Laravel 应用? 。

2.2.6. Production 生产环境

线上环境部署可以参考 Ubuntu 14/16 下的 Laravel LNMP 线上环境自动部署脚本,此脚本参照了 Homestead 环境设置脚本 ,并做了更加适用于生产环境的效率和安全调优。

处于安全考虑,线上环境 必须 只开放以下端口:

  • 80 HTTP
  • 443 HTTPS
  • 22 SSH

2.2.7. Staging 线上测试环境

除了域名等其他独立应用配置以外,环境 必须 跟 Production 保持高度一致性,可以的话 应该 与 Production 使用同台机器。

2.2.8. 环境变量

在项目中 必须 使用环境变量(.env文件)来区分敏感数据的设置。

在项目中 必须 存在以下 env 文件,分别存放对应环境下的敏感数据配置,如数据库相关配置,缓存相关配置等

  • .env.production
  • .env.staging
  • .env.local

并在对应的环境配置 APP_ENV 环境变量为 local,staging 或 production

配置环境变量的做法是在 nginx 的配置文件中写下这句代码:

APP_ENV production;```


**绝不** 在除 `config` 目录意外使用 `env` 函数,原因是如果运行如下命令缓存了配置后,laravel 就不会把 .env 中的配置加载到 $_ENV 全局变量中。

```bash
php artisan config:cache

2.3. 扩展包的引入

Laravel 扩展包的注册会对应用造成消耗。有一些扩展包是开发环境中专用,生产环境中并不会使用到,为了避免无用的负载, 必须严格控制其安装和加载。

2.3.1. 安装

安装开发专用扩展包时 必须 使用 --dev 参数,如:

composer require laracasts/generators --dev

2.3.2. 加载

开发专用的 provider 绝不 在 config/app.php 里面注册,必须 在 app/Providers/AppServiceProvider.php 文件中使用如以下方式:

public function register()
{
if ($this->app->environment() == 'local') {
$this->app->register('Laracasts\Generators\GeneratorsServiceProvider');
}
}

2.4. 配置信息与环境变量

假如我们有个『CDN 域名』的变量,在 Laravel 中有以下几种方法:

  1. 硬代码,直接写死。- ❌ 可维护性低
  2. 写死在 config/app.php 文件中。 - ❌ 无法区分环境进行配置
  3. 存储于 .env 文件中,使用 env() 方法直接读取。 - ❌ 虽然解决了环境变量问题但是不推荐
  4. 存储在 .env 和 config/app.php 文件中,然后使用 config() 函数来读取。- ✅ 最佳实践

第一种方法是最古老的方法,代码可维护性极低,一旦域名变更就只能全局替换。第二种方法无法区分环境,例如本地使用开发环境域名测试,线上才是正式的 CDN 域名。第三种方法虽然解决了环境变量的问题,并且也具备一定的灵活性,但是不够灵活,假如你的网站流量巨大,需要配置几个 CDN 域名,使其在加载静态资源时随机支配域名,这种做法就无法满足需求了。第四种方法即支持环境变量,又具备极高的灵活性,假如遇到同样的 CDN 多域名随机问题,你只需要写一个辅助方法,然后在 config/app.php 中调用即可,不需要动到任何一行业务逻辑代码。

2.4.1. 代码示例

.env 文件中设置:

CDN_DOMAIN=cdndomain.com

config/app.php 文件中设置:

'cdn_domain' => env('CDN_DOMAIN', null),

程序中两种获取 相同配置 的方法:

  1. env('CDN_DOMAIN')
  2. config('app.cdn_domain')

在此统一规定:所有程序配置信息 必须 通过 config() 来读取,所有的 .env 配置信息 必须 通过 config() 来读取,绝不 在配置文件以外的范围使用 env()。

2.4.2. 有何优势

这样做主要有以下几个优势:

  1. 定义分明,config() 是配置信息,env() 只是用来区分不同环境;
  2. 统一放置于 config 中还可以利用框架的 配置信息缓存功能 来提交运行效率;
  3. 代码健壮性, config() 在 env() 之上多出来一个抽象层,会使代码更加健壮,更加灵活。

注: Laravel 5.2 以后的官方文档也提倡用此方法。

2.5. 辅助函数

Laravel 提供了很多 辅助函数,有时候我们也需要创建自己的辅助函数。

必须 把所有的『自定义辅助函数』存放于 bootstrap/helpers.php 中。

并在 bootstrap/app.php 文件的最顶部进行加载:

<?php

require __DIR__ . 'helpers.php';

...

2.6. 代码风格

代码风格 必须 严格遵守 PSR2 规范

在代码提交前 应该 使用 PHP_CodeSniffer 工具做规范检查

2.7. 路由器 todo

2.7.1 路由闭包

绝不 在路由配置文件里书写『闭包路由』或者其他业务逻辑代码,因为一旦使用将无法使用 路由缓存 。

路由器要保持干净整洁,绝不 放置除路由配置以外的其他程序逻辑。

2.7.2. Restful 路由

必须 优先使用 Restful 路由,配合资源控制器使用,见 文档。

file

超出 Restful 路由的,应该 模仿上图的方式来定义路由。

2.7.3. resource 方法正确使用

一般资源路由定义:

Route::resource('photos', 'PhotosController');

等于以下路由定义:

Route::get('/photos', 'PhotosController@index')->name('photos.index');
Route::get('/photos/create', 'PhotosController@create')->name('photos.create');
Route::post('/photos', 'PhotosController@store')->name('photos.store');
Route::get('/photos/{photo}', 'PhotosController@show')->name('photos.show');
Route::get('/photos/{photo}/edit', 'PhotosController@edit')->name('photos.edit');
Route::put('/photos/{photo}', 'PhotosController@update')->name('photos.update');
Route::delete('/photos/{photo}', 'PhotosController@destroy')->name('photos.destroy');

使用 resource 方法时,如果仅使用到部分路由,必须 使用 only 列出所有可用路由:

Route::resource('photos', 'PhotosController', ['only' => ['index', 'show']]);

绝不 使用 except,因为 only 相当于白名单,相对于 except 更加直观。路由使用白名单有利于养成『安全习惯』。

2.7.3. 单数 or 复数?

资源路由路由 URI 必须 使用复数形式,如:

  • /photos/create
  • /photos/{photo}

错误的例子如:

  • /photo/create
  • /photo/{photo}

2.7.4. 路由模型绑定

在允许使用路由 模型绑定 的地方 必须 使用。

模型绑定代码 必须 放置于 app/Providers/RouteServiceProvider.php 文件的 boot 方法中:

public function boot()
{
Route::bind('user_name', function ($value) {
return User::where('name', $value)->first();
});

Route::bind('photo', function ($value) {
return Photo::find($value);
});

parent::boot();
}

2.7.5. 全局路由器参数

出于安全考虑,应该 使用全局路由器参数限制,详见 文档。

必须 在 RouteServiceProvider 文件的 boot 方法里定义模式:

/**
* 定义你的路由模型绑定,模式过滤器等。
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Router $router)
{
$router->pattern('id', '[0-9]+');

parent::boot($router);
}

模式一旦被定义,便会自动应用到所有使用该参数名称的路由上:

Route::get('users/{id}', 'UsersController@show');
Route::get('photos/{id}', 'PhotosController@show');

只有在 id 为数字时,才会路由到控制器方法中,否则 404 错误。

2.7.6. 路由命名#

除了 resource 资源路由以外,其他所有路由都 必须 使用 name 方法进行命名。

必须 使用『资源前缀』作为命名规范,如下的 users.follow,资源前缀的值是 users.:

Route::post('users/{id}/follow', 'UsersController@follow')->name('users.follow');

2.7.7. 获取 URL

必须 使用 route 来获取 URL:

$url = route('profile', ['id' => 1]);

无法使用 route 的情况下,可以 使用 url 方法来获取 URL:

url('profile', [1]);

2.8. 数据模型

2.8.1. 放置位置

所有的数据模型文件,都 必须 存放在:app/Models/ 文件夹中。

命名空间:

namespace App\Models;

2.8.2. User.php

Laravel 5.1 之后默认安装会把 User 模型存放在 app/User.php,必须 移动到 app/Models 文件夹中,并修改命名空间声明为 App/Models,同上。

为了不破坏原有的逻辑点,必须 全局搜索 App/User 并替换为 App/Models/User。

2.8.3. 使用基类

所有的 Eloquent 数据模型 都 必须 继承统一的基类 App/Models/BaseModel,此基类存放位置为 /app/Models/BaseModel.php,内容参考以下:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model as EloquentModel;

abstract class BaseModel extends EloquentModel
{

}

以 Photo 数据模型作为例子继承 BaseModel 基类:

<?php

namespace App\Models;

class Photo extends BaseModel
{
protected $fillable = ['id', 'user_id'];

public function user()
{
return $this->belongsTo(User::class);
}
}

2.8.4. 命名规范

数据模型相关的命名规范:

  • 数据模型类名 必须 为「单数」, 如:App\Models\Photo
  • 类文件名 必须 为「单数」,如:app/Models/Photo.php
  • 数据库表名字 必须 为「复数」,多个单词情况下使用「Snake Case」 如:photos, my_photos
  • 数据库表迁移名字 必须 为「复数」,如:2014_08_08_234417_create_photos_table.php
  • 数据填充文件名 必须 为「复数」,如:PhotosTableSeeder.php
  • 数据库字段名 必须 为「Snake Case」,如:view_count, is_vip
  • 数据库表主键 必须 为「id」
  • 数据库表外键 必须 为「resource_id」,如:user_id, post_id
  • 数据模型变量 必须 为「resource_id」,如:$user_id, $post_id

2.8.5. 利用 Trait 来扩展数据模型

有时候数据模型里的代码会变得很臃肿,应该 利用 Trait 来精简逻辑代码量,提高可读性,类似于 Ruby China 源码。

借鉴于 Rails 的设计理念:「Fat Models, Skinny Controllers」。

存放于文件夹:app/Models/Traits 文件夹中。

2.8.6. 关于 SQL 文件

  • 绝不 使用命令行或者 PHPMyAdmin 直接创建索引或表。必须 使用 数据库迁移 去创建表结构,并提交版本控制器中;
  • 绝不 为了共享对数据库更改就直接导出 SQL,所有修改都 必须 使用 数据库迁移 ,并提交版本控制器中;
  • 绝不 直接向数据库手动写入伪造的测试数据。必须 使用 数据填充 来插入假数据,并提交版本控制器中。

2.8.7. 全局作用域

Laravel 的 Model 全局作用域 允许我们为给定模型的所有查询添加默认的条件约束。

所有的全局作用域都 必须 统一使用 闭包定义全局作用域,如下:

/**
* 数据模型的启动方法
*
* @return void
*/
protected static function boot()
{
parent::boot();

static::addGlobalScope('age', function(Builder $builder) {
$builder->where('age', '>', 200);
});
}

2.9. 控制器

todo

2.10. 视图

todo

2.11. Artisan 命令行

所有的自定义命令,都 必须 有项目的命名空间

如:

php artisan ssc:data-manager
-->