Laravel 开发最佳实践:18个提升代码质量的黄金法则
Orion K Lv6

Laravel 开发最佳实践:18个提升代码质量的黄金法则

这篇文章并不是什么由 Laravel 改编的 SOLID 原则、模式等。只是为了让你注意你在现实生活的 Laravel 项目中最常忽略的内容。这些最佳实践都是从实际项目开发中总结出来的经验,能够帮助你写出更加优雅、可维护的 Laravel 代码。

1. 单一职责原则

一个类和一个方法应该只有一个职责。

错误的做法:

1
2
3
4
5
6
7
8
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;
}
}

推荐的做法:

1
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
public function getFullNameAttribute()
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}

/**
* 检查是否为已验证的客户端用户
* @return bool
*/
public function isVerifiedClient()
{
return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}

/**
* 获取完整姓名(长格式)
* @return string
*/
public function getFullNameLong()
{
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}

/**
* 获取简短姓名格式
* @return string
*/
public function getFullNameShort()
{
return $this->first_name[0] . '. ' . $this->last_name;
}

2. 强大的模型 & 简单控制器

如果你使用查询构造器或原始 SQL 来查询,请将所有与数据库相关的逻辑放入 Eloquent 模型或存储库类中。

错误的做法:

1
2
3
4
5
6
7
8
9
10
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();

return view('index', ['clients' => $clients]);
}

推荐的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 控制器
public function index()
{
return view('index', ['clients' => $this->client->getWithNewOrders()]);
}

// 模型
class Client extends Model
{
/**
* 获取有新订单的已验证客户
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getWithNewOrders()
{
return $this->verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
}
}

3. 验证逻辑分离

将验证从控制器移动到请求类。

错误的做法:

1
2
3
4
5
6
7
8
9
10
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);

// ...
}

推荐的做法:

1
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
// 控制器
public function store(PostRequest $request)
{
// ...
}

// 请求类
class PostRequest extends FormRequest
{
/**
* 获取验证规则
* @return array
*/
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
];
}

/**
* 获取自定义错误消息
* @return array
*/
public function messages()
{
return [
'title.required' => '标题不能为空',
'title.unique' => '标题已存在',
'body.required' => '内容不能为空',
];
}
}

4. 业务逻辑应该在服务类中

一个控制器必须只有一个职责,因此应该将业务逻辑从控制器移到服务类。

错误的做法:

1
2
3
4
5
6
7
8
public function store(Request $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}

// ...
}

推荐的做法:

1
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
// 控制器
public function store(Request $request)
{
$this->articleService->handleUploadedImage($request->file('image'));

// ...
}

// 服务类
class ArticleService
{
/**
* 处理上传的图片
* @param \Illuminate\Http\UploadedFile|null $image
* @return string|null 返回图片路径
*/
public function handleUploadedImage($image)
{
if (!is_null($image)) {
$filename = time() . '_' . $image->getClientOriginalName();
$image->move(public_path('images'), $filename);
return 'images/' . $filename;
}

return null;
}
}

5. 不要重复你自己(DRY)

尽可能重用代码。SRP(单一职责原则)正在帮助你避免重复。当然,这也包括了 Blade 模板、Eloquent 的范围等。

错误的做法:

1
2
3
4
5
6
7
8
9
10
11
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();
}

推荐的做法:

1
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
/**
* 活跃用户查询范围
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('verified', 1)->whereNotNull('deleted_at');
}

/**
* 获取活跃用户
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getActive()
{
return $this->active()->get();
}

/**
* 获取活跃用户的文章
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->active();
})->get();
}

6. 优先使用 Eloquent 而不是 Query Builder 和原生 SQL

Eloquent 可以编写可读和可维护的代码。此外,Eloquent 也拥有很棒的内置工具,比如软删除、事件、范围等。

错误的做法:

1
2
3
4
5
6
7
8
9
10
11
12
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

推荐的做法:

1
2
3
4
5
6
// 使用 Eloquent 关联和范围
Article::has('user.profile')
->verified()
->active()
->latest()
->get();

7. 批量赋值

错误的做法:

1
2
3
4
5
6
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
$article->category_id = $category->id;
$article->save();

推荐的做法:

1
2
3
4
5
6
7
8
9
10
// 使用关联创建
$category->articles()->create($request->validated());

// 或者使用批量赋值
$article = Article::create([
'title' => $request->title,
'content' => $request->content,
'verified' => $request->verified,
'category_id' => $category->id,
]);

8. 不要在 Blade 模板中执行查询

使用关联加载解决 N+1 问题。

错误的做法:

1
2
3
4
5
// 控制器
public function index()
{
return view('index', ['users' => User::all()]);
}
1
2
3
4
{{-- 视图 --}}
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach

推荐的做法:

1
2
3
4
5
// 控制器
public function index()
{
return view('index', ['users' => User::with('profile')->get()]);
}
1
2
3
4
{{-- 视图 --}}
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach

9. 注释你的代码,但是更倾向于描述性的方法和变量名

错误的做法:

1
2
// 检查用户是否活跃
if (count((array) $builder->getQuery()->joins) > 0)

推荐的做法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 检查查询是否包含 JOIN 子句
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return bool
*/
if ($this->hasJoins($builder))

private function hasJoins($builder)
{
return count((array) $builder->getQuery()->joins) > 0;
}

10. 不要把 JS 和 CSS 放在 Blade 模板中

不要把 HTML 放在 PHP 类中。

错误的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{-- 视图文件 --}}
<script>
let app = new Vue({
el: '#app',
data: {
messages: []
}
});
</script>

<style>
.container {
margin: 0 auto;
}
</style>

推荐的做法:

1
2
3
4
5
6
7
8
{{-- 视图文件 --}}
@push('scripts')
<script src="{{ mix('js/pages/dashboard.js') }}"></script>
@endpush

@push('styles')
<link href="{{ mix('css/pages/dashboard.css') }}" rel="stylesheet">
@endpush

11. 在代码中使用配置和语言文件、常量

而不是硬编码。

错误的做法:

1
2
3
4
5
6
public function isNormal()
{
return $this->status === 1;
}

return back()->with('message', 'Your article has been added!');

推荐的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 配置文件 config/article.php
return [
'statuses' => [
'draft' => 0,
'normal' => 1,
'featured' => 2,
]
];

// 语言文件 resources/lang/zh/messages.php
return [
'article_added' => '文章添加成功!',
];

// 模型
public function isNormal()
{
return $this->status === config('article.statuses.normal');
}

// 控制器
return back()->with('message', __('messages.article_added'));

12. 使用 Laravel 社区接受的标准工具

优先使用内置的 Laravel 功能和社区包,而不是第三方包和工具。

任务 标准工具 第三方工具
授权 Policies Entrust, Sentinel 等
编译资源 Laravel Mix Grunt, Gulp 等
开发环境 Laravel Sail, Homestead Docker
部署 Laravel Forge Deployer 等
单元测试 PHPUnit, Mockery Phpspec
浏览器测试 Laravel Dusk Codeception
数据库 Eloquent SQL, Doctrine
模板 Blade Twig
数据操作 Laravel 集合 数组

13. 遵循 Laravel 命名约定

遵循 PSR 标准

命名规范:

类型 规则 正确 错误
控制器 单数 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
契约(接口) 形容词或名词 AuthenticationInterface Authenticatable, IAuthentication
Trait 形容词 Notifiable NotificationTrait

14. 尽可能使用更短、更可读的语法

错误的做法:

1
2
$request->session()->get('cart');
$request->input('name');

推荐的做法:

1
2
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'))

15. 使用 IoC 容器或门面而不是新的类

新的类语法创建了类之间的紧密耦合,使测试变得复杂。使用 IoC 容器或门面代替。

错误的做法:

1
2
$user = new User;
$user->create($request->validated());

推荐的做法:

1
2
3
4
5
6
7
8
9
10
11
// 使用依赖注入
public function __construct(User $user)
{
$this->user = $user;
}

// 在方法中使用
$this->user->create($request->validated());

// 或者使用门面
User::create($request->validated());

16. 不要直接从 .env 文件获取数据

将数据传递给配置文件,然后使用 config() 辅助函数在应用程序中使用数据。

错误的做法:

1
$apiKey = env('API_KEY');

推荐的做法:

1
2
3
4
5
6
7
// config/api.php
return [
'key' => env('API_KEY'),
];

// 使用配置
$apiKey = config('api.key');

17. 以标准格式存储日期,使用访问器和修改器来修改日期格式

错误的做法:

1
2
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}

推荐的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 模型
protected $dates = ['ordered_at', 'created_at', 'updated_at'];

/**
* 获取格式化的订单日期
* @return string
*/
public function getOrderedAtFormattedAttribute()
{
return $this->ordered_at->format('m-d');
}

/**
* 获取订单日期字符串
* @return string
*/
public function getOrderedAtStringAttribute()
{
return $this->ordered_at->toDateString();
}
1
2
3
{{-- 视图 --}}
{{ $object->ordered_at_string }}
{{ $object->ordered_at_formatted }}

18. 其他好的做法

使用资源类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建资源类
php artisan make:resource UserResource

// 资源类
class UserResource extends JsonResource
{
/**
* 转换资源为数组
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
];
}
}

// 控制器中使用
return UserResource::collection(User::all());

使用事件和监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建事件
php artisan make:event UserRegistered

// 创建监听器
php artisan make:listener SendWelcomeEmail --event=UserRegistered

// 在 EventServiceProvider 中注册
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
],
];

// 触发事件
event(new UserRegistered($user));

使用策略类进行授权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建策略
php artisan make:policy PostPolicy --model=Post

// 策略类
class PostPolicy
{
/**
* 确定用户是否可以更新文章
* @param \App\Models\User $user
* @param \App\Models\Post $post
* @return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}

// 在控制器中使用
$this->authorize('update', $post);

总结

这些最佳实践不是绝对的规则,而是经过实践验证的指导原则。在实际开发中,应该根据项目的具体需求和团队的约定来灵活应用这些实践。

记住,好的代码不仅仅是能够运行的代码,更是易于理解、维护和扩展的代码。通过遵循这些最佳实践,你可以写出更加优雅、可维护的 Laravel 应用程序。


这些最佳实践基于 Laravel 社区的经验总结,建议结合具体项目需求灵活应用。

本站由 提供部署服务