Skip to content

Latest commit

 

History

History
346 lines (256 loc) · 10.4 KB

File metadata and controls

346 lines (256 loc) · 10.4 KB

Basic Usage

This guide covers the fundamental concepts and usage patterns of the Lemmon Validator.

The Validator Factory

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();

Basic Validation Pattern

Every validator follows the same pattern:

  1. Create a validator
  2. Configure it with rules (optional)
  3. 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());
}

Two Validation Methods

validate() - Exception-based

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());
}

tryValidate() - Tuple-based

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);
}

Common Validator Methods

All validators inherit these methods from FieldValidator:

Required vs Optional

// 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

Default Values

$withDefault = Validator::isString()->default('Hello World');

[$valid, $data, $errors] = $withDefault->tryValidate(null);
// $valid = true, $data = 'Hello World'

Type Coercion

// 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 (not 0/0.0/false) to prevent dangerous defaults in form handling. See Core Concepts - Form-Safe Empty String Handling for details.

Empty String Nullification

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: 5

When to use nullifyEmpty():

  • Form validation where empty fields should be null
  • Optional fields with meaningful defaults
  • Database schemas preferring NULL over empty strings
  • API endpoints normalizing empty strings to null

Execution Order Matters

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
]);

Allowed Values

// 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

Schema Validation

For complex data structures, use schema validation:

Associative Arrays

$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);

Objects (stdClass)

$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);

Plain Arrays

$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);

Error Handling

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'
// ]

Method Chaining

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'
    );

Data Transformations

Transform and process data after successful validation using the type-aware transformation system.

Basic Transformations

// 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

When to Use Each Method

  • 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. Use transform($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)

Next Steps