The Woes of PHP Type Declaration

Tanveer Karim
Feb 10, 2020

Type declarations, sometimes referred to as type hinting, are a valuable feature of PHP. They help to document functions, show developer intent, and detect errors, ensuring a more safer and secure application.

// Declare the required data type in function parameter
function getPersonName(Person $name) {
  ...
}

// Arugment must be a instance of Person
$newPerson = new Person();
getPersonName($newPerson);

/**
* Incorrect argument type will throw an error:
* TypeError: Argument1 passed to getPersonName() 
* must be an instance of Person, 
* instance of Organization given
*/
$newPerson = new Organization();
getPersonName($newPerson);

Type declarations were introduced in PHP 5, but was limited to classes/interfaces, arrays, and callable functions. PHP 7 expanded this list to allow scalar types like integers, floating-point numbers, boolean, and strings. Now these scalar types could enjoy the benefits of type checking and error detection. But there was a catch - by default PHP will not throw an error if a function was called with the incorrect data type argument.

// PHP7 can use scalar type declaration
function getSum(float $first, int $second) {}

// But this will not throw a TypeError, 
// even though the types do not match
getSum(false, 4.55);
Link

A Tale of Two Scalar Type Declaration

With the introduction of scalar type declaration in PHP 7, we were also introduced to two different methods of type declaration - coercive and strict.

Link

Coercive Typing

Coercive typing (the default method), will try to convert values of one scalar type into another scalar type. For example, converting the string "5" into the integer 5. So if your function declared that the argument must be an integer, but you pass a string, PHP will try to convert the string to an integer.

function addValues(int $one, int $two) {
	return $one + $two;
}

// In coercive typing mode, php will try to convert the string into an integer
// So the expected return value should be 10;
echo addValues("5", 5);

One of the benefits of type declaration is, better error detection. It prevents you from passing incorrect data types to a function, and makes your program more predictable. Unfortunately because of type coercion, you lose this additional error checks, and instead PHP will try to fix the error for you.

While type coercion may be useful when dealing with legacy code, it can end up causing unintended side effect in your application.

Take the following hypothetical example:

We have a function that performs some math calculation. Both parameter require a float value.

function calculateInterest(float $apr, float $amount) {
	return $amount * ($apr / 100);
}

// 2100.5 * (4.375 / 100)
// = 91.896875
echo calculateInterest(4.375, 2100.50);

Due to some refactoring, we mistakenly change one of the type declaration from a float to int.

function calculateInterest(int $apr, float $amount) {
	return $amount * ($apr / 100);
}

// Floating-point value of 4.375 will be "coerced" 
// into an integer and our calculation is no longer accurate:
// 2100.5 * (4 / 100)
// = 84.02 != 91.896875
echo calculateInterest(4.375, 2100.50);

Unfortunately, because of coercive typing, the regression will be ignored and our application will calculate the wrong value. If we were dealing with money, this could be a costly mistake.

Link

Strict Typing

Luckily, PHP 7 provided a way to prevent this type of issue, via strict typing. When strict typing is enabled, PHP will validate scalar data types and if it finds that the wrong argument type was provided, it will throw a TypeError.

Strict typing is enabled on a per file basis, by adding a declare directive at the top of the script.

<?php
declare(strict_types=1);

function calculateInterest(int $apr, float $amount) {
	return $amount * ($apr / 100);
}

// Error: Uncaught TypeError: Argument 1 passed to
// calculateInterest() must be of the type integer, float given
echo calculateInterest(4.375, 2100.50);

There are some caveats. You cannot enable strict typing globally. Additionally, strict typing only applies to the function call made within the file which contains the directive. If a function was called from a file without the strict typing directive, it will fall back to coercive typing.

<?php
declare(strict_types=1);
// Will use strict typing
calculateInterest(4.5, 200.54);
<?php
// Will NOT use strict typing
// Typing will be coerced
calculateInterest(4.5, 200.54);
Link

Why the dual typing methods?

So why have coercive typing as the default and offer strict typing as opt-int? Wouldn't strict typing by default mean a safer type system?

The answer gets a little complicated due to PHP being a weakly type language and having to support legacy code. In fact, there have been a lot of back and forth between supporters of both strict and weak/coercive typing. A detailed explanation can be found here: https://wiki.php.net/rfc/scalar_type_hints_v5#why_both

A short summary of why it was decided to use a dual typing system:

  • Opt-in strict typing allows users to choose what's best for them
  • APIs do not decide type declaration for the end users
  • Coercive typing by default will not break existing code bases
  • One syntax for scalar type declaration
  • Code bases can share both weak and strict typing