Four years ago, in 2015, I published an article on the idea of providing extensive type definition files for arbitrary PHP code. Primarily keeping in mind frameworks and libraries. “Definitely Typed PHP” it is. At the time that idea seemed to be the only good way to solve the existing problem of insufficient type definitions capabilities available with phpDoc to describe and document existing code we currently have.
TL;DR
It makes more sense to go ahead with PHP language as it evolves, not against it:
- Don’t use overloaded functions
- Use native PHP language argument and return type hints everywhere
- Refactor overload functions to sets of functions with proper type hints
Strictly Typed PHP
There’s a lot changed since 2015. PHP 7.0 was released, got a few rounds of bug-fixing updates, and was finally stable enough for businesses to consider upgrading. Then 7.1, 7.2, 7.3, and pretty soon we’ll see the official release of PHP 7.4.
PHP 7.x brought so much new stuff to the table that it was a no-brainer for us at Prezly to upgrade our main application. Suddenly we’ve got native language syntax for documenting our code: return types, scalar & nullable types, void, object, and iterable were added.
We've immediately started to use them everywhere — native type-hints were much more reliable compared to phpDoc. Native ones never outdate silently, they are invariants to guarantee that you get exactly what the method is declared to return.
And then the advantages of using type-hints outweighed the unnecessary niceties of overloaded functions and union-type parameters. Our code has gradually become type-strict.
And you know what? We’ve liked it!
The (weak) PHP type-system forced us to simplify our code, making it straightforward and thus less prone to errors. It made us to be disciplined. Yes, the code became more verbose in some cases, but the gained stability made us to not worry about it.
Overloaded Functions
Yes, they’re nice, but almost always they are not worth the hassle.
Every overloaded function can be rewritten to a set of strictly typed functions. One for every use case. As there’s no native language support, we consider them poor practice that gives very little but takes away our confidence about the code.
Let’s see how can we fix one of the examples I’ve been using back then in 2015: the session helper function from the Laravel framework:
/**
* Get Session object
*/
declare function session(): SessionInterface;
/**
* Get specified session value
*/
declare function session(string $key, mixed $default = null): mixed;
/**
* Set specified session values [$key => $value, ... ]
*/
declare function session(array $values): void;
Look, this is not a single function. In fact, there are three different functions merged into a single body. Splitting them will make the code about 417%* more clear and straightforward:
final class Session
{
public static function instance(): SessionInterface
{
// ...
}
public static function getValue(string $key): mixed
{
// ...
}
public static function setValue(string $key, mixed $value): void
{
// ...
}
public static function setValues(array $values): void
{
// ...
}
}
Even though the code has become more verbose, it’s also super clear on what’s going on. Have you
noticed that now we don’t even need phpDoc here to explain what a developer can do with this code?
Method names, input arguments, and return types do clearly communicate every specific method specs
and purpose (unlike the initial implementation of the session()
helper, where you had to either
memorize it or consult the documentation every time).
* — rough estimate based on author’s subjective perception ;)
Union Types
Union-type arguments and return values, just like overloaded functions, can be easily refactored to a set of functions that accept strictly one type of argument, and return strictly one type of value. This is a much better practice, backed by current PHP language capabilities.
When the PHP language will support union types natively — we'll be using that in no time! But not sooner.
Missing Functionality
Obviously, the type-system of PHP 7.x is currently offering is rather limited and is still lacking features to cover the most practical use cases. Especially compared to languages designed with a type-system initially.
Most notably we miss:
- type-hinting array items and keys (i.e.
array<Book>
or array<string,Author>
) - type-hinting generic functions and classes (i.e.
Collection<Book>
) - type-hinting shapes of associative arrays and stdClass POPO objects (i.e.
{ id: int, name: string }
)
But looking at the pace PHP evolves — we'll get there soon.
The Conclusion
Now in 2019, I believe that we don’t really need type definitions files, but a more powerful native type system instead.
A lot of use cases that seemed to justify the existence of .d.php files before can be refactored to simpler, more clear, and more reliable versions of themselves. With proper native type-hints provided for every argument and return value.
Of course, there’s still a long way to go. However, I truly believe the existing native type syntax is helping the PHP community to become better by encouraging us to follow best practices and write cleaner code.
I also highly recommend to watch this kinda related “Extremely defensive PHP” video by Marco Pivetta:
Cheers! 🖖