What is PHP's declare(strict_types=1); and why you should use it

Jul 4, 2024·
Gert de Pagter
Gert de Pagter
· 5 min read

You may have had people tell you that you should use declare(strict_types=1); in your PHP files, but what exactly does it do, and why should (or shouldn’t) you use it?

History

A long, long time ago PHP didn’t have scalar types, or even return types. In PHP 7.0 both of these got introduced. In 5.6 and before we had to use annotations, and couldn’t add it in the language. (If you still see code like this it’s probably from that era.)

/** 
 * @param string $input
 * @return string
 **/
function foo($input) {
    var_dump($input);
    return $input;
}

Nowadays we can use scalar types, return types, property types. We even have unions and intersection types now. Which means we can write the same code from before like so.

function foo(string $input): string {
    var_dump($input);
    return $input;
}

Using declare(strict_types=1)

But what do you think happens if we then call this method with an integer, or a boolean like so? Will this give us a TypeError, will it cast the type or will it stay the original type?

foo(3);
foo(2.5);
foo(true);

To determine what happens we need to look at the top of the file that is doing the calls. If we see declare(strict_types=1), then a TypeError will be thrown when we run this code. If you see nothing related to strict_types or declare(strict_types=0); Then the type will be cast, and we would get the following output:

string(1) "3"
string(3) "2.5"
string(1) "1"

Why you should use declare strict types

Unintended type casts can result in unexpected behavior. If we have a function that takes a boolean, but we pass it a string, any truthy string will become true. So if you get user input from a JSON POST for example, and you pass the incorrectly typed data, it will always be interpreted as true, and may cause hard to uncover bugs.

However, if you use declare(strict_types=1); everywhere in your code base, you instead have a TypeError being thrown, causing execution of your code to stop, and (hopefully) your monitoring going off. So while it may break your website for a bit, it will help you figure out the issue much faster and reduce weird behaviour.

When something breaks you want it to break loudly. It’s generally better to have an exception or error being thrown when unexpected issues arise, rather than ignore it and continue your program, even if everything is going wrong. Because before you know it, you have wrong data in the database, or even emails being sent to customers when it wasn’t intended.

For example, we had an older part of the code base that handled the login flow. The login method looked something like this, and had no types as it was originally written for PHP 5.X. Now the types here are actually wrong, for password a string (namely the password itself), was provided if there was one, and false was passed if it was a login without a password (a log in with google button for example).

/**
 *
 * @param string $method
 * @param string $userName
 * @param bool $password
 */
function login($method, $userName, $password = false)

At some point we had to do some refactors in this part of the codebase, and decided to add types. And suddenly, no one could log in anymore with a password. Any password they passed was just cast to true, and then later when that was checked with password_verify, the 2 passwords didn’t match.

function login(string $method, string $userName, bool $password = false)

If we had declare(strict_types=1); at the top of this file, we would’ve had a TypeError. This does mean customers would’ve seen an Oops something went wrong! type of screen. It also would’ve meant our alerting would’ve gone off, and the change would’ve been rolled back, while we looked into the issue. With a stack trace it would’ve been easy to spot where this went wrong, and we could have updated the type like so. But because we didn’t have that, we had to spend hours debugging why customers reported not being able to log in.

function login(string $method, string $userName, bool|string $password = false)

Caveats of strict_types in PHP

As I mentioned earlier, you have to look at where the code is calling from.

Let’s say we have 2 files, functions.php and index.php, where one has declare(strict_types=1), and the other doesn’t. In this case, no TypeError is thrown, as the code that is executing the takesString, isn’t using strict_types.

<?php
//functions.php
declare(strict_types=1);

function takesString(string $input) {}
<?php
//index.php

require_once  __DIR__ . '/functions.php'
takesString(3); // no error

But if we now turn this around and make index.php strictly typed, then we get a TypeError, this is because the code that is executing takesString is now in strict_type mode.

<?php
//functions.php

function takesString(string $input) {}
<?php
//index.php
declare(strict_types=1);

require_once  __DIR__ . '/functions.php'
takesString(3); // throws a TypeError

How to use declare(strict_types=1) in PHP

If you decide to use strict_types in PHP, then I would advise to use it everywhere in your code base. When you are using tools like php-cs-fixer or php code sniffer, then you can add checks to make it required and add it to the whole code base at once. That may not always be possible for a legacy project. In that case you could use a tool like PHPStan to make it required, and add all files where you currently can’t add strict types to the baseline. See this rule on how to do that.

If you want to get notified of the next blog post, join the newsletter.

Gert de Pagter
Authors
Software Engineer
My interests include software development and math.