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 (); } 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 模型或存储库类中。
错误的做法: 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 { 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 { public function rules ( ) { return [ 'title' => 'required|unique:posts|max:255' , 'body' => 'required' , 'publish_at' => 'nullable|date' , ]; } 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 { 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 public function scopeActive ($query ) { return $query ->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 而不是 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 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 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 return [ 'statuses' => [ 'draft' => 0 , 'normal' => 1 , 'featured' => 2 , ] ]; 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 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' ];public function getOrderedAtFormattedAttribute ( ) { return $this ->ordered_at->format ('m-d' ); } 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 { 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 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 { public function update (User $user , Post $post ) { return $user ->id === $post ->user_id; } } $this ->authorize ('update' , $post );
总结 这些最佳实践不是绝对的规则,而是经过实践验证的指导原则。在实际开发中,应该根据项目的具体需求和团队的约定来灵活应用这些实践。
记住,好的代码不仅仅是能够运行的代码,更是易于理解、维护和扩展的代码。通过遵循这些最佳实践,你可以写出更加优雅、可维护的 Laravel 应用程序。
这些最佳实践基于 Laravel 社区的经验总结,建议结合具体项目需求灵活应用。