ITADN
tighten/parental
tighten/parental · 文件 下载 ZIP
文件最后提交记录最后更新时间
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

Parental - Use single table inheritance in your Laravel App

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() 将返回子模型的一个实例(在本例中为 AdminGuest)。

类型别名

如果你不想在 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'],
]);

这将确保,如果 $messageTextMessage 的实例,则 mentions 关系将被预加载。如果它是 ImageMessage 的实例,则 attachments 关系将被预加载。

或者,您可以使用 loadChildrenCount 方法预加载关系计数:

$message = Message::first();

$message->loadChildrenCount([
    TextMessage::class => ['mentions'],
    ImageMessage::class => ['attachments'],
]);

这将确保,如果 $messageTextMessage 的实例,则 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 出色的标志设计,以及 @DanielCoulbourneTwenty Percent Time 上帮助头脑风暴这一想法。