Skip to content

Latest commit

 

History

History
593 lines (452 loc) · 17 KB

File metadata and controls

593 lines (452 loc) · 17 KB

Array Validation

This guide covers array validation using the ArrayValidator class, which handles indexed arrays (lists) with optional item validation.

Table of Contents

Basic Array Validation

Simple Array Validation

use Lemmon\Validator\Validator;

// Validate indexed arrays
$validator = Validator::isArray();

$result = $validator->validate([1, 2, 3, 'foo']);
// Result: [1, 2, 3, 'foo']

$result = $validator->validate(['a', 'b', 'c']);
// Result: ['a', 'b', 'c']

$result = $validator->validate([]);
// Result: []

Required vs Optional Arrays

// Optional array (allows null)
$optional = Validator::isArray();
$result = $optional->validate(null); // null

// Required array
$required = Validator::isArray()->required();
$required->validate(null); // Throws ValidationException

// Array with default value
$withDefault = Validator::isArray()->default(['default']);
$result = $withDefault->validate(null); // ['default']

Associative Arrays are Rejected

The ArrayValidator only accepts indexed arrays (lists). Associative arrays will be rejected:

$validator = Validator::isArray();

// This will throw ValidationException
$validator->validate(['key' => 'value']);

For associative arrays, use Validator::isAssociative() instead. See the Object & Schema Validation guide for complete documentation on associative array validation.

Item Validation

Validating Array Items

Use the items() method to validate each item in the array:

// Array of strings
$stringArray = Validator::isArray()->items(Validator::isString());
$result = $stringArray->validate(['foo', 'bar', 'baz']);
// Result: ['foo', 'bar', 'baz']

// Array of integers
$intArray = Validator::isArray()->items(Validator::isInt());
$result = $intArray->validate([1, 2, 3, 4]);
// Result: [1, 2, 3, 4]

// Array of emails
$emailArray = Validator::isArray()->items(
    Validator::isString()->email()
);
$result = $emailArray->validate([
    'user@example.com',
    'admin@test.org'
]);

Complex Item Validation

// Array of user objects
$userArray = Validator::isArray()->items(
    Validator::isAssociative([
        'name' => Validator::isString()->required(),
        'age' => Validator::isInt()->min(0)->max(150),
        'email' => Validator::isString()->email()
    ])
);

$users = [
    ['name' => 'John', 'age' => 30, 'email' => 'john@example.com'],
    ['name' => 'Jane', 'age' => 25, 'email' => 'jane@example.com']
];

$result = $userArray->validate($users);

Item Validation with Coercion

// Array of integers with coercion
$intArray = Validator::isArray()->items(
    Validator::isInt()->coerce()
);

$result = $intArray->validate(['1', '2', '3']);
// Result: [1, 2, 3] (strings converted to integers)

Array Filtering

Removing Empty Values with filterEmpty()

The filterEmpty() method removes empty values (empty strings and null) from arrays and automatically reindexes them to maintain the indexed array structure:

$validator = Validator::isArray()->filterEmpty();

// Remove empty values and reindex
$result = $validator->validate(['apple', '', 'banana', null, 'cherry']);
// Result: ['apple', 'banana', 'cherry'] (properly reindexed: [0, 1, 2])

// Works with mixed data types
$mixedValidator = Validator::isArray()->filterEmpty();
$result = $mixedValidator->validate([1, '', 2, null, 3, 0, false]);
// Result: [1, 2, 3, 0, false] (only empty strings and null removed)

Combining with Item Validation

// Filter empty values then validate remaining items
$emailValidator = Validator::isArray()
    ->filterEmpty()                    // Remove empty values first
    ->items(Validator::isString()->email()); // Then validate emails

$emails = ['john@example.com', '', 'jane@example.com', null];
$result = $emailValidator->validate($emails);
// Result: ['john@example.com', 'jane@example.com'] (filtered)

Real-World Use Cases

// Form data with optional fields
$tagsValidator = Validator::isArray()
    ->filterEmpty()                           // Remove empty tag inputs
    ->items(Validator::isString()->minLength(2)); // Validate remaining tags

