- Should we prohibit direct instantiation of featured annotation class objects?
- If so should for eg.
@class
be used to declare featured annotation class? - What should happen if featured annotation used with non-annotation class?
Custom featured annotation must be a valid class name and cannot be one of:
Annotation
Attribute
Required
Default
Soft-reserved featured annotation names:
Compiled
JIT
Override
Deprecated
Inherited
Repeatable
SupressErrors
SupressWarning
Package
Module
Ignored
Throws
Consist only with a name and optional value.
Simple annotations don't need any value.
<?php
[SimpleAnnotation]
class Foo {}
Simple annotations can contain a value which expects to be any possible value that can be assigned to variable.
<?php
[SimpleValuedAnnotation("foo")]
class Foo {}
Values passed to simple annotations can consist with a closure which could be any valid closure (either long one or arrow function).
<?php
[SimpleValuedAnnotation(fn(Foo $foo) => $foo->isValid())]
[SimpleValuedAnnotation(static function(Foo $foo): bool { return $foo->isValid(); })]
class Foo {
public function isValid(): bool {
return true;
}
}
Note! Simple annotations can consist with only one optional value.
Consist with a name and value being an object instantiated on read.
Featured annotation don't need any properties and even a constructor if value is not expected.
<?php
@class Foo {}
[@Foo]
class Bar {}
Defining featured annotation requires to use @class
keyword which tells interpreter to treat
it as an annotation and prevents direct object instantiation.
Above examples will trow an exception.
<?php
@class Foo {}
new Foo(); // throws RuntimeException
<?php
class Foo {}
[@Foo]
class Bar {}
// TODO: figure out desired behaviour
(new ReflectionClass(Bar::class))->getAnnotations(Foo::class); // empty []
Featured annotations without constructor can contain public properties annotated with
[@Attribute]
annotation then they can be used with cusrly braces assigning each
attribute a value with it's name. If property annotated with [@Attribute({required: true})]
or contain standalone [@Required]
annotation it requires passing an attribute value.
Passing values using curly braces is highly recommended if there are multiple obligatory properties mixed with optional ones. Optional values can have default value if it is possible to define them on property declaration.
<?php
@class Foo {
[@Attribute]
[@Required]
public string $foo;
[@Attribute]
public bool $bar = false;
}
[@Foo({foo: "foo", bar: false})]
class Bar {}
Annotation of class Foo
was applied to class Bar
with curly braces between parentheses
using attribute names and their value. Which in detail is equivalent to:
<?php
$annotation = new Foo();
$annotation->foo = "foo";
$annotation->bar = false;
Featured annotation can consist with an attribute annotated as [@Attribute({default: true})]
or using standalone [@Default]
annotation which instructs annotation machinery to use that
attribute when creating an annotation without specifying any attribute name.
<?php
@class Foo {
[@Attribute]
[@Default]
public string $foo;
[@Attribute]
public bool $bar = false;
}
[@Foo("bar")]
class Bar {}
// is equivalent to
[@Foo({foo: "bar"})]
class Bar {}
Note! There can be only one attribute defined as default.
Featured annotations created with values through curly braces can contain
read and write guards. When magic method __set(string $name, $value): void
is defined all annotation attributes will be set through given that guard.
It is also possible to define magic method __get(string $name)
when intent
to declare protected or private properties.
<?php
@class Foo {
[@Attribute]
[@Required]
private string $email;
[@Attribute]
public bool $bar = false;
public function __set(string $name, $value): void {
if ($name === "email" && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email address");
}
}
public function __get(string $name) {
if ($name === "email") {
return $this->email;
}
}
}
[@Foo{{email: "demo@example.org", bar: false})]
class Bar {}
Featured annotations can contain constructor then annotation itself requires passing arguments in parentheses like when creating an object or they may skip constructor if there is no requirement for that.
<?php
@class Foo {
public string $foo;
public bool $bar = false;
public function __construct(string $foo, bool $bar = false) {
$this->foo = $foo;
$this->bar = $bar;
}
}
[@Foo("foo", false)]
class Bar {}
All featured annotation containing constructor can be used in mixed mode when declaring with named attributes.
<?php
@class Foo {
public string $foo;
public bool $bar = false;
[@Attribute]
public int $baz = 0;
public function __construct(string $foo, bool $bar = false) {
$this->foo = $foo;
$this->bar = $bar;
}
}
[@Foo("foo", false, {baz: 123})]
class Bar {}
<?php
@class Annotation {
public const TARGET_ALL = "ALL";
public const TARGET_ANNOTATION = "ANNOTATION";
public const TARGET_CLASS = "CLASS";
public const TARGET_FUNCTION = "FUNCTION";
public const TARGET_METHOD = "METHOD";
public const TARGET_PARAMETER = "PARAMETER";
public const TARGET_PROPERTY = "PROPERTY";
[@Attribute({default: true})]
public string $target = self::TARGET_ALL;
[@Attribute]
public bool $inherit = true;
}
@class Attribute {
[@Attribute]
public bool $default = false;
[@Attribute]
public bool $required = false;
}
@class Inherit {}
@class Default {}
@class Required {}
Reading annotations requires use of reflection. There are new methods in wide range of reflectors which help retrieving annotations.
<?php
class ReflectionClass {
/**
* @return array|ReflectionAnnotation[]
*/
public function getAnnotations(?string $name): array {}
public function hasAnnotations(string $name): bool {}
}
class ReflectionProperty {
/**
* @return array|ReflectionAnnotation[]
*/
public function getAnnotations(?string $name): array {}
public function hasAnnotations(string $name): bool {}
}
class ReflectionFunctionAbstract {
/**
* @return array|ReflectionAnnotation[]
*/
public function getAnnotations(?string $name): array {}
public function hasAnnotations(string $name): bool {}
}
class ReflectionParameter {
/**
* @return array|ReflectionAnnotation[]
*/
public function getAnnotations(?string $name): array {}
public function hasAnnotations(string $name): bool {}
}
There are two different types of annotations although both are accessible through the same reflector.
<?php
class ReflectionAnnotation implements Reflector {
public string $name;
public bool $usesConstructor;
private __construct() {}
public function getName(): string {}
public function getValue() {}
public function getClass(): ?ReflectionAnnotationClass {}
}
class ReflectionAnnotationClass extends ReflectionClass {
public function allowAnnotationTarget(): bool {} // true if [Annotation("ANNOTATION")]
public function allowClassTarget(): bool {} // true if [Annotation("CLASS")]
public function allowFunctionTarget(): bool {} // true if [Annotation("FUNCTION")]
public function allowMethodTarget(): bool {} // true if [Annotation("METHOD")]
public function allowParameterTarget(): bool {} // true if [Annotation("PARAMETER")]
public function allowPropertyTarget(): bool {} // true if [Annotation("PROPERTY")]
}
Example code using mixed - featured and simple annotations.
<?php
@class Foo {
[@Attribute]
[@Required]
public string $bar;
}
[SimpleValue("foo")]
[@Foo{bar: "baz"}]
class Bar {
[@Foo{bar: "const"}]
public const FOO = "bar";
[@Foo{bar: "prop"}]
public string $foo;
[@Foo{bar: "foo"}]
public function foo([@Foo{bar: "param"}] string $bar = "baz"): void {}
}
[@Foo{bar: "foo"}]
function foo([@Foo{bar: "param"}] string $bar = "baz"): void {}
Reading simple annotations
<?php
$reflection = new ReflectionClass(Bar::class);
var_dump($reflection->hasAnnotation('SimpleValue')); // true
$simpleAnnotationReflection = $reflection->getAnnotations('SimpleValue')[0];
var_dump($simpleAnnotationReflection->getName());
// string(11) "SimpleValue"
var_dump($simpleAnnotationReflection->getValue());
// string(3) "foo"
var_dump($simpleAnnotationReflection->getClass());
// null
Reading featured annotations
<?php
$reflection = new ReflectionClass(Bar::class);
$fooAnnotationReflection = $reflection->getAnnotations(Foo::class)[0];
var_dump($fooAnnotationReflection->getName());
// string(3) "Foo"
var_dump($simpleAnnotationReflection->getValue());
// object(Foo)#1 (0) {
// ["bar"]=>
// string(3) "baz"
// }
var_dump($simpleAnnotationReflection->getClass());
// object(ReflectionClass)#2 (1) {
// ["name"]=>
// string(3) "Foo"
// }