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

Laravel Value Objects Laravel Package

michael-rubel/laravel-value-objects

Laravel Value Objects offers an abstract base class and Eloquent casts for turning model attributes into immutable value objects, with validation, comparison helpers, and PHP 8 support for cleaner, type-safe domain code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer; the package is auto-discovered and needs no service provider or config file for basic use:

composer require michael-rubel/laravel-value-objects

Start by reading two places in the source:

  1. src/ValueObject.php — the abstract base. This is where make(), value(), validation, string/JSON serialization and equality live. Everything else inherits from it.
  2. src/Primitive/ and src/Collection/ — the shipped objects (StringValue, IntValue, FloatValue, Number, BoolValue under primitives; Email, Phone, Url, Uuid, Name, FullName, Title and similar under collections). Class names and the exact catalogue can shift between releases, so treat the directory listing as the source of truth.

The first practical use case is replacing a raw scalar at an application boundary with a validated object:

use MichaelRubel\ValueObjects\Collection\Complex\Email;

$email = Email::make($request->string('email')->toString());

echo $email;       // string representation of the value
$email->value();   // underlying primitive

The mental model: construct once at the edge (request, command, job payload, seeder), validate/normalize there, then pass the object through the domain. Inside services and actions, type-hint the value object instead of string, so invalid values cannot enter.

Because make() validates, the first thing to verify on day one is what happens on bad input in your app: whether it throws a validation exception, and how your exception handler renders that. Decide early whether you want a 422, a domain exception, or whether the FormRequest should have rejected the input before make() is ever called.

Version note: releases up to and including 7.0.2 are tested against PHP 8.4, so a modern PHP 8.3/8.4 runtime is well covered. Pin at ^7.0.2 if you rely on signed numeric values (see the Number gotcha below).

Implementation Patterns

1. Validate at the boundary with the object's own rules. The value objects expose their validation rules, which lets you reuse the exact same constraints in a FormRequest instead of duplicating regexes:

use MichaelRubel\ValueObjects\Collection\Complex\Email;

public function rules(): array
{
    return [
        'email' => Email::rules(),
    ];
}

If your installed version does not expose a convenient rule object, write a tiny custom rule that calls Email::make($value) inside passes() and translates failures into a message. Either way, the FormRequest becomes the single source of truth for user-facing errors and the value object remains the invariant enforcer.

2. Cast Eloquent attributes explicitly. Laravel's $casts array does not know how to call make(), so wire it yourself. The cleanest option is a small CastsAttributes implementation parameterised by the VO class:

final class ValueObjectCast implements CastsAttributes
{
    public function __construct(private string $class) {}

    public function get($model, string $key, $value, array $attributes): ?object
    {
        return $value === null ? null : $this->class::make($value);
    }

    public function set($model, string $key, $value, array $attributes): array
    {
        if ($value === null) {
            return [$key => null];
        }

        $value = $value instanceof $this->class
            ? $value
            : $this->class::make($value);

        return [$key => (string) $value];
    }
}
protected $casts = [
    'email' => ValueObjectCast::class.':'.Email::class,
];

For a handful of attributes, a native Illuminate\Database\Eloquent\Casts\Attribute accessor/mutator pair is less machinery and just as effective.

3. Keep DTOs and action signatures typed. A DTO whose constructor takes Email $email and Number $amount documents invariants better than docblocks. Services accept the object, not the primitive, and only convert back to a scalar at the persistence or HTTP boundary.

4. Compare with equals(), not ===. Two separately constructed Email objects representing the same address will not be identical instances. Use the equality method the base class provides (or compare ->value() results) in tests and domain logic.

5. Serialize deliberately for APIs. The base implements array/JSON serialization and __toString, so VOs generally render as their primitive in toArray()/toJson(). Confirm the exact shape with a quick json_encode($vo) before shipping it in a resource — if toArray() returns ['value' => ...] in your version, wrap it in an API Resource rather than returning the raw object.

6. Use collections for bulk conversion. When mapping request arrays or CSV rows:

