PHP
Article

Can We Have Static Types in PHP without PHP 7 or HHVM?

By Younes Rafie

Now that PHP 7 has been out for a while with interesting features like error handling, null coalescing operator, scalar type declarations, etc., we often hear the people still stuck with PHP 5 saying it has a weak typing system, and that things quickly become unpredictable.

Vector illustration of programmer's desktop

Even though this is partially true, PHP allows you to keep control of your application when you know what you’re doing. Let’s see some code examples to illustrate this:

function plusone($a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone("1"));
var_dump(plusone("1 apple"));
// output

int(2)
int(2)
int(2)

Our function will increment the number passed as an argument by one. However, the second and third calls are passing a string, and the function still returns integer values. This is called string conversion. We can make sure that the user passes a numeric value through validation.

function plusone($a)
{
    if ( !is_numeric($a) )
    {
        throw new InvalidArgumentException("I can only increment numbers!", 1);
    }

    return $a + 1;
}

This will throw an InvalidArgumentException on the third call as expected. If we specify the desired type on the function prototype…

function plusone(int $a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone("1"));
var_dump(plusone("1 apple"));
// output

PHP Catchable fatal error:  Argument 1 passed to plusone() must be an instance of int, integer given, called in /vagrant/test_at/test.php on line 7 and defined in /vagrant/test_at/test.php on line 2

This error seems a bit weird at first, because the first call to our function is using an integer!

If we read the message carefully, we’ll see that the error message says “must be an instance of int” – it assumes that integer is a class, because PHP prior to version 7 only supported type hinting of classes!

Things get even more awkward with function return arguments in PHP 5. In short, we can’t lock in their types automatically and we should check the expected value after the function call returns a value.

Augmented Types

Prior to the release of PHP 7, the team at Box came up with a nice idea to solve the typing safety problem on their PHP 5 application. After using assertions, type hints, etc., they decided to work on a cleaner solution for this problem.

We’ve seen how Facebook pushed PHP a little bit forward by launching HHVM and Hack, but the team at Box didn’t want to fork the PHP source code or modify anything in the core. Their solution was to create a separate extension called augmented types to parse the method’s phpDoc and assert types on runtime.

Installation

The below extension is meant to be used with PHP 5 – if you’re on PHP 7, just sit back and enjoy the ride!

The installation process is not very complicated, and doesn’t require any specific configuration. The instructions below apply to Ubuntu, but are easily applicable to other *nix based OS as well.

# update system
sudo apt-get update

# install required dependencies
sudo apt-get install php5-dev bison flex

# clone the repo
git clone git@github.com:box/augmented_types.git

# install extension

phpize
./configure --enable-augmented_types
make
make test
sudo make install

We must tell PHP to load our extension by editing the php.ini file.

# Get php.ini location from PHP info. This will print the cli configuration file, you may need to edit /etc/php5/fpm/php.ini )
php -i | grep 'Loaded Configuration File'

# output: /etc/php5/cli/php.ini
vim /etc/php5/cli/php.ini
# Get `extension_dir` the PHP info
php -i | grep extension_dir
# output: extension_dir => /usr/lib/php5/20100525 => /usr/lib/php5/20100525

# Add this line at the end of the file
zend_extension=/usr/lib/php5/20100525/augmented_types.so

The extension can be enabled on a per-file basis using the ini_set function.

ini_set("augmented_types.enforce_by_default",1);

Or directly on the php.ini configuration file. Be sure to check the documentation for more details about the installation process.

Usage

We mentioned earlier that the extension uses phpDoc for function/method prototypes. Most of its functionality can be explained through code examples.

/**
 * Add one
 *
 * @param   int $a
 * @return    int
 */
function plusone($a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone("1"));
var_dump(plusone("1 apple"));

You might think you’ll be able to guess the output of the above code, but you’d probably be wrong!

int(2)
PHP Fatal error:  Wrong type encountered for argument 1 of function plusone, was expecting a integer but got a (string) '1' in /vagrant/test_at/test.php on line 15

Even the second call doesn’t pass! This happens because we’re not doing the PHP conversion we talked about earlier, the function requires us to strictly pass an integer. Now, what about calling it using a float?

var_dump(plusone(1.5));
PHP Fatal error:  Wrong type encountered for argument 1 of function plusone, was expecting a integer but got a (float) 1.500000 in /vagrant/test_at/test.php on line 14

We made our function accept two types of arguments (int and float) by using the composite types definition.

/**
 * Add one
 *
 * @param   int|float $a
 * @return    int
 */
function plusone($a)
{
    return $a + 1;
}

var_dump(plusone(1));
var_dump(plusone(1.5));

Now our function should work as expected.

int(2)
PHP Fatal error:  Wrong type returned by function plusone, was expecting a integer but got a (float) 2.500000 in /vagrant/test_at/test.php on line 0

Oops! We should also define the return type for our function or cast the return value to match the phpDoc.

/**
 * Add one
 *
 * @param   int|float $a
 * @return    int|float
 */
function plusone($a)
{
    return $a + 1;
}

We can also work with compound types: the following example sums the elements of an array.

/**
 * Calculate sum
 *
 * @param   array $nums
 * @return    int
 */
function sum($nums)
{
    $sum = 0;
    foreach ($nums as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum([10, 12, 76]));

// output
int(98)

The function returns the expected value. What if the array contains something other than integers, though?

var_dump(sum([10, 12, "something"]));

// output
int(22)

The extension gives us the ability to work with array types. The previous example will throw a fatal error as expected.

/**
 * Calculate sum
 *
 * @param   int[] $nums
 * @return    int
 */
function sum($nums)
{
    $sum = 0;
    foreach ($nums as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum([10, 12, 76]));
var_dump(sum([10, 12, "something"]));
int(98)
PHP Fatal error:  Wrong type encountered for argument 1 of function sum, was expecting a (integer)[] but got a array in /vagrant/test_at/test.php on line 20

An interesting case is when we have a function that accepts an arbitrary number of arguments. We can make our previous function return the sum of all the passed arguments.

/**
 * Calculate sum
 *
 * @param   *int $nums
 * @return    int
 */
function sum($nums)
{
    $args = func_get_args();
    $sum = 0;
    foreach ($args as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum(10, 12, 76));

The *int type definition lets our function receive any number of arguments as long as it’s an integer. We can combine the *int and int[] type definitions to make our function accept both previous cases.

/**
 * Calculate sum
 *
 * @param   *int|int[] $nums
 * @return    int
 */
function sum($nums)
{
    if ( !is_array($nums) )
    {
        $nums = func_get_args();
    }

    $sum = 0;
    foreach ($nums as $value) {
        $sum += $value;
    }

    return $sum;
}

var_dump(sum(10, 12, 76));
var_dump(sum([10, 12, 76]));

Now both function calls will return the same value (int(98)).

Default Arguments

Most of the time, functions contain optional arguments which may be initialized using default values. We use the void type definition to tell the extension that our value is optional but OK if passed in.

/**
 * SSH to server.
 *
 * @param   string      $url
 * @param   int|void    $port
 * @return  bool
 */
function ssh($url, $port = 2222)
{
    return true;
}

Note: When an argument has a default value it will not enforce the specified type. It means that passing a string on the port argument won’t throw an error.

Return Types

The same thing we said earlier about argument types applies to return type definitions. We can specify normal scalar types, classes, composite types and array types. Every function should have a @return <type> definition and must use @return void if it doesn’t return anything.

class User
{
    protected $id;

    protected $name;

    /**
     * Constructor
     * @param int       $id
     * @param string    $name
     * @return void
     */
    public function __construct($id, $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    /**
     * Return the user info as a string.
     *
     * @return    string
     */
    public function __toString()
    {
        return $this->name;
    }
}

Ignoring Files

Most of the time, our application contains other people’s packages, and we don’t want the augmented types extension to throw errors and halt our application because of missing definitions. Luckily, the extension provides a clean way to blacklist and whitelist files and folders.

augmented_types_blacklist([__DIR__."/vendor"]);

The extension will now ignore our vendor directory. If we have some of our own inspectable directories inside the vendor directory, we can use the whitelist function to add them back. You can read more about this in the documentation.

augmented_types_whitelist([__DIR__."/vendor/mylib"]);

We can also achieve the same result using the php.ini file, and this is the recommended way to do it, according to the extension’s developers.

# php.ini
augmented_types.whitelist=./vendor
augmented_types.blacklist=./vendor/mylib

PHP 7 Changes

PHP 7 introduced scalar type declarations and return types to functions/methods, thus eliminating the need for further validation on arguments and return types. Check out the list of changes on the official PHP website.

If PHP 7 already supports this, what could be the benefit of installing a new extension?

Well, the extension provides a wide variety of options that we don’t have in PHP 7, like:

  • Composite types (@param string|int $var)
  • Array types (@param int[] $var)
  • Forcing functions to return a value (at least void)

Conclusion

It’s always a good idea to upgrade to PHP 7 and benefit from all the exciting new features. However, we don’t always have the luxury of using cutting edge technology in our projects.

The augmented types extension will absolutely slow down the application’s runtime (as would be expected), which is why it should only be used during development and testing, to keep the production server clean and fast.

What do you think about the extension? Is it useful? Do you have other ideas on how to do what it does? We’re curious to know what you think, let us know!

  • http://evertpot.com/ Evert

    Strict typing is not the same as static typing. If PHP had static types, the scripts wouldn’t even run at all if the types are wrong.

    Aside from that, I think you have larger problems in your project/organization if you’re able to install a language-modifying PHP extension but you can’t update to PHP 7.

    • Bruno Škvorc

      Generally, I’d agree, but if you’re stuck in an ancient PHP 5x shop, you *could* use this to improve the team’s “coding safety” (devs can install the extension on their local machines) while ignoring it on the live server, no? I think I’d try and get people on board with this if I couldn’t get management to agree to a PHP 7 upgrade in production. Can’t do any harm – worst case scenario, you end up with overdocumented code, no?

      • http://evertpot.com/ Evert

        I agree. The only risk is if devs rely on type-safety and forget that it doesn’t exist in production, but since these are fatals, hopefully that never actually becomes a problem.

  • http://funivan.com/ Funivan

    Void is introduced at 7.1
    Genetic RFC is under discussion.
    Main difference between extention and php7 is that we can control strict types on specific files in php7.

  • cracks2016

    nice post admin

  • http://www.difriends.com Ricardo Sánchez

    It doesnt solve any problem that php already doesn’t solve without TYPE HINTING, in PHP there is no such thing as static types.
    There is no work arounds to declare the type of a variable (maybe it was some extension but it was never popular).
    In php7 (and maybe in this extension), you can take an int argument and assign a string to it. So it was never typed static.
    Anyway, we still don’t have a compile phase in our dev environments, PHP is not Haskell, it will tolerate your type mismatch and run improperly on prod without any warning.
    It is better to avoid this kind of features, because the validation is in execution time and creates extra load in every function call, there is no a deterministic way to predict the type of a variable in compilation time in this kind of languages, so 0 optimization.

  • http://brucemesnekoff.com Bruce Mesnekoff

    I was actually captured with the piece of resources you have got here. big thumbs up for making such wonderful blog spot. I like and recommend it very much.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.