PHP para Iniciantes - Classes e Objetos - Construtores e Destrutores

Olá, quanto tempo! Não sei o que rolou, mas meu último artigo fugiu totalmente da ordenação original dos assuntos. Se você voltar lá para a Introdução da série PHP para Iniciantes, verá que eu listei todos os assuntos que irei abordar. Namespace está citado fora do conceito de Classes e Objetos, e mesmo assim escrevi sobre isso no último artigo, rs.

E não, não foi uma falha no manual oficial do PHP, pois lá também está separado dessa forma. Eu que pulei algum conteúdo mesmo. Ainda assim, se prestarmos atenção sobre a temática daquele tópico, faz muito sentido mencioná-lo enquanto falamos sobre classes, concorda? Talvez meio cedo demais, mas com certeza tem muito a ver.

Nesse artigo, voltaremos ao fluxo natural das coisas e vamos abordar o que estava agendado como "próximo tópico" no final do penúltimo artigo (Autoloading):

Construtores & Destrutores

Como de costume, antes de irmos direto para o código eu gostaria de abrir um espaço para reflexão semântica, que é um espaço para pensarmos no significado das coisas. Por exemplo: o que raios é um construtor?? E o que é um destrutor?! São opostos?

De acordo com o dicionário, construtor é:

  1. que ou aquele que constrói; construidor.
    "indústria c. de navios"
  2. que ou aquele que possui empresa dedicada à construção de imóveis.

E destrutor é:

m.q. DESTRUIDOR.

Ou seja:

que ou quem destrói; destrutor.


Pô, Kiko, essa seção foi uma perda de tempo, não é mesmo? Como isso poderia ajudar a entender o que isso deveria ser no código?

Se você pensou assim, calma, jovem! Na realidade, entender o significado das coisas antes de ir atrás delas ajuda a compreender 10000% melhor na hora de ver a prática, ok?! Vamos avançando...

Falando de classes e objetos, eu tentei deixar evidente que "Classe" é apenas uma definição de alguma coisa e nada mais que isso. O que deve representar algo "mais consolidado" é o "Objeto".

Esse sim, pode ter todo um ciclo de vida. Por exemplo: se temos uma classe "Pessoa", você seria um objeto dessa classe, uma instância dela. Depois de ler isso, o que você imagina que é um construtor e um destrutor? Sobre o que está relacionado?

Pô, Kiko, para de mistério e fala de uma vez...

Hahaha, ok! É simples: construtor é a função que inicializa um objeto. Ou seja, quando você está criando um objeto a partir de uma classe, o construtor é acionado.

Enquanto isso, destrutor é a função que finaliza o objeto. Quando ele está prestes a ser eliminado no ciclo de vida do sistema, o destrutor é acionado.

Diagrama do Ciclo de Vida de um Objeto

Construtores

Quando você chama o operador new junto do nome de uma classe, o que você está acionando, na verdade, é uma função __construct dentro dela. Os argumentos que você insere no nome são injetados diretamente dentro dela, portanto, precisa respeitar a assinatura do que quer que tenha criado ali dentro.

Exemplificando:

<?php

class Example {
    private int $a;
    public function __construct(int $a) {
        $this->a = $a;
    }
}

No construtor da classe Example acima, eu espero como argumento obrigatório int $a. Se você chamar new Example(1), tudo vai funcionar perfeitamente. Mas se chamar new Example() (sem argumentos), teremos um grande erro acontecendo ao executar esse código.

Como você percebeu, a única coisa que meu construtor faz é pegar o argumento que você injetou na construção do objeto e definí-lo como valor de uma propriedade com mesmo nome dentro. Essa propriedade poderia ter qualquer outro nome:

<?php

class Example {
    private int $a;
    public function __construct(int $b) {
        $this->a = $b;
    }
}

O importante aqui é perceber que $a é diferente de $this->a. $a no exemplo anterior era apenas a variável criada dentro do construtor, recebida como argumento. $this->a era uma propriedade interna do objeto que estamos instanciando. No exemplo atual, agora ficou bem mais claro pois nomeei de forma diferente.

Ah, Kiko, eu preciso declarar a propriedade fora e dentro do construtor?

Depende. Se você usa PHP antes do 8.1: sim, você precisa. O PHP até consegue criar propriedades dinâmicas mas isso é horrível! Principalmente se você usa alguma ferramenta de análise estática do seu código. Então sempre declare essas propriedades direitinho.

Agora se você já está mais atualizado, é possível declarar as propriedades na assinatura do construtor! Veja o exemplo a seguir:

<?php

class Example {
    public function __construct(
        private int $a
    ) { }
}

Sim, isso é o equivalente ao primeiro exemplo, mas não preciso nem declarar a propriedade fora do construtor, nem atribuir o valor de uma variável do argumento. O PHP já faz isso pra mim nessa assinatura aí!

MEME: quê?!

Tirando esse detalhe, outro fator muito importante para as versões mais recentes da linguagem é sobre os campos readonly, ou seja, somente leitura. Quando você cria uma propriedade que não pode ser alterada, significa que o único momento de todo o ciclo de vida do objeto em que podemos atribuir algum valor à propriedade é no construtor. Dá pra fazer um monte de coisa bacana, hein?

<?php

class Cents {
    public function __construct(
        public readonly int $value
    ) { }

    public function add(Cents $cents): Cents {
        return new static($this->value + $cents->value);
    }

    public function sub(Cents $cents): Cents {
        return new static($this->value - $cents->value);
    }

    public function toString(string $symbol = "R$ "): string {
        $str = "{$this->value}"; // só dá pra fazer isso aqui com inteiros pequenos, OK???
        return $symbol . preg_replace("/(\d{2})$/", ",$1", $str);
    }

    public static function fromFloat(float $num): Cents {
        return new static(intval($num*100));
    }
}

Com uma classe como essa, podemos fazer a seguinte brincadeira:

<?php

include_once( __DIR__ . "/Cents.php"); // ou substitua isso aqui pela classe criada anteriormente

$venda = new Cents(0);
$venda = $venda->add(Cents::fromFloat(1.20));
$venda = $venda->add(Cents::fromFloat(3.99));
$venda = $venda->add(Cents::fromFloat(5.32));
echo $venda->toString(); // R$ 10,51

// também é possível acessar a variável somente leitura
var_dump($venda->value); // int(1051)

Destrutores

Diferentemente do construtor, nós não temos a liberdade de injetarmos argumentos nos destrutores. Isso porque não somos nós que o acionamos, mas o interpretador do PHP. Para escrever um destrutor, basta adicionar um método __destruct na classe. Por exemplo:

<?php

class Example {
    public function __destruct() {
        echo "Adeus mundo!";
    }
}

Os destrutores são acionados somente em duas situações:

  1. quando um objeto perde todas as referências ligadas a ele (isso seria o limbo absoluto para uma instância - felizmente o PHP consegue eliminar isso sozinho);
  2. quando nós solicitamos propositalmente que o PHP o elimine.

Em ambos os casos, se trata do fim do ciclo de vida do objeto, então podemos sempre resumir dessa forma.

Pra que eu usaria isso, Kiko?

Olha, tirando Graceful Shutdown, eu nunca vi nenhuma outra aplicação que não fosse algum tipo de gambiarra. Isso que mencionei é basicamente uma garantia de que os recursos do seu sistema serão encerrados da melhor forma possível quando o servidor estiver prestes a ser desligado.

Um exemplo disso é quando o servidor recebe um comando de shutdown no meio de um processo onde coisas estão sendo armazenadas no banco de dados... Nesse cenário, faz sentido ter uma tratativa no objeto onde essas coisas estão acontecendo para evitar que os dados se percam, seja salvando todo o estado atual em algum lugar temporário para continuar quando o servidor iniciar novamente ou só direcionando para que outra máquina continue o trabalho.

O Graceful Shutdown não é a parte de controlar a máquina de estado do seu produto, somente a parte de garantir que, ao desligar, nada será brutalmente interrompido. Sacou?

Aí sim, faz sentido ter um destrutor.

Você também pode usar isso para centralizar o disparo de logs, mas isso pode dar muito errado. Vou dar um exemplo básico:

<?php

class Logger {
    public function __construct(
        private string $logs = ""
    ) { }

    public function log(string $message): void {
        $timestamp = date('Y-m-d H:i:s');
        $this->logs .= PHP_EOL . "[$timestamp] $message";
    }

    public function __destruct() {
        echo $this->logs;
        // imagine que, no lugar de echo, estaríamos registrando os logs onde quer seja
        // desse modo, os logs só seriam enviados quando o objeto responsável por acumular os logs fosse destruído
        // mas isso pode dar muito errado!
        // se você for armazenar num banco, por exemplo, o tempo de conexão pode não ser tão rápido quanto o comando de desligamento do servidor e aí você perderia esses logs todinhos :)
    }
}

O uso da classe acima seria:

<?php

include_once( __DIR__ . "/Logger.php"); // ou só trocar pela classe declarada acima

function exemplo(): void {
    $logger = new Logger();
    $logger->log("testando"); // nada acontece
    $logger->log("ue"); // nada acontece
} // quando a função morre, é que os logs vão parar no echo!

exemplo();

Observações importantes

  1. Os destrutores são acionados mesmo se você tentar matar a execução do script com um exit();
  2. Entretanto, se você acionar o exit() dentro de um destrutor, outros destrutores de outros objetos não serão executados;
  3. Lançar exceções (Exception) dentro de um destrutor resultará em um erro fatal no PHP - não faz sentido acontecer isso num ciclo de destruição, não é mesmo? Se houver a mínima possibilidade de algo disparar uma exceção, encapsule com um try-catch e faça um tratamento, beleza?

E por hoje é só! Curtiu?? Comenta e compartilha!! Comecei a escrever esse artigo para testar meu novo teclado mecânico, o qual estou amando MUITO. Não sei porque não tinha aderido a isso ainda, é gostoso demais de digitar, hahaha. Dá uma saudade enorme de datilografar (sim, eu peguei o final do final dessa fase com a máquina de datilografar da minha mãe). De qualquer forma, estou tentando voltar, pessoal!

Vocês já conheciam o destrutor? Já fizeram alguma solução com ele que não foi uma gambiarra? Comenta aí! Confesso que me falta experiência com ele, por isso nem pude mostrar algo muito relevante sobre isso. Deixa sua marca, hein?

Inté!!