Validation in PHP. Beauty or noodles?

Choosing the best PHP validator out of ten popular ones, I faced a dilemma. What is more important to me? Adherence to all SOLID / OOP canons or usability and clarity of the code? What will users of the Comet framework prefer ? If you think that the question is far from simple - welcome under the cut on a long journey through code snippets :)





In addition to concerns about performance for REST API and microservices, I am very worried about the readability of the code that we type every day to solve work problems, including data validation.



I want to show pieces of code from my own benchmarks so that you can appreciate the breadth of approaches to solving the same problem. Let's imagine that the following data set has arrived to us:



$form = [
    'name'           => 'Elon Mask', 
    'name_wrong'     => 'Mask',
    'login'          => 'mask', 
    'login_wrong'    => 'm@sk', 
    'email'          => 'elon@tesla.com', 
    'email_wrong'    => 'elon@tesla_com', 
    'password'       => '1q!~|w2o<z', 
    'password_wrong' => '123456',
    'date'           => '2020-06-05 15:52:00',
    'date_wrong'     => '2020:06:05 15-52-00',
    'ipv4'           => '192.168.1.1',
    'ipv4_wrong'     => '402.28.6.12',
    'uuid'           => '70fcf623-6c4e-453b-826d-072c4862d133',
    'uuid_wrong'     => 'abcd-xyz-6c4e-453b-826d-072c4862d133',
    'extra'          => 'that field out of scope of validation',
    'empty'          => ''
];


Our goal is to run this array through a set of validation rules, receiving a list of all fields with errors and standard messages for demonstration to the user.



Industry standard and icon of pure OOP - Symfony of course





use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Translation\MessageSelector;

$validator = Validation::createValidator();

$constraint = new Assert\Collection([    
    'name' => new Assert\Regex('/^[A-Za-z]+\s[A-Za-z]+$/u'),   	
    'login' => new Assert\Regex('/^[a-zA-Z0-9]-_+$/'),
    'email' => new Assert\Email(),
    'password' => [
        new Assert\NotBlank(),
        new Assert\Length(['max' => 64]),
        new Assert\Type(['type' => 'string'])
    ],
    'agreed' => new Assert\Type(['type' => 'boolean'])
]);

$violations = $validator->validate($form, $constraint);

$errors = [];
if (0 !== count($violations)) {
    foreach ($violations as $violation) {
        $errors[] = $violation->getPropertyPath() . ' : ' . $violation->getMessage();
    }
} 

return $errors;


Razor-eyed code in pure PHP



$errors = [];

if (!preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $form['name']))
    $errors['name'] = 'should consist of two words!';
if (!preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $form['name_wrong']))
    $errors['name_wrong'] = 'should consist of two words!';
if (!preg_match('/^[a-zA-Z0-9-_]+$/', $form['login']))
    $errors['login'] = 'should contain only alphanumeric!';
if (!preg_match('/^[a-zA-Z0-9]-_+$/', $form['login_wrong']))
    $errors['login_wrong'] = 'should contain only alphanumeric!';

if (filter_var($form['email'], FILTER_VALIDATE_EMAIL) != $form['email'])
    $errors['email'] = 'provide correct email!';
if (filter_var($form['email_wrong'], FILTER_VALIDATE_EMAIL) != $form['email_wrong'])
    $errors['email_wrong'] = 'provide correct email!';

if (!is_string($form['password']) ||
    $form['password'] == '' ||
    strlen($form['password']) < 8 ||
    strlen($form['password']) > 64 
)
    $errors['password'] = 'provide correct password!';

if (!is_string($form['password_wrong']) ||
    $form['password_wrong'] == '' ||
    strlen($form['password_wrong']) < 8 ||
    strlen($form['password_wrong']) > 64 
)
    $errors['password_wrong'] = 'provide correct password!';

if (!preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $form['date']))
    $errors['date'] = 'provide correct date!';
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $form['date_wrong']))
    $errors['date_wrong'] = 'provide correct date!';

if (filter_var($form['ipv4'], FILTER_VALIDATE_IP) != $form['ipv4'])
    $errors['ipv4'] = 'provide correct ip4!';
if (filter_var($form['ipv4_wrong'], FILTER_VALIDATE_IP) != $form['ipv4_wrong'])
    $errors['ipv4_wrong'] = 'provide correct ip4!';

if (!preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $form['uuid']))
    $errors['uuid'] = 'provide correct uuid!';
if (!preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $form['uuid_wrong']))
    $errors['uuid_wrong'] = 'provide correct uuid!';

if (!isset($form['agreed']) || !is_bool($form['agreed']) || $form['agreed'] != true)
    $errors['agreed'] = 'you should agree with terms!';

return $errors;


Solution based on one of the most popular Respect Validation libraries



use Respect\Validation\Validator as v;
use Respect\Validation\Factory;

Factory::setDefaultInstance(
    (new Factory())
        ->withRuleNamespace('Validation')
        ->withExceptionNamespace('Validation')
);

$messages = [];

try {
    v::attribute('name', v::RespectRule())
        ->attribute('name_wrong', v::RespectRule())
        ->attribute('login', v::alnum('-_'))
        ->attribute('login_wrong', v::alnum('-_'))
        ->attribute('email', v::email())
        ->attribute('email_wrong', v::email())
        ->attribute('password', v::notEmpty()->stringType()->length(null, 64))
        ->attribute('password_wrong', v::notEmpty()->stringType()->length(null, 64))
        ->attribute('date', v::date())
        ->attribute('date_wrong', v::date())
        ->attribute('ipv4', v::ipv4())
        ->attribute('ipv4_wrong', v::ipv4())
        ->attribute('uuid', v::uuid())
        ->attribute('uuid_wrong', v::uuid())
        ->attribute('agreed', v::trueVal())
        ->assert((object) $form);
} catch (\Exception $ex) {
    $messages = $ex->getMessages();
}

