PhpStorm 2020.3 EAP #4: Custom PHP 8 Attributes

Roman Pronskiy

PhpStorm 2020.3 will come with several PHP 8 attributes available out-of-the-box:
#[ArrayShape], #[ExpectedValues], #[NoReturn], #[Pure], #[Deprecated], #[Immutable]. Read on to learn more about the attributes, and please share your feedback about the design.

Download PhpStorm 2020.3 EAP

You’ve probably already heard about the attributes in PHP 8. But just in case you haven’t, they are the new format for structured metadata that replaced PHPDoc and will now be part of the language.

What attributes are in PHP 8?

Apart from the syntax definition and validation when calling ReflectionAttribute::newInstance(), PHP 8 does not provide any attributes out-of-the-box. For attributes that you define, you have to implement their behavior yourself.

What attributes will be available in PhpStorm 2020.3?

Several attributes will be available in PhpStorm 2020.3 under \JetBrains\PhpStorm\ namespace. #[ExpectedValues] and #[NoReturn] are more advanced descendants of .phpstorm.meta.php functions. And #[ArrayShape] is a highly anticipated evolution of PHPDoc’s array description. There also will be #[Deprecated], #[Pure], and #[Immutable].

The design of the attributes below is still a work in progress, and your feedback is very welcome.

#[Deprecated]

This attribute is similar to @deprecated PHPDoc tag and is used to mark methods, functions, classes, or class constants and it indicates that they will be removed in future versions as they have become obsolete.

The main advantage of this new attribute is that you can specify replacement for functions and methods. That will help users of the deprecated functionality migrate.

If you specify the reason argument for the attribute, then it will be shown to a user in the inspection tooltip.

#[Deprecated(reason: '', replacement: '')]

Let’s take a look at a real-world example.

In Symfony 5.2 the \Symfony\Component\DependencyInjection\Alias::setPrivate() will be deprecated. With #[Deprecated] attribute we can make migration easier.

#[Deprecated(
    reason: 'since Symfony 5.2, use setPublic() instead',
    replacement: '%class%->setPublic(!%parameter0%)'
)]

#[ArrayShape]

One of the most requested features for PhpStorm was support for more specific array PHPDoc annotations. This was partially implemented with Psalm support.

But the other part – specifying the possible keys and what value type they correspond to – was still missing. This functionality could be useful when working with simple data structures or object-like arrays when defining a real class may feel excessive.

Starting from PhpStorm 2020.3, it will be possible to define the structure of such arrays with an #[ArrayShape].

The syntax is as follows:

#[ArrayShape([
 // ‘key’ => ’type’,
    ‘key1’ => ‘int’,
    ‘key2’ => ‘string’,
    ‘key3’ => ‘Foo’,
    ‘key3’ => App\PHP 8\Foo::class,
])]
function functionName(...): array

As you can see, the ‘type’ can be specified as a scalar in a string or as a class reference in the form of an FQN string or a ::class constant.

You can extract an array that defines a shape into a constant and then reuse it inside the attributes where it applies:

const  MY_ARRAY_SHAPE = [];
#[ArrayShape(MY_ARRAY_SHAPE)]

What about legacy projects that can’t upgrade to PHP 8?
Fortunately, the syntax of one-line attributes is backward compatible. This means that if you add the #[ArrayShape] attribute in a separate line in your PHP 7.* project, the PHP interpreter will parse it as just a line comment and you won’t get a parse error. However, multiline attributes are not safe for versions of PHP prior to 8.

Unlike the PHP interpreter, PhpStorm will analyze attributes anyway! So even if your project runs on PHP 7.4 or lower, you still benefit from adding #[ArrayShape] attributes.

Note, you’ll have code completion when working with earlier PHP versions in PhpStorm, but inspections will run only with language level 8 and above.

#[Immutable]

Immutable objects are the ones that can not be changed after they are initialized or created. The benefits of using them are the following:

  • The program state is more predictable.
  • Debugging is easier.

It was possible to somewhat emulate immutable objects using getters and setters or magic methods. Starting from PhpStorm 2020.3, you can simply mark objects or properties with the #[Immutable] attribute.

PhpStorm will check the usages of objects and properties and highlight change attempts.

