AttributeValidatorFactory.php
PHP
Path: src/Validator/AttributeValidatorFactory.php
<?php
namespace mini\Validator;
use ReflectionClass;
use ReflectionProperty;
use mini\Validator\Attributes;
/**
* Builds validators from PHP class attributes
*
* Scans class properties for validation attributes and constructs
* a Validator instance that validates the entire object structure.
*
* Supports purpose-scoped validation (like Jakarta/Symfony groups):
* - forClass(User::class) returns core validator (attributes without purpose)
* - forClass(User::class, Purpose::Create) returns purpose-specific validator
*/
class AttributeValidatorFactory
{
/**
* Build a validator from a class using reflection
*
* @param class-string $className Class to build validator for
* @param Purpose|string|null $purpose Filter attributes by purpose (null = core validation)
* @return Validator Object validator with property validators
*/
public function forClass(string $className, Purpose|string|null $purpose = null): Validator
{
$reflection = new ReflectionClass($className);
$validator = (new Validator())->type('object');
$properties = [];
// First, process Field attributes on the class itself
foreach ($reflection->getAttributes(Attributes\Field::class) as $attribute) {
$field = $attribute->newInstance();
// Filter by purpose
if (!$this->purposeMatches($field->purpose, $purpose)) {
continue;
}
$fieldValidator = $this->buildFieldValidator($field);
if ($fieldValidator !== null) {
$properties[$field->name] = $fieldValidator;
}
}
// Then, process actual properties
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
$propertyValidator = $this->buildPropertyValidator($property, $purpose);
if ($propertyValidator !== null) {
$properties[$property->getName()] = $propertyValidator;
}
}
// Add property validators
if (!empty($properties)) {
$validator = $validator->properties($properties);
}
// Note: Required properties are marked via the Required attribute
// on the property validators themselves. The jsonSerialize() method
// derives the JSON Schema 'required' array from $validator->isRequired.
return $validator;
}
/**
* Check if an attribute's purpose matches the requested purpose
*
* @param Purpose|string|null $attrPurpose The attribute's purpose
* @param Purpose|string|null $requestedPurpose The requested purpose filter
* @return bool True if the attribute should be included
*/
private function purposeMatches(Purpose|string|null $attrPurpose, Purpose|string|null $requestedPurpose): bool
{
// Normalize to string values
$attrValue = $attrPurpose instanceof Purpose ? $attrPurpose->value : $attrPurpose;
$requestedValue = $requestedPurpose instanceof Purpose ? $requestedPurpose->value : $requestedPurpose;
// Core validator ($requestedPurpose === null) only includes attributes without purpose
// Purpose validator includes only attributes with matching purpose
return $attrValue === $requestedValue;
}
/**
* Build a validator from a Field attribute
*
* @param Attributes\Field $field Field attribute instance
* @return Validator|null Field validator
*/
private function buildFieldValidator(Attributes\Field $field): ?Validator
{
$validator = new Validator();
// Apply all validation rules from the Field attribute
if ($field->type !== null) {
$validator = $validator->type($field->type, $field->message);
}
if ($field->minLength !== null) {
$validator = $validator->minLength($field->minLength, $field->message);
}
if ($field->maxLength !== null) {
$validator = $validator->maxLength($field->maxLength, $field->message);
}
if ($field->minimum !== null) {
$validator = $validator->minimum($field->minimum, $field->message);
}
if ($field->maximum !== null) {
$validator = $validator->maximum($field->maximum, $field->message);
}
if ($field->exclusiveMinimum !== null) {
$validator = $validator->exclusiveMinimum($field->exclusiveMinimum, $field->message);
}
if ($field->exclusiveMaximum !== null) {
$validator = $validator->exclusiveMaximum($field->exclusiveMaximum, $field->message);
}
if ($field->multipleOf !== null) {
$validator = $validator->multipleOf($field->multipleOf, $field->message);
}
if ($field->pattern !== null) {
$validator = $validator->pattern($field->pattern, $field->message);
}
if ($field->format !== null) {
$validator = $validator->format($field->format, $field->message);
}
if ($field->minItems !== null) {
$validator = $validator->minItems($field->minItems, $field->message);
}
if ($field->maxItems !== null) {
$validator = $validator->maxItems($field->maxItems, $field->message);
}
if ($field->uniqueItems === true) {
$validator = $validator->uniqueItems();
}
if ($field->minProperties !== null) {
$validator = $validator->minProperties($field->minProperties, $field->message);
}
if ($field->maxProperties !== null) {
$validator = $validator->maxProperties($field->maxProperties, $field->message);
}
if ($field->required === true) {
$validator = $validator->required($field->message);
}
if ($field->const !== null) {
$validator = $validator->const($field->const, $field->message);
}
if ($field->enum !== null) {
$validator = $validator->enum($field->enum, $field->message);
}
return $validator;
}
/**
* Build a validator for a single property from its attributes
*
* @param ReflectionProperty $property Property to build validator for
* @param Purpose|string|null $purpose Filter attributes by purpose
* @return Validator|null Property validator, or null if no validation attributes
*/
private function buildPropertyValidator(ReflectionProperty $property, Purpose|string|null $purpose): ?Validator
{
$attributes = $property->getAttributes();
if (empty($attributes)) {
return null;
}
$validator = new Validator();
$hasValidation = false;
foreach ($attributes as $attribute) {
// Skip non-validator attributes (e.g., Tables attributes)
if (!str_starts_with($attribute->getName(), 'mini\\Validator\\Attributes\\')) {
continue;
}
$instance = $attribute->newInstance();
// Filter by purpose
$attrPurpose = $instance->purpose ?? null;
if (!$this->purposeMatches($attrPurpose, $purpose)) {
continue;
}
$hasValidation = true;
$validator = $this->applyAttribute($validator, $instance);
}
return $hasValidation ? $validator : null;
}
/**
* Apply a validation attribute to a validator
*
* @param Validator $validator Base validator
* @param object $attribute Attribute instance
* @return Validator Validator with attribute applied
*/
private function applyAttribute(Validator $validator, object $attribute): Validator
{
return match(get_class($attribute)) {
Attributes\Type::class => $validator->type($attribute->type, $attribute->message),
Attributes\MinLength::class => $validator->minLength($attribute->min, $attribute->message),
Attributes\MaxLength::class => $validator->maxLength($attribute->max, $attribute->message),
Attributes\Minimum::class => $validator->minimum($attribute->min, $attribute->message),
Attributes\Maximum::class => $validator->maximum($attribute->max, $attribute->message),
Attributes\ExclusiveMinimum::class => $validator->exclusiveMinimum($attribute->min, $attribute->message),
Attributes\ExclusiveMaximum::class => $validator->exclusiveMaximum($attribute->max, $attribute->message),
Attributes\MultipleOf::class => $validator->multipleOf($attribute->divisor, $attribute->message),
Attributes\Pattern::class => $validator->pattern($attribute->pattern, $attribute->message),
Attributes\Format::class => $validator->format($attribute->format, $attribute->message),
Attributes\MinItems::class => $validator->minItems($attribute->min, $attribute->message),
Attributes\MaxItems::class => $validator->maxItems($attribute->max, $attribute->message),
Attributes\UniqueItems::class => $validator->uniqueItems(),
Attributes\MinProperties::class => $validator->minProperties($attribute->min, $attribute->message),
Attributes\MaxProperties::class => $validator->maxProperties($attribute->max, $attribute->message),
Attributes\Required::class => $validator->required($attribute->message),
Attributes\Const::class => $validator->const($attribute->value, $attribute->message),
Attributes\Enum::class => $validator->enum($attribute->values, $attribute->message),
default => $validator
};
}
}