return $messages;


Also Known Name: Valitron



use Valitron\Validator;

Validator::addRule('uuid', function($field, $value) {
    return (bool) preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value);
}, 'UUID should confirm RFC style!');

$rules = [
    'required'  => [ 'login', 'agreed' ],
    'regex'     => [ ['name', '/^[A-Za-z]+\s[A-Za-z]+$/'] ],
    'lengthMin' => [ [ 'password', '8'], [ 'password_wrong', '8'] ],
    'lengthMax' => [ [ 'password', '64'], [ 'password_wrong', '64'] ],
    'slug'      => [ 'login', 'login_wrong' ],
    'email'     => [ 'email', 'email_wrong' ],
    'date'      => [ 'date', 'date_wrong' ],
    'ipv4'      => [ 'ipv4', 'ipv4_wrong' ],
    'uuid'      => [ 'uuid', 'uuid_wrong' ],
    'accepted'  => 'agreed'
];

$validator = new Validator($form);
$validator->rules($rules);
$validator->rule('accepted', 'agreed')->message('You should set {field} value!');
$validator->validate();

return $validator->errors());


Lovely Sirius




$validator = new \Sirius\Validation\Validator;

$validator
    ->add('name', 'required | \Validation\SiriusRule')
    ->add('login', 'required | alphanumhyphen', null, 'Only latin chars, underscores and dashes please.')
    ->add('email', 'required | email', null, 'Give correct email please.')
    ->add('password', 'required | maxlength(64)', null, 'Wrong password.')
    ->add('agreed', 'required | equal(true)', null, 'Where is your agreement?');

$validator->validate($form);

$errors = [];
foreach ($validator->getMessages() as $attribute => $messages) {
    foreach ($messages as $message) {
        $errors[] = $attribute . ' : '. $message->getTemplate();
    }
}

return $errors;


And this is how they validate in Laravel



use Illuminate\Validation\Factory as ValidatorFactory;
use Illuminate\Translation\Translator;
use Illuminate\Translation\ArrayLoader;
use Symfony\Component\Translation\MessageSelector;
use Illuminate\Support\Facades\Validator as FacadeValidator;

$rules = array(
    'name' => ['regex:/^[A-Za-z]+\s[A-Za-z]+$/u'],
    'name_wrong' => ['regex:/^[A-Za-z]+\s[A-Za-z]+$/u'],
    'login' => ['required', 'alpha_num'],
    'login_wrong' => ['required', 'alpha_num'],
    'email' => ['email'],
    'email_wrong' => ['email'],
    'password' => ['required', 'min:8', 'max:64'],
    'password_wrong' => ['required', 'min:8', 'max:64'],
    'date' => ['date'],
    'date_wrong' => ['date'],
    'ipv4' => ['ipv4'],
    'ipv4_wrong' => ['ipv4'],
    'uuid' => ['uuid'],
    'uuid_wrong' => ['uuid'],
    'agreed' => ['required', 'boolean']
);

$messages = [
    'name_wrong.regex' => 'Username is required.',
    'password_wrong.required' => 'Password is required.',
    'password_wrong.max' => 'Password must be no more than :max characters.',
    'email_wrong.email' => 'Email is required.',
    'login_wrong.required' => 'Login is required.',
    'login_wrong.alpha_num' => 'Login must consist of alfa numeric chars.',
    'agreed.required' => 'Confirm radio box required.',
);

$loader = new ArrayLoader();
$translator = new Translator($loader, 'en');
$validatorFactory = new ValidatorFactory($translator);

$validator = $validatorFactory->make($form, $rules, $messages);

return $validator->messages();


Rakit Validation Unexpected Diamond



$validator = new \Rakit\Validation\Validator;
$validator->addValidator('uuid', new \Validation\RakitRule);

$validation = $validator->make($form, [
    'name'           => 'regex:/^[A-Za-z]+\s[A-Za-z]+$/u',
    'name_wrong'     => 'regex:/^[A-Za-z]+\s[A-Za-z]+$/u',
    'email'          => 'email',
    'email_wrong'    => 'email',
    'password'       => 'required|min:8|max:64',
    'password_wrong' => 'required|min:8|max:64',
    'login'          => 'alpha_dash',
    'login_wrong'    => 'alpha_dash',
    'date'           => 'date:Y-m-d H:i:s',
    'date_wrong'     => 'date:Y-m-d H:i:s',
    'ipv4'           => 'ipv4',
    'ipv4_wrong'     => 'ipv4',
    'uuid'           => 'uuid',
    'uuid_wrong'     => 'uuid',
    'agreed'         => 'required|accepted'
]); 	

$validation->setMessages([
    'uuid'     => 'UUID should confirm RFC rules!',
    'required' => ':attribute is required!',
    // etc
]);

$validation->validate();

return $validation->errors()->toArray();


so what? Which code example is the most descriptive, idiomatic, correct, and generally β€œcorrect”? My personal choice is at the docks on Comet: github.com/gotzmann/comet Finally



, a little poll for posterity.



All Articles