Skip to content

Custom Rules

Valicomb makes it easy to add custom validation rules - either as one-time closures or as reusable global rules.

Closure-based rules are perfect for one-off validations that are specific to a single form or use case. They’re defined inline and don’t need to be registered globally.

Add a validation rule inline using a closure:

use Frostybee\Valicomb\Validator;
$v = new Validator(['username' => 'admin']);
$v->rule(function($field, $value, $params, $fields) {
return $value !== 'admin';
}, 'username')->message('Username cannot be "admin"');
function(string $field, mixed $value, array $params, array $fields): bool
ParameterDescription
$fieldThe field name being validated
$valueThe current value of the field
$paramsAdditional parameters passed to the rule
$fieldsAll field data (for cross-field validation)
Returntrue if valid, false if invalid
$v->rule(function($field, $value, $params, $fields) {
$min = $params[0] ?? 0;
$max = $params[1] ?? 100;
return $value >= $min && $value <= $max;
}, 'score', 0, 100)->message('Score must be between %d and %d');

Use $fields to access other field values:

$v->rule(function($field, $value, $params, $fields) {
// Password cannot be the same as username
return $value !== ($fields['username'] ?? null);
}, 'password')->message('Password cannot be the same as username');

When you have validation logic that you’ll reuse across multiple forms or throughout your application, register it as a global rule. Global rules are available to all Validator instances and help keep your code DRY.

Register a rule that can be used across all Validator instances:

use Frostybee\Valicomb\Validator;
// Register globally
Validator::addRule('strongPassword', function($field, $value, $params) {
return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/', $value);
}, 'Password must be at least 8 characters with uppercase, lowercase, and number');
$v = new Validator($_POST);
$v->rule('strongPassword', 'password');
// Register rule that accepts parameters
Validator::addRule('between', function($field, $value, $params) {
$min = $params[0];
$max = $params[1];
return $value >= $min && $value <= $max;
}, '{field} must be between %d and %d');
// Use with parameters
$v->rule('between', 'age', 18, 65);

Instance rules sit between closures and global rules. They’re reusable within a single Validator instance but don’t pollute the global namespace. This is useful when validation logic depends on runtime context or when you want to avoid global state.

Register a rule only for a specific Validator instance:

$v = new Validator($data);
$v->addInstanceRule('customCheck', function($field, $value) {
return $value === 'expected';
}, 'Value must be "expected"');
$v->rule('customCheck', 'field_name');
// This rule is only available to this $v instance
  • Validation logic specific to one form
  • Rules that depend on instance state
  • Temporary or context-specific validation

Error messages in custom rules can include dynamic placeholders that get replaced with actual values at runtime. This makes your error messages more informative and user-friendly.

Custom rule error messages support placeholders like {field}, {value}, %s, and %d. For complete documentation, see Error Messages - Message Placeholders.

Validator::addRule('divisibleBy', function($field, $value, $params) {
return $value % $params[0] === 0;
}, '{field} must be divisible by %d');
$v->rule('divisibleBy', 'quantity', 5);
// Error: "Quantity must be divisible by 5"

These examples demonstrate common validation patterns you might encounter in production applications. Each example shows how to encapsulate complex business rules into reusable validators.

Validator::addRule('validUsername', function($field, $value, $params) {
// 3-20 chars, alphanumeric and underscores only, must start with letter
return preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/', $value);
}, '{field} must be 3-20 characters, start with a letter, and contain only letters, numbers, and underscores');
Validator::addRule('noProfanity', function($field, $value, $params) {
$badWords = ['badword1', 'badword2']; // Your list
$valueLower = strtolower($value);
foreach ($badWords as $word) {
if (str_contains($valueLower, $word)) {
return false;
}
}
return true;
}, '{field} contains inappropriate language');
Validator::addRule('uniqueEmail', function($field, $value, $params, $fields) {
// Example with PDO (inject your database connection)
global $pdo;
$stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email = ?');
$stmt->execute([$value]);
return $stmt->fetchColumn() === 0;
}, 'This email address is already registered');
Validator::addRule('confirmed', function($field, $value, $params, $fields) {
$confirmField = $field . '_confirmation';
return isset($fields[$confirmField]) && $value === $fields[$confirmField];
}, '{field} confirmation does not match');
Validator::addRule('securePassword', function($field, $value, $params) {
$minLength = $params[0] ?? 8;
if (strlen($value) < $minLength) return false;
if (!preg_match('/[a-z]/', $value)) return false; // lowercase
if (!preg_match('/[A-Z]/', $value)) return false; // uppercase
if (!preg_match('/[0-9]/', $value)) return false; // digit
if (!preg_match('/[^a-zA-Z0-9]/', $value)) return false; // special char
return true;
}, '{field} must be at least %d characters with uppercase, lowercase, number, and special character');
// Usage
$v->rule('securePassword', 'password', 12);

Sometimes a single error message isn’t enough. When a validation rule can fail for multiple reasons, you’ll want to tell users exactly what went wrong. Dynamic error messages let your custom rules return context-specific feedback based on why the validation failed.

Instead of returning just a boolean, return an array with the result and a custom message:

// Simple boolean (traditional)
return true; // passed
return false; // failed (uses default message)
// Array with custom message (new)
return [true]; // passed
return [false, 'Custom message']; // failed with custom message
$v->addInstanceRule('strongPassword', function($field, $value, $params) {
if (strlen($value) < 8) {
return [false, 'Password must be at least 8 characters'];
}
if (!preg_match('/[A-Z]/', $value)) {
return [false, 'Password must contain an uppercase letter'];
}
if (!preg_match('/[0-9]/', $value)) {
return [false, 'Password must contain a number'];
}
return [true];
});
$v->rule('strongPassword', 'password');

Each failure reason produces a specific, helpful error message.

Validator::addRule('validCoupon', function($field, $value, $params) {
$coupons = [
'SAVE10' => ['active' => true, 'min_order' => 50],
'EXPIRED' => ['active' => false],
];
if (!isset($coupons[$value])) {
return [false, '{field} "{value}" is not a valid coupon code'];
}
$coupon = $coupons[$value];
if (!$coupon['active']) {
return [false, '{field} "{value}" has expired'];
}
return [true];
});

Dynamic messages support all standard placeholders:

$v->addInstanceRule('inStock', function($field, $value, $params) {
$stock = getStockLevel($value); // hypothetical function
if ($stock === 0) {
return [false, '{field} "{value}" is out of stock'];
}
return [true];
});