PHP para Iniciantes: Estruturas de Controle - MATCH

No artigo anterior, falamos um pouco sobre a estrutura switch, que nos dá o poder de criar um chaveador ou seletor baseado no valor final de uma expressão. Um dos detalhes que comentei lá, é que a comparação que é feita entre o valor e os cases do switch é feita de forma não-rígida, ou seja, usa o == ao invés do ===.

E por que você tá mencionando tudo isso, Kiko?

Porque, na estrutura de hoje, essas são algumas das principais diferenças, além do fato que o match funciona mais ou menos como uma função, retornando alguma coisa.

Vale ressaltar: o match surgiu super recentemente, na versão 8.0 do PHP. Nós recebemos hoje, 28 de Outubro de 2021, o lançamento oficial do PHP 8.1.0-RC5, pra você ter ideia de que realmente foi muito recente.

Reflexão sobre semântica

Não tem como falar de match sem pensar nas aplicações que mais usam esse termo no mercado, né? Pode falar que lembrou do Tinder, não precisa fazer cara de paisagem.

Safadinho...

A questão é que o Tinder abusou desse termo ao criar o conceito "perfect match", porque a ideia de match já é sobre ser exato, preciso, perfeito. É redundância, sabia? Mas tudo bem, produto é produto e os consumidores compraram bem a ideia.

De qualquer forma, vamos esquecer que existe o termo redundante e tratar como exemplo casos onde os parceiros tem exatamente os mesmos gostos. Com isso, pra não deixar o exemplo muito difícil de entender, vamos imaginar que os indivíduos só tem duas escolhas: cor preferida e número favorito.

Nós estamos desenvolvendo um algoritmo que cruze os dados da Juliana, que tem cor preferida roxo e número favorito 8, com as informações de Tom, Rafael e Luan.

  • Tom: branco, 8;
  • Rafael: roxo, 3;
  • Luan: roxo, 8.

Obs.: nomes em homenagem a alguns amigos, mas essa relação de Tinder é só resenha viu? Pelamor, tem gente casada/comprometida aí nessa lista. Até a Ju, inclusive, hahuhauhauhau. Mas saudades, caso acabem lendo isso aqui.

Visualmente, você percebe que ambos tem algo em comum com a Juliana, mas somente Luan pode passar no match.

Tecnicamente falando, mesmo se Rafael tivesse o número favorito 8.0 (float), ainda não passaria no match.

Dito isso, uma escrita semântica para essa estrutura seria DADA (expressão), CASO EXATAMENTE IGUAL A (caso[1]) ENTÃO (retorno[1]), CASO EXATAMENTE IGUAL A (caso[2]) ENTÃO (retorno[2]), (...), CASO EXATAMENTE IGUAL A (caso[n]) ENTÃO (retorno[n]), CASO NENHUMA DAS OPÇÕES ANTERIORES, (retorno[n+1]).

Ué, Kiko, isso não a mesma coisa que o switch?

Não exatamente. Tem algumas diferenças:

  • switch compara com ==, match compara com ===;
  • match retorna o valor final da expressão selecionada, quando possível (você pode disparar Exceptions, daí não teria o que retornar, não é mesmo?);
  • match tem uma sintaxe parecida, mas fundamentalmente diferente. Ela não recebe um bloco de instruções, por exemplo, mas aceita expressões que gerem um valor final. É como se fosse um dicionário de arrow functions.

A sintaxe

Assim como switch, o match inicia sua sintaxe com a declaração da estrutura e a expressão que será a base das comparações, ficando match($expressao). Seu bloco de código também é encapsulado por chaves ({ }). Aqui não tem sintaxe alternativa (a que chamo de procedural), então pode se prender às chaves mesmo.

Porém, sua sintaxe interna é mais conectada a estrutura de arrays, aquela escrita chave => valor, onde tanto a chave quanto o valor podem ser expressões.

Hmmm... Se eu preciso informar uma chave em tudo, como eu determino a saída padrão, Kiko?

