PHP Construtores

Até agora, a Ana criava um produto vazio com new Produto() e preenchia as propriedades uma por uma. É fácil esquecer uma. O construtor resolve isso: é um método especial que roda sozinho no momento do new e recebe os dados que o objeto precisa para nascer completo. Nesta aula você também vai ver a promoção de propriedades do PHP 8.0, que faz a mesma coisa com muito menos código.

Você vai aprender
  • Escrever um construtor com __construct e passar os dados no new.
  • Dar valores padrão aos parâmetros do construtor.
  • Validar os dados e recusar um objeto inválido.
  • A promoção de propriedades no construtor (PHP 8.0).
  • Criar objetos com argumentos nomeados: new Produto(nome: "Caneca", preco: 25.90).
  • A ideia do __destruct, o método que roda quando o objeto é descartado.

O problema: objetos "pela metade"

Com propriedades sem valor inicial (como você viu em Propriedades e Métodos), basta esquecer uma atribuição para o programa quebrar mais adiante:

<?php
class Produto
{
    public string $nome;
    public float $preco;
}

$caneca = new Produto();
$caneca->nome = "Caneca";
// a Ana esqueceu o preço...

echo "{$caneca->nome}: R$ {$caneca->preco}\n";
$ php esqueceu.php

Fatal error: Uncaught Error: Typed property Produto::$preco must not be accessed before initialization in /home/ana/loja/esqueceu.php:12
Stack trace:
#0 {main}
  thrown in /home/ana/loja/esqueceu.php on line 12

O erro aparece na linha 12, longe da linha onde o preço deveria ter sido dado. Num programa grande, achar onde a Ana "esqueceu" é difícil. O ideal é que seja impossível criar um produto sem nome e sem preço.

Pense na maternidade: um bebê não sai do hospital sem certidão de nascimento com nome, data e local. O construtor é essa certidão: o objeto só "nasce" com os dados obrigatórios.


O construtor: __construct

<?php
class Produto
{
    public string $nome;
    public float $preco;
    public int $estoque;

    public function __construct(string $nome, float $preco, int $estoque)
    {
        $this->nome = $nome;
        $this->preco = $preco;
        $this->estoque = $estoque;
        echo "Produto criado: {$this->nome}\n";
    }
}

$caneca = new Produto("Caneca", 25.90, 10);
$camiseta = new Produto("Camiseta", 49.90, 4);

echo "{$caneca->nome} custa R$ {$caneca->preco} ({$caneca->estoque} em estoque)\n";
$ php construtor.php
Produto criado: Caneca
Produto criado: Camiseta
Caneca custa R$ 25.9 (10 em estoque)
public function __construct(…)
O construtor. O nome é fixo: dois sublinhados e construct ("construir"). Métodos que começam com __ são métodos mágicos: o PHP os chama sozinho em momentos especiais (veja Métodos Mágicos).
string $nome, float $preco, int $estoque
Parâmetros, como em qualquer função. São variáveis locais do construtor: somem quando ele termina.
$this->nome = $nome;
Copia o valor do parâmetro $nome para a propriedade nome do objeto. Os dois têm o mesmo nome por costume, mas são coisas diferentes: $nome é o parâmetro, $this->nome é a propriedade.
new Produto("Caneca", 25.90, 10)
Os valores entre parênteses no new vão para os parâmetros do construtor, na ordem.
: (sem tipo de retorno)
O construtor não tem tipo de retorno e não devolve nada: quem "devolve" o objeto é o new.

Repare na saída: a mensagem Produto criado apareceu duas vezes, uma em cada new. Ninguém chamou __construct diretamente: o PHP chamou sozinho. A sequência de um new é:

  1. O PHP cria um objeto novo, com as propriedades nos valores iniciais (ou não inicializadas).
  2. Chama o __construct desse objeto, passando os valores dos parênteses.
  3. Quando o construtor termina, o new entrega o objeto pronto para a variável.

Agora, esquecer o preço vira um erro na linha do new, que é exatamente onde está o problema (veja Quando dá errado).

Valores padrão

Parâmetros do construtor podem ter valor padrão, como os de qualquer função. Os opcionais ficam no fim:

<?php
class Produto
{
    public string $nome;
    public float $preco;
    public int $estoque;

    public function __construct(string $nome, float $preco, int $estoque = 0)
    {
        $this->nome = $nome;
        $this->preco = $preco;
        $this->estoque = $estoque;
    }
}

$caneca = new Produto("Caneca", 25.90, 10);
$chaveiro = new Produto("Chaveiro", 12.00);

echo "{$caneca->nome}: {$caneca->estoque}\n";
echo "{$chaveiro->nome}: {$chaveiro->estoque}\n";
$ php padrao.php
Caneca: 10
Chaveiro: 0

Validar no construtor

O construtor é o porteiro do objeto: é o lugar certo para recusar dados inválidos. Se algo está errado, ele lança uma exceção (throw) e o objeto não é criado. (Exceções têm a sua aula: Exceções.)

<?php
class Produto
{
    public string $nome;
    public float $preco;

    public function __construct(string $nome, float $preco)
    {
        if (trim($nome) === "") {
            throw new InvalidArgumentException("O nome não pode ser vazio");
        }
        if ($preco < 0) {
            throw new InvalidArgumentException("Preço negativo: $preco");
        }
        $this->nome = trim($nome);
        $this->preco = $preco;
    }
}

$caneca = new Produto("  Caneca ", 25.90);
echo "[{$caneca->nome}]\n";

try {
    $erro = new Produto("Camiseta", -5);
} catch (InvalidArgumentException $e) {
    echo "Não criei o produto: " . $e->getMessage() . "\n";
}

$outro = new Produto("", 10);
$ php validar.php
[Caneca]                                  « o construtor também limpou os espaços
Não criei o produto: Preço negativo: -5   « o try/catch tratou a exceção

Fatal error: Uncaught InvalidArgumentException: O nome não pode ser vazio in /home/ana/loja/validar.php:10
Stack trace:
#0 /home/ana/loja/validar.php(29): Produto->__construct('', 10.0)
#1 {main}
  thrown in /home/ana/loja/validar.php on line 10

Três situações: a caneca passou nas regras (e o nome foi limpo com trim); a camiseta com preço negativo foi recusada, e o try/catch tratou o problema; o produto sem nome foi recusado sem try, e o programa parou com Uncaught ("não capturada"). Nos três casos, nenhum produto inválido chegou a existir.


Promoção de propriedades no construtor PHP 8.0+

Olhe de novo o construtor da caneca. Cada dado aparece três vezes: na declaração da propriedade, no parâmetro e na atribuição $this->nome = $nome. O PHP 8.0 criou um atalho: escreva a visibilidade na frente do parâmetro, e ele vira propriedade e recebe o valor sozinho. Isso se chama promoção de propriedades no construtor (constructor property promotion):

<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
        public int $estoque = 0,
    ) {
    }
}

$caneca = new Produto("Caneca", 25.90, 10);
var_dump($caneca);
$ php promocao.php
object(Produto)#1 (3) {
  ["nome"]=>
  string(6) "Caneca"
  ["preco"]=>
  float(25.9)
  ["estoque"]=>
  int(10)
}

O resultado é idêntico ao da versão longa: o objeto tem as três propriedades com os valores certos. Compare as duas formas:

Forma longa (qualquer versão)Com promoção (PHP 8.0+)
class Produto
{
    public string $nome;
    public float $preco;
    public int $estoque;

    public function __construct(
        string $nome,
        float $preco,
        int $estoque = 0,
    ) {
        $this->nome = $nome;
        $this->preco = $preco;
        $this->estoque = $estoque;
    }
}
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
        public int $estoque = 0,
    ) {
    }
}
public string $nome
Dentro dos parênteses do construtor, a palavra public (ou protected/private, de Visibilidade) é o que "promove" o parâmetro a propriedade. Sem ela, é um parâmetro comum.
um parâmetro por linha
Costume para construtores promovidos: fica fácil de ler e de acrescentar um campo.
, (vírgula depois do último)
A "vírgula final" é permitida (PHP 8.0+) e recomendada: ao acrescentar um parâmetro, você não esquece a vírgula.
{ }
O corpo pode ficar vazio. As atribuições acontecem sozinhas, antes do corpo rodar.

Misturando: promovidos, comuns e código no corpo

Nem todo parâmetro precisa virar propriedade, e o corpo pode ter código. Aqui o número só serve para montar o código do produto:

<?php
class Produto
{
    public string $codigo;

    public function __construct(
        public string $nome,
        public float $preco,
        int $numero,
    ) {
        $this->codigo = "P-" . str_pad((string) $numero, 4, "0", STR_PAD_LEFT);
    }
}

$caneca = new Produto("Caneca", 25.90, 7);
echo "{$caneca->codigo} {$caneca->nome}\n";
var_dump(isset($caneca->numero));
$ php misto.php
P-0007 Caneca
bool(false)       « $numero não tem public: é só um parâmetro, não virou propriedade

Missão: reescreva a classe com promoção de propriedades: apague as três declarações e as três atribuições, e ponha public na frente de cada parâmetro do construtor. A saída deve continuar Camiseta: R$ 49.9 (4 em estoque), sem avisos.

<?php
class Produto
{
    public string $nome;
    public float $preco;
    public int $estoque;

    public function __construct(string $nome, float $preco, int $estoque = 0)
    {
        $this->nome = $nome;
        $this->preco = $preco;
        $this->estoque = $estoque;
    }
}

$camiseta = new Produto("Camiseta", 49.90, 4);
echo "{$camiseta->nome}: R$ {$camiseta->preco} ({$camiseta->estoque} em estoque)\n";
<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
        public int $estoque = 0,
    ) {
    }
}

$camiseta = new Produto("Camiseta", 49.90, 4);
echo "{$camiseta->nome}: R$ {$camiseta->preco} ({$camiseta->estoque} em estoque)\n";

Argumentos nomeados com construtores PHP 8.0+

Com muitos parâmetros, new Produto("Caneca", 25.90, 0, true, "Cozinha") fica difícil de ler: o que é o 0? E o true? Os argumentos nomeados (que você conheceu em Parâmetros) funcionam no new também: escreva o nome do parâmetro, dois-pontos e o valor:

<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
        public int $estoque = 0,
        public bool $ativo = true,
        public string $categoria = "Geral",
    ) {
    }
}

$a = new Produto("Caneca", 25.90, 0, true, "Cozinha");
$b = new Produto(nome: "Caneca", preco: 25.90, categoria: "Cozinha");
$c = new Produto(preco: 3.50, nome: "Adesivo");
$d = new Produto("Camiseta", 49.90, ativo: false);

echo "{$b->nome} | {$b->estoque} | {$b->categoria}\n";
echo "{$c->nome} | {$c->preco} | {$c->categoria}\n";
var_dump($d->ativo);
$ php nomeados.php
Caneca | 0 | Cozinha
Adesivo | 3.5 | Geral
bool(false)
LinhaO que mostra
$a = new Produto("Caneca", 25.90, 0, true, "Cozinha");Tudo por posição: para dar a categoria, a Ana teve que repetir os padrões 0 e true.
$b = new Produto(nome: "Caneca", preco: 25.90, categoria: "Cozinha");Com nomes, ela pula os do meio: estoque e ativo ficam no padrão.
$c = new Produto(preco: 3.50, nome: "Adesivo");Com nomes, a ordem não importa.
$d = new Produto("Camiseta", 49.90, ativo: false);Dá para misturar: primeiro os posicionais, depois os nomeados.

Os nomes são os dos parâmetros do construtor, sem o $. Com a promoção, eles coincidem com os nomes das propriedades, o que torna a leitura ainda mais clara.


__destruct: quando o objeto vai embora (só a ideia)

O contrário do construtor é o destrutor, __destruct ("destruir"). O PHP o chama sozinho quando o objeto deixa de ser usado: quando nenhuma variável aponta mais para ele, ou no fim do script.

<?php
class Conexao
{
    public function __construct(public string $nome)
    {
        echo "Abrindo {$this->nome}\n";
    }

    public function __destruct()
    {
        echo "Fechando {$this->nome}\n";
    }
}

$a = new Conexao("banco");
$b = new Conexao("arquivo de log");
unset($a);
echo "Fim do script\n";
$ php destrutor.php
Abrindo banco
Abrindo arquivo de log
Fechando banco            « o unset tirou a última "etiqueta" do objeto: ele foi destruído na hora
Fim do script
Fechando arquivo de log   « o $b foi destruído quando o script terminou

Na prática, você quase nunca vai escrever um __destruct: o PHP já libera memória, fecha arquivos e conexões sozinho no fim de cada script (e cada página da web é um script curto). Ele aparece em classes que guardam um recurso que precisa ser "devolvido" (um arquivo aberto, uma trava). Basta saber que existe e quando roda.


Formas de criar um objeto com dados

FormaVantagemQuando usar
Sem construtor; atribuir depois do newSimplesSó em testes rápidos. Fácil esquecer um campo.
Construtor na forma longaFunciona em qualquer versão; dá para tratar cada valor antes de guardarCódigo antigo, ou quando cada valor precisa de transformação.
Construtor com promoçãoCurto, sem repetiçãoO padrão no PHP moderno. Combine com validação no corpo, se precisar.
Método estático "fábrica" (Produto::deArray($dados))Vários jeitos de criar, cada um com nome próprioQuando os dados vêm de fontes diferentes (formulário, banco, JSON). Veja static.

Quando dá errado

Too few arguments (ArgumentCountError)
<?php
class Produto
{
    public string $nome;
    public float $preco;

    public function __construct(string $nome, float $preco)
    {
        $this->nome = $nome;
        $this->preco = $preco;
    }
}

$caneca = new Produto("Caneca");
$ php poucos.php

Fatal error: Uncaught ArgumentCountError: Too few arguments to function Produto::__construct(), 1 passed in /home/ana/loja/poucos.php on line 14 and exactly 2 expected in /home/ana/loja/poucos.php:7
Stack trace:
#0 /home/ana/loja/poucos.php(14): Produto->__construct('Caneca')
#1 {main}
  thrown in /home/ana/loja/poucos.php on line 7

O que significa: "argumentos de menos para Produto::__construct(): 1 passado na linha 14, e exatamente 2 esperados". O Stack trace mostra a chamada: Produto->__construct('Caneca'), só com o nome.

Como consertar: passe todos os obrigatórios no new (new Produto("Caneca", 25.90)) ou dê um valor padrão ao parâmetro que pode faltar.

Argument #2 ($preco) must be of type float (TypeError)
<?php
class Produto
{
    public string $nome;
    public float $preco;

    public function __construct(string $nome, float $preco)
    {
        $this->nome = $nome;
        $this->preco = $preco;
    }
}

$caneca = new Produto("Caneca", "barato");
$ php tipo-errado.php

Fatal error: Uncaught TypeError: Produto::__construct(): Argument #2 ($preco) must be of type float, string given, called in /home/ana/loja/tipo-errado.php on line 14 and defined in /home/ana/loja/tipo-errado.php:7
Stack trace:
#0 /home/ana/loja/tipo-errado.php(14): Produto->__construct('Caneca', 'barato')
#1 {main}
  thrown in /home/ana/loja/tipo-errado.php on line 7

O que significa: o segundo argumento ($preco) devia ser float, mas chegou uma string. A mensagem diz onde foi chamado (linha 14) e onde o construtor está definido (linha 7).

Como consertar: passe um número. Se o valor vem de um formulário (onde tudo é texto), converta e valide antes (veja Validar Dados).

Um sublinhado só: o construtor não roda
<?php
class Produto
{
    public string $nome = "sem nome";

    public function _construct(string $nome)
    {
        $this->nome = $nome;
    }
}

$c = new Produto("Caneca");
echo $c->nome . "\n";
$ php construct-errado.php
sem nome

O que significa: nenhum erro, mas o nome não foi guardado. _construct (com um sublinhado) é um método comum, que ninguém chamou. E o PHP não reclama dos argumentos extras do new, porque a classe não tem construtor. O mesmo acontece com um método chamado igual à classe (function Produto(…)): era o construtor no PHP 4, e desde o PHP 8 é só um método comum.

Como consertar: são dois sublinhados: __construct.

Esquecer o public na promoção
<?php
class Produto
{
    public function __construct(string $nome, float $preco)
    {
    }
}

$c = new Produto("Caneca", 25.90);
echo $c->nome . "\n";
$ php sem-public.php

Warning: Undefined property: Produto::$nome in /home/ana/loja/sem-public.php on line 10

O que significa: sem a visibilidade, string $nome é só um parâmetro: o valor chega ao construtor e some. O objeto não tem propriedade nome.

Como consertar: public string $nome dentro dos parênteses.

Cannot redeclare Produto::$nome
<?php
class Produto
{
    public string $nome;

    public function __construct(public string $nome)
    {
    }
}
$ php repetida.php

Fatal error: Cannot redeclare Produto::$nome in /home/ana/loja/repetida.php on line 6
Stack trace:
#0 {main}

O que significa: a propriedade foi declarada duas vezes: na classe (linha 4) e promovida no construtor (linha 6).

Como consertar: escolha uma forma. Com promoção, apague a declaração de cima.

Unknown named parameter
<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }
}

$c = new Produto(nome: "Caneca", preço: 25.90);
$ php nome-errado.php

Fatal error: Uncaught Error: Unknown named parameter $preço in /home/ana/loja/nome-errado.php:11
Stack trace:
#0 {main}
  thrown in /home/ana/loja/nome-errado.php on line 11

O que significa: "parâmetro nomeado desconhecido $preço". O construtor tem $preco, sem cedilha.

Como consertar: use exatamente o nome do parâmetro: preco: 25.90.

Cannot use positional argument after named argument
<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }
}

$c = new Produto(nome: "Caneca", 25.90);
$ php posicional-depois.php

Fatal error: Cannot use positional argument after named argument in /home/ana/loja/posicional-depois.php on line 11
Stack trace:
#0 {main}

O que significa: depois de um argumento com nome, o PHP não sabe a que posição um argumento sem nome corresponde. O arquivo nem começa a rodar.

Como consertar: primeiro os posicionais, depois os nomeados: new Produto("Caneca", preco: 25.90), ou todos com nome.


Referência

EscritaO que faz
public function __construct(tipo $p) { … }Construtor: roda sozinho no new
new Classe(arg1, arg2)Passa os argumentos para o construtor
tipo $p = valorParâmetro opcional, com valor padrão
public tipo $p (nos parênteses do construtor) PHP 8.0+Promoção: vira propriedade e recebe o valor
new Classe(nome: valor) PHP 8.0+Argumento nomeado
throw new InvalidArgumentException("…")Recusa dados inválidos no construtor
public function __destruct() { … }Destrutor: roda quando o objeto é descartado

Missão final

Missão: escreva a classe Cliente com um construtor promovido com três propriedades públicas: nome (texto), email (texto) e cidade (texto, com padrão "Recife"). Escreva também o método ficha(): string, que devolve "Nome (email) - Cidade". Crie a Ana só com nome e e-mail, e o Bruno usando argumentos nomeados (nome:, email:, cidade: "Natal"). A saída deve ser Ana (ana@exemplo.com) - Recife e Bruno (bruno@exemplo.com) - Natal.

<?php
// escreva a classe Cliente aqui


$ana = new Cliente("Ana", "ana@exemplo.com");
// crie o $bruno com argumentos nomeados


echo $ana->ficha() . "\n";
echo $bruno->ficha() . "\n";
<?php
class Cliente
{
    public function __construct(
        public string $nome,
        public string $email,
        public string $cidade = "Recife",
    ) {
    }

    public function ficha(): string
    {
        return "{$this->nome} ({$this->email}) - {$this->cidade}";
    }
}

$ana = new Cliente("Ana", "ana@exemplo.com");
$bruno = new Cliente(nome: "Bruno", email: "bruno@exemplo.com", cidade: "Natal");

echo $ana->ficha() . "\n";
echo $bruno->ficha() . "\n";

Exercícios

1. O nome do construtor

Complete a declaração do construtor:

public function (string $nome)

2. Promoção

Complete o parâmetro para ele virar uma propriedade pública do tipo float:

public function __construct() {}

3. Argumento nomeado

O construtor é __construct(public string $nome, public int $estoque = 0, public bool $ativo = true). Complete para criar um "Chaveiro" inativo, sem informar o estoque:

new Produto("Chaveiro", )

4. Quando o __construct é executado?

5. O construtor é __construct(string $nome, float $preco). O que acontece com new Produto("Caneca")?

6. Em __construct(public string $nome, int $codigo), quais propriedades o objeto ganha?

7. Qual destas chamadas dá erro de sintaxe?


Resumo

  • O construtor __construct (dois sublinhados) roda sozinho a cada new e recebe os argumentos dos parênteses.
  • Use-o para que todo objeto nasça completo e para validar os dados (lançando uma exceção se algo estiver errado).
  • Parâmetros podem ter valor padrão; os opcionais ficam no fim.
  • Promoção (8.0+): public tipo $nome nos parênteses do construtor declara a propriedade e guarda o valor. É o padrão no PHP moderno.
  • Argumentos nomeados: new Produto(nome: "Caneca", preco: 25.90); pule opcionais e esqueça a ordem. Posicionais sempre antes dos nomeados.
  • __destruct roda quando o objeto é descartado; raramente é necessário.