30 个 Laravel Eloquent 的隐藏技巧

下面列出了30Laravel Elequent的隐藏技巧,这些技巧可以帮助我们更快速、便捷的开发。有关使用 Laravel Elequent 的更多信息,请参阅 原文出处Github 仓库

Laravel

1. 不可见的数据库字段

不可见列是 MySQL 8 中的一个新概念。作用是当运行 select * 查询时,它不会检索任何不可见字段。 如果你需要不可见列的值,则必须在 select 语句中明确指定它。

在 Laravel 也可以这样查询:

Schema::table('users', function (Blueprint $table){
  $table->string('password')->invisble();
});
$user = User::first();
$user->secret == null;

2. saveQuietly

当保存模型时不想触发任何模型事件,可以使用此方法:

$user = User::first();
$user->name = "Hamid Afghan";
$user->saveQuietly();

3. 默认属性值

在 Laravel 中,你可以在两个地方为列定义默认值:迁移和模型

Schema::create('orders', function(Blueprint $table){
  $table->bigIncrements('id');
  $table->string('status', 20)
    ->nullable(false)
    ->default(App\Enums\OrderStatuses::DRAFT);
});

这是一个众所周知的特性。status 列将有一个默认的值。

但是下面这个呢?

$order = new Order();
$order->status = null;

在本例中,status 将为 null ,因为它还没有持久化。有时它会导致令人讨厌的空值错误。但幸运的是,你也可以在模型中指定默认属性值:

class Order extends Model
{
   protected $attributes = [
      'status' => App\Enums\OrderStatuses::DRAFT,
   ];
}

现在 Order 的 status 值将会是「draft」:

$order = new Order();
$order->status;  // 'draft'

可以将这两种方法一起使用,保证永远不会再遇到空值错误。

4. 属性转换

在 Laravel 8.x 之前,我们通常这样编写的属性访问器 / 修改器:

class User extends Model{

  public function getNameAttribute(string $value): string
  {
      return Str::upper($value);
  }

  public function setNameAttribute(string $value): string 
  {
      $this->attributes['name'] = Str::lower($value); 
  }

}

这没什么问题,但正如 Taylor Otwell 在 PR 中所说:

框架的这个方面对我来说总是感觉有点「过时」。老实说,我认为它是当前存在的框架中最不优雅的部分之一。首先,它需要两种方法。其次,框架通常不会在获取或设置对象数据的方法前面加上 getset

所以他以这种方式重新创建了这个特性:

use Illuminate\Database\Eloquent\Casts\Attribute; 
class User extends Model { 
  protected function name(): Attribute { 
    return new Attribute(
       get: fn (string $value) => Str::upper($value),
       set: fn (string $value) => Str::lower($value)
    ); 
  } 
}

主要区别:

  • 你只需要编写一种方法
  • 它返回一个 Attribute 而不是一个标量值
  • Attribute 本身带有一个 getter 和一个 setter 函数

在此示例中,我使用了 PHP 8 命名参数(函数之前的 get 和 set)。

5. find

find 方法大家都知道,但是你知道它接受一个 ID 数组吗?以下写法:

$users = User::whereIn('id', $ids)->get();

可以替换成:

$users = User::find($ids);

6. getDirty

在 Eloquent 中,可以检查模型是否「脏」。「脏」意味着它有一些尚未持久的更改:

$user = User::first();
$user->name = 'Hamid Afghan';
$user->isDirty();   // true
$user->getDirty();  // ['name' => 'Guest User']

isDirty 只返回一个布尔值,而 getDirty 返回每个脏属性。

7. push

如果要保存模型及其关系数据。可以使用 push 方法:

$employee = Employee::first();
$employee->name = 'New Name';
$employee->address->city = 'New York';
$employee->push();

save 只会保存employee表中的name字段,而不保存address表中的city字段。push 方法将保存两者。

8. 在 Trait 中定义 boot

我们在 Eloquent 使用了模型的 Trait。如果想要在模型触发事件时初始化 Trait 来实现某些功能,你可以自定义一个 Trait。

例如,你的多个模型都带有 slug 的字段,你不想在每个模型中重写 slug 创建逻辑。可以定义一个 Trait,并在 boot 方法中使用创建事件:

trait HasSlug { 
   // 注意,须以 boot 开头
   // Trait 中定义一个静态的 bootHasSlug() 方法,注:HasSlug 是你的 Trait 名称
   public static function bootHasSlug() { 
      static::creating(function (Model $model) {
         $model->slug = Str::slug($model->title); 
      });
   } 
}

可以定义一个 bootTraitName 方法,Eloquent 在初始化模型时会自动调用它。

9. updateOrCreate

创建和更新模型时,通常会使用相同的逻辑。幸运的是,Eloquent 提供了一个非常方便的 updateOrCreate 方法:

$flight = Flight::updateOrCreate(
   ['id' => $id],
   ['price' => 99, 'discounted' => 1], 
);

它需要两个数组:

  • 第一个用于确定模型是否存在。在此示例中,我使用该ID。
  • 第二个是要插入或更新的属性。

它的工作方式:

  • 如果基于给定的ID找到 Flight,它将使用第二个数组更新。
  • 如果没有与给定的ID Flight,它将插入第二个数组。

我想向您展示如何处理创建和更新模型的真实案例

控制器:

public function store(UpsertDepartmentRequest $request): JsonResponse {
    return DepartmentResource::make($this->upsert($request, new Department()))
        ->response()
        ->setStatusCode(Response::HTTP_CREATED);
}

public function update( UpsertDepartmentRequest $request, Department $department): HttpResponse { 
    $this->upsert($request, $department);
    return response()->noContent();
}

private function upsert(UpsertDepartmentRequest $request, Department $department): Department {
    $departmentData = new DepartmentData(...$request->validated());
    return $this->upsertDepartment->execute($department, $departmentData);
}

正如您所看到的,我经常提取一个名为 upsert 的方法。 此方法接受一个 Department 实例。在保存的方法中,我使用一个空的 Department 实例,因为 在这种情况下,没有真实的实例。 但在更新中,
我传了当前更新的实例

$this->upsertDepartment 引用了一个 Action:

class UpsertDepartmentAction {
    public function execute( Department $department, DepartmentData $departmentData): Department {
        return Department->updateOrCreate(
          ['id' => $department->id],$departmentData->toArray()
        );
     } 
}

他需要一个空的或者更新的 Department 模型实例,和一个包含数据的简单 DTO 对象。在第一个数组中,我使用 $department->id 即:

  • 如果是新模型,则为null。
  • 如果是一个更新的模型,则是有效的ID。

而第二个参数是 DTO 作为一个数组,所以该处的属性。

10. upsert

upsert 方法来表示多个更新或创建操作。它是这样使用的:

Flight::upsert([
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' =>99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
    ],
    ['departure', 'destination'], ['price']
);

这有点复杂:

  • 第一个数组:要插入或更新的值
  • 第二:select 语句中使用的唯一标识符字段
  • 第三:如果记录存在则会更新的字段

所以这个例子将会:

  • 插入或更新从 OaklandSan Diego 的航班,价格为 99
  • 插入或更新从 ChicagoNew York 的航班,价格为 150

11. when

我们经常需要根据某些条件(例如,请求参数)将 where 子句附加到查询中。您可以使用 when 方法代替 if 语句:

User::query()->when($request->searchTerm, function ($query) {
   $query->where('username', 'LIKE', "%$request->searchTerm%"); 
})->get();

如果第一个参数为 true 时,则才会运行回调。

不使用 when 的写法:

$query = User::query();
if ($request->searchTerm) {
   $query->where('username', 'LIKE', "%$request->searchTerm%");
}

return $query->get();

12. appends

