本文章并不是 Laravel 对应的 SOLID 原则和相关模式的说明。而是我们在实际的 Laravel 项目中经常忽略的最佳实践。
- 1. 单一职责原则
- 2. 丰富模型,精简控制器
- 3. 验证
- 4. 业务逻辑放在业务类中
- 5. 不要重复自己(DRY)
- 6. 优先使用 Eloquent 和集合
- 7. 批量赋值
- 8. 不要在 Blade 模板中执行查询,使用预加载(避免 N+1)
- 9. 为代码写注释,但优先使用描述性方法名或变量名
- 10. 不要在 Blade 模板中放 JS 和 CSS,不要在 PHP 类中放 HTML
- 11. 使用配置和语言文件、常量,而不是在代码中写死
- 12. 使用社区接受的标准的 Laravel 工具
- 13. 遵循 Laravel 命名约定
- 14. 尽量使用更短、更易阅读的语法
- 15. 使用容器的依赖注入而不是用 new 实例化一个类
- 16. 不要直接从 .env 文件获取数据
- 17. 以标准格式存储日期。使用访问器和修改器修改日期格式
- 18. 其它好的实践
1. 单一职责原则
一个类和一个方法应该只有一个职责。
错误:
public function getFullNameAttribute()
{
if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
} else {
return $this->first_name[0] . '. ' . $this->last_name;
}
}
正确:
public function getFullNameAttribute()
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient()
{
return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}
public function getFullNameLong()
{
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}
public function getFullNameShort()
{
return $this->first_name[0] . '. ' . $this->last_name;
}
2. 丰富模型,精简控制器
如果使用查询构造器或原生的 SQL 查询,请将所有数据库相关的逻辑放到 Eloquent 模型中或者存储库类中。
错误:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
return view('index', ['clients' => $clients]);
}
正确:
public function index()
{
return view('index', ['clients' => $this->client->getWithNewOrders()]);
}
class Client extends Model
{
public function getWithNewOrders()
{
return $this->verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
}
}
3. 验证
将验证从控制器移动到请求类。
错误:
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
....
}
正确:
public function store(PostRequest $request)
{
....
}
class PostRequest extends Request
{
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
];
}
}
4. 业务逻辑放在业务类中
由于一个控制器必须只有一个职责,因此将业务逻辑从控制器移动到业务类中。
错误:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}
....
}
正确:
public function store(Request $request)
{
$this->articleService->handleUploadedImage($request->file('image'));
....
}
class ArticleService
{
public function handleUploadedImage($image)
{
if (!is_null($image)) {
$image->move(public_path('images') . 'temp');
}
}
}
5. 不要重复自己(DRY)
在可以复用代码的地方复用代码。SRP(单一职责原则)帮助你避免重复代码。当然,也可以复用 Blade 模板,使用 Eloquent 作用域等。
错误:
public function getActive()
{
return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
}
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->where('verified', 1)->whereNotNull('deleted_at');
})->get();
}
正确:
public function scopeActive($q)
{
return $q->where('verified', 1)->whereNotNull('deleted_at');
}
public function getActive()
{
return $this->active()->get();
}
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->active();
})->get();
}
6. 优先使用 Eloquent 和集合
优先使用 Eloquent 而不是查询构造器或原生 SQL。优先使用集合而不是数组。
Eloquent 可以让您编写易于阅读和易于维护的代码。同时,Eloquent 也内置了很好的工具,例如软删除、事件、作用域等。
错误:
SELECT *
FROM `articles`
WHERE EXISTS (SELECT *
FROM `users`
WHERE `articles`.`user_id` = `users`.`id`
AND EXISTS (SELECT *
FROM `profiles`
WHERE `profiles`.`user_id` = `users`.`id`)
AND `users`.`deleted_at` IS NULL)
AND `verified` = '1'
AND `active` = '1'
ORDER BY `created_at` DESC
正确:
Article::has('user.profile')->verified()->latest()->get();
7. 批量赋值
错误:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// 为文章添加分类
$article->category_id = $category->id;
$article->save();
正确:
$category->article()->create($request->all());
8. 不要在 Blade 模板中执行查询,使用预加载(避免 N+1)
错误(如果有 100 个用户,会执行 101 条数据库查询):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
正确(如果有 100 个用户,会执行 2 条数据库查询):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
9. 为代码写注释,但优先使用描述性方法名或变量名
错误:
if (count((array) $builder->getQuery()->joins) > 0)
更好:
// 判断是否有任何连接
if (count((array) $builder->getQuery()->joins) > 0)
正确:
if ($this->hasJoins())
10. 不要在 Blade 模板中放 JS 和 CSS,不要在 PHP 类中放 HTML
错误:
let article = `{{ json_encode($article) }}`;
更好:
<input id="article" type="hidden" value="@json($article)">
或
<button class="js-fav-article" data-article="@json($article)">{{ $article->name }}<button>
在 Javascript 文件中:
let article = $('#article').val();
正确的方式是使用专门的 PHP 转 JS 扩展包来传递数据。
11. 使用配置和语言文件、常量,而不是在代码中写死
错误:
public function isNormal()
{
return $article->type === 'normal';
}
return back()->with('message', 'Your article has been added!');
正确:
public function isNormal()
{
return $article->type === Article::TYPE_NORMAL;
}
return back()->with('message', __('app.article_added'));
12. 使用社区接受的标准的 Laravel 工具
优先使用自带的 Laravel 功能和社区扩展包,而不是使用第三方扩展包和工具。因为任何要加入应用的开发者都需要学习新的工具。同时,使用第三方扩展包或工具时,从 Laravel 社区获取帮助的机会会大大降低。不要让客户为此付出代价。
任务 | 标准工具 | 第三方工具 |
---|---|---|
授权 | 策略类 | Entrust,Sentinel 和其它扩展包 |
编译资源文件 | Laravel Mix | Grunt,Gulp,第三方扩展包 |
开发环境 | Homestead | Docker |
部署 | Laravel Forge | Deployer 或其它解决方案 |
单元测试 | PHPUnit,Mockery | Phpspec |
浏览器测试 | Laravel Dusk | Codeception |
操作数据库 | Eloquent | SQL,Doctrine |
模板 | Blade | Twig |
处理数据 | Laravel 集合 | 数组 |
表单验证 | Request 类 | 第三方扩展包,控制器中的验证 |
身份认证 | 自带 | 第三方扩展包,你自己的解决方案 |
API 认证 | Laravel Passport | 第三方 JWT 和 OAuth 扩展包 |
创建 API | 自带 | Dingo API 和类似的扩展包 |
操作数据库结构 | 数据库迁移 | 直接操作数据库结构 |
本地化 | 自带 | 第三方扩展包 |
实时用户接口 | Laravel Echo,Pusher | 第三方扩展包,直接使用 WebSockets 处理 |
生成测试数据 | Seeder 类,模型工厂,Faker | 手动创建测试数据 |
任务调度 | Laravel 任务调度程序 | 脚本或第三方扩展包 |
数据库 | MySQL,PostgreSQL,SQLite,SQL Server | MongoDB |
13. 遵循 Laravel 命名约定
遵循 PSR-2 规范。
同时,遵循 Laravel 社区接受的命名约定:
类型 | 约定 | 正确 | 错误 |
---|---|---|---|
控制器 | 单数 | ArticleController | ArticlesController |
路由 | 复数 | articles/1 | article/1 |
命名路由 | 带点的蛇形命名法 | users.show_active | users.show-active,show-active-users |
模型 | 单数 | User | Users |
hasOne 或 belongsTo 关联 | 单数 | articleComment | articleComments,article_comment |
所有其它关联 | 复数 | articleComments | articleComment,article_comments |
数据表 | 复数 | article_comments | article_comment,articleComments |
中间表 | 按字母排序的模型名单数 | article_user | user_article,articles_users |
数据表字段 | 不带模型名的蛇形命名法 | meta_title | MetaTitle,article_meta_title |
模型属性 | 蛇形命名法 | $model->created_at | $model->createdAt |
外键 | 模型名单数加 _id 后缀 | article_id | ArticleId,id_article,articles_id |
主键 | - | id | custom_id |
迁移文件 | - | 2017_01_01_000000_create_articles_table | 2017_01_01_000000_articles |
方法 | 驼峰命名法 | getAll | get_all |
资源控制器方法 | 参见表格 | store | saveArticle |
测试类方法 | 驼峰命名法 | testGuestCannotSeeArticle | test_guest_cannot_see_article |
变量 | 驼峰命名法 | $articlesWithAuthor | $articles_with_author |
集合 | 描述性,复数 | $activeUsers = User::active()->get() | $active,$data |
对象 | 描述性,单数 | $activeUser = User::active()->first() | $users,$obj |
配置和语言文件索引项 | 蛇形命名法 | articles_enabled | ArticlesEnabled,articles-enabled |
视图 | 蛇形命名法 | show_filtered.blade.php | showFiltered.blade.php,show-filtered.blade.php |
配置 | 蛇形命名法 | google_calendar.php | googleCalendar.php,google-calendar.php |
Contract(接口) | 形容词或名词 | Authenticatable | AuthenticationInterface,IAuthentication |
Trait | 形容词 | Notifiable | NotificationTrait |
14. 尽量使用更短、更易阅读的语法
错误:
$request->session()->get('cart');
$request->input('name');
正确:
session('cart');
$request->name;
更多示例:
常见的语法 | 更短并更易阅读的语法 |
---|---|
Session::get('cart') |
session('cart') |
$request->session()->get('cart') |
session('cart') |
Session::put('cart', $data) |
session(['cart' => $data]) |
$request->input('name'), Request::get('name') |
$request->name, request('name') |
return Redirect::back() |
return back() |
is_null($object->relation) ? null : $object->relation->id |
optional($object->relation)->id |
return view('index')->with('title', $title)->with('client', $client) |
return view('index', compact('title', 'client')) |
$request->has('value') ? $request->value : 'default'; |
$request->get('value', 'default') |
Carbon::now(), Carbon::today() |
now(), today() |
App::make('Class') |
app('Class') |
->where('column', '=', 1) |
->where('column', 1) |
->orderBy('created_at', 'desc') |
->latest() |
->orderBy('age', 'desc') |
->latest('age') |
->orderBy('created_at', 'asc') |
->oldest() |
->select('id', 'name')->get() |
->get(['id', 'name']) |
->first()->name |
->value('name') |
15. 使用容器的依赖注入而不是用 new
实例化一个类
使用 new
实例化类会在类和复杂的测试紧密耦合。因此应该使用容器的依赖注入或 Facade。
错误:
$user = new User;
$user->create($request->all());
正确:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->all());
16. 不要直接从 .env
文件获取数据
应该将环境变量传递给配置文件,然后使用 config()
辅助函数在应用中使用数据。
错误:
$apiKey = env('API_KEY');
正确:
// config/api.php
'key' => env('API_KEY'),
// 获取数据
$apiKey = config('api.key');
17. 以标准格式存储日期。使用访问器和修改器修改日期格式
错误:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
正确:
// 模型
protected $dates = ['ordered_at', 'created_at', 'updated_at']
public function getSomeDateAttribute($date)
{
return $date->format('m-d');
}
// 视图
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}
18. 其它好的实践
绝不在路由文件中放置任何逻辑。
在 Blade 模板中少使用原生 PHP。
本文翻译自:Laravel best practices