kphp
kphp copied to clipboard
RFC: Type aliases
RFC: Type aliases
Background
Some VKCOM legacy code is built on associative arrays with lots of parameters. To type such arrays, KPHP have a special shape
type that allows to describe the types of each parameter.
However, in some places, the amount parameters may exceed some large number, which becomes inconvenient when developers need to write a function that takes the same shape
.
<?php
/**
* @param shape(name: string, age: int, status: string, id: int, other: string) $a
*/
function set_data($a) { ... }
/**
* @param [need to copy the entire shape] $a
*/
function set_data_2($a) { ... }
For situations like this, we want to be able to create an alias for the type, as developers can do in many other languages.
Aliases kinds
Since most type aliases in languages are syntactic constructs with native support, their use is allowed anywhere. So, for example, in Hack aliases for types can be used as type hints for function parameters.
But KPHP is limited by backward compatibility with PHP and KPHP aliases can only work where PHP allows it. The only such place is PHPDoc, where KPHP can use aliases, and it will work in PHP as well.
It might be possible with some hack to make aliases be classes with lots of magic methods to make it work in PHP, but it doesn't seem worth it, and the PHPDoc-only option is optimal and solves the problem described earlier.
Solutions
Since KPHP must remain compatible with PHP, we can't introduce new syntactic constructs as Hack can:
type Server = shape('name' => string, 'age' => int);
Since this, only options remain within the various comments.
Static analysis tools
Psalm
Psalm has support aliases for types. However, each alias isn't a top-level definition; it's attached to a class:
/**
* @psalm-type User = array{name: string, age: int}
*/
class UserHandler {
/**
* @psalm-param User $a
*/
public function set_data($a) { ... }
}
To use it inside another class, need to use an import annotation:
/**
* @psalm-import-type User from UserHandler
*/
class UserWatcher {}
Because such aliases are limited to classes, they aren't suitable for KPHP, since a legacy is built on plain functions.
PHPStan
PHPStan also has its own syntax for specifying type aliases. It divides them into two types, local, and global.
Local aliases are completely identical to Psalm.
Global aliases are set in the config file:
parameters:
typeAliases:
Name: 'string'
NameResolver: 'callable(): string'
NameOrResolver: 'Name|NameResolver'
This solution isn't convenient, since aliases aren't part of the source code.
Static analysis solutions review
The solutions above have disadvantages because static analyzers can't introduce their own syntactic constructions and such options are the best they can think of. However, KPHP aren't bound by such restrictions and may introduce new pseudo constructs as long as they're compatible with PHP.
When prototyping such constructs, however, one must not forget that they must be supported in the IDE. If this isn't possible, then many refactorings in the IDE can become dangerous because they won't consider aliases.
Pseudo new constructs
-
Hash comment:
#typealias User = shape(name: string, age: int)
Pros:
- Good readability.
- Looks line native syntax, if highlights
#typealias
as a keyword. - Skip by PHP and not need polyfills.
Cons:
- Due to the AST in PhpStorm, there may be problems with parsing and autocompletion in the plugin.
- In the documentation and other places where there is code, it will be highlighted as a comment.
-
PHPDoc:
/** @typealias User = shape(name: string, age: int) */
Pros:
- Looks line standard for PHP.
- Easy support in IDE.
Cons:
- Looks strange as top level definition.
- No syntactic construction (class or function) to which the comment is attached.
- In the documentation and other places where there is code, it will be highlighted as a comment.
-
typealias
function:typealias("User", "shape(name: string, age: int)");
Pros:
- Pretty easy to support in a plugin (since it's a simple function, finding all aliases is out of the box).
- Looks good as top level definition.
- Looks like a native
class_alias
which has support in the IDE.
Cons:
-
Need to create a polyfill:
function typealias(string $name, string $type): void {}
-
More noise than previous variants.
Proposal
Add aliases for types using syntax options 1–3 described earlier (I prefer option 3).
All aliases for a file must be written before any classes or functions, but after use
clauses.
<?php
namespace VK\Wall;
use VK\Feed;
typealias("User", "shape(name: string, age: int)");
/**
* @kphp-use-alias User
*
* @return User
*/
function get_user() { ... }
To use an alias, need to add the @kphp-use-alias
annotation to PHPDoc. This is necessary to understand at the parsing stage that this type isn't a class, but an alias. This will simplify the implementation in KPHP and the plugin, and make it more obvious to the user that this isn't a class, but some alias for the other type.
The name for an alias follows the standard rules for identifiers.
The alias type must be a valid PHPDoc type and can be anything.
Namespaces
All aliases, like other top level definitions, are subject to the rules of namespaces. If an alias is defined in some namespace, then its use in another namespace must specify the fully qualified name:
// 1.php
<?php
namespace VK\Wall;
typealias("User", "shape(name: string, age: int)");
// 2.php
<?php
/**
* @kphp-use-alias \VK\Wall\User
*
* @return User
*/
function get_user() { ... }
The fully qualified name must be specified only in @kphp-use-alias
, further uses in this PHPDoc may only use the alias name.
Since different namespaces can have aliases with the same name, the @kphp-use-alias
tag can optionally specify a new name after as
:
<?php
/**
* @kphp-use-alias \VK\Wall\User
* @kphp-use-alias \VK\ID\User as IdUser
*
* $param IdUser $a
* @return User
*/
function get_user($a) { ... }
Backward Incompatible Changes
None.