Search code examples
phplaravelmany-to-manylaravel-nova

how to require attaching related resources upon creation of resources - Laravel Nova


I have a model called Tree that is supposed to be associated to 1..n Things. Things can be associated to 0..n things. In other words this is a many-to-many relationship, and a Thing must be chosen when a Tree is being created. My thing_tree migration looks like this (there's also a thing_thing pivot table but that's irrelevant):

  public function up()
  {
    Schema::create('thing_tree', function (Blueprint $table) {
      $table->id();
      $table->timestamps();
      $table->unsignedBigInteger('tree_id')->nullable();
      $table->unsignedBigInteger('thing_id')->nullable();
      $table->unique(['tree_id', 'thing_id']);
      $table->foreign('tree_id')->references('id')->on('trees')->onDelete('cascade');
      $table->foreign('thing_id')->references('id')->on('things')->onDelete('cascade');
    });
  }

My Tree model looks like this:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Tree extends Model
{
    use HasFactory;
    protected $guarded = [];
    public function path(){
      $path = '/trees/' . $this->id;
      return $path;
    }

  public function associatedThings () {
    return $this->belongsToMany(Thing::class);
  }
}

The Thing model looks like this:

  public function trees()
  {
    return $this->belongsToMany(Tree::class);
  }
  public function parentOf (){
    return $this->belongsToMany(Thing::class, 'thing_thing', 'parent_id', 'child_id');
  }
  public function childOf(){
    return $this->belongsToMany(Thing::class, 'thing_thing', 'child_id', 'parent_id');
  }

Finally, the Tree Nova resource has these fields:

  public function fields(Request $request)
  {
    return [
      ID::make(__('ID'), 'id')->sortable(),
      Text::make('name'),
      ID::make('user_id')->hideWhenUpdating()->hideWhenCreating(),
      Boolean::make('public'),
      BelongsToMany::make('Things', 'associatedThings')
    ];
  }

It should not be possible to create a Tree without an attached Thing, but the creation screen looks like this: enter image description here

How do I require this in Nova?


Solution

  • This is not possible through nova's default features. Here is how I would go about it with the least effort (you Might want to create a custom field for that yourself) - or at least how I solved a similar issue in the past:

    1. Add the nova checkboxes field to your project

    2. Add the field to your nova ressource :

    // create an array( id => name) of things 
    $options = Things::all()->groupBy('id')->map(fn($e) => $e->name)->toArray();
    
    // ... 
    
    // add checkboxes to your $fields
    Checkboxes::make('Things', 'things_checkboxes')->options($options)
    

    3. Add a validator that requires the things_checkboxes to be not empty

    4. Add an observer php artisan make:observer CheckboxObserver that will sync the model's relations with the given id-array through the checkboxes and then remove the checkboxes field from the object (as it will throw a column not found otherwise), so something like this:

    public function saving($tree)
        {
               // Note: In my case I would use the checkbox_relations method of the HasCheckboxes trait and loop over all checkbox relations to perform the following and get the respective array keys and relation names
    
                $available_ids = array_unique($tree['things_checkboxes']);
    
                // Attach new ones, remove old ones (Relation name in my case comes from the HasCheckboxes Trait)
                $tree->things->sync($available_ids);
    
                // Unset Checkboxes as the Key doesn't exist as column in the Table
                unset($tree['things_checkboxes']);
    
            return true;
        }
    

    5. Add the same thing in reverse for the retreived method in your observer if you want to keep using the checkboxes to handle relations. Otherwise, add ->hideWhenUpdating() to your checkbox field


    I added a trait for that to easily attach the relations through checkboxes to a model:

    trait HasCheckboxRelations
    {
        /**
         * Boot the trait
         *
         * @return void
         */
        public static function bootHasCheckboxRelations()
        {
            static::observe(CheckboxObserver::class);
        }
    
        /**
         * Defines which relations should be display as checkboxes instead of
         * @return CheckboxRelation[]
         */
        public static function checkbox_relations()
        {
            return [];
        }
    }
    

    And checkbox_relations holds an array of instances of class CheckboxRelation which again holds informations about the key name, the relation name and so on.

        public function __construct(string $relationName, string $relatedClass, string $fieldName, bool $hasOverrides = false, string $relationType = null, array $_fields = [])
    

    Also, I added a method attachCheckboxRelationFields to the default nova resource which will be called on the $fields when the model uses the trait.

    Now, I only have to add HasCheckboxRelations to a model, add the array of checkbox_relations and thats it - I have a belongsToMany relation on the nova resource through checkboxes. Of course you don't have the option to manage pivot fields anymore if you go for it this way - which might be why it was not done by the nova devs - but for simple belongsToMany relations I really like to work with the checkbox solution instead of the default attach-table. And for data with pivot fields you can still use the default way.

    Also note that parts of the code where written on the fly so it might not work out of the box, but the overall idea should be delivered.

    Hope it helped!