Write Your Laravel Validation Logic Like a Senior Dev, Part 3

Write Your Laravel Validation Logic Like a Senior Dev, Part 3

Extending the Rule Object with a DateTime Trait

Play this article

In one of my projects, I needed some DateTime-related rules for Laravel, such as preventing the creation of an item after a certain time. Laravel does come with some DateTime validation rules out-of-the-box, but sometimes you want more control than what you can get. In this article, I'll discuss how I used a custom DateTime Trait in my Rules object to create and extend a powerful custom rule. Below, I'll show you how to create an extended Rule object that prevents an event from being added after the date has passed. I know part 3 to Laravel Validation was long overdue, so let's get down to business, shall we?

Prerequisites:

  • Please read part 1 and part 2 of this series. Setup information can be found in those articles.
  • It will be great to know what traits are, and how to make them. If you don't know already, I explain it in detail here.
  • Our app will be headless (no frontend), so Postman will be needed.

First Steps:

  • We'll need to make a DateTime Trait that will have all our methods concerning DateTime. Please follow the steps in the article mentioned earlier if you don't know how to create one. Name your Trait DateTimeTrait. We'll be importing the Carbon library to help us handle DateTime functions.
  • Let's make a method that will get the current time in an Eastern timezone. You can use whatever timezone you wish, just make sure it's already supported by PHP. After that, we'll create a function that can convert any time that is passed to our trait to Eastern time.

<?php

namespace App\Traits;

use Carbon\Carbon;

trait DateTimeTrait {
    public function getCurrentTime(){
        $current = Carbon::now('America/New_York');
        return $current;
    }

    public function convertTimetoEastern($date, $tz){
        $newdate = Carbon::createFromFormat('Y-m-d H:i:s', $date, $tz)
                            ->setTimezone('America/New_York');
        return $newdate; 
    }
}
  • For this article, the most important DateTime thing we'll need to do is to tell if the time of the event has passed the current time or not. We can do this by making a method that determines the difference between the current time and the event time.
    • In the below code, you'll see Carbon::parse(). This method is another way of instantiating Carbon and passing a time string to the object so that it can be manipulated.
    • With the new Carbon-readable date, we can use the diffInSeconds() method to get the difference in seconds. This method accepts a boolean as an optional second parameter to say whether we want an absolute value or not. Because we want our value to be positive or negative, we set it to false.
    public function getDateDifferenceFromNow($event_date, $timezone){
        $date = Carbon::parse(
            $this->convertTimetoEastern($event_date, $timezone)
        );
        $difference = $date->diffInSeconds($this->getCurrentTime(), false);
        return $difference;
    }

The Work Begins:

  • The first thing we'll need to do is create a separate Form Request class that will send JSON responses to our API. Please refer to the first part of this series to learn how to do that. This Form Request will be called EventRequest and it will handle our validation.
<?php

namespace App\Http\Requests;

use App\Http\Requests\APIFormRequest;

class EventRequest extends APIFormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required',
            'start_time' => 'required',
            'end_time' => 'date_format:Y-m-d H:i:s|after:start_time',
            'venue' => 'required',
            'price' => 'required',
            'timezone' => 'required|timezone'
        ];
    }
}
  • We'll make a custom rule called TimeTooLate by running php artisan make:rule TimeTooLate. We'll then import our newly-made DateTimeTrait.
    • Our new Rule class will have a constructor, a passes() method, and a message() method.
    • The passes() method takes in two parameters, $attribute and $value. The $attribute describes the name of the field that we'll be working with, which in this case, is start_time. The $value describes the value that is attached to that field. The $value param will be used in the passes() method to determine whether the rule passes or not.
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\Traits\DateTimeTrait;

class TimeTooLate implements Rule
{
    use DateTimeTrait;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct(string $timezone)
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

The Work Continues:

  • We'll also need to pass the timezone to the TimeTooLate rule, so we'll utilize our constructor. We can strictly type our parameter so that it only accepts a string.
public $tz;

public function __construct(string $timezone)
{
    $this->tz = $timezone;
}
  • In our passes() method, we need to make sure that it returns false so that message() can return an error message. With the getDateDifferenceFromNow() method in our DateTimeTrait, a positive integer is returned if the event date has surpassed the current date and time.
public function passes($attribute, $value)
{
    $difference = $this->getDateDifferenceFromNow($value, $this->tz);
    return $difference >= 0 ? false : true;
}
  • Once this returns false, the message() method will fire. You can put anything here. Also, usually, you could access the name of the $attribute and bind it to your returned string by using :attribute. We won't use it here, but you're free to use it if you wish.
public function message()
{
    return 'It\'s too late to add this event now';
}

The Work Ends:

  • We'll now need to implement our custom rule into our Request. First, import the TimeTooLate rule. To use it in our rules, we'll change the value for start_time into an array and include TimeTooLate as an element. We'll instantiate TimeTooLate, and pass in the timezone.
<?php

namespace App\Http\Requests;

use App\Http\Requests\APIFormRequest;
use App\Rules\TimeTooLate;

class ShowRequest extends ApiFormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required',
            'start_time' => ['required', 
                            'date_format:Y-m-d H:i:s', 
                            new TimeTooLate(request('timezone'))],
            'end_time' => 'date_format:Y-m-d H:i:s|after:start_time',
            'venue' => 'required',
            'price' => 'required',
            'timezone' => 'required|timezone'
        ];
    }
  • Instead of creating a controller, let's create an API route that will have a closure that will validate our request. So go to routes/api.php and make one route where we'll test our enhanced validation mechanism.
<?php

use Illuminate\Http\Request;
use App\Http\Requests\EventRequest;
use Illuminate\Support\Facades\Route;

Route::post('event/new', function(EventRequest $request){
    $validated = $request->validated();
    dd($validated);
});
  • Run $ php artisan serve.
  • Open up Postman, and enter some data into the form-data section of the Body tab that matches the fields we're trying to validate. Make sure that your start_time is some time in the past. Once you run the request, it should look something like this:

Screenshot 2021-04-22 164552.png

Conclusion:

  • If you didn't get that error, that means you didn't do the proper setup. You'll need to make the abstract class that we discussed in our first Laravel Validation article.
  • There are so many possibilities you can accomplish by extending Rule objects. They're even better when you attach Traits into the mix, a group of methods that are not bound by the classes they are in.
  • There is at least one more part to come for this Laravel Validation series. In the next article, we'll discuss how you can implement After Hook validation.