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(', ', $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(', ', $e->getErrors());
}Returns a tuple [bool $valid, mixed $data, array $errors]:
$validator = Validator::isString()->email();
[$valid, $data, $errors] = $validator->tryValidate('invalid-email');
if ($valid) {
echo "Valid email: " . $data;
} else {
echo "Errors: " . implode(', ', $errors);
}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.
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'); // ❌ 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 might contain:
// [
// '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