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

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 中所说:
框架的这个方面对我来说总是感觉有点「过时」。老实说,我认为它是当前存在的框架中最不优雅的部分之一。首先,它需要两种方法。其次,框架通常不会在获取或设置对象数据的方法前面加上
get和set。
所以他以这种方式重新创建了这个特性:
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 语句中使用的唯一标识符字段
 - 第三:如果记录存在则会更新的字段
 
所以这个例子将会:
- 插入或更新从 
Oakland到San Diego的航班,价格为 99 - 插入或更新从 
Chicago到New 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;
取而代之的是,你可以使用 hasManyThrough 在 Department 模型上定义 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
          
            
                    
                    
          
          
                关于 LearnKu
              
                    
                    
                    
 
推荐文章: