Construtores e propriedades tipadas

1. Introdução aos Construtores em PHP

O método __construct() é um método especial em PHP que é automaticamente invocado quando um novo objeto de uma classe é criado. Sua principal finalidade é inicializar o estado do objeto, configurando propriedades, estabelecendo conexões ou executando qualquer lógica necessária antes que o objeto seja utilizado.

<?php

class MinhaClasse
{
    public function __construct()
    {
        echo "Objeto criado!";
    }
}

$objeto = new MinhaClasse(); // Exibe: "Objeto criado!"

A sintaxe básica de um construtor é simples: ele é definido como um método público com o nome __construct. Construtores podem ser vazios (sem parâmetros) ou receber argumentos para personalizar a inicialização do objeto.

<?php

class Carro
{
    public string $modelo;
    public int $ano;

    // Construtor sem parâmetros
    public function __construct()
    {
        $this->modelo = "Desconhecido";
        $this->ano = 2024;
    }
}

// Construtor com parâmetros
class CarroPersonalizado
{
    public string $modelo;
    public int $ano;

    public function __construct(string $modelo, int $ano)
    {
        $this->modelo = $modelo;
        $this->ano = $ano;
    }
}

2. Propriedades Tipadas: Conceitos Fundamentais

Desde o PHP 7.4, é possível declarar tipos para propriedades de classe, garantindo que apenas valores do tipo especificado sejam atribuídos a elas. Isso traz segurança e previsibilidade ao código.

<?php

class Produto
{
    public string $nome;
    public float $preco;
    public int $estoque;
    public bool $disponivel;
    public array $categorias;
}

Os tipos suportados incluem:
- Escalares: int, float, string, bool
- Compostos: array, object, callable, iterable
- Especiais: mixed, never, void (apenas em métodos)
- Classes e interfaces: qualquer nome de classe ou interface

Propriedades tipadas exigem inicialização antes do uso, seja com um valor padrão ou no construtor. Caso contrário, o PHP emitirá um erro.

<?php

class Configuracao
{
    public string $nome = "Padrão"; // Valor padrão
    public int $versao; // Deve ser inicializada no construtor

    public function __construct(int $versao)
    {
        $this->versao = $versao;
    }
}

3. Construtores com Parâmetros Tipados

A tipagem explícita nos parâmetros do construtor oferece validação automática no momento da instanciação, evitando que valores incorretos sejam passados.

<?php

class Usuario
{
    public string $nome;
    public string $email;
    public int $idade;
    public bool $ativo;

    public function __construct(
        string $nome,
        string $email,
        int $idade,
        bool $ativo = true
    ) {
        $this->nome = $nome;
        $this->email = $email;
        $this->idade = $idade;
        $this->ativo = $ativo;
    }
}

// Uso correto
$usuario = new Usuario("Maria", "maria@email.com", 28, true);

// Isso gerará um TypeError: Argumento 3 deve ser do tipo int, string fornecido
// $usuarioInvalido = new Usuario("João", "joao@email.com", "trinta", false);

4. Promoção de Propriedades no Construtor (PHP 8+)

O PHP 8 introduziu a promoção de propriedades no construtor, uma sintaxe reduzida que permite declarar e inicializar propriedades diretamente nos parâmetros do construtor, eliminando a duplicação de código.

Antes da promoção (PHP 7.4 e anteriores):

<?php

class Cliente
{
    private string $nome;
    private string $email;
    private int $pontos;

    public function __construct(string $nome, string $email, int $pontos)
    {
        $this->nome = $nome;
        $this->email = $email;
        $this->pontos = $pontos;
    }
}

Depois da promoção (PHP 8+):

<?php

class Cliente
{
    public function __construct(
        private string $nome,
        private string $email,
        private int $pontos
    ) {}
}

A promoção funciona com qualquer modificador de acesso (public, protected, private) e aceita todos os tipos suportados pelo PHP.

5. Propriedades Tipadas com Valores Opcionais e Nullable

O operador ? permite declarar propriedades que podem aceitar null além do tipo especificado. Combinado com valores padrão, oferece flexibilidade na inicialização.

<?php

class Endereco
{
    public function __construct(
        public string $rua,
        public string $cidade,
        public ?string $complemento = null, // Pode ser string ou null
        public ?int $numero = null          // Pode ser int ou null
    ) {}
}

$endereco1 = new Endereco("Rua A", "São Paulo");
$endereco2 = new Endereco("Rua B", "Rio", "Apto 42", 100);
$endereco3 = new Endereco("Rua C", "Belo Horizonte", null, null);

Em construtores promovidos, o tratamento de null é igualmente simples:

<?php

class Funcionario
{
    public function __construct(
        private string $nome,
        private ?string $telefone = null,
        private ?float $salario = null
    ) {}

    public function getTelefone(): ?string
    {
        return $this->telefone;
    }
}

6. Construtores e Herança

Quando trabalhamos com herança, é comum que a classe filha precise chamar o construtor da classe pai usando parent::__construct().

<?php

class Animal
{
    public function __construct(
        protected string $nome,
        protected int $idade
    ) {}
}

class Cachorro extends Animal
{
    public string $raca;

    public function __construct(string $nome, int $idade, string $raca)
    {
        parent::__construct($nome, $idade); // Chama construtor da classe pai
        $this->raca = $raca;
    }
}

$rex = new Cachorro("Rex", 3, "Labrador");

A sobrescrita de construtores deve manter consistência de tipos com a classe base, especialmente em relação aos parâmetros obrigatórios.

7. Boas Práticas e Armadilhas Comuns

Evite lógica pesada no construtor: O construtor deve ser leve e focado apenas na inicialização. Operações complexas, como consultas a banco de dados ou chamadas de API, devem ser movidas para métodos específicos.

<?php

// Evite isso
class Repositorio
{
    public function __construct()
    {
        $this->conexao = new PDO("mysql:host=localhost;dbname=teste", "user", "pass");
        $this->conexao->query("SELECT ..."); // Lógica pesada aqui
    }
}

// Prefira isso
class Repositorio
{
    public function __construct(
        private PDO $conexao
    ) {}

    public function buscarDados(): array
    {
        return $this->conexao->query("SELECT ...")->fetchAll();
    }
}

Uso de readonly com propriedades tipadas (PHP 8.1+): O modificador readonly torna a propriedade imutável após a inicialização, ideal para objetos de valor.

<?php

class ConfiguracaoBanco
{
    public function __construct(
        readonly public string $host,
        readonly public int $porta,
        readonly public string $usuario,
        readonly private string $senha
    ) {}
}

$config = new ConfiguracaoBanco("localhost", 3306, "root", "123");
// $config->host = "outro"; // Erro! Propriedade readonly

Erros frequentes:
- Tentar acessar propriedades não inicializadas
- Passar tipos incompatíveis nos parâmetros
- Esquecer de chamar parent::__construct() na herança

8. Exemplo Completo: Sistema de Pedidos

Vamos construir um sistema simples de pedidos utilizando todos os conceitos abordados.

<?php

class ItemPedido
{
    public function __construct(
        readonly public string $produto,
        readonly public float $precoUnitario,
        readonly public int $quantidade
    ) {
        if ($precoUnitario <= 0) {
            throw new InvalidArgumentException("Preço deve ser positivo");
        }
        if ($quantidade <= 0) {
            throw new InvalidArgumentException("Quantidade deve ser positiva");
        }
    }

    public function calcularTotal(): float
    {
        return $this->precoUnitario * $this->quantidade;
    }
}

class Pedido
{
    /** @var ItemPedido[] */
    private array $itens = [];

    public function __construct(
        readonly public int $codigo,
        readonly public string $cliente,
        readonly public ?string $observacao = null
    ) {}

    public function adicionarItem(ItemPedido $item): void
    {
        $this->itens[] = $item;
    }

    public function calcularTotal(): float
    {
        $total = 0;
        foreach ($this->itens as $item) {
            $total += $item->calcularTotal();
        }
        return $total;
    }

    /** @return ItemPedido[] */
    public function getItens(): array
    {
        return $this->itens;
    }
}

class PedidoPremium extends Pedido
{
    public function __construct(
        int $codigo,
        string $cliente,
        readonly public float $descontoPercentual,
        ?string $observacao = null
    ) {
        parent::__construct($codigo, $cliente, $observacao);
        if ($descontoPercentual < 0 || $descontoPercentual > 100) {
            throw new InvalidArgumentException("Desconto deve estar entre 0 e 100");
        }
    }

    public function calcularTotal(): float
    {
        $total = parent::calcularTotal();
        return $total * (1 - $this->descontoPercentual / 100);
    }
}

// Demonstração de uso
try {
    $pedido = new PedidoPremium(101, "Ana Silva", 10, "Entrega rápida");

    $pedido->adicionarItem(new ItemPedido("Notebook", 3500.00, 1));
    $pedido->adicionarItem(new ItemPedido("Mouse", 150.00, 2));
    $pedido->adicionarItem(new ItemPedido("Teclado", 200.00, 1));

    echo "Pedido #{$pedido->codigo}\n";
    echo "Cliente: {$pedido->cliente}\n";
    echo "Observação: " . ($pedido->observacao ?? "Nenhuma") . "\n";
    echo "Total com desconto: R$ " . number_format($pedido->calcularTotal(), 2, ',', '.') . "\n";

} catch (InvalidArgumentException $e) {
    echo "Erro: " . $e->getMessage();
}

Este exemplo demonstra:
- Promoção de propriedades no construtor
- Propriedades tipadas (incluindo readonly)
- Tipos nullable (?string)
- Validação de tipos e valores no construtor
- Herança com chamada a parent::__construct()
- Sobrescrita de métodos em classes filhas

Referências