$formTags = ['php', '', 'javascript', null, 'css', ''];
$result = $tagsValidator->validate($formTags);
// Result: ['php', 'javascript', 'css']

// CSV-like data processing
$csvRowValidator = Validator::isArray()
    ->filterEmpty()                     // Remove empty CSV cells
    ->items(Validator::isString()->pipe('trim')); // Clean remaining values

$csvRow = ['John', '', 'Doe', null, '30', ''];
$result = $csvRowValidator->validate($csvRow);
// Result: ['John', 'Doe', '30']

Type Coercion

The ArrayValidator supports several coercion strategies when coerce() is enabled:

Scalar to Array Coercion

$validator = Validator::isArray()->coerce();

// String to single-item array
$result = $validator->validate('single');
// Result: ['single']

// Number to single-item array
$result = $validator->validate(123);
// Result: [123]

// Boolean to single-item array
$result = $validator->validate(true);
// Result: [true]

// Empty string to empty array
$result = $validator->validate('');
// Result: []

Associative Array to Indexed Array

$validator = Validator::isArray()->coerce();

// Associative array gets converted to indexed array (values only)
$result = $validator->validate(['key1' => 'value1', 'key2' => 'value2']);
// Result: ['value1', 'value2']

$result = $validator->validate(['a' => 1, 'b' => 2, 'c' => 3]);
// Result: [1, 2, 3]

Coercion with Null Handling

// Without required - null passes through
$validator = Validator::isArray()->coerce();
$result = $validator->validate(null); // null

// With default - null uses default
$validator = Validator::isArray()->coerce()->default(['default']);
$result = $validator->validate(null); // ['default']

// With required - null throws error
$validator = Validator::isArray()->coerce()->required();
$validator->validate(null); // Throws ValidationException

Array Length Constraints

Non-Empty Arrays

Use notEmpty() as a clearer alternative to minItems(1) when you just need at least one item. By default it skips null; add required() if null should fail.

$validator = Validator::isArray()->notEmpty();
$validator->validate([1]); // Valid
$validator->validate([]); // Throws ValidationException

To ignore empty values first, combine it with filterEmpty():

$validator = Validator::isArray()->filterEmpty()->notEmpty();
$validator->validate(['', null]); // Throws ValidationException (becomes [])

Minimum and Maximum Items

Use minItems() and maxItems() to validate array length, similar to minLength() and maxLength() for strings:

// Minimum items constraint
$validator = Validator::isArray()->minItems(3);
$result = $validator->validate([1, 2, 3]); // Valid
$result = $validator->validate([1, 2, 3, 4]); // Valid
$validator->validate([1, 2]); // Throws ValidationException: "Value must contain at least 3 items"

// Maximum items constraint
$validator = Validator::isArray()->maxItems(5);
$result = $validator->validate([1, 2, 3]); // Valid
$result = $validator->validate([1, 2, 3, 4, 5]); // Valid
$validator->validate([1, 2, 3, 4, 5, 6]); // Throws ValidationException: "Value must contain at most 5 items"

// Combined constraints
$validator = Validator::isArray()->minItems(2)->maxItems(4);
$result = $validator->validate([1, 2]); // Valid
$result = $validator->validate([1, 2, 3, 4]); // Valid
$validator->validate([1]); // Throws ValidationException: "Value must contain at least 2 items"
$validator->validate([1, 2, 3, 4, 5]); // Throws ValidationException: "Value must contain at most 4 items"

Custom Error Messages

$validator = Validator::isArray()
    ->minItems(3, 'Array must have at least 3 elements')
    ->maxItems(10, 'Array cannot exceed 10 elements');

Array Contains Validation

The contains() method validates that an array contains a specific value or an item matching a validator:

// Contains specific value (strict comparison)
$validator = Validator::isArray()->contains('banana');
$result = $validator->validate(['apple', 'banana', 'cherry']); // Valid
$validator->validate(['apple', 'cherry']); // Throws ValidationException

