Помимо озабоченности вопросами быстродействия для 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
В заключение — небольшой опрос для потомков.
zoryamba
Это не сработает. Ключи массива должны быть уникальны.
gotz Автор
Действительно, спасибо за комментарий!