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 环境变量为 localstagingproduction

配置环境变量的做法是在 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. 存储在 .envconfig/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
-->