$emails = collect($payload['recipients'])
    ->map(fn (string $email) => Email::make($email));

Be aware that the first invalid element throws; if you need partial success, catch per item and collect failures separately.

7. Extend the base for domain-specific values. Custom objects follow the same pattern as the built-ins: extend ValueObject, declare rules() (mirroring the key/shape the built-in VOs use), and optionally override string conversion or add domain helpers. This is the extension point worth investing in — a VatNumber, Iban, PostalCode or Money object costs ~20 lines and pays off across every request that touches it.

Gotchas and Tips

  • Construction can throw. make() is not a permissive cast; invalid input fails loudly. In jobs and console commands decide whether that should retry, fail permanently, or be caught and logged. In HTTP flows, prefer validating in the FormRequest first so users get field-level errors instead of a 500 from deep in a service.
  • Nullability is your responsibility. Value objects generally represent a present value. Do not try to encode "no email" as Email::make(null). Keep nullable columns and nullable DTO properties nullable, and convert to a VO only when a value exists.
  • Normalization happens at construction. Several built-ins normalize (trimming, lowercasing emails, formatting phones). This is usually a feature, but it means (string) Email::make($input) may not equal $input. Do not normalize twice, and do not assume the stored database value matches the raw request byte-for-byte.
  • Signed numbers in Number — require 7.0.2. 7.0.2 fixes the handling of negative numbers in the Number value object. If you use Number for balances, deltas, temperatures or adjustments, pin ^7.0.2 (or newer) and add a regression test with a negative value; anything earlier can misbehave on Number::make(-5). IntValue/FloatValue are unaffected.
  • Avoid double validation drift. If the FormRequest validates with Email::rules() and the service constructs Email::make(), that is fine — but if someone copies the regex into the FormRequest instead of reusing rules(), the two will diverge. Grep for duplicated regexes during review.
  • Casting and null in set(). A custom cast must return [$key => null] for null input, otherwise mass-assignment of an empty optional field will throw. Test Model::create(['email' => null]) explicitly.
  • Query bindings. Do not rely on implicit string conversion in the query builder. Use ->where('email', $email->value()) (or an explicit (string) $email) so behavior is obvious and static analysis stays happy.
  • Column sizing. Normalized phone numbers (E.164) and UUIDs have predictable lengths; make sure the migration matches. A varchar(20) phone column and a formatted phone value is a classic first-week bug. Signed numeric columns need a type that actually allows negatives — do not pair a negative Number with an unsignedInteger.
  • Composite constructors vary. Objects like FullName may take multiple arguments or a single formatted string. Read the class before assuming; the base make() signature is variadic in some versions.
  • json_encode vs toArray. These can differ. If VOs appear in model toArray() output, cast/convert them in the resource layer rather than letting the raw object determine your public API contract.
  • Exception rendering. If construction failures surface as ValidationException, Laravel renders 422 automatically only in HTTP context. In APIs with custom exception handlers, verify the mapping; otherwise a bad queue payload looks like an unhandled 500.
  • Maturity check. The package is MIT, small, and maintained (7.0.2 is the current patch, on top of the 7.x line, with CI running against PHP 8.4) but has very few dependents. That is fine for a utility of this size — the surface area is small and the objects are easy to fork or vendor — but pin the version in composer.json and skim the changelog before upgrades, since class names, rule shapes and numeric edge cases have changed across releases.
  • Debugging. When a VO misbehaves, dump(get_class($vo), $vo->value(), (string) $vo) distinguishes "validation rejected input" from "normalization changed the value" from "serialization shape differs." That trio resolves most confusion in minutes. For numeric VOs, also var_dump($vo->value()) to confirm the sign survived construction.
  • Tests. Assert both the type and the primitive: assertInstanceOf(Email::class, $dto->email) plus assertSame('[email protected]', $dto->email->value()). For equality semantics use assertTrue($a->equals($b)) rather than assertEquals on objects, which can pass for the wrong reasons.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums