Type System

What is the difference between 1 and "1"? Or between 1 and 1.0? Are there operations that can be performed using 1 but not with true? To answer these questions, we should take a step back and look at how a programming language can handle the type of values that are stored in variables and passed around as arguments.

A type determines what kind of values a variable can have and what operations can be performed on it. PHP supports the four scalar types boolean, integer number, floating-point number, and string as well as the two compound types array and object. Furthermore, PHP knows the special types null, callable, iterable, and resource.

A type system is a set of rules for assigning a type to a variable or argument. These rules as well as how and when they are enforced are one of the key characteristics in which programming languages differ. Common criteria for characterizing type systems are strong typing versus weak typing, static typing versus dynamic typing, and explicit typing versus implicit typing.

Strong Typing versus Weak Typing

C++, C#, Lisp, Java, and Python are strongly typed. This means that they put restrictions on the intermixing of types that is permitted to occur. Consider the Java code shown below:

double d = 1.23;
int i;
i = d;

A Java compiler will not accept the code shown above. It will print an error (see below) and abort the compilation without producing a binary. And without a binary the code cannot be executed.

incompatible types: possible lossy conversion from double to int
 i = d;
     ^

An explicit cast operation must be used to adapt the type of a variable from double to int in Java:

double d = 1.23;
int i;
i = (int) d;

In the example above, we use the “cast to integer” operator, (int), to convert the value stored in d from double to int. This conversion comes with a loss of precision as the value 1.23 cannot be represented exactly as an integer and has to be approximated through rounding. The variable i contains the value 1 (rounded down from 1.23) after the cast operation.

C and PHP are weakly typed. This means that they support implicit casting from one type to another. The code shown below prints the result it does because PHP is weakly typed and we can rely on its implicit casting:

var_dump(1 + '2');
int(3)

If PHP was strongly typed we would have to explicitly cast the string to an integer to get the same result:

var_dump(1 + (int) '2');

Of course, the PHP code shown above also works. It serves as an example that we can use explicit cast operations in PHP. We just do not have to use them thanks to PHP’s support for implicit casting.

Static Typing versus Dynamic Typing

C, C++, C#, and Java are statically typed. They associate a type with a variable name upon declaration and enforce rules for type safety at compile-time. Only values of the type that a variable was declared to hold can be assigned to it. Consider the Java code shown below:

int i;
i = true;

A Java compiler will not accept the code shown above. It will print an error (see below) and abort the compilation without producing a binary.

incompatible types: boolean cannot be converted to int
 i = true;
     ^

Lisp, PHP, and Python are dynamically typed. They associate a type with a value upon creation and, if at all, enforce rules for type safety at runtime. The example shown below is valid PHP code because PHP is dynamically typed and a variable can hold values of different types over the course of its lifecycle:

$variable = 1;
$variable = 'string';

In a dynamically typed language, a variable that holds values of different types over the course of its lifecycle is said to be polymorphic. A variable that only ever holds values of the same type over the course of its lifecycle is said to be monomorphic. In the example above, the second assignment would fail in a statically typed programming language because the variable currently contains a value of type integer which differs from the type of the value that is to be stored now.

Some programming languages leverage a technique called type inference to automatically deduce the type of monomorphic variables. The PHP interpreter itself does not use type inference (various tools do), but it is still a best practice to only use monomorphic variables in PHP code. This makes the code easier to understand, both for human developers as well as for static code analysis tools.

PHP has two operators, == and ===, for comparing values. The equality operator == considers only the value itself while the identity operator === takes both the value and its type into consideration:

var_dump(1 ==  '1');  // Will print bool(true)
var_dump(1 === '1');  // Will print bool(false)
var_dump(1 ==  1.0);  // Will print bool(true)
var_dump(1 === 1.0);  // Will print bool(false)
var_dump(1 ==  true); // Will print bool(true)
var_dump(1 === true); // Will print bool(false)

PHP interprets values as needed based on the context they are used in. This pragmatic approach makes a lot of sense because PHP is mostly used for web development: the web “speaks” HTTP and HTTP is based on strings. The dynamic and weak typing of PHP with its implicit casting reduces the amount of code a developer has to write for processing an HTTP request.

This pragmatism comes at a price, though: errors that can be detected at compile-time with languages that are statically and strongly typed can only be detected at runtime with PHP. An example for this will be given in the next section.

Explicit Typing versus Implicit Typing

C, C++, C#, and Java are explicitly typed and require all variables to have a declared type. Consider the Java code shown below:

public int add(int a, int b)
{
    int result;
    result = a + b;
    return result;
}

In the Java code shown above, the types of all parameters, return values, and local variables are explicitly declared. The compiler uses this information to enforce the rules of Java’s type system at compile-time.

Lisp, PHP, and Python are implicitly typed and do not require type declarations. Consider the PHP code shown below:

public function add($a, $b)
{
    $result = $a + $b;
    return $result;
}

In the PHP code shown above, no type information for parameters, return values, and local variables is available at compile-time. This means that the passing of a parameter of the wrong type to a function or method can only be detected at runtime. And if this is not detected or not handled properly then this can lead to a fatal error.

class Money
{
    public function add($other)
    {
        if ($other->getCurrency() != $this->getCurrency()) {
            // ...
        }

        // ...
    }

    // ...
}

$m = new Money;
$m->add(new stdClass);

Executing the code above leads to the following fatal error:

Fatal error: Call to undefined method stdClass::getCurrency()

In PHP 4, the only way to make sure that an instance of Money was passed would have been a guard clause with an is_a() check:

class Money
{
    public function add($other)
    {
        if (!is_a($other, 'Money')) {
            trigger_error(
                'Passed argument is not a Money object',
                E_USER_ERROR
            );
        }

        // ...
    }

    // ...
}

While executing the code above still lead to a fatal error the message was now more descriptive:

Fatal error: Passed argument is not a Money object

PHP 5 introduced optional type declarations (unfortunately often referred to as “type hints”) for function and method parameters. Classes and interfaces as well as array and callable could already be used in PHP 5 in the signature of a function or method to denote the expected type of a parameter:

class Money
{
    public function add(Money $other)
    {
        // ...
    }

    // ...
}

In the example above, Money $other denotes that a variable of the type Money must be passed as an argument to the add() method. If we pass an argument that is not of the type Money to the add() method then PHP will raise an error at runtime:

$m = new Money;
$m->add(1);
Argument 1 passed to Money::add() must be an instance of Money,
integer given

Technically, the program’s behavior is the same as in the previous PHP 4 example that used the is_a() function, but with less code.

The terms parameter and argument, by the way, are often used interchangeably. Parameter refers to the variable as found in the declaration (or signature) of a function or method while argument refers to the actual input passed.

Due to its dynamic nature, PHP cannot enforce type declarations at compile-time. Instead, they are enforced at runtime and can therefore result in runtime errors.

PHP does not require explicit typing but gives developers the option to declare types in more and more places: parameters in PHP 5, return values in PHP 7.0, and properties in PHP 7.4. This is in line with a trend in programming language design called gradual typing.

We will cover the explicit typing of return values and properties later. Before we get to that, though, we have to look at scalar types in detail.