One to Many (Polymorphic)

One to Many (Polymorphic) can be used to refer to a model that may not necessarily always be the same.

A great example of this would be having blogs and news models, and a comment model that references either the blog or news model.

We will use a similar setup to the example above to set this up, we will not go in depth on the other modules, but you can view the full code used in this example at: vendor/twill/examples/portfolio

Database setup

On our database not much needs to happen. In our comments migration we have to add a nullableMorphs that we will use to store the relational data.

A morph automatically creates a {morphName}_id and {morphName}_type that Laravel understands.

The _id will hold the model id we are referencing. _type will hold the type of the model we are targeting.

File:

database/migrations/2022_04_06_070334_create_comments_tables.php

1<?php
2 
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6 
7return new class extends Migration
8{
9 public function up(): void
10 {
11 Schema::create('comments', function (Blueprint $table) {
12 createDefaultTableFields($table);
13 $table->string('title', 200)->nullable();
14 $table->text('comment')->nullable();
15 $table->nullableMorphs('commentable');
16 $table->boolean('approved')->default(false);
17 });
18 }
19 
20 public function down(): void
21 {
22 Schema::dropIfExists('comments');
23 }
24};

Define the relation

Now that our migration is in place we can move onward to our model setup. Here we will follow the Laravel documentation for the model structure.

File:

app/Models/Comment.php

1<?php
2 
3namespace App\Models;
4 
5use A17\Twill\Models\Model;...
6use Illuminate\Database\Eloquent\Relations\MorphTo;
7 
8class Comment extends Model
9{
10 protected $fillable = [...
11 'published',
12 'title',
13 'comment',
14 ];
15 
16 public function commentable(): MorphTo
17 {
18 return $this->morphTo();
19 }
20}

As our database column is named the same as our method, we do not have to specify anything else in the ->morphTo method.

Defining the inverse

In the example we allow comments on Partners and Projects, so what we need to do now is define the inverse of this relation in each of these models:

File:

app/Models/Partner.php

1<?php
2 
3namespace App\Models;
4 
5use A17\Twill\Models\Behaviors\HasTranslation;...
6use A17\Twill\Models\Behaviors\HasSlug;
7use A17\Twill\Models\Behaviors\HasMedias;
8use A17\Twill\Models\Behaviors\HasRevisions;
9use A17\Twill\Models\Model;
10use Illuminate\Database\Eloquent\Relations\MorphMany;
11 
12class Partner extends Model
13{
14 use HasTranslation, HasSlug, HasMedias, HasRevisions;
15 
16 protected $fillable = [...
17 'published',
18 'title',
19 'description',
20 ];
21 
22 public $translatedAttributes = [...
23 'title',
24 'description',
25 ];
26 
27 public $slugAttributes = [...
28 'title',
29 ];
30 
31 public function comments(): MorphMany
32 {
33 return $this->morphMany(Comment::class, 'commentable');
34 }
35}

File:

app/Models/Project.php

1<?php
2 
3namespace App\Models;
4 
5use A17\Twill\Models\Behaviors\HasBlocks;...
6use A17\Twill\Models\Behaviors\HasTranslation;
7use A17\Twill\Models\Behaviors\HasSlug;
8use A17\Twill\Models\Behaviors\HasMedias;
9use A17\Twill\Models\Behaviors\HasRevisions;
10use A17\Twill\Models\Model;
11use Illuminate\Database\Eloquent\Relations\BelongsToMany;
12use Illuminate\Database\Eloquent\Relations\HasMany;
13use Illuminate\Database\Eloquent\Relations\MorphMany;
14 
15class Project extends Model
16{
17 use HasBlocks, HasTranslation, HasSlug, HasMedias, HasRevisions;
18 
19 protected $fillable = [...
20 'published',
21 'title',
22 'description',
23 ];
24 
25 public $translatedAttributes = [...
26 'title',
27 'description',
28 ];
29 
30 public $slugAttributes = [...
31 'title',
32 ];
33 
34 public function links(): HasMany...
35 {
36 return $this->hasMany(Link::class)->orderBy('position');
37 }
38 
39 public function partners(): BelongsToMany...
40 {
41 return $this->belongsToMany(Partner::class)->orderByPivot('position');
42 }
43 
44 public function comments(): MorphMany
45 {
46 return $this->morphMany(Comment::class, 'commentable');
47 }
48}

Now that we have our inverse set up we can continue setting up our repository.

Update the repository

Creating the repeater

We want to be able to add comments from both our Partner and Project module.

First we will create a repeater that we can use to create new comments on any of these modules

File:

resources/views/twill/repeaters/comment.blade.php

1@twillRepeaterTitle('Comment')
2@twillRepeaterTitleField('title', ['hidePrefix' => false])
3@twillRepeaterTrigger('Add comment')
4@twillRepeaterGroup('app')
5 
6<x-twill::input
7 name="title"
8 label="Title"
9/>
10 
11<x-twill::input
12 name="title"
13 label="Title"
14 :maxlength="100"
15/>

Then in both resources/views/twill/partners/form.blade.php and resources/views/twill/projects/form.blade.php we add:

1<x-twill::repeater type="comment"/>

When you now visit either of the forms you should see the new Add comment button.

Configuring the repository

Next we have to update our repository so that when the form is submitted, the comment is saved properly.

For this we will use the method updateRepeaterMorphMany and getFormFieldsForRepeater.

File:

app/Repositories/PartnerRepository.php

1<?php
2 
3namespace App\Repositories;
4 
5use A17\Twill\Models\Contracts\TwillModelContract;...
6use A17\Twill\Repositories\Behaviors\HandleTranslations;
7use A17\Twill\Repositories\Behaviors\HandleSlugs;
8use A17\Twill\Repositories\Behaviors\HandleMedias;
9use A17\Twill\Repositories\Behaviors\HandleRevisions;
10use A17\Twill\Repositories\ModuleRepository;
11use App\Models\Partner;
12 
13class PartnerRepository extends ModuleRepository
14{
15 use HandleTranslations, HandleSlugs, HandleMedias, HandleRevisions;
16 
17 public function __construct(Partner $model)...
18 {
19 $this->model = $model;
20 }
21 
22 public function afterSave(TwillModelContract $model, array $fields): void
23 {
24 $this->updateRepeaterMorphMany(
25 $model,
26 $fields,
27 'comments',
28 'commentable',
29 'Comment',
30 'comment'
31 );
32 
33 parent::afterSave($model, $fields);
34 }
35 
36 public function getFormFields(TwillModelContract $object): array
37 {
38 $fields = parent::getFormFields($object);
39 
40 return $this->getFormFieldsForRepeater(
41 $object,
42 $fields,
43 'comments',
44 'Comment',
45 'comment'
46 );
47 }
48}

Do the same for app/Repositories/ProjectRepository.php and then everything should be fully functional.

Do not forget to run your migrations if you run into any issues.