Laravel FormRequest Mutators / Attribute Casting
FormRequest.
A a quick Google search revealed that I was not alone but there wasn’t any package available to accomplish what I needed.Why do we need this package?
To give you guys idea what I wanted, let’s put what I wanted to accomplish.
To create/update user’s details I was doing the following.
...
public function store(UserFormRequest $request)
{
$first_name = ucwords($request->first_name);
$last_name = ucwords($request->last_name);
$fullname = $first_name . ' ' . $last_name;
...
$user = User::create([ ... 'first_name' => $first_name,
'last_name' => $last_name,
... ]); ...
return redirect(route('users.index'))
->with(['message' => "User ({$fullname}) created"]);
}
I was not happy with the code above because it made my controller method look dirty by putting decorative stuff into the controller, there must be a separation of concern here.
I thought for a moment and said to myself, I must do something about it and started digging about Laravel Requests. Soon I learned about validate()
method that resides in ValidatesWhenResolvedTrait
, which is used in FormRequest
class.
The implementation
I began by creating a Trait
called RequestCasterTrait
and added my casting methods like so:
...
protected function castToLowerCaseWords(): void
{
if (property_exists($this, 'toLowerCaseWords') && $this->toLowerCaseWords)
{
foreach ($this->toLowerCaseWords as $key)
{
if ($this->request->has($key))
{
$this->request->set($key, strtolower(request($key)));
}
}
}
}
...
Now I added another method to calls all the necessary methods:
public function mapCasts(): void
{
$this->castToLowerCaseWords();
$this->castToUpperCaseWords();
$this->castUCFirstWords();
$this->castToSlugs();
$this->castToInteger();
$this->castToFloats();
$this->castToBoolean();
$this->castJsonToArray();
// call this in last so for the fields that need to be casted first
$this->castJoinFields();
}
I decided to use the validate()
method because it is called before data is either passed back to form with errors or passed to controller. Here is the method in the ValidatesWhenResolvedTrait:
trait ValidatesWhenResolvedTrait
{
/**
* Validate the class instance.
*
* @return void
*/
public function validate()
{
$this->prepareForValidation();
$instance = $this->getValidatorInstance();
if (! $this->passesAuthorization())
{
$this->failedAuthorization();
}
elseif (! $instance->passes())
{
$this->failedValidation($instance);
}
}
...
I overrided this method to accommodate my needs:
public function validate()
{
$this->prepareForValidation();
$instance = $this->getValidatorInstance();
if (!$this->passesAuthorization())
{
$this->failedAuthorization();
}
else if (!$instance->passes())
{
$this->failedValidation($instance);
}
if ($instance->passes())
{
$this->mapCasts();
}
}
The casts and conversions will only take place when the validations pass the FormRequest’s rules. And, that’s pretty much it.
Now let’s start using it in our dummy FormRequest
and Controller
:
UserFormRequest
namespace App\Http\Requests\Admin;
use Stahiralijan\RequestCaster\Traits\RequestCasterTrait;
use Illuminate\Foundation\Http\FormRequest;
class UserFormRequest extends FormRequest
{
use RequestCasterTrait;
protected $toUCFirstWords = ['first_name','last_name'];
protected $joinStrings = ['fullname'=>' |first_name,last_name'];
public function authorize()
{
return TRUE;
}
public function rules()
{
return [
'first_name' => 'required',
'last_name' => 'required',
];
}
}
Let me explain what’s going on here:
First you need to import the trait by use Stahiralijan\RequestCaster\Traits\RequestCasterTrait
and then use this trait in the class like shown in the code above.
Here I wanted to Capitalize first name and last name, and join both names to create a new field called fullname.
You can use the following attributes for mutation / casting:
$toLowerCaseWords
: Appliesstrtolower()
to the selected field(s).$toUpperCaseWords
: Appliesstrtoupper()
to the selected field(s).$toUCFirstWords
: Appliesucwords()
to the selected field(s).$toSlugs
: Appliesstr_slug()
to the selected field(s).$toIntegers
: Casts selected field(s) toint
.$toFloats
: Casts selected field(s) tofloat
.$toBooleans
: Casts selected field(s) tobool
.$toArrayFromJson
: Appliesjson_decode()
to the selected fields.
$joinStrings
is a special attribute that takes joins two or more fields, you can specify joining rule in the following manner:
protected $joinStrings = ['newFieldName' => 'glue|first_field,second_field,...,nthfield'];
The result of the above UserFormRequest
for $first_name = 'Tahir'; $last_name = 'Jan';
will be $fullname = 'Tahir Jan';
as we have provided a space ' '
for a glue. Now lets see how we can use the UserFormRequest
in our controller’s store method:
public function store(UserFormRequest $request)
{
// Here first_name will be 'Tahir' even if user submitted 'tahir'
User::create($request->all());
return redirect(route('users.index'))
->with(['message'=>"User ({$request->fullname}) created"]);
}
I’ve also created a special method called collection()
which returns an Illuminate\Support\Collection
so that we get a collection of all the fields instead of an array
by $request->all()
method. We can use it in controller like this:
public function store(UserFormRequest $request)
{
$request->collection()->map(function($item){
...
// go nuts
});
// or like this for debugging
$request->collection()->dd();
// thanks to the upgrades in Laravel 5.5
// or
$request->collection()->dump();
...
You can install this package in your Laravel 5.5+ application:
composer require stahiralijan/request-caster
Future plans
It would be nice to have a mutator/caster that would run a user method(s) in a similar fashion before FormRequest
validates the data.
I don’t know if this package will get broken in next Laravel release because of ValidatesWhenResolvedTrait
, please help me by test it in older versions of Laravel.
I hope this package helps you in reducing your dev-time and increases your productivity.
Thanks for reading!