Custom Validation Rules in Laravel
When writing any app, validating user inputs is absolutely essential to ensure everything works they way it should. It's most obvious for functional data (like a user account's email address), but sometimes the business case also requires some additional checks.
The app I'm currently working on allows companies from all over the world to register and pay for a service. However, for legal reasons this service may only be offered to EU companies, not to EU individuals. Therefor it is essential for the client to ensure that no private person based in EU can sign up without owning an actual business.
In order to make sure of this, multiple checks have to be run:
If the company signing up is registered outside of the EU, the VAT ID should be optional and no further checks should be run.
If it is a EU company, the VAT ID needs to be provided.
A VAT ID provided needs to follow the official syntax
Furthermore, the provided VAT ID needs to be valid and belong to a company with the exact name that was provided.
As companies can be added and edited in multiple places in this application, the logic for these checks should obviously be easily reusable.
Laravel Validation Rules
The Laravel framework comes with very powerful validation features which cover a lot of the most common use cases. We could even use the existing validation rules to check for the first point on our list above, by using
Rule::requiredIf() and checking the provided country.
For the other two items we need custom functionality however, so it makes sense to bundle all of this logic together in a new, custom validation rule. So let's create one by running
php artisan make:rule VatID.
Before we start to add the actual functionality, the new validation rule needs a bit more data to work with. Per default it is only passed the variable that is being checked, so in our case the VAT ID itself.
But we also need to pass it the company name as well as the country provided by the user for our additional checks. To do so, we simply provide the entire
$input array with all user data to the rule class when applying it inside a validator where we actually want to verify the ID.
Running the basic checks
Most of the checks we need to run on the provided data a fairly straight forward, so I chose to bundle them all directly inside the validation rule class. If any check fails, we end the execution by returning early from the function as there is no point of running further checks on incomplete or incorrect data.
Essentially, this rule for now goes through four steps:
If no country or no name are provided, we ignore the VAT ID for now. We simply don't have the data required to run our checks yet. The user will get errors about those two required fields and we can run this check again once they've been filled.
It checks the selected country in our database to see if it is part European Union (EU). If it is not, we don't need to validate the VAT ID and can end the check.
If the country is part of the EU but the user failed to provide a VAT ID, we send back an error message and end the check.
If a VAT ID was provided we run a simple Regex test to ensure it has a valid format and the alert the user otherwise.
Only if all of these checks pass do we actually verify if the VAT ID is valid and if the company name matches up. The reason is simple: to do so we need to talk to an external API, which takes time, and we'd rather not have the user wait a couple of seconds for the error message when we already knew the data wasn't valid.
Checking the Validity using an API
To make sure the VAT ID is actually valid, we need to check the EU database for a matching entry. There are a bunch of paid services for this, but luckily the EU also offers a free REST or SOAP service to check against. I decided to go with SOAP this time as it required less information to be sent in the request.
To keep the code tidy, this code will live in its own action class. In there, we use the native PHP SOAP client, send our VAT ID to the endpoint, and check whether the results indicates a valid ID and a matching company name.
With the new action class in place we can simply add one final check to our rule. It hands over the VAT ID and company name to the action and then send an error message back to the user if the API call found any issues.
With these checks in place, we can be sure that no EU customer signs up without a valid VAT ID, keeping my client out of trouble.