https://a.storyblok.com/f/270183/57319/61dc887e0a/type-safety-done-right.png

Segurança de tipos bem feita — Hacking de arrays em PHP

Publicado em October 31, 2023

Tempo de leitura: 6 minutos

Nos primeiros cinco ou seis anos da minha experiência no desenvolvimento de aplicativos PHP em grande escala, enfrentei e criei muitos dos problemas que as pessoas que estão começando no desenvolvimento de back-end provavelmente já viram com frequência. Só agora, especialmente depois de herdar o SDK PHP da Vonage, que percebi que estou em uma posição em que posso orientar outras pessoas a não cometerem os mesmos erros. Na verdade, esses problemas decorrem, em grande parte, da maneira como eu via o PHP em particular ser escrito ao longo dos anos, independentemente do uso ou não de um framework. Vamos lançar a primeira granada:

Os arrays do PHP são meio ruins

OK, elas não são horríveis, seria estranho dizer isso. O que quero dizer é que, a maneira como costumam ser usadas é horrível. Há um motivo muito bom para isso: comecei com PHP como minha primeira linguagem (depois de VBA e SQL), eu não tinha noção de como outras linguagens lidavam com coleções de dados. Simplesmente presumi que fossem iguais ao PHP:

  • Você tem uma matriz indexada, que possui uma chave numérica ou

  • Você tem um array associativo, que possui chaves alfanuméricas definidas.

Ambas são consideradas do mesmo tipo de variável, ou seja, array, pois o mecanismo já determinou que tipo de matriz você deseja criar.

$myAssociatedArray = ['foo' => 'bar']; // array
$myHashedArray = ['foo', 'bar']; // also array

É assim que provavelmente tudo o mais funciona, certo?

Não, não é. Minha inexperiência no uso exclusivo do PHP fez com que eu não estivesse ciente da seguinte afirmação:

Em todas as outras principais linguagens de backend, os arrays indexados e associativos são divididos em duas ou mais classes-base diferentes.

Além disso, “matrizes associativas” é um termo usado apenas no PHP. Daqui em diante, vamos chamar matrizes associativas matrizes com hash , pois esse é o termo mais utilizado em outros lugares.

Veja a seguir como funciona em outros idiomas:

Node.js

  • Array (matriz indexada)

  • Object (matriz com hash)

Python

  • List (matriz indexada)

  • Dictionary (matriz com hash), usando compreensão de lista

Ruby

  • Array (matriz indexada)

  • Hash (matriz com hash)

Vale ressaltar que o Ruby, na verdade, pode ter uma matriz associativa da mesma forma que o PHP, mas, devido à existência do objeto `Hash`, é muito raro ver uma matriz sendo usada como um hash.

Go (lang)

  • Slice (matriz indexada)

  • Map (matriz com hash)

Java

  • Array (matriz indexada, primitiva)

  • List (interface, implementada por ArrayList)

  • ArrayList (matriz indexada, cria um novo objeto para dimensionamento)

  • Hashmap (matriz com hash, implementação de Map)

  • LinkedHashMap (matriz hash, reordena a ordem de inserção)

Vale a pena observar que, como você pode ver, o Java é —muito— rigoroso com as estruturas de dados do tipo conjunto. Todas elas são mutáveis, no sentido de que é possível alterar seus valores, mas não é possível alterar o tamanho do objeto no que diz respeito aos seus nós. É por isso que, no ArrayList, pois, na verdade, ele cria um novo objeto ao ser modificado.

C#

  • Array (matriz indexada)

  • List (matriz indexada com genéricos, mutável)

  • Dictionary (matriz com hash)

Portanto, considerando que todas essas diferentes classes fazem parte da API básica dessas linguagens, isso significa que todas elas têm a capacidade de impor um comportamento de tipos mais rigoroso.

E daí?

Não temos isso no PHP. Isso significa que que os arrays do PHP podem, de certa forma, ser qualquer coisa. É esse fato que resultou no tipo de código que venho lendo há anos, que se parece com isto:

public function updateRecord(int $id, array $options) {
	// do some stuff here
}

Quero atualizar o ID de algo. E então... tenho $options. Porque são opções. Mas é um array simples de PHP. O que há nele? A função não tem como saber.

O que há de errado com esse código? Se for para me basear na minha experiência pessoal, quatro horas de xdebug de depuração passo a passo para descobrir de onde veio uma chave de array. Não temos ideia de como ela deveria ser. Esse pessoal pesadelo meu é algo que eu mesmo costumava programar por anos, especialmente em agências, onde tempo é dinheiro. Se a única preocupação for “Isso funciona de acordo com os requisitos do cliente?”, então se vai economizar o máximo possível para entregar no prazo. E a pessoa que vem depois de você e precisa corrigir um bug introduzido por algum efeito colateral? Isso é dívida técnica em ação.

OK, e agora?

O caminho que segui para obter um código sustentável foi usar dicas de tipo em todos os lugares, o tempo todo. Esse é o tipo de desenvolvimento de software que, se feito de forma consistente e minuciosa, deveria resultar no mítico código autodocumentado. Mas só podemos usar dicas de tipo em um array, então isso não nos ajuda neste caso. Portanto, temos três opções:

  • Genéricos

  • Formato da matriz (por meio de Generics “modificados”)

  • Objetos de valor

Genéricos

Desculpe, eu te enganei: não temos genéricos no PHP devido a limitações do próprio mecanismo. No entanto, como você deve ter notado que esse termo tem sido bastante mencionado, considere o seguinte código do TypeScript:

interface UserProfile {
    id: string;
    name: string;
    age: integer;
}

class Collection<Type> {
    private items: Type[] = [];

    add(item: Type) {
        this.items.push(item);
    }

    get(index: number): Type | undefined {
        return this.items[index];
    }
}

const userProfiles = new Collection<UserProfile>();

Como alguém que se identifica totalmente com a filosofia de “tornar esse PHP o mais robusto possível, mais resistente do que o aço valiriano!”, é uma verdadeira alegria trabalhar com genéricos como esse. Os <> parênteses indicam que, ao criar um novo Collection objeto, ele deve conter apenas o tipo especificado durante sua instanciação. Portanto, quando criamos um novo Collection<UserProfile>, estamos dizendo que esse objeto só pode conter UserProfile objetos. Isso, amigos e inimigos, é código autodocumentado.

Existe alguma outra maneira de fazer isso no PHP?

Que bom que você perguntou, sim, é possível! Isso vai nos levar ao segundo ponto da lista, que é a definição de um array. Isso parece tão bobo para mim dizer isso, mas até eu entrar na Vonage nunca tinha me ocorrido que eu poderia estender a partir objetos do SPL dentro da API do PHP. Portanto, você pode estender arrays!

class Collection extends \ArrayObject {

	protected function __construct(...$args) {
		parent::__construct(...args);
	}
}

Atualmente, isso não faz nada além de passar os argumentos para a classe pai usando o operador splat, que é um ArrayObject. Mas nós a estendemos, o que significa que podemos sobrecarregar esse comportamento!

Precisamos fazer duas coisas aqui:

  • Defina um tipo, da mesma forma que fizemos no TypeScript

  • Sobrecarregue o offsetSet() método, que é o método SPL para adicionar elementos a um array (seja ao adicionar um valor após a instanciação, seja durante a criação do construtor).

Então, vamos abordar essa questão:

class Collection extends \ArrayObject {  

	protected $typeError = 'Only UserProfile objects can be added!'
	
    public function __construct(...$args) {  
    
        foreach ($args as $arg) {  
            if (!$arg instanceof UserProfile) {  
                throw new \TypeError($this->typeError);  
            }  
        }  
        
        parent::__construct(...$args);  
    }  
  
    public function offsetSet($key, $value): void  
    {  
  
        if (!$value instanceof UserProfile) {  
            throw new \TypeError($this->typeError);  
        }  
  
        parent::offsetSet($key, $value);  
    }  
}

E pronto. offsetSet foi sobrecarregado para que apenas UserProfile objetos possam ser adicionados; e, ao criar o Collection você pode passar qualquer número de argumentos, desde que sejam UserProfile sejam objetos.

Objetos de valor

Existe uma solução muito mais simples para o problema de passar matrizes de um lado para outro: não faça isso. Continuei o trabalho realizado pelos meus antecessores no SDK do PHP Core da Vonage para garantir que tudo o que for passado para os métodos do Client seja um objeto de valor.

Sim, há desvantagens nisso. Isso significa que as propriedades da classe precisam ser definidas e que métodos getter e setter precisam ser adicionados. Isso significa mais código. No entanto, também contamos com ferramentas para tornar o código menos pesado no PHP moderno, seja com o uso de enum ou da promoção de propriedades no construtor, acompanhada de modificadores de acesso para manter o número de linhas de código baixo.

Um bom exemplo disso foi quando desenvolvi a implementação do SDK do PHP para Verify v2. O envio de uma solicitação de verificação ficaria assim sem objetos de valor:

$payload = [
	'locale' => 'en_us',
	'channel_timeout' => 300,
	'client_ref' => 'a-reference',
	'code_length' => 4,
	'workflow' => [
		'channel' => 'sms',
		'to' => '123456789'
	]
];

$myVonageClient = new Client('apiKey', 'apiSecret');
$myVonageClient->verify2()->startVerification($payload);

O objeto cliente não tem como saber que esse array está sendo passado para ele. E se uma chave estiver errada? E se faltar uma chave? Não há nada no código que indique como ele se comportará: e se eu tivesse colocasse a validação no startVerification() método, você teria que vasculhar todo o meu código para descobrir o que estava passando pela minha cabeça.

Em vez disso, ao usar objetos de valor, transferimos a lógica para um PHP bem tipado. O código passa a ser autodocumentado e mais robusto por definição.

class SMSRequest extends BaseVerifyRequest  
{
	public function __construct(  
	    protected string $to,  
	    protected string $brand,  
	    protected ?VerificationLocale $locale = null,  
	) {  
	    if (!$this->locale) {  
	        $this->locale = new VerificationLocale();  
	    }  
	  
	    $workflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_SMS, $to);  
	  
	    $this->addWorkflow($workflow);  
	}
}

Esse objeto agora é passado para o startVerification() método. O que você pode ou não fazer nos objetos passados agora pode ser definido no nível mais baixo do código. Por exemplo, aqui está o VerificationLocale objeto.

class VerificationLocale  
{  
    private array $allowedCodes = [  
        'en-us',  
        'en-gb',  
        'es-es',  
        'es-mx',  
        'es-us',  
        'it-it',  
        'fr-fr',  
        'de-de',  
        'ru-ru',  
        'hi-in',  
        'pt-br',  
        'pt-pt',  
        'id-id',  
    ];  
  
    public function __construct(protected string $code = 'en-us')  
    {  
        if (! in_array($code, $this->allowedCodes, true)) {  
            throw new \InvalidArgumentException('Invalid Locale Code Provided');  
        }  
    }  
  
    public function getCode(): string  
    {  
        return $this->code;  
    }  
  
    public function setCode(string $code): static  
    {  
        $this->code = $code;  
  
        return $this;  
    }  
}

E aí está. Código autodocumentado. Sim, é preciso criar todos os objetos para interligá-los, mas o comportamento do nosso SDK é rigoroso e explícito.

Compartilhar:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondePromotor Sênior de Desenvolvimento em PHP

Sou ator formado, com uma dissertação sobre stand-up comedy, e comecei a me dedicar ao desenvolvimento em PHP por meio dos encontros da comunidade. Você pode me encontrar dando palestras e escrevendo sobre tecnologia, ou ouvindo e comprando discos curiosos da minha coleção de vinil.