
Parental
Parental 是一个为 Eloquent 引入 STI(单表继承)功能的 Laravel 包。
什么是单表继承(STI)?
这是一个简单概念的花哨名称:扩展一个模型(通常是为了添加特定行为),但引用同一张表。
安装
composer require tightenco/parental
简单用法
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Parental\HasChildren;
// The "parent"
class User extends Model
{
use HasChildren;
//
}
namespace App\Models;
use Parental\HasParent;
// The "child"
class Admin extends User
{
use HasParent;
public function impersonate($user) {
//...
}
}
use App\Models\Admin;
// Returns "Admin" model, but reference "users" table:
$admin = Admin::first();
// Can now access behavior exclusive to "Admin"s
$admin->impersonate($user);
我们刚刚解决了什么问题?
如果没有 Parental,调用 Admin::first() 会抛出错误,因为 Laravel 会查找 admins 表。Laravel 使用模型的类名来生成预期的表名,以及外键和中间表名。通过在 Admin 模型中添加 HasParent trait,Laravel 现在将引用父模型的类名 users。
从父模型访问子模型
// First, we need to create a `type` column on the `users` table
Schema::table('users', function ($table) {
$table->string('type')->nullable();
});
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Parental\HasChildren;
// The "parent"
class User extends Model
{
use HasChildren;
protected $fillable = ['type'];
}
namespace App\Models;
use Parental\HasParent;
// A "child"
class Admin extends User
{
use HasParent;
}
namespace App\Models;
use Parental\HasParent;
// Another "child"
class Guest extends User
{
use HasParent;
}
use App\Models\Admin;
use App\Models\Guest;
use App\Models\User;
// Adds row to "users" table with "type" column set to: "App/Admin"
Admin::create(...);
// Adds row to "users" table with "type" column set to: "App/Guest"
Guest::create(...);
// Returns 2 model instances: Admin, and Guest
User::all();
我们刚刚解决了什么问题?
之前,如果我们运行:User::first(),我们只会得到 User 个模型。通过向 users 表添加 HasChildren trait 和一个 type 列,运行 User::first() 将返回子模型的一个实例(在本例中为 Admin 或 Guest)。
类型别名
如果你不想在 type 列中存储原始类名,可以使用 $childTypes 属性进行覆盖。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Parental\HasChildren;
class User extends Model
{
use HasChildren;
protected $fillable = ['type'];
protected $childTypes = [
'admin' => Admin::class,
'guest' => Guest::class,
];
}
现在,运行 Admin::create() 会将 users 表中的 type 列设置为 admin,而不是 App\Models\Admin。
如果您正在处理现有的类型列,或者希望将应用程序细节与数据库解耦,此功能非常有用。
自定义类型列名称
您可以通过在父模型上设置 $childColumn 属性来覆盖默认的类型列。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Parental\HasChildren;
class User extends Model
{
use HasChildren;
protected $fillable = ['parental_type'];
protected $childColumn = 'parental_type';
}
在类型之间转换模型
您可以使用 become() 方法将模型从一种类型转换为另一种类型。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Parental\HasChildren;
use Parental\HasParent;
class Order extends Model
{
use HasChildren;
protected $fillable = ['type', 'total'];
protected $childTypes = [
'pending' => PendingOrder::class,
'shipped' => ShippedOrder::class,
];
}
class PendingOrder extends Order
{
use HasParent;
}
class ShippedOrder extends Order
{
use HasParent;
}
use App\Models\Order;
use App\Models\ShippedOrder;
// Retrieve a pending order
$order = Order::first();
// Ship the order by transforming it
$order = $order->become(ShippedOrder::class);
// Updates the "type" column to "shipped" and returns a ShippedOrder instance
$order->save();
我们刚刚解决了什么问题?
become() 方法将返回指定子模型的一个新实例,并包含原始模型的所有属性。您必须对返回的模型调用 save() 以将更改持久化到数据库。这允许您在保持数据完整性的同时,轻松地在不同模型类型之间进行转换,例如将订单从待处理状态更改为已发货状态,或将草稿文章更改为已发布文章。
当您使用观察者或回调时,这也非常有用,因为特定子模型的行为将在转换后触发。
当模型正在 becoming 另一种类型时,会触发一个新的模型事件,您可以像这样监听它:
ShippedOrder::becoming(function ($shippedOrder) {
// Do something before the model is saved...
});
预加载子模型
[!WARNING] 预加载关系仅在 Laravel 11 及以上版本中受支持。
为了帮助在子模型上预加载关系,Parental 提供了一组可在查询中使用的辅助方法。在示例中,我们将使用以下模型:
class Message extends Model
{
use HasChildren;
protected $fillable = ['type', 'content'];
protected $childTypes = [
'text' => TextMessage::class,
'image' => ImageMessage::class,
];
}
class TextMessage extends Message
{
use HasParent;
public function mentions(): HasMany
{
return $this->hasMany(User::class);
}
}
class ImageMessage extends Message
{
use HasParent;
public function attachments(): HasMany
{
return $this->hasMany(Attachment::class);
}
}
从模型实例中预加载
您可以使用 loadChildren 方法从父模型实例中预加载不同模型的关系:
$message = Message::first();
$message->loadChildren([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
]);
这将确保,如果 $message 是 TextMessage 的实例,则 mentions 关系将被预加载。如果它是 ImageMessage 的实例,则 attachments 关系将被预加载。
或者,您可以使用 loadChildrenCount 方法预加载关系计数:
$message = Message::first();
$message->loadChildrenCount([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
]);
这将确保,如果 $message 是 TextMessage 的实例,则 mentions_count 属性将被填充。如果它是 ImageMessage 的实例,则 attachments_count 属性将被填充。
Eager Loading From Eloquent Collection
您可以使用 loadChildren 方法从 Eloquent Collection 中预加载关系:
$messages = Message::all();
$messages->loadChildren([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
]);
这将确保根据集合中每个子模型的类型,为其预加载相应的关系。
或者,您可以使用 loadChildrenCount 方法预加载关系计数:
$messages = Message::all();
$messages->loadChildrenCount([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
]);
这将确保 mentions_count 属性会为 TextMessage 模型的实例填充,并且 attachments_count 属性会为 ImageMessage 模型的实例填充。
从查询和关系进行预加载
您可以使用 childrenWith 方法直接从查询或关系预加载关系:
// From a query...
$messages = Message::query()->childrenWith([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
])->get();
您也可以通过关联进行预加载。例如,如果我们有一个 Room 父模型,它包含 messages:
class Room extends Model
{
public function messages(): HasMany
{
return $this->hasMany(Message::class);
}
}
然后,我们可以像这样预加载子关系:
// From a relationship...
$room = Room::first();
$messages = $room->messages()->childrenWith([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
])->get();
这将确保根据结果集中每个子模型的类型,为其预加载相应的关系。
或者,您可以使用 childrenWithCount 方法预加载关系计数:
// From a query...
$messages = Message::query()->childrenWithCount([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
])->get();
// From a relationship...
$room = Room::first();
$messages = $room->messages()->childrenWithCount([
TextMessage::class => ['mentions'],
ImageMessage::class => ['attachments'],
])->get();
这将确保 mentions_count 属性在 TextMessage 模型的实例上被填充,并且 attachments_count 属性在 ImageMessage 模型的实例上被填充。
Laravel Nova 支持
如果您希望使用共享父级 Nova 资源与子模型,您可以在 NovaServiceProvider 的 boot 方法末尾注册以下服务提供者:
class NovaServiceProvider extends NovaApplicationServiceProvider
{
public function boot() {
parent::boot();
// ...
$this->app->register(\Parental\Providers\NovaResourceProvider::class);
}
}
感谢 @sschoger 出色的标志设计,以及 @DanielCoulbourne 在 Twenty Percent Time 上帮助头脑风暴这一想法。