将“优雅”进行到底——laravel的最佳实践与建议

潘安平
2023-12-01

Laravel已经是众所周知的“优雅”、“简洁”、“实用”、“敏捷”、“特性丰富”等的代名词,尤其是对PHP开发者来说。当然了,也正是因为它特性过于“丰富”,导致很多特性并没有列在文档里,或者曾经列出来过,但是出于某些原因,后来又移除了。很多时候,我们给新手学习建议的时候,都不会建议他直接去看文档,因为文档体系的展开,往往并不是新手能hold住的。我们往往会推荐他们从一些过来人的、真正优雅规范的教程开始上手,上手以后,再在实际当中自行查阅文档。

本文里,将列出一些截至laravel 5.7的最佳实践与使用建议,不论新手与否,相信都可以帮你将laravel用得更好,将laravel的优雅进行到底,同时更充分地利用起laravel的特性来。

更好的阅读体验,请直接移步原文出处:www.pilishen.com/posts/pushi…

(一)将一些常用的数据库查询,放到local scopes中

$orders = Order::where('status', 'delivered')->where('paid', true)->get();
复制代码

我们经常会写些类似的逻辑,但是当条件查询啥的多了以后,也会变得繁琐,而且期间的很多条件限定与查询,就不能复用,每次都得重复写,这个时候local scopes就登场了。local scopes你可以理解成本地的查询域,或者更直白一些,就是一个常用的查询条件片段。我们可以在model里这样:

class Order extends Model
{
   ...
   public function scopeDelivered($query) {
      return $query->where('status', 'delivered');
   }
   public function scopePaid($query) {
      return $query->where('paid', true);
   }
}
复制代码

可以看到这些查询域,都是以scope开头的,后面接上你想要的名字,然后在调用的时候,只需要调用scope后面的实际名字即可:

$orders = Order::delivered()->paid()->get();
复制代码

所以这个查询域,就是laravel query builder的一个个片段,或者说数据库条件查询的片段,然后它们可以拼接起来,让我们终端的代码更加简洁直观。

当然,我们可以更进一步,给这个查询域传上参数,形成动态的条件查询,比如下面这样:

class Order extends Model
{
   ...
   public function scopeStatus($query, string $status) {
      return $query->where('status', $status);
   }
}
$orders = Order::status('delivered')->paid()->get();
复制代码

(二)尽可能地使用自定义Form Request

在很多不那么规范的教程里,你经常会看到如下的代码:

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 artisan make:request StoreBlogPost
复制代码

执行了以后,在app/Http/Requests/文件夹下,就会多出下面这么个单独的类:

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

use App\Http\Requests\StoreBlogPostRequest;

public function store(StoreBlogPostRequest $request)
{
    // 到这儿的时候,post的相关数据就已经验证好了
}
复制代码

当然在这个自定义的request类里,我们可以做的文章不止这一点,比如我们还可以具体定义每一个字段验证失败时的错误信息,这个可以通过定义messages()方法来实现,我们也可以将request类里的错误信息,在blade当中去相应地返回。这些呢,在我们的《Laravel5.7优雅实战入门:第二版》课程里都已经详细说了,这里就实在懒得啰嗦了。

(三)将一些自带的魔术查询方法用起来

  1. 按照created_at一栏,进行降序排列(‘desc’)
User::latest()->get();
复制代码
  1. 按照任意字段,降序排列查询
User::latest('last_login_at')->get();
复制代码
  1. 以随机的顺序获取数据
User::inRandomOrder()->get();
复制代码
  1. 只有当特定条件为true时,才执行相应查询
// 假设用户在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类有什么明显的优越性吗?》

当然这些,在我们的《Laravel5.7优雅实战入门:第二版》课程里也都详尽演示了。

(五)使用队列job来处理耗时操作

原来我们课程里老说,“高性能离不开异步,异步离不开队列”。这方面,laravel的队列job,在处理耗时的、可以放在后台运行的任务上,是一个强大的、必须要掌握的工具。

想发送邮件?队列job。 想发送广播消息?队列job。 想进行较多的图片操作?队列job。 。。。

类似的耗时操作,队列可以让你的前台用户无需等待,背后处理即可。这期间,你甚至可以设置队列的频道或名字,设置优先级,设置超时时间,设置重试次数,等等等等。

(六)善用属性获取器(accessor)与修改器(mutator)

假设你只有first_namelast_name两个字段,然后你想着每次都能轻松地取得user的全名name,那么可以:

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实例上呢?可以如下:

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

class BettingOdds extends Model
{
   ...
}
复制代码

可能有的人会将一些静态内容放到比如说config/bettingOdds.php中:

return [
   'sports' => [
      'soccer' => 'sport:1',
      'tennis' => 'sport:2',
      'basketball' => 'sport:3',
      ...
   ],
];
复制代码

然后呢,获取的时候这样config(’bettingOdds.sports.soccer’);,但是这样并不好,存在一些隐患,不如直接放到model当中:

class BettingOdds extends Model
{
   protected static $sports = [
      'soccer' => 'sport:1',
      'tennis' => 'sport:2',
      'basketball' => 'sport:3',
      ...
   ];
}
复制代码

然后这样来获取BettingOdds::$sports['soccer'];

而且这样,在进一步的数据调用中,更方便,比如:

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来操作数据

$fruits = ['apple', 'pear', 'banana', 'strawberry'];
foreach ($fruits as $fruit) {
   echo 'I have '. $fruit;
}
复制代码

以前,我们都习惯直接用array来操作相关数据,但是在laravel里,更优雅的方式是多使用collection及相关方法

$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专场:《每个程序猿必须且一定要懂的“数据库索引”》

参考文章: //medium.com/@alexrenoki/pushing-laravel-further-best-tips-good-practices-for-laravel-5-7-ac97305b8cac(在该文基础上有所删减和更改)

来我的网站做个客吧,你会发现更多精彩内容的:www.pilishen.com/posts/pushi…

转载于:https://juejin.im/post/5cd33e535188255568740433

 类似资料: