illuminatech / sync-many-attribute
Allows control over Eloquent many-to-many relation via array attribute
Fund package maintenance!
klimov-paul
Patreon
Installs: 6 647
Dependents: 0
Suggesters: 0
Security: 0
Stars: 14
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- illuminate/database: ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0
Requires (Dev)
- illuminate/events: *
- phpunit/phpunit: ^7.5 || ^8.0 || ^9.3 || ^10.5
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.
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')), ]; } }