Always Use Type Declarations For Function Parameters and Return Values in PHP

What if I told you PHP has a built-in feature that will reduce the number of bugs in our code base? What if I told you it’s super simple to introduce into your daily coding routine? Would you be interested in using it?

In this article, we discuss why we should always use type declarations for function parameters and return values.

The Problem

Let’s say we’ve just written the following function to generate a formatted string indicating the length of another string.

<?php
function lengthFormatted($value)
{
    return "length is " . strlen($value);
}

When we call this with a string we get the expected output.

<?php
echo lengthFormatted("string"), PHP_EOL;
length is 6

But what if we accidentally send it a boolean? What happens then?

<?php
echo lengthFormatted(true), PHP_EOL;
length is 1

Length is 1? That’s not what we would want to happen and we’ve just found a potential bug in our software.

As a brief sidebar, you might be wondering why this happened. Because PHP is loosely typed when we pass a boolean to a function that’s expecting a string it automatically type juggles it into a string. In the background, true is stored as an integer 1 so PHP converts it to a string "1" and then finds the length of the single digit.

<?php
true => 1 => "1"

Our example above is simple so it’s not a huge problem and most likely people aren’t going to notice it but what if we were doing a calculation with the length of that string?

<?php
function initThermonuclearWar($verify)
{
    // check for no with a poor check condition
    if (strlen($verify) == 2) {
        return;
    }

    sendLaunchCodes();
}

initThermonuclearWar('No'); // safe
initThermonuclearWar(false); // doom

This is not something we want to have happened but what is the solution?

Type Declarations for Parameters

Now what we want to have happened is for PHP to say “Hey, I’m looking for a string and not a boolean” but instead, it allows us to pass it data we didn’t account for in our function.

Thankfully PHP gives us a way to not shoot ourselves in the foot. This is done by adding declare(strict_types=1); at the top of our PHP file and then specifying the type of the variable we’re looking for in our function call.

<?php
declare(strict_types=1);

function initThermonuclearWar(string $verify)
{
    // check for no with a poor check condition
    if (strlen($verify) == 2) {
        return;
    }

    sendLaunchCodes();
}

initThermonuclearWar('No'); // safe
initThermonuclearWar(false); // error

Now when we run this code we’ll get an error:

Fatal error: Uncaught TypeError: Argument 1 passed to initThermonuclearWar() must be of the type string, bool given, called in Standard input code on line 16 and defined in Standard input code:4
Stack trace:
#0 Standard input code(16): initThermonuclearWar(false)
#1 {main}
  thrown in Standard input code on line 4

We’ve started requiring declare(strict_types=1); and types on our parameters on every PR we review just because of the potential to catch bugs like this before it causes problems. We also wouldn’t have allowed such a sloppy check. :-)

What If We Want To Support Both?

Let’s say there’s a case where we might want to support both a string and a bool type in our function? How could we support that?

Before PHP 8.0 our only option was to specify the parameter as a mixed type. A mixed type parameter will take any type but to work with the variable we have to add checks to determine what type we’re working with.

<?php
declare(strict_types=1);

function initThermonuclearWar(mixed $verify)
{
    // check for no with a poor check condition
    if (is_string($verify) && strlen($verify) == 2) {
        return;
    }

    if (is_bool($verify) && !$verify) {
        return;
    }

    sendLaunchCodes();
}

initThermonuclearWar('No'); // safe
initThermonuclearWar(false); // safe

Of course, the downside to this is that we’ve run into the same problem as before because we can now pass a new type to our function and run into the same problem.

<?php
declare(strict_types=1);

function initThermonuclearWar(mixed $verify)
{
    // check for no with a poor check condition
    if (is_string($verify) && strlen($verify) == 2) {
        return;
    }

    if (is_bool($verify) && !$verify) {
        return;
    }

    sendLaunchCodes();
}

initThermonuclearWar('No'); // safe
initThermonuclearWar(false); // safe
initThermonuclearWar(0); // doom

To solve this problem PHP 8.0 added Union Types which allow us to specify a range of possible types. This is done by listing each of the types that could be safely accepted as a parameter using a pipe symbol (|) to separate them. For example, if we want to accept a string or a bool we would use string|bool.

Now we can change our function to support Union Types.

<?php
declare(strict_types=1);

function initThermonuclearWar(string|bool $verify)
{
    // check for no with a poor check condition
    if (is_string($verify) && strlen($verify) == 2) {
        return;
    }

    if (is_bool($verify) && !$verify) {
        return;
    }

    sendLaunchCodes();
}

initThermonuclearWar('No'); // safe
initThermonuclearWar(false); // safe
initThermonuclearWar(0); // error

When we run this we’ll get an error.

Type Declarations for Return Values

Another feature that will help us reduce bugs is to always declare our return type. Again because PHP is a loosely typed language we can get a value from a function and then operate on it in ways that are “unexpected”. For example, we might have a function that returns a string and then we check its string length to see if it’s what we expect.

<?php
function getLaunchCodes()
{
    // snip - some logic here to determine if code is invalid
    if ($invalid) {
        return 'invalid-code';
    }

    return 'valid-code';
}

// please don't actually do this
if (strlen(getLaunchCodes()) <= 10) {
    echo 'Valid launch codes.  Initialize missiles', PHP_EOL;
}

In the future, another developer comes along and adds some additional checks. They don’t realize what the return value of the function is and modify the function to return a false (maybe through a quick copy and paste in multiple locations).

function getLaunchCodes()
{
    if (missingValidationHardware()) {
        return false;
    }

    // snip - some logic here to determine if code is invalid
    if ($invalid) {
        return 'invalid-code';
    }

    return 'valid-code';
}

When we run our code again and it returns false our results may be unexpected.

Valid launch codes.  Initialize missiles

Again not what we want.

To fix this we can declare the return type when we create the function.

<?php
declare(strict_types=1);

function getLaunchCodes(): string
{
    if (missingValidationHardware()) {
        return false;
    }

    // snip - some logic here to determine if code is invalid
    if ($invalid) {
        return 'invalid-code';
    }

    return 'valid-code';
}

Then when we run our code again we’ll get an error instead of unexpected behavior.

Fatal error: Uncaught TypeError: Return value of getLaunchCodes() must be of the type 
string, bool returned in Standard input code:6

Conclusion

In this article, we discussed how to use type declarations for parameters and return values to reduce bugs and make our code easier to read. By declaring these we can more easily have bugs brought to our attention and act on them.