Why @@ is the best attribute syntax for PHP

Update 2020-09-02: The Shorter Attribute Syntax Change proposal was accepted, with #[Attr] voted as the final attribute syntax for PHP 8.0.

Background

Analogous to docblock annotations, attributes make it possible to apply metadata to a class, function, or property with a native syntax and reflection API.

The Attributes v2 RFC (accepted on May 4, 2020) added support for native attributes in PHP 8 using the <<Attr>> syntax borrowed from Hacklang. In June, I authored the Shorter Attribute Syntax RFC to propose using @@Attr or #[Attr] to address five shortcomings of the original syntax (verbosity, poor readability of nested attributes, confusion with generics and shift operators, and dissimilarity to other languages). The result of this RFC was that the @@Attr syntax was accepted and implemented in PHP 8.0 beta 1.

However, several people were unhappy with this outcome, and proposed a new RFC to vote again on the declined attribute syntaxes (as well as a couple new alternatives) in hopes of a different result. This RFC puts forward four main arguments for changing the syntax: grouped attribute support, consistency, forward compatibility, and “potential future benefits”. Are these really good arguments?

Attribute grouping

Grouped attribute support (e.g. <<Attr1, Attr2>>) was added to partially reduce the verbosity of the <<>> syntax. However, this has the downside of creating two different syntaxes for declaring attributes. Furthermore, grouped attributes result in unnecessary diff noise when adding or removing a second attribute on its own line:

<<SomeAttr("argument")>>
function foo() {}

// changes to
<<
    SomeAttribute("argument"),
    OtherAttr([1, 2, 3]),
>>
function foo() {}

In contrast, with the @@Attr syntax, individual attributes can always be added or removed without having to change attributes on other lines:

@@SomeAttribute("argument")
@@OtherAttr([1, 2, 3]) // can be added/removed independently
function foo() {}

Finally, the @@Attr syntax without grouping is equally concise as the alternatives with grouping, so this is not a reason to prefer any other syntax over @@Attr.

Consistency

The RFC argues that an attribute end delimiter is necessary for consistency with other syntax constructs in PHP. However, attributes are not standalone declarations, but modifiers on the declaration that follows them, similar to type declarations and visibility/extendibility modifiers:

// declaration modifiers do not have end delimiters like this:
[final] class X {
    [public] function foo([int|float] $bar) {}
}

// the @@ syntax is consistent with other declaration modifiers:
@@Attribute
final class X {
    @@Jit
    public function foo(@@Deprecated int|float $bar) {}
}

The RFC responds to this by arguing that attributes are more complex than modifiers and type declarations since they have an optional argument list. However, this fails to recognize that an attribute’s argument list already has its own start/end delimiters (parentheses)! So adding another end delimiter if anything reduces consistency rather than improving it.

The @@Attr syntax is consistent with existing declaration modifiers in PHP, as well as docblock annotations and other languages using the @Attr syntax.

Forward compatibility

The #[Attr] syntax could provide a temporary forward compatibility benefit to library authors, who would be able to reuse the same class both as a PHP 8 attribute and to store information from a docblock annotation when the library is used on PHP 7. However, this benefit will be irrelevant once most of the library users upgrade to PHP 8, and the library wants to take advantage of any other PHP 8 syntax.

Even without partial syntax forward compatibility, a library can support both PHP 8 attributes and PHP 7 annotations with a small amount of extra work. A parent class can be used to store/handle annotation information, and a child class can be registered as an attribute for PHP 8 users.

The downside of forward compatibility is that it can result in code that is valid in both PHP 7 and PHP 8, but runs very differently on both. For example:

class X {
    // This comments out the first parameter entirely in
    // PHP 7, silently leading to different behavior.
    public function __construct(
        #[MyImmutable] public bool $x,
        private bool $flag = false,
    ) {}
}
$f1 = #[ExampleAttribute] function () {};

$f2 = #[ExampleAttribute] fn() => 1;

$object = new #[ExampleAttribute] class () {};
foo();

// On PHP 7 this is interpreted as
$f1 = $f2 = $object = new foo();
<?php
// This example echoes the rest of the source code in
// PHP 7 and echoes "Test" in PHP 8.
#[DeprecationReason('reason: <https://some-website/reason?>')]
function main() {}
const APP_SECRET = 'app-secret';
echo "Test";

Is the temporary forward compatibility benefit (which realistically will only simplify code slightly for library authors) really worth the downside of a larger BC break and risk of valid code being interpreted very differently across PHP versions?

Potential future benefits?

Lastly, the RFC argues that an end delimiter could be helpful for enabling future syntaxes such as applying a function decorator:

#[fn ($x) => $x * 4]
function foo($x) {...}

However, there are other potential ways to accomplish the same thing that would arguably be even more readable and flexible:

// via a built-in attribute:
@@Before(fn ($x) => $x * 4)
function foo($x) {...}

// via a syntax for checked attributes:
@@!MyDecorator
function foo($x) {...}

Whether the attribute syntax has an end delimiter or not, it will be possible to extend functionality in the future. The @Attr syntax has been proven over many years through its use in docblock annotations and other languages, and if it was deficient in some way it almost certainly would have been discovered long ago.

The case for @@

The @@Attr syntax arguably strikes the best balance between conciseness, familiarity with docblock annotations, and a very small BC break. Unlike the #[Attr] and @[Attr] proposals, it does not break useful, functional syntax.

The lack of a separate end delimiter is consistent with other declaration modifiers, and can help avoid confusion when both docblocks and attributes are being used. The syntaxes with an end delimiter make attributes appear as if they are standalone declarations which a docblock can be applied to, even though they are not.

If you read this far, thank you! Let me know if this post changed your mind, or if there is some other argument that convinces you why @@ or another syntax is best.

By Theodore Brown

Software developer and open source contributor from Minnesota. Interested in education, astronomy, and the future of programming.