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.0+ constructor property promotion with type hints and PHPDoc annotations (e.g., @var non-empty-string).
final class User {
public function __construct(
public string $name,
public int $age,
) {}
}
Map Input to Object:
Use MapperBuilder to convert raw input (JSON, array, HTTP request) into a strongly-typed object.
$user = (new MapperBuilder())
->mapper()
->map(User::class, Source::json('{"name": "John", "age": 30}'));
Handle Errors:
Wrap mapping in a try-catch block to handle MappingError exceptions.
try {
$user = $mapper->map(User::class, Source::json('{"name": "", "age": "invalid"}'));
} catch (MappingError $error) {
// Log or return validation errors
foreach ($error->getErrors() as $error) {
echo $error->getMessage();
}
}
API Request Validation: Map HTTP request data (route, query, or body) to a DTO for validation before processing.
use CuyZ\Valinor\Mapper\Http\FromBody;
final class CreateUser {
public function __invoke(
#[FromBody] User $user,
) {
// $user is validated and typed
}
}
DTO Mapping: Use Valinor to convert untrusted input (e.g., JSON, form data) into typed objects.
$data = json_decode(file_get_contents('php://input'), true);
$dto = $mapper->map(MyDto::class, Source::array($data));
HTTP Request Handling:
Leverage HttpRequest and attributes (#[FromRoute], #[FromQuery], #[FromBody]) for framework-agnostic request binding.
$request = HttpRequest::fromPsr($psrRequest, $routeParams);
$arguments = $mapper->mapArguments(new MyController(), $request);
Nested Structures: Handle complex nested objects/arrays using PHPDoc annotations.
final class Post {
public function __construct(
public string $title,
public array $tags, // @var list<string>
) {}
}
Normalization: Convert objects back to arrays/JSON for APIs or storage.
$normalizer = (new NormalizerBuilder())->normalizer();
$array = $normalizer->normalize($object);
Laravel Service Providers:
Bind MapperBuilder and NormalizerBuilder as singletons in AppServiceProvider.
$this->app->singleton(MapperBuilder::class, fn() => new MapperBuilder());
Middleware for Validation: Create middleware to validate incoming requests using Valinor.
public function handle(Request $request, Closure $next) {
$dto = $mapper->map(MyDto::class, Source::json($request->getContent()));
$request->merge(['dto' => $dto]);
return $next($request);
}
Custom Sources:
Extend Source for domain-specific inputs (e.g., database records).
class DatabaseSource implements Source {
public function __construct(private array $data) {}
public function getContent(): array { return $this->data; }
}
Reusable Configurators: Define configurators for shared settings (e.g., date parsing, custom types).
$mapperBuilder->configureWith(new DateTimeConfigurator());
Case Sensitivity:
Valinor is case-sensitive by default. Use MapperBuilder::withKeysCaseRestriction() to enforce snake_case or camelCase.
$mapperBuilder->withKeysCaseRestriction(KeysCaseRestriction::CAMEL_CASE);
Missing Required Fields:
Omit default values for required fields. Valinor will throw MappingError if missing.
// ❌ Throws error if 'name' is missing
public function __construct(public string $name) {}
// ✅ Optional field with default
public function __construct(public ?string $name = null) {}
Type Mismatches: Ensure PHPDoc types match constructor types. Valinor uses PHPDoc for validation.
// ❌ Incorrect: PHPDoc says 'string' but constructor expects 'int'
/** @var string */
public function __construct(public int $age) {}
Circular References:
Avoid circular references in nested objects (e.g., User ↔ Post). Use #[AsArray] or lazy loading.
HTTP Request Collisions:
If a key exists in multiple sources (e.g., route and query), use attributes (#[FromRoute], #[FromQuery]) to disambiguate.
Detailed Errors:
Use $error->getErrors() to get granular validation messages.
foreach ($error->getErrors() as $error) {
echo $error->getPath() . ': ' . $error->getMessage();
}
Logging:
Enable debug mode in MapperBuilder for verbose output.
$mapperBuilder->withDebugMode(true);
Validation Rules:
Use PHPDoc to enforce custom rules (e.g., @var int<1, 100> for ranges).
Custom Types:
Register custom types via MapperBuilder::withCustomType().
$mapperBuilder->withCustomType(new UuidType());
Normalization:
Extend Normalizer to handle custom objects.
$normalizer->registerNormalizer(new MyObjectNormalizer());
Sources:
Implement Source interface for new input formats (e.g., XML, CSV).
Configurators: Create reusable configurators for shared settings (e.g., date formats, custom validators).
class MyConfigurator implements MapperBuilderConfigurator {
public function configure(MapperBuilder $builder) {
$builder->withDateTimeFormat('Y-m-d H:i:s');
}
}
MapperBuilder once and reuse it (e.g., as a singleton).MapperBuilder::withValidationMode(ValidationMode::LOOSE) for trusted inputs (e.g., internal APIs).MapperBuilder::withBatchMode(true).How can I help you explore Laravel packages today?