PHP - arrays e valores como chave
Tags: php, arrays, webdev, backend
Published: 2024-04-13 10:27:40

**Qual a maneira mais eficaz, tanto de escrita como de processamento, de alcançarmos um key by no PHP?**

### O problema

Na minha pequena carreira de programador (já estou perto dos 6 anos nisto), um dos maiores problemas que encontro, com certa frequ**ê**ncia, a trabalhar com arrays é o típico *key by -* que em termos gerais consiste em utilizar um valor do array multidimensional como a chave para ele mesmo.

Um exemplo de um array que tipicamente podemos encontrar no dia a dia (embora reduzido para exemplificar).

```php
<?php

$products = [
    [
        'id' => 437,
        'name' => 'Sapatilhas Amarelas',
        'price' => 3500,
        'slug' => 'sapatilhas-amarelas'
    ],
    [
        'id' => 548,
        'name' => 'Sapatilhas Azuis',
        'price' => 7500,
        'slug' => 'sapatilhas-azuis'
    ],
];
```

Agora independentemente do motivo, imaginemos que precisamos de encontrar um dos artigos através da chave `slug`.

### Encontrar um array baseado na key do valor

Existe duas formas comuns de alcançar isso, sendo a mais comum e amadora, efetuar um foreach até encontrarmos o valor que pretendemos - pode-se ver isto como efetuar um [brute force attack](https://en.wikipedia.org/wiki/Brute-force_attack) ao array.

Outra abordagem é o [array\_filter](https://www.php.net/manual/en/function.array-filter.php), que de certa forma é um foreach que valida se um condição é verdade e decide excluir ou manter o registo do array que proporcionamos. A vantagem deste meio é que podemos atribuir o resultado diretamente a uma variável e usar uma [função anónima](https://www.php.net/manual/en/functions.anonymous.php) ou um [função de arrow](https://www.php.net/manual/en/functions.arrow.php).

A desvantagem é que se pretendemos obter só um 1 registo teremos que chamar a primeira posição do array - pois apenas foi removido do array os valores que não pretendemos.

Tipicamente podiamos fazer um foreach até encontrar o que queremos, da seguinte forma:

```php
<?php

$slug = 'sapatilhas-azuis';
$store = [];

foreach($products as $product) {
    if($product['slug'] === $slug) {
        $store = $slug;
        continue;
    }
}
```

Usando o [onlinephp.io](https://onlinephp.io/) para efetuar os benchmarks, com **50 000 registos no array**, este metódo de efetuar um foreach demorou em média `0,0055` **segundos** o que podemos considerar como "nada".

Por outro lado se efetuarmos um array\_filter com o mesmo número de artigos:

```php
<?php

$slug = 'sapatilhas-azuis';
$store = [];

$filteredProducts = array_filter(
    $products, 
    fn($product) => $product['slug'] === $slug
);

$product = $filteredProducts[0];
```

Conluimos que, infelizmente *apesar de ser mais elegante não parece o mais eficiente*, pois a média resulta em `0,0107` **segundos**, o que representa um acréscimo de **~50% de tempo**.

Estes processos são insignificantes quando apenas queremos ir buscar uma key. Mas quando precisamos de repetir a busca no mesmo processo, e aumentamos a complexidade da condição, já começa a ser bastante custuoso tanto em termos de código como processamento desnecessário.

### Qual a forma mais correta de efetuar o key by?

Podemos simplesmente dizer que o mais eficiente é fazer um *loop* ao array e colocar a propria key como o valor que pretendemos.

```php
<?php

function keyBy(string $key, array $values): array {
    $array = [];

    foreach($values as $value){
        // antes de cada associamente deviamos
        // primeiro validar se a key existe
        // no array disponibilizado
        $array[$value[$key]] = $value;
    }

    return $array;
}

$formated = keyBy('slug', [ /* array de produtos */ ])
```

Este processo demorou em média `0,0123` segundos. Mais uma vez, nada de mais.

Resumidamente podemos concluir que este processo é o melhor e mais rápido deles todos, no qual até a [framework Laravel utiliza](https://github.com/laravel/framework/blob/10.x/src/Illuminate/Collections/Collection.php#L539) este mecanismo para alcançar o keyBy - apesar de existir mais algumas validações para garantir consistência no resultado.

Isto é a solução mais correta quando temos a disponibilidade de ter uma função global para isto.

Mas nem sempre isso é o caso - recentemente estive a implementar uma funcionalidade num produto relativamente antigo, onde não me sinto à vontade de começar a criar funções a *torto e a direito no código* - então queria uma solução mais elegante/"bonita" que podesse repetir sem caso fosse necessário:

```php
<?php
$products = [ /* ... */ ];

$keys = array_column($products, 'slug');
$array = array_combine($keys, $products);

$product = $array['sapatilhas-azuis'];
```

Esta solução tem como duração média `0,0068` segundos - significativamente mais rápido que o foreach.

**Passando a explicar:**

Com o array\_column extraimos todos os valores que estão associados à key *slug* do array *products*.

De seguida tiramos todo o partido do array\_combine que vai utilizar os valores do `array_column` para associar esse resultado às keys do array dos produtos.

Mas há um pequeno inconveniente com esta solução! Ao contrário do foreach, no qual podemos ter um fallback caso o campo não exista no array, com esta solução não é possível.

A falta do valor irá resultar num missmatch de contagem de *keys*:

`Fatal error: Uncaught ValueError: array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements in...`

### Conclusão

Pode-se concluir que, a maneira mais correta é de facto utilizar um `foreach`, mas se a maneira mais correta for uma impossibilidade - tanto em termos de rapidez de execução e desenvolvimento do programador - podemos utilizar a solução do `array_combine` + `array_combine` - que é uma extremamente intuitiva e rápida de compreender.