PHPackages                             nld-labs/laravel-persist-relationships - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. nld-labs/laravel-persist-relationships

ActiveLibrary[Database &amp; ORM](/categories/database)

nld-labs/laravel-persist-relationships
======================================

Persist an Eloquent model and sync relationships atomically.

0.1.0(3mo ago)00MITPHPPHP &gt;=8.2

Since Feb 6Pushed 3mo agoCompare

[ Source](https://github.com/nld-labs/laravel-persist-relationships)[ Packagist](https://packagist.org/packages/nld-labs/laravel-persist-relationships)[ RSS](/packages/nld-labs-laravel-persist-relationships/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (4)Versions (2)Used By (0)

Laravel Persist Relationships
=============================

[](#laravel-persist-relationships)

Persist an Eloquent model and sync relationships atomically in a single transaction.

Installation
------------

[](#installation)

```
composer require nld-labs/laravel-persist-relationships
```

Requirements
------------

[](#requirements)

- PHP 8.2 or higher
- Laravel 11.0 or 12.0

Overview
--------

[](#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

[](#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_at` and `updated_at` columns
- **Configurable Deletion**: Control whether missing related records should be deleted or preserved
- **Pivot Support**: Handle pivot attributes for many-to-many relationships

Usage
-----

[](#usage)

### Basic Example

[](#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

[](#hasmany-relationships)

#### Creating a Model with HasMany Children

[](#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

[](#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

[](#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 bulk `upsert()` and `insert()` for speed, but bypasses model events, observers, and mutators
- `useEloquent: true`: Uses individual model `save()` and `create()` calls, fires all events and observers, but slower

### MorphMany Relationships

[](#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

[](#belongstomany-relationships)

#### Simple ID Array

[](#simple-id-array)

```
$post = $persister->run($post, [
    'tags' => [1, 2, 3],
], [
    'tags' => [
        'detachMissing' => true,  // Sync (default: true)
    ],
]);
```

#### With Pivot Data

[](#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` - Uses `sync()` to detach missing relationships (default)
- `detachMissing: false` - Uses `syncWithoutDetaching()` to preserve existing relationships

### Clearing 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

[](#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
--------------------

[](#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
------------------

[](#timestamp-handling)

The package automatically manages timestamps:

- **New records**: Sets both `created_at` and `updated_at`
- **Updated records**: Only updates `updated_at`, preserves original `created_at`
- **Models without timestamps**: Skips timestamp columns entirely

This works correctly for both bulk and Eloquent modes.

Complete Example
----------------

[](#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
-------

[](#testing)

```
composer test
```

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

Credits
-------

[](#credits)

- [Vitauts Stočka](https://github.com/vit)

Contributing
------------

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

###  Health Score

32

—

LowBetter than 72% of packages

Maintenance82

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

96d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/a7080d68b08c41a268daab0de9e121cd06f79925fdb11b696be38d7ea3fbb170?d=identicon)[vits](/maintainers/vits)

---

Top Contributors

[![vits](https://avatars.githubusercontent.com/u/22852?v=4)](https://github.com/vits "vits (1 commits)")

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/nld-labs-laravel-persist-relationships/health.svg)

```
[![Health](https://phpackages.com/badges/nld-labs-laravel-persist-relationships/health.svg)](https://phpackages.com/packages/nld-labs-laravel-persist-relationships)
```

###  Alternatives

[owen-it/laravel-auditing

Audit changes of your Eloquent models in Laravel

3.4k33.0M95](/packages/owen-it-laravel-auditing)[staudenmeir/eloquent-json-relations

Laravel Eloquent relationships with JSON keys

1.1k5.8M24](/packages/staudenmeir-eloquent-json-relations)[bavix/laravel-wallet

It's easy to work with a virtual wallet.

1.3k1.1M11](/packages/bavix-laravel-wallet)[dragon-code/migrate-db

Easy data transfer from one database to another

15717.4k](/packages/dragon-code-migrate-db)[gearbox-solutions/eloquent-filemaker

A package for getting FileMaker records as Eloquent models in Laravel

6454.8k2](/packages/gearbox-solutions-eloquent-filemaker)[cybercog/laravel-ownership

Laravel Ownership simplify management of Eloquent model's owner.

9126.6k3](/packages/cybercog-laravel-ownership)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
