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

Php Value Objects Laravel Package

bruli/php-value-objects

A lightweight PHP library for building immutable value objects with typed primitive wrappers and validation. bruli/php-value-objects helps enforce domain invariants, reduce primitive obsession, and keep Laravel or any PHP codebase clearer and safer.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install

    composer require bruli/php-value-objects
    

    Check composer.json for the required PHP version (the library targets modern PHP) and confirm it matches your app; if you're on an older runtime you'll get parser/runtime errors, not install failures.

  2. Orient yourself in src/ The package is deliberately flat — every class is a single, self-contained value object. Read these first:

    • The base ValueObject (or equivalent abstract) that defines equals() and __toString().
    • Uuid — the one you'll use most for aggregate IDs.
    • Email, StringLiteral, Integer, Boolean, DateTime, Money — the primitives you'll wrap.
  3. First use case: typed IDs and validated primitives on an entity

    use Bruli\ValueObject\Uuid;
    use Bruli\ValueObject\Email;
    
    final class User
    {
        public function __construct(
            private Uuid $id,
            private Email $email,
        ) {}
    }
    
    $user = new User(
        Uuid::random(),
        Email::fromString('[email protected]'),
    );
    

    The value the package buys you immediately: an invalid email throws at construction, not three layers later when you try to send mail.

  4. Next step — wire one value object into persistence before you roll it out everywhere. Reconstruct with the matching factory (Uuid::fromString((string) $row['id'])) so you discover the storage-format questions early (see Gotchas).

Implementation Patterns

Static named constructors, never new where possible Use Uuid::random(), Email::fromString(), DateTime::fromString(). Named constructors keep intent in the domain and let the library swap internals without breaking call sites.

Equality via equals(), not == or === Two Uuid instances wrapping the same value are different PHP objects.

if (!$this->id->equals($other->id)) {
    throw new \DomainException('Cannot compare different aggregates');
}

Use this inside aggregate equals() methods and in tests.

Persist as primitives at the boundary, hydrate through factories The package gives you __toString() / string accessors, not ORM mappings. Two common bridges:

Eloquent cast:

class UuidCast implements \Illuminate\Contracts\Database\Eloquent\CastsAttributes
{
    public function get($model, $key, $value, $attributes): ?Uuid
    { return $value === null ? null : Uuid::fromString($value); }

    public function set($model, $key, $value, $attributes): ?string
    { return $value === null ? null : (string) $value; }
}

Then protected $casts = ['id' => UuidCast::class];.

Doctrine: register a DBAL\Types\Type per VO, or use a ValueObject hydrator in your mapper. There is no shipped DBAL type, so budget for this up front if you're on Doctrine.

Validate in the VO, translate at the framework edge The package throws generic exceptions (typically InvalidArgumentException) from constructors. In Laravel, catch them in a Form Request or a small Rule:

public function passes($attribute, $value): bool
{
    try { Email::fromString($value); return true; }
    catch (\InvalidArgumentException) { return false; }
}

This keeps HTTP validation messages in your app and leaves the domain strict.

Serialize at the boundary, not in views Most VOs implement JsonSerializable; if so, json_encode($dto) "just works" as long as every nested VO does too. For Blade/Inertia, convert to primitives in a resource or presenter — don't leak VO objects into templates.

Compose VOs for richer types Money typically holds an Integer (cents) plus a currency string; Address holds several StringLiterals. Build your own composite VOs the same way rather than flattening to arrays — the validation comes for free.

Immutable by default There are no setters. To change an email, produce a new instance and assign it on the entity; if you need an update, model it as a domain method (changeEmail(Email $email)), not a setter.

Gotchas and Tips

  • Timezone and format drift in DateTime. Be explicit about the format string and timezone you feed fromString(). If your DB stores UTC but the VO parses local time you'll get "off by hours" bugs that only appear around midnight. Standardise on ISO-8601 with an offset at every boundary.
  • String vs binary UUIDs. Uuid supports both representations in most versions; pick one for storage and never mix. Binary in a BINARY(16) column is faster but unreadable in logs; string in CHAR(36) is easier to debug. Document the choice — a half-migrated table is painful.
  • null is not a value object. Constructors reject empty strings and invalid input. Model "optional email" as ?Email on the entity, not as an Email wrapping ''.
  • Generic exceptions. Catching InvalidArgumentException to distinguish "bad email" from "framework bug" is fragile. Wrap construction in a small domain-exception factory in your app layer, or extend the VO classes whose constructors you want to specialise.
  • equals() is class-sensitive. Most implementations will refuse to compare an Email with a StringLiteral holding the same text. That's correct — don't bypass it with string casts in comparisons.
  • No shipped ORM integrations. Eloquent casts, Doctrine types, and API resources are all on you. Write them once, in one place (app/Casts, App\Doctrine\Types), and test the round-trip: VO → DB → VO → equals(original).
  • Serialization of nested VOs. json_encode an entity holding a VO that doesn't implement JsonSerializable gives you {} or a private-property dump. Add a test that asserts the JSON shape of your main DTOs.
  • Extension points. For a custom VO, extend the base class and implement equals() and __toString() consistently; keep the wrapped value private readonly and add one or two named constructors (fromString, random). Don't expose a public constructor — you lose the validation-once guarantee that makes the library worth using.
  • Debugging. Because these objects are immutable and throw on construction, a var_dump in a loop will show many short-lived objects — that's expected, not a leak. When a constructor throws, the stack trace points at the VO; the useful frame is the caller, so log the offending raw value in your factory wrapper.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core