Laravel 的十八个最佳实践

本文翻译改编自 Laravel 的十八个最佳实践
这篇文章并不是什么由 Laravel 改编的 SOLID 原则、模式等。
只是为了让你注意你在现实生活的 Laravel 项目中最常忽略的内容。
单一责任原则
一个类和一个方法应该只有一个职责。
错误的做法:
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;
}
强大的模型 & 简单控制器
如果你使用查询构造器或原始 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();
}
}
验证
将验证从控制器移动到请求类。
很常见但不推荐的做法:
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',
];
}
}
业务逻辑应该在服务类中
一个控制器必须只有一个职责,因此应该将业务逻辑从控制器移到服务类。
坏:
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');
}
}
}
不要重复你自己(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();
}
最好倾向于使用 Eloquent 而不是 Query Builder 和原生的 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();
批量赋值
比如你这样写:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();
是不是还不如这样写:
$category->article()->create($request->all());
不要在 Blade 模板中执行查询并使用关联加载(N + 1 问题)
不好的地方在于,这对于100 个用户来说,等于执行 101 个 DB 查询:
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
下面的做法,对于 100 个用户来说,仅仅只执行 2 个 DB 查询:
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
与其花尽心思给你的代码写注释,还不如对方法或变量写一个描述性的名称
坏:
if (count((array) $builder->getQuery()->joins) > 0)
好:
// 确定是否有任何连接。
if (count((array) $builder->getQuery()->joins) > 0)
最好:
if ($this->hasJoins())
不要把 JS 和 CSS 放在 Blade 模板中,也不要将任何 HTML 放在 PHP 类中
坏:
let article = `{{ json_encode($article) }}`;
好:
<input id="article" type="hidden" value="{{ json_encode($article) }}">
Or
<button class="js-fav-article" data-article="{{ json_encode($article) }}">{{ $article->name }}<button>
最好的方法是使用在 Javascript 中这样来传输数据:
let article = $('#article').val();
在代码中使用配置和语言文件、常量,而不是写死它
坏:
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'));
使用社区接受的标准的 Laravel 工具
最好使用内置的 Laravel 功能和社区软件包,而不是其他第三方软件包和工具。因为将来与你的应用程序一起工作的开发人员都需要学习新的工具。另外,使用第三方软件包或工具的话,如果遇到困难,从 Laravel 社区获得帮助的机会会大大降低。不要让你的客户为此付出代价!
| 任务 | 标准工具 | 第三方工具 |
|---|---|---|
| 授权 | Policies | Entrust, Sentinel and other packages |
| 前端编译 | Laravel Mix | Grunt, Gulp, 3rd party packages |
| 开发环境 | Homestead | Docker |
| 部署 | Laravel Forge | Deployer and other solutions |
| 单元测试 | PHPUnit, Mockery | Phpspec |
| 浏览器测试 | Laravel Dusk | Codeception |
| 数据库操作 | Eloquent | SQL, Doctrine |
| 模板 | Blade | Twig |
| 数据操作 | Laravel collections | Arrays |
| 表单验证 | Request classes | 3rd party packages, validation in controller |
| 认证 | Built-in | 3rd party packages, your own solution |
| API 认证 | Laravel Passport | 3rd party JWT and OAuth packages |
| 创建 API | Built-in | Dingo API and similar packages |
| 数据库结构操作 | Migrations | Working with DB structure directly |
| 局部化 | Built-in | 3rd party packages |
| 实时用户接口 | Laravel Echo, Pusher | 3rd party packages and working with WebSockets directly |
| Generating testing data | Seeder classes, Model Factories, Faker | Creating testing data manually |
| 生成测试数据 | Laravel Task Scheduler | Scripts and 3rd party packages |
| 数据库 | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
遵循Laravel命名约定
遵循 PSR 标准。 另外,请遵循 Laravel 社区接受的命名约定:
| 类型 | 规则 | 正确示例 | 错误示例 |
|---|---|---|---|
| Controller | 单数 | ArticleController | |
| Route | 复数 | articles/1 | |
| Named route | 带点符号的蛇形命名 | users.show_active | |
| Model | 单数 | User | |
| hasOne or belongsTo relationship | 单数 | articleComment | |
| All other relationships | 复数 | articleComments | |
| Table | 复数 | article_comments | |
| Pivot table | 按字母顺序排列的单数模型名称 | article_user | |
| Table column | 带着模型名称的蛇形命名 | meta_title | |
| Foreign key | 带_id后缀的单数型号名称 | article_id | |
| Primary key | - | id | |
| Migration | - | 2017_01_01_000_create_xx_table | |
| Method | 小驼峰命名 | getAll | |
| Method in resource controller | 具体看表格 | store | |
| Method in test class | 小驼峰命名 | testGuestCannotSeeArticle | |
| Variable | 小驼峰命名 | $articlesWithAuthor | |
| Collection | 具描述性的复数形式 | $activeUsers = User::active()->get() | |
| Object | 具描述性的单数形式 | $activeUser = User::active()->first() | |
| Config and language files index | 蛇形命名 | articles_enabled | |
| View | 蛇形命名 | show_filtered.blade.php | |
| Config | 蛇形命名 | google_calendar.php | |
| Contract (interface) | 形容词或名词 | Authenticatable | |
| Trait | 形容词 | Notifiable |
尽可能使用更短、更易读的语法
坏:
$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) ? $object->relation->id : null } |
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') |
使用 IoC 容器或 facades 代替新的 Class
新的 Class 语法创建类时,不仅使得类与类之间紧密耦合,还加重了测试的复杂度。推荐改用 IoC 容器或 facades。
坏:
$user = new User;
$user->create($request->all());
好:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->all());
不要直接从 .env 文件获取数据
将数据传递给配置文件,然后使用辅助函数 config() 在应用程序中使用数据。
坏:
$apiKey = env('API_KEY');
好:
// config/api.php
'key' => env('API_KEY'),
// Use the data
$apiKey = config('api.key');
以标准格式存储日期,必要时就使用访问器和修改器来修改日期格式
坏:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
好:
// Model
protected $dates = ['ordered_at', 'created_at', 'updated_at']
public function getMonthDayAttribute($date)
{
return $date->format('m-d');
}
// View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->monthDay }}
其他良好做法
- 千万不要在路由文件中放置任何逻辑。
- 在 Blade 模板中最小化 vanilla PHP 的使用。
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
高认可度评论:
我刚翻译完,算了算了
话说,既然有这么多标准.为什么平时很少有遵守,包括你们出的教程也没有遵守额.我不是来找茬,只是想了解下.
行动迅速啊,厉害了:+1:
刚刚在github看这个文章的原版
这里这里
@bitqiu 这个应该是站长大人的坑 @Summer
我刚翻译完,算了算了
翻译的好快啊
这到底是坏还是好!看了半年没能区分出来 :grin:
@Olivia-outshine 这个部分是在讲注释。不好的是没有注释,好的是有注释,下面更好的方案是名字起得好连注释都免了。这样能理解了么?
@JokerLinly 他这里只显示标题 内容也翻译了
好厉害,赞一个,哈哈
是否TP框架也是通用呢!! 感觉很规范的样子呢~
学到很多
非常好!
求指点,请问这里的服务是指服务提供者吗?
这句没看懂,在 Controller 里面直接调用 articleService 方法?
@FreeMason 不是,建议你去学习一下 MVC
@JokerLinly
MVC 思想每个人理解都会有点差异,个人简单理解:M 数据逻辑层,C 调度中心, V 显示层,看来之前 MVC 的理解还不够全不够深入,百度上的 MVC 大同小异,像你上面的模式。能推荐篇好的 MVC 文章吗?
@FreeMason 博客: 面对现实吧!维护大型 PHP 应用程序不简单!
控制器层这样写基本跟配置文件差不多了,控制器层应该负责调度。。。
话说 既然都翻译完了 为什么不扔到github上呢 其实我之前一直想翻译这篇来着 手头的活太多了 然后刚想开始干活就被翻译了哈哈哈
@FreeMason 关于
是在构造函数注入过,只是作者省略了这一步
IOC的思想了解一下
这里不应该是$q->scopeActive()吗?怎么是 $q->active();
@jyliumin 建议你回去翻文档,或者,直接看框架底层代码
@JokerLinly 好吧,找到了
最近在重构以前写的代码。看这个深有体会。
请问不要在路由文件中使用逻辑,是为什么?
这篇文章真是每次看都会有收获,赞一个。
真没注意使用IOC这条
@Olivia-outshine 加了个注释,这样就变成好的了
@jyliumin scope在手册中有讲,局部范围限制查询的时候用到,使用中不带scope
@JokerLinly Service和Repository设计感觉好像?最近在了解repository仓库,意思貌似是讲控制器单一原则,将复杂的控制器逻辑交给repository在处理,这个service好像,他们可以理解为同一类意思吗?
@Summer AT的识别机制缺陷啊,代码块里blade模板的@foreach 被强行替换为md格式的链接了。详情看文中的示例代码
@mokeyjay fixed
话说,既然有这么多标准.为什么平时很少有遵守,包括你们出的教程也没有遵守额.我不是来找茬,只是想了解下.
@Olivia-outshine hasJoins
这里单词错误
@xiaofeng94 好滴,改过来了
@Chasers9527 手速没琳姐快。。
@select_and_action 那是必须的
PSR-2 标准
学了 《L06 Laravel 教程 - 电商进阶 ( Laravel 5.8 ) 》
里面控制器命名为 ProductsControler,这里是复数
这里说控制器命名标准应该写为单数,那么应该是 ProductControler 不太懂区分了~
这文章不错,之前写代码都没注意
使用ioc容器在来构造方法里面注入,如果我注入的实例不是在每个方法都用到,注入进来不就浪费了吗?
verified() 这个啥意思
控制器命名社区文档中的《Laravel 开发规范》中不是建议使用复数命名的吗
@nbutluo 单数居多吧,这种算约定,而且共识程度也不算高,没必要“必须”遵守。