cuyz/valinor
Valinor maps raw inputs (JSON/arrays) into validated, strongly typed PHP objects. Supports advanced PHPStan/Psalm types (shaped arrays, generics, ranges), produces precise human-readable errors, and can normalize data back to formats like JSON or CSV.
Installation:
composer require cuyz/valinor
Define a DTO/Entity:
Use PHP 8+ features like readonly properties and type hints (including shaped arrays, generics, and ranges).
final readonly class User {
public function __construct(
public string $name,
public array $roles,
) {}
}
Map Input to Object:
$mapper = (new MapperBuilder())->mapper();
$user = $mapper->map(User::class, Source::json('{"name": "John", "roles": ["admin"]}'));
Handle Errors:
try {
$user = $mapper->map(User::class, Source::json('{"name": 123}'));
} catch (MappingError $error) {
// Handle validation errors
}
API Request Handling: Map HTTP requests (route/query/body) to controller arguments using attributes:
final class CreateUser {
public function __invoke(
#[FromBody] string $name,
#[FromQuery] array $roles = [],
) {}
}
DTO Mapping:
MapperBuilder to create a mapper instance.HTTP Request Mapping:
#[FromRoute], #[FromQuery], #[FromBody] to bind parameters.ServerRequestInterface to HttpRequest:
$request = HttpRequest::fromPsr($psrRequest, $routeParams);
$args = (new MapperBuilder())->argumentsMapper()->mapArguments($controller, $request);
Normalization:
$normalizer = (new NormalizerBuilder())->normalizer();
$data = $normalizer->normalize($user);
Reusable Configuration:
$mapper = (new MapperBuilder())
->configureWith(new MyConfigurator())
->mapper();
public function handle(Request $request, Closure $next) {
$data = $request->all();
$mapper = (new MapperBuilder())->mapper();
$mapper->map(User::class, Source::array($data));
return $next($request);
}
MapperBuilder to the container for dependency injection.MapperBuilder to validate test inputs:
$this->expectException(MappingError::class);
$mapper->map(User::class, Source::json('{"name": ""}'));
Key Mismatches:
#[KeyName] to override property names:
final class User {
#[KeyName('user_name')] public string $name;
}
caseSensitiveKeys in MapperBuilder if needed.Nested Validation:
HTTP Request Collisions:
CollisionError. Use attributes to disambiguate.Default Values:
int $page = 1). Runtime values (e.g., time()) are unsupported.cities[0].timeZone).MapperBuilder for verbose output:
$mapper = (new MapperBuilder())->debug()->mapper();
Custom Types:
Mapper\Type\Type for unsupported types (e.g., UUIDs):
final class UuidType implements Type {
public function map(string $value): Uuid { ... }
}
MapperBuilder:
$mapper = (new MapperBuilder())->withType(UuidType::class)->mapper();
Normalization:
Normalizer\Normalizer to customize output (e.g., exclude properties):
$normalizer = (new NormalizerBuilder())
->withNormalizer(new MyNormalizer())
->normalizer();
Configurators:
final class DateConfigurator implements MapperBuilderConfigurator {
public function configure(MapperBuilder $builder): void {
$builder->withType(DateTimeType::class);
}
}
MapperBuilder once (e.g., in a service provider) and reuse the mapper.MapperBuilder::withCache()).#[Assert] sparingly; prefer type hints for performance.ValidatesRequests for hybrid validation:
public function rules() {
return ['name' => 'required|string'];
}
Illuminate\Http\Request to DTOs:
$mapper->map(User::class, Source::array($request->all()));
$normalizer->normalize($user)->toJson();
How can I help you explore Laravel packages today?