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

Valueobjects Laravel Package

runn/valueobjects

Runn ValueObjects gives you a lightweight foundation for immutable PHP value objects, with typed base classes for strings, integers, booleans and arrays, validation helpers, and Eloquent casts to keep domain values consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin. composer require runn/valueobjects pulls the RunnMe primitives/validation packages transitively. Because the package is LGPL-3.0 and has almost no public downstream usage, pin an exact tag ("runn/valueobjects": "1.0.*" or a commit hash) and commit composer.lock. There is no Laravel service provider, no config file to publish, and no facade — it is a plain library you compose into your domain layer.

Where to look first.

  • src/ValueObject.php, src/SingleValueObject.php, src/ComplexValueObject.php — the base contracts you will subclass.
  • src/Values/ — ready-made scalars (IntValue, FloatValue, StringValue, BoolValue, plus domain types such as email/URL/UUID/date-time depending on version).
  • src/Field.php and src/Fields.php — the schema mechanism ComplexValueObject uses.
  • tests/ — the README is thin, so the PHPUnit suite is effectively the documentation. Read tests for any class before trusting it in production.

First use case. Replace a raw scalar at a boundary with a typed, self-validating object:

use Runn\ValueObjects\Values\IntValue;
use Runn\ValueObjects\Values\StringValue;

$qty   = new IntValue('3');            // coerced + validated in the constructor
$email = new StringValue('[email protected]');    // or EmailValue if your version ships it

$qty->getValue();                      // int(3)
(string) $email;                       // "[email protected]"

The point is that an invalid value cannot exist as an object: construction throws, so the rest of your code never re-checks is_int() or filter_var().

Implementation Patterns

Wrap at the edges, unwrap at the edges. Construct value objects in FormRequests/controllers/console commands and in model casts. Keep them out of query builders and raw SQL — call ->getValue() (or cast to string) when you cross back into the framework.

Single value objects for domain scalars. Subclass SingleValueObject when you have a primitive with rules (SKU, phone, currency code, percentage). Override the coercion/validation hook rather than validating in the caller:

final class Sku extends \Runn\ValueObjects\SingleValueObject
{
    protected function validate($value): void
    {
        if (!preg_match('/^[A-Z]{3}-\d{4}$/', (string) $value)) {
            throw new \InvalidArgumentException('Bad SKU');
        }
    }
}

Complex value objects for composite concepts. Money, Address, DateRange, GeoPoint. Declare the field schema the way the base class expects (mirror tests/ComplexValueObjectTest.php for your version — the declaration style has changed across RunnMe releases):

final class Money extends \Runn\ValueObjects\ComplexValueObject
{
    public static function getFields(): \Runn\ValueObjects\Fields
    {
        return new \Runn\ValueObjects\Fields([
            'amount'   => new \Runn\ValueObjects\Field(['type' => \Runn\ValueObjects\Values\FloatValue::class, 'required' => true]),
            'currency' => new \Runn\ValueObjects\Field(['type' => \Runn\ValueObjects\Values\StringValue::class, 'required' => true, 'default' => 'USD']),
        ]);
    }
}

$price = new Money(['amount' => 19.99, 'currency' => 'EUR']);
$price->toArray();

Entities hold value objects, not scalars. Give your Eloquent/domain entity typed accessors returning Money/Email; make setters replace the whole VO (immutability) rather than mutating fields. This is the single biggest day-to-day win: illegal states become unrepresentable at the entity boundary.

Eloquent casts as the integration seam. If you persist complex VOs, write one CastsAttributes class per VO and serialize via toArray()/jsonSerialize(), restoring with the VO constructor. That keeps serialization logic in one place instead of scattered json_decode calls.

Translate exceptions into Laravel validation errors. Wrap construction in a try/catch on the package's value-object exception base (inspect src/Exception/) and rethrow as ValidationException::withMessages([...]). Better still, build a reusable Rule object that instantiates the VO inside passes() — then $request->validate() handles everything uniformly.

Comparison and collections. Use the VO's equals() (where provided) instead of ==; == on objects compares properties and will bite you with nested VOs. For lists, keep them as arrays of VOs or the package's typed collections, and map to scalars only when rendering.

Gotchas and Tips

  • Tiny ecosystem, zero dependents. Nine stars and no downstream users means you are the QA team. Read the source of every class you rely on, and write characterization tests around the behaviors you depend on before upgrading — minor releases may change coercion rules.
  • LGPL-3.0 is not MIT. Fine for most SaaS use via Composer (dynamic linking), but if you modify the library and distribute the result, you must publish those modifications. Get legal sign-off before shipping it inside a distributed product.
  • Immutability is not enforced by the language. Treat every VO as frozen. If you find yourself wanting $money->amount = ..., add a withAmount() that returns a new instance; do not add setters.
  • Coercion happens before validation. new IntValue('3abc') may or may not throw depending on the primitive's cast rules — test your exact inputs (empty string, null, "0", floats) rather than assuming.
  • Field-order and required/default semantics are version-sensitive. Missing optional fields, null vs. absent, and default application are the most common source of surprises in ComplexValueObject. Add a test per field for the absent case.
  • Reflection cost in hot paths. Schema/field construction per instantiation can be measurable in loops (importers, pricing engines). Cache the Fields object statically or hydrate in batches; profile before optimizing.
  • DateTime timezone drift. Date/time VOs frequently normalize to UTC or to PHP's default timezone on construction. Assert round-tripping explicitly (toArray() → reconstruct → compare) for anything crossing storage or an API boundary.
  • Don't use it for IDs unless you must. If your app already uses ramsey/uuid or Laravel's HasUuids, keep one UUID representation; mixing the package's UUID VO with framework UUIDs creates conversion noise for no gain.
  • No framework wiring means no surprises — and no help. There is nothing to php artisan vendor:publish, no config to tune. Debugging is var_dump/Xdebug plus reading src/. The extension points are simply subclassing ValueObject/SingleValueObject/ComplexValueObject and supplying custom Field types; there is no event or macro system.
  • Assert exception classes, not messages. Message text is unversioned and will break your tests on any patch release.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings