illuminatech/sync-many-attribute

Allows control over Eloquent many-to-many relation via array attribute

1.1.5 2024-03-25 11:09 UTC

This package is auto-updated.

Last update: 2024-11-25 12:28:57 UTC


README

Sync Eloquent Many-to-Many via Array Attribute


This extension allows control over Eloquent many-to-many relations via array attributes.

For license information check the LICENSE-file.

Latest Stable Version Total Downloads Build Status

Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist illuminatech/sync-many-attribute

or add

"illuminatech/sync-many-attribute": "*"

to the require section of your composer.json.

Usage

This extension allows control over Eloquent many-to-many relations via array attributes. Each such attribute matches particular BelongsToMany relation and accepts array of related model IDs. Relations will be automatically synchronized during model saving.

Note: in general such approach makes a little sense, since Eloquent already provides fluent interface for many-to-many relation synchronization. However, this extension make come in handy while working with 3rd party CMS like Nova, where you have a little control over model saving and post processing. Also it may simplify controller code, removing relation operations in favor to regular attribute mass assignment.

In order to use the feature you should add \Illuminatech\SyncManyAttribute\SyncManyToManyAttribute trait to your model class and declare syncManyToManyAttributes() method, defining attributes for relation synchronization. This method should return an array, which each key is the name of the new virtual attribute and value is the name of the relation to be synchronized.

For example:

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminatech\SyncManyAttribute\SyncManyToManyAttribute;

/**
 * @property int[] $category_ids
 * @property int[] $tag_ids
 */
class Item extends Model
{
    use SyncManyToManyAttribute;

    protected function syncManyToManyAttributes(): array
    {
        return [
            'category_ids' => 'categories',
            'tag_ids' => 'tags',
        ];
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)->withPivot(['created_at']);
    }

    // ...
}

Usage example:

<?php

$item = new Item();
$item->category_ids = Category::query()->pluck('id')->toArray();
// ...
$item->save(); // relation `Item::categories()` synchronized automatically

$item = $item->fresh();
var_dump($item->category_ids); // outputs array of category IDs like `[1, 3, 8, ...]`

You may use sync attributes during HTML form input composition. For example:

...
<select multiple="multiple" name="category_ids[]" id="category_ids">
@foreach ($allCategories as $category)
    <option value="{{ $category->id }}" @if(in_array($category->id, $item->category_ids)) selected="selected" @endif>{{ $category->name }}</option>
@endforeach
...
</select>

Controller code example:

<?php

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class KioskController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'name' => ['required', 'string'],
            // ...
            'category_ids' => ['required', 'array'],
            'category_ids.*' => ['int', 'exists:categories,id'],
            'tag_ids' => ['required', 'array'],
            'tag_ids.*' => ['int', 'exists:tags,id'],
        ]);
        
        $item = new Item;
        $item->fill($validatedData); // single assignment covers all many-to-many relations
        $item->save(); // relation `Item::categories()` synchronized automatically
        
        // return response
    }
}

Note: remember you need to add the names of attribute for many-to-many synchronization to \Illuminate\Database\Eloquent\Model::$fillable in order to make them available for mass assignment.

Pivot attributes setup

You may setup the pivot attributes, which should be saved during each relation synchronization. To do so, you should define the sync attribute as an array, which key defines relation name and value - the pivot attributes. \Closure can be used here for definition of particular pivot attribute value or entire pivot attributes set. For example:

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminatech\SyncManyAttribute\SyncManyToManyAttribute;

class Item extends Model
{
    use SyncManyToManyAttribute;

    protected function syncManyToManyAttributes(): array
    {
        return [
            'category_ids' => [
                'categories' => [
                    'type' => 'help-content',
                ],
            ],
            'tag_ids' => [
                'tags' => [
                    'attached_at' => function (Item $model) {
                        return now();
                    }
                ],
            ],
        ];
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class)->withPivot(['type']);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)->withPivot(['attached_at']);
    }

    // ...
}

You may use \Illuminatech\SyncManyAttribute\ManyToManyAttribute to create sync attribute definition in more OOP style:

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminatech\SyncManyAttribute\ManyToManyAttribute;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminatech\SyncManyAttribute\SyncManyToManyAttribute;

class Item extends Model
{
    use SyncManyToManyAttribute;

    protected function syncManyToManyAttributes(): array
    {
        return [
            'category_ids' => (new ManyToManyAttribute)
                ->relationName('categories')
                ->pivotAttributes(['type' => 'help-content']),
            'tag_ids' => (new ManyToManyAttribute)
                ->relationName('tags')
                ->pivotAttributes([
                    'attached_at' => function (Item $model) {
                        return now();
                    },
                ]),
        ];
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class)->withPivot(['type']);
    }
    
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)->withPivot(['attached_at']);
    }

    // ...
}

Defined pivot attributes will be automatically saved during relation synchronization on model saving:

<?php

$item = new Item();
$item->category_ids = Category::query()->pluck('id')->toArray();
// ...
$item->save(); // relation `Item::categories()` synchronized automatically

$category = $item->categories()->first();
var_dump($category->pivot->type); // outputs 'help-content'

Nova Integration

One of the main benefit of this extension is support of 3rd party CMS like Nova. You may use sync attributes, allowing user to setup many-to-many relation directly from create/update form, instead of operating separated listing from details page.

You can create input for BelongsToMany relation as multiple select or checkbox list. Packages like fourstacks/nova-checkboxes might be used for such fields. The final Nova resource may look like following:

<?php

use Laravel\Nova\Resource;
use Laravel\Nova\Fields\ID;
use Fourstacks\NovaCheckboxes\Checkboxes;

class Item extends Resource
{
    public static $model = \App\Models\Item::class; // uses `SyncManyToManyAttribute` for 'categories'
    
    public function fields(Request $request)
        {
            return [
                ID::make()->sortable(),
    
                // ...
    
                // use single checkbox list input instead of `\Laravel\Nova\Fields\BelongsToMany`:
                Checkboxes::make(__('Categories'), 'category_ids')
                    ->options(\App\Models\Category::pluck('name', 'id')),
            ];
        }
}