Usando a palavra reservada default na chave! Além disso, assim como o switch, é possível atribuir o mesmo valor para mais de um caso. Mas a forma de escrever isso é diferente: você pode listar várias chaves separando por vírgulas, escrevendo chave1, chave2 => valor. Vou bolar outro caso pra mostrar isso mais abaixo.

Dito tudo isso, nosso primeiro exemplo de hoje pode ser escrito assim:

<?php

$juliana = ['roxo', 8];

$tom = ['branco', 8];
$rafael = ['roxo', 8.0]; // coloquei o 8.0 pra validar o ===
$luan = ['roxo', 8];

$resultado = match($juliana) {
    $tom => 'deu match com Tom!',
    $rafael => 'deu match com Rafa!',
    $luan => 'deu match com Luan!',
    default => 'não deu match com ninguém...'
};

var_dump($resultado); // string(deu match com Luan!)

Veja que, com switch, teria dado Rafael:

<?php

$juliana = ['roxo', 8];

$tom = ['branco', 8];
$rafael = ['roxo', 8.0]; // coloquei o 8.0 pra validar o ==
$luan = ['roxo', 8];

switch ($juliana) {
    case $tom:
        $resultado = 'deu switch com Tom!';
        break;
    case $rafael:
        $resultado = 'deu switch com Rafa!';
        break;
    case $luan:
        $resultado = 'deu switch com Luan!';
        break;
    default:
        $resultado = 'não deu switch com ninguém...';
        break;
}

var_dump($resultado); // string(deu switch com Rafa!)

Com isso, comprovamos a grande diferença no comparador usado por baixo no switch e no match, certo? Mas eu ainda não falei sobre usar mais de um caso para o mesmo valor. Para isso, vamos imaginar o seguinte caso de uso:

Eu, como consumidor,

quero saber o status do meu pedido,

para poder acompanhar o fluxo até a entrega.

Isso é bem comum em vários projetos, certo? Você pode ter pensado em pedido de comida, mas na verdade pode ser até uma compra on-line, de qualquer objeto. Uma Sexy Shop, por exemplo, pode ter esse caso de uso em sua loja on-line. Então isso está muito vago, concorda? Mas já que mencionei Sexy Shop, vamos continuar com esse caso.

Buy Buy Buy Buy

Nessa loja, quando um pedido é criado, algumas coisas precisam ser levadas em consideração:

  1. O local de entrega do meu cliente é próximo à loja?
  2. O(s) produto(s) do pedido está(ão) disponível(eis) em meu estoque?
  3. Meu cliente deseja uma embalagem discreta?

No momento em que o indivíduo se faz essas perguntas, nós concluímos que o cliente já:

  1. Iniciou um pedido;
  2. Adicionou produtos no pedido;
  3. Incluiu endereço de entrega;
  4. Efetuou pagamento (nós nem mencionamos pagamento ali em cima, então vamos supor que já está pago).

Mas Kiko, por que raios você tá mencionando tudo isso?

Pra evidenciar que o pedido passou por diversos pontos-chave e já podemos prever outras partes do processo. O pedido foi:

  1. Iniciado;
  2. Endereçado;
  3. Pago.

E o pedido irá ser:

  1. Embalado;
  2. Despachado;
  3. Entregue.

São seis status pra generalizar o caso de uso. Vamos desde já criar as constantes que representarão esses seis status no código:

<?php // CartStatusEnum.php

class CartStatusEnum
{
    public const STARTED = 'iniciado';
    public const ADDRESSED = 'endereçado';
    public const PAID = 'pago';
    public const PACKED = 'embalado';
    public const DISPATCHED = 'despachado';
    public const DELIVERED = 'entregue';
}

Podemos ver em alguns projetos por aí que é comum exibir todos os status de forma transparente para o consumidor, mas há quem prefira só exibir status importantes, que será o que faremos agora. Nós queremos que o cliente veja somente os status:

  1. Aberto: quando o pedido está iniciado;
  2. Aguardando pagamento: quando o pedido está endereçado;
  3. Entregando: quando o pedido está pago, embalado ou despachado;
  4. Entregue, que é o último status mesmo.

Precisaremos, assim, transformar seis status em quatro.

