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
Rétrospective et explications Les nouveautés Utilisation des attributs dans Symfony Exemple d'application Conclusion
Les annotations
En programmat ion , une annota tion est un élém ent permett ant d ' ajout er 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
|
|
|
|
|
#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 !
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
{
...
}
}!
#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")]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