Валидация в PHP. Красота или лапша?

от автора

Выбирая лучший PHP-валидатор из десятка популярных, я столкнулся с дилеммой. Что для меня важнее? Следование всем SOLID / ООП-канонам или удобство работы и наглядность кода? Что предпочтут пользователи фреймворка Comet? Если вы считаете, что вопрос далеко не прост — добро пожаловать под кат в длинное путешествие по фрагментам кода 🙂


Помимо озабоченности вопросами быстродействия для REST API и микросервисов, я очень переживаю за читаемость кода, который мы ежедневно набиваем для решения рабочих задач, в том числе — валидации данных.

Хочу показать куски кода из собственных бенчмарков, чтобы вы смогли оценить широту подходов к решению одной и той же проблемы. Представим, что к нам прилетел следующий набор данных:

$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'          => '' ]; 

Наша цель — прогнать этот массив через набор правил валидации, получив на выходе список всех полей с ошибками и стандартные сообщения для демонстрации пользователю.

Отраслевой стандарт и икона чистого ООП — конечно же Symfony

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; 

Вырвиглазный код на чистом 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; 

Решение на базе одной из самых популярных библитек Respect Validation

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; 

Еще одно известное имя: 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()); 

Прекрасный 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; 

А вот так валидируют в 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

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

Ну так что? Какой из примеров кода наиболее наглядный, идиоматичный, корректный и вообще «правильный»? Мой личный выбор — в доках на Comet: github.com/gotzmann/comet

В заключение — небольшой опрос для потомков.

ссылка на оригинал статьи https://habr.com/ru/post/521292/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *