BelongsToMany is a great way to make one model refer to many others.
Examples could be:
In addition to that we could use pivot data to further extend this relation, to complete the examples above:
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.
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
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 Migration10{11 public function up(): void12 {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(): void41 {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.
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 Model16{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(): BelongsToMany40 {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}
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 BaseModuleController16{17 protected function setUpController(): void
18 {19 $this->setModuleName('projects');20 }21 22 public function getForm(TwillModelContract $model): Form23 {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}
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 ModuleRepository14{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): void23 {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): array51 {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.
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.