// Contains value with strict type checking
$validator = Validator::isArray()->contains(0);
$result = $validator->validate([0, 1, 2]); // Valid (finds integer 0)
$validator->validate(['0', 1, 2]); // Throws ValidationException (string '0' !== integer 0)

// Contains item matching validator
$validator = Validator::isArray()->contains(Validator::isString()->email());
$result = $validator->validate(['not-email', 'test@example.com', 'also-not-email']); // Valid
$validator->validate(['not-email', 'also-not-email']); // Throws ValidationException

// Contains item matching complex validator
$validator = Validator::isArray()->contains(Validator::isInt()->positive());
$result = $validator->validate([-1, 0, 5, -2]); // Valid (contains positive integer 5)
$validator->validate([-1, 0, -2]); // Throws ValidationException (no positive integers)

Combining Contains with Item Validation

// Validate all items are strings AND array contains an email
$validator = Validator::isArray()
    ->items(Validator::isString())
    ->contains(Validator::isString()->email());

$result = $validator->validate(['hello', 'test@example.com', 'world']); // Valid
$validator->validate(['hello', 'world']); // Throws ValidationException (no email found)
$validator->validate(['hello', 123, 'test@example.com']); // Throws ValidationException (item validation fails first)

Common Patterns

Custom Array Validation

// For specific array validation, use custom validation
$validator = Validator::isArray()->satisfies(
    fn($value) => in_array($value, [[1, 2], [3, 4]], true),
    'Array must be exactly [1, 2] or [3, 4]'
);
$result = $validator->validate([1, 2]); // Valid
$validator->validate([1, 2, 3]); // Throws ValidationException

// Note: in() is not available on ArrayValidator as it doesn't make semantic sense for complex types

Empty Array Handling

// Nullify empty arrays
$validator = Validator::isArray()->nullifyEmpty();
$result = $validator->validate([]); // null
$result = $validator->validate([1, 2]); // [1, 2]

// Default for empty arrays
$validator = Validator::isArray()->default(['fallback']);
$result = $validator->validate(null); // ['fallback']

Mixed Type Arrays

// Array that can contain strings or numbers
$mixedArray = Validator::isArray()->items(
    Validator::anyOf([
        Validator::isString(),
        Validator::isInt(),
        Validator::isFloat()
    ])
);

$result = $mixedArray->validate(['hello', 42, 3.14]);
// Result: ['hello', 42, 3.14]

Nested Array Validation

// Array of arrays
$nestedArray = Validator::isArray()->items(
    Validator::isArray()->items(Validator::isString())
);

$result = $nestedArray->validate([
    ['a', 'b'],
    ['c', 'd'],
    ['e', 'f']
]);

Error Handling

Basic Error Handling

use Lemmon\Validator\ValidationException;

$validator = Validator::isArray();

try {
    $validator->validate('not an array');
} catch (ValidationException $e) {
    echo $e->getMessage(); // "Validation failed"
    print_r($e->getErrors()); // ['Value must be an array']
}

Item Validation Errors

$validator = Validator::isArray()->items(Validator::isString());

try {
    $validator->validate(['valid', 123, 'also valid']);
} catch (ValidationException $e) {
    // Errors preserve array indices to identify which item failed
    print_r($e->getErrors());
    // Output:
    // [
    //     '1' => ['Value must be a string']
    // ]

    // Flattened errors show full path with index
    $flattened = $e->getFlattenedErrors();
    // [
    //     ['path' => '1', 'message' => 'Value must be a string']
    // ]
}

Using tryValidate for Error Handling

$validator = Validator::isArray()->items(Validator::isInt()->coerce());

[$valid, $result, $errors] = $validator->tryValidate(['1', 'invalid', '3']);

if (!$valid) {
    echo "Validation failed:\n";
    print_r($errors);
    // Output:
    // [
    //     '1' => ['Value must be an integer']
    // ]

    // Flattened errors preserve array indices
    $flattened = ValidationException::flattenErrors($errors);
    // [
    //     ['path' => '1', 'message' => 'Value must be an integer']
    // ]
} else {
    echo "Valid array:\n";
    print_r($result);
}