Antes de pensarmos em como fazer essa transformação, vamos criar a classe com as constantes dos status públicos:

<?php // CartPublicStatusEnum.php

class CartPublicStatusEnum
{
    public const OPEN = 'aberto';
    public const WAITING_PAYMENT = 'aguardando pagamento';
    public const DELIVERING = 'entregando';
    public const DELIVERED = 'entregue';
}

Note que o objetivo aqui não é traduzir e sim modificar os valores deCartStatusEnum para CartPublicStatusEnum.

Como seria isso com switch?

<?php // index.php
include_once __DIR__ . '/CartStatusEnum.php';
include_once __DIR__ . '/CartPublicStatusEnum.php';

function getPublicStatus(string $cartStatus): string
{
    switch($cartStatus) {
        case CartStatusEnum::STARTED:
            return CartPublicStatusEnum::OPEN;
        case CartStatusEnum::ADDRESSED:
            return CartPublicStatusEnum::WAITING_PAYMENT;
        case CartStatusEnum::PAID:
        case CartStatusEnum::PACKED:
        case CartStatusEnum::DISPATCHED:
            return CartPublicStatusEnum::DELIVERING;
        case CartStatusEnum::DELIVERED:
            return CartPublicStatusEnum::DELIVERED;
        default:
            throw new Exception('Status inválido.');
    }
}

echo getPublicStatus(CartStatusEnum::STARTED) . PHP_EOL; // 'aberto'
echo getPublicStatus(CartStatusEnum::ADDRESSED) . PHP_EOL; // 'aguardando pagamento'
echo getPublicStatus(CartStatusEnum::PAID) . PHP_EOL; // 'entregando'
echo getPublicStatus(CartStatusEnum::PACKED) . PHP_EOL; // 'entregando'
echo getPublicStatus(CartStatusEnum::DISPATCHED) . PHP_EOL; // 'entregando'
echo getPublicStatus(CartStatusEnum::DELIVERED) . PHP_EOL; // 'entregue'

Parece legal e otimizado? Pois veja a mesma coisa em match!

Como seria com match?

<?php // index.php
include_once __DIR__ . '/CartStatusEnum.php';
include_once __DIR__ . '/CartPublicStatusEnum.php';

function getPublicStatus(string $cartStatus): string
{
    return match($cartStatus) {
        CartStatusEnum::STARTED => CartPublicStatusEnum::OPEN,
        CartStatusEnum::ADDRESSED => CartPublicStatusEnum::WAITING_PAYMENT,
        CartStatusEnum::PAID, CartStatusEnum::PACKED, CartStatusEnum::DISPATCHED => CartPublicStatusEnum::DELIVERING,
        CartStatusEnum::DELIVERED => CartPublicStatusEnum::DELIVERED,
        default => throw new Exception('Status inválido.')
    };
}

echo getPublicStatus(CartStatusEnum::STARTED) . PHP_EOL; // 'aberto'
echo getPublicStatus(CartStatusEnum::ADDRESSED) . PHP_EOL; // 'aguardando pagamento'
echo getPublicStatus(CartStatusEnum::PAID) . PHP_EOL; // 'entregando'
echo getPublicStatus(CartStatusEnum::PACKED) . PHP_EOL; // 'entregando'
echo getPublicStatus(CartStatusEnum::DISPATCHED) . PHP_EOL; // 'entregando'
echo getPublicStatus(CartStatusEnum::DELIVERED) . PHP_EOL; // 'entregue'

E aí, qual forma está mais legível?

É, PHP 8 está com tudo!... E por hoje é só! Curtiu? Comenta e compartilha! Sei que posso ter pecado em considerar somente os status de sucesso no exemplo de hoje, mas é só para aplicar algum case, tá bom? Não é pra te ensinar como fazer algo do gênero. A reflexão foi legal, admita. Hahaha.

Enfim, no próximo artigo, falaremos sobre a estrutura declare, que apesar de parecer uma função, não é. Eu já cheguei a mencioná-la no artigo sobre declaração de tipagens ao declarar strict_types=1, lembra? Mas tem outras coisas que dá pra fazer com ela, então vamos nessa!

Inté!!