splitstack / readonly-models
Block Eloquent persistence on read-only / value-object models.
Requires
- php: ^8.2
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-drift: ^5.0
- pestphp/pest-plugin-phpstan: ^5.0
- pestphp/pest-plugin-type-coverage: ^5.0
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.2
- rector/rector: ^2.5
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.