illuminatech/db-role

为Laravel提供对Eloquent关系角色(表继承)组合的支持

1.1.5 2024-03-25 11:32 UTC

This package is auto-updated.

Last update: 2024-08-25 12:19:52 UTC


README

Eloquent角色继承扩展


此扩展为Eloquent关系角色(表继承)组合提供支持。

有关许可信息,请查看 LICENSE 文件。

Latest Stable Version Total Downloads Build Status

安装

安装此扩展的首选方式是通过 composer

运行

php composer.phar require --prefer-dist illuminatech/db-role

或在您的 composer.json 的 require 部分添加

"illuminatech/db-role": "*"

to

使用

此扩展为Eloquent关系角色组合提供支持,这也称为表继承。

例如:假设我们有一个大学的数据库。大学里有学生和教师。学生有学习小组和奖学金信息,而教师有职称和薪水。然而,学生和教师都有姓名、地址、电话号码等。因此,我们可以将它们的数据分成三个不同的表

  • 'humans' - 存储通用数据
  • 'students' - 存储学生特殊数据和指向 'humans' 记录的引用
  • 'instructors' - 存储教师特殊数据和指向 'humans' 记录的引用

此解决方案的DDL可能如下所示

CREATE TABLE `humans`
(
   `id` integer NOT NULL AUTO_INCREMENT,
   `role` varchar(20) NOT NULL,
   `name` varchar(64) NOT NULL,
   `address` varchar(64) NOT NULL,
   `phone` varchar(20) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE InnoDB;

CREATE TABLE `students`
(
   `human_id` integer NOT NULL,
   `study_group_id` integer NOT NULL,
   `has_scholarship` integer(1) NOT NULL,
    PRIMARY KEY (`human_id`)
    FOREIGN KEY (`human_id`) REFERENCES `humans` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
) ENGINE InnoDB;

CREATE TABLE `instructors`
(
   `human_id` integer NOT NULL,
   `rank_id` integer NOT NULL,
   `salary` integer NOT NULL,
    PRIMARY KEY (`human_id`)
    FOREIGN KEY (`human_id`) REFERENCES `humans` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
) ENGINE InnoDB;

此扩展引入了 \Illuminatech\DbRole\InheritRole 特性,它允许基于角色关系进行Eloquent继承。为了使其工作,首先您应该为基本表创建一个Eloquent类,在我们的示例中将是 'humans'。

<?php

use Illuminate\Database\Eloquent\Model;

class Human extends Model
{
    /**
     * {@inheritdoc}
     */
    protected $table = 'humans';
    
    // ...
}

然后您将能够通过 \Illuminatech\DbRole\InheritRole 来组合Eloquent类,这些类使用基于角色的继承。此类组合有2种不同的方式

  • 主角色继承
  • 从属角色继承

主角色继承

这种方法假设角色Eloquent类是基本角色类的子类,通过 'has-one' 关系与从属类相关联。

<?php

use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminatech\DbRole\InheritRole;

class Student extends Human // extending `Human` - not `Model`!
{
    use InheritRole;

    /**
     * Defines name of the relation to the slave table
     * 
     * @return string
     */
    protected function roleRelationName(): string
    {
        return 'studentRole';
    }

    /**
     * Defines attribute values, which should be automatically saved to 'humans' table
     * 
     * @return array
     */
    protected function roleMarkingAttributes(): array
    {
        return [
            'role_id' => 'student', // mark 'Human' record as 'student'
        ];
    }
    
    public function studentRole(): HasOne
    {
        // Here `StudentRole` is an Eloquent, which uses 'students' table :
        return $this->hasOne(StudentRole::class, 'human_id');
    }
}

这种方法的主要优点是角色类直接继承自基本类中所有方法和逻辑。但是,您需要声明一个额外的Eloquent类,该类对应于角色表。为了在搜索过程中区分 'Student' 记录和 'Instructor' 记录,自动定义了一个名为 'inherit-role' 的默认作用域,将 roleMarkingAttributes() 添加到查询的 'where' 条件中。

如果大多数功能依赖于 'Human' 属性,应选择此方法。

从属角色继承

这种方法假设角色Eloquent类不扩展基本类,而是通过 'belongs-to' 与其相关联

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminatech\DbRole\InheritRole;

class Instructor extends Model // do not extend `Human`!
{
    use InheritRole;

    /**
     * {@inheritdoc}
     */
    protected $primaryKey = 'human_id';

    /**
     * {@inheritdoc}
     */
    public $incrementing = false;

    /**
     * Defines name of the relation to the master table
     * 
     * @return string
     */
    protected function roleRelationName(): string
    {
        return 'human';
    }

    /**
     * Defines attribute values, which should be automatically saved to 'humans' table
     * 
     * @return array
     */
    protected function roleMarkingAttributes(): array
    {
        return [
            'role' => 'instructor',
        ];
    }
    
    public function human(): BelongsTo
    {
        return $this->belongsTo(Human::class);
    }
}

这种方法不需要额外的Eloquent类即可运行,也不需要指定默认作用域。它不会直接继承基本Eloquent模型中声明的逻辑,但是任何在相关类中声明的自定义方法将通过 __call() 魔法方法机制可用。因此,如果类 Human 有方法 sayHello(),您可以通过 Instructor 实例调用它。

如果大多数功能依赖于 'Instructor' 属性,应选择此方法。

访问角色属性

在附加后,\Illuminatech\DbRole\InheritRole 提供了通过 \Illuminatech\DbRole\InheritRole::roleRelationName() 指定的关联模型所绑定模型的属性访问权限,因为它们是主要的

<?php

$model = Student::query()->first();
echo $model->study_group_id; // equals to $model->studentRole->study_group_id

$model = Instructor::query()->first();
echo $model->name; // equals to $model->human->name

然而,这仅适用于已在相关模型中通过 \Illuminate\Database\Eloquent\Model::$fillable\Illuminate\Database\Eloquent\Model::$guarded 明确定义的属性。因此,为了使上述示例功能,用于关联的类应按以下方式定义

<?php

use Illuminate\Database\Eloquent\Model;

class StudentRole extends Model
{
    protected $table = 'students';

    protected $primaryKey = 'human_id';

    public $incrementing = false;

    /**
     * All attributes listed here will be postponed to the role model
     */
    protected $fillable = [
        'study_group_id',
        'has_scholarship',
    ];

    /**
     * All attributes listed here will be postponed to the role model
     */
    protected $guarded = [
        'human_id',
    ];
    
    // ...
}

class Human extends Model
{
    protected $table = 'humans';

     /**
      * All attributes listed here will be postponed to the role model
      */
    protected $fillable = [
        'role',
        'name',
        'address',
    ];
    
    /**
     * All attributes listed here will be postponed to the role model
     */
    protected $guarded = [
        'id',
    ];
    
    // ...
}

如果相关模型不存在,例如在创建新记录的情况下,它将在尝试设置角色属性的第一次自动实例化

<?php

$model = new Student();
$model->study_group_id = 12;
var_dump($model->studentRole); // outputs object

$model = new Instructor();
$model->name = 'John Doe';
var_dump($model->human); // outputs object

访问角色方法

在通过 \Illuminatech\DbRole\InheritRole::roleRelationName() 关联的模型中声明的任何非静态方法都可以从拥有模型中访问

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminatech\DbRole\InheritRole;

class Human extends Model
{
    // ...

    public function sayHello($name)
    {
        return 'Hello, ' . $name;
    }
}

class Instructor extends Model
{
    use InheritRole;
    
    // ...
}

$model = new Instructor();
echo $model->sayHello('John'); // outputs: 'Hello, John'

此功能允许在采用“从属”方法时从基本角色模型继承逻辑。然而,这对“主”和“从属”角色方法都适用。

保存角色数据

当主模型保存时,相关的角色模型也将被保存

<?php

$model = new Student();
$model->name = 'John Doe';
$model->address = 'Wall Street, 12';
$model->study_group_id = 14;
$model->save(); // insert one record to the 'humans' table and one record - to the 'students' table

当主模型被删除时,相关的角色模型也将被删除

<?php

$student = Student::query()->first();
$student->delete(); // Deletes one record from 'humans' table and one record from 'students' table

查询角色记录

\Illuminatech\DbRole\InheritRole 通过关系工作。因此,为了使角色属性功能正常工作,它将执行额外的查询来检索角色从属或主模型,这可能会在处理多个模型时对性能产生影响。为了减少查询次数,您可以在角色关系上使用 with()

<?php

$students = Student::query()->with('studentRole')->get(); // only 2 queries will be performed
foreach ($students as $student) {
    echo $student->study_group_id . '<br>';
}

$instructors = Instructor::query()->with('human')->get(); // only 2 queries will be performed
foreach ($instructors as $instructor) {
    echo $instructor->name . '<br>';
}

您可以将“with”应用于角色关系作为 Eloquent 查询的默认作用域

<?php

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Instructor extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('with-role', function (Builder $builder) {
            $builder->with('human');
        });
    }
    
    // ...
}

