Cachet 3.x is under development. Learn more.

Laravel Form Request Tip

Over Christmas I started tinkering with a little project to learn about some of the emerging technologies and frameworks that I don't have a chance to play with day to day. For this project, I've been using Jetstream 2 and Inertia.js, which I've loved! I'd like to write a bit more about these when I get chance.

Whilst I've been working on this, I've been trying to use some of Laravel's features that I've not really used much before. One of these is the FormRequest feature. A quick php artisan make:request CreateCampaignRequest command and all I need to do is authorize the request and add my rules — how great is that?!

Part of my choice in using Jetstream (aside from, well, everything about it) is that I was able to support teams out of the box. That's great, but now my app needs to validate differently based on the team. For example, two teams can use the same "Campaign" name, but one team can't have two campaigns with the same name.

As an aside, I always create a app/helpers.php file in my project and have started adding this to my Jetstream projects:

if (! function_exists('current_team')) {
    /**
     * Get the user's current team.
     *
     * @return \App\Models\Team|null
     */
    function current_team(): ?Team
    {
        return optional(auth()->user())->currentTeam;
    }
}

In my requests I need to validate that the campaign name is unique for the current team. That's easy enough:

use Illuminate\Validation\Rule;

public function rules()
{
    $currentTeam = $this->route('team') ?? current_team();

    return [
        'name' => [
            'required',
            Rule::unique('experiments')->where(function ($query) {
                return $query->where('team_id', '=', optional($currentTeam)->id);
            }),
        ],
    ];
}

It's not awful, but it's not great either. And what about if I need to access other route bindings or values?

To solve this, I created a new BaseRequest class, which overrides the prepareForValidation method to merge in data that won't exist in the original request itself. Because I'm also using route model bindings, I can easily pull each model back from the request:

class BaseRequest extends FormRequest
{
    /**
     * Prepare the data for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $currentTeam = $this->route('team') ?? current_team();

        $this->merge([
            'campaign_id' => optional($this->route('campaign'))->id,
            'subscriber_id' => optional($this->route('subscriber'))->id,
            'sub_campaign_id' => optional($this->route('sub_campaign'))->id,
            'team_id' => optional($currentTeam)->id,
        ]);
    }
}

Now I can quickly access any of this data within the rules. Our form request now becomes:

use Illuminate\Validation\Rule;

public function rules()
{
    return [
        'name' => [
            'required',
            Rule::unique('experiments')->where(function ($query) {
                return $query->where('team_id', '=', $this->team_id);
            }),
        ],
    ];
}

Of course, the prepareForValidation method can be used for a lot more, but this is just one use-case where I found it particulary nice.