Comic by: http://comics.ptengine.com/
Comic by: http://comics.ptengine.com/

Laravel FormRequest Mutators / Attribute Casting

When I was handling form requests in Laravel controller, I felt a need for some intermediate class or a trait in the FormRequest. A a quick Google search revealed that I was not alone but there wasn’t any package available to accomplish what I needed.
 
Checkout the github repo for this project: https://github.com/stahiralijan/request-caster

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: Applies strtolower() to the selected field(s).
  • $toUpperCaseWords: Applies strtoupper() to the selected field(s).
  • $toUCFirstWords: Applies ucwords() to the selected field(s).
  • $toSlugs: Applies str_slug() to the selected field(s).
  • $toIntegers: Casts selected field(s) to int.
  • $toFloats: Casts selected field(s) to float.
  • $toBooleans: Casts selected field(s) to bool.
  • $toArrayFromJson: Applies json_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\Collectionso 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!