如果您有一个属性访问器,并且在将模型转换为 JSON 时经常需要它,那么可以使用 $appends 属性:

class Product extends Model {
  protected $appends = ['current_price'];

  public function getCurrentPriceAttribute(): float {

    return $this->prices
        ->where('from', '<=' now())
        ->where('to', '>=', now())
        ->first()
        ->price;
  } 
}

现在 current_price 列将在每次转换为 JSON 时附加到 Product 模型中。 在使用 Blade 模板时它很有用。 对于 API,我会坚持使用 Resources。

13. whereRelation

假设你正在开发一个像投资组合跟踪器这样的金融应用程序,并且您有以下模型:

  • 股票:每家公司都有一只股票,股票代码和当前价格。
  • 控股:每家控股公司的投资组合中都有一些股票。它有诸如invested_capital, market_value 等列

一个 Holding 属于一个 Stock,一个 Stock 有多个 Holdings。 你可以编写一个简单的 join 来获取每个 Apple 的股份,例如:

$apple = Holding::select('holdings.*')
        ->leftJoin('stocks', 'stocks.id', 'holdings.stock_id')
        ->where('stocks.ticker', 'AAPL')
        ->get();

或者您可以使用 whereRelation 助手:

$apple = Holding::whereRelation('stock', 'ticker', 'AAPL')->get();

它的意思是:返回每一个 Holdings 的 Stock 关联关系的 ticker 列值等于 AAPL (这是苹果的股票代码符号)的数据。它将在引擎里执行一个 EXISTS 查询。 但是,如果你实际上需要的数据来自于 STOCKS 表,那么这不是一个好的解决方案。

14. whereBelongsTo

下面我们来看一下下面这个片段:

public function index(User $user) {
    $sum = Order::where('user_id', $user->id)->sum('total_amount');
}

这看起来不错的样子。这个怎么样?

public function index(User $user) {
    $sum = Order::whereBelongsTo($user)->sum('total_amount');
}

你在 Eloquent 里通过使用 whereBelongsTo 方法,可以清晰的知道「whereBelongsTo」表达的逻辑!

15. oldestOfMany

有一种特殊的关系叫做 oldestOfMany 。如果您经常需要 hasMany 关系中最旧的模型,则可以使用它。

在此示例中,我们有一个 Employee 和一个 Paycheck 模型。 一对多关系:

class Employee extends Models {
  public function paychecks() {
      return $this->hasMany(Paycheck::class); 
  }
}

想要获得最早的 Paycheck ,不必每次都编写自定义查询,可以为它创建一个关系。

class Employee extends Models {
  public function oldestPaycheck() {
     return $this->hasOne(Paycheck::class)->oldestOfMany();
  }
}

在这种情况下,必须使用 hasOne 方法,因为它只会返回一个 Paycheck 模型,即最早的那条。获取的是 Paycheck 是最小的自增 ID 。因此,如果使用 UUID 作为外键,它将无法正常工作。

16. latestOfMany

Oldestofmany 类似,我们也可以使用 Newestofmany

class Employee extends Models {
  public function latestPaycheck() {
    return $this->hasOne(Paycheck::class)->latestOfMany();
  } 
}

最新的 paycheck 自增 ID 最大的那一条数据。

17. ofMany

您还可以将 ofMany 关系与一些自定义逻辑结合使用,例如:

class User extends Authenticable { 
  public function mostPopularPost() {
      return $this->hasOne(Post::class)->ofMany('like_count', 'max'); 
  } 
}

这将返回 like_count 最高的 post 实例。

因此,您可以使用以下关系来代替每次编写自定义查询:

  • oldestOfMany
  • latestOfMany
  • ofMany

18. hasManyThrough

我们经常有像 $parent->child->child 这样的关系。例如,一个部门有员工,每个员工都有薪水。这是一个简单的 hasMany 关系:

class Department extends Model {
    public function employees(): HasMany {
        return $this->hasMany(Employee::class); 
    }
}

