Laravel 5.1, Laravel 5.2, Lara… Код прогрессирует, оптимизируется и развивается. В новой (5.2) версии появился валидатор массивов, например, но что делать, если необходимо провалидировать входящий timestamp? Правильно, писать
Жил был и живёт один проект на Laravel 5.1. Точнее, живёт его API сторона. Есть необходимость «гонять» туда-сюда различные даты. Но как их гонять, если существуют часовые пояса? Принято решение установить сервер в UTC+0 и общаться с помощью timestamp, который на фронтенде легко преобразуется в нужное время. Окей, вопросов по этому не возникло. Кроме одного — как валидировать входящие данные? Создадим собственный валидатор.
Полный код валидатора в самом конце статьи.
Поехали!
В папке app/Extensions/Validators создаём файл и именуем TimestampValidator.php.
namespace Lame\Extensions\Validators;
use Illuminate\Validation\Validator;
class TimestampValidator extends Validator{
}
Нам необходимо принимать, чтобы входящая дата подходила под «до» и «после».
Рассмотрим первый пример. У нас есть дата рождения пользователя. Пользователь должен быть старше 10 лет, т.е. рождён до 2016 года. Соответственно, нам необходимо принимать дату, которая будет до 2016 года. В правилах валидации указываем:
/** Берем текущую дату, отнимаем 10 лет, прибавляем один день и получаем timestamp от необходимой даты */
$date = Carbon::now()->subYears(10)->addDay(1)->timestamp;
/** Указываем, что входящая дата в формате timestamp должна быть до нужной даты в timestamp */
$rules = [
"bDay" => "numeric|before_timestamp:".$date,
];
Появилось правило «before_timestamp». Возвращаемся в наш валидатор и создаём метод, который будет осуществлять нужную проверку. Название метода должно иметь следующую структуру: «validate<правило в camelCase формате>». $value среди входящих параметров — значение, которое поступило из вне. $parameters — массив параметров, которые указали в правилах (before_timestamp:".$date).
public function validateBeforeTimestamp($attribute, $value, $parameters)
{
$value = (int)$value;
if ((int)$parameters[0] <= 0) {
throw new \Exception("Timestamp parameter in the beforeTimestamp validator not valid!");
}
if ($value != "" && $value >= $parameters[0]) {
return false;
}
return true;
}
Второй пример. Нам необходимо создать задачу с дедлайном. Минимальный дедлайн — 4 часа. Создаём правила:
$date = Carbon::now()->addHours(4)->timestamp;
$rules = [
"deadline" => "required|numeric|after_timestamp:".$date
];
Появилось новое правило — «after_timestamp». Обработаем его в нашем валидаторе:
public function validateAfterTimestamp($attribute, $value, $parameters)
{
$value = (int)$value;
if ((int)$parameters[0] <= 0) {
throw new \Exception("Timestamp parameter in the beforeTimestamp validator not valid!");
}
if ($value != "" && $value <= $parameters[0]) {
return false;
}
return true;
}
Чтобы подключить наш валидатор, я создал свой ServiceProvider в папке app/Providers — CustomValidateServiceProvider.php.
<?php
namespace Lame\Providers;
use Illuminate\Support\ServiceProvider;
use Lame\Extensions\Validators\TimestampValidator;
use Validator;
class CustomValidateServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Validator::resolver(function ($translator, $data, $rules, $messages) {
return new TimestampValidator($translator, $data, $rules, $messages);
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}
На этом, в принципе всё. Сообщения об ошибках указываются в файле validation.php.
"custom" => [
"deadline" => [
"after_timestamp" => "Deadline should be minimum 4 hours"
],
"bDay" => [
"before_timestamp" => "Age should be minimum 10 years",
"numeric" => "Birthday date should be in timestamp"
]
]
<?php
namespace Lame\Extensions\Validators;
use Illuminate\Validation\Validator;
class TimestampValidator extends Validator
{
#region timestamp valitators - after_timestamp:{timestamp} | before_timestamp:{timestamp}
/**
* @param $attribute
* @param $value
* @param $parameters = ["date" => "Date before which should be input timestamp"]
* @return bool
* @throws \Exception
*/
public function validateBeforeTimestamp($attribute, $value, $parameters)
{
$value = (int)$value;
if ((int)$parameters[0] <= 0) {
throw new \Exception("Timestamp parameter in the beforeTimestamp validator not valid!");
}
if ($value != "" && $value >= $parameters[0]) {
return false;
}
return true;
}
/**
* @param $attribute
* @param $value
* @param $parameters = ["date" => "Date before which should be input timestamp"]
* @return bool
* @throws \Exception
*/
public function validateAfterTimestamp($attribute, $value, $parameters)
{
$value = (int)$value;
if ((int)$parameters[0] <= 0) {
throw new \Exception("Timestamp parameter in the beforeTimestamp validator not valid!");
}
if ($value != "" && $value <= $parameters[0]) {
return false;
}
return true;
}
#endregion
}
С помощью date, after, before timestamp не проверишь. Или можно проверить? Если можно, буду рад в комментариях, сообщениях прочитать существующие варианты.
Комментарии (16)
Samorai
18.04.2016 16:42А почему приняли решение общаться посредством timestamp, а не даты в ISO формате?
alutskevich
18.04.2016 16:58Дело в том, что на время на сервере и клиенте (клиентах) отличается. Приложению (в нашем случае браузерному) необходимо показывать время с учетом часового пояса клиента. А, используя timestamp, это сделать проще всего.
Samorai
18.04.2016 17:33Насколько я понимаю, клиенту нужно переводить время в ISO формат, и добавлять часовой пояс.
Но как вариант вы можете принимать заголовок от клиента, например Accept-Timezone и отдавать время в нужном формате и в нужной тайм-зоне.
С этим прекрасно справляется сам php, например так:
public function getDateWithTimeZone($time, $timezone = 'UTC') { return (new \DateTime($time, new \DateTimeZone(date_default_timezone_get()))) ->setTimezone(new \DateTimeZone($timezone)) ->format('Y-m-d H:i:s'); }
Тогда клиенту не нужно будет задумываться о переводе времени и часовых поясах. И Laravel будет спокойно валидировать даты, без изобретения велосипеда.
ellrion
18.04.2016 19:01+1Как то слишком просто для статьи на хабре…
Да еще и расширение сервиса валидации не самое качественное. Хотя такой пример и дан в документации. Но сервисы лучше расширять отложено через $this->app->extend… ибо не на каждый запрос нужна валидация. А в случае расширения через фасад, сервис который за ним, будет порожден, даже если он не нужен.
AlexLeonov
Имхо это — плохо. Это чудовищно плохо.
Это просто какая-то эпидемия среди разработчиков фреймворков, придумывать собственные DSL. Кривые, косые, и совершенно ненужные.
чем хуже?
alutskevich
DSL нет) или я неправильно понял)
эта цепочка — методы Carbon класса, не мною придуманы, а мною использованы. А массив $rules оформлен согласно документации Laravel.
Вариант, предложенный Вами, отнюдь не хуже)… Но о такой возможности я, увы, не знал)…
ellrion
Именно, это свой DSL и это прекрасно. Он короче и лучше читаем. Учить вам придется много чего если вы хотите писать на фреймворке.
И ваш пример очень рафинированный. Добавьте туда еще пару тройку правил на поле. И в вашем примере как мне задать сообщение об ошибке? А мультиязычное сообщение об ошибке? А Лара имеет конвенцию привязанную к имени правила валидации.
AlexLeonov
Это отвратительно, повторю.
Вместо яркого, лаконичного и понятного кода на PHP мне зачем-то предлагается учить еще один язык, который заведомо хуже.
К лямбде, например, я в любой момент могу привязать с помощью замыкания контекст. Как это сделать со строкой «numeric|before_timestamp:»? И да, что вообще в этой строке значит "|", логическое ИЛИ? Или последовательно выполнение валидации? И как об этом догадаться?
Рано или поздно осознание ненужности левых DSL придёт к авторам фреймворков.
ellrion
Это прекрасно, повторю.)
Видите мы зашли в тупик. Так что давайте не будем кидаться такими ничего незначащими кроме нашего отношения фразами и попробуем выделить суть.
Если я правильно понимаю, вы упираете на то, что минус DSL в том что его нужно учить. Претензия к пайпу туда же. И Собственно вы правы, нужно. Но для этого есть документация.
Озвученный минус про контекст я отвергаю, так как он просто не нужен для правила валидации.
> Вместо яркого, лаконичного и понятного кода на PHP
Вот не надо. Даже в какой нибудь небольшой форме есть не одно поле, на которое не одно правило валидации. И ваш код на чистом php не будет лаконичным. И будет он дублироваться темболее если форм много. И придется вам выносить эти функции куда то. и т.д.
И вы так и не сказали как мне в это дело поместить сообщение.
И в итоге сравнивая плюсы и минусы я очень рад что в Ларе сделали DSL именно на правила валидации.
AlexLeonov
А давайте без «давайте». Я давно уже вырос из возраста, когда мог повестись на такой приемчик )))
Вы неправильно понимаете. Минус собственных, свежепридуманных DSL в том, что они не нужны. Эта зараза идет со времен первого Yii, где были чудовищные array-style валидаторы и eval в дата-гридах и постепенно доползает до современных фреймворков.
Ой. Надо же. «Отвергаю» :)
А я вот не отвергаю ничего. Позволить себе не могу отвергать. Потому что в реальном мире почти не бывает правил валидации без контекста.
Прикиньте, например, такой кейс: админу позволительно ввести любой email в форму создания нового юзера, а аккаунт-менеджеру партнера — только email, принадлежащий его организации.
Буду счастлив увидеть это правило валидации описанным в виде «numeric|before» и что там у вас еще дальше…
Собственно на этом можно было бы закончить дискуссию. Форм на сервере, разумеется, нет.
Но я выше задал вопрос, так что, пожалуй, дождусь вашего ответа.
MikielD
Я позволю себе дать ответ на ваш вопрос.
Прикиньте, например, такой кейс: админу позволительно ввести любой email в форму создания нового юзера, а аккаунт-менеджеру партнера — только email, принадлежащий его организации.
Для таких случаев мы не будем использовать валидацию формы. Ведь валидация формы должна отвечать лишь за проверку данных.
Мы будем проверять уровень доступа у пользователя (то ли админ, то ли аккаунт-менеджер).
Для этого мы используем Policies.
Пример:
class Gallery
{
use HandlesAuthorization;
public function add(User $user, Form $form)
{
#Тут мы уже проверим в зависимости от пользователя, может ли он добавить любой email либо же только определенные
# В зависимости от его статуса (админ или аккаунт-менеджер) я вызову определенную валидацию в которой задам что email может быть любой, а в другой валидации задам что email должен соответствовать патерну (если например задача добавлять email только с @mydomain.com) либо чтобы он существовал в таблице users.
}
}
Ну опять же, это всего лишь мое решение, каждый программист в работе с Laravel может выбрать свой путь, и лишь ему учиться и решать какой будет правильным.
AlexLeonov
Как вы добросите до формы ошибку «Вы вводите неверный email, вам разрешены только адреса из домена example.com»?
И да, вы подтвердили мою мысль — в таких случаях свои выдуманные DSL только вредят. Не так ли?
MikielD
1. Сделаю редирект назад на форму с информацией о ошибке.
пример: return redirect()->back()->with('error','Вы вводите неверный email, вам разрешены только адреса из домена example.com');
2. Я предпочитаю считать, каждому свое.
При таком подходе я лучше понимаю архитектуру приложения.
Fantyk
Вы правда не в курсе существования библиотеки Carbon?
franzose
Хуже тем, что схожу не поймешь, что происходит. А если необходимо сразу несколько правил применить?
AlexLeonov
Вы не умеете читать код на PHP?
Я не могу иначе объяснить то, что вам непонятно
зато понятнее