PHP开发规范
# 1.1 关于规范
# 目的#
希望整个团队的项目经验能够得到继承,在每一次实战中不断进行总结和摸索,找到兼备开发效率、程序执行效率、扩展性和安全性的最佳实践,最终实现团体智慧的延续和精进。
# 优势
- 高效编码 - 避免了过多的选择造成的『决策时间』浪费;
- 风格统一 - 最大程度统一了开发团队成员代码书写风格和思路,代码阅读起来如出一辙;
# 开发哲学
- DRY –「Don't Repeat Yourself」不写重复的逻辑代码;
- 约定俗成 - 「Convention Over Configuration」,优先选择框架提倡的做法,不过度配置;
- KISS - 「Keep it Simple, Stupid」提倡简单易读的代码,不写高深、晦涩难懂的代码,不过度设计;
- 主厨精选 - 让有经验的人来为你选择方案,不独创方案;
- 官方提倡 - 优先选择官方推崇的方案。
# 设计理念
以下是一些优秀的『程序设计理念』:
- MVC - Model, View, Controller ,以 MVC 为核心,严格控制 Controller 的可读性和代码行数;
- Restful - 利用『资源化概念』和标准的 HTTP 动词来组织你的程序;
# 1.2 开发专用扩展包
# 说明
我们都知道 Laravel 扩展包的注册会对应用造成消耗。有一些扩展包是开发环境中专用,生产环境中并不会使用到,为了避免无用的负载, 必须严格控制其安装和加载。
# 安装
安装开发专用扩展包时 必须 使用 --dev
参数,如:
composer require laracasts/generators --dev
# 加载
开发专用的 provider 绝不
在config/app.php
里面注册,必须
在
app/Providers/AppService.php
文件中使用如以下方式:
public function register()
public function register()
{
if ($this->app->environment() == 'local') {
$this->app->register('Laracasts\Generators\GeneratorsServiceProvider');
}
}
2
3
4
5
6
7
# 2 编码规范
# 2.1 代码风格
代码风格必须严格遵循 PSR-2、PSR-4规范。
# 2.2 路由器
# 路由闭包
绝不 在路由配置文件里书写『闭包路由』或者其他业务逻辑代码,因为一旦使用将无法使用 路由缓存 (opens new window) 。
路由器要保持干净整洁,绝不 放置除路由配置以外的其他程序逻辑。
# Restful 路由
必须 优先使用 Restful 路由,配合资源控制器使用,见 文档 (opens new window)。
动作 | URI | 行为 | 路由名称 |
---|---|---|---|
GET | /photos | index | photos.index |
GET | /photos/create | create | photos.create |
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 |
超出 Restful 路由的,应该 模仿上图的方式来定义路由。
# 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');
2
3
4
5
6
7
使用 resource
方法时,如果仅使用到部分路由,必须 使用 only
列出所有可用路由:
Route::resource('photos', 'PhotosController', ['only' => ['index', 'show']]);
绝不 使用 except
,因为 only
相当于白名单,相对于 except
更加直观。路由使用白名单有利于养成『安全习惯』。
# 单数 or 复数?
资源路由路由 URI 必须 使用复数形式,如:
/photos/create
/photos/{photo}
错误的例子如:
/photo/create
/photo/{photo}
# 路由模型绑定
在允许使用路由 模型绑定 (opens new window) 的地方 必须 使用。
# 全局路由器参数
出于安全考虑,应该 使用全局路由器参数限制,详见 文档 (opens new window)。
必须 在 RouteServiceProvider
文件的 boot 方法里定义模式:
/**
* 定义你的路由模型绑定,模式过滤器等。
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Router $router)
{
$router->pattern('id', '[0-9]+');
parent::boot($router);
}
2
3
4
5
6
7
8
9
10
11
12
模式一旦被定义,便会自动应用到所有使用该参数名称的路由上:
Route::get('users/{id}', 'UsersController@show');
Route::get('photos/{id}', 'PhotosController@show');
2
只有在 id
为数字时,才会路由到控制器方法中,否则 404 错误。
# 路由命名
除了 resource
资源路由以外,其他所有路由都 必须 使用 name
方法进行命名。
必须 使用『资源前缀』作为命名规范,如下的 users.follow
,资源前缀的值是 users.
:
Route::post('users/{id}/follow', 'UsersController@follow')->name('users.follow');
# 获取 url
路由 必须 使用 route
来获取 URL:
$url = route('profile', ['id' => 1]);
无法使用 route
的情况下,可以 使用 url
方法来获取 URL:
url('profile', [1]);
# 2.3 数据模型
# 命名规范
数据模型相关的命名规范:
- 数据模型类名
必须
为「单数」, 如:App\Models\Photo
- 类文件名
必须
为「单数」,如:app/Models/Photo.php
- 数据库表名字
必须
为「复数」,多个单词情况下使用「Snake Case (opens new window)」 如:photos
,my_photos
- 数据库表迁移名字
必须
为「复数」,如:2014_08_08_234417_create_photos_table.php
- 数据填充文件名
必须
为「复数」,如:PhotosTableSeeder.php
- 数据库字段名
必须
为「Snake Case (opens new window)」,如:view_count
,is_vip
- 数据库表主键
必须
为「id」 - 数据库表外键
必须
为「resource_id」,如:user_id
,post_id
- 数据模型变量
必须
为「resource_id」,如:$user_id
,$post_id
# 关于 SQL 文件
- 绝不 使用命令行或者 PHPMyAdmin 直接创建索引或表。必须 使用 数据库迁移 (opens new window) 去创建表结构,并提交版本控制器中;
- 绝不 为了共享对数据库更改就直接导出 SQL,所有修改都 必须 使用 数据库迁移 (opens new window) ,并提交版本控制器中;
- 绝不 直接向数据库手动写入伪造的测试数据。必须 使用 数据填充 (opens new window) 来插入假数据,并提交版本控制器中。
# 2.3 控制器
# 资源控制器
必须 优先使用 Restful 资源控制器 (opens new window) 。
# 单数 or 复数?
必须 使用资源的复数形式,如:
- 类名:PhotosController
- 文件名:PhotosController.php
错误的例子:
- 类名:PhotoController
- 文件名:PhotoController.php
# 保持短小精炼
必须 保持控制器文件代码行数最小化,还有可读性。
- 不应该 为「方法」书写注释,这要求方法取名要足够合理,不需要过多注释;
- 应该 为一些复杂的逻辑代码块书写注释,主要介绍产品逻辑 -
为什么要这么做。
; - 不应该 在控制器中书写「私有方法」,控制器里
应该
只存放「路由动作方法」; - 绝不 遗留「死方法」,就是没有用到的方法,控制器里的所有方法,都应该被使用到,否则应该删除;
- 绝不 在控制器里批量注释掉代码,无用的逻辑代码就必须清除掉。
# 2.4 视图
# 保持目录清晰
- layouts - 页面布局文件 必须 放置于此目录下;
- common - 存放页面通用元素;
- pages - 简单的页面存放文件夹,如:about、contact 等;
- resources - 对应 Restful 路由的资源路径名称,以 URI
photos/create
为例,对应create.blade.php
文件,存放在文件夹photos
下。
# 局部视图
局部视图文件 必须 使用 _
前缀来命名,如:photos/_upload_form.blade.php
# 视图命名要释义
为了和 Restful 路由器和资源控制器保持一致,视图命名也 必须 使用资源视图的命名方式。以 photos
为例:
photos/index.blade.php
1- 内容列表视图
- 对应路由器
/photos
,命名photos.index
- 控制器方法
PhotosController@index
photos/show.blade.php
1- 单个内容视图
- 对应路由器
/photos/{id}
,命名photos.show
- 控制器方法
PhotosController@show
photos/create.blade.php
1- 内容创建视图
- 对应路由器
/photos/create
,命名photos.create
- 控制器方法
PhotosController@create
photos/edit.blade.php
1- 内容编辑的视图
- 对应路由器
/photos/edit
,命名photos.edit
- 控制器方法
PhotosController@edit
# create_and_edit 视图
很多情况下,创建和编辑视图里的页面结构接近相似,在这种情况下,应该 使用 create_and_edit
视图。以 photos
为例:
PhotosController@create
- 对应视图:/photos/create_and_edit.blade.php
PhotosController@edit
- 对应 视图:/photos/create_and_edit.blade.php
这样一来,通常情况下,一个完整的 photos
资源对应的视图文件为以下:
├── photos
│ ├── create_and_edit.blade.php
│ ├── index.blade.php
│ └── show.blade.php
2
3
4
# 2.5 表单验证
# 表单请求验证类
必须 使用 表单请求 - FormRequest 类 (opens new window) 来处理控制器里的表单验证。
# 使用基类
所有 FormRequest 表验证类 必须 继承 app/Http/Requests/Request.php
基类。基类文件如下:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class Request extends FormRequest
{
public function authorize()
{
// Using policy for Authorization
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 验证类命名
FormRequest 表验证类 必须 遵循 资源路由 方式进行命名,photos
对应 app/Http/Requests/PhotoRequest.php
。
# 类文件参考
FormRequest 表验证类文件请参考以下:
<?php
namespace App\Http\Requests;
class PhotoRequest extends Request
{
public function rules()
{
switch($this->method())
{
// CREATE
case 'POST':
{
return [
// CREATE ROLES
];
}
// UPDATE
case 'PUT':
case 'PATCH':
{
return [
// UPDATE ROLES
];
}
case 'GET':
case 'DELETE':
default:
{
return [];
};
}
}
public function messages()
{
return [
// Validation messages
];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 2.6 数据填充
# factory 辅助函数
必须
使用 factory
方法来做数据填充,因为是框架提倡的,并且可以同时为测试代码服务。
# 批量入库
所有假数据入库操作,都 必须 是批量操作,配合 factory
使用以下方法:
$users = factory(User::class)->times(1000)->make();
User::insert($users->toArray());
2
以上只执行一条数据库语句,推荐阅读 大批量假数据填充的正确方法 (opens new window)。
# 2.7 Artisan 命令行
所有的自定义命令,都 必须 有项目的命名空间。
如:
php artisan phphub:clear-token
php artisan phphub:send-status-email
...
2
3
错误的例子为:
php artisan clear-token
php artisan send-status-email
...
2
3
# 2.8 日期和时间
必须 使用 Carbon (opens new window) 来处理日期和时间相关的操作。
Laravel 5.1 中文的 diffForHumans
可以使用 jenssegers/date (opens new window)。
Laravel 5.3 及以上版本的 diffForHumans
,只需要在 config/app.php
文件中配置 locale
选项即可 :
'locale' => 'zh-CN',
# 2.9 前端开发
- 必须 使用 Laravel 官方前端工具做前端开发自动化;
- 必须 保证页面只加载一个
.css
文件; - 必须 保证页面只加载一个
.js
文件; - 必须 为
.css
和.js
增加 版本控制 (opens new window); - 必须 使用 SASS (opens new window) 来书写 CSS 代码;
# 2.10 中间件
# Auth 中间件
Auth 中间件 必须 书写在控制器的 __construct
方法中,并且 必须 使用 except
黑名单进行过滤,这样当你新增控制器方法时,默认是安全的。
public function __construct()
{
$this->middleware('auth', [
'except' => ['show', 'index']
]);
}
2
3
4
5
6
# 3.1 laravel 安全实践
# 关闭 debug
Laravel Debug 开启时,会暴露很多能被黑客利用的服务器信息,所以,生产环境下请 必须 确保:
APP_DEBUG=false
# XSS
跨站脚本攻击(cross-site scripting,简称 XSS),具体危害体现在黑客能控制你网站页面,包括使用 JS 盗取 Cookie 等,关于 XSS 的介绍请前往 IBM 文档库:跨站点脚本攻击深入解析 (opens new window) 。
默认情况下,在无法保证用户提交内容是 100% 安全的情况下,必须 使用 Blade 模板引擎的 语法会对用户内容进行转义。
Blade 的 {!! $content !!}
语法会直接对内容进行 非转义 输出,使用此语法时,必须 使用 HTMLPurifier for Laravel 5 (opens new window) 来为用户输入内容进行过滤。使用方法参见: 使用 HTMLPurifier 来解决 Laravel 5 中的 XSS 跨站脚本攻击安全问题 (opens new window)
# SQL 注入
Laravel 的 查询构造器 (opens new window) 和 Eloquent (opens new window) 是基于 PHP 的 PDO,PDO 使用 prepared
来准备查询语句,保障了安全性。
在使用 raw()
来编写复杂查询语句时,必须 使用数据绑定。
错误的做法:
Route::get('sql-injection', function() {
$name = "admin"; // 假设用户提交
$password = "xx' OR 1='1"; // // 假设用户提交
$result = DB::select(DB::raw("SELECT * FROM users WHERE name ='$name' and password = '$password'"));
dd($result);
});
2
3
4
5
6
以下是正确的做法,利用 select 方法 (opens new window) 的第二个参数做数据绑定:
Route::get('sql-injection', function() {
$name = "admin"; // 假设用户提交
$password = "xx' OR 1='1"; // // 假设用户提交
$result = DB::select(
DB::raw("SELECT * FROM users WHERE name =:name and password = :password"),
[
'name' => $name,
'password' => $password,
]
);
dd($result);
});
2
3
4
5
6
7
8
9
10
11
12
DB
类里的大部分执行 SQL 的函数都可传参第二个参数 $bindings
,详见:API 文档 (opens new window) 。
# 批量赋值
Laravel 提供白名单和黑名单过滤($fillable
和 $guarded
),开发者 应该 清楚认识批量赋值安全威胁的情况下合理灵活地运用。
批量赋值安全威胁,指的是用户可更新本来不应有权限更新的字段。举例,users
表里的 is_admin
字段是用来标识用户『是否是管理员』,某不怀好意的用户,更改了『修改个人资料』的表单,增加了一个字段:
<input name="is_admin" value="1" />
这个时候如果你更新代码如下:
Auth::user()->update(Request::all());
此用户将获取到管理员权限。可以有很多种方法来避免这种情况出现,最简单的方法是通过设置 User 模型里的 $guarded
字段来避免:
protected $guarded = ['id', 'is_admin'];
# CSRF
CSRF 跨站请求伪造是 Web 应用中最常见的安全威胁之一,具体请见 Wiki - 跨站请求伪造 (opens new window) 或者 Web 应用程序常见漏洞 CSRF 的入侵检测与防范 (opens new window)。
Laravel 默认对所有『非幂等的请求』强制使用 VerifyCsrfToken
中间件防护,需要开发者做的,是区分清楚什么时候该使用『非幂等的请求』。
幂等请求指的是:'HEAD', 'GET', 'OPTIONS',既无论你执行多少次重复的操作都不会给资源造成变更。
- 所有删除的动作,必须 使用 DELETE 作为请求方法;
- 所有对数据更新的动作,必须 使用 POST、PUT 或者 PATCH 请求方法。
# 3.2 laravel 程序优化
# 配置信息缓存
生产环境中的 应该 使用『配置信息缓存』来加速 Laravel 配置信息的读取。
使用以下 Artisan 自带命令,把 config
文件夹里所有配置信息合并到一个文件里,减少运行时文件的载入数量:
php artisan config:cache
缓存文件存放在 bootstrap/cache/
文件夹中。
可以使用以下命令来取消配置信息缓存:
php artisan config:clear
注意:配置信息缓存不会随着更新而自动重载,所以,开发时候建议关闭配置信息缓存,一般在生产环境中使用。可以配合 Envoy 任务运行器 (opens new window) 使用,在每次上线代码时执行 config:clear
命令。
# 路由缓存
生产环境中的 应该 使用『路由缓存』来加速 Laravel 的路由注册。
路由缓存可以有效的提高路由器的注册效率,在大型应用程序中效果越加明显,可以使用以下命令:
php artisan route:cache
缓存文件存放在 bootstrap/cache/
文件夹中。另外,路由缓存不支持路由匿名函数编写逻辑,详见:文档 - 路由缓存 (opens new window)。
可以使用下面命令清除路由缓存:
php artisan route:clear
注意:路由缓存不会随着更新而自动重载,所以,开发时候建议关闭路由缓存,一般在生产环境中使用。可以配合 Envoy 任务运行器 (opens new window) 使用,在每次上线代码时执行 route:clear
命令。
# 类映射加载优化
optimize
命令把常用加载的类合并到一个文件里,通过减少文件的加载,来提高运行效率。生产环境中的 应该使用 optimize 命令来优化类的加载速度:
php artisan optimize --force
以上命令会在 bootstrap/cache/
文件夹中生成缓存文件。你可以可以通过修改 config/compile.php
文件来添加要合并的类。在 production
环境中,参数 --force
不需要指定,文件就会自动生成。
要清除类映射加载优化,请运行以下命令:
php artisan clear-compiled
此命令会删除上面 optimize
生成的两个文件。
注意:此命令要运行在 php artisan config:cache
后,因为 optimize
命令是根据配置信息(如:config/app.php
文件的 providers
数组)来生成文件的。
# 自动加载优化
此命令不止针对于 Laravel 程序,适用于所有使用 composer
来构建的程序。此命令会把 PSR-0
和 PSR-4
转换为一个类映射表,来提高类的加载速度。
composer dumpautoload -o
注意:
php artisan optimize --force
命令里已经做了这个操作。
# 数据库请求优化
关联模型数据读取时 必须 使用 延迟预加载 (opens new window) 和 预加载 (opens new window) 。
临近上线时 必须 使用 Laravel Debugbar (opens new window) 或者 Clockwork (opens new window) 留意每一个页面的总 SQL 请求条数,进行数据库请求调优。
# 为数据集书写缓存逻辑
应该 合理的使用 Laravel 提供的缓存层操作,把从数据库里面拿出来的数据集合进行缓存,减少数据库的压力,运行在内存上的专业缓存软件对数据的读取也远远快于数据库。
$hot_posts = Cache::remember('posts.hot_posts', $minutes = 30, function()
{
return Post::getHotPosts();
});
2
3
4
remember
甚至连数据关联模型也都一并缓存了,多么方便呀。
# 4.1 推荐阅读
# 文档资料
- 必须 熟记 Laravel 5.5 官方文档 (opens new window),查阅时能快速定位,5 遍以上;
- 必须 熟记 Laravel 5.5 API 文档 (opens new window) 的类结构,查阅时能快速定位;
- 必须 熟记所有 PSR 通过的标准 (opens new window);
- PSR 目前还未通过的标准,也要 应该 知晓 http://www.php-fig.org/psr/#draft (opens new window)
- 应该 熟悉 PHP 最佳实践 (opens new window)
—————————参考自 laravel China 项目开发规范 —————————