This guide covers the fundamental concepts and usage patterns of the Lemmon Validator.
All validation starts with the Validator factory class, which provides static methods to create specific validator instances:
use Lemmon\Validator\Validator;
// Create validators for different types
$stringValidator = Validator::isString();
$intValidator = Validator::isInt();
$floatValidator = Validator::isFloat();
$arrayValidator = Validator::isArray();
$objectValidator = Validator::isObject();
$associativeValidator = Validator::isAssociative();
$boolValidator = Validator::isBool();Every validator follows the same pattern:
- Create a validator
- Configure it with rules (optional)
- Validate the data
// 1. Create and configure
$validator = Validator::isString()
->required()
->minLength(3)
->email();
// 2. Validate (throws exception on failure)
try {
$result = $validator->validate('user@example.com');
echo "Valid email: " . $result;
} catch (ValidationException $e) {
echo "Validation failed: " . implode(', ', array_map(fn($err) => $err->getMessage(), $e->getErrors()));
}Throws a ValidationException if validation fails:
$validator = Validator::isInt()->min(1)->max(100);
try {
$result = $validator->validate(50); // Returns: 50
echo "Valid: " . $result;
} catch (ValidationException $e) {
echo "Invalid: " . implode(', ', array_map(fn($err) => $err->getMessage(), $e->getErrors()));
}Returns a tuple [bool $valid, mixed $data, array $errors], where $errors is a flat list of
ValidationError objects (empty on success):
$validator = Validator::isString()->email();
[$valid, $data, $errors] = $validator->tryValidate('invalid-email');
if ($valid) {
echo "Valid email: " . $data;
} else {
echo "Errors: " . implode(', ', array_map(fn($e) => $e->getMessage(), $errors));
// Each error also exposes getPath(), getSegments(), getCode(), and getParams()
}All validators inherit these methods from FieldValidator:
// Optional by default (accepts null)
$optional = Validator::isString();
[$valid, $data, $errors] = $optional->tryValidate(null); // $valid = true, $data = null
// Required (rejects null)
$required = Validator::isString()->required();
[$valid, $data, $errors] = $required->tryValidate(null); // $valid = false$withDefault = Validator::isString()->default('Hello World');
[$valid, $data, $errors] = $withDefault->tryValidate(null);
// $valid = true, $data = 'Hello World'// Enable automatic type conversion
$coercing = Validator::isInt()->coerce();
$result = $coercing->validate('123'); // Returns: 123 (integer)
$result = $coercing->validate(''); // Returns: null (form-safe!)Form Safety Note: Empty strings convert to
null(not0/0.0/false) to prevent dangerous defaults in form handling. See Core Concepts - Form-Safe Empty String Handling for details.
Integer coercion accepts integer-form strings but does not truncate decimal strings. isFloat()
accepts floats and integers (widening integers to float, as PHP's strict_types mode does) but
rejects numeric strings unless coerce() is enabled. Container validators likewise preserve the
boundaries between lists, associative arrays, and stdClass.
For explicit control over empty string handling, use nullifyEmpty():
// Convert empty strings to null
$nullifying = Validator::isString()->nullifyEmpty();
$result = $nullifying->validate(''); // Returns: null
$result = $nullifying->validate('hello'); // Returns: 'hello'
// Combined with defaults for form-safe optional fields
$optional = Validator::isString()
->nullifyEmpty() // Empty strings → null
->default('Not provided'); // Use default for null
$result = $optional->validate(''); // Returns: 'Not provided'
$result = $optional->validate('John'); // Returns: 'John'
// Form-safe numeric validation
$safeQuantity = Validator::isInt()
->coerce()
->nullifyEmpty() // Empty strings → null (not dangerous 0)
->required('Quantity is required')
->min(1, 'Quantity must be at least 1');
$result = $safeQuantity->validate(''); // ❌ "Quantity is required" (safe!)
$result = $safeQuantity->validate('5'); // Returns: 5When to use nullifyEmpty():
- Form validation where empty fields should be
null - Optional fields with meaningful defaults
- Database schemas preferring
NULLover empty strings - API endpoints normalizing empty strings to
null
Critical: Pipeline steps execute in the exact order written. This matters for pipe(), transform(), and nullifyEmpty(). Both required() and default() are flags — their position does not change execution order. default() fills in null after the pipeline as a last resort; required() enforces presence at the very end.
// Order matters for transformations
$trimThenNullify = Validator::isString()
->pipe('trim') // 1. Remove whitespace
->nullifyEmpty(); // 2. Empty strings → null
$trimThenNullify->validate(' '); // Returns: null
$nullifyThenTrim = Validator::isString()
->nullifyEmpty() // 1. Empty strings → null (only if already empty)
->pipe('trim'); // 2. Trim whitespace
$nullifyThenTrim->validate(' '); // Returns: '' (empty string)Real-world form validation pattern:
// Common pattern: clean input → handle empty → validate requirements
$formValidator = Validator::isAssociative([
'name' => Validator::isString()
->pipe('trim') // Clean whitespace
->nullifyEmpty() // Handle empty fields
->required('Name is required'), // Enforce requirements
'email' => Validator::isString()
->pipe('trim', 'strtolower') // Clean and normalize
->nullifyEmpty() // Handle empty fields
->email('Invalid email format') // Validate format
->required('Email is required'), // Enforce requirements
'age' => Validator::isInt()
->coerce() // Convert strings to int
->nullifyEmpty() // Handle empty fields (form-safe)
->min(18, 'Must be 18 or older'), // Optional field with constraints
]);// Multiple allowed values
$restricted = Validator::isString()->in(['red', 'green', 'blue']);
$result = $restricted->validate('red'); // Valid
$result = $restricted->validate('yellow'); // ❌ ValidationException
// Single allowed value (use const() instead of in([x]))
$exact = Validator::isString()->const('active');
$result = $exact->validate('active'); // Valid
$result = $exact->validate('pending'); // ❌ ValidationException
// PHP BackedEnum validation (StatusEnum: string with cases Active='active', Pending='pending')
$status = Validator::isString()->enum(StatusEnum::class);
$result = $status->validate('active'); // Valid
$result = $status->validate('unknown'); // ❌ ValidationException
// PHP UnitEnum (non-backed): match by case name string, or use a mixed validator for enum instances
$color = Validator::isString()->enum(ColorEnum::class);
$result = $color->validate('Red'); // Valid (case name)
$result = $color->validate('Magenta'); // ❌ ValidationExceptionFor complex data structures, use schema validation:
$userSchema = Validator::isAssociative([
'name' => Validator::isString()->required(),
'age' => Validator::isInt()->min(0)->max(150),
'email' => Validator::isString()->email()
]);
$userData = [
'name' => 'John Doe',
'age' => 30,
'email' => 'john@example.com'
];
$validUser = $userSchema->validate($userData);$configSchema = Validator::isObject([
'debug' => Validator::isBool()->default(false),
'timeout' => Validator::isInt()->min(1)->default(30)
]);
$config = new stdClass();
$config->debug = true;
$config->timeout = 60;
$validConfig = $configSchema->validate($config);$numbersValidator = Validator::isArray()->items(
Validator::isInt()->min(0) // Each item must be a non-negative integer
);
$numbers = [1, 2, 3, 4, 5];
$validNumbers = $numbersValidator->validate($numbers);Validation fails fast per field. Schema validation still collects errors across fields:
$validator = Validator::isString()
->required()
->minLength(5)
->email();
[$valid, $data, $errors] = $validator->tryValidate('ab');
// $errors is a flat list of ValidationError objects:
// [
// ValidationError(path: '', code: 'STRING_TOO_SHORT', message: 'Value must be at least 5 characters long'),
// ]All validator methods return the validator instance, enabling fluent chaining:
$complexValidator = Validator::isString()
->required()
->minLength(8)
->maxLength(100)
->pattern('/^[A-Za-z0-9]+$/')
->satisfies(
fn($value) => !in_array(strtolower($value), ['password', '123456']),
'Password cannot be a common weak password'
);Transform and process data after successful validation using the type-aware transformation system.
// Type-preserving transformations with pipe()
$name = Validator::isString()
->pipe('trim', 'strtoupper') // Multiple string operations
->validate(' john doe '); // Returns: "JOHN DOE"
// Type-changing transformations with transform()
$count = Validator::isString()
->transform(fn($v) => explode(',', $v)) // String → Array
->transform('count') // Array → Int
->validate('a,b,c'); // Returns: 3- Use
pipe()for same-type operations (string → string, array → array) - Use
transform()for type changes (string → array, array → int) - Null handling -
pipe()skips null values to avoid type errors,transform()skips null by default. Usetransform($fn, skipNull: false)to process null values.
// Correct usage
$result = Validator::isArray()
->pipe('array_unique', 'array_reverse') // Array operations (same type)
->transform(fn($v) => implode(',', $v)) // Array → String (type change)
->pipe('trim', 'strtoupper') // String operations (same type)
->validate(['a', 'b', 'a']); // Returns: "A,B"
// Null handling: default behavior (skips null)
$date = Validator::isString()
->transform(fn($v) => DateTime::createFromFormat('Y-m-d', $v))
->validate(null); // Returns: null (transform skipped, safe)
// Null handling: explicit processing when needed
$withFallback = Validator::isString()
->transform(fn($v) => $v ?? 'Not provided', skipNull: false)
->validate(null); // Returns: 'Not provided' (transform executed)- Core Concepts — Understand the architecture
- String Validation Guide — Detailed string validation
- Numeric Validation Guide — Integer and float validation
- Custom Validation Guide — Create your own validation rules