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

Value Objects Bundle Laravel Package

plusforta/value-objects-bundle

A Laravel bundle of reusable value objects for common domain concepts, helping you keep validation, casting, and type safety consistent across your application instead of scattering raw strings and primitives throughout your models and services.

View on GitHub
Deep Wiki
Context7

Getting Started

Install. This is a GitLab-hosted package with zero dependents, so verify whether it resolves from Packagist before assuming it does. If it does not, add a VCS repository entry:

{
  "repositories": [
    { "type": "vcs", "url": "https://gitlab.com/plusforta/public/value-objects-bundle.git" }
  ]
}
composer require plusforta/value-objects-bundle

Then confirm the constraint set actually matches your stack before writing code — this package is small enough that the Laravel/PHP support matrix is the only thing that can block you, and that is a two-minute check:

composer show plusforta/value-objects-bundle
composer why-not plusforta/value-objects-bundle

First place to look. Read src/ before reading any README example. The whole library is likely a few hundred lines, and you need four things from it:

  1. The abstract base value object — how construction and immutability are enforced (readonly? private constructor? final?), and where the validation hook lives (validate(), assertValid(), or similar).
  2. The equality method (equals() / isEqualTo()) — whether it compares by value or by identity, and how it treats floats and nested collections.
  3. The normalization surface — toNative(), toPrimitive(), jsonSerialize(), __toString(), and the matching fromNative()/fromString() static factory. This is the contract your database columns and JSON payloads will depend on.
  4. The type catalog — which concrete wrappers ship out of the box (identifiers/UUID, string, int, float, bool, collection) versus what you are expected to subclass.

Read one concrete type top to bottom. It will tell you the extension pattern the author intended, which is faster than guessing.

First use case. Pick a single attribute on a single model where the primitive is causing real pain — a reference/identifier, an email, a currency code. Wrap it, cast it, and check the JSON output. Do not start with money; start with something where validation is the whole value and arithmetic is not involved.

// 1. Subclass for a domain concept rather than using the raw base type.
final class OrderReference extends StringValueObject
{
    protected function validate(string $value): void
    {
        if (! preg_match('/^ORD-[A-Z0-9]{10}$/', $value)) {
            throw new InvalidArgumentException("Malformed order reference: {$value}");
        }
    }
}
// 2. Cast at the model boundary.
protected $casts = [
    'reference' => OrderReferenceCast::class,
];

Then write a throwaway test covering the three things you will rely on every day: invalid input throws, two instances with the same primitive are equals(), and toNative() round-trips through the cast. That test is your regression net if you later fork or upgrade.


Implementation Patterns

Subclass for domain concepts, instantiate at boundaries. Use the packaged scalars (StringValueObject, IntValueObject, etc.) as base classes, not as the types flowing through your domain. OrderReference, Iban, EmailAddress — not StringValueObject. The payoff is that function refund(Iban $iban) is self-documenting and un-forgeable; function refund(string $iban) is neither. Instantiate from primitives only at the edges: HTTP input, database hydration, queue payloads, third-party API responses.

Put the rule in the validation hook, once. The pattern that makes these objects worth the ceremony is that an instance existing is proof the value is well-formed. Resist adding if ($iban->isValid()) checks downstream — they are dead code that erodes the guarantee. Anything that must be true for the object to be meaningful belongs in the hook at construction.

Prefer named constructors for intent. Raw new Money(1250) at a call site is a unit ambiguity waiting to happen. Wrap the base constructor with factories that say what the number means:

public static function fromMinorUnits(int $cents, Currency $currency): self
public static function fromDecimalString(string $amount, Currency $currency): self

Let the cast be the only place primitives and objects meet. In Eloquent, the attribute is the object in PHP and a string/int in the column; the cast does the conversion. Do not scatter (string) $model->reference through your codebase — that is a sign the object should not be there, or the cast is wrong. Likewise, in API resources serialize explicitly:

return ['reference' => (string) $this->reference];

Compare with equals(), never == or ===. Two Iban instances wrapping the same value are different object identities. Any aggregate that contains value objects — Order, Payment, an event payload — must override its own equality in terms of the constituent equals() calls, or you will get spurious equality failures in tests and in domain logic.

