ValidationError.php
PHP
Path: src/Validator/ValidationError.php
<?php
namespace mini\Validator;
use Stringable;
use ArrayAccess;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
use ArrayIterator;
/**
* Represents validation errors for both scalar values and complex objects
*
* ValidationError provides a unified interface for validation results:
* - Stringable: Cast to string for simple error display
* - ArrayAccess: Access property errors via $error['fieldName']
* - IteratorAggregate: Iterate over property errors with foreach
* - JsonSerializable: Export for API responses
*
* ## Usage
*
* ```php
* $error = $validator->isInvalid($value);
* if ($error) {
* // Scalar value - just echo it
* echo $error; // "Must be at least 3 characters."
*
* // Object value - access property errors
* echo $error['username']; // "Username is required."
* echo $error['email']; // "Invalid email format."
*
* // Iterate all property errors
* foreach ($error as $field => $fieldError) {
* echo "$field: $fieldError\n";
* }
*
* // Nested objects
* echo $error['address']['city']; // drills down
*
* // JSON for API responses
* echo json_encode($error);
* }
* ```
*/
class ValidationError implements Stringable, ArrayAccess, IteratorAggregate, JsonSerializable
{
/**
* @param string|Stringable $message Error message for this level
* @param array<string, ValidationError> $propertyErrors Nested property errors
*/
public function __construct(
private string|Stringable $message,
private array $propertyErrors = []
) {}
/**
* Get the error message
*/
public function getMessage(): string|Stringable
{
return $this->message;
}
/**
* Check if this error has property-level errors
*/
public function hasPropertyErrors(): bool
{
return !empty($this->propertyErrors);
}
/**
* Get all property errors
*
* @return array<string, ValidationError>
*/
public function getPropertyErrors(): array
{
return $this->propertyErrors;
}
// ========================================================================
// Stringable
// ========================================================================
public function __toString(): string
{
return (string) $this->message;
}
// ========================================================================
// ArrayAccess
// ========================================================================
public function offsetExists(mixed $offset): bool
{
return isset($this->propertyErrors[$offset]);
}
public function offsetGet(mixed $offset): ?ValidationError
{
return $this->propertyErrors[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
throw new \BadMethodCallException('ValidationError is immutable');
}
public function offsetUnset(mixed $offset): void
{
throw new \BadMethodCallException('ValidationError is immutable');
}
// ========================================================================
// IteratorAggregate
// ========================================================================
public function getIterator(): Traversable
{
return new ArrayIterator($this->propertyErrors);
}
// ========================================================================
// JsonSerializable
// ========================================================================
public function jsonSerialize(): mixed
{
if (empty($this->propertyErrors)) {
return (string) $this->message;
}
// For objects with property errors, serialize as object
return array_map(
fn(ValidationError $error) => $error->jsonSerialize(),
$this->propertyErrors
);
}
}