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

Valinor Laravel Package

cuyz/valinor

Valinor maps raw inputs like JSON and plain arrays into strongly typed PHP objects, validating every value along the way. It supports native types plus PHPStan and Psalm advanced types, gives precise human-readable errors, and can normalize objects back into formats like JSON or CSV.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require cuyz/valinor

Start at valinor-php.dev and read, in this order: mapping → validation errors → caching. Those three cover 90% of day-to-day use. The README example is the whole mental model: you describe a target class with native types plus PHPDoc types, hand the mapper raw input, and either get a fully valid object graph or a structured error tree.

use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Mapper\Source\Source;
use CuyZ\Valinor\Mapper\MappingError;

final readonly class User
{
    public function __construct(
        /** @var non-empty-string */
        public string $name,
        public int $age,
        /** @var list<string> */
        public array $roles,
    ) {}
}

try {
    $user = (new MapperBuilder())
        ->mapper()
        ->map(User::class, Source::json($request->getContent()));
} catch (MappingError $error) {
    // $error->messages() holds the validation tree
}

First use case: mapping an HTTP request body or a third-party API response into a readonly DTO. Replace hand-written isset()/is_int()/json_decode() validation with a class whose constructor is the schema. Two classes to keep in your head: MapperBuilder (configuration) and Source (input decoding — Source::json(), Source::file()).

Add a cache before you ship anything to production:

(new MapperBuilder())
    ->withCache($psr16Cache) // e.g. a filesystem or Redis PSR-16 pool
    ->mapper();

Implementation Patterns

DTO-first, PHPDoc-as-schema. The docblock is the contract. list<City>, array<string, int>, int<0, 100>, non-empty-string, class-string<Foo>, unions and shaped arrays all work because Valinor parses PHPStan/Psalm types. Keep DTOs final readonly with all-promoted constructors — no setters, no post-construction mutation.

HTTP boundary pattern. Map early in the controller / form-request layer, catch MappingError once, and flatten messages() into a field => message bag for a 422 response. The message tree exposes the input path for each error, so you can point at cities.0.timeZone rather than "invalid input".

Loose input from forms, CSV, and query strings. Strictness is deliberate by default; opt in per type:

(new MapperBuilder())
    ->allowCastingToInteger()
    ->allowCastingToBoolean()
    ->mapper()
    ->map('int', '42'); // 42

Prefer the attribute form for a single field (#[MapAsInt], #[MapAsBool(true: ['on','yes'], false: ['off','no'])]) so the rest of the payload stays strict. Other attributes that earn their keep daily: #[MapFromKey('zipCode')] (upstream snake_case/camelCase mismatch), #[MapExplodedStringToList(separator: ',')] (CSV columns), #[MapArrayToList] (sparse/associative arrays), #[MapToDateTimeFromFormat('d/m/Y')] (legacy date formats), #[MapFromJson] (JSON embedded in a string column).

Interfaces and polymorphism. You cannot map to an interface without telling the mapper which implementation to build — use registerConstructor() with a callable or infer() with generics for Collection<T>-style types.

Custom converters. Any attribute class implementing a converter protocol can intercept a property — ideal for value objects like Money, Email, or a UserId that wraps a UUID string. This keeps parsing rules next to the type instead of in a service method.

Mapping to arguments. argumentsMapper()->mapArguments($callable, $source) resolves callable parameters from an array, which is a clean way to wire a hand-rolled dispatcher or message handler without a container binding per argument.

The reverse direction: normalizing. Use NormalizerBuilder to go back out — Format::array() for API resources, Format::json() for responses, Format::csv() for exports. #[NormalizeKeyTo('town')] renames keys for the wire format, #[IgnoreOnNormalization] drops passwords, #[NormalizeToSingleValue] flattens single-property wrappers. Mapping and normalizing the same DTO pair keeps the contract symmetric and testable.

Testing. Build fixtures by mapping plain arrays through the real mapper — your test data then goes through the same validation as production. Assert on the error tree for invalid payloads rather than asserting exceptions.

Gotchas and Tips

  • Source::json() is mandatory for JSON strings. Passing a raw JSON string to map() tries to map it as a string and fails confusingly. Same for files: use Source::file().
  • Defaults are strict. Unknown keys are rejected (enable allowSuperfluousKeys() only if you truly want to ignore extras — rejecting them catches client bugs). Scalars are not cast unless you opt in. Missing keys are errors even for nullable types; give the parameter a default (= null) if absence is legal.
  • array without a docblock is untyped. PHP erases generics, so public array $cities maps to a bare array of mixed. Always write /** @var list<City> */ — the single most common source of "why is this not validated" bugs.
  • list<T> and array<int, T> differ. list rejects gaps and non-sequential keys; if upstream may send a sparse array, use #[MapArrayToList] or array<int, T>.
  • Date formats. Strings map to DateTimeInterface only if the format is recognised. Register additional formats globally with supportDateFormats('Y-m-d', 'd/m/Y') or per-field with #[MapToDateTimeFromFormat].
  • Cache invalidation is your responsibility. The mapper caches class metadata; after a deploy that changes DTOs, stale cache entries can produce baffling errors. Use a cache namespace that changes per release, and warm the mapper on boot if cold-start latency matters.
  • Some attributes require registration. #[IgnoreOnNormalization] only takes effect if you also call configureWith(new IgnoreOnNormalization()) on the normalizer builder. Check each configurator's docs before assuming the attribute alone is enough.
  • Error handling placement. Catch MappingError at the boundary, never deep inside domain code. Log $message->toString() for a human-readable rendering; use the node path to build precise API error payloads.
  • Don't map into behavior-rich entities. Map to a DTO, then construct your domain object. Entities with side effects, lazy loading, or invariants of their own don't play well with constructor-injection mapping.
  • Enums: backed enums map from their scalar value, pure enums from the case name. Be explicit in tests — this is a silent behaviour difference when you refactor one into the other.
  • Unmapped interfaces/abstract classes produce an error at map time, not at build time. If your DTO tree contains an interface, register a constructor for it up front or the failure shows up only on the request that hits that path.
  • Profile before optimising. Mapping is fast once metadata is cached; if you see reflection cost in profiles, the cache is missing, not the library.
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.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo