PHP argument validation using assert
When creating classes, we either pass arguments to the constructor function or have setter-getter functions to access class properties. These arguments may sometime have a specific format or requirements. For Example :
/**
* $age integer
*
* @return string
*/
function($age){
return "You are $age years old";
}
Above function expects $age to be an integer. If any other type is passed, function will no work as expected.
The example was very granular. When your core classes have functions which affect important domain logics of your project, argument validation becomes an important part.
PHP comes with default function(or more specifically a language construct for PHP version 7) called assert
, which basically checks if provided assertion if false. If yes then you can handle the exceptions in callback. Even if this is useful, I will strongly suggest using composer plugin webmozart/assert for this. It has very useful intuitive methods for the same.
- Example :
Let's consider a function placeOrder()
which takes array of $items
having instances of Item
class, a $shippingPrice
and $customer
having instance of Customer
class.
<?php
use Webmozart\Assert\Assert;
Class Order{
/**
* Array of Item instances
* @var array
*/
private $items;
/**
* Shipping Price
* @var float
*/
private $shippingPrice;
/**
* Customer Instance
* @var Customer
*/
private $customer;
/**
* Instantiate Order class
*
* @param $items
* @param $shippingPrice
* @param $customer
*/
function __construct($items, $shippingPrice, $customer)
{
Assert::isArray($items, '$items must be an array. Got: %s');
Assert::allIsInstanceOf($items, '\Item', '$items must contain array of Item class instances. Got: %s');
Assert::numeric($shippingPrice, '$shippingPrice must be numeric. Got: %s');
Assert::isInstanceOf($customer, '\Customer', '$customer must be an instance of Customer class. Got: %s');
$this->shippingPrice = $shippingPrice;
$this->customer = $customer;
$this->items = $items;
}
/**
* Places order
*
* @return Order
*/
public function placeOrder(){
// PlaceOrder
}
}
Now, when we instantiate the order class :
$customer = new Customer('John Doe');
$order = new Order([], 12.22, $customer);
We are passing an invalid value for $items
argument. It will throw \InvalidArgumentException
with error message : $items must contain array of Item class instances. Got: Array
.
If you are using setter-getter functions instead of constructor parameters, you can have these assertions inside the setter functions. Above methodology is sometimes referred as defensive programming where we are making sure function will not break at the intial stages itself. The counter concept of defensive programming will be validating these arguments as and when they are actually used inside placeOrder
function.
Advantages :
- Prevents errors caused due to unexpected type of arguments passed
- Easier to debug
- Easier to create unit tests and catch asset exceptions
- Easier to write readable custom error messages
Disadvantages :
If not properly thought, duplicate validations can cause redundancies. For example in above case, if instance of Item
class contains a valid price is a concern of Item
class. If that validation is also present in Order
class as well, it created redundancies.
You can find more assertion options available here : https://github.com/webmozart/assert