Type declarations in PHP
Marcos Marcolin • September 18, 2023 • 12 min read
phpUpdated in February 2025, covering changes up to PHP 8.4.
PHP has historically been known for its dynamic typing, often associated with so-called weak typing. For many years, this was used as an argument to criticize the language, especially when compared to statically typed languages.
However, that view doesn't keep up with PHP's evolution over the last few versions. Since PHP 7, the type system has undergone a significant transformation, making the language much safer, more predictable, and better suited to medium and large applications.
In recent weeks, there's been intense debate about the importance of data typing in modern applications, and that's exactly the topic this article covers. Here, we'll explore how to declare types in PHP and how this system has evolved across versions.
It's worth noting that I don't intend to get into the discussion of whether typing is the best approach in every scenario. Personally, I'm an advocate for typing whenever possible, although I understand that its absence can be justified, especially when dealing with legacy code.
In this article, we'll focus only on the PHP versions that introduced significant changes related to data type handling, omitting other improvements shipped in each version, so we can keep our focus on data typing.
This post may not cover every aspect of types, but I'm confident it covers most of them.
What are data types?
In short, data types are classifications that determine what a value can represent. For example, an integer type can represent whole numbers, while a string type can represent text.
Data types are useful for several reasons:
- They let the language's compiler or interpreter check whether the code is valid. For example, the compiler won't allow an integer variable to be assigned a text value.
- They help ensure the code is efficient. The compiler or interpreter can optimize the code based on the data type of variables.
- They make code easier to read and understand. Programmers can use data types to understand what a variable represents.
Let's take a brief look at PHP's history and explore the types it supports.
If you're interested in going deeper into PHP types, including at more advanced levels, I've included a talk given by a Core Developer in this article's references.
PHP 4.0 (2000)
Unfortunately, I never had the chance to work with PHP version 4.
However, after doing some research, I found that this version showed no sign of data typing (other than resource) in userland, being fully dynamic and relying on PHP's internal engine to determine the data types used based on their value.
The resource type is a special data type that represents an external resource, such as a database connection, a file, or a socket. Resources are created by functions specific to each PHP extension.
A classic example is creating a file:
$fp = fopen('file.txt', 'w');
var_dump($fp); // resource(2) of type (stream)
It's worth noting that version 4 was marked by the introduction of a rewrite of a fundamental part of the language's core, with the inclusion of the Zend Engine. That's when important changes happened that made possible significant improvements in later PHP versions.
PHP 5.0 (2004)
At this point, things got more serious with the introduction of Type Hinting. Now, you could explicitly say what data type you expected in function arguments.
But don't get too excited, dear reader. It wasn't like today (2023); back then, only object instances were accepted, meaning class/interface, plus the use of self.
See an example below.
class Car
{
protected $brand;
public function __construct(Brand $brand)
{
$this->brand = $brand;
}
}
class Brand {}
class Driver {}
new Car(new Brand()); // Ok
new Car(new Driver()); // Error Argument 1 passed to Car::__construct() must be an instance of Brand, instance of Driver given
Basically, Car's constructor expects an object of type Brand. When it isn't provided, an error is thrown.
PHP 5.1 (2005)
Now we had a new toy in the box: the array type.
In PHP, array is a data structure that stores a collection of values, such as numbers, strings, or other data types, in a single variable.
function myFunction (array $names)
{
foreach($names as $name){
echo $name . PHP_EOL;
}
}
myFunction(array('Marcos', 'Marcolin')); // Marcos Marcolin
myFunction('Marcos'); // Error Argument 1 passed to myFunction() must be an array, string given
PHP 5.4 (2012)
A few years later, the callable type was added.
In PHP, callable is a special data type that represents a reference to something that can be called like a function or method.
function apply(callable $callback, $value1, $value2)
{
return $callback($value1, $value2);
}
function sum($a, $b)
{
return $a + $b;
}
function subtract($a, $b)
{
return $a - $b;
}
$resultSum = apply('sum', 10, 2); // 12
$resultSubtraction = apply('subtract', 10, 2); // 8
$resultMultiplication= apply('multiplication', 10, 2); // Error Argument 1 passed to apply() must be callable, string given \ FATAL ERROR Call to undefined function multiplication()
Obviously, if you pass a callback that doesn't exist, it results in a fatal error.
PHP 7.0 (2015)
We finally reach PHP version 7.0, which gained notoriety due to significant optimizations made in the language's core, along with several improvements.
However, our focus here is on the aspects related to types.
First, it's worth noting that the terminology changed. It's no longer Type Hinting, but rather Type Declarations.
Additionally, scalar type declarations were introduced, covering strings (string), integers (int), floating-point numbers (float), and booleans (bool). This change represented a notable evolution, consolidating a stricter approach to type handling in the language.
See some examples below.
string
function greet(string $name)
{
return "Hello, $name!";
}
echo greet('Marcos'); // Hello, Marcos!
int
function calculateAge(int $birthYear)
{
return date('Y') - $birthYear;
}
echo calculateAge(1994); // 29
echo calculateAge('1994'); // 29
float
function calculateTotalPrice(float $unitPrice, int $quantity)
{
return $unitPrice * $quantity;
}
echo calculateTotalPrice(12.5, 5); // 62.5
bool
function printStatus(bool $status)
{
return $status ? 'Enabled' : 'Disabled';
}
echo printStatus(true); // Enabled
echo printStatus(false); // Disabled
And of course, we can't skip mentioning return type declarations, which complement type declarations on arguments.
This means we now have the ability to specify the data type a function will return. Let's reuse two earlier examples to illustrate this.
stringreturn type
function greet(string $name): string
{
return "Hello, $name!";
}
echo greet('Marcos'); // Hello, Marcos!
intreturn type
function calculateAge(int $birthYear): int
{
return date('Y') - $birthYear;
}
echo calculateAge(1994); // 29
This version also introduced support for Strict Types, complementing coercive types.
But what does that mean? In PHP, the declare(strict_types=1); statement is used to enable strict type mode. This mode means implicit conversions between data types aren't allowed, ensuring rigorous checking of argument types and function return values. This helps avoid unexpected behavior and type-related bugs in your code.
To enable strict type mode in a PHP file, you can add the declare(strict_types=1); statement at the top of the file.
<?php
declare(strict_types=1);
function calculateAge(int $birthYear)
{
return date('Y') - $birthYear;
}
echo calculateAge(1994); // 29
echo calculateAge('1994'); // FATAL ERROR Uncaught TypeError: Argument 1 passed to calculateAge() must be of the type integer, string given
Did you notice that in the previous example, passing a string output resulted in 29? Now, with strict types in use, validation becomes stricter, leading to a fatal error, and that's highly beneficial.
PHP 7.1 (2016)
In PHP 7.1, two important new features were introduced: Nullable Types and the Iterable type, along with void return type.
- Nullable Types
Nullable types in PHP refer to the ability to declare that a variable or function parameter can accept a value of a specific type or the value null. This lets you explicitly indicate that a variable might not have a defined value.
This feature was introduced through the ? operator before the data type, indicating that the variable can be null. For example, ?string means the variable can be a string or null. This addition to PHP provides a more precise way of handling variables that may or may not have a valid value.
function greet(?string $name)
{
if ($name === null) {
echo 'Hello, anonymous!';
} else {
echo "Hello, $name!";
}
}
greet(null); // Hello, anonymous!
greet('Marcos'); // Hello, Marcos!
greet(); // FATAL ERROR Uncaught ArgumentCountError: Too few arguments to function greet(), 0 passed
- Iterable
The Iterable type refers to any value that can be iterated over through a foreach() loop, and can be used as a data type for function arguments and return values. This means variables declared with the Iterable type can hold data collections that can easily be traversed and processed using the foreach() loop construct.
function sumValues(Iterable $numbers)
{
$sum = 0;
foreach ($numbers as $number) {
$sum += $number;
}
return $sum;
}
$array = [1, 2, 3, 4, 5];
$result = sumValues($array);
echo "Sum: $result"; // Sum: 15
voidreturn type
The void return type in PHP is a declaration you can use when defining a function to indicate that this function returns no value.
This declaration is useful when a function has desired side effects but doesn't need to return any specific data as a result of its execution.
function greetUser(string $name): void
{
echo "Hello, $name!";
}
greetUser('Marcos'); // Hello, Marcos!
$greet = greetUser('Marcolin'); // FATAL ERROR Uncaught TypeError: Return value of greetUser() must be an instance of void, none returned
PHP 7.2 (2017)
This version introduced support for object as a function parameter or return value.
To use it, just specify the object keyword before the parameter type or return value.
function getClassName(object $obj): string
{
return get_class($obj);
}
var_dump(getClassName(new stdClass())); // string(8) "stdClass"
var_dump(getClassName('class')); // FATAL ERROR Uncaught TypeError: Argument 1 passed to getClassName() must be an object, string given
function getObject(): object
{
return json_decode('{}');
}
var_dump(getObject()); // object(stdClass)#1 (0) { }
PHP 7.4 (2019)
PHP version 7.4 brought support for Typed properties, allowing type declarations on class properties.
Typed properties let you specify the data type of a class's properties. This means you can explicitly declare what data type a class property can hold. Typed properties are particularly useful for enforcing data type constraints and improving code clarity, ensuring a class's properties only contain the expected data types.
class Person
{
public string $name;
public int $age;
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
}
$person = new Person('Marcos', 29); // Ok
$person2 = new Person('Marcolin', '29'); // Ok
With declare(strict_types=1); in use, the output will be affected, since this enables strict type mode in PHP.
declare(strict_types=1);
// code...
$person = new Person('Marcos', 29); // Ok
$person2 = new Person('Marcolin', '29'); // FATAL ERROR Uncaught TypeError: Argument 2 passed to Person::__construct() must be of the type int, string given,
It's important to note that typed properties aren't limited to scalar data alone. You can use typed properties with a wide range of data types, spanning from scalar data to objects, arrays, and other complex types.
class Address
{
public string $street;
public string $city;
public string $zipCode;
public function __construct(string $street, string $city, string $zipCode)
{
$this->street = $street;
$this->city = $city;
$this->zipCode = $zipCode;
}
}
class Person
{
public string $name;
public int $age;
public Address $address;
public function __construct(string $name, int $age, Address $address)
{
$this->name = $name;
$this->age = $age;
$this->address = $address;
}
}
$address = new Address('PHP Street', 'Chapecó', '12345-678');
$person = new Person('Marcos', 28, $address);
PHP 8.0 (2020)
PHP version 8.0 brought a series of improvements and new features that pleasantly surprised language enthusiasts and its vast community, including the introduction of the JIT (Just-In-Time compiler) and other features.
As far as types go, one of the notable additions was support for Union Types, which I personally love.
Union types let you declare that a parameter, return value, or class property can accept values of two or more different types. Types are specified separated by a vertical bar character |, better known as pipe.
function printValue(string|int $value)
{
echo "The value is: $value\n";
}
printValue('Hello, World!'); // The value is: Hello, World!
printValue(2023); // The value is: 2023
printValue(); //FATAL ERROR Uncaught ArgumentCountError: Too few arguments to function printValue(), 0
Now, let's apply Union Types to a function's return value:
function generateValue(bool $useString): string|int
{
if ($useString) {
return "Hello, World!";
} else {
return 2023;
}
}
echo generateValue(true); // Hello, World!
echo generateValue(false); // Hello, 2023!
Additionally, the mixed pseudo-type was introduced, representing a parameter, return, or property that can take on any value type.
mixed is equivalent to the union of types string|int|float|bool|null|array|object|callable|resource.
class Example
{
public mixed $exampleProperty;
public function foo(mixed $foo): mixed {}
}
You're not allowed to cast a variable to the mixed type, since it isn't a real type.
$foo = (mixed) $bar; // FATAL ERROR syntax error
The mixed type also can't be used in type unions.
function foo (mixed|FooClass $bar): int|mixed {} // Fatal error: Type mixed can only be used as a standalone type
PHP 8.1 (2021)
PHP version 8.1 brought Intersection Types and the never return type.
- Intersection Types
Complementing the feature introduced in version 8.0, it's now possible to declare a type for a method's parameter, property, or return that must belong to all the class/interface types declared in the intersection. This is the opposite of Union Types, which allow any of the declared types, giving greater control over the acceptable types in a given context.
function login(User & CanLogin $user): void
{
// code
}
Only values that are instances of the User class and implement the CanLogin interface can be passed to this function.
Now, regarding returns:
class User
{
public User & CanLogin $currentUser;
public function getLoggedInUser(): User & CanLogin
{
return $this->currentUser;
}
}
The currentUser property must be an instance of the User class and implement the CanLogin interface. The getLoggedInUser() method returns an instance of the User class that implements the CanLogin interface.
neverreturn type
The never return type is a feature that lets you declare a function or method that will never return a value. This is useful for functions meant to always throw an exception or terminate script execution, ensuring no return value is ever produced.
function redirect(string $uri): never
{
header('Location: ' . $uri);
exit();
}
It's important to remember that the never return type indicates the function shouldn't return a value and must terminate abruptly, for example by throwing an exception or using the exit function. Any attempt to return a value other than that will result in a fatal error.
function dispatch(string $message): never
{
echo $message;
}
dispatch('test'); // FATAL ERROR Uncaught TypeError: dispatch(): never-returning function must not implicitly return
function foo(): never
{
return;
}
foo(); // FATAL ERROR A never-returning function must not return
PHP 8.2 (2022)
The inclusion of standalone types, which cover null, true, and false, in PHP 8.2, lets developers declare that a parameter, property, or method return can take on one of those values.
In short, this means the return will always match the chosen definition.
Before PHP 8.2, functions were defined this way:
function alwaysFalse(): bool
{
return false;
}
Now, we have the ability to write it like this:
function alwaysFalse(): false
{
return false;
}
The same principle also applies to null and true.
While this addition might not be particularly exciting for some, it enriches the language with extra features that can be useful in various development scenarios.
- Disjunctive Normal Form Types
Another addition in this version is Disjunctive Normal Form Types, a new way to declare data types that lets you combine union and intersection types.
With the introduction of union types in PHP 8.0, it became possible to declare that a value could be of one or more types. With the introduction of intersection types in PHP 8.1, it became possible to declare that a value had to belong to all the specified types.
DNF Types let you combine these two ways of declaring types. For example, the type string|int|object can be declared as (string&object)|(int&object), and can be used anywhere data types are accepted, such as parameters, function returns, and properties.
function foo(int|string | (DateTime & bool) $bar)
{
// code
}
The value of $bar must be an integer, a string, OR an instance of the DateTime class that is also a boolean.
class Storage
{
private (ArrayAccess&Countable)|null $container;
public function getContainer(): (ArrayAccess & Countable) | null
{
return $this->container;
}
public function setContainer((ArrayAccess & Countable) | null $container): void
{
$this->container = $container;
}
}
In this case, (ArrayAccess & Countable) | null is the DNF type. The $container parameter can be an instance of a class that implements the ArrayAccess and Countable interfaces, or a null value.
PHP 8.3 (2023)
PHP version 8.3, expected in November 2023, will include a feature called Typed class constants.
The Typed class constants feature is a significant addition that lets you declare a type for class constants. This ensures type compatibility of constants, even when child classes or interface implementations override them.
To declare a class constant with a type, use the following syntax:
class Foo
{
public const string A = 'a';
public const int B = 1;
public const float C = 1.1;
public const array E = ['a', 'b'];
}
PHP 8.4 (2024)
PHP version 8.4 brought an important adjustment related to type declarations on function parameters.
Up to earlier versions, it was possible to declare a parameter with a default value of null without explicitly stating that it accepted null in its type, as in the example below:
function formatName(string $name = null): string
{
return strtoupper($name);
}
While functional, this approach was inconsistent, since the declared type (string) didn't correctly reflect the null value allowed in the signature.
Starting with PHP 8.4, this behavior started generating a deprecation warning. Now, you must explicitly declare that the parameter can be null, using ? or union types.
Here's the correct way to do it:
function formatName(?string $name = null): string
{
return strtoupper($name ?? 'anonymous');
}
// Or
function formatName(string|null $name = null): string
{
return strtoupper($name ?? 'anonymous');
}
Typing beyond the language: Static analysis with PHPStan
Even with all the evolution of PHP's native type system, it's still possible to raise the level of safety further using static analysis tools like PHPStan.
PHPStan analyzes code without running it and can identify:
- Type inconsistencies
- Calls to nonexistent methods
- Incorrect use of null
- Issues in generics via PHPDoc
- Dead or unreachable code
With higher analysis levels, it's possible to reach a rigor comparable to statically typed languages, even in legacy projects.
Conclusions
The continuous evolution of the PHP language over time is fundamental and expected by the community. The introduction of type-related features plays a crucial role in that evolution. Using types offers significant benefits, such as greater safety in data validation, which in turn makes it easier to develop and maintain code that's more agile and functional.
I'm an advocate of using types whenever possible. When applying them to legacy systems, it's common for inconsistencies to surface, but that isn't a problem with typing itself, it's a reflection of implicit contracts that weren't clear before.
For new code, declaring types is a preventive measure against bugs and unexpected behavior, and it also raises the project's overall quality level.
Considering the evolution up to PHP 8.5, claiming that PHP's problem is the lack of typing doesn't match the language's current reality.
Thanks for reading this far, see you next time!