On March 16, 2015, something amazing happened in the world of PHP. The long-awaited, hotly debated Scalar Type Declarations RFC was accepted for PHP 7! Finally, it will be possible to declare scalar types (int, float, bool, and string) for function parameters and return values:
<?php
function itemTotal(int $quantity, float $price): float
{
return $quantity * $price;
}
The need for safe type casts
By default, scalar types are enforced weakly. So while passing a value such as “my string” to an int parameter would produce an error, values such as 10.9, “42.5”, true, and false would be accepted and cast to 10, 42, 1, and 0, respectively. This behavior lacks safety, since any of these values are likely to be errors, and casting them results in data loss.
Enabling the optional strict mode will prevent values with an incorrect type from being passed, but this isn’t a complete solution. Whenever you are dealing with user input, whether from a posted form, url parameters, or an uploaded CSV, the data will arrive as a string. Before it can be passed to a function expecting an int or float, the data must be converted to the corresponding type.
Simple, right?
<?php
declare(strict_types=1);
$total = itemTotal((int)$_POST['quantity'], (float)$_POST['price']);
Wrong. This is even less safe than the default type coercion! A user could pass a value such as “5 hundred” or “ten” and it would be cast to 5
or 0
without producing an error. This is especially concerning in scenarios where sensitive financial information is being handled.
PHP filters?
In the past I’ve tried to solve this problem by using PHP’s built-in FILTER_VALIDATE_INT
and FILTER_VALIDATE_FLOAT
validation filters. However, there are two problems with this approach. First is verbosity: validating just two inputs for our itemTotal
function requires eight additional lines of code:
<?php
declare(strict_types=1);
$quantity = filter_var($_POST['quantity'], FILTER_VALIDATE_INT);
$price = filter_var($_POST['price'], FILTER_VALIDATE_FLOAT);
if ($quantity === false) {
throw new Exception("quantity must be an integer");
} elseif ($price === false) {
throw new Exception("price must be a number");
}
$total = itemTotal($quantity, $price);
Secondly, and even more problematic, filter_var
casts the value being checked to a string and trims whitespace, which results in various unsafe conversions being accepted.
Introducing PolyCast
In October of last year, Andrea Faulds proposed a Safe Casting Functions RFC to fill the need for safe type conversion. At the same time, I started developing a userland implementation called PolyCast. Although Andrea’s RFC was ultimately declined, I continued to move PolyCast forward, with a number of improvements based on community feedback.
PolyCast comes with two sets of functions. The first (safe_int
, safe_float
, and safe_string
) return true if a value can be cast to the corresponding type without data loss, and false if it cannot. The second (to_int
, to_float
, and to_string
) will directly cast and return a value if it is safe, and otherwise throw a CastException
.
This makes safe type conversion nearly as simple as forced casts, without compromising safety:
<?php
declare(strict_types=1);
use function theodorejb\polycast\{ to_int, to_float };
$total = itemTotal(to_int($_POST['quantity']), to_float($_POST['price']));
For more examples and details on which values are considered safe, check out the project on GitHub. PolyCast is tested on PHP 5.4+, and you can easily install it with composer require theodorejb/polycast
.