Skip to content

Instantly share code, notes, and snippets.

@brzuchal
Last active August 27, 2019 07:08
Show Gist options
  • Save brzuchal/35ba8dbc2a405d9428c902ca938b2c40 to your computer and use it in GitHub Desktop.
Save brzuchal/35ba8dbc2a405d9428c902ca938b2c40 to your computer and use it in GitHub Desktop.

Annotations

Key questions

  1. Should we prohibit direct instantiation of featured annotation class objects?
  2. If so should for eg. @class be used to declare featured annotation class?
  3. What should happen if featured annotation used with non-annotation class?

Restrictions

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

Types

Simple annotations

Consist only with a name and optional value.

Simple annotation without value

Simple annotations don't need any value.

<?php

[SimpleAnnotation]
class Foo {}

Simple annotation with value

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.

Featured annotations

Consist with a name and value being an object instantiated on read.

Featured annotation example without values.

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.

Trying to instantiate annotation class object
<?php

@class Foo {}

new Foo(); // throws RuntimeException
Trying to annotate using non-annotation class
<?php

class Foo {}

[@Foo]
class Bar {}

// TODO: figure out desired behaviour
(new ReflectionClass(Bar::class))->getAnnotations(Foo::class); // empty []

Featured annotation example with values

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 annotation example with values and guards

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 annotation example with constructor

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 {}

Prototypes

<?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

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 of reading annotations

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"
// }
<?php
// featured annotation targeted to ALL
@class FeaturedAnnotation
{
[@Attribute]
[@Required]
public string $foo;
}
@class FeaturedSubAnnotation extends @FeaturedAnnotation
{
[@Attribute]
[@Required]
public string $bar;
}
// featured annotation targeted to classes only
[@Annotation("CLASS")]
@class FeaturedCustomizedAnnotation
{
public string $foo;
public function __construct(string $foo)
{
$this->foo = $foo;
}
}
[SimpleAnnotation]
[SimpleValueAnnotation("bar")]
[SimpleClosureAnnotation(fn(Example $example) => $example->foo === "bar")]
[@FeaturedCustomizedAnnotation("bar")]
[@FeaturedAnnotation{foo: "bar"}]
[@FeaturedSubAnnotation{foo: "bar", bar: "baz"}]
class Example
{
protected $foo = "bar";
}
<?php
namespace Aspect;
use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Before;
/**
* Monitor aspect
*/
class MonitorAspect implements Aspect
{
/**
* Method that will be called before real method
*/
[@Before{execution: "public Example->*(*)"}]
public function beforeMethodExecution(MethodInvocation $invocation)
{
echo 'Calling Before Interceptor for: ',
$invocation,
' with arguments: ',
json_encode($invocation->getArguments()),
"<br>\n";
}
}
<?php
namespace Go\Lang\Annotation;
[Annotation("METHOD")]
@class Before
{
[Attribute]
[Required]
public string $execution;
public function __set(string $name, $value): void
{
if ($name === "execution" && empty($value)) {
throw new RuntimeException("Execution attribute cannot be empty");
}
}
}
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class Bar {
[@Route(["path" => "/hello", "methods" => ["GET"], "name" => "bar_hello"])]
public function __invoke(Request $request): Response
{
return new Response('Hello World!');
}
}
<?php
namespace Symfony\Component\Routing\Annotation;
/**
* @see https://github.com/symfony/routing/blob/master/Annotation/Route.php
*/
[Annotation(["CLASS", "METHOD"])]
@class Route
{
private $path;
private $localizedPaths = [];
private $name;
private $requirements = [];
private $options = [];
private $defaults = [];
private $host;
private $methods = [];
private $schemes = [];
private $condition;
private $locale;
private $format;
private $utf8;
/**
* @param array $data An array of key/value parameters
*
* @throws \BadMethodCallException
*/
public function __construct(array $data)
{
if (isset($data['localized_paths'])) {
throw new \BadMethodCallException(sprintf('Unknown property "localized_paths" on annotation "%s".', \get_class($this)));
}
if (isset($data['value'])) {
$data[\is_array($data['value']) ? 'localized_paths' : 'path'] = $data['value'];
unset($data['value']);
}
if (isset($data['path']) && \is_array($data['path'])) {
$data['localized_paths'] = $data['path'];
unset($data['path']);
}
if (isset($data['locale'])) {
$data['defaults']['_locale'] = $data['locale'];
unset($data['locale']);
}
if (isset($data['format'])) {
$data['defaults']['_format'] = $data['format'];
unset($data['format']);
}
if (isset($data['utf8'])) {
$data['options']['utf8'] = filter_var($data['utf8'], FILTER_VALIDATE_BOOLEAN) ?: false;
unset($data['utf8']);
}
foreach ($data as $key => $value) {
$method = 'set'.str_replace('_', '', $key);
if (!method_exists($this, $method)) {
throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, \get_class($this)));
}
$this->$method($value);
}
}
public function setPath($path)
{
$this->path = $path;
}
public function getPath()
{
return $this->path;
}
public function setLocalizedPaths(array $localizedPaths)
{
$this->localizedPaths = $localizedPaths;
}
public function getLocalizedPaths(): array
{
return $this->localizedPaths;
}
public function setHost($pattern)
{
$this->host = $pattern;
}
public function getHost()
{
return $this->host;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function setRequirements($requirements)
{
$this->requirements = $requirements;
}
public function getRequirements()
{
return $this->requirements;
}
public function setOptions($options)
{
$this->options = $options;
}
public function getOptions()
{
return $this->options;
}
public function setDefaults($defaults)
{
$this->defaults = $defaults;
}
public function getDefaults()
{
return $this->defaults;
}
public function setSchemes($schemes)
{
$this->schemes = \is_array($schemes) ? $schemes : [$schemes];
}
public function getSchemes()
{
return $this->schemes;
}
public function setMethods($methods)
{
$this->methods = \is_array($methods) ? $methods : [$methods];
}
public function getMethods()
{
return $this->methods;
}
public function setCondition($condition)
{
$this->condition = $condition;
}
public function getCondition()
{
return $this->condition;
}
}
<?php
use Doctrine\ORM\Mapping as ORM;
[@ORM\Entity(FooRepository::class)]
[@ORM\ChangeTrackingPolicy("DEFERRED_IMPLICIT")]
[@ORM\HasLifecycleCallbacks]
class Foo
{
[@ORM\Id]
[@ORM\Column("foo", "int", true])]
[@ORM\GeneratedValue("IDENTITY")]
[require(fn(int $value) => $value > 0)]
protected int $id;
}
<?php
namespace Doctrine\ORM\Mapping;
/**
* @see https://github.com/doctrine/orm/tree/master/lib/Doctrine/ORM/Annotation
*/
[Annotation("CLASS")]
@class Entity
{
[Attribute]
[Required]
public string $repositoryClass;
[Attribute]
public bool $readOnly = false;
}
[Annotation("CLASS")]
@class ChangeTrackingPolicy
{
[Attribute]
[Required]
[Enum(["DEFERRED_IMPLICIT"])]
public string $policy;
}
[Annotation("CLASS")]
@class HasLifecycleCallbacks
{
}
[Annotation("PROPERTY")]
@class Id
{
}
[Annotation("PROPERTY")]
@class Column
{
[Attribute]
public ?string $name = null;
[Attribute]
[Required]
public string $type = "string";
/**
* The length for a string column (Applied only for string-based column).
*/
[Attribute]
public int $length = 255;
/**
* The precision for a decimal (exact numeric) column (Applies only for decimal column).
*/
[Attribute]
public int $precision = 0;
/**
* The scale for a decimal (exact numeric) column (Applies only for decimal column).
*/
[Attribute]
public int $scale = 0;
[Attribute]
public boolean $unique = false;
[Attribute]
public bool $nullable = false;
[Attribute]
public array $options = [];
[Attribute]
public string $columnDefinition;
}
[Annotation("PROPERTY")]
@class GeneratedValue
{
[Required]
[Enum(["AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "CUSTOM"])]
public string $strategy = "AUTO";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment