src/
├── Exception/
│ └── ValidationException.php
├── TypeValidatorVisitor.php
├── ValidationError.php
├── ValidationOptions.php
├── ValidationResult.php
└── Validator.php
<?php
declare(strict_types=1);
namespace Typhoon\TypeValidator;
final class ValidationOptions
{
public function __construct(
public readonly bool $coerceTypes = false,
public readonly bool $allowExtraKeys = false,
public readonly int $maxDepth = 64,
) {}
public static function default(): self
{
return new self();
}
public function withCoerceTypes(bool $coerceTypes): self
{
return new self(
coerceTypes: $coerceTypes,
allowExtraKeys: $this->allowExtraKeys,
maxDepth: $this->maxDepth
);
}
public function withAllowExtraKeys(bool $allowExtraKeys): self
{
return new self(
coerceTypes: $this->coerceTypes,
allowExtraKeys: $allowExtraKeys,
maxDepth: $this->maxDepth
);
}
public function withMaxDepth(int $maxDepth): self
{
return new self(
coerceTypes: $this->coerceTypes,
allowExtraKeys: $this->allowExtraKeys,
maxDepth: $maxDepth
);
}
}
<?php
declare(strict_types=1);
namespace Typhoon\TypeValidator;
final class ValidationError
{
public function __construct(
private readonly string $path,
private readonly mixed $value,
private readonly string $expectedType,
private readonly string $message,
) {}
public function getPath(): string
{
return $this->path;
}
public function getValue(): mixed
{
return $this->value;
}
public function getExpectedType(): string
{
return $this->expectedType;
}
public function getMessage(): string
{
return $this->message;
}
}
<?php
declare(strict_types=1);
namespace Typhoon\TypeValidator;
final class ValidationResult
{
/**
* @param ValidationError[] $errors
*/
public function __construct(
private readonly bool $valid,
private readonly array $errors = [],
) {}
public static function valid(): self
{
return new self(true);
}
/**
* @param ValidationError[] $errors
*/
public static function invalid(array $errors): self
{
return new self(false, $errors);
}
public function isValid(): bool
{
return $this->valid;
}
/**
* @return ValidationError[]
*/
public function getErrors(): array
{
return $this->errors;
}
public function merge(self $other): self
{
if ($this->valid && $other->valid) {
return self::valid();
}
return self::invalid([...$this->errors, ...$other->errors]);
}
}
<?php
declare(strict_types=1);
namespace Typhoon\TypeValidator\Exception;
use Typhoon\TypeValidator\ValidationResult;
final class ValidationException extends \RuntimeException
{
public function __construct(
private readonly ValidationResult $result,
string $message = 'Validation failed',
int $code = 0,
?\Throwable $previous = null,
) {
$errorMessages = array_map(
fn($error) => $error->getMessage(),
$result->getErrors()
);
$detailedMessage = $message . ":\n• " . implode("\n• ", $errorMessages);
parent::__construct($detailedMessage, $code, $previous);
}
public function getValidationResult(): ValidationResult
{
return $this->result;
}
}
<?php
declare(strict_types=1);
namespace Typhoon\TypeValidator;
use Typhoon\Type\Type;
use Typhoon\TypeValidator\Exception\ValidationException;
final class Validator
{
private ?ValidationResult $lastResult = null;
private TypeValidatorVisitor $visitor;
public function __construct(
private readonly Type $type,
private readonly ?ValidationOptions $options = null,
) {
$this->visitor = new TypeValidatorVisitor($this->options ?? ValidationOptions::default());
}
public function isValid(mixed $value): bool
{
return $this->validate($value)->isValid();
}
public function validate(mixed $value): ValidationResult
{
$this->lastResult = $this->visitor->validate($this->type, $value);
return $this->lastResult;
}
/**
* @return ValidationError[]
*/
public function getErrors(): array
{
return $this->lastResult?->getErrors() ?? [];
}
/**
* @throws ValidationException
*/
public function validateOrThrow(mixed $value): void
{
$result = $this->validate($value);
if (!$result->isValid()) {
throw new ValidationException($result);
}
}
}
This is the core of the validation system. It implements the TypeVisitor interface and contains methods for validating each type of type definition.
<?php
declare(strict_types=1);
namespace Typhoon\TypeValidator;
use Typhoon\Type\Type;
use Typhoon\Type\TypeVisitor;
use Typhoon\Type\Visitor\DefaultTypeVisitor;
use Typhoon\Type\types;
/**
* @implements TypeVisitor<ValidationResult>
*/
final class TypeValidatorVisitor implements TypeVisitor
{
private mixed $value;
private string $path;
private array $errors = [];
private int $currentDepth = 0;
public function __construct(
private readonly ValidationOptions $options,
) {}
public function validate(Type $type, mixed $value, string $path = 'value'): ValidationResult
{
$this->value = $value;
$this->path = $path;
$this->errors = [];
$this->currentDepth = 0;
$result = $type->accept($this);
return $result->isValid()
? ValidationResult::valid()
: ValidationResult::invalid($this->errors);
}
// Implement all the TypeVisitor methods...
// This would be a very large class with methods for each type
public function never(Type $type): mixed
{
$this->addError($type, 'Value can never be valid for never type');
return ValidationResult::invalid($this->errors);
}
public function void(Type $type): mixed
{
$this->addError($type, 'void type can only be used as a return type');
return ValidationResult::invalid($this->errors);
}
public function null(Type $type): mixed
{
if ($this->value === null) {
return ValidationResult::valid();
}
if ($this->options->coerceTypes && $this->value === '') {
$this->value = null;
return ValidationResult::valid();
}
$this->addError($type, 'Value must be null');
return ValidationResult::invalid($this->errors);
}
// And so on for all the other types...
private function addError(Type $expectedType, string $message): void
{
$this->errors[] = new ValidationError(
$this->path,
$this->value,
\Typhoon\Type\stringify($expectedType),
sprintf(
"Value at path '%s' %s, %s given.",
$this->path,
$message,
$this->getTypeName($this->value)
)
);
}
private function getTypeName(mixed $value): string
{
return match (true) {
$value === null => 'null',
is_array($value) => 'array',
is_object($value) => get_class($value),
default => gettype($value),
};
}
}
-
The
TypeValidatorVisitor
would need to implement all the methods from theTypeVisitor
interface, which would make it quite large. Each method would validate a specific type of type definition. -
For complex validation (like object shapes, array shapes, etc.), the visitor would need to recurse into the structure, updating the path as it goes.
-
We'd need to handle the
maxDepth
option to prevent infinite recursion for recursive types. -
Type coercion (when
coerceTypes
is true) would need special handling for each type. -
For union types, we'd need to try validating against each type in the union until one succeeds.
-
For intersection types, we'd need to validate against all types in the intersection.
-
The visitor should maintain path information to provide clear error messages about where validation failed.
-
We might need additional helper methods for specific validations, like validating callables, checking array shapes, etc.
The most complex part of this implementation would be the TypeValidatorVisitor
class, which would need to handle all the different types in the type system correctly. The other classes are relatively straightforward.