splitstack/readonly-models

Block Eloquent persistence on read-only / value-object models.

Maintainers

Package info

github.com/EmilienKopp/readonly-models

pkg:composer/splitstack/readonly-models

Transparency log

Statistics

Installs: 17

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-08-01 00:39 UTC

This package is auto-updated.

Last update: 2026-08-01 00:39:57 UTC


README

Block persistence on Eloquent models. Drop the PreventsWrites trait onto a model and every write path (save, update, delete, forceDelete) throws a ReadOnlyException instead of touching the database.

Useful for value objects, in-memory entities, database views, and any model that should never be written back.

Installation

composer require splitstack/readonly-models

Requires PHP 8.2+ and illuminate/database 10 through 13. It only needs the database component, not the full framework, so it works inside a Laravel app or standalone with the Eloquent Capsule.

Usage

use Illuminate\Database\Eloquent\Model;
use Splitstack\ReadonlyModels\Concerns\PreventsWrites;

class ExchangeRate extends Model
{
    use PreventsWrites;
}
$rate = ExchangeRate::find(1); // reads work as normal
$rate->rate = 1.23;
$rate->save();                 // throws ReadOnlyException

Because create(), saveQuietly() and push() all route through save(), and the static destroy() routes through delete(), those four overrides cover the whole write surface.

Catching the exception

ReadOnlyException extends BadMethodCallException (itself a LogicException), so you can catch it at whichever level fits:

use Splitstack\ReadonlyModels\Exceptions\ReadOnlyException;

try {
    $model->save();
} catch (ReadOnlyException $e) {
    // ...
}

Customising the behaviour

Keep one write method working. A method declared directly on the class always wins over the trait's version, so you can carve out an exception without any trait conflict. For example, route update() through another model:

class ViewModel extends Model
{
    use PreventsWrites;

    public function update(array $attributes = [], array $options = [])
    {
        // your own logic — save/delete/forceDelete stay blocked
    }
}

Change the exception or message. Override the protected denyWrite() hook. It receives the blocked method name and returns the exception to throw:

use Splitstack\ReadonlyModels\Exceptions\ReadOnlyException;

protected function denyWrite(string $method): ReadOnlyException
{
    return new MyReadOnlyException(match ($method) {
        'save'   => 'This record is immutable.',
        'delete' => 'This record cannot be removed.',
        default  => "Cannot {$method} this record.",
    });
}

MyReadOnlyException just needs to extend ReadOnlyException.

License

MIT. See LICENSE.