Laravel Timestamp Validator

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 не проверишь. Или можно проверить? Если можно, буду рад в комментариях, сообщениях прочитать существующие варианты.
Share post

Comments 16

    +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');
      }
    ];
    

    чем хуже?
      0
      DSL нет) или я неправильно понял)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                class Gallery
                {
                use HandlesAuthorization;

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

                Ну опять же, это всего лишь мое решение, каждый программист в работе с Laravel может выбрать свой путь, и лишь ему учиться и решать какой будет правильным.
                  0
                  Как вы добросите до формы ошибку «Вы вводите неверный email, вам разрешены только адреса из домена example.com»?

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

                    2. Я предпочитаю считать, каждому свое.
                    При таком подходе я лучше понимаю архитектуру приложения.
          0
          Вы правда не в курсе существования библиотеки Carbon?
            0
            Хуже тем, что схожу не поймешь, что происходит. А если необходимо сразу несколько правил применить?
              0
              Вы не умеете читать код на PHP?

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

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


              зато понятнее
              numeric|before_timestamp:
            0
            А почему приняли решение общаться посредством timestamp, а не даты в ISO формате?
              0
              Дело в том, что на время на сервере и клиенте (клиентах) отличается. Приложению (в нашем случае браузерному) необходимо показывать время с учетом часового пояса клиента. А, используя timestamp, это сделать проще всего.
                0
                Насколько я понимаю, клиенту нужно переводить время в 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
              Как то слишком просто для статьи на хабре…
              Да еще и расширение сервиса валидации не самое качественное. Хотя такой пример и дан в документации. Но сервисы лучше расширять отложено через $this->app->extend… ибо не на каждый запрос нужна валидация. А в случае расширения через фасад, сервис который за ним, будет порожден, даже если он не нужен.

              Only users with full accounts can post comments. Log in, please.