
Compartilhar:
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.
Segurança de tipos bem feita — Hacking de arrays em PHP
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 porArrayList)ArrayList(matriz indexada, cria um novo objeto para dimensionamento)Hashmap(matriz com hash, implementação deMap)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:
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.