Wrap the package behind your own namespace. Define App\Domain\ValueObjects\{Iban, OrderReference, Money} as thin subclasses of the package types, and let application code import only your namespace. This costs one small file per type and buys a one-file-per-type exit ramp — if the package is abandoned or a type misbehaves, you change the wrapper's parent and nothing else in the codebase moves. Given zero stars and zero dependents, this is not optional paranoia; it is the reason the dependency is safe to adopt at all.

Treat collections as immutable throughout. If the bundle ships a collection value object, use it for lists that cross a domain boundary and treat the underlying array as private. Exposing toArray() and mutating it is how "immutable" value objects stop being immutable in practice.

Test the contract, not the implementation. For each type you adopt, assert: (a) construction rejects the invalid shapes you care about, (b) equals() is reflexive, symmetric, and transitive for representative values, (c) serialization round-trips. Those three tests are what let you fork, upgrade, or replace the package without a re-audit.


Gotchas and Tips

Immutability is often nominal in PHP value objects. Verify, don't assume: is the constructor final or private? Are properties readonly/private? Does a collection type hold a mutable array or DateTime internally and return it by reference? A value object that leaks a mutable internal is worse than a primitive, because it looks safe. Check this on day one, on the types you actually intend to use.

Serialization output is a public contract you now own. jsonSerialize() / toArray() / __toString() shape your API responses and possibly your stored columns. Pin them with tests. If a future fork changes toArray() from ['amount' => 1000, 'currency' => 'EUR'] to ['amount' => '10.00', ...], you want a red test, not a production incident.

Keep the database schema package-agnostic. Store the raw primitive (string column for IBAN, integer minor units for money) and cast at the model boundary. You then have a single line — the cast — coupling persistence to the package. Do not let a package-specific JSON envelope dictate your column shape.

Float equality and currency mismatch are the two landmines in the numeric types. If the bundled number type uses strict === on floats, 0.1 + 0.2 !== 0.3 applies to your value objects too. And unless the bundle explicitly guards it, nothing stops Money(100, EUR)->add(Money(100, USD)). Do not use this package as your money implementation — use moneyphp/money + brick/money for arithmetic, rounding modes, and allocation, and let this bundle handle the surrounding identifiers and scalars. The value of this library is typed identity and validation, not numerical correctness.

Decide your exception policy before you adopt, not after. Construction failures throw. In a form request or API controller you need to map those to 422s; in a queue consumer you probably want them to fail loudly. Pick one boundary-mapping layer and route all conversion through it. If you catch the package exceptions in fifty controllers, you have coupled your error handling to the package's class names.

Watch the null path through Eloquent casts. Laravel handles null attributes by not invoking the cast, but a custom Castable implementation can still receive null or a raw string depending on how it is written. Test nullable columns explicitly — a cast that throws on null will surface as a confusing ValueError in production rather than in your test suite.

Log the class and the offending value, guard for PII. A validation message that says "invalid email" with no value and no type costs hours of debugging. One that echoes a full IBAN into a log aggregator can cost you a compliance finding. Include the value object class name always; include the raw value only where the field is not personal data.

The validation hook and equality are the extension points; the constructor usually is not. Override validate() and equals() freely. If you find yourself needing to override the constructor to add a third field, reconsider — a value object with two independently varying fields is usually two value objects.

The package encodes one company's conventions, and that company is German. Before spreading IBAN/BIC, postal-code, tax-ID, and EUR assumptions through your codebase, check the validation rules against your actual markets. A rule that is too strict (rejecting a valid Dutch postcode) is a launch blocker; one that is too loose is a data-quality bug you will find in six months.

Supply-chain notes that affect your day-to-day. No changelog discipline is guaranteed, no Stack Overflow corpus exists, and there is no ecosystem to tell you a release broke something. Concretely: pin the version in composer.json (no ^ range that could drift on a Friday deploy), vendor or fork the source into your repo if policy allows, and record the decision in an ADR with a named owner. When a developer hits a bug in six months, the fix path needs to be "patch our fork," not "file an issue on a dormant GitLab project and wait."

When in doubt, read the source — it is small. For a library this size, "check the docs" is usually slower than grepping src/ for the method you are unsure about. Treat the code as the specification.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib