PHP para Iniciantes: Operador de Tipos

[ERRATA para quem leu o artigo Operadores de Arrays (https://blog.kaiquegarcia.dev/php-para-iniciantes-operadores-de-arrays) antes de 05/10/2021](): No final do artigo anterior, eu mencionei que este seria o último artigo falando sobre operadores, certo? Eu cometi um pequeno engano, mas com razão. O grande problema do momento é que tem um operador específico que não é mencionado na documentação do PHP, que é o **nullsafe operator**. Então deixo aqui como primeira contestação de errata: esse não é o último artigo de operadores, e sim o penúltimo! Para quem leu o artigo mencionado depois do dia 05/10/2021, eu já corrigi a informação e essa errata não fará sentido pra você, rs.

Falando sobre tipos de dados primitivos, uma das necessidades que podemos ter no código é restringir o tipo de dado que queremos lidar. Uma das formas conhecidas é usando a assinatura dos métodos, destacando o tipo de dado que aceitamos receber e o interpretador se vira ou para converter o tipo ou para lançar um Fatal Error.

Mas para quem lida com versões do PHP antes do 7, essa abordagem não existe. Constantemente você precisa validar os tipos dentro do fluxo dos métodos e/ou funções. Chegamos ao ponto de precisar escrever uma série de pré-testes para fazer o mesmo comportamento do PHP, por exemplo...

Ao invés de escrevermos um código super simples como:

<?php

function funcPhp7Mais(float $a): void
{
    // faz alguma coisa
}

Seria necessário criar as funções que fazem essa assinatura de tipos:

<?php

// prepara uma função para assinar tipos de dados
function assign_float($var): float
{
    if (is_float($var)) {
        // se é float, não precisa fazer nada
        return $var;
    }
    if (is_numeric($var)) {
        // se não é float, mas é numérico, só converter pra float e tudo certo
        return floatval($var);
    }
    throw new \Exception('Fatal Error: "float" expected, received "' . gettype($var) . '".');
}

// e depois cria a função

function funcPhp5_6Menos($a): void
{
    $a = assign_float($a);
    // faz alguma coisa
    var_dump($a);
}

funcPhp5_6Menos('0.1'); // float(0.1)
funcPhp5_6Menos(150); // float(150)
funcPhp5_6Menos(new stdClass); // Exception(FatalError: "float" expected, received "object".)

Inclusive, se você lida com versões inferiores ao PHP 7, acho uma boa ter esse tipo de tratativas para ficar próximo da organização de assinatura de métodos com as conversões de tipagem que o interpretador faz. #fikdik

Voltando ao tópico, o fato de assinar um tipo nos métodos e funções é uma forma segura de preparar o seu código e que funciona bem, de certa forma. Mas às vezes nós precisamos validar objetos durante o processo, especificamente em um if. Quando falamos de validação de objetos, falamos em orientação a objeto, hierarquia de classes, etc. Muitas necessidades podem surgir nesse tópico e a principal delas é: como saber se um objeto é uma instância de uma classe ou outro objeto? Bem, esse é o operador que falaremos nesse artigo... (rufem os tambores)

Operador instanceof

Esse é, de longe, o operador mais longo que tem na nossa linguagem. Ele é longo de escrita, mas é semanticamente legível, o que deixa difícil não entender qual é a sua proposta. Instance of significa Instância de em inglês, o que nos induz a escrever uma pergunta: $a é uma instância de $b?

E agora reflita: quais as possíveis respostas para essa pergunta? Sim ou não, certo? Se são duas possibilidades, o retorno disso é Booleano.

Com isso, o operador instanceof sempre vai retornar true ou false. Mas preciso ressaltar que o conceito de instância é muito mais amplo do que identificar se um $objeto é uma instância de uma classe, ok?

Exemplo 1: $objeto é uma instância da classe Blablabla?

<?php

class Exemplo { }

$exemplo = new Exemplo;
// aqui, temos claramente que $exemplo é uma instância da classe Exemplo
var_dump($exemplo instanceof Exemplo); // bool(true)

Exemplo 2: $objetoA é da mesma instância do $objetoB?

<?php

class Exemplo1 {}
class Exemplo2 {}

$exemplo = new Exemplo1;
$outroExemplo = new Exemplo2;
$maisUmExemplo = new Exemplo1;

var_dump($exemplo instanceof $outroExemplo); // bool(false)
var_dump($outroExemplo instanceof $maisUmExemplo); // bool(false)
var_dump($exemplo instanceof $maisUmExemplo); // bool(true)
var_dump($exemplo instanceof Exemplo2); // bool(false)

Exemplo 3: $objeto é uma instância da classe pai/interface Blablabla?

<?php

interface UmaInterface {}
abstract class PaiAbstrato {}
class Filho extends PaiAbstrato implements UmaInterface {}

$filho = new Filho;

var_dump($filho instanceof Filho); // bool(true)
var_dump($filho instanceof PaiAbstrato); // bool(true), pois a classe Filho extende do PaiAbstrato
var_dump($filho instanceof UmaInterface); // bool(true), pois a classe Filho implementa a interface UmaInterface

Exemplo 4: *$objeto é uma instância da classe cujo nome em string é 'Blalabla'?

<?php

class Exemplo {}

$exemplo = new Exemplo;
$nomeDaClasse = 'Exemplo';

var_dump($exemplo instanceof $nomeDaClasse); // bool(true)

Ok, Kiko, entendi a função do instanceof. Mas qual seria a implementação desse operador?

Que pergunta complicada, hein? Hahah... Mas é legal, vou aceitar o desafio e tentar reescrever isso em funções. Porém, teremos algumas limitações: por exemplo, ao passar um nome de uma classe você não poderá usar a referência dela, ou seja, deverá escrever o nome da classe direto na chamada ou usar a propriedade estática ::class (por ex.: Exemplo::class), que será como escreverei nos exemplos. Além disso, usarei a sintaxe do PHP 8, afinal, instanceof existe desde o PHP 5. Antes disso, usávamos a função is_a() que hoje nem deveríamos mencionar, hehe. Bora?!

Case 1: $objeto instanceof Classe (diretamente)

<?php

function isDirectlyInstanceOfClass(object $object, string $className): bool
{
    return get_class($object) === $className;
}

// --- exemplo 1

class Exemplo {}
$exemplo = new Exemplo;

var_dump(isDirectlyInstanceOfClass($exemplo, Exemplo::class)); // bool(true)

Percebemos que o primeiro caso funciona, mas o exemplo de hierarquia já quebraria. Vamos com calma :hehe:

Case 2: $objectA instanceof $objectB

<?php

function isInstanceOfObject(object $objectA, object $objectB): bool
{
    return get_class($objectA) === get_class($objectB);
}

// --- exemplo 2
class Exemplo1 {}
class Exemplo2 {}

$exemplo = new Exemplo1;
$outroExemplo = new Exemplo2;
$maisUmExemplo = new Exemplo1;

var_dump(isInstanceOfObject($exemplo, $outroExemplo)); // bool(false)
var_dump(isInstanceOfObject($outroExemplo, $maisUmExemplo)); // bool(false)
var_dump(isInstanceOfObject($exemplo, $maisUmExemplo)); // bool(true)
// nesse caso, removi a comparação com o nome da classe, pois é do case 1

Case 3: $objeto instanceof Classe (hierarquicamente)

Esse é mais complexo, pois precisamos saber se a classe do objeto é igual diretamente ou uma subclasse da outra classe. Com isso, reutilizaremos a função criada anteriormente e faremos uma extensão das possibilidades:

<?php

// função do case 1
function isDirectlyInstanceOfClass(object $object, string $className): bool
{
    return get_class($object) === $className;
}

// case 3
function isInstanceOfClass(object $object, string $className): bool
{
    if (isDirectlyInstanceOfClass($object, $className)) {
        return true;
    }
    return is_subclass_of($object, $className); // hahahahahaha, trollei vocês xD
    // essa função poderia ser escrita em uma linha só, ok?
    // return isDirectlyInstanceOfClass($object, $className) || is_subclass_of($object, $className);
    // preferi deixar separado para que possam ler com mais facilidade ;)
    // isso é um dos princípios de SOLID, inclusive
}

// --- exemplo 1
class Exemplo {}

$exemplo = new Exemplo;

var_dump(isInstanceOfClass($exemplo, Exemplo::class)); // bool(true), afinal, é direto

// --- exemplo 3

interface UmaInterface {}
abstract class PaiAbstrato {}
class Filho extends PaiAbstrato implements UmaInterface {}

$filho = new Filho;

var_dump(isInstanceOfClass($filho, Filho::class)); // bool(true)
var_dump(isInstanceOfClass($filho, PaiAbstrato::class)); // bool(true), pois a classe Filho extende do PaiAbstrato
var_dump(isInstanceOfClass($filho, UmaInterface::class)); // bool(true), pois a classe Filho implementa a interface UmaInterface

Agora pegou um corpo, né? A gente nem precisa fazer o exemplo 4 pois fomos limitados a operar com string, então já está cobrindo esse case, desde que você escreva o nome da classe certinho.

A implementação final, então, seria:

<?php
// função do case 1
function isDirectlyInstanceOfClass(object $object, string $className): bool
{
    return get_class($object) === $className;
}

// função do case 2
function isInstanceOfObject(object $objectA, object $objectB): bool
{
    return get_class($objectA) === get_class($objectB);
}

// função do case 3
function isInstanceOfClass(object $object, string $className): bool
{
    if (isDirectlyInstanceOfClass($object, $className)) {
        return true;
    }
    return is_subclass_of($object, $className);
}

// função principal, com acúmulo de responsabilidades
function isInstanceOf(object $object, string|object $classNameOrObject): bool
{
    if (is_object($classNameOrObject)) {
        return isInstanceOfObject($object, $classNameOrObject);
    }
    return isInstanceOfClass($object, $classNameOrObject);
}

// então, bastaria chamar a função com acúmulo de responsabilidades

// --- exemplo 1
class Exemplo {}

$exemplo = new Exemplo;

echo 'Exemplo 1' . PHP_EOL;
var_dump(isInstanceOf($exemplo, Exemplo::class)); // bool(true), afinal, é direto

// --- exemplo 2

class Exemplo1 {}
class Exemplo2 {}

$exemplo = new Exemplo1;
$outroExemplo = new Exemplo2;
$maisUmExemplo = new Exemplo1;

echo 'Exemplo 2' . PHP_EOL;
var_dump(isInstanceOf($exemplo, $outroExemplo)); // bool(false)
var_dump(isInstanceOf($outroExemplo, $maisUmExemplo)); // bool(false)
var_dump(isInstanceOf($exemplo, $maisUmExemplo)); // bool(true)
var_dump(isInstanceOf($exemplo, Exemplo2::class)); // bool(false)

// --- exemplo 3

interface UmaInterface {}
abstract class PaiAbstrato {}
class Filho extends PaiAbstrato implements UmaInterface {}

$filho = new Filho;

echo 'Exemplo 3' . PHP_EOL;
var_dump(isInstanceOf($filho, Filho::class)); // bool(true)
var_dump(isInstanceOf($filho, PaiAbstrato::class)); // bool(true), pois a classe Filho extende do PaiAbstrato
var_dump(isInstanceOf($filho, UmaInterface::class)); // bool(true), pois a classe Filho implementa a interface UmaInterface

E é isso, pessoal! Curtiu? Comenta e compartilha! Sempre que possível, dê preferência aos operadores nativos, pois eles são melhor otimizados do que recriar a operação em funções. Lembre-se que eu só reescrevi para mostrar o que rola por baixo, não use isso nos ambientes não, hein?

No próximo artigo, falaremos sobre Nullsafe Operator, um operador muito útil no dia a dia para encurtar código, sem parecer gambiarra. Vamo?!

Inté!