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.
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().
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.
$money->amount = ..., add a withAmount() that returns a new instance; do not add setters.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.null vs. absent, and default application are the most common source of surprises in ComplexValueObject. Add a test per field for the absent case.Fields object statically or hydrate in batches; profile before optimizing.toArray() → reconstruct → compare) for anything crossing storage or an API boundary.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.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.How can I help you explore Laravel packages today?