Laravel / PHP — расширенные полиморфные отношения

#php #laravel #eloquent

#php #laravel #красноречивый

Вопрос:

В моем веб-приложении пользователи могут загружать documents или emails в channels .

Кроме того, канал может иметь document_tags и email_tags , которые автоматически должны наследовать все загруженные документы / электронные письма.

Кроме того, document_tags и email_tags будут иметь разные описания: tag_descriptions . Так, например, если у нас есть документ, загруженный на канал с тегами: animals (id = 1) и pets (id = 2)

  1. Document #55 увеличено до Channel #8 .
  2. Document #55 автоматически наследует теги, которые имеют document_tags.channel_id = 55 (к ним можно получить доступ с помощью следующей связи: $channel->documenttags ). В данном случае animals и pets .
  3. Теперь пользователь должен иметь возможность задавать уникальное описание для тегов animals и pets в tag_descriptions , например:

tag_descriptions

 id | taggable_type   | taggable_id  | typeable_type | typeable_id | description
1  | AppDocumentTag | 1            |  AppDocument | 55          | My unique description for animals. 
2  | AppDocumentTag | 2            |  AppDocument | 55          | My unique description for pets.
  

Теперь в приведенном выше дизайне базы данных загруженные document #55 , имеют теги: animals и pets связанные, но в дальнейшем эти два тега имеют уникальное описание, которое уникально для конкретного документа.

Если я загружу другой документ или электронное письмо (скажем email #20 ), то я представляю, как это будет выглядеть:

tag_descriptions :

 id | taggable_type   | taggable_id  | typeable_type | typeable_id | description
1  | AppDocumentTag | 1            |  AppDocument | 55          | My unique description for animals. 
2  | AppDocumentTag | 2            |  AppDocument | 55          | My unique description for pets.
3  | AppEmailTag    | 1            |  AppEmail    | 20          | Another unique description for animals. 
4  | AppEmailTag    | 2            |  AppEmail    | 20          | Yet another unique description for pets.
  

Теперь у email #20 также есть теги animals и pets , но в этом случае пользователь может установить уникальные описания для тегов.

Теперь мой вопрос:

Выполним ли приведенный выше дизайн и считается ли это лучшей практикой в Laravel / PHP? Я немного не уверен, как структурировать код, потому что у TagDescription модели внезапно появятся два полиморфных отношения ( taggable и typeable ), и я не могу найти ничего в документации, что это поддерживается.

Кроме того, я не уверен, смогу ли я использовать вышеуказанный дизайн для доступа к уникальным описаниям через конкретный загруженный документ, например:

 //In my Document.php model:
public function tagdescriptions()
{
    return $this->morphMany(TagDescription::class, 'typeable');
}
  

Затем используйте это как: $document->tagdescriptions .

И последнее, но не менее важное — я немного не уверен, как сохранить уникальное описание тега для конкретного taggable_id / taggable_type и уникального электронного письма / документа. (typeable_id и typeable_type).

Комментарии:

1. Это сложная проблема, не решаемая напрямую с помощью Laravel’s eloquent. Возможно, вы найдете вдохновение в этом посте laravel.io/forum/03-04-2014-hasmanythrough-with-many-to-many и особенно предложение Maxeee09 под названием HasManyThroughBelongsTo расширение модельных отношений.

2. На первый взгляд, сводная таблица для хранения всех полиморфных отношений была бы лучше.

3. Могут ли document_tags и email_tags канала меняться со временем и каковы последствия для существующих документов? На этот вопрос следует ответить как для новых тегов, которые добавляются, так и для тегов, которые удаляются.

Ответ №1:

Я не совсем уверен, что вы пытаетесь сделать, но таблица с двумя полиморфными отношениями не имеет смысла. Таблица, которая поддерживает полиморфные отношения, является сводной таблицей. Хотя я понимаю, что вам нужны уникальные описания для каждого тега и типа отношений, сводная таблица должна иметь только два столбца внешнего ключа, один из которых относится к таблице и связан с ней.
Существует другой способ, который заключается в использовании полиморфных отношений в качестве ограничения для сводной таблицы. Для начала сводную таблицу в полиморфных отношениях следует переименовать в «taggables». Вам не нужны таблицы «email_tag» и «document_tag», вы можете использовать таблицу под названием «теги». Чтобы получить уникальное описание для каждого тега, вы можете добавить описание в таблицу «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');
    }
}
  

Вам не нужна табличная модель «Taggables».

Вот что происходит. Когда вы возитесь…

 $email = AppEmail::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` = 'AppEmail'
  

То, что вы видите, является ограничением полиморфных отношений, может запрашивать уникальное описание. Подводя итог, вы не можете поместить более двух отношений внешнего ключа в сводную таблицу, но вы можете добавить ограничение в свою сводную таблицу.

Я думаю, что добавление этого вашего AppServiceProvider.php файл…

 public function boot()
{
    Relation::morphMap([
        'email'     => Email::class,
        'document'  => Document::class
    ]);
}
  

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

Это позволит вам сохранить значение «email» или «document» в качестве вашего тегируемого типа.

Для публикации в таблицах, я думаю, это выглядит самым чистым…

 $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

Надеюсь, это поможет.

Комментарии:

1. Большое спасибо, Джед, за подробный ответ! Однако у меня есть два вопроса. При создании тега — это должно быть довольно стандартным, верно? Просто сделайте create . Однако, когда я создаю описание тега, как я это сделаю?

2. Итак, я немного попробовал, и это, кажется, работает: $document>tags()->attach($tag, ['description' => "Custom Tag Description"]); Это правильный способ сделать это?

3. Я добавил к ответу, чтобы показать, как опубликовать новый тег. Прикрепить будет работать, но сохранить, я думаю, чище. Одна вещь с сохранением заключается в том, что вам нужны временные метки в вашей схеме базы данных.

4. Большое вам спасибо за это. Я потратил недели, пытаясь разобраться в этом, и ваше решение работает отлично!!!