I guess this has been written about a couple of times already but I haven’t really found anything that puts all the pieces together nicely. This is an article about adding a custom validator to Lumen(v5.2) specifically but can be ported to Laravel as well.
After taking a read through the docs [https://laravel.com/docs/5.2/validation] about custom validation rules it is apparent that you can do a lot with it. Kudos to the creators.
Model
I started by defining the $rules on my Model. The rules array can be accessed anywhere and makes for very readable Validation setups across your app. As you can see required min come bundled with Lumen but region is my custom validator I would like to add.
<?php
namespace Legit\Verification;
use Illuminate\Database\Eloquent\Model;
class Verification extends Model
{
...
public static $rules = [
'client_user_id' => 'required',
'phone_number' => 'required|region|min:11',
];
...
}
Custom Validator
<?php
namespace Legit\Validation;
use Illuminate\Support\Facades\Config;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberType;
use libphonenumber\PhoneNumberUtil;
class Phone
{
private $numberUtil;
public function __construct()
{
$this->numberUtil = PhoneNumberUtil::getInstance();
}
/**
* @param $attribute
* @param $value
* @param $parameters
* @param $validator
* @return boolean
*/
public function validateRegion($attribute, $value, $parameters, $validator)
{
try {
$validatedNumber = $this->numberUtil->parse($value, Config::get('country_iso'));
} catch (NumberParseException $e) {
return false;
}
return $this->returnIsValidNumber($validatedNumber);
}
/**
* @param $validatedNumber
* @return boolean
*/
private function returnIsValidNumber($validatedNumber)
{
if ($this->numberUtil->isValidNumber($validatedNumber)
&& $this->numberUtil->getNumberType($validatedNumber) === PhoneNumberType::MOBILE) {
return true;
}
return false;
}
}
This is the phone validator with a method to validate regional phone numbers by the country iso code. I also use the giggsey/libphonenumber-for-php [https://packagist.org/packages/giggsey/libphonenumber-for-php] composer library that implements this standard really well.
I set the Config for the country iso dynamically somewhere else.
Config::set('country_iso', $country->country_iso);
The validateRegion()
is where the magic happens and it will get run when the
validation gets setup and run against data from your request.
See how the format prefixes validate in front of your defined rule for Region
Controller
<?php
class VerificationController extends Controller
{
/**
* @param Request $request
* @return Response
*/
public function check(Request $request)
{
$validator = Validator::make($request->all(), Verification::$rules);
if ($validator->fails()) {
return $this->respondWithErrorMessage($validator);
}
...
The only thing that has to happen in the controller method is where you make the Validator using the Facade Alias that is specified.
Also see how readable the rules are, now anyone knows you are validating from the Verification Model. This allows for rules to get re-used and won’t float around in the controller space.
Note: If you are using Lumen you need to uncomment/add the following line for this to work.
// bootstrap/app.php
$app->withFacades();
resources/lang/en/validation.php
I didn’t know this file actually existed until now. This is where the validation component reads the messages from and you can do fancy things like have different translations for them by setting your app locale etc. We’ll stick with english this time around but the same applies for any other language.
NOTE: Lumen does not come with this file so you have to add it by yourself.
<?php
return [
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
],
'required' => 'The :attribute field is required.',
'custom' => [
'phone_number' => [
'region' => 'The phone number is not the correct format for :region',
],
],
];
Notice that it just a simple array, I didn’t copy the full Laravel one but only the rules I use. The custom key contains your newly created validator’s error message with a placeholder to be replaced. I will show you how to do that now.
This is the full Laravel format FYI.
<?php
/*
|-------------------------------------------------------------------
| Custom Validation Language Lines
|-------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
AppServiceProvider.php
This will still fail if you ran this and it took me a while to figure out why but you have to bind all these magic things together in your app service provider.
NOTE: Uncomment the following line if you are using Lumen.
<?php
// bootstrap/app.php
$app->register(App\Providers\AppServiceProvider::class);
Add the following to the app/Providers/AppServiceProvider.php
<?php
app('validator')->extend(
'region',
'Legit\Validation\Phone@validateRegion'
);
This registers the custom extension on the application instance’s validator.
Replacer
<?php
app('validator')->replacer('region',
function($message, $attribute, $rule, $parameters) {
return str_replace(
':region',
Config::get('country_iso'),
$message
);
});
This registers how the validator should replace the placeholder error message with a custom value.
Notice how I added the extension directly on the app instance validator. Adding it directly on the one in the container eliminates the validator losing it’s references to the extensions when you are running the tests.
Binding it together
<?php namespace App\Providers;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
// Registers custom validator
app('validator')->extend(
'region',
'Legit\Validation\Phone@validateRegion'
);
// Registers how to replace the placeholder
app('validator')->replacer('region',
function($message, $attribute, $rule, $parameters) {
return str_replace(
':region',
Config::get('country_iso'),
$message
);
});
}
public function register()
{
//
}
}
Output
Now you should be golden. Expected output something like this:
Bonus
Because if you read to here you are truly amazing :+1:
To render error messages like that here is a little helper I made to collapse multiple validation errors into a single response. If you saw, this is the Controller class the top one inherits from.
<?php namespace App\Http\Controllers;
use Laravel\Lumen\Routing\Controller as BaseController;
use Symfony\Component\HttpFoundation\Response;
class Controller extends BaseController
{
/**
* @param $message
* @return Response
*/
protected function respondWithMissingField($message)
{
return response()->json([
'status' => 400,
'message' => $message,
], 400);
}
/**
* @param $message
* @return Response
*/
private function respondWithValidationError($message)
{
return response()->json([
'status' => 406,
'message' => $message,
], 406);
}
/**
* @param $validator
* @return Response
*/
protected function respondWithErrorMessage($validator)
{
$required = $messages = [];
$validatorMessages = $validator->errors()->toArray();
foreach($validatorMessages as $field => $message) {
if (strpos($message[0], 'required')) {
$required[] = $field;
}
foreach ($message as $error) {
$messages[] = $error;
}
}
if (count($required) > 0) {
$fields = implode(', ', $required);
$message = "Missing required fields $fields";
return $this->respondWithMissingField($message);
}
return $this->respondWithValidationError(implode(', ', $messages));
}
}