BelongsToMany with pivot data

BelongsToMany is a great way to make one model refer to many others.

Examples could be:

  • An order containing multiple products
  • An artwork having multiple artists
  • A project having multiple contributors

In addition to that we could use pivot data to further extend this relation, to complete the examples above:

  • Each product in the order can have an order specific price
  • Each artwork artist may have focused on something specific
  • Each contributor might have worked on a different part of a project

In Twill, we can set this up using a repeater and below we will go thorough all the steps to make this work, using the project/contributor example.

Note

The pivot can be left out in this example as well if you just want to have a BelongsToMany relation.

If you want to quickly test this in a new project, you can install twill using the portfolio example: php artisan twill:install portfolio

Database setup

We will set up 2 models, one is a Project model, you can do this using php artisan twill:module Project

And afterwards a Partner model: php artisan twill:module Partner.

In the Partner migration we add a relational table that we will use for our BelongsToMany relation.

File:

database/migrations/2022_04_01_071748_create_partners_tables.php

1<?php
2 
3use App\Models\Partner;
4use App\Models\Project;
5use Illuminate\Database\Migrations\Migration;
6use Illuminate\Database\Schema\Blueprint;
7use Illuminate\Support\Facades\Schema;
8 
9return new class extends Migration
10{
11 public function up(): void
12 {
13 Schema::create('partners', function (Blueprint $table) {
14 createDefaultTableFields($table);
15 });
16 
17 Schema::create('partner_translations', function (Blueprint $table) {
18 createDefaultTranslationsTableFields($table, 'partner');
19 $table->string('title', 200)->nullable();
20 $table->text('description')->nullable();
21 });
22 
23 Schema::create('partner_slugs', function (Blueprint $table) {
24 createDefaultSlugsTableFields($table, 'partner');
25 });
26 
27 Schema::create('partner_revisions', function (Blueprint $table) {
28 createDefaultRevisionsTableFields($table, 'partner');
29 });
30 
31 Schema::create('partner_project', function (Blueprint $table) {
32 $table->increments('id');
33 $table->foreignIdFor(Partner::class);
34 $table->foreignIdFor(Project::class);
35 $table->json('role')->nullable();
36 $table->integer('position')->default(999);
37 });
38 }
39 
40 public function down(): void
41 {
42 Schema::dropIfExists('partner_revisions');
43 Schema::dropIfExists('partner_translations');
44 Schema::dropIfExists('partner_slugs');
45 Schema::dropIfExists('partners');
46 }
47};

As you can see we added $table->json('role')->nullable(); and $table->integer('position')->default(999);. While only the position one is mandatory, we will use the role pivot to store how the partner collaborated on the project.

In this case we chose json as our column type as we will make it multilingual, in other cases you can use any other type as per your requirements.

Define the relation

Now with the migration setup we can set up our relation in the Project model:

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}

Set up the repeater and form

To expose the relation in the ui, we will use an inline repeater. We will name this repeater a bit more specific as we want to make clear it is for the pivot table. But you can name it however you like.

File:

app/Http/Controllers/Twill/ProjectController.php

1<?php
2 
3namespace App\Http\Controllers\Twill;
4 
5use A17\Twill\Http\Controllers\Admin\ModuleController as BaseModuleController;
6use A17\Twill\Models\Contracts\TwillModelContract;
7use A17\Twill\Services\Forms\Fields\BlockEditor;
8use A17\Twill\Services\Forms\Fields\Input;
9use A17\Twill\Services\Forms\Fields\Repeater;
10use A17\Twill\Services\Forms\Form;
11use A17\Twill\Services\Forms\InlineRepeater;
12use App\Models\Link;
13use App\Models\Partner;
14 
15class ProjectController extends BaseModuleController
16{
17 protected function setUpController(): void...
18 {
19 $this->setModuleName('projects');
20 }
21 
22 public function getForm(TwillModelContract $model): Form
23 {
24 return Form::make([
25 Input::make()
26 ->translatable()
27 ->name('description'),
28 // Inline repeater that can select existing entries.//
29 InlineRepeater::make()
30 ->label('Partners')
31 ->name('project_partner')
32 ->triggerText('Add partner') // Can be omitted as it generates this.
33 ->selectTriggerText('Select partner') // Can be omitted as it generates this.
34 ->allowBrowser()
35 ->relation(Partner::class)
36 ->fields([
37 Input::make()
38 ->name('title')
39 ->translatable(),
40 Input::make()
41 ->name('role')
42 ->translatable()
43 ->required(),
44 ]),
45 Repeater::make()->type('comment'), // Regular repeater using a view.
46 // Regular repeater for creating items without a managed model.
47 InlineRepeater::make()
48 ->name('links')
49 ->fields([
50 Input::make()
51 ->name('title'),
52 Input::make()
53 ->name('url')
54 ]),
55 
56 BlockEditor::make()
57 ]);
58 }
59}

Update the repository

As a final step we have to update the repository to map the repeater field to the relation.

File:

app/Repositories/ProjectRepository.php

1<?php
2 
3namespace App\Repositories;
4 
5use A17\Twill\Repositories\Behaviors\HandleBlocks;...
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\Project;
12 
13class ProjectRepository extends ModuleRepository
14{
15 use HandleBlocks, HandleTranslations, HandleSlugs, HandleMedias, HandleRevisions;
16 
17 public function __construct(Project $model)...
18 {
19 $this->model = $model;
20 }
21 
22 public function afterSave($model, $fields): void
23 {
24 $this->updateRepeaterMorphMany(
25 $model,
26 $fields,
27 'comments',
28 'commentable',
29 'Comment',
30 'comment'
31 );
32 
33 $this->updateRepeater(
34 $model,
35 $fields,
36 'links',
37 );
38 
39 $this->updateRepeaterWithPivot(
40 $model,
41 $fields,
42 'partners',
43 ['role'],
44 'Partner',
45 'project_partner',
46 );
47 parent::afterSave($model, $fields);
48 }
49 
50 public function getFormFields($object): array
51 {
52 $fields = parent::getFormFields($object);
53 
54 $fields = $this->getFormFieldsForRepeater(
55 $object,
56 $fields,
57 'comments',
58 'Comment',
59 'comment'
60 );
61 
62 $fields = $this->getFormFieldsForRepeater(
63 $object,
64 $fields,
65 'links',
66 'Link',
67 'links'
68 );
69 
70 return $this->getFormFieldForRepeaterWithPivot(
71 $object,
72 $fields,
73 'partners',
74 ['role'],
75 'Partner',
76 'project_partner'
77 );
78 }
79}

Take note of the methods as they are different from other repeater methods: updateRepeaterWithPivot, getFormFieldForRepeaterWithPivot

In the 4th parameter you can pass all the fields that should be written into the pivot table, the position is automatically taken care of.

Result

And you are done. When you now create or edit a Project model you can select existing partners and set their role in the project.

demo