Nested Array Item Errors

For arrays with complex item validators (like associative arrays), errors preserve the full path including array indices:

$schema = Validator::isAssociative([
    'users' => Validator::isArray()->items(Validator::isAssociative([
        'name' => Validator::isString()->required(),
        'email' => Validator::isString()->email()->required(),
    ])),
]);

$input = [
    'users' => [
        ['name' => 'John'], // Missing email
        ['name' => 'Jane', 'email' => 'invalid-email'], // Invalid email
    ],
];

try {
    $schema->validate($input);
} catch (ValidationException $e) {
    $flattened = $e->getFlattenedErrors();
    // [
    //     ['path' => 'users.0.email', 'message' => 'Value is required'],
    //     ['path' => 'users.1.email', 'message' => 'Value must be a valid email address']
    // ]
}

Cross-Item Validation

uniqueField() for Uniqueness

Use uniqueField() to validate that a nested field is unique across all array items. All members of a duplicate group receive errors with field-level paths automatically. Items where the field is null or missing are skipped.

$schema = Validator::isAssociative([
    'symlinks' => Validator::isArray()
        ->items(Validator::isAssociative([
            'source' => Validator::isString()->default('.'),
            'destination' => Validator::isString()->required(),
        ]))
        ->uniqueField('destination')
        ->required(),
]);

$input = [
    'symlinks' => [
        ['source' => 'path1', 'destination' => '/same/path'],
        ['source' => 'path2', 'destination' => '/unique/path'],
        ['source' => 'path3', 'destination' => '/same/path'], // Duplicate
    ],
];

try {
    $schema->validate($input);
} catch (ValidationException $e) {
    $flattened = $e->getFlattenedErrors();
    // [
    //     ['path' => 'symlinks.0.destination', 'message' => "Value '/same/path' is not unique (also at index 2)"],
    //     ['path' => 'symlinks.2.destination', 'message' => "Value '/same/path' is not unique (also at index 0)"],
    // ]
}

Custom message:

->uniqueField('destination', 'Destination must be unique across all symlinks')

Custom Cross-Item Validation with satisfies()

For validations beyond uniqueness (ordering, dependencies, custom logic), use satisfies() on the array validator. Structure errors as [arrayIndex => [fieldName => [errorMessage]]] to get field-level paths.

Simple Uniqueness Check (Array-Level Error)

If you don't need field-level errors, you can use a simpler approach that returns a single error at the array level:

->satisfies(
    function ($items) {
        $destinations = array_column($items, 'destination');
        return count($destinations) === count(array_unique($destinations));
    },
    'All destination values must be unique'
)

This will produce an error at symlinks rather than symlinks.2.destination.

Advanced Examples

File Upload Validation

// Validate array of uploaded files
$fileValidator = Validator::isArray()->items(
    Validator::isAssociative([
        'name' => Validator::isString()->required(),
        'size' => Validator::isInt()->min(1)->max(10485760), // Max 10MB
        'type' => Validator::isString()->in([
            'image/jpeg', 'image/png', 'image/gif'
        ])
    ])
);

Tag System Validation

// Array of unique tags
$tagValidator = Validator::isArray()->items(
    Validator::isString()
        ->pattern('/^[a-zA-Z0-9-_]+$/', 'Tags can only contain letters, numbers, hyphens, and underscores')
        ->minLength(2)
        ->maxLength(50)
);

$result = $tagValidator->validate(['php', 'validation', 'array-handling']);

Configuration Array Validation

// Validate configuration arrays
$configValidator = Validator::isArray()->items(
    Validator::anyOf([
        Validator::isString(),
        Validator::isInt(),
        Validator::isBool(),
        Validator::isArray() // Allow nested arrays
    ])
);

$config = ['debug' => true, 'timeout' => 30, 'hosts' => ['localhost', '127.0.0.1']];
$result = $configValidator->validate($config);

Next Steps