In this publication of my PHP related blog series, I want to address Pure Intersection Types in PHP. In the previous blogs of the series, I addressed different features added to PHP with PHP 8 and PHP 8.1. The first blog was about readonly properties, the second one focused on Enums and the third and so far last one addressed Fibers in PHP.
What are “Pure Intersection Types”
Pure Intersection Types, introduced in PHP 8.1, are a language feature that enables developers to express more precise type constraints for function parameters, return types, and class properties. They allow specifying that a value must belong to multiple types at the same time, rather than just one type as in the case of Union Types.
The syntax for Intersection Types uses the ‘&’ symbol to combine types, for example:
1 2 3 4 | function foo(A&B $arg): void { // doo something with $arg, which is // type of A AND B } |
In this example, the $arg
parameter must be an instance of both classes A
and B
. If it does not meet these requirements, a TypeError
will be thrown.
The addition of Pure Intersection Types to PHP was driven by several key motivations:
- Improved Type Safety: Intersection Types enable developers to express more precise type constraints, reducing the risk of type-related errors at runtime. This results in more robust and reliable code.
- Enhanced Code Readability: With more accurate type information, the code becomes easier to understand and maintain. Developers can quickly grasp the requirements of a function or a class property, leading to increased productivity.
- Better IDE and Tooling Support: Intersection Types help IDEs and other development tools to provide more accurate autocompletion, type checking, and code analysis. This improves the overall development experience and reduces the likelihood of introducing type-related bugs.
- Facilitating Dependency Injection: Intersection Types are particularly useful in scenarios where multiple interfaces need to be implemented by a single object, such as in Dependency Injection (DI) systems. They provide a more concise and expressive way to define such requirements.
Pure Intersection Types may provide developers with a powerful way to express type constraints, making PHP code more reliable and maintainable.
Use Cases for Pure Intersection Types
Of course, there are several use cases. And not all make sense in every situation. But in general, I would advice to not use the feature too much or everywhere. Some use cases are quite useful – especially if you work with public interfaces for others (libraries, for example).
One practical example is with Keestash, where we implemented an EventDispatcher
. Among other things, the EventDispatcher
class calls listener classes for events that got triggered. The IListener
interface is as followings:
1 2 3 4 5 6 7 8 9 | /** * Interface IEvent * @package KSP\Core\Manager\EventManager */ interface IListener { public function execute(IEvent $event): void; } |
As you can see, the execute
method of IListener
class gets an IEvent
object passed as an argument. IEvent
is a loose and abstract event class, which does not hold any values.
However, implementing event classes, have sometimes arguments which are needed in the listener class in order to process the event. But having IEvent
as an argument only, you have to check for the concrete implementation with the instanceof
operator. With Pure Intersection Types, you can do the following:
1 2 3 4 | public function execute(IEvent&ConcreteEvent $event): void { // do something with $event, which is an instance of // IEvent and ConcreteEvent } |
Notice that there are other ways to solve this issue as well. However, this is just a demonstrating example.
“Good to Know”s about Pure Intersection Types
Pure Intersection Types have more important points to consider while working with them:
- No duplicates allowed: A Pure Intersection Type cannot contain duplicate types. For example,
A & A
would be invalid. This is because duplicates would not provide any additional value and may introduce confusion. - No support for mixed type: Intersection Types do not support the
mixed
type, which represents any value. Includingmixed
would essentially make the intersection type redundant since it already covers all possible types. - Covariance and contravariance: When using Pure Intersection Types with inheritance or interface implementation, it is crucial to understand PHP’s rules for covariance and contravariance. Return types are covariant, meaning that a subclass or implementing class can have a more specific return type. On the other hand, parameter types are contravariant, which means that a subclass or implementing class can accept a more general parameter type.
- No support for nullable intersection types: You cannot use the
?
symbol to denote a nullable intersection type directly. To achieve this, you need to use a union type in conjunction with the intersection type, for example:A & B | null
. - No intersection of scalar types: Intersection Types do not support the intersection of scalar types (e.g.,
int & float
). This is because scalar types are mutually exclusive and cannot overlap.
Summary
If you read my blog, you will have noticed that I am not a fan of inheritance, Traits, loosely coupled (smelly) code and everything what may lead to ambiguity. However, being tooooooo strict leads sometimes to overengineered code, which does not make sense (at least not in every case).
Therefore, in contrast to Traits or inheritance, I recommend to evaluate critically if there is not a good solution without using constructs like Pure Intersection Types. If you see yourself doing several workarounds or write boilerplate code, stop doing this and check how to implement features like Pure Intersection Types as clean as possible.