Laravel 5.7 最佳实践和开发技巧分享

file

Laravel 因可编写出干净,可用可调试的代码而为广大的 PHP 开发者所熟知。它同样也支持许许多多的功能,有时却未能在文档中体现,或者由于某种原因它们出现过又被移除了。

我已经在生产环境中使用 Laravel 2 年了,从中我学到如何把代码变得更好,从我首次使用它以来我都充分发掘它的优势。接下来我将向你展示一些可能对你在用 Laravel 写代码时很有帮助的奥义之招。


查询数据时使用本地范围

Laravel 有一种非常棒的方式来使用 查询构造器 编写查询。就像这样:

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

很不错。这让我专注于编写更友好的代码而不是 SQL 语句。但如果用 本地范围 ,我们可以让这行代码变得更好些。

当查询数据时, 本地范围 允许我们创建自己的 查询构造器 链式方法。举个例子,取代 ->where() ,我们可以用更简洁的 ->delivered()->paid()

首先在 Order 模型,我们加入一些方法:

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

当声明本地范围时,你应该使用 scope[Something] 来命名。这样 Laravel 便会知道这是一个本地范围并且可以在查询构造器中使用。请确保你在方法中传入了第一个参数 $query,也就是由 Laravel 自动注入的查询构造器实例。

$orders = Order::delivered()->paid()->get();

对于可接受额外参数的查询,你可以使用动态范围。每个范围都允许你传入额外的参数。

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

在本文的后面,你会知道为什么数据库字段应该使用 蛇形命名,但这里有第一个原因:Laravel 默认用 where[Something] 来替换 scope[Something] 。所以作为 scopeStatus 范围的代替,你可以这样做:

Order::whereStatus('delivered')->paid()->get();

对于 where[Something] ,Laravel 会搜索 蛇形命名 版本的数据库字段。如果你的数据库中有个 status 字段,你可以用上面那个例子。如果有个 shipping_status 字段,你可以用:

Order::whereShippingStatus('delivered')->paid()->get();

由你决定!

必要的时候使用请求类

Laravel 提供了一种优秀的方式来验证表单提交的数据。如果你需要它,不管是 POST 还是 GET 请求,它都可以验证。

在控制器中,你可以这样做:

public function store(Request $request)
{
    $validatedData = $request->validate([
        'title' => 'required|unique:posts|max:255',
        'body'  => 'required',
    ]);

    // 如果这篇博客的内容无效……
}

但是当控制器中已经有很多代码时,再把验证表单数据的代码加进去就会显得很凌乱。你想尽可能地减少控制器的代码 —— 至少这是我在控制器中写很多逻辑时想到的第一件事。

Laravel 提供了一种很萌的方式来验证表单请求,那就是创建并使用专门的 请求类 而不是用原始的 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',
       ];
   }
}

现在,你应该用新创建的 App\Http\Requests\StoreBlogPostRequest 来代替原先的 Illuminate\Http\Request 类:

use App\Http\Requests\StoreBlogPostRequest;

public function store(StoreBlogPostRequest $request)
{
    // 如果这篇博客的内容无效……
}

请求类中的 authorize() 方法应返回一个布尔值。如果返回了 false,它会抛出一个 403 异常,请确保你在 app/Exceptions/Handler.phprender() 方法中捕获了这个异常:

public function render($request, Exception $exception)
{
    if ($exception instanceof \Illuminate\Auth\Access\AuthorizationException) {
        //
    }
    
    return parent::render($request, $exception);
}

请求类中还有一个 messages() 方法,当验证失败时,它会返回一个包含了错误信息的数组:

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',
       ];
   }
     
   public function messages()
   {
      return [
        'title.required' => 'The title is required.',
        'title.unique' => 'The post title already exists.',
        ...
      ];
   }
}
@if ($errors->any())
   @foreach ($errors->all() as $error)
      {{ $error }}
   @endforeach
@endif

如果你想得到某个字段的验证信息,你可以这样做(当这个字段验证通过时 $errors->has() 会返回一个 false):

<input type="text" name="title" />
@if ($errors->has('title'))
   <label class="error">{{ $errors->first('title') }}</label>
@endif

魔术范围

构建查询时,可以使用已有的魔术范围:

  • 根据 created_at 倒序查询:
User::latest()->get();
  • 根据任意字段倒序查询:
User::latest('last_login_at')->get();
  • 随机查询(即 SQL 语句中的 ORDER BY RAND()
User::inRandomOrder()->get();

使用关联关系代替冗长的查询(或者写得不好的查询)

你是否曾经为了获取更多的信息而在查询语句中使用大量的 join 操作?即使在使用查询构造器的情况下,编写这样的 SQL 语句也是困难的,但是数据模型已经使用 关联关系 来实现同样的功能。由于文档提供了太多的信息,因此刚开始时你可能对关联关系并不熟悉,但是这些内容可以帮助你更好的理解事物的运行原理,同时让你的程序运行得更加顺畅。

通过 这里 查询关联关系的文档。

为耗时的任务使用任务系统

Laravel 的任务 是后台运行程序必用的功能强大的工具。

  • 你要发送电子邮件? 任务系统。
  • 你要广播一个消息? 任务系统。
  • 你要处理一张图片? 任务系统。

任务系统能够帮助你实现,在执行上述这些任务时,减少你的用户的应用加载时间。这些任务可以被放进命名的队列,它们能够被安排优先级,Laravel 几乎在所有可能的地方都实现了队列:无论在后台执行一些 PHP 任务,或者发送消息,或者广播事件,队列都在这些场景中出现。

你可以在 这里 查询队列的文档。

在使用队列时,我喜欢使用 Laravel Horizon ,因为它很容易安装,它能够通过 Supervisor 工具或者配置文件实现后台运行,同时我能够告诉 Horizon 我希望每个队列产生多少个进程。

遵守数据库标准 & 访问器

Laravel 从一开始就教给你变量和方法应使用像 $camelCase camelCase() 这样的小驼峰命名而数据库字段应使用像 snake_case 这样的蛇形命名。为什么呢?因为这有助于我们构造更好的 访问器

访问器是可以直接在模型中构造的自定义字段。如果我们的数据库包含了 first_namelast_nameage 这几个字段,我们可以增加一个叫做 name 的自定义字段来把 first_namelast_name 拼接起来。别担心,这个 name 不会被写入到数据库。它只是某个模型的自定义属性。所有的访问器,和 范围 一样,都有自定义命名语法:getSomethingAttribute

class User extends Model
{
   ...
   public function getNameAttribute(): string
   {
       return $this->first_name.' '.$this->last_name;
   }
}

当使用 $user->name,访问器会返回拼接好的字符串。


默认情况下,用 dd($user) 是看不到 name 属性的,但是通过 $appends 变量我们可以使它一直可用:

class User extends Model
{
   protected $appends = [
      'name',
   ];
   ...
   public function getNameAttribute(): string
   {
       return $this->first_name.' '.$this->last_name;
   }
}

现在每次 dd($user),我们都可以看到 name 了。(不过仍然,这个属性不是从数据库取得的,而是每次使用时将 first_namelast_name 拼接得到的)。

要注意下,如果你数据库里已经有 name 这个字段了,那情况就会有点不一样:$appends 数组里的 name 元素就不需要了,然后访问器需要传入一个参数,这个参数就是数据库中的 name (也就是说我们用不着再使用 $this 了)。

举个例子,我们也许想用 ucfirst() 来使名字的首字母转为大写:

class User extends Model
{
   protected $appends = [
      //
   ];
   ...
   public function getFirstNameAttribute($firstName): string
   {
       return ucfirst($firstName);
   }
     
   public function getLastNameAttribute($lastName): string
   {
      return ucfirst($lastName);
   }
}

现在当我们用 $user->first_name,它会返回一个首字母大写的字符串。

由于这个特性,数据库字段最好是用 snake_case 这种蛇形命名。

不要在配置文件中存储模型相关的静态数据

我喜欢把与模型相关的静态数据存放在模型文件中。让我们一起来看一下。

不要像下面这样:

BettingOdds.php

class BettingOdds extends Model
{
   ...
}

config/bettingOdds.php

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

使用下面的方式访问:

config('bettingOdds.sports.soccer');

我更喜欢这样做:

BettingOdds.php

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();

使用集合替代原始的数组处理

在过去,我们通常以一种原始的方式使用数组:

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

现在,我们可以使用一种高级的方法(译者注:集合的方式)处理数组中的数据。我们可以过滤、转换、遍历和修改数组中数据:

$fruits = collect($fruits);
$fruits = $fruits->reject(function ($fruit) {
   return $fruit === 'apple';
})->toArray();
['pear', 'banana', 'strawberry']

想要了解细节, 请查看 集合的文档.

当使用 查询构造器时,->get() 方法返回一个 Collection 实例。但要注意别搞混了 CollectionQuery builder:

  • 从 Query Builder 中,我们无法获取任何数据.。但我们有大量的查询相关的方法可以使用:orderBy(), where(),等等。
  • 最终调用 ->get() 方法之后,数据被获取到,内存空间被消耗。它返回一个 Collection 实例。某些查询构造器不可用或者说可用但是方法名不同,关于这些请查阅 所有集合的方法

如果你能在 Query Builder 层次过滤数据,就去做吧!不要依赖于等到结果 Collection 实例返回时再过滤---你将会消耗更多的内存空间。 使用 Limit 限制结果条数,在 DB 层使用索引来加快查询。

善用扩展包、不要重复造轮子

如下是一些我在用的扩展包:

以下是我(原文作者)编写的一些扩展包:

  • Befriended(类似社交媒体的点赞、收听、屏蔽操作)
  • Schedule(创建日程表并检查某个时间点是否可用)
  • Rating(为模型增加评分功能)
  • Guardian(易于使用的权限系统)

太难理解?联系我吧!

如果你有更多关于 Laravel 的问题,如果你需要运维方面的帮助,或者只是想说声 谢谢,你可以在 Twitter @rennokki 上找到我!

转自 PHP / Laravel 开发者社区 https://laravel-china.org/topics/22165

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容