Stop Using Pseudo-Types
(Published on Jan 20, 2025 - Version française)
In 2011, with the release of PHP 5.4, the pseudo-type callable
was introduced via the RFC: Callable.
In 2016, another pseudo-type, iterable
, was added in PHP 7.1.0 through the RFC: Iterable.
Pseudo-types are not true types like string
or array
.
They are sets of types with specific validation logic.
Let’s look at the callable
pseudo-type for a better understanding.
The callable
Pseudo-Type
In PHP 4, the call_user_func()
function was introduced to call a function by its name as a string.
There was also a call_user_method()
function, but it was quickly deprecated in PHP 4.1 and removed in PHP 7.0 because call_user_func()
could also accept an array where the first element was an object, and the second was the method name.
<?php
$format = [new Datetime('2025-01-30'), 'format'];
echo call_user_func($format, 'd/m/Y');
// Output: 30/01/2025
PHP 4.0.6 introduced the is_callable()
function, which checks if a parameter is callable.
In fact, a string or an array can be callable, but only under certain conditions.
<?php
$create = 'Datetime::createFromFormat';
$invoke = [new stdClass, '__invoke'];
var_dump(is_callable($create), is_callable($invoke));
// Output:
// bool(true)
// bool(false)
I suggest reading the article by my friend Damien Seguy: How to call a method in PHP.
For a string to be callable, it must contain the name of an existing function or method.
For example, trim
is callable, but unknown
will not be if no such function exists.
Note that certain names are not callable, such as isset
, empty
, echo
, or include
.
exit
and die
were not callable before PHP 8.4 but are now callable: PHP RFC: Transform exit() from a language construct into a standard function.
An interesting read: A Look At PHP’s isset().
In short, at runtime, you cannot always determine whether a string is callable.
In March 2012, with the release of PHP 5.4, the logic of the is_callable()
function was extended to introduce the callable
pseudo-type.
This is why it is called a pseudo-type: callable
is not a type; it is a union of the types Closure
, string
, and array
with logic to verify, at runtime, whether a parameter is callable.
At compile time, unless it’s a Closure
, you cannot know if a function or class will exist during validation.
This is also why it is not possible to use callable
for typed properties: PHP RFC: Typed Properties 2.0, Supported Types.
The (Not So) Pseudo-Type iterable
In PHP 7.1, the iterable
pseudo-type was formally introduced.
It allowed validation of whether a value was iterable by checking if it implemented the type array
or the interface Traversable
.
With the release of PHP 8.2, iterable
lost its pseudo-type status and became a union type: Traversable|array
.
To better understand this difference, observe the behavior of the following code in PHP 8.1 and 8.2:
<?php
function foo(): iterable {
return [];
}
function bar(): iterable|bool {
return [];
}
echo (new ReflectionFunction('foo'))->getReturnType()->__toString();
echo ", ";
echo (new ReflectionFunction('bar'))->getReturnType()->__toString();
- PHP 8.1.0:
iterable, iterable|bool
- PHP 8.2.0:
iterable, Traversable|array|bool
This internal change was introduced by the RFC: Make the iterator_*() family accept all iterables.
However, the Traversable
interface is somewhat unique and could almost be considered a pseudo-type, as it is an interface that cannot be implemented directly but is extended by the Iterator
and IteratorAggregate
interfaces.
In fact, the iterable
type should have been a union of types: Iterator|IteratorAggregate|array
.
Data Typing with iterable
The iterable
type should not be used to type a function or method return, as this can create confusion if the data is misused.
For example, developers might be tempted to manipulate the returned value upon discovering it is an array.
They might use count
to check if there are values to display alternative text.
However, this type could evolve later to return an iterator.
Example:
<?php
class A {
public function get(): iterable {
return [17];
}
}
class B extends A {
public function get(): iterable {
yield 42;
}
}
echo (new A)->get()[0]; // 17
echo (new B)->get()[0]; // Fatal error
This is a blatant mistake, but a good static analyzer like PHPStan or Exakat should catch this. Just keep in mind that developers make mistakes, especially when they're starting out.
To handle this variation in results correctly, you need to check whether the returned data is an array (is_array
) or an iterator (instanceof Iterator
), or use a function like iterator_to_array()
:
<?php
// ...
echo iterator_to_array((new B)->get())[0];
This solution works but can impact performance, especially if a Generator is used to optimize memory usage for large results.
A good Defensive Programming approach is to limit the possibilities by offering either an array or an iterator.
Personally, I tend to prefer iterators via generators.
When typing parameters in a function or method, using iterable
indicates that your implementation will only iterate over the data.
Thus, you can pass either an array or an iterator interchangeably, improving the Developer Experience (DX).
Data Typing with callable
While the use of iterable
is disputable, callable
is less so.
This is partly because it cannot be used to type a property and partly because it allows developers to define the call as a string, which creates numerous problems for static analysis or even just for searching the usage of a method, for instance.
Even though IDEs have made significant progress, it can be challenging for them to find function usages when the function is defined in a variable as a string.
The introduction of first-class callables simplifies callback handling. You no longer have an excuse to define your callbacks like this:
<?php
$data = array_map(trim(...), [' x', 'z ']);
It is therefore better to use the Closure
class when defining callbacks.
If you are using a version prior to PHP 8.1 and cannot use first-class callables, I suggest using Closure::fromCallable()
:
<?php
$data = array_map(
Closure::fromCallable('trim'),
[' x', 'z ']
);
Typing Properties with Closures
One detail when using properties of type Closure
is that you cannot call the closure like this:
<?php
class A {
public function __construct(
private Closure $callback
) {}
public function show(): void {
$this->callback();
}
}
This generates an error: Fatal error: Uncaught Error: Call to undefined method A::callback()
.
This makes sense since it is difficult to distinguish between a method call and an invokable property.
You must call the property like this: ($this->callback)();
(Try this code).
You can also use: call_user_func($this->callback);
(Try this code).
Another interesting approach is that the Closure
class provides an __invoke
method.
You can write: $this->callback->__invoke();
(Try this code).
I find this syntax appealing because it is very explicit: “We are invoking the closure.”
Conclusion
Pseudo-types like callable
and iterable
may seem convenient at first glance, but they introduce ambiguities and make code harder to analyze.
Favoring clear and explicit types not only improves code readability but also reduces the risk of errors.
For example, replacing callable
with Closure
makes it easier to search for methods within a project and optimizes compiler performance.
Similarly, using generators instead of a simple iterable
type allows for better handling of large datasets while ensuring a better Developer Experience.
Additionally, if you use static analyzers like PHPStan (and you should if you don’t), complement your Closure
types with as much information as possible: callable typehint.
In summary, avoid pseudo-types whenever possible.
Using precise types strengthens code robustness and maximizes the potential of modern tools like PHPStan.
This will help you produce more maintainable and efficient code — an objective every developer should strive for.
Follow me on Bluesky: @bouchery.fr