Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Attributes in PHP 8

Attributes in PHP 8

This talk provides an overview of the "metadata in code" functionality introduced in PHP 8.0 and extended in PHP 8.1. These attributes replace the previously everyday use of docblocks for metadata configuration with a much better system in the core of the PHP language. With attributes, you can provide a machine-readable specification of code elements such as classes, properties, methods, arguments, and constants. We will discuss use cases, the implementation, and pitfalls you should avoid when using attributes.

Benjamin Eberlei

May 19, 2023
Tweet

More Decks by Benjamin Eberlei

Other Decks in Programming

Transcript

  1. ATTRIBUTES IN PHP 8
    Metadata in the Language, Not in Docblocks
    Benjamin Eberlei

    View Slide

  2. ATTRIBUTES
    IN PHP 8
    1. Hello World
    2. History of Metadata in PHP
    3. Attributes in PHP 8
    4. Use-Cases and Examples
    AGENDA
    2

    View Slide

  3. Hello World Example
    #[Route("/hello/world")]
    function helloWorld() {
    echo "Hello World";
    }
    $router = new Router();
    $router->collectRoutePaths();
    $router->dispatch($_SERVER['REQUEST_URI'], $_GET);

    View Slide

  4. Declaring an Attribute Class
    #[Attribute]
    class Route
    {
    public readonly string $path;
    public function __construct(string $path)
    {
    $this->path = $path;
    }
    }

    View Slide

  5. Fetching Attributes from Reflection
    public function collectRoutePaths(): void
    {
    foreach (get_defined_functions()['user'] as $fn) {
    $refl = new ReflectionFunction($fn);
    $attributes = $refl->getAttributes(Route::class);
    foreach ($attributes as $attribute) {
    $this->collectRoutePath($attribute, $refl);
    }
    }
    }

    View Slide

  6. Instantiating Attribute from Reflection
    private function collectRoutePath(
    ReflectionAttribute $reflectionAttribute,
    ReflectionFunction $reflFn
    ): void {
    $attribute = $reflectionAttribute->newInstance();
    $this->paths[$attribute->path] = [
    'function' => $reflFn,
    'attribute' => $attribute
    ];
    }

    View Slide

  7. Tying Metadata and Code Together
    public function dispatch(string $path)
    {
    if (isset($this->paths[$requestedPath])) {
    $route = $this->paths[$requestedPath];
    $route['function']->invoke();
    } else {
    http_response_code(404);
    }
    }

    View Slide

  8. Meta-Programming
    Attributes allow to implement a feature “on the meta-level”, without
    knowledge who will use it.

    View Slide

  9. HISTORY OF METADATA IN PHP
    20 Years of Evolution
    9

    View Slide

  10. History of Docblocks in PHP
    • February 2000: First discussions about phpdoc format and tooling
    • October 2000: Ulf Wendel presents first version of phpdoc at PHP
    Congress in Cologne
    • March 2003: First code for Reflection*::getDocComment by Andrei
    Zmievski, shipping in PHP 5
    • 2004/2005: @test Annotation (and others) in PHPUnit
    • 2010: Doctrine ORM 2.0 released with Annotations Parser
    10

    View Slide

  11. Docblocks
    /**
    * @param int $x
    * @param int $y
    * @return int
    */
    function ($x, $y) {
    return $x + $y;
    }

    View Slide

  12. Reflection API for Docblocks (2005)
    $reflectionFunction = new ReflectionFunction('add');
    var_dump($reflectionFunction->getDocComment());
    ?>
    string(56) "/**
    * @param int $x
    * @param int $y
    * @return int
    */"

    View Slide

  13. Doctrine Annotations
    /** @Entity(repositoryClass=PostRepository::class) */
    class Post
    {
    }
    /** @Annotation */
    final class Entity
    {
    public $repositoryClass;
    }

    View Slide

  14. Doctrine Annotations
    $reflectionClass = new ReflectionClass(Post::class);
    $annotationReader = new AnnotationReader();
    $entityAnnnot = $annotationReader->getClassAnnotation(
    $reflectionClass,
    Entity::class,
    );
    echo $entityAnnnot->repositoryClass;

    View Slide

  15. HISTORY OF METADATA IN PHP
    • “ClassMetadata RFC”, 2010, Guilherme Blanco, Pierrick Charron“
    • Annotations in DocBlock”, 2011, Guilherme Blanco, Pierrick
    Charron“
    • Reflection Annotations using the Doc-Comment”, 2013, Yahav Gindi
    Bar
    • “Attributes”, April 2016, Dmitry Stogov
    • “Simple Annotations”, Mai 2016, Rasmus Scholz
    • “Annotations v2”, Februar 2019, Michał Brzuchalski
    • “Attributes v2”, Februar 2020, Martin Schröder, Benjamin Eberlei
    15

    View Slide

  16. HISTORY OF METADATA IN PHP
    Various Syntax suggestions and
    after three separate RFCs voting
    on them we decided on:
    #[Attribute]
    The following alternatives were
    rejected:
    @@Attribute
    @[Attribute]
    <>
    @:Attribute
    @{Attribute}
    16

    View Slide

  17. ATTRIBUTE IN PHP 8
    Metadaten als Feature der Sprache
    17

    View Slide

  18. SYNTAX
    use PHPUnit\Framework\Attributes\Test;
    use PHPUnit\Framework\TestCase;
    final class ExampleTest extends TestCase
    {
    #[Test]
    public function it_does_something(): void
    {
    // ...
    }
    }

    View Slide

  19. SYNTAX
    use PHPUnit\Framework\Attributes\TestDox;
    use PHPUnit\Framework\TestCase;
    final class ExampleTest extends TestCase
    {
    #[TestDox('It does something')]
    public function testOne(): void
    {
    // ...
    }
    }

    View Slide

  20. SYNTAX
    use PHPUnit\Framework\Attributes\TestDox;
    use PHPUnit\Framework\TestCase;
    final class ExampleTest extends TestCase
    {
    #[TestDox(text: 'It does something')]
    public function testOne(): void
    {
    // ...
    }
    }

    View Slide

  21. SYNTAX
    use PHPUnit\Framework\Attributes\Test;
    use PHPUnit\Framework\Attributes\TestDox;
    use PHPUnit\Framework\TestCase;
    final class ExampleTest extends TestCase
    {
    #[Test]
    #[TestDox(text: 'It does something')]
    public function testOne(): void
    {
    // ...
    }
    }

    View Slide

  22. SYNTAX
    use PHPUnit\Framework\Attributes\Test;
    use PHPUnit\Framework\Attributes\TestDox;
    use PHPUnit\Framework\TestCase;
    final class ExampleTest extends TestCase
    {
    #[Test, TestDox(text: 'It does something')]
    public function testOne(): void
    {
    // ...
    }
    }

    View Slide

  23. SYNTAX
    use PHPUnit\Framework\Attributes\TestDox;
    use PHPUnit\Framework\TestCase;
    final class ExampleTest extends TestCase
    {
    #[TestWith(["hello world", "foo" => 1])]
    #[TestWith([new \stdClass, \PDO::PARAM_INT])]
    public function testOne($a, $b): void
    {
    // …
    }
    }

    View Slide

  24. SYNTAX
    Use of # as beginning symbol allows attributes to be interpreted as
    comments in PHP 7 and before.

    View Slide

  25. TARGETS OF ATTRIBUTES
    ● Classes
    ● Methods
    ● Functions
    ● Constants
    ● Class Properties
    ● Function/Method Arguments

    View Slide

  26. TARGETS OF ATTRIBUTES
    #[FunctionAttr]
    function test(#[ArgumentAttr] $argument) {}
    #[ClassAttr]
    class AttributedClass {
    #[ConstantAttr]
    const FOO = 1;
    #[PropertyAttr]
    public $bar;
    #[MethodAttr]
    public function baz() {}
    }

    View Slide

  27. ReflectionAttribute
    class ReflectionAttribute {
    public function getName(): string {}
    public function getArguments(): array {}
    public function getTarget(): int {}
    public function isRepeated(): bool {}
    public function newInstance(): object {}
    }

    View Slide

  28. Attributes map to Classes
    An attribute is namespaced and maps to a class name.
    This class is not required to exist until/unless
    ReflectionAttribute::newInstance() is called.

    View Slide

  29. Attribute Classes
    use Attribute;
    #[Attribute(flags: Attribute::TARGET_FUNCTION|
    Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE
    )]
    class Route
    {
    public readonly string $path;
    public function __construct(string $path)
    {
    $this->path = $path;
    }
    }

    View Slide

  30. Validation of Attribute Flag
    A violation of the target or repeated attribute flags only leads to an
    error upon using ReflectionAttribute::newInstance()

    View Slide

  31. Use-Cases: PHP Internal Attributes

    View Slide

  32. Attributes in PHP: #[SensitiveParameter]
    use SensitiveParameter;
    class ConnectionFactory
    {
    public function connect(
    string $host,
    string $user,
    #[SensitiveParameter]
    string $password) {
    }
    }

    View Slide

  33. Attributes in PHP: #[SensitiveParameter]
    Fatal error: Uncaught Exception
    Stack trace:
    #0 (15): ConnectionFactory->connect('foo', 'bar',
    Object(SensitiveParameterValue))

    View Slide

  34. Attributes: #[AllowDynamicProperties]
    use AllowDynamicProperties;
    class UserA { }
    #[AllowDynamicProperties]
    class UserB { }

    View Slide

  35. Attributes: #[AllowDynamicProperties]
    $o1 = new UserA();
    // Deprecated: Creation of dynamic property UserA::name is
    deprecated
    $o1->name = true;
    $o2 = new UserB();
    $o2->name = true;

    View Slide

  36. Use-Cases: Validating Objects

    View Slide

  37. Validating Objects
    class User
    {
    #[NotBlank(message: "Name should not be blank.")]
    public string $name;
    #[Minimum(min: 18, message: "Must be 18+ years old.")]
    public int $age;
    #[Email(message: "E-Mail address is not valid.")]
    public string $email;
    }

    View Slide

  38. Validating Objects
    $user = new User();
    $user->name = '';
    $user->age = 17;
    $user->email = 'foo';
    $validator = new Validator();
    $violations = $validator->validateObject($user);
    var_dump($violations);
    // array("name" => array("Name should not be blank"), ...)

    View Slide

  39. Validating Objects
    abstract class Assertion {
    public abstract function isValid(mixed $value): bool;
    }

    View Slide

  40. Validating Objects
    #[Attribute(flags: Attribute::TARGET_PROPERTY)]
    class NotBlank extends Assertion
    {
    public function __construct(
    public string $message = 'Property is blank.',
    ) {}
    public function isValid(mixed $value): bool
    {
    return is_string($value) &&
    strlen(trim($value)) > 0;
    }
    }

    View Slide

  41. Validating Objects
    public function validateObject(object $object): Violations
    {
    $violations = [];
    $properties = (new ReflectionObject($object))->getProperties();
    foreach ($properties as $property) {
    $attributes = $property->getAttributes(
    Assertion::class,
    ReflectionAttribute::IS_INSTANCEOF
    );
    $violations = $violations +
    $this->validateProperty($attributes, $object, $property);
    }
    return new Violations($violations);
    }

    View Slide

  42. Validating Objects
    private function validateProperty($attributes, $object, $property)
    {
    $violations = [];
    foreach ($attributes as $attribute) {
    $assertion = $attribute->newInstance();
    if (!$assertion->isValid($property->getValue($object))) {
    $violations[$property->getName()][] = $assertion->message;
    }
    }
    return $violations;
    }

    View Slide

  43. Use-Case: Routing again

    View Slide

  44. Routing
    How to get all methods that have a route?
    https://github.com/olvlvl/composer-attribute-collector

    View Slide

  45. Routing
    use olvlvl\ComposerAttributeCollector\Attributes;
    class Router
    {
    public function collectRoutePaths(): void
    {
    foreach (Attributes::findTargetMethods(Route::class) as $target) {
    $this->collectRoutePath(
    $target->attribute,
    new ReflectionMethod($target->class, $target->method)
    );
    }
    }
    }

    View Slide

  46. Use-Case: Object-Relational Mapping (ORM)

    View Slide

  47. Doctrine ORM
    use Doctrine\ORM\Mapping as ORM;
    #[ORM\Entity]
    #[ORM\Table(name: 'products')]
    class Product
    {
    #[ORM\Id]
    #[ORM\Column(type: 'integer')]
    #[ORM\GeneratedValue]
    private int|null $id = null;
    #[ORM\Column(type: 'string')]
    private string $name;
    // .. (other code)
    }

    View Slide

  48. THANK YOU!
    E-Mail: [email protected]
    Linkedin: https://www.linkedin.com/in/benjamin-eberlei-4b5628116/
    Mastodon: @[email protected]

    View Slide