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 1. Hello World 2. History of

    Metadata in PHP 3. Attributes in PHP 8 4. Use-Cases and Examples AGENDA 2
  2. Hello World Example #[Route("/hello/world")] function helloWorld() { echo "Hello World";

    } $router = new Router(); $router->collectRoutePaths(); $router->dispatch($_SERVER['REQUEST_URI'], $_GET);
  3. Declaring an Attribute Class #[Attribute] class Route { public readonly

    string $path; public function __construct(string $path) { $this->path = $path; } }
  4. 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); } } }
  5. Instantiating Attribute from Reflection private function collectRoutePath( ReflectionAttribute $reflectionAttribute, ReflectionFunction

    $reflFn ): void { $attribute = $reflectionAttribute->newInstance(); $this->paths[$attribute->path] = [ 'function' => $reflFn, 'attribute' => $attribute ]; }
  6. 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); } }
  7. 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
  8. Docblocks /** * @param int $x * @param int $y

    * @return int */ function ($x, $y) { return $x + $y; }
  9. Doctrine Annotations /** @Entity(repositoryClass=PostRepository::class) */ class Post { } /**

    @Annotation */ final class Entity { public $repositoryClass; }
  10. Doctrine Annotations $reflectionClass = new ReflectionClass(Post::class); $annotationReader = new AnnotationReader();

    $entityAnnnot = $annotationReader->getClassAnnotation( $reflectionClass, Entity::class, ); echo $entityAnnnot->repositoryClass;
  11. 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
  12. 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 @{Attribute} 16
  13. SYNTAX use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; final class ExampleTest extends TestCase

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

    { #[TestDox(text: 'It does something')] public function testOne(): void { // ... } }
  15. 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 { // ... } }
  16. 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 { // ... } }
  17. 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 { // … } }
  18. SYNTAX Use of # as beginning symbol allows attributes to

    be interpreted as comments in PHP 7 and before.
  19. TARGETS OF ATTRIBUTES • Classes • Methods • Functions •

    Constants • Class Properties • Function/Method Arguments
  20. TARGETS OF ATTRIBUTES #[FunctionAttr] function test(#[ArgumentAttr] $argument) {} #[ClassAttr] class

    AttributedClass { #[ConstantAttr] const FOO = 1; #[PropertyAttr] public $bar; #[MethodAttr] public function baz() {} }
  21. ReflectionAttribute class ReflectionAttribute { public function getName(): string {} public

    function getArguments(): array {} public function getTarget(): int {} public function isRepeated(): bool {} public function newInstance(): object {} }
  22. 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.
  23. 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; } }
  24. Validation of Attribute Flag A violation of the target or

    repeated attribute flags only leads to an error upon using ReflectionAttribute::newInstance()
  25. Attributes in PHP: #[SensitiveParameter] use SensitiveParameter; class ConnectionFactory { public

    function connect( string $host, string $user, #[SensitiveParameter] string $password) { } }
  26. Attributes in PHP: #[SensitiveParameter] Fatal error: Uncaught Exception Stack trace:

    #0 (15): ConnectionFactory->connect('foo', 'bar', Object(SensitiveParameterValue))
  27. Attributes: #[AllowDynamicProperties] <?php $o1 = new UserA(); // Deprecated: Creation

    of dynamic property UserA::name is deprecated $o1->name = true; $o2 = new UserB(); $o2->name = true;
  28. 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; }
  29. 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"), ...)
  30. 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; } }
  31. 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); }
  32. 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; }
  33. Routing How to get all methods that have a route?

    https://github.com/olvlvl/composer-attribute-collector
  34. 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) ); } } }
  35. 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) }