PHPackages                             brighten/immutable-model - 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. brighten/immutable-model

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

brighten/immutable-model
========================

An Eloquent-compatible, read-only model kernel for Laravel 11+

v0.12.2(3mo ago)110MITPHPPHP ^8.2CI passing

Since Jan 28Pushed 3mo agoCompare

[ Source](https://github.com/therecluse26/laravel-immutable-model)[ Packagist](https://packagist.org/packages/brighten/immutable-model)[ Docs](https://github.com/therecluse26/laravel-immutable-model)[ RSS](/packages/brighten-immutable-model/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (4)Versions (15)Used By (0)

ImmutableModel
==============

[](#immutablemodel)

**An Eloquent-compatible, read-only model kernel for Laravel 11+**

ImmutableModel provides first-class, enforceable read-only models for Laravel applications. It's perfect for SQL views, read-only tables, denormalized projections, and as a CQRS read-side primitive.

What "Immutable" Means
----------------------

[](#what-immutable-means)

ImmutableModel enforces **database immutability**, not strict object immutability:

OperationAllowed?ExampleRead from database✅ Yes`User::find(1)`, `User::where(...)->get()`In-memory attribute changes✅ Yes`$user->computed_field = 'value'`Database persistence❌ Throws`$user->save()`, `$user->update()`, `$user->delete()`Static write methods❌ Throws`User::create()`, `User::insert()`This design prevents accidental database writes while remaining compatible with common Laravel patterns like adding computed properties for API responses, serialization, and working with collections.

Why ImmutableModel?
-------------------

[](#why-immutablemodel)

- **Enforce architectural boundaries**: Prevent accidental database writes at the model level
- **Eliminate persistence bugs**: Any save/update/delete attempt throws immediately - no silent failures
- **Improved performance**: 47-74% faster hydration, 25-70% faster eager loading
- **Lower memory footprint**: ~41% less memory (~1 KB vs ~1.65 KB per model)
- **Familiar API**: Eloquent-compatible read semantics for easy adoption
- **Laravel ecosystem compatible**: Works with API Resources, serialization, and other common patterns

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

[](#installation)

```
composer require brighten/immutable-model
```

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

[](#requirements)

- PHP 8.2+
- Laravel 11+

Quick Start
-----------

[](#quick-start)

```
use Brighten\ImmutableModel\ImmutableModel;

class UserView extends ImmutableModel
{
    protected string $table = 'user_views';

    protected ?string $primaryKey = 'id';

    protected array $casts = [
        'settings' => 'array',
        'created_at' => 'datetime',
    ];
}

// Query just like Eloquent
$users = UserView::where('active', true)->get();
$user = UserView::find(1);
$user = UserView::with('posts')->first();

// In-memory changes are allowed (for computed fields, API responses, etc.)
$user->computed_field = 'some value';  // Works fine
$user->name = 'Modified';              // Works fine (in-memory only)

// But database persistence is blocked
$user->save();              // Throws ImmutableModelViolationException
$user->update([...]);       // Throws ImmutableModelViolationException
$user->delete();            // Throws ImmutableModelViolationException
UserView::create([...]);    // Throws ImmutableModelViolationException
```

API Reference
-------------

[](#api-reference)

### Model Configuration

[](#model-configuration)

```
class MyModel extends ImmutableModel
{
    // Required: The database table
    protected string $table = 'my_table';

    // Optional: Primary key (null = non-identifiable model)
    protected ?string $primaryKey = 'id';

    // Optional: Database connection (null = default)
    protected ?string $connection = null;

    // Optional: Attribute casting
    protected array $casts = [
        'settings' => 'array',
        'created_at' => 'datetime',
    ];

    // Optional: Relations to eager load by default
    protected array $with = ['author'];

    // Optional: Accessors to append to array/JSON output
    protected array $appends = ['full_name'];

    // Optional: Hidden attributes
    protected array $hidden = ['internal_id'];

    // Optional: Visible attributes (whitelist)
    protected array $visible = ['id', 'name', 'email'];
}
```

### Querying

[](#querying)

All standard Eloquent read operations are supported:

```
// Finding records
MyModel::find($id);
MyModel::findOrFail($id);
MyModel::first();
MyModel::all();

// Where clauses
MyModel::where('status', 'active')
    ->where('created_at', '>', now()->subWeek())
    ->orWhere('featured', true)
    ->whereIn('category_id', [1, 2, 3])
    ->whereNotNull('published_at')
    ->get();

// Ordering & limiting
MyModel::orderBy('created_at', 'desc')
    ->limit(10)
    ->offset(20)
    ->get();

// Aggregates
MyModel::count();
MyModel::sum('price');
MyModel::avg('rating');
MyModel::max('views');
```

### Relationships

[](#relationships)

Supported relationship types:

```
class Post extends ImmutableModel
{
    protected string $table = 'posts';

    public function author()
    {
        return $this->belongsTo(User::class, 'user_id', 'id');
    }

    public function comments()
    {
        return $this->hasMany(Comment::class, 'post_id', 'id');
    }

    public function featuredImage()
    {
        return $this->hasOne(Image::class, 'post_id', 'id');
    }
}

// Eager loading
$posts = Post::with('author', 'comments')->get();

// Eager loading with constraints
$posts = Post::with(['comments' => fn($q) => $q->where('approved', true)])->get();

// Lazy loading (works, but watch for N+1)
$post = Post::find(1);
$author = $post->author;

// Relation queries
$comments = $post->comments()->where('approved', true)->get();
```

### Casting

[](#casting)

Full Eloquent casting support:

```
protected array $casts = [
    // Scalar types
    'count' => 'int',
    'price' => 'float',
    'active' => 'bool',
    'name' => 'string',

    // Date/time
    'published_at' => 'datetime',
    'birthday' => 'date',
    'updated_at' => 'immutable_datetime',
    'timestamp' => 'timestamp',

    // Complex types
    'settings' => 'array',
    'metadata' => 'json',
    'tags' => 'collection',

    // Custom casters
    'address' => AddressCast::class,
];
```

Custom casters must implement `Illuminate\Contracts\Database\Eloquent\CastsAttributes`. Only the `get()` method is called.

### Collections

[](#collections)

Query results return Laravel's standard `Eloquent\Collection`. All collection methods work normally - immutability is enforced on **database operations**, not on in-memory manipulation:

```
$users = User::all();

// All collection operations work normally
$active = $users->filter(fn($u) => $u->active);
$sorted = $users->sortBy('name');
$names = $users->pluck('name');
$mapped = $users->map(fn($u) => $u->toArray());
$users->push($newUser);     // Works - this is in-memory only
$users->transform(fn($u) => $u);  // Works

// In-memory model changes are allowed
$users->first()->name = 'New';       // Works (in-memory only)
$users->first()->computed = 'value'; // Works (add computed fields)

// But database persistence is blocked
$users->first()->save();   // Throws ImmutableModelViolationException
$users->first()->delete(); // Throws ImmutableModelViolationException
```

### Pagination

[](#pagination)

Full pagination support:

```
$paginated = MyModel::paginate(15);
$simple = MyModel::simplePaginate(15);
$cursor = MyModel::cursorPaginate(15);
```

### Chunking &amp; Lazy Loading

[](#chunking--lazy-loading)

```
// Chunk for batch processing
MyModel::chunk(1000, function ($models) {
    foreach ($models as $model) {
        // Process
    }
});

// Cursor for memory-efficient iteration
foreach (MyModel::cursor() as $model) {
    // Process one at a time
}
```

### Global Scopes

[](#global-scopes)

Apply query constraints automatically using Laravel's native `Scope` interface:

```
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('tenant_id', auth()->user()->tenant_id);
    }
}

class TenantModel extends ImmutableModel
{
    protected static function booted(): void
    {
        static::addGlobalScope(new TenantScope);
    }
}

// Bypass scopes when needed
TenantModel::withoutGlobalScopes()->get();
TenantModel::withoutGlobalScope(TenantScope::class)->get();
```

### Hydration from Raw Data

[](#hydration-from-raw-data)

Create models from existing data without database queries:

```
// Single model
$user = User::fromRow(['id' => 1, 'name' => 'John']);

// Collection of models
$users = User::fromRows([
    ['id' => 1, 'name' => 'John'],
    ['id' => 2, 'name' => 'Jane'],
]);
```

Comparison: ImmutableModel vs Eloquent
--------------------------------------

[](#comparison-immutablemodel-vs-eloquent)

FeatureImmutableModelEloquentRead queriesYesYesRelationshipsYesYesEager loadingYesYesAttribute castingYesYesAccessorsYesYesPaginationYesYesGlobal scopesYesYesWrite operations**Throws**YesDirty trackingNoYesEvents/ObserversNoYesMutatorsNoYesTimestampsNoYesMass assignmentNoYesPerformance
-----------

[](#performance)

Benchmarks show ImmutableModel is significantly faster for read operations:

### Hydration Speed

[](#hydration-speed)

RowsEloquentImmutableModelImprovement1000.30ms0.09ms-70%1,0003.09ms0.80ms-74%10,00034.27ms9.37ms-73%100,000447.29ms236.63ms-47%### Memory Usage

[](#memory-usage)

RowsEloquentImmutableModelPer Model (E)Per Model (I)Savings100166 KB97 KB1.66 KB998 B41%1,0001.61 MB973 KB1.65 KB996 B41%10,00016.2 MB9.56 MB1.66 KB1003 B41%100,000161.5 MB95.1 MB1.65 KB997 B41%### Eager Loading (10 posts per user)

[](#eager-loading-10-posts-per-user)

UsersModelsEloquentImmutableTime ΔEloquent MemImmutable MemMem Δ101101.68ms1.27ms-25%184 KB105 KB43%1001,1005.75ms2.23ms-61%1.76 MB1.02 MB42%1,00011,00064.22ms19.34ms-70%17.53 MB10.23 MB42%Use Cases
---------

[](#use-cases)

ImmutableModel is ideal for:

- **SQL Views**: Represent database views as read-only models
- **Read Replicas**: Query read-only database replicas safely
- **CQRS Read Models**: Enforce read-side immutability in CQRS architectures
- **Denormalized Projections**: Work with pre-computed, read-only data
- **API Responses**: Build response data with computed fields, knowing it won't accidentally persist
- **Architectural Boundaries**: Enforce that certain models are never written to from application code

Not Intended For
----------------

[](#not-intended-for)

- Models that need write operations
- Models using Eloquent events/observers
- Models requiring dirty tracking or timestamps
- Drop-in replacement for all Eloquent models

Exceptions
----------

[](#exceptions)

ExceptionWhen Thrown`ImmutableModelViolationException`Any database persistence attempt (save, update, delete, create, etc.)`ImmutableModelConfigurationException`Invalid model configurationContributing
------------

[](#contributing)

Contributions are welcome! Please ensure all tests pass before submitting a PR:

```
composer test
```

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

36

—

LowBetter than 82% of packages

Maintenance81

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 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

Every ~1 days

Total

12

Last Release

102d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6200f7bb602adea612ea1c80ba5bb26871be42d5b1ab79bc86018857623a2e0d?d=identicon)[therecluse26](/maintainers/therecluse26)

---

Top Contributors

[![therecluse26](https://avatars.githubusercontent.com/u/8239106?v=4)](https://github.com/therecluse26 "therecluse26 (28 commits)")

---

Tags

laravelmodeleloquentimmutablereadonly

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/brighten-immutable-model/health.svg)

```
[![Health](https://phpackages.com/badges/brighten-immutable-model/health.svg)](https://phpackages.com/packages/brighten-immutable-model)
```

###  Alternatives

[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k7.2M71](/packages/mongodb-laravel-mongodb)[tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models

1.8k4.8M26](/packages/tucker-eric-eloquentfilter)[dyrynda/laravel-model-uuid

This package allows you to easily work with UUIDs in your Laravel models.

4802.8M8](/packages/dyrynda-laravel-model-uuid)[spiritix/lada-cache

A Redis based, automated and scalable database caching layer for Laravel

591444.8k2](/packages/spiritix-lada-cache)[pdphilip/elasticsearch

An Elasticsearch implementation of Laravel's Eloquent ORM

145360.2k4](/packages/pdphilip-elasticsearch)[sebastiaanluca/laravel-boolean-dates

Automatically convert Eloquent model boolean attributes to dates (and back).

40111.7k1](/packages/sebastiaanluca-laravel-boolean-dates)

PHPackages © 2026

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