PHP Strict Types: Hard to get right

Some people live and swear by strict types in PHP, and it’s understandable why they do. I want to start by saying that strict types have their purpose, but many programmers don’t understand what they mean and misuse them. I’ve seen plenty of subtle bugs related to strict types, so if you use strict types, this post is for you.

Strict by default – for objects

When you call a method/function, PHP performs some type checks. When the arguments are object types or arrays, and the target method/function includes type hints in the parameters, the check is strict by default.

Get an notified via email when there is a new post!

Join 502 other subscribers

If your entire file never uses scalar types, turning on strict types will have no effect.

Null erasure

One of the most common subtle bugs I see in strictly typed PHP is that nulls can and will be silently erased. PHP is already quite strict about passing null to a method/function that doesn’t accept a nullable argument. Enabling strict types doesn’t make it more strict in regards to null, as a null argument cannot be passed to a non-nullable parameter, regardless of strict types.

Where it gets dicey is when you get a variable that might be string|int|null and want to pass it to a method/function that only accepts string|null, so you cast it: (string) $var.

At first glance, to the uninitiated, this looks pretty sane. However, there is a subtle bug in the case $var is null. When casting null to another scalar type, the results are in the following table:

(string) null""
(int) null0
(float) null0.0
(bool) nullfalse

As you can see, none of these are null, so by casting to a scalar type, you actually erase null, which can lead to very subtle bugs if it isn’t caught during a code review.

If you find yourself casting in your strictly typed code … you are probably doing something wrong. A more sustainable solution is to ensure you have the correct types in the first place. For the example above, that would be ensuring whatever gave you $var in the first place, returns string|null instead of string|int|null or refactoring the method/function you are calling to accept string|int|null instead of string|null. This means if there is a subtle bug, there will only be one place to fix it (the method/function we are calling) instead of putting the responsibility on the caller, especially since refactoring all the code sites to change the type may be quite a bit of code churn.

Alternatively, you can disable strict types and rely on coercion. There are only a few coercions to worry about. Most of the time, this is precisely what you want. When you need strictness, turn on strict types.

PHP doesn’t offer a way to cast to a type and retain its “nullableness,” so if you want that, I wrote a darn simple library: withinboredom/null-cast – Packagist that you can drop in.

When strict types apply

Most people think that enabling strict types will make your code safer. This is correct, to a degree, but not entirely correct. A better way would be, “Strict types makes it clear that the value I’m returning or calling with is the type it expects.”

Perhaps a good way to describe it would be to use an example:

<?php declare(strict_types=1);

function test(int $number, bool $isEnabled): string {
    if($isEnabled) {
        return (string) ($number * 2);
    }
    
    return (string) $number;
}

echo test(3, false) . "\n";
echo test(3, true) . "\n";
// will cause a type error in this file:
//echo test(3, 0);

With strict types enabled, it will throw a TypeError when we try to return an int, so we will need to cast it to a string. When it comes to return types, strict types apply. When we call code in the same file, type-checking is strict.

However, if we call test("42", 0) from non-strict typed code … what will happen? It will cast “42” to an int and 0 to a bool. What about test(42, null)? A TypeError is thrown because null is always strictly typed. Strict types only apply in the file in which it is defined and only when calling or returning scalar types. Beyond that, it does absolutely nothing.

This is a little off-topic, but in my opinion, not using strict types makes the code immediately clearer to the intent and easier to read:

<?php

function test(int $number, bool $isEnabled): string {
    if($isEnabled) {
        return $number * 2;
    }

    return $number;
}

echo test(3, false) . "\n";
echo test(3, true) . "\n";
echo test(3, 0);

In the context of reading the body, I am not distracted by typecasting. The return type is an implementation detail I don’t need to concern myself with in multiple places. I digress.

Why is it important to know when strict typing applies? If you are writing a library that takes input and calls outside code, this can cause breaking issues.

For example, if you take a callback and call that callback, if the user isn’t using strict types (and you are) and relies on PHP to handle the type coercion, you’ll crash.

A practical example is DI containers that allow specifying environment variables and passing them to constructors. In Posix, environment variables are always strings if they are set. User code may specify that it takes an int (say, for example, a database port). If you rely on coercion when constructing the user code, your code will “just work” (so long as the string can be coerced), but if you enable strict types, you’ll have to inspect the callback and cast to the appropriate type (basically, implementing your own flavor of coercion, which may be subtly different than PHP’s if you aren’t careful – see previous section).

If you find yourself reimplementing coercion, it’s probably a sign that you shouldn’t be using strict types.

When to use strict types

Don’t get me wrong, my goal isn’t to try and convince you not to use strict types. I aim to persuade you to be careful and educate you in their behavior. Like goto, it can be a helpful tool. Misused, strict types can introduce subtle bugs through liberal casting, calling user code, or may not even apply to the current file (when using value objects).

I hope I dispelled a common misconception that strict types “make the code safer.” To many people, being forced to be explicit can feel safer but introduce new silent error conditions they may not be used to or prepared for handling; like a return type adding nullability and getting cast to a default value when called or changing a parameter from a float to an int. Remember that it’s all about tradeoffs; there’s no free lunch.

Want an inside scoop?

Check out PHP Shenanigans for only €5/mo or €30/yr


Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.