Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Anotaciones a atributos

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ahora que estamos en PHP 8, vamos a convertir nuestras anotaciones de PHP en los atributos de PHP 8, que están más de moda. Refactorizar las anotaciones a atributos es básicamente... un trabajo muy ocupado. Puedes hacerlo a mano: los atributos y las anotaciones funcionan exactamente igual y utilizan las mismas clases. Incluso la sintaxis es sólo un poco diferente: utilizas dos puntos para separar los argumentos... porque en realidad estás aprovechando los argumentos con nombre de PHP. Genial.

Configurar Rector para actualizar las anotaciones

Así que la conversión es sencilla... pero uf, no me entusiasma hacer todo eso manualmente. Afortunadamente, ¡Rector vuelve al rescate! Busca "rector annotations to attributes" para encontrar una entrada del blog que te dice la configuración exacta de importación que necesitamos en rector.php. Copia estas tres cosas. Ah, y a partir de Rector 0.12, hay un nuevo objeto RectorConfig más sencillo que verás en esta página. Si tienes esa versión, no dudes en utilizar ese código.

Ah, y antes de pegar esto, busca tu terminal, añade todo... y luego confirma. Perfecto

De vuelta en rector.php, sustituye la única línea por estas cuatro líneas... excepto que no necesitamos la NetteSetList... y tenemos que añadir unas cuantas declaraciones use. Volveré a escribir la "t" en DoctrineSetList, pulsaré "tab", y haré lo mismo paraSensiolabsSetList.

35 lines rector.php
... lines 1 - 6
use Rector\Doctrine\Set\DoctrineSetList;
... lines 8 - 9
use Rector\Symfony\Set\SensiolabsSetList;
... lines 11 - 14
return static function (ContainerConfigurator $containerConfigurator): void {
... lines 16 - 24
$containerConfigurator->import(DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES);
$containerConfigurator->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES);
$containerConfigurator->import(SensiolabsSetList::FRAMEWORK_EXTRA_61);
... lines 28 - 33
};

Ahora, ya sabes lo que hay que hacer. Ejecuta

vendor/bin/rector process src

y mira lo que pasa. Vaya... ¡esto es impresionante! Mira! Ha refactorizado maravillosamente esta anotación a un atributo y... ¡lo ha hecho por todas partes! Tenemos rutas aquí arriba. Y todas nuestras anotaciones de entidades, como la entidad Answer también se han convertido. Eso fue una tonelada de trabajo... ¡todo automático!

... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends BaseController
{
#[Route(path: '/api/me', name: 'app_user_api_me')]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
public function apiMe(): \Symfony\Component\HttpFoundation\Response
{
... lines 14 - 16
}
}

... lines 1 - 11
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
... lines 20 - 203
}

Arreglando PHP CS

Aunque, como hace a veces Rector, estropeó algunas de nuestras normas de codificación. Por ejemplo, hasta el final, refactorizó esta anotación Route a un atributo... pero luego añadió un pequeño espacio extra antes del tipo de retorno Response. Eso no es un problema. Después de ejecutar Rector, siempre es una buena idea ejecutar PHP CS Fixer. Hazlo:

tools/php-cs-fixer/vendor/bin/php-cs-fixer fix

Me encanta. Un montón de correcciones para que nuestro código vuelva a estar en línea. Ejecuta

git diff

para ver cómo quedan las cosas ahora. La anotación Route se ha convertido en un atributo... y PHP CS Fixer ha vuelto a poner el tipo de retorno Response como estaba antes. Rector incluso refactorizó IsGranted de SensioFrameworkExtraBundle a un atributo.

Pero si sigues desplazándote hacia abajo hasta que encuentres una entidad... aquí vamos... ¡oh! ¡Se ha cargado los saltos de línea entre nuestras propiedades! No es súper obvio en el diff, pero si abres cualquier entidad... ¡vaya! Esto parece... estrecho. Me gustan los saltos de línea entre las propiedades de mis entidades.

... lines 1 - 9
class Answer
{
use TimestampableEntity;
public const STATUS_NEEDS_APPROVAL = 'needs_approval';
public const STATUS_SPAM = 'spam';
public const STATUS_APPROVED = 'approved';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'text')]
private $content;
... lines 22 - 48
public function getUsername(): ?string
... lines 50 - 113
}

Podríamos arreglar esto a mano... pero me pregunto si podemos enseñar a PHP CS Fixer a hacer esto por nosotros.

Abre php-cs-fixer.php. La regla que controla estos saltos de línea se llamaclass_attributes_separation con una "s" - lo arreglaré en un minuto. Ponlo en una matriz que describa todas las diferentes partes de nuestra clase y cómo debe comportarse cada una. Por ejemplo, podemos decir ['method' => 'one'] para decir que queremos una línea vacía entre cada método. También podemos decir ['property' => 'one'] para tener un salto de línea entre nuestras propiedades. También hay otro llamado trait_import. Ponlo también en one. Eso nos da una línea vacía entre nuestras importaciones de rasgos, que es algo que tenemos encima de Answer.

... lines 1 - 7
return $config->setRules([
... lines 9 - 10
'class_attributes_separation' => [
'elements' => ['method' => 'one', 'property' => 'one', 'trait_import' => 'one']
]
])
... line 15
;

Ahora prueba de nuevo con php-cs-fixer:

tools/php-cs-fixer/vendor/bin/php-cs-fixer fix

¡Ups!

Las reglas contienen fijadores desconocidos: "class_attribute_separation"

Quise decir class_attributes_separation con una "s". Sin embargo, qué gran error. Probemos de nuevo y... ¡genial! Ha cambiado cinco archivos, y si los compruebas... ¡han vuelto!

... lines 1 - 9
class Answer
{
use TimestampableEntity;
public const STATUS_NEEDS_APPROVAL = 'needs_approval';
public const STATUS_SPAM = 'spam';
public const STATUS_APPROVED = 'approved';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'text')]
private $content;
... lines 25 - 120
}

Con sólo unos pocos comandos hemos convertido todo nuestro sitio de anotaciones a atributos. ¡Guau!

A continuación, vamos a añadir tipos de propiedades a nuestras entidades. Eso nos va a permitir tener menos configuración de entidades gracias a una nueva función de Doctrine.

Leave a comment!

8
Login or Register to join the conversation
Cameron Avatar
Cameron Avatar Cameron | posted hace 1 mes

A brief comment on why php introduced attributes (and where to find more info) would be useful or what could be done with them. Then the training would be a bit more holistic instead of just the mechanics of upgrading. It's mine and likely a few other people's first time seeing this php change and we don't understand it

Reply
weaverryan Avatar weaverryan | SFCASTS | Cameron | posted hace 1 mes | HIGHLIGHTED

Hey Cameron!

Very fair suggestion! It's sometimes hard to back up as an instructor and remember all the little details. I can at least give some explanation here. In short, annotations were created around 10 years ago by Doctrine, mostly so that we could add the @ORM\Column annotations inside entities. Other languages have similar syntaxes. Over the years, these got more and more popular, with things like @Route, assertions for validation constraints, etc.

But, annotations were still not part of PHP's language. They're just comments, and Doctrine maintained a library that could read and parse those comments. It worked great, but it's still a bit odd for us to "invent" this new way of adding configuration into the comments system and not have it be part of PHP. And so, someone finally proposed putting into PHP itself. It has a slightly different syntax by necessity: we needed to introduce some new syntax that nobody could possibly be using already in their app (so that the introduction of PHP attributes wouldn't suddenly start making your code do something). Of course, there was a lot of debate on the syntax, and this was finally chosen as a balance between attractive/logical/simple.

I hope that helps - thanks for commenting about this :).

Cheers!

1 Reply
Fabrice Avatar

Hey! Attributes are great! But I have a question. I heard that you can use functions inside attributes uses for repetitive things.

I am thinking of a specific use case on API Platform for example (but this applies almost everywhere in reality).

Imagine, a Post entity. We want to create a custom action with API Platform that targets "/api/posts/count" to return the number of posts.

Now imagine that we would like to write the OpenAPI documentation for this action from our Post entity.

It could look like this:

#[ApiResource(
    collectionOperations:
        'get',
        'post',
        'count' => [
            'method' => 'GET',
            'path' => '/posts/count',
            'controller' => PostCountController::class,
            'openapi_context' => [
                'summary' => 'Get posts count',
                'parameters' => [
                    [
                        'in' => 'query',
                        'name' => 'online',
                        'schema' => [
                            'type' => 'integer',
                            'maximum' => 1,
                            'minimum' => 0
                        ],
                        'description' => 'Filter posts by online'
                    ],
                ],
                'response' => [
                    '200' => [
                        'description' => 'OK',
                        'content' => [
                            'application/json' => [
                                'schema' => [
                                    'type' => 'integer',
                                    'example' => 3
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
)]

Which is huge (I voluntarily took this kind of case but you can imagine that it applies to other things).

Therefore, the API Platform configuration of the Post entity is less readable and it would be necessary to move for example the entire value of the "count" key somewhere, in a private function, or even in an "api_doc" class which would contain functions with the configuration of each of our custom actions, and thus be able to simply call them from the entity.

Let's say we create a function getCountActionConfig() which would return all the content currently present in the 'count' key, allowing us to do:

#[ApiResource(
    collectionOperations:
        'get',
        'post',
        'count' => $this->getCountActionConfig()
)]

Only, it doesn't seem possible, since the call to the method will be underlined in red by PhpStorm with the following error: Constant expression contains invalid operations.

Moving the contents of the function directly into a constant would work I guess, but the goal would still be to do it through a function. And we could even go further by having the possibility of automatically generating the configuration by passing the name of the method, the description...

But a priori it does not seem possible. Do you have a solution for this?

Reply

Hi,

Have you tried static function?

#[ApiResource(
    collectionOperations:
        'get',
        'post',
        'count' => YourClassName::getCountActionConfig()
)]

Cheers!

Reply
Fabrice Avatar

Hello, sorry for delay, yes I tried and no, same problem, I'll have the same error Constant expression contains invalid operations

Reply

woh yeah, looks like the only way is to define a constant with the configuration you need and then it will work

I hope so )

Cheers!

Reply
Fabrice Avatar

Yes, this is so bad. I have to create a separate file that will contain a multitude of constants representing the configurations of my API Platform custom actions. It works, but...

It would be interesting if in a future PHP update we could use functions.

Thanks for your answers anyway!

Reply

Agree that's not very useful now, maybe something will change in future, so lets wait =)

Cheers and happy coding!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.6", // v3.6.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.5
        "doctrine/annotations": "^1.13", // 1.13.2
        "doctrine/dbal": "^3.3", // 3.3.5
        "doctrine/doctrine-bundle": "^2.0", // 2.6.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.0", // 2.11.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
        "knplabs/knp-time-bundle": "^1.18", // v1.18.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.6
        "sentry/sentry-symfony": "^4.0", // 4.2.8
        "stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.7
        "symfony/console": "6.0.*", // v6.0.7
        "symfony/dotenv": "6.0.*", // v6.0.5
        "symfony/flex": "^2.1", // v2.1.7
        "symfony/form": "6.0.*", // v6.0.7
        "symfony/framework-bundle": "6.0.*", // v6.0.7
        "symfony/mailer": "6.0.*", // v6.0.5
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/property-access": "6.0.*", // v6.0.7
        "symfony/property-info": "6.0.*", // v6.0.7
        "symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
        "symfony/routing": "6.0.*", // v6.0.5
        "symfony/runtime": "6.0.*", // v6.0.7
        "symfony/security-bundle": "6.0.*", // v6.0.5
        "symfony/serializer": "6.0.*", // v6.0.7
        "symfony/stopwatch": "6.0.*", // v6.0.5
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-chartjs": "^2.0", // v2.1.0
        "symfony/validator": "6.0.*", // v6.0.7
        "symfony/webpack-encore-bundle": "^1.7", // v1.14.0
        "symfony/yaml": "6.0.*", // v6.0.3
        "symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/string-extra": "^3.3", // v3.3.5
        "twig/twig": "^2.12|^3.0" // v3.3.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
        "phpunit/phpunit": "^9.5", // 9.5.20
        "rector/rector": "^0.12.17", // 0.12.20
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/maker-bundle": "^1.15", // v1.38.0
        "symfony/var-dumper": "6.0.*", // v6.0.6
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.6
        "zenstruck/foundry": "^1.16" // v1.18.0
    }
}