You can adjust the write scope restriction to a constructor only, or simulate private and protected scopes. To do that, pass one of the constants CONSTRUCTOR_WRITE_SCOPE, PRIVATE_WRITE_SCOPE, PROTECTED_WRITE_SCOPE to the #[Immutable] attribute constructor.

The #[Immutable] attribute will work even with PHP 7.4 and lower!

#[Pure]

You can mark functions that do not produce any side effects as pure. Such functions can be safely removed if the result from executing them is not used in the code after.

PhpStorm will detect redundant calls of the pure functions.

If the function is marked as pure, but you try to change something outside it, i.e. it produces a side effect, then PhpStorm will warn you and highlight the unsafe code.

#[ExpectedValues]

With this attribute, you can specify which values a function accepts as parameters and which it can return.

This is similar to what the expectedArguments() function could do in .phpstorm.meta.php, except that the meta version is more like a completion adversary. The attribute, by contrast, assumes that there are no other possible values for the argument or return value.

For example, let’s take the count function:
count ( array|Countable $array_or_countable [, int $mode = COUNT_NORMAL ] ) : int

The second argument it takes is an integer, but in reality, it is not an integer. Rather it is one of the constants COUNT_NORMAL or COUNT_RECURSIVE, which correspond to the 0 and 1.

You can add an attribute #[ExpectedValues] to the second parameter. And this is how the code completion will change in this case.

No meta

With expectedArguments() in .phpstorm.meta.php

With #[ExpectedValues] attribute

How to specify possible values or bitmasks.

Expected values are passed to the attribute constructor and can be any of the following:

  • Numbers: #[ExpectedValues(values: [1,2,3])]
  • String literals: #[ExpectedValues(values: [‘red’, ‘black’, ‘green’])]
  • Constant references: #[ExpectedValues(values: [COUNT_NORMAL, COUNT_RECURSIVE])]
  • Class constant references: #[ExpectedValues(values: [Code::OK, Code::ERROR])]

And there are a few ways to specify expected arguments:

  • #[ExpectedValues(values: [1,2,3])] means that only one of the values is expected.
  • #[ExpectedValues(flags: [1, 2, 3])] means that a bitmask of the specified values is expected, e.g. 1 | 3.
  • #[ExpectedValues(valuesFromClass: MyClass::class)] means that any of the constants from the class `MyClass` is expected.
  • #[ExpectedValues(flagsFromClass: ExpectedValues::class)] means that a bitmask of the constants from the class `MyClass` is expected.

#[ExpectedValues] examples

Let’s take a look at the response() helper in Laravel. It takes the HTTP status code as the second argument.

This leaves us missing two key features:

  • Code completion for possible status codes
  • Validation in the editor

Let’s fix this by adding the attribute #[ExpectedValues(valuesFromClass: Response::class)]

#[NoReturn]

Some functions in a codebase may cause the execution of a script to stop. First, this is not always obvious from a function name, for example, trigger_error() can stop execution depending on the second argument. And second, PhpStorm cannot always detect such functions, because deep analysis can cause performance problems.

This is why it makes sense to mark such functions as exit points to get a more accurate control flow analysis by adding the #[NoReturn] attribute.

Also, PhpStorm will offer to propagate the attribute down across the hierarchy with a quick-fix to get even more well-defined analysis.

Show me the code!

The definitions of these attributes are available in the github.com/JetBrains/phpstorm-stubs. We are going to annotate some internal functions like parse_url() with #[ArrayShape] in the stubs. And also migrate @property-read to #[Immutable].

What other attributes are in the works?

There are ideas for more attributes, such as the Contract attribute. We are interested to know which ones you would find useful for your work. Feel free to share any comments or suggestions with us.

Final notes

PhpStorm won’t look for attributes deeper than one level. So we expect users to propagate attributes with a quick-fix.

Currently, these attributes are distributed with github.com/JetBrains/phpstorm-stubs. It means they are available in the IDE out-of-the-box. But we may reconsider how the distribution is done in the future.


Download PhpStorm 2020.3 EAP

The full list of changes in this EAP build is available in the release notes.

  • Important! PhpStorm EAP builds are not fully tested and may be unstable.
  • You can install an EAP build side by side with a stable PhpStorm version to try out the latest features.
  • EAP builds are free to use but expire 30 days after the build date.

Please report any problems you find to our issue tracker, or mention them in the comments to this post.

Your JetBrains PhpStorm team
The Drive to Develop

Subscribe

Subscribe for updates