Reputation: 6062
In my web application, users can upload documents
or emails
to channels
.
A channel can furthermore then have document_tags
and email_tags
, that all uploaded documents/emails automatically should inherit.
Furthermore, document_tags
and email_tags
will have different descriptions: tag_descriptions
. So for example if we have a document, uploaded to a channel that have the tags: animals (id = 1)
and pets (id = 2)
Document #55
is upladed to Channel #8
.Document #55
will automatically inherit the tags, that have document_tags.channel_id = 55
(this can be accessed with the following relationship: $channel->documenttags
). In this case animals
and pets
. animals
and pets
in tag_descriptions
, for example: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.
Now in above database design the uploaded document #55
, have the tags: animals
and pets
associated, but further these two tags have a unique description, that is unique for the specific document.
If I upload another document, or an email (let's say email #20
), then I imagine it will look like:
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.
Now the email #20
also have the tags animals
and pets
, but in this case, the user can set unique descriptions for the tags.
Now my question is:
Is above design doable, and is it considered best practice in Laravel / PHP? I am a bit unsure how to structure the code, because the TagDescription
model will suddenly have two polymorphic relationships (taggable
and typeable
), and I cannot find anything in the documentation that this is supported.
Furthermore, I am unsure if I can use the above design to access the unique descriptions through the specific uploaded document, such as:
//In my Document.php model:
public function tagdescriptions()
{
return $this->morphMany(TagDescription::class, 'typeable');
}
Then use it like: $document->tagdescriptions
.
Last but not least - I am a bit unsure how to save the unique tag description for the specific taggable_id / taggable_type and unique email/document. (typeable_id and typeable_type).
Upvotes: 4
Views: 2429
Reputation: 2166
I'm not sure exactly what you are trying to do, but a table with two polymorphic relations doesn't make sense. The table that enables the polymorphic relation is a pivot table. While I get that you want unique descriptions for each tag and relationship type, a pivot table should only have two foreign key columns, one the table relating to and relating from.
There is another way, which is to use the polymorphic relationship as a constraint on the pivot table. To start with the pivot table in the polymorphic relationship should be renamed to "taggables". You do not need a "email_tag" table and "document_tag" table, you can use a table called "tags". To get a unique description for each tag, you can add the description to the table "tabblables".
The migration file looks like this....
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();
});
}
Here what you need to do in your email and document models.
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');
}
}
The "withPivot" function will return the value of the column specified in the query.
Here is what you need to do in your Tags Model.
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');
}
}
You do not need a "Taggables" table model.
Here is what is going. When you tinker...
$email = App\Email::find(1)->tags;
This query will run...
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'
What you see is the constraint of the polymorphic relationship can query the unique description. In summing up, you can't put more than two foreign key relations in a pivot table, but you can add the constraint to your pivot table.
I think it looks cleaner to add this your AppServiceProvider.php file...
public function boot()
{
Relation::morphMap([
'email' => Email::class,
'document' => Document::class
]);
}
https://laravel.com/docs/5.8/eloquent-relationships#polymorphic-relationships
This will enable you to store a value of "email" or "document" as your taggable type.
For posting to the tables I think this looks the cleanest...
$tag = Tag::firstOrNew(["name"=>"#tag"]);
$tag->name = "#tag";
$document = new Document();
$document->name = "new doc";
$document->save();
$document->tags()->save($tag,["description"=>"some description"]);
You can certainly use attach(). Save() uses attach() as shown here... https://github.com/laravel/framework/blob/5.8/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php#L871
Hope this helps.
Upvotes: 3