提示:您可以将从属表的主键命名为与主键相同:使用 'id' 而不是 'human_id'。在这种情况下,基于主键的条件将始终相同。然而,这个技巧在您需要在某个时刻使用连接进行角色关系时可能会带来额外的问题。

如果您需要根据两个实体的字段指定搜索条件,并且您正在使用关系型数据库,则可以使用 join() 方法。

创建角色设置网页界面

从字面上讲,\Illuminatech\DbRole\InheritRole 将两个 Eloquent 类合并成一个。这意味着在创建它们的编辑网页界面时不需要任何特殊操作。但是,您应该记住将角色属性添加到 \Illuminate\Database\Eloquent\Model::$fillable 列表中,以便使它们可用于批量分配。

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminatech\DbRole\InheritRole;

class Instructor extends Model
{
    use InheritRole;

    protected $fillable = [
        // own fillable attributes:
        'rank_id',
        'salary',
        // role fillable attributes:
        'name',
        'address',
    ];

    // ...
}

然后,执行数据存储的数据控制器可能看起来如下

<?php

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class InstructorController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'salary' => ['required', 'number', 'min:0'],
            'rank_id' => ['required', 'string'],
            // role attributes
            'name' => ['required', 'string'],
            'address' => ['required', 'string'],
        ]);
        
        $item = new Instructor;
        $item->fill($validatedData); // single assignment covers both main model and role model
        $item->save(); // role model saved automatically
        
        // return response
    }
}