We should implement .d.ts for PHP. Really.
Everything started with this basic idea. An ability to describe PHP classes and functions with special support files like TypeScript Definition files. This article explores this idea for solving common PHP development problems.
The Why
Laravel's Magic
Laravel is great at being simple. But that simplicity comes at a cost. It uses lots and lots of magic methods and overloaded functions.
I use PhpStorm as my primary IDE and like it a lot. The IDE has a strong autocompletion engine that helps to write code faster and make fewer mistakes. It’s understandable, that the engine cannot analyze magic methods. So the typical Laravel controller looks like this:
Note the methods with olive backgrounds: findOrFail()
and where()
. IDE does not recognize those
methods—they do not exist. They are magic. That’s why we have IDE warnings and no autocompletion.
IDE Plugins
You can still keep coding, ignoring this problem. It’s ok!
Though it's far from being anyway near the "convenient". I find these code warnings very distracting.
There is a Laravel plugin for PhpStorm that solves some of the problems, for now, it looks like it’s not adapted for Laravel 5 yet (Laravel 5 was officially released on Feb 4 2015 – 25 days ago at the time of writing this article).
The problems with IDE plugins are:
- The community has to support plugins for each IDE.
- Need to know how to develop IDE plugins, which almost always requires skills not related to your main skillset (i.e. to develop a PhpStorm PHP-specific plugin one needs to know Java).
- Need to support plugins for different framework versions.
- The delay between the framework rolling out new features, and plugin development falling behind.
Typescript Definition Files
I was thinking about these problems for some time and then had one of those aha moments! The JavaScript community has the same problems and even more! All IDEs have a really hard time trying to make sense of JavaScript code and define return types to suggest some auto-completion options. And I know one great approach to fight this. TypeScript definition files!
The JavaScript community has created a definition files repository to support IDEs with vital information for the autocompletion engine: DefinitelyTyped. All you need is to download (or install via the package manager) the definition file and link it to your typescript file explicitly:
/// <reference path=”jquery/jquery.d.ts” />
Also, an IDE can use .d.ts internally. It can automatically download .d.ts
files for libraries being
used in the project and link them implicitly. Wouldn’t that be great?!
Definition files solve all the problems of framework autocompletion plugins:
- Files are IDE-independent. All is needed is to implement that files support for IDEs. Once for each IDE.
- No need to know Java/C++/ObjectiveC/any other language to implement auto-completion for any PHP
- The community can easily support plugins for each framework version
- Framework community can bundle up-to-date definition files with a framework!
Examples
Sample definition from DefinitelyTyped.org site (overloaded function in javascript):
declare function duration(): number;
declare function duration(value: number): void;
Another example, css()
function from the JQuery definition file:
declare interface JQuery {
// ...
/**
* Get the value of style properties for the first element
* in the set of matched elements.
*
* @param propertyName A CSS property.
*/
css(propertyName: string): string;
/**
* Set one or more CSS properties for the set of matched elements.
*
* @param propertyName A CSS property name.
* @param value A value to set for the property.
*/
css(propertyName: string, value: string | number): JQuery;
/**
* Set one or more CSS properties for the set of matched elements.
*
* @param propertyName A CSS property name.
* @param value A function returning the value to set.
* this is the current element. Receives the index
* position of the element in the set and the old
* value as arguments.
*/
css(propertyName: string, value: (index: number, value: string) => string | number): JQuery;
/**
* Set one or more CSS properties for the set of matched elements.
*
* @param properties An object of property-value pairs to set.
*/
css(properties: Object): JQuery;
// ...
}
It is a curious detail to note that jQuery itself entirely consists of overloaded helper functions.
PHP Definition Files 💡
What if we could use the same approach for PHP?! These definition files can solve some problems with PHP coding:
- Magic methods hinting
- Overloaded functions hinting
- Application-specific service container keys hinting
Magic Methods
Let’s take the first example with Laravel’s Eloquent model class. Its definition could look like this (a little simplified):
class Question
{
public string $title;
public string $body;
// and so on ...
public static find(int|string $id): Question;
public static find(array $ids): Collection;
// and so on ...
}
Overloaded Functions
Though the main cause of thinking of PHP Definition Files was to hint magic methods, it also can help with overloaded functions (like TypeScript Definition does)!
Think of the PHP's parse_url()
function. This is how it's described in the php.net docs:
mixed parse_url ( string $url [, int $component = -1 ] )
If the $component parameter is omitted, an associative array is returned.
If the $component parameter is specified, parse_url() returns a string (or an integer, in the case of PHP_URL_PORT) instead of an array. If the requested component doesn’t exist within the given URL, NULL will be returned.
Currently, even phpDoc cannot describe it. The most you can do is to set the return type to mixed. But we can document that behavior using a special definition file:
declare function parse_url(string $url): string[];
// Note: constant value matching
declare function parse_url(string $url, PHP_URL_PORT): int | null;
declare function parse_url(string $url, int $component): string | null;
Another example. The session helper from the Laravel framework:
/**
* Get / set the specified session value.
*
* If an array is passed as the key,
* we will assume you want to set an array of values.
*
* @param array|string $key
* @param mixed $default
* @return mixed
*/
function session($key = null, $default = null)
{
if (is_null($key)) return app('session');
if (is_array($key)) return app('session')->put($key);
return app('session')->get($key, $default);
}
The definition for this function could look something like this:
/**
* 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, ... ]
* @param array<string,mixed> $values
*/
declare function session(array $values): void;
Service Container Hints
Today is the era of Service Containers and Dependency Injection in PHP. Every modern framework uses some kind of Service Container implementation. Every time we use a method of some service we get an IDE warning.
$container->get("session")->set("user_id", $user_id)
But to make the IDE happy with this code we need to add some boring boilerplate around it:
$session = $container->get("session");
assert($session instanceof SessionInterface);
$session->set("user_id", $user_id);
A simple one-liner now is three times bigger! And it looks more complicated when you try to look
through your code quickly! What if IDE could tell the return type that the service container get()
calls? Framework-specific IDE plugins already do this (see Working with the Service Container —
Symfony Development using PhpStorm).
Try to look though this Symfony 2 plugin’s repo codebase at GitHub: Haehnchen/idea-php-symfony2-plugin. It’s going be very difficult for an average PHP developer to implement it!
Instead, in a Perfect World™, a developer could hint the application's service container entries with application-specific definition files:
declare class ServiceContainer {
public function get('session'): SessionInterface;
public function get('files'): FilesystemInterface;
public function get('templating'): \Twig\Environment;
// .. and so on
// general case
public function get(string $key): mixed;
}
That could give an IDE a lot of ideas of what’s going on. Also, developers could have dedicated definition generator scripts for each framework to leverage this functionality automatically!
Alternatives
Yes, there are some alternative approaches to solve the same issues with existing tools.
phpDoc
You can use phpDoc blocks with @method
hints to denote magic methods. Yes, it works. Although you
can already auto-generate them and inline them into existing classes, it will only solve the first
half of the issue.
Maybe we can extend phpDoc to allow describing overloaded functions in the future. But it definitely won’t help to deal with service containers hinting.
Autogenerated Helper Files
It is possible to generate special helper files with all phpDocs included to help IDE understand the code. Some plugins already do so. See Laravel IDE Helper Generator.
To hint existing classes we can:
- Generate a separate file with a dummy class declaration of the same name with all phpDocs included. The IDE will notice it and use it for code analysis, but at the same time, it will also display a warning: duplicated class declaration.
- Inline generated phpDoc blocks into existing files. Which is not really convenient as it can replace our hand-crafted doc blocks. And also cannot be used to describe non-project 3-rd party libraries code (i.e. the files from vendor folder).
The Conclusion
Implementing such functionality requires a significant amount of work to be done. We'd need to introduce a new language (though it almost matches the PHP itself) and support its specification. Some of the problems Definition Files could solve may be solved by improving existing tools. So we should consider if it’s worth powder and shot first.
As for me, I think it is a great idea. It may be a serious game-changer! Many developers and projects would benefit from it:
- framework communities could easily provide rich methods specifications for IDE autocompletion
- web application developers could increase development speed by leveraging autocompletion for 100%
- IDE developers could use these definition files as specifications in addition to phpDoc blocks
I’ve filed a feature request at JetBrains issue tracker system: https://youtrack.jetbrains.com/issue/WI-26563
Any thoughts and feedback will be appreciated. Feel free to email me.
Cheers! 🖖
Update
In 2019 I've published a follow-up post on this topic with lessons learned in 4 years, adapted to the latest changes in the PHP ecosystem: Definitely Typed PHP — Four Years later.