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

Component Value Objects Laravel Package

serendipity_hq/component-value-objects

A Laravel/PHP value object component for modeling domain concepts with immutable, type-safe objects. Encapsulate validation and formatting, reduce primitive obsession, and bring clarity and safety to your codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and pin the version, since the package is archived:

composer require serendipity_hq/component-value-objects

Look first at:

  1. src/ — one directory per value object (Email/, Url/, Address/, Money/, PhoneNumber/, Hostname/, Percentage/, TaxRate/, etc.). Each contains the VO plus an interface.
  2. src/ValueObjectInterface.php, SimpleValueObjectInterface.php, CompositeValueObjectInterface.php — the contract every class honours (__toString(), get*() getters, validation).
  3. src/Converter/ — StringConverter, JsonConverter, ArrayConverter; these are how you turn a VO into something you can persist or ship over the wire.
  4. README.md — the author's own examples are the fastest reference for array key names per VO.

The canonical first use case is wrapping an email address so it can never be an invalid string in your domain:

use SerendipityHQ\Component\ValueObjects\Email\Email;

$email = new Email(['email' => '[email protected]']);

echo $email->getEmail();   // [email protected]
echo (string) $email;      // [email protected]

That is the whole mental model: constructor takes an array of named parameters; validation runs immediately; getters return primitives; converters emit serialised forms. Composite VOs (Address, Money, Person) follow the same shape with more keys:

use SerendipityHQ\Component\ValueObjects\Address\Address;

$address = new Address([
    'streetAddress' => 'Via Roma 1',
    'postalCode'    => '00100',
    'city'          => 'Roma',
    'country'       => 'IT',
]);

Implementation Patterns

Array-shape construction + DTOs. Build VOs inside DTOs or action classes, not inside controllers. A CreateCustomerData object that holds Email, PhoneNumber, and Address gives you a validated boundary between HTTP and Eloquent.

Validate in the FormRequest, construct the VO in the DTO. The package throws InvalidArgumentException on bad input, so you want that failure to happen in one place (the mapping layer) rather than scattered through model accessors.

Custom Eloquent casts. This is the highest-leverage integration for day-to-day work — the VO lives in memory, the string lives in the database:

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use SerendipityHQ\Component\ValueObjects\Email\Email;

class EmailCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): ?Email
    {
        return $value === null ? null : new Email(['email' => $value]);
    }

    public function set($model, string $key, $value, array $attributes): ?string
    {
        if ($value === null) { return null; }
        $vo = $value instanceof Email ? $value : new Email(['email' => $value]);
        return $vo->getEmail();
    }
}

Register with protected $casts = ['email' => EmailCast::class];. Repeat for Url, PhoneNumber, Hostname — the pattern is identical.

Comparisons and de-duplication. Use the comparable interface instead of comparing strings:

if ($existing->equals($incoming)) { /* same address */ }

Serialisation via converters. For API resources, don't reach into getters individually — let the converter flatten composite VOs:

use SerendipityHQ\Component\ValueObjects\Converter\StringConverter;
use SerendipityHQ\Component\ValueObjects\Converter\JsonConverter;

$emailString = (new StringConverter($email))->convert();
$addressJson = (new JsonConverter($address))->convert();

Money / Price / TaxRate as a cluster. These VOs are designed to be composed: a Price carries an amount, a currency, and a TaxRate; a Money is the bare amount + currency. Model your pricing domain around that composition rather than reinventing it.

Extension point for your own VOs. Subclass SimpleValueObject (or CompositeValueObject), implement the matching interface, and override validate(). You get string conversion and comparison traits for free.

Gotchas and Tips

  • Archived. No fixes, no PHP 8.x guarantees. Either accept the risk, or fork it into your own namespace ("repositories": [{ "type": "vcs", "url": "..." }] in composer.json) before you depend on it heavily. Check composer.json for the PHP constraint — the code predates enums, readonly properties, and attributes.
  • Constructor takes an array, always. new Email('[email protected]') will not work; it must be new Email(['email' => '[email protected]']). Missing required keys throw immediately — there are no defaults for the essential fields.
  • Validation is constructor-time. Every new is a validation checkpoint. Wrap user-supplied data in try/catch (InvalidArgumentException $e) and translate to a 422. Do not construct VOs inside get() accessors without a try/catch, because a legacy bad row will blow up a list view.
  • __toString() may differ from your input. VOs normalise (e.g. trimming, lower-casing, standardising phone numbers). Always persist the VO's own string form, not the request payload, or you'll churn the DB on every save.
  • Phone numbers need context. PhoneNumber typically relies on libphonenumber-for-php and a default region — supply defaultRegion in the constructor array or validate nationally and hope. Don't assume it accepts free-form strings.
  • Money amounts. Check whether the amount key is major or minor units before trusting it; getting this wrong is a silent 100× bug. Do the same for Percentage — string "10" vs int 10 may mean 10% vs 1000%.
  • No JsonSerializable. json_encode($email) will not do what you expect. Use JsonConverter, or implement JsonSerializable in a small wrapper.
  • equals() is not ===. It compares normalised comparable values, which is the point — but it will throw if you feed it the wrong type. Guard before calling.
  • Casts short-circuit validation on read. Eloquent hydrates through get(), so historical rows that would fail validation today will still throw when your cast constructs the VO. Consider returning null for obviously malformed values, or backfill the data.
  • Don't pass VOs to the query builder. where('email', $email) relies on __toString(), which is implicit magic. Use $email->getEmail() explicitly so static analysis and future refactors stay honest.
  • Testing. Assert on both the getter (getEmail()) and the string form — that pair is the contract. For composite VOs, assert on the converter output so changes to the internal array shape surface in one place.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect