Attributs en PHP :
un regard rétrospectif de la magie à l'adoption

Qui a ~10 ans d'expérience en PHP ?


NON, NON, NOOOOON
C'est trop magique !!!
Bien sûr, nous ne sommes pas des dévs YAML, LOL
Je peux utiliser les annotations Doctrine ?

Kevin Balicot @kevinbalicot

CTO Akawaka

Les attributs PHP

  1. Rétrospective et explications
  2. Les nouveautés
  3. Utilisation des attributs dans Symfony
  4. Exemple d'application
  5. Conclusion

Les annotations

En programmation, une annotation est un élément permettant d'ajouter des méta-données à un code source.

- Wikipedia

Dit Jamie,
Est-ce que ça existe

dans d'autres langages

C / C++

  • Permet de donner des informations au compilateur
  • Depuis C pour les plus anciennes utilisations
  • C++ 11- août 2011

C / C++

[[deprecated]]
void foo(int i, int j) {
  //...
}

Python

  • Métadonnées sur les variables et fonctions
  • Permet de spécifier le typage, les arguments etc.
  • Depuis python 3 - décembre 2008

Python

class Point:
	def __init__(self, x, y):
        self.x = x
        self.y = y
        

Java

  • Métadonnées pouvant être exploitées à la compilation ou à l'exécution
  • Depuis Java 5 - octobre 2009
  • Exemple : Spring boot 🍃

Java

@RestController
public class RestController {

    @GetMapping(value = "/student/{studentId}")
    public Student getTestData(@PathVariable Integer studentId) {
        Student student = new Student();
        student.setName("Peter");
        student.setId(studentId);

        return student;
    }
}

C#

  • Appelé "Attribut"
  • Associe des métadonnées sur une méthode, propriété ou variable
  • Natif au langage - 2001

C#

public class Author
{
	[Required(ErrorMessage = "{0} is required")]
	[StringLength(50, MinimumLength = 3, ErrorMessage = "...")]
	[DataType(DataType.Text)]
	public string FullName { get; set; }

	[DataType(DataType.PhoneNumber)]
	[Phone]
	public string PhoneNumber { get; set; }

	[DataType(DataType.EmailAddress)]
	[EmailAddress]
	public string Email { get; set; }
}

Annotation
=

Une fonctionnalité des langages de programmation

Annotation PHP

  • Métadonnées sur classe, attribut et méthode
  • Non natif au langage
  • Utilise la syntaxe PhpDoc
  • doctrine/annotations v1.0 - janvier 2013

Annotation PHP

<?php
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;
}

La magie 🪄

doctrine/annotations

<?php

namespace App\Service;

/**
 * @MyAnnotation(foo="bar", array="{1, 2}")
 */
class MyService {
	...
}

doctrine/annotations
ReflectionClass

<?php

$reflection = new ReflectionClass(MyService::class);

$reflection->getDocComment(); 

// /** @MyAnnotation(foo="bar", array="{1, 2}") */

doctrine/annotation
doctrine/lexer

<?php

// /** @MyAnnotation(foo="bar", array="{1, 2}") */

$class = 'MyAnnotation';
$args = ['foo' => 'bar', 'array' => [1, 2]];

$instance = new $class($args['foo'], $args['array']);

// La suite dans le conteneur de services Symfony 🪄

Pas de la magie

  • ~100 collaborateurs
  • v1.0 janvier 2013
  • 5.6k de commits
  • Utilisé par +2300 paquets
  • Téléchargé +400k fois

Et maintenant.
Le présent & futur ✨

Attribut PHP

  • Métadonnées sur classe, attribut / propriété, méthode / fonction et constante
  • Natif au langage
  • Utilise la syntaxe #[Attribute]
  • Depuis PHP 8.0 - novembre 2020

Attribut PHP

<?php

namespace App\Service;

#[MyAttribute(foo: "bar", array: [1, 2])]
class MyService {
	public function __construct(
        #[MyAttribute(...)] public mixed $arg
    ) {}
    
    #[MyAttribute(...)]
    public function method() {}
}

Attribut PHP

<?php

// Syntaxe

#[MyAttribute('bar', [1, 2])]

#[MyAttribute(array: [1, 2], foo: 'bar')]

#[MyAttribute(Other::CONSTANTE)]

#[MyAttribute(new Other())]

#[Namespace\MyAttribute()]

Création Attribut

<?php

namespace App\Attribute;

#[Attribute(
	Attribute::TARGET_CLASS |
    Attribute::TARGET_FUNCTION |
    Attribute::TARGET_METHOD |
    Attribute::TARGET_PROPERTY |
    Attribute::TARGET_CLASS_CONSTANT |
    Attribute::TARGET_PARAMETER
	ou
	Attribute::TARGET_ALL
)]
class MyAttribute {
	public function __construct(
    	public mixed $foo,
        public array $array
    ) {}
}

Utilisation Attribut

<?php

$ref = new ReflectionClass(MyService::class);

// Attributs "MyAttribute" sur classe
$attributes = $ref->getAttributes(MyAttribute::class);

// Attributs "MyAttribute" sur propriété
$attributes = $ref->getProperties()[0]->getAttributes(MyAttribute::class);

// Attributs "MyAttribute" sur méthode
$attributes = $ref->getMethods()[0]->getAttributes(MyAttribute::class);

// Attributs "MyAttribute" sur paramètre de méthode
$attributes = $ref->getMethods()[0]
	->getParameters()[0]
	->getAttributes(MyAttribute::class);
    
// etc...

⚠️ Utilisation Attribut

<?php

$ref = new ReflectionClass(MyService::class);

// Attributs "MyAttribute" sur classe
$attributes = $ref->getAttributes(MyAttribute::class);

/** @var ReflectionAttribute $attribute **/
foreach ($attributes as $attribute) {
	
    $attribute->getName(); // Get attribute name
    $attribute->getArguments(); // Get array of arguments
    $attribute->newInstance(); // Get new instance of attribute with arguments
    
}

Les attributs natifs

PHP 8.0 #[Attribute]
PHP 8.1 #[ReturnTypeWillChange]
PHP 8.2 #[SensitiveParameter]
#[AllowDynamicProperties]
PHP 8.3 #[Override]
RFC #[NotSerializable]
#[Deprectated]

#ReturnTypeWillChange

<?php

class MyCounter implements Countable {
	#[ReturnTypeWillChange]
    public function count() {
    	// ...
    }
}

Sinon ...
```
Deprecated: Return type of Example::count() 
should either be compatible with Countable::count(): int, 

or the #[\ReturnTypeWillChange] attribute should be used 
to temporarily suppress the notice
```

#SensitiveParameter

<?php

function signUp(string $username, #[SensitiveParameter] string $password)
{
	throw new Exception('Error');
}

signUp('Jean', 'sandwichAuJambon');

```
PHP Fatal error:  Uncaught Exception: Error in /main.php
Stack trace:
#0 /main.php(8): signUp('Jean', Object(SensitiveParameterValue))
#1 {main}
  thrown in /main.php on line 5
```

#AllowDynamicProperties

<?php

class MyLegacyClass {}

#[AllowDynamicProperties]
class MyDynamicalClass {}

$legacy = new MyLegacyClass();
$dynamic = new MyDynamicalClass();

legacy->foo = 'bar';
$dynamic->foo = 'bar';


```
Deprecated: Creation of dynamic property MyLegacyClass::$foo is deprecated
```

#Override

<?php

interface MyInterface {
	public function foo(): string;
}


class MyService implemente MyInterface {
	#[Override]
    public function boo(): string {
    	...
    }
}

```
Fatal error: MyService::boo() has #[\Override] attribute, 
but no matching parent method exists
```

Et Symfony,
Il utilise des attributs ?

+48 attributs Symfony qui vont changer votre vie !

Le rand(1, 48)ème va vous étonner !

HttpKernel

#AsController

<?php

namespace App\Controller;

use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
class MyAction
{
	public function __invoke(): Response
    {
    	...
    }
}

#Route

<?php

namespace App\Controller;

use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

#[AsController]
class MyAction
{
	#[Route(path: '/route', name: 'route.name', methods: ['GET'])]
	public function __invoke(): Response
    {
    	...
    }
}

#AsTargetedValueResolver

<?php

namespace App\ValueResolver;

use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;

#[AsTargetedValueResolver('object')]
class MyResolver implements ValueResolverInterface
{
	...
}

#ValueResolver

<?php

namespace App\Controller;

use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;

#[AsController]
class MyAction
{
	public function __invoke(
    	#[ValueResolver('object')] Object $obj
    ): Response
    {
    	...
    }
}

#WithHttpStatus

<?php

namespace App\Exception;

use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;

#[WithHttpStatus(422, [
   'Retry-After' => 10,
   'X-Custom-Header' => 'header-value',
])]
class MyException extends HttpException
{
	...
}

#WithLogLevel

<?php

namespace App\Exception;

use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;

#[WithLogLevel(LogLevel::WARNING)]
class MyException extends HttpException
{
	...
}

Security

#CurrentUser

<?php

namespace App\Controller;

use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Security\Http\Attribute\CurrentUser;

#[AsController]
class MyAction
{
	public function __invoke(
    	#[CurrentUser] ?User $user
    ): Response
    {
    	...
    }
}

!

Controller only

#IsGranted

<?php

namespace App\Controller;

use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN')]
class MyAction
{
	#[IsGranted('ROLE_SUPER_ADMIN', statusCode: 403, message: 'Blabla.')]
	public function __invoke(): Response
    {
    	...
    }
}

#Serializer

<?php

namespace App;

class Person
{
	#[MaxDepth(1)]
    #[Groups('person:read')]
	public Person $parain;
    
    #[Groups('person:read')]
    #[SerializedName('fullname')]
    public string $firstName;
    
    #[Groups('person:write')]
    public string $lastName;
    
    #[Ignore]
    public string $password;
    
    // Et les autres Context, DiscriminatorMap, SerializedPath ...
}

Dependency Injection

#AsAlias

<?php

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(id: 'service.other', public: true)]
class MyService implements MyServiceInterface
{
	...
}

#Autowire

<?php

namespace App\Service;

class MyService
{
	public function __construct(
    	#[Autowire(service: 'monolog.logger.request')]
        private LoggerInterface $logger,
        
        #[Autowire(env: 'URL_ENV')]
        string $apiUrl,
        
        #[Autowire(param: 'kernel.debug')]
        bool $debugMode,
    )
}

#Autoconfigure

<?php

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(lazy: true)]
class MyService
{
	...
}

#AutoconfigureTag

<?php

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('app.custom_tag')]
interface MyServiceInterface
{
	...
}

#TaggedIterator

<?php

namespace App\Service;

use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

class MyServiceHandler
{
	public function __construct(
        #[TaggedIterator('app.custom_tag')] iterable $handlers
    ) {
    }
}

#When

<?php

namespace App\Client;

use Symfony\Component\DependencyInjection\Attribute\When;

#[When(env: 'prod')]
class StripeClient
{
	...
}

#[When(env: 'dev')]
#[When(env: 'test')]
class MockedStripeClient
{
	...
}

#AsDecorator ...

<?php

namespace App\Decorator;

use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

#[AsDecorator(decorates: TotalPriceCalculator::class)]
class PromotedPriceCalculator implement PriceCalculatorInterface
{
	public function __construct(
        #[AutowireDecorated] private object $inner
    ) {
    }
    
    public function calculate(array $products)
    {
    	// Logique de calcule des promotions
        
        return $this->inner->calculate($products);
    }
}

Messenger

#AsMessageHandler

<?php

namespace App\Handler;

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

class MyMessage { ... }

class MyOtherMessage { ... }

#[AsMessageHandler(
	bus: 'default',
	fromTransport: 'async', 
    handles: [MyMessage::class, MyOtherMessage::class, ...],
    priority: 10
)]
class MyMessageHandler
{
	public function __invoke(MyMessage $message)
 	{
    	...
    }
}

Scheduler

#AsCronTask

<?php

namespace App\Scheduler\Task;

use Symfony\Component\Scheduler\Attribute\AsCronTask;

#[AsCronTask('0 0 * * *')]
class MyTask
{
	public function __invoke()
    {
        ...
    }
}

#[AsCronTask('0 0 * * *', method: 'foo', arguments: ['bar' => 'truite'])]
class MyTask
{
	public function foo(string $bar)
    {
        ...
    }
}

#AsPeriodicTask

<?php

namespace App\Scheduler\Task;

use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;

#[AsPeriodicTask(frequency: '1 day', from: '2024-01-01', until: '2024-06-12')]
class MySeasonalTask
{
    public function __invoke()
    {
        ...
    }
}

Workflow

#AsTranstionListener ...

<?php

namespace App\Workflow\EventListener;

class MyEventListener
{
	#[AsTransitionListener(workflow: 'my-workflow', transition: 'published')]
	public function onPublishedTransition(TransitionEvent $event): void
    {
        ...
    }
    
    // Mais aussi AsEnteredListener, AsCompletedListener, AsLeaveListener ...
}

Validator

NotBlank, Blank, NotNull, IsNull, IsFalse, Email, Lenght, Url, Regex, Json, Uuid, EqualTo, NotEqualTo, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual, Range ...

Bref.

Créons des attributs

Exemple 1
Feature Flag

Fonctionnement

<?php

#[FeatureFlag(enabled: false, method: 'foo')]
class Feature
{
	public function foo() { ... }
}

class MyService
{
	public function contructor(
    	private Feature $feature
    ) {}
    
    public __invoke()
    {
    	$this->feature->foo(); // Ne fait rien
    }
}

#FeatureFlag

<?php

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class FeatureFlag
{
	public function __construct(
    	public bool $enabled,
        public string $method
    )
}

FeatureFlagChecker

<?php

class FeatureFlagChecker
{
	public function __constructor(
    	private bool $enabled,
        private string $method,
        private $feature
    ) {}
    
    public function __call(string $name, array $arguments)
    {
    	if (
        	$name !== $this->method || 
            ($name === $this->method && true === $this->enabled)
        ) {
            return $this->feature->{$name}(...$arguments);
        }
        
        return;
    }
}

Kernel

<?php

final class Kernel extends BaseKernel
{
	protected function build(ContainerBuilder $container): void
    {
    	$container->registerAttributeForAutoconfiguration(
        	FeatureFlag::class,
            static function (
            	Definition $definition, 
                FeatureFlag $attribute,
                \ReflectionClass $reflector
            ) use ($container) {
            	...
            }
       );
       
       // ...
    }
}

Kernel

<?php

static function (
  Definition $definition, 
  FeatureFlag $attribute,
  \ReflectionClass $reflector
) use ($container) {
	$serviceName = sprintf('%s.feature_flag_checker', $reflector->getName());
    
	$container->register($serviceName, FeatureFlagChecker::class)
      ->setDecoratedService($reflector->getName())
      
      ->setArgument('$enabled', $attribute->enabled) // Info de l'attribut
      ->setArgument('$method', $attribute->method) // Info de l'attribut
      ->setArgument('$feature', new Reference($serviceName . '.inner')) // Notre feature
      
      ->setAutowired(true)
      ->setPublic(false);
}

😮‍💨

Exemple 2
Profiler

Fonctionnement

<?php

#[AsController]
class Action {

	#[Profile(name: 'my-profile', max: 0.05)]
	#[Route('/')]
	public __invoke(): Response
    {
    	...
    }
}

#Profile

<?php

#[Attribute(Attribute::TARGET_METHOD)]
class Profile {

	public function __construct(
        public string $name,
        public float $max,
    ) {
    }
}

ProfilerEventListener

<?php

#[AsEventListener(event: 'kernel.controller', method: 'onKernelController')]
#[AsEventListener(event: 'kernel.response', method: 'onKernelResponse')]
final class ProfilerEventListener
{
	public function __construct(
        private readonly LoggerInterface $logger
    ) {
    }

	public function onKernelController(ControllerEvent $event): void {}
    public function onKernelResponse(ResponseEvent $event): void {}
}

ProfilerEventListener

<?php

#[AsEventListener(event: 'kernel.controller', method: 'onKernelController')]
final class ProfilerEventListener
{
	public function onKernelController(ControllerEvent $event): void
    {
    	$attributs = $event->getControllerReflector()->getAttributes(Profile::class);

        /** @var ReflectionAttribute $attribut */
        foreach ($attributs as $attribut) {
        	/** @var Profile $attributeInstance **/
            $attributeInstance = $attribut->newInstance();
            
			$requestAttr = $event->getRequest()->attributes;
            $requestAttr->set('_profiler_id', $attributeInstance->name);
            $requestAttr->set('_profiler_max', $attributeInstance->max);
            $requestAttr->set('_profiler_start', microtime(true)); // Timer
        }
    }
}

ProfilerEventListener

<?php

#[AsEventListener(event: 'kernel.response', method: 'onKernelResponse')]
final class ProfilerEventListener
{
    public function onKernelResponse(ResponseEvent $event): void
    {
    	if ($event->getRequest()->attributes->has('_profiler_id')) {
            $start = $event->getRequest()->attributes->get('_profiler_start');
            $finish = microtime(true);
            $duration = $finish - $start;
            
            $max = $event->getRequest()->attributes->get('_profiler_max');
            if ($duration > $max) {
                $this->logger->alert('The profile took too long');
            }
        }
    }
}

🥹

Conclusion.
Les attributs c'est 💥

Rector 0.12

use Doctrine\ORM\Mapping as ORM;

-/**
- * @ORM\Column(type="string")
- */
+#[ORM\Column(type: "string")]

Transforme les annotations en attributs

VITE, JE DOIS CRÉER DES ATTRIBUTS !

Keep calm
and
listen to
instructions

Les attributs PHP

  • Ajoutent de la métadonnée
  • Permettent d'avoir configuration et code au même endroit
  • Vision KISS
  • Plus performant que les annotations
  • Mieux compris par les IDE

Les attributs PHP

  • Permettent l'ajout de comportement…
  • ... mais ne doivent pas être une dépendance
  • Ne sont pas des contrats
  • Un coût avec l'utilisation de ReflectionClass

Merci !

Kevin Balicot @kevinbalicot

Les attributs PHP

Attributs en PHP : un regard rétrospectif de la magie à l'adoption

By Kevin Balicot

Attributs en PHP : un regard rétrospectif de la magie à l'adoption

  • 315