nld-labs / laravel-persist-relationships
Persist an Eloquent model and sync relationships atomically.
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/nld-labs/laravel-persist-relationships
Requires
- php: >=8.2
- illuminate/database: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.3
README
Persist an Eloquent model and sync relationships atomically in a single transaction.
Installation
composer require nld-labs/laravel-persist-relationships
Requirements
- PHP 8.2 or higher
- Laravel 11.0 or 12.0
Overview
This package provides a clean way to persist Eloquent models along with their relationships in a single atomic transaction. It handles creating/updating parent models and synchronizing HasMany, MorphMany, and BelongsToMany relationships with proper ownership validation and timestamp management.
Key Features
- Atomic Operations: All changes happen within a database transaction
- Flexible Sync Strategies: Choose between bulk operations (fast) or Eloquent saves (fires events)
- Ownership Protection: Prevents unauthorized modification of related records
- Smart Timestamp Handling: Automatically manages
created_atandupdated_atcolumns - Configurable Deletion: Control whether missing related records should be deleted or preserved
- Pivot Support: Handle pivot attributes for many-to-many relationships
Usage
Basic Example
use NLD\PersistRelationships\PersistWithRelationships; $persister = new PersistWithRelationships(); $post = $persister->run( model: new Post(), validated: [ 'title' => 'My Blog Post', 'comments' => [ ['body' => 'First comment'], ['body' => 'Second comment'], ], 'tags' => [1, 2, 3], ], relationships: [ 'comments' => [ 'columns' => ['body'], ], 'tags' => [], ] );
HasMany Relationships
Creating a Model with HasMany Children
$post = $persister->run(new Post(), [ 'title' => 'My Post', 'comments' => [ ['body' => 'First comment'], ['body' => 'Second comment'], ], ], [ 'comments' => ['columns' => ['body']], ]);
Updating Existing Children and Adding New Ones
$post = $persister->run($existingPost, [ 'title' => 'Updated Post', 'comments' => [ ['id' => 1, 'body' => 'Updated comment'], ['body' => 'New comment'], ], ], [ 'comments' => ['columns' => ['body']], ]);
Configuration Options for HasMany
'comments' => [ 'columns' => ['body', 'author_name'], // Allowed columns to write 'deleteMissing' => true, // Delete children not in payload (default: true) 'enforceOwnership' => true, // Verify children belong to parent (default: true) 'useEloquent' => false, // Use Eloquent saves vs bulk operations (default: false) ]
Performance vs Events Trade-off:
useEloquent: false(default): Uses bulkupsert()andinsert()for speed, but bypasses model events, observers, and mutatorsuseEloquent: true: Uses individual modelsave()andcreate()calls, fires all events and observers, but slower
MorphMany Relationships
MorphMany relationships work identically to HasMany:
$post = $persister->run(new Post(), [ 'title' => 'My Post', 'notes' => [ ['content' => 'First note'], ['content' => 'Second note'], ], ], [ 'notes' => [ 'columns' => ['content'], 'deleteMissing' => true, ], ]);
BelongsToMany Relationships
Simple ID Array
$post = $persister->run($post, [ 'tags' => [1, 2, 3], ], [ 'tags' => [ 'detachMissing' => true, // Sync (default: true) ], ]);
With Pivot Data
$post = $persister->run($post, [ 'tags' => [ ['id' => 1, 'sort_order' => 10], ['id' => 2, 'sort_order' => 20], ['id' => 3, 'sort_order' => 30], ], ], [ 'tags' => [ 'detachMissing' => true, // Sync vs syncWithoutDetaching 'pivot' => ['sort_order'], // Allowed pivot columns ], ]);
Configuration Options:
detachMissing: true- Usessync()to detach missing relationships (default)detachMissing: false- UsessyncWithoutDetaching()to preserve existing relationships
Clearing Relationships
Pass null as the relationship payload to clear all related records:
$post = $persister->run($post, [ 'comments' => null, // Deletes all comments 'tags' => null, // Detaches all tags ], [ 'comments' => ['columns' => ['body']], 'tags' => [], ]);
Skipping Relationships
Simply omit the relationship key from the validated data to leave it untouched:
// Only updates title, leaves comments unchanged $post = $persister->run($post, [ 'title' => 'Updated Title', // comments key not present ], [ 'comments' => ['columns' => ['body']], ]);
Ownership Protection
By default, enforceOwnership: true prevents updating children that don't belong to the parent model:
$post1 = Post::create(['title' => 'Post 1']); $post2 = Post::create(['title' => 'Post 2']); $comment = Comment::create(['post_id' => $post2->id, 'body' => 'Belongs to post 2']); // This will ignore the comment since it doesn't belong to $post1 $persister->run($post1, [ 'comments' => [ ['id' => $comment->id, 'body' => 'Try to steal'], ], ], [ 'comments' => [ 'columns' => ['body'], 'enforceOwnership' => true, // default ], ]);
Set enforceOwnership: false to disable this protection if needed.
Timestamp Handling
The package automatically manages timestamps:
- New records: Sets both
created_atandupdated_at - Updated records: Only updates
updated_at, preserves originalcreated_at - Models without timestamps: Skips timestamp columns entirely
This works correctly for both bulk and Eloquent modes.
Complete Example
use NLD\PersistRelationships\PersistWithRelationships; class PostController extends Controller { public function store(StorePostRequest $request) { $persister = new PersistWithRelationships(); $post = $persister->run( model: new Post(), validated: $request->validated(), relationships: [ 'comments' => [ 'columns' => ['body', 'author_name'], 'deleteMissing' => true, 'enforceOwnership' => true, 'useEloquent' => false, ], 'notes' => [ 'columns' => ['content'], 'deleteMissing' => true, ], 'tags' => [ 'detachMissing' => true, 'pivot' => ['sort_order'], ], ] ); return new PostResource($post); } public function update(UpdatePostRequest $request, Post $post) { $persister = new PersistWithRelationships(); $post = $persister->run( model: $post, validated: $request->validated(), relationships: [ 'comments' => [ 'columns' => ['body', 'author_name'], ], 'tags' => [ 'pivot' => ['sort_order'], ], ] ); return new PostResource($post); } }
Testing
composer test
License
The MIT License (MIT). Please see License File for more information.
Credits
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.