Laravel / PHP - 高级多态关联

4

在我的Web应用程序中,用户可以上传文档电子邮件频道

频道可以进一步拥有document_tagsemail_tags,所有已上传的文档/电子邮件都应自动继承它们。

此外,document_tagsemail_tags将拥有不同的描述:tag_descriptions。例如,如果我们有一个文档,上传到具有标签animals(id = 1)pets(id = 2)的频道:

  1. 文档#55被上传到频道#8
  2. 文档#55将自动继承具有document_tags.channel_id = 55的标签(可以使用以下关系访问:$channel->documenttags)。 在这种情况下,是animalspets
  3. 现在用户应该能够为tag_descriptions中的标签animalspets设置唯一的描述,例如:

tag_descriptions

id | taggable_type   | taggable_id  | typeable_type | typeable_id | description
1  | App\DocumentTag | 1            |  App\Document | 55          | My unique description for animals. 
2  | App\DocumentTag | 2            |  App\Document | 55          | My unique description for pets.

在上述数据库设计中,已上传的文档 #55具有标签:动物宠物相关联,但进一步地,这两个标签有一个独特的描述,该描述对于特定的文档是唯一的。
如果我上传另一个文档或电子邮件(假设电子邮件 #20),那么它看起来会像: tag_descriptions:
id | taggable_type   | taggable_id  | typeable_type | typeable_id | description
1  | App\DocumentTag | 1            |  App\Document | 55          | My unique description for animals. 
2  | App\DocumentTag | 2            |  App\Document | 55          | My unique description for pets.
3  | App\EmailTag    | 1            |  App\Email    | 20          | Another unique description for animals. 
4  | App\EmailTag    | 2            |  App\Email    | 20          | Yet another unique description for pets.

现在,电子邮件#20也有标签animalspets,但在这种情况下,用户可以为标签设置独特的描述。
现在我的问题是:
以上设计可行,并且是否被视为 Laravel/PHP 的最佳实践?我有点不确定如何结构化代码,因为 TagDescription 模型将突然拥有两个多态关系(taggabletypeable),而我在文档中找不到任何支持此功能的内容。
此外,我不确定我能否使用上述设计通过特定上传的文档来访问唯一的描述,例如:
//In my Document.php model:
public function tagdescriptions()
{
    return $this->morphMany(TagDescription::class, 'typeable');
}

然后像这样使用:$document->tagdescriptions

最后一点 - 我有点不确定如何保存特定标签的唯一标签描述,针对特定的可标记ID / 可标记类型和唯一电子邮件/文档。(typeable_id 和 typeable_type)。


1
这是一个复杂的问题,不能直接使用Laravel的Eloquent解决。您可以在此帖子https://laravel.io/forum/03-04-2014-hasmanythrough-with-many-to-many中找到一些灵感,特别是Maxeee09提出的`HasManyThroughBelongsTo`模型关系扩展。 - Dimitri Mostrey
1
一眼看去,使用透明多态关系存储的数据透视表会更好。 - Jed Lynch
频道的 document_tagsemail_tags 是否会随时间而改变,对于现有文档会有什么后果?这个问题应该同时回答新增标签和删除标签的情况。 - Namoshek
1个回答

3
我不确定您试图做什么,但是一个带有两个多态关系的表没有意义。启用多态关系的表是一个中间表。虽然我知道您想为每个标签和关系类型提供唯一的描述,但是中间表应该只有两个外键列,一个是关联到的表,另一个是从中关联的表。
还有另一种方法,就是将多态关系用作中间表的约束条件。首先,多态关系中的中间表应该重命名为“taggables”。您不需要“email_tag”表和“document_tag”表,可以使用一个名为“tags”的表。要为每个标签获取唯一的描述,可以将描述添加到“tabblables”表中。

迁移文件如下所示...

public function up()
{
    Schema::create('taggables', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('tag_id');
        $table->unsignedInteger('taggable_id');
        $table->string('taggable_type');
        $table->string('description');
        $table->timestamps();
    });

    Schema::create('tags', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->timestamps();
    });

    Schema::create('documents', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->timestamps();
    });

    Schema::create('emails', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->timestamps();
    });
}

以下是您在电子邮件和文档模板中需要做的事情。

class Email extends Model
{
    /**
     * Get all of the tags.
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable')->withPivot('description');
    }
}

class Document extends Model
{
    /**
     * Get all of the tag descriptions.
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable')->withPivot('description');
    }
}

"withPivot"函数将返回查询中指定列的值。在您的标签模型中,需要执行以下操作。
class Tag extends Model
{
    /**
     * Get all of the tag descriptions.
     */
    public function documents()
    {
        return $this->morphByMany(Document::class, 'taggable');
    }

    /**
     * Get all of the tag descriptions.
     */
    public function emails()
    {
        return $this->morphByMany(Email::class, 'taggable');
    }
}

你不需要一个“可标记的”表模型。
以下是发生的事情。当你进行调整时...
$email = App\Email::find(1)->tags;

这个查询将会运行...

select `tags`.*, 
    `taggables`.`taggable_id` as `pivot_taggable_id`, 
    `taggables`.`tag_id` as `pivot_tag_id`, 
    `taggables`.`taggable_type` as `pivot_taggable_type`, 
    `taggables`.`description` as `pivot_description` 
from `tags` 
inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` 
where `taggables`.`taggable_id` = 1 
and `taggables`.`taggable_type` = 'App\Email'

你看到的是多态关系的限制,可以查询唯一描述。总之,在一个数据透视表中不能放置超过两个外键关系,但你可以将限制添加到你的数据透视表中。
我认为将其添加到你的AppServiceProvider.php文件中会更加清晰。
public function boot()
{
    Relation::morphMap([
        'email'     => Email::class,
        'document'  => Document::class
    ]);
}

https://laravel.com/docs/5.8/eloquent-relationships#polymorphic-relationships

这将使您能够将“电子邮件”或“文档”作为可标记类型存储价值。
对于发布到表格,我认为这是最清晰的方式...
$tag = Tag::firstOrNew(["name"=>"#tag"]);
$tag->name = "#tag";

$document = new Document();
$document->name = "new doc";
$document->save();
$document->tags()->save($tag,["description"=>"some description"]);

你可以使用attach()。Save()方法在这里使用了attach()方法,如下所示:https://github.com/laravel/framework/blob/5.8/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php#L871 希望这能帮到你。

非常感谢Jed提供详细的答案!不过我还有两个问题。创建标签时,这应该是相当标准的,对吧?只需执行“创建”操作即可。但是,当我创建标签描述时,我该如何操作呢? - oliverbj
我试了一下,这个方法好像可以工作: $document->tags()->attach($tag, ['description' => "自定义标签描述"]);这是正确的方法吗? - oliverbj
我添加了答案以展示如何发布新标签。附加将起作用,但我认为保存更干净。使用保存的一个问题是您需要在数据库架构中添加时间戳。 - Jed Lynch
非常感谢您。我花了几周的时间来尝试解决这个问题,而您的解决方案完美地解决了它! - oliverbj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接