Few people know that you can piggyback on a Form Requests to transform the data you receive before getting it into the controller - this is super useful to keep controllers clean and to make validation easier.
Imagine someone sends you a line-separated email list to your API without doing any client-side transformations on their side (this can easily happen by just sending the value of a textarea) so you’ll probably want to validate that data, and use it down the line in a format that’s easier for your program to handle (array/Collection).
First thing we need to do is create our Form Request and our controller.
php artisan make:controller -i EmailsController
php artisan make:request UserEmailsRequest
this will create a new file under App\Http\Requests
, this file should be injected into your controller instead of the default request so that the validation and data transformations can happen auto-magically.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserEmailsRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class EmailsController extends Controller
{
/**
* Handle the incoming request.
*
* @param UserEmailsRequest $request
* @return JsonResponse
*/
public function __invoke(UserEmailsRequest $request): JsonResponse
{
return response()->json([], Response::HTTP_NOT_IMPLEMENTED);
}
}
To use this controller in your routes/api.php
file just add it like this:
Route::post('somewhere', 'EmailsController');
The __invoke
function in the controller gets called automatically by Laravel whenever you don’t specify a function to call.
Now, to the interesting part - this are all the basic functions you’ll want to override in your form request.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserEmailsRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
];
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
}
}
$request->emails
looks like this "foo@bar.com\nbar@foo.com,foobar@barfoo.com"
and we want to transform it into an array and validate that each one of the values is also a valid email, the final array will look like this:
[
"foo@bar",
"bar@foo.com",
"foobar@barfoo.com",
]
We will start by transforming the data using the prepareForValidation
function, this is called before validation is even attempted as the name suggests, in here we can transform the request and validate against the transformed data (the original data is still available through the $this->get('emails')
function)
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
// This is where the magic happens, the data from the key 'emails' will be overwritten
// with the return value of the formatEmails function.
$this->merge([
'emails' => $this->formatEmails($this->emails),
]);
}
/**
* Transforms the request from a list of emails separated by a \n to an array of emails.
*
* @param string $emails
*
* @return array
*/
protected function formatEmails(string $emails): array
{
// Simple transformation in here, explode the string by the delimiter, then filter
// all the empty lines and then trim the empty values from the final array.
// this is not production ready code so don't use it.
return collect(explode("\n", $emails))
->filter(static function ($email) {
return !empty($email);
})
->map(static function ($email) {
return trim($email);
})->toArray();
}
Before the transformation, validating the data that was sent to us would have required to do the same transformation we just did as a custom rule, now since the data is nicely packed into a standard array we can use standard validation rules against it.
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'emails' => 'required|array',
'emails.*' => 'required|email',
];
}
}
And that’s it! if we go back to our controller we can use $request->emails
as an array now plus all the values in the array will be validated using standard laravel rules
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserEmailsRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class EmailsController extends Controller
{
/**
* Handle the incoming request.
*
* @param UserEmailsRequest $request
* @return JsonResponse
*/
public function __invoke(UserEmailsRequest $request): JsonResponse
{
// do something here with your clean copy of $request->emails remember that you
// that you can get the original request values by doing $request->get('emails')
return response()->json([
['emails' => $request->emails]
], Response::HTTP_OK);
}
}
Response:
{
"emails": [
"foo@bar.com",
"bar@foo.com",
"foobar@barfoo.com"
]
}
The response from the endpoint will be a validated json array, just as we wanted! now down the line you can use this transformed value in other methods, your one-off function that handled the transformation is tucked away into the request itself and you don’t have to add noise to your controllers - if the function you made is something you’re able to re-use for other requests then make a base request controller, drop that function in there and inherit the requests from that base controller instead, that way you’ll have access to that function wherever you need it.
Top comments (3)
I am usually overide all method and do formating inside the method.
But the method prepareForValidation seems the correct way. Thanks
wait, what? what if someone will send you $emails like:
[1, null, 'hello there']
? :DIt will throw like hell because you assumed that
$emails
key is a string ;)Thanks for the prepareForValidation