image

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)


  1. AlexLeonov
    18.04.2016 15:52
    +1

    /** Берем текущую дату, отнимаем 10 лет, прибавляем один день и получаем timestamp от необходимой даты */
    $date = Carbon::now()->subYears(10)->addDay(1)->timestamp;
    
    /** Указываем, что входящая дата в формате timestamp должна быть до нужной даты в timestamp */
    $rules = [
      "bDay" => "numeric|before_timestamp:".$date,
    ];
    


    Имхо это — плохо. Это чудовищно плохо.
    1. Неочевидно, потому что написано не на PHP а на каком-то собственном DSL
    2. Мне этот DSL для чего-то требуется учить
    3. В результате фреймворк начинает красть моё время, вместо помощи в его экономии


    Это просто какая-то эпидемия среди разработчиков фреймворков, придумывать собственные DSL. Кривые, косые, и совершенно ненужные.

    $rules = [
      'bDay' => function ($value) {
        return !empty($value) && $value <= strtotime('-10 years');
      }
    ];
    

    чем хуже?


    1. alutskevich
      18.04.2016 16:46

      DSL нет) или я неправильно понял)

      $date = Carbon::now()->subYears(10)->addDay(1)->timestamp;
      

      эта цепочка — методы Carbon класса, не мною придуманы, а мною использованы. А массив $rules оформлен согласно документации Laravel.

      Вариант, предложенный Вами, отнюдь не хуже)… Но о такой возможности я, увы, не знал)…


    1. ellrion
      18.04.2016 17:48

      Именно, это свой DSL и это прекрасно. Он короче и лучше читаем. Учить вам придется много чего если вы хотите писать на фреймворке.
      И ваш пример очень рафинированный. Добавьте туда еще пару тройку правил на поле. И в вашем примере как мне задать сообщение об ошибке? А мультиязычное сообщение об ошибке? А Лара имеет конвенцию привязанную к имени правила валидации.


      1. AlexLeonov
        18.04.2016 18:08

        Это отвратительно, повторю.
        Вместо яркого, лаконичного и понятного кода на PHP мне зачем-то предлагается учить еще один язык, который заведомо хуже.

        К лямбде, например, я в любой момент могу привязать с помощью замыкания контекст. Как это сделать со строкой «numeric|before_timestamp:»? И да, что вообще в этой строке значит "|", логическое ИЛИ? Или последовательно выполнение валидации? И как об этом догадаться?

        Рано или поздно осознание ненужности левых DSL придёт к авторам фреймворков.


        1. ellrion
          18.04.2016 18:28
          +1

          Это прекрасно, повторю.)
          Видите мы зашли в тупик. Так что давайте не будем кидаться такими ничего незначащими кроме нашего отношения фразами и попробуем выделить суть.
          Если я правильно понимаю, вы упираете на то, что минус DSL в том что его нужно учить. Претензия к пайпу туда же. И Собственно вы правы, нужно. Но для этого есть документация.
          Озвученный минус про контекст я отвергаю, так как он просто не нужен для правила валидации.
          > Вместо яркого, лаконичного и понятного кода на PHP
          Вот не надо. Даже в какой нибудь небольшой форме есть не одно поле, на которое не одно правило валидации. И ваш код на чистом php не будет лаконичным. И будет он дублироваться темболее если форм много. И придется вам выносить эти функции куда то. и т.д.
          И вы так и не сказали как мне в это дело поместить сообщение.
          И в итоге сравнивая плюсы и минусы я очень рад что в Ларе сделали DSL именно на правила валидации.


          1. AlexLeonov
            18.04.2016 22:30
            -2

            давайте не будем кидаться такими ничего незначащими кроме нашего отношения фразами

            А давайте без «давайте». Я давно уже вырос из возраста, когда мог повестись на такой приемчик )))

            Если я правильно понимаю, вы упираете на то, что минус DSL в том что его нужно учить.

            Вы неправильно понимаете. Минус собственных, свежепридуманных DSL в том, что они не нужны. Эта зараза идет со времен первого Yii, где были чудовищные array-style валидаторы и eval в дата-гридах и постепенно доползает до современных фреймворков.

            Озвученный минус про контекст я отвергаю, так как он просто не нужен для правила валидации.

            Ой. Надо же. «Отвергаю» :)
            А я вот не отвергаю ничего. Позволить себе не могу отвергать. Потому что в реальном мире почти не бывает правил валидации без контекста.
            Прикиньте, например, такой кейс: админу позволительно ввести любой email в форму создания нового юзера, а аккаунт-менеджеру партнера — только email, принадлежащий его организации.
            Буду счастлив увидеть это правило валидации описанным в виде «numeric|before» и что там у вас еще дальше…

            Даже в какой нибудь небольшой форме есть не одно поле, на которое не одно правило валидации.

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


            1. MikielD
              19.04.2016 12:05

              Я позволю себе дать ответ на ваш вопрос.

              Прикиньте, например, такой кейс: админу позволительно ввести любой email в форму создания нового юзера, а аккаунт-менеджеру партнера — только email, принадлежащий его организации.

              Для таких случаев мы не будем использовать валидацию формы. Ведь валидация формы должна отвечать лишь за проверку данных.
              Мы будем проверять уровень доступа у пользователя (то ли админ, то ли аккаунт-менеджер).
              Для этого мы используем Policies.
              Пример:

              class Gallery
              {
              use HandlesAuthorization;

              public function add(User $user, Form $form)
              {
              #Тут мы уже проверим в зависимости от пользователя, может ли он добавить любой email либо же только определенные
              # В зависимости от его статуса (админ или аккаунт-менеджер) я вызову определенную валидацию в которой задам что email может быть любой, а в другой валидации задам что email должен соответствовать патерну (если например задача добавлять email только с @mydomain.com) либо чтобы он существовал в таблице users.
              }
              }

              Ну опять же, это всего лишь мое решение, каждый программист в работе с Laravel может выбрать свой путь, и лишь ему учиться и решать какой будет правильным.


              1. AlexLeonov
                19.04.2016 12:22

                Как вы добросите до формы ошибку «Вы вводите неверный email, вам разрешены только адреса из домена example.com»?

                И да, вы подтвердили мою мысль — в таких случаях свои выдуманные DSL только вредят. Не так ли?


                1. MikielD
                  20.04.2016 12:45

                  1. Сделаю редирект назад на форму с информацией о ошибке.
                  пример: return redirect()->back()->with('error','Вы вводите неверный email, вам разрешены только адреса из домена example.com');

                  2. Я предпочитаю считать, каждому свое.
                  При таком подходе я лучше понимаю архитектуру приложения.


    1. Fantyk
      18.04.2016 22:43

      Вы правда не в курсе существования библиотеки Carbon?


    1. franzose
      26.04.2016 13:08

      Хуже тем, что схожу не поймешь, что происходит. А если необходимо сразу несколько правил применить?


      1. AlexLeonov
        26.04.2016 15:13

        Вы не умеете читать код на PHP?

        Я не могу иначе объяснить то, что вам непонятно

        !empty($value) && $value <= strtotime('-10 years')
        


        зато понятнее
        numeric|before_timestamp:


  1. Samorai
    18.04.2016 16:42

    А почему приняли решение общаться посредством timestamp, а не даты в ISO формате?


    1. alutskevich
      18.04.2016 16:58

      Дело в том, что на время на сервере и клиенте (клиентах) отличается. Приложению (в нашем случае браузерному) необходимо показывать время с учетом часового пояса клиента. А, используя timestamp, это сделать проще всего.


      1. 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 будет спокойно валидировать даты, без изобретения велосипеда.


  1. ellrion
    18.04.2016 19:01
    +1

    Как то слишком просто для статьи на хабре…
    Да еще и расширение сервиса валидации не самое качественное. Хотя такой пример и дан в документации. Но сервисы лучше расширять отложено через $this->app->extend… ибо не на каждый запрос нужна валидация. А в случае расширения через фасад, сервис который за ним, будет порожден, даже если он не нужен.