class Employee extends Model {
    public function paychecks(): HasMany {
         return $this->hasMany(Paycheck::class);
    }
}

如果我们需要一个部门内的所有薪水,我们可以这样写:

$department->employees->paychecks;

取而代之的是,你可以使用 hasManyThroughDepartment 模型上定义 paychecks 关系:

class Department extends Model {
  public function employees(): HasMany {
    return $this->hasMany(Employee::class); 
  }

  public function paychecks(): HasManyThrough {  
     return $this->hasManyThrough(Paycheck::class, Employee::class); 
  }
}

第二个参数「Through」模型。 现在只需:

$department->paychecks;

19. hasManyDeep

Laravel 中并没有 hasManyDeep 关系,但是有一个很棒的包,叫做 eloquent-has-many-deep。

考虑以下关系:

Country->hasMany->User->hasMany->Post->hasMany->Comment

要使用 Eloquent 从某个国家/地区获取每条评论,将花费大约 10 亿次数据库查询。但是有了这个包,你可以使用 Eloquent 在一个查询中查询每一条评论:

class Country extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
  public function comments() {

    return $this->hasManyDeep(Comment::class, [User::class, Post::class]); 
  } 
}

所以它可以处理更多层次的关系。在底层它使用子查询。

20. withDefault

假设有一个 Post 模型,该模型具有 Author 关系,即 User 模型。用户可以被删除,但我们通常需要保留他们的数据。这意味着 Author 是一个可以为空的关系,所以可能会导致一些像这样的代码:

$authorName = $post->author ? $post->author->name : 'Guest Author';

我们显然希望避免使用这样的代码。幸运的是,PHP 为我们提供了可为空的运算符,因此我们可以这样写:

$authorName = $post->author?->name || 'Guest Author';

硬编码的访客作者似乎是一种反模式,您每次都必须编写此代码段。

class Post extends Model {
  public function author(): BelongsTo {

    return $this->belongsTo(User::class)
      ->withDefault(['name' => 'Guest Author']);
  }
}

这里发生了两件事:

如果 Post 中的 author_id 为 null ,则作者关系不会返回 null 而是返回一个新的 User 模型。

新用户模型的名称是 Guest User 。

现在不需要检查空值:

// 无论是真名还是 'Guest Author'
$authorName = $post->author->name;

21. 按相关模型的平均值排序

在这个例子中,我们有一个 Book 和一个 Rating 模型。 一对多关系。 假设我们需要按平均评分订购书籍。可以使用 withAvg 方法:

public function bestBooks() {
    Book::query()
    ->withAvg('ratings as average_rating', 'rating')
    ->orderByDesc('average_rating');
}

这里有很多评分,所以让我们澄清一下:

评级为 average_rating

  • 我们想要评级表中的平均值
  • 我们想要一个名为 average_rating 的别名评分
  • 这是评级表中想要平均的列

22. 懒加载特定字段

select * 查询可能会很慢并且很消耗内存。 如果您想建立关系但不需要每一列,你可以指定要加载哪些列:

Product::with('category:id,name')->get();

在这种情况下,Eloquent 将运行 select id, name from categories 查询。

23. saveMany

使用 saveMany 函数,可以在一个函数调用中保存多个相关模型。
考虑以下关系:一个产品有多个价格。

更新产品时,如果要删除所有价格并保存新价格。在这种情况下,可以使用 saveMany

$productPrices = collect($prices)
 ->map(fn (array $priceData) => new ProductPrice([ 
    'from_date' => $priceData['fromDate'],
    'to_date' => $priceData['toDate'],
    'price' => $priceData['price'], 
])); 
$product->prices()->delete();
$product->prices()->saveMany($productPrices);

它将在一个查询中创建所有价格,让你不必处理循环:

foreach ($productPrices as $price) {
    $product->prices()->save($price);
}

24. createMany

saveMany 类似,模型不存在,也可以使用 createMany,但可以使用数组来代替:

$prices = [
  [
    'from' => '2022-01-10',
    'to' => '2022-02-28',
    'price' => 9.99
  ],
  [
    'from' => '2022-03-01',
    'to' => null,
    'price' => 14.99
  ]
];

$product->prices()->createMany($prices);

25. 外键与约束

这是在迁移中写入外键的默认方式:

$table->foreignId('category_id')->references('id')->on('categories');

这还不错,但这里有一个更酷的方法:

$table->foreignId('category_id')->constrained();

约束将调用:references('id')->on('categories')

更好的是,你可以这样做:

$table->foreignIdFor(Category::class)->constrained();

26. nullOnDelete

如果一个关系可为空,只需使用 nullOnDelete 方法:

$table->foreignId('category_id')
  ->nullable()
  ->constrained()
  ->nullOnDelete();

27. afterCreating

Factory 类上有一个 afterCreating 方法,您可以在创建模型后使用它来执行某些操作。

当我有带有头像的用户时,我总是使用这个功能:

class UserFactory extends Factory {
  public function definition() {
      return ['username' => $faker->username];
  }
  public function configure()
      return $this->afterCreating(function (User $user) { 
          $faker = FakerFactory::create();
          $dir = storage_path('images');
          $path = $faker->image($dir, 640, 640, null, false);
          $user->profile_picture_path = $dir . DIRECTORY_SEPARATOR . $path;
          $user->save(); 
    });  
  }
}

28. Factory For

如果要创建具有类别的产品:

$category = Category::factory()->create();
$product = Product::factory(['category_id' => $category->id])->create();

除了创建类别并将其作为属性传递之外,还可以执行以下操作:

$product = Product::factory()->for(Category::factory())->create();

你可以将此方法用于 belongs 关系。

29. Factory Has

使用 has 方法,可以反转关系。 所以你可以用它来处理很多关系:

Category::factory()->has(Product::factory()->count(10));

如果关系名称与表名称不同,您还可以指定关系名称:

Product::factory()->has(ProductPrice::factory(), 'prices');

在此示例中,价格是 Product 模型上的关系名称。

30. Factory state

在与模型工厂合作进行测试(或填充)时,我们通常需要设置模型中「状态」。假设我们有一个 Product 模型并且它有一个 active 字段。

你可以这样做:

class ProductFactory extends Factory {
    public function definition() {

        return [
            'name' => $this->faker->words(3, true),
            'description' => $this->faker->paragraph(3),
            'category_id' =>  fn () => Category::factory()->create()->id, 
            'active' => !!rand(0, 1),
       ];
    }              
}

使用此设置,必须在每次要设置它时指定 active 字段:

$product = Product::factory(['active' !& false])->create();

但是工厂提供了一种为模型定义「状态」的方法。 状态可以类似于非活动产品:

class ProductFactory extends Factory {
    public function definition() {

        return [
            'name' => $this->faker->words(3, true),
            'description' => $this->faker->paragraph(3),
            'category_id' =>  fn () => Category::factory()->create()->id, 
            'active' => !!rand(0, 1),
       ];
    }
    public function inactive(): ProductFactory {

        return $this->state(fn (array $attributes) => [
            'active' => false,
         ]);
    }
}

现在您可以创建一个像这样的非活动产品:

$product = Product::factory()->state('inactive')->create();

当你的状态影响 3–4 列时,它非常有用。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@HamidAfghan/30-larav...

译文地址:https://learnku.com/laravel/t/65470

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 17

这是那个版本才有的

2年前 评论
Horizon 2年前

不错,很多是 laravel9才有的写法

2年前 评论

第 11 个用 when 的闭包函数记得使用 use 传递参数,不然函数里面不能使用 $request 外部变量,经常犯错哈哈

2年前 评论
xiaopi

mark

2年前 评论

恐怖的收藏量

1年前 评论

第11个 有一个语法错误

 $query=->where()

应该是

 $query->where()
1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!