Loading... Laravel已经是众所周知的“优雅”、“简洁”、“实用”、“敏捷”、“特性丰富”等的代名词,尤其是对PHP开发者来说。当然了,也正是因为它特性过于“丰富”,导致很多特性并没有列在文档里,或者曾经列出来过,但是出于某些原因,后来又移除了。很多时候,我们给新手学习建议的时候,都不会建议他直接去看文档,因为文档体系的展开,往往并不是新手能hold住的。我们往往会推荐他们从一些过来人的、真正优雅规范的教程开始上手,上手以后,再在实际当中自行查阅文档。 本文里,将列出一些截至laravel 5.7的最佳实践与使用建议,不论新手与否,相信都可以帮你将laravel用得更好,将laravel的优雅进行到底,同时更充分地利用起laravel的特性来。 ### (一)将一些常用的数据库查询,放到local scopes中 ```php $orders = Order::where('status', 'delivered')->where('paid', true)->get(); ``` 我们经常会写些类似的逻辑,但是当条件查询啥的多了以后,也会变得繁琐,而且期间的很多条件限定与查询,就不能复用,每次都得重复写,这个时候local scopes就登场了。local scopes你可以理解成本地的查询域,或者更直白一些,就是一个常用的查询条件片段。我们可以在model里这样: ```php class Order extends Model { ... public function scopeDelivered($query) { return $query->where('status', 'delivered'); } public function scopePaid($query) { return $query->where('paid', true); } } ``` 可以看到这些查询域,都是以scope开头的,后面接上你想要的名字,然后在调用的时候,只需要调用scope后面的实际名字即可: ```php $orders = Order::delivered()->paid()->get(); ``` 所以这个查询域,就是laravel query builder的一个个片段,或者说数据库条件查询的片段,然后它们可以拼接起来,让我们终端的代码更加简洁直观。 当然,我们可以更进一步,给这个查询域传上参数,形成动态的条件查询,比如下面这样: ```php class Order extends Model { ... public function scopeStatus($query, string $status) { return $query->where('status', $status); } } $orders = Order::status('delivered')->paid()->get(); ``` ### (二)尽可能地使用自定义Form Request 在很多不那么规范的教程里,你经常会看到如下的代码: ```php public function store(Request $request) { $validatedData = $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]); // The blog post is valid... } ``` 不规范的原因,就是他们把所有能想到的东西,都丢在controller里就完事了,但这样触犯的忌讳就太多了,违背的原则也就太多了,更重要的,随着项目的进行,很容易让代码变成大花脸,变得越来越没法维护和扩展,更不用说别人甚至自己回头阅读了。那么上面的这个例子,就是让controller去管了它本不应该负责的数据验证逻辑,而这块,laravel有更优雅的方式来解放controller,也即是我们的自定义form request: ```php php artisan make:request StoreBlogPost ``` 执行了以后,在`app/Http/Requests/`文件夹下,就会多出下面这么个单独的类: ```php class StoreBlogPostRequest extends FormRequest { public function authorize() { return $this->user()->can('create.posts'); } public function rules() { return [ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]; } } ``` 这样了以后,在controller里,就不需要传`Request`了,而是传上我们刚才定义的这个`StoreBlogPostRequest`: ```php use App\Http\Requests\StoreBlogPostRequest; public function store(StoreBlogPostRequest $request) { // 到这儿的时候,post的相关数据就已经验证好了 } ``` 当然在这个自定义的request类里,我们可以做的文章不止这一点,比如我们还可以具体定义每一个字段验证失败时的错误信息,这个可以通过定义`messages()`方法来实现,我们也可以将request类里的错误信息,在blade当中去相应地返回。这些呢,在我们的[《Laravel5.7优雅实战入门:第二版》](https://study.163.com/course/courseMain.htm?courseId=1003163020&share=2&shareId=1018568251)课程里都已经详细说了,这里就实在懒得啰嗦了。 ### (三)将一些自带的魔术查询方法用起来 1. 按照`created_at`一栏,进行降序排列(‘desc’) ```php User::latest()->get(); ``` 2. 按照任意字段,降序排列查询 ```php User::latest('last_login_at')->get(); ``` 3. 以随机的顺序获取数据 ```php User::inRandomOrder()->get(); ``` 4. 只有当特定条件为`true`时,才执行相应查询 ```php // 假设用户在news页面,想通过下面的url进行最新消息的排序 // mydomain.com/news?sort=new User::when($request->query('sort'), function ($query, $sort) { if ($sort == 'new') { return $query->latest(); } return $query; })->get(); ``` 与这个`when()`方法相对应的,还有一个`unless()`, 它是`when()`的反面,也即只有为false的时候,才执行某个查询 ### (四)使用Model关系,来避免大段的查询语句,甚至糟糕的查询 时至今日,你是否还在写一些大段的、难写难读又难复用的SQL查询语句呢?比如为了获取更多的信息,使用了大量的`join`。即使你利用laravel的Query Builder,这些语句也非常难写,每次写一个是否得花半天呢?而且如果你不是那么懂,本来一个很快的查询,可能让你写糟糕了,反而非常非常慢。 但是如果你懂得laravel里的Model关系,懂得ORM这种更现代的查询方式,那么往往几乎没有什么难的查询,写的时候根本无需动脑,而且很多逻辑都可以复用,并且避免了因不懂背后原理而导致性能反而下降的问题。 这一方面,我们pilishen.com已经说得足够多了,比如可以看看我们之前的一篇相关帖子[《laravel框架中的Model操作数据库 , 相比DB类有什么明显的优越性吗?》](https://www.pilishen.com/posts/eloquent-model-vs-db-query-builder-in-laravel) 当然这些,在我们的[《Laravel5.7优雅实战入门:第二版》](https://study.163.com/course/courseMain.htm?courseId=1003163020&share=2&shareId=1018568251)课程里也都详尽演示了。 ### (五)使用队列job来处理耗时操作 原来我们课程里老说,“高性能离不开异步,异步离不开队列”。这方面,laravel的队列job,在处理耗时的、可以放在后台运行的任务上,是一个强大的、必须要掌握的工具。 想发送邮件?队列job。 想发送广播消息?队列job。 想进行较多的图片操作?队列job。 。。。 类似的耗时操作,队列可以让你的前台用户无需等待,背后处理即可。这期间,你甚至可以设置队列的频道或名字,设置优先级,设置超时时间,设置重试次数,等等等等。 ### (六)善用属性获取器(accessor)与修改器(mutator) 假设你只有`first_name` 和 `last_name`两个字段,然后你想着每次都能轻松地取得user的全名name,那么可以: ```php class User extends Model { ... public function getNameAttribute(): string { return $this->first_name.' '.$this->last_name; } } ``` 这样当你获取`$user->name`时,就能自动拼接起全名来。默认的这个`$user->name`属性,并不会自动附加到你的user实例上,也即这个时候你`dd($user)`,并不会显示出`name`属性来,那么怎么样让这个`name`属性自动附加到`$user`实例上呢?可以如下: ```php class User extends Model { protected $appends = [ 'name', ]; ... public function getNameAttribute(): string { return $this->first_name.' '.$this->last_name; } } ``` 这个时候,每次我们`dd($user)`,或者在blade视图以及js前端调用中,就自然会带上这个拼接出来的属性了,当然了,这个属性,并没有实际存在数据库中。 ### (七)不要将model相关的静态属性或内容,放到config当中 假设有个`BettingOdds.php`: ```php class BettingOdds extends Model { ... } ``` 可能有的人会将一些静态内容放到比如说`config/bettingOdds.php`中: ```php return [ 'sports' => [ 'soccer' => 'sport:1', 'tennis' => 'sport:2', 'basketball' => 'sport:3', ... ], ]; ``` 然后呢,获取的时候这样`config(’bettingOdds.sports.soccer’);`,但是这样并不好,存在一些隐患,不如直接放到model当中: ```php class BettingOdds extends Model { protected static $sports = [ 'soccer' => 'sport:1', 'tennis' => 'sport:2', 'basketball' => 'sport:3', ... ]; } ``` 然后这样来获取`BettingOdds::$sports['soccer'];` 而且这样,在进一步的数据调用中,更方便,比如: ```php class BettingOdds extends Model { protected static $sports = [ 'soccer' => 'sport:1', 'tennis' => 'sport:2', 'basketball' => 'sport:3', ... ]; public function scopeSport($query, string $sport) { if (! isset(self::$sports[$sport])) { return $query; } return $query->where('sport_id', self::$sports[$sport]); } } ``` 那么这个时候,我们就可以像前面说到的,`BettingOdds::sport('soccer')->get();` ### (八)多使用集合collection,而不是原始的array来操作数据 ```php $fruits = ['apple', 'pear', 'banana', 'strawberry']; foreach ($fruits as $fruit) { echo 'I have '. $fruit; } ``` 以前,我们都习惯直接用array来操作相关数据,但是在laravel里,更优雅的方式是多使用collection及相关方法 ```php $fruits = collect($fruits); $fruits = $fruits->reject(function ($fruit) { return $fruit === 'apple'; })->toArray(); ['pear', 'banana', 'strawberry'] ``` 这样我们就可以方便地利用上一些filter、map、transform、reject等等方法,既简单,又优雅 我们知道laravel的Query Builders,当在它后面最终调用`->get()`方法时,返回的是一个集合Collection实例。但是这里,不要将单纯的Collection和Query builder混淆起来: 1. 在Query Builder中,在你最终调用`->get()`或`first()`这些方法前,我们实际上并没有获取任何数据,比如当我们用`orderBy()`、`where()`这些查询方法时 2. 当我们调用了`->get()`或`first()`方法时,数据才真的去获取,相应的内存才真的占用,然后返回一个集合实例 所以,如果能在Query Builder的环节,就进行数据的过滤或条件限定,那就要在这一环节做。而不要说先获取了数据,返回了Collection实例以后,再用Collection相关的方法去过滤或操作,这样的话耗费的内存资源就容易多得多。当然了,这期间也要限定好查询,利用好数据库索引,关于数据库索引,如果你还不够精通,记得看我们的国际IT专场:[《每个程序猿必须且一定要懂的“数据库索引”》](https://www.pilishen.com/casts/things-every-developer-absolutely-needs-to-know-about-database-indexing) Last modification:March 7, 2023 © Allow specification reprint Like 如果觉得我的文章对你有用,请随意赞赏