Attributes - PHP

Attributes - PHP

Hoje vamos falar de uma feature que foi introduzida no PHP 8, os Attributes.

Para que serve esses Attributes? Que melhoria trarão ao meu software? Vamos ver e analisar essas perguntas neste artigo.

Visão geral dos Attributes

Os Attributes oferecem a capacidade de adicionar metadados (informações) a: Classes, métodos, funções, parâmetros, propriedades e constantes de classe.

Estes metadados serão declarados no codigo, podem ser resgatados em tempo de execução usando a Reflection API.

Reflection Api
Reflection Api é uma API completa que adiciona a capacidade de examinar classes, interfaces, funções, métodos e extensões. Além disso, a API de reflexão oferece maneiras de recuperar comentários de documentos para funções, classes e métodos. (Em um futuro proximo vamos falar sobre, fiquem atentos 😉😉)

Os atributos poderiam, portanto, ser pensados ​​como uma linguagem de configuração incorporada diretamente no seu código.

Sintaxe

Existem várias partes na sintaxe dos atributos.

Primeiro, uma declaração de atributo é sempre delimitada por um início #[e um final correspondente ]. Dentro, um ou mais atributos são listados, separados por vírgula. Os argumentos para o atributo são opcionais, caso decida usar, são os parênteses usuais (). Os argumentos para atributos só podem ser valores literais ou expressões constantes.

Embora não seja estritamente obrigatório, é recomendado criar uma classe real para cada atributo.

Em um exemplo mais simples, é necessário apenas uma classe vazia com a expressão #[Attribute] declarada que pode ser importado usando a expressão use Attribute; .

Veja um exemplo simples:

<?php

namespace Example;

use Attribute;

#[Attribute]
class MyAttribute
{
}

Vamos ver como pode passar, parametros e manipular constantes de um Attribute.

Veja o exemplo abaixo:

<?php

// a.php
namespace MyExample;

use Attribute;

#[Attribute] // Declaração de um attibute
class MyAttribute
{
    const VALUE = 'value';

    private $value;

    public function __construct($value = null)
    {
        $this->value = $value;
    }
}

Muito simples até agora, uma classe normal, com a notação que é um Attribute.

Agora, vamos usar esse nosso novo attribute. No codigo abaixo importamos o nosso attribute chamando o seu namespace. Podemos passar atraves do construtor parametros de varias maneiras. Podemos até dentro da mesma assinatura passar 2 attributes, como mostra o exemplo na classe AnotherThing

<?php 

// b.php

namespace Another;

use MyExample\MyAttribute;

#[MyAttribute]
#[MyAttribute]
#[MyAttribute(1234)]
#[MyAttribute(value: 1234)]
#[MyAttribute(MyAttribute::VALUE)]
#[MyAttribute(array("key" => "value"))]
#[MyAttribute(100 + 200)]
class Thing
{
}

#[MyAttribute(1234), MyAttribute(5678)]
class AnotherThing
{
}

Para restringir o uso ao qual um atributo pode ser usado, uma máscara de bits pode ser passada como primeiro argumento do #[Attribute] .

No exemplo abaixo, o Attribute apenas sera usado em metodos e funções. Declarar MyAttribute em outro tipo, que não seja Metodos ou Função resultara em uma exceção durante a chamada para ReflectionAttribute::newInstance()

<?php

namespace Example;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class MyAttribute
{
}

Uma lista detalhada dos targets

  • Attribute::TARGET_FUNCTION

  • Attribute::TARGET_METHOD

  • Attribute::TARGET_PROPERTY

  • Attribute::TARGET_CLASS_CONSTANT

  • Attribute::TARGET_PARAMETER

  • Attribute::TARGET_ALL

Pegou a Ideia?

Perfeito, agora vamos ver um exemplo da vida real.

Show me the Code

Vamos fazer o seguinte: vou criar uma classe, que sera usada como attribute e será responsavel por fazer a Serialização de uma valor.

Eu vou definir que esse Attribute apenas será usado em constantes da classe e em propriedades. Usando as Constructor property promotion fica mais facil declarar propriedades. Repare que no construtor eu define um valor que por padrão é null, quer dizer que ele vai esperar a passagem de 1 parametro do tipo string e o parametro será opcional.

<?php

#[Attribute(Attribute::TARGET_CLASS_CONSTANT|Attribute::TARGET_PROPERTY)]
class JsonSerialize
{
    public function __construct(public ?string $fieldName = null) {}
}

De seguinda, vou criar uma outra classe, que vai usar o Attribute nas suas propriedades. No

<?php
class UserLandClass
{

    #[JsonSerialize('foobar')]
    public string $myValue = '';

    #[JsonSerialize('companyName')]
    public string $company = '';

    #[JsonSerialize('Value3')]
    public string $number = '';

}

Agora vamos criar uma implementação dos Attributes, porque no final os attributes não fazem magia, precisa existir uma implementação que faça as coisas acontecerem, no nosso caso seriallizar um valor.

Essa classe se vai chamar AttributeBasedJsonSerializer e vai ter um metodo que vai serializar os dados, e depois devolver um Json. Mas antes vamos precisar de extrair as propriedades e entregar esse valor pronto a função json_encode($data, JSON_THROW_ON_ERROR); Vamos fazer isso dentro do metodo extract

<?php
class AttributeBasedJsonSerializer 
{
 protected const ATTRIBUTE_NAME = 'JsonSerialize'; // Nome do atributo

public function serialize($object)
 {
     $data = $this->extract($object);
     return json_encode($data, JSON_THROW_ON_ERROR);
 }
}

Aqui é onde a magia acontece, vou receber o objecto e usar a Reflection Api para pegar as informações das propriedades que estão a usar os Attributes. Criei um metodo separado onde vou cuidar dessa coisa toda, ele vai retornar um array com as propriedades.

// Extract

private function extract(object $object)
  {
        $data = [];
        $reflectionClass = new ReflectionClass($object);
        $data = $this->reflectProperties($data, $reflectionClass, $object);

        return $data;
  }

Basicamente esse medoto faz oi seguinte:

  1. Recupera todas as propriedades que estão a usar o atributo JsonSerialize

  2. Recuperar o valor delas

  3. Empacotar tudo em um array

  4. Retornar esse valor

// Reflection Properties Methods
private function reflectProperties(array $data, ReflectionClass $reflectionClass, object $object)
    {
        $reflectionProperties = $reflectionClass->getProperties();
        foreach ($reflectionProperties as $reflectionProperty) {
            $attributes = $reflectionProperty->getAttributes(static::ATTRIBUTE_NAME);
            foreach ($attributes as $attribute) {
                $instance = $attribute->newInstance();
                $name = $instance->fieldName ?? $reflectionProperty->getName();
                $value = $reflectionProperty->getValue($object);
                if (is_object($value)) {
                    $value = $this->extract($value);
                }
                $data[$name] = $value;
            }
        }

        return $data;
    }

A implementação final ficou assim:

<?php

declare(strict_types=1);

//--------------------------------------------------------

#[Attribute(Attribute::TARGET_CLASS_CONSTANT | Attribute::TARGET_PROPERTY)]
class JsonSerialize
{
    public function __construct(public ?string $fieldName = null)
    {
    }
}

//--------------------------------------------------------
class UserLandClass
{
    #[JsonSerialize('foobar')]
    public string $myValue = '';

    #[JsonSerialize('companyName')]
    public string $company = '';

    #[JsonSerialize('Value3')]
    public string $number = '';
}

//--------------------------------------------------------

class AttributeBasedJsonSerializer
{

    protected const ATTRIBUTE_NAME = 'JsonSerialize';

    public function serialize($object)
    {
        $data = $this->extract($object);
        return json_encode($data, JSON_THROW_ON_ERROR);
    }

    private function reflectProperties(array $data, ReflectionClass $reflectionClass, object $object)
    {
        $reflectionProperties = $reflectionClass->getProperties();
        foreach ($reflectionProperties as $reflectionProperty) {
            $attributes = $reflectionProperty->getAttributes(static::ATTRIBUTE_NAME);
            foreach ($attributes as $attribute) {
                $instance = $attribute->newInstance();
                $name = $instance->fieldName ?? $reflectionProperty->getName();
                $value = $reflectionProperty->getValue($object);
                if (is_object($value)) {
                    $value = $this->extract($value);
                }
                $data[$name] = $value;
            }
        }

        return $data;
    }

    private function extract(object $object)
    {
        $data = [];
        $reflectionClass = new ReflectionClass($object);
        $data = $this->reflectProperties($data, $reflectionClass, $object);

        return $data;
    }
}

Agora vamos colocar isso a funcionar.

<?php

$userLandClass = new UserLandClass();
$userLandClass->company = 'second';
$userLandClass->myValue = 'my second value';
$userLandClass->number = 'Alguma coosa';

$serializer = new AttributeBasedJsonSerializer();
$json = $serializer->serialize($userLandClass);

var_dump($json);
/**
string(75) "{"foobar":"my second value","companyName":"second","Value3":"Alguma coosa"}"
*/

Fantastico, não é?

Qualquer propriedade de uma classe que quisermos transformar em Json precisaremos apenas de adicionar esse nosso novo Attribute e já está feito.

Conclusão

Neste artigo vimos uma visão geral dos attributes e como de maneira pratica podemos usar. Qualquer duvida, critica, sugestão use a zona dos comentarios para isso.

Se gostou deste artigo, lhe convido a assinar a minha newsletter e acompanhar meus Posts sobre PHP, Laravel, e carreira no mundo de Dev.

Por hoje é tudo, valeuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu

Did you find this article valuable?

Support Tilson Mateus by becoming a sponsor. Any amount is appreciated!