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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Mark
这是那个版本才有的
Mark
Mark
不错,很多是 laravel9才有的写法
mark
mark
Mark
第 11 个用 when 的闭包函数记得使用 use 传递参数,不然函数里面不能使用 $request 外部变量,经常犯错哈哈
mark
mark
mark
Mark
先收藏再说
恐怖的收藏量
第11个 有一个语法错误
应该是