Laravel 最佳实践
这不是对 Laravel 改编的 SOLID 原则、模式等。这是在现实生活中Laravel项目中通常被忽略的最佳实践。
目录
[丰富的模型, 简单的控制器](#丰富的模型, 简单的控制器)
[最好倾向于使用 Eloquent 而不是 Query Builder 和原生的 SQL 查询。要优先于数组的集合](#最好倾向于使用 Eloquent 而不是 Query Builder 和原生的 SQL 查询。要优先于数组的集合)
[不要在 Blade 模板中执行查询并使用关联加载(N + 1 问题)](#不要在 Blade 模板中执行查询并使用关联加载(N + 1 问题))
[不要把 JS 和 CSS 放在 Blade 模板中,也不要将任何 HTML 放在 PHP 类中](#不要把 JS 和 CSS 放在 Blade 模板中,也不要将任何 HTML 放在 PHP 类中)
[使用 IoC 容器或 facades 代替 new Class()](#使用 IoC 容器或 facades 代替 new Class())
[不要直接从 .env 文件获取数据](#不要直接从 .env 文件获取数据)
以标准格式存储日期,必要时就使用访问器和修改器来修改日期格式
单一责任原则
一个类和一个方法应该只有一个职责。
不好的实践:
1 | public function getFullNameAttribute() |
好的实践:
1 | public function getFullNameAttribute() |
丰富的模型, 简单的控制器
如果你使用Laravel查询构造器或原始 SQL语句 来查询,请将全部的与数据库相关的逻辑放到Eloquent 模型或存储库类中。
坏的实践:
1 | public function index() |
好的实践:
1 | public function index() |
验证
将验证从控制器中移动到请求类中。
不好的实践:
1 | public function store(Request $request) |
好的实践:
1 | public function store(PostRequest $request) |
业务逻辑应该放在服务类中
一个控制器必须仅有一个职责,所以请将业务逻辑放到服务类中。
不好的实践:
1 | public function store(Request $request) |
好的实践:
1 | public function store(Request $request) |
不要重复你自己 (DRY)
尽可能的复用代码. SRP(单一职责原则)正在帮助你避免重复。当然,这也包括了 Blade 模板、Eloquent 的范围等。
不好的实践:
1 | public function getActive() |
好的实践:
1 | public function scopeActive($q) |
最好倾向于使用 Eloquent 而不是 Query Builder 和原生的 SQL 查询。要优先于数组的集合
Eloquent 可以帮助你编写可读性高和可维护性好的代码。此外,Eloquent 也拥有很棒的内置工具,比如软删除、事件、范围等。
不好的实践:
1 | SELECT * |
好的实践:
1 | Article::has('user.profile')->verified()->latest()->get(); |
批量赋值
不好的实践:
1 | $article = new Article; |
好的实践:
1 | $category->article()->create($request->all()); |
不要在 Blade 模板中执行查询并使用关联加载(N + 1 问题)
不好的实践 (对于100个用户,下面需要 101 次查询):
1 | @foreach (User::all() as $user) |
好的实践 (对于100个用户,下面只需 2 次查询):
1 | $users = User::with('profile')->get(); |
给予方法或变量一个优雅的描述比注释代码更好。
不好的实践:
1 | if (count((array) $builder->getQuery()->joins) > 0) |
更好的实践:
1 | // 判断是否有更多的连接 |
好的实践:
1 | if ($this->hasJoins()) |
不要把 JS 和 CSS 放在 Blade 模板中,也不要将任何 HTML 放在 PHP 类中
不好的实践:
1 | let article = `{{ json_encode($article) }}`; |
好的实践:
1 | <input id="article" type="hidden" value="{{ json_encode($article) }}"> |
在一个 Javascript 文件中:
1 | let article = $('#article').val(); |
最好的方法是使用专门的PHP到JS包来传输数据。
在代码中使用配置和语言文件、常量,而不是固定写死
不好的实践:
1 | public function isNormal() |
好的实践:
1 | public function isNormal() |
使用社区推荐的标准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 |
初始化测试数据 | 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 | |
Model property | 蛇形命名 | $model->created_at | |
Foreign key | 带_id后缀的单数型号名称 | article_id | |
Primary key | - | id | |
Migration | - | 2017_01_01_000000_create_articles_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 |
运用更短、可读性更强的语法
不好的实践:
1 | $request->session()->get('cart'); |
好的实践:
1 | session('cart'); |
更多实例:
通用语法 | 简短可读性强的语法 |
---|---|
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 代替 new Class()
new Class() 语法创建类时,不仅使得类与类之间紧密耦合,还加重了测试的复杂度。推荐改用 IoC 容器或 facades。
不好的实践:
1 | $user = new User; |
好的实践:
1 | public function __construct(User $user) |
不要直接从 .env 文件获取数据
将数据传递给配置文件,然后使用辅助函数 config()
在应用程序中使用数据。
不好的实践:
1 | $apiKey = env('API_KEY'); |
好的实践:
1 | // config/api.php |
以标准格式存储日期,必要时就使用访问器和修改器来修改日期格式
不好的实践:
1 | {{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }} |
好的实践:
1 | // Model |
其它好的实践
千万不要在路由文件中放置任何逻辑。
在 Blade 模板中最小化 vanilla PHP 的使用。