Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Immutable Laravel Package

teknoo/immutable

teknoo/immutable helps PHP and Laravel developers build immutable value objects and data structures, reducing accidental state changes and making code easier to reason about, test, and maintain. Lightweight, framework-agnostic, and simple to drop into existing projects.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require teknoo/immutable

There is no service provider, no config file, and no facade — the entire package is a single trait. Open vendor/teknoo/immutable/src/ImmutableTrait.php first; reading those ~40 lines is the whole onboarding and tells you exactly what guard behaviour you are opting into.

Minimal usage:

use Teknoo\Immutable\ImmutableTrait;

final class Email
{
    use ImmutableTrait;

    private string $value;

    public function __construct(string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: {$value}");
        }
        $this->value = $value;
    }

    public function value(): string { return $this->value; }
}

First real use case in a Laravel app: replace loose scalar parameters in a domain service or event payload with a small value object, so validation lives in the constructor and the object cannot be mutated after it leaves its factory. Start with one or two types (an ID, a Money, an Email), not a wholesale DTO rewrite — this package is a 4-star, single-file dependency with zero dependents, so treat it as a vendored helper you have personally audited.

Implementation Patterns

Value objects with named constructors. Keep the constructor private and expose static fromString(), fromRequest(), fromEloquent() factories. The trait only blocks writes after construction; it does not enforce validation, so the factories are where you put your filter_var/Assert-style checks.

Wither methods via clone. The idiomatic pattern with this trait is a copy-on-write setter:

public function withAmount(int $cents): self
{
    $copy = clone $this;   // clone resets the immutability guard
    $copy->cents = $cents; // allowed on the fresh clone
    return $copy;
}

This pairs well with Laravel's tap() style and with Eloquent accessors that need to derive a new value rather than mutate.

Custom Eloquent casts. Wire value objects into models so controllers and Blade never see primitives:

class MoneyCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): ?Money
    {
        return $value === null ? null : Money::fromCents((int) $value);
    }
    public function set($model, $key, $value, $attributes): ?int
    {
        return $value instanceof Money ? $value->cents() : $value;
    }
}

Form Requests as factories. Let FormRequest::validated() feed named constructors, keeping HTTP concerns out of the value object and the value object out of the request layer.

Queues and events. Use value objects as public properties on events/jobs, but add a __serialize/__unserialize pair (or implement Serializable) that reconstructs through the constructor — Laravel's job serialization will otherwise hit the immutability guard on hydrate. The trait's clone-based write window usually does not survive a plain unserialize(); test one round-trip through the database queue before committing to it.

Boundary discipline. Keep these objects in app/Domain or app/Support and out of Eloquent models themselves — use casts/accessors at the edge. That keeps the immutable layer unit-testable without a database and makes it trivial to swap for readonly classes later.

Gotchas and Tips

Audit before you adopt. Four stars, zero dependents, no changelog-heavy maintenance. Pin an exact version in composer.json and read the trait source in CI review — it is small enough to copy into your own codebase, which is often the better call for something this load-bearing.

Magic __set hides writes from tooling. Static analysers and IDEs will not see property assignment through the wither path, and PHPStan/Psalm may flag "property is never written" or "access to undefined property". Add @property-read annotations or explicit @internal setter methods, and run your static analysis after adding the trait.

Shallow clone semantics. clone shares nested objects. A withAddress() on an object holding a Country still shares the same Country instance; if that nested object is itself mutable, your "immutable" graph is not. Either make every nested type immutable too, or implement __clone and clone children explicitly — remember to call parent::__clone() if the trait defines it.

Serialization and queue hydration. This is the most common production failure: an immutable DTO is dispatched onto a queue, comes back via unserialize(), and the first internal write throws. Symptoms are LogicException in failed_jobs. Fix by constructing inside __unserialize() (which PHP calls instead of __wakeup on Serializable-less classes in 7.4+) rather than relying on restored properties.

var_dump and debugging. Because properties are protected/private and guarded, dd() on the object looks fine but an attempted mutation throws deep in a stack trace. When debugging, dump the result of a getter, not the raw object, and check Xdebug settings — the trait's guard is sometimes implemented via a backtrace/flag, and step-debugging through __set is noisy.

No config, no extension points beyond inheritance. There is no config file to publish and no contract to bind in the container. The only extension point is subclassing or overriding __set/__clone; if you override either, call the trait's version via parent:: or you silently disable the guard. In practice, prefer composition (a small abstract ValueObject in your own code that uses the trait) over editing vendor behaviour.

Compare against readonly first. On PHP 8.1+ the language gives you public readonly properties, and Laravel 10/11 codebases are almost universally on that. Use this package only if you need the wither-with-clone ergonomics on older PHP, or you specifically want write-once-then-throw rather than readonly's reinitialisation rules. A quick side-by-side in one service class beats a migration you later regret.

Testing tip. Assert immutability explicitly — $this->expectException(LogicException::class); $obj->foo = 'bar'; — so that a future package upgrade or a stray parent::__set override doesn't quietly turn your value objects mutable.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons