PHP Propriedades e Métodos

Uma classe tem duas partes: o que o objeto sabe (as propriedades, os dados) e o que ele faz (os métodos, as ações). Nesta aula você vai declarar propriedades com tipo, entender o que acontece com uma propriedade que ainda não recebeu valor, escrever métodos que usam $this e conhecer o aviso que o PHP 8.2 passou a dar quando você inventa uma propriedade na hora.

Você vai aprender
  • Declarar propriedades com tipo (string, int, float, bool, array, ?string) e valor inicial.
  • O estado "não inicializada" e o erro Typed property … must not be accessed before initialization.
  • Como o PHP converte (ou recusa) valores do tipo errado.
  • Escrever métodos com parâmetros e retorno, e usar $this.
  • Um método chamando outro, e métodos encadeados (->a()->b()).
  • Propriedades dinâmicas e o Deprecated do PHP 8.2.

Você já sabe criar objetos com new e usar a seta -> (Classes e Objetos). Aqui, todos os exemplos usam a classe Produto da lojinha da Ana.


A ideia: a ficha e o que se faz com ela

Pense na ficha de um produto na lojinha. Ela tem campos (nome, preço, estoque): são as propriedades. E a Ana tem um procedimento para cada coisa que faz com o produto (vender, conferir o estoque, mostrar o preço formatado): são os métodos. Um método é só uma função que mora dentro da classe e trabalha com os campos daquela ficha.


Propriedades

Uma propriedade é declarada dentro da classe, fora de qualquer método:

public
A visibilidade: quem pode mexer nela. public (pública) = qualquer código. As outras opções estão em Visibilidade.
float
O tipo: que tipo de valor ela aceita. Opcional, mas recomendado.
$preco
O nome, com $ (como uma variável). Na hora de usar, o $ some: $caneca->preco.
= 25.90
O valor inicial que todo objeto novo recebe. Opcional. Precisa ser um valor fixo (número, texto, array, null, constante); não pode chamar função.

Os tipos são os mesmos que você usa em parâmetros e retornos:

<?php
class Produto
{
    public string $nome = "Caneca";
    public float $preco = 25.90;
    public int $estoque = 10;
    public bool $ativo = true;
    public array $tags = ["cozinha", "presente"];
    public ?string $descricao = null;
    public $antiga;
}

$caneca = new Produto();
var_dump($caneca);
$ php tipos.php
object(Produto)#1 (7) {
  ["nome"]=>
  string(6) "Caneca"
  ["preco"]=>
  float(25.9)
  ["estoque"]=>
  int(10)
  ["ativo"]=>
  bool(true)
  ["tags"]=>
  array(2) {
    [0]=>
    string(7) "cozinha"
    [1]=>
    string(8) "presente"
  }
  ["descricao"]=>
  NULL
  ["antiga"]=>
  NULL
}
DeclaraçãoAceita
public string $nomeTexto
public int $estoqueNúmero inteiro
public float $precoNúmero com casas decimais (e inteiros, que viram float)
public bool $ativotrue ou false
public array $tagsUm array
public ?string $descricaoTexto ou null (o ? quer dizer "pode ser nulo")
public Cliente $donoUm objeto da classe Cliente
public $antigaQualquer coisa (sem tipo). Começa com null. Estilo antigo: prefira sempre declarar o tipo.
E o var?

Em código muito antigo (do PHP 4), você vai ver var $nome;. É o mesmo que public $nome;. Funciona até hoje, mas ninguém usa mais em código novo.

Propriedade sem valor inicial: "não inicializada"

Uma propriedade com tipo e sem valor inicial começa num estado especial: não inicializada (uninitialized). Ela não vale null, nem "", nem 0: ela simplesmente ainda não tem valor. Ler essa propriedade antes de dar um valor a ela é um erro:

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

$caneca = new Produto();
var_dump($caneca);
$caneca->nome = "Caneca";
echo $caneca->nome . "\n";
echo $caneca->preco . "\n";
$ php sem-inicial.php
object(Produto)#1 (0) {          « (0): nenhuma propriedade tem valor ainda
  ["nome"]=>
  uninitialized(string)
  ["preco"]=>
  uninitialized(float)
}
Caneca                           « o nome recebeu valor na linha 10: pode ser lido

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

A mensagem diz: "a propriedade tipada Produto::$preco não pode ser acessada antes da inicialização". Ninguém deu um preço à caneca, e o PHP prefere parar a inventar um valor.

DeclaraçãoValor no objeto novoLer antes de atribuir
public float $preco = 0;0.0Funciona: dá 0.0
public ?float $preco = null;nullFunciona: dá null
public float $preco;não inicializadaErro (must not be accessed before initialization)
public $preco; (sem tipo)nullFunciona: dá null

Qual usar? Se existe um valor inicial que faz sentido (estoque 0, lista vazia []), declare-o. Se não existe (todo produto precisa de um nome de verdade), deixe sem valor inicial e preencha no construtor, que é o assunto da próxima aula (Construtores). Evite usar null só para fugir do erro: aí você terá que conferir null em todo lugar.

O tipo errado: convertido ou recusado

Quando você atribui um valor de outro tipo, o PHP tenta converter, se a conversão for "segura". Se não for, ele recusa:

<?php
class Produto
{
    public float $preco = 0;
    public int $estoque = 0;
}

$p = new Produto();
$p->preco = 10;
$p->estoque = "7";
var_dump($p->preco, $p->estoque);
$p->estoque = 7.5;
$ php coercao.php
float(10)        « o inteiro 10 virou 10.0: sem problema
int(7)           « o texto "7" virou o número 7

Deprecated: Implicit conversion from float 7.5 to int loses precision in /home/ana/loja/coercao.php on line 12

O 7.5 num int perderia o .5: o PHP guarda 7 e avisa (Deprecated: em versões futuras isso vai virar erro). E um texto que não é número, como "barato", é sempre recusado com TypeError (você viu em O que é POO).

Se você quer que o PHP não converta nada, ligue o modo estrito na primeira linha do arquivo. Aí só o inteiro → float continua aceito:

<?php
declare(strict_types=1);

class Produto
{
    public float $preco = 0;
    public int $estoque = 0;
}

$p = new Produto();
$p->preco = 10;
var_dump($p->preco);
$p->estoque = "7";
$ php estrito.php
float(10)

Fatal error: Uncaught TypeError: Cannot assign string to property Produto::$estoque of type int in /home/ana/loja/estrito.php:13
Stack trace:
#0 {main}
  thrown in /home/ana/loja/estrito.php on line 13

O declare(strict_types=1) vale para o arquivo onde está escrito. Muitos projetos profissionais o usam em todos os arquivos (veja Tipos e Retorno).


Métodos

Um método é declarado como uma função, só que dentro da classe e com a visibilidade na frente:

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

    public function temEstoque(int $quantidade): bool
    {
        return $this->estoque >= $quantidade;
    }

    public function vender(int $quantidade): float
    {
        if (!$this->temEstoque($quantidade)) {
            echo "Sem estoque: pediram {$quantidade}, há {$this->estoque}\n";
            return 0;
        }
        $this->estoque -= $quantidade;
        return $quantidade * $this->preco;
    }

    public function precoFormatado(): string
    {
        return "R$ " . number_format($this->preco, 2, ",", ".");
    }
}

$caneca = new Produto();
$caneca->nome = "Caneca";
$caneca->preco = 25.90;
$caneca->estoque = 5;

echo $caneca->nome . " custa " . $caneca->precoFormatado() . "\n";
$valor = $caneca->vender(3);
echo "Venda de R$ " . number_format($valor, 2, ",", ".") . "; restam {$caneca->estoque}\n";
$caneca->vender(4);
var_dump($caneca->temEstoque(2));
$ php metodos.php
Caneca custa R$ 25,90
Venda de R$ 77,70; restam 2
Sem estoque: pediram 4, há 2
bool(true)
public function
Declara um método público (que pode ser chamado de fora da classe).
temEstoque(int $quantidade)
Nome e parâmetros, como numa função. Por convenção, nomes de método começam com letra minúscula, e cada palavra nova com maiúscula (camelCase): temEstoque, precoFormatado.
: bool
O tipo de retorno. : void quer dizer "não devolve nada".
$this->estoque
A propriedade estoque do objeto que chamou o método.
$this->temEstoque($quantidade)
Dentro de vender, o objeto chama outro método dele mesmo. Assim a regra "tem estoque?" fica escrita num lugar só.

Repare que os métodos fazem dois tipos de trabalho: temEstoque e precoFormatado só consultam o objeto; vender muda o objeto (diminui o estoque) e ainda devolve o valor da venda.

$this: "este objeto"

O método é escrito uma vez só, na classe. Então como ele sabe de qual produto está falando? Pelo $this ("este"). Quando você escreve $caneca->vender(3), durante aquela chamada $this é a $caneca. Quando escreve $camiseta->vender(1), $this é a $camiseta:

<?php
class Produto
{
    public string $nome = "";

    public function quemSouEu(): string
    {
        return "Eu sou a {$this->nome}";
    }
}

$a = new Produto();
$a->nome = "Caneca";
$b = new Produto();
$b->nome = "Camiseta";

echo $a->quemSouEu() . "\n";
echo $b->quemSouEu() . "\n";
$ php this.php
Eu sou a Caneca
Eu sou a Camiseta

O mesmo método, a mesma linha de código, dois resultados: cada chamada recebe o seu próprio $this. É como a frase "eu sou a Ana" escrita num crachá-modelo: cada pessoa que usa o crachá diz o próprio nome.

Missão: acrescente na classe um método total(int $quantidade): float que devolve $quantidade vezes o preço do produto (use $this->preco). A última linha já usa o método: a saída deve terminar com Total: R$ 129,50.

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

    public function precoFormatado(): string
    {
        return "R$ " . number_format($this->preco, 2, ",", ".");
    }

    // escreva o método total aqui
}

$caneca = new Produto();
$caneca->nome = "Caneca";
$caneca->preco = 25.90;

echo $caneca->nome . " custa " . $caneca->precoFormatado() . "\n";
echo "Total: R$ " . number_format($caneca->total(5), 2, ",", ".") . "\n";
<?php
class Produto
{
    public string $nome = "";
    public float $preco = 0;

    public function precoFormatado(): string
    {
        return "R$ " . number_format($this->preco, 2, ",", ".");
    }

    public function total(int $quantidade): float
    {
        return $quantidade * $this->preco;
    }
}

$caneca = new Produto();
$caneca->nome = "Caneca";
$caneca->preco = 25.90;

echo $caneca->nome . " custa " . $caneca->precoFormatado() . "\n";
echo "Total: R$ " . number_format($caneca->total(5), 2, ",", ".") . "\n";

Métodos encadeados

Se um método devolve o próprio objeto (return $this;), você pode chamar outro método logo em seguida, numa linha só. Esse estilo se chama interface fluente:

<?php
class Carrinho
{
    public array $itens = [];

    public function adicionar(string $item): Carrinho
    {
        $this->itens[] = $item;
        return $this;
    }
}

$c = new Carrinho();
$c->adicionar("Caneca")->adicionar("Camiseta")->adicionar("Adesivo");
echo implode(", ", $c->itens) . "\n";
$ php fluente.php
Caneca, Camiseta, Adesivo

O tipo de retorno : Carrinho diz "este método devolve um Carrinho". Cada ->adicionar(…) devolve o mesmo carrinho, e o próximo ->adicionar(…) é chamado nele. Você vai ver muito isso em bibliotecas (por exemplo, para montar consultas ao banco).

Propriedade e método com o mesmo nome

Uma propriedade e um método podem ter o mesmo nome; o que decide é o parêntese:

<?php
class Produto
{
    public string $nome = "Caneca";
    public function nome(): string { return strtoupper($this->nome); }
}
$c = new Produto();
echo $c->nome . " / " . $c->nome() . "\n";
echo $c->nome();
echo "\n";
echo $c->preco();
$ php propfunc.php
Caneca / CANECA       « $c->nome é a propriedade; $c->nome() é o método
CANECA

Fatal error: Uncaught Error: Call to undefined method Produto::preco() in /home/ana/loja/propfunc.php:11
Stack trace:
#0 {main}
  thrown in /home/ana/loja/propfunc.php on line 11

Com parênteses, o PHP procura um método. Não existe método preco(), então vem o erro Call to undefined method ("chamada de método indefinido").


Propriedades dinâmicas e o aviso do PHP 8.2 PHP 8.2+

Uma propriedade dinâmica é uma propriedade que não foi declarada na classe e foi criada na hora, só por receber um valor. Até o PHP 8.1, isso acontecia em silêncio. Do 8.2 em diante, o PHP avisa:

<?php
class Produto
{
    public string $nome = "Caneca";
}

$caneca = new Produto();
$caneca->cor = "azul";
echo $caneca->cor . "\n";
var_dump($caneca);
$ php dinamica.php

Deprecated: Creation of dynamic property Produto::$cor is deprecated in /home/ana/loja/dinamica.php on line 8
azul
object(Produto)#1 (2) {
  ["nome"]=>
  string(6) "Caneca"
  ["cor"]=>
  string(4) "azul"
}

O programa continuou (a cor foi criada e mostrada), mas com um Deprecated: "a criação da propriedade dinâmica Produto::$cor está obsoleta". Obsoleta quer dizer que um dia (o plano é o PHP 9) isso vai virar erro. O motivo: na enorme maioria das vezes, uma propriedade dinâmica é um erro de digitação ($caneca->preço em vez de $caneca->preco) que antes passava despercebido.

Como resolver, do melhor para o pior:

  1. Declare a propriedade na classe: public string $cor = "";. É o certo em 99% dos casos.
  2. Se você precisa mesmo de "chaves livres", use um array: public array $extras = []; e $caneca->extras["cor"] = "azul";.
  3. Se a classe é antiga e você não pode mudá-la agora, marque-a com o atributo #[\AllowDynamicProperties] ("permitir propriedades dinâmicas"):
<?php
#[\AllowDynamicProperties]
class Produto
{
    public string $nome = "Caneca";
}

$caneca = new Produto();
$caneca->cor = "azul";
echo $caneca->cor . "\n";

$generico = new stdClass();
$generico->qualquer = "coisa";
echo $generico->qualquer . "\n";
$ php permitir.php
azul
coisa

Sem aviso. Repare também no stdClass, o objeto genérico do PHP: ele aceita propriedades dinâmicas sempre, sem atributo nenhum, porque existe justamente para isso. (Atributos como #[\AllowDynamicProperties] têm uma aula própria: Atributos.)


Formas de dar valor às propriedades

FormaExemploQuando usar
Valor inicial na declaraçãopublic int $estoque = 0;Quando existe um valor "padrão" que serve para todo objeto novo.
Atribuir de fora, depois do new$caneca->nome = "Caneca";Para experimentar e em objetos simples. Fica fácil esquecer uma propriedade.
Um método que muda o objeto$caneca->vender(3);Quando a mudança tem regras (não vender sem estoque). O melhor para dados importantes.
No construtor, na hora do newnew Produto("Caneca", 25.90)O mais usado para dados obrigatórios. É a próxima aula: Construtores.

Quando dá errado

Esquecer o $this->
<?php
class Produto
{
    public float $preco = 25.90;

    public function total(int $quantidade): float
    {
        return $preco * $quantidade;
    }
}

$caneca = new Produto();
echo $caneca->total(2) . "\n";
$ php sem-this.php

Warning: Undefined variable $preco in /home/ana/loja/sem-this.php on line 8
0

O que significa: dentro do método, $preco é uma variável local (que não existe), e não a propriedade. O PHP avisa Undefined variable $preco, trata o valor como null e a conta dá 0. O programa nem para: o erro vira um resultado errado.

Como consertar: dentro de um método, propriedades são sempre $this->nome: return $this->preco * $quantidade;. (Quem vem do Java estranha: lá o this é opcional; no PHP, não.)

Using $this when not in object context
<?php
class Produto
{
    public string $nome = "Caneca";
}
echo $this->nome;
$ php this-fora.php

Fatal error: Uncaught Error: Using $this when not in object context in /home/ana/loja/this-fora.php:6
Stack trace:
#0 {main}
  thrown in /home/ana/loja/this-fora.php on line 6

O que significa: "usando $this fora do contexto de um objeto". O $this só existe dentro de um método, durante uma chamada. Fora da classe, não há "este objeto".

Como consertar: fora da classe, use a variável do objeto: $caneca->nome.

Call to undefined method
<?php
class Produto
{
    public function vender(int $quantidade): void {}
}
$caneca = new Produto();
$caneca->vendr(3);
$ php metodo-errado.php

Fatal error: Uncaught Error: Call to undefined method Produto::vendr() in /home/ana/loja/metodo-errado.php:7
Stack trace:
#0 {main}
  thrown in /home/ana/loja/metodo-errado.php on line 7

O que significa: o método vendr não existe na classe Produto (faltou o e).

Como consertar: confira o nome na classe. Nomes de método, como os de classe, não diferenciam maiúsculas e minúsculas (VENDER() funcionaria), mas escreva sempre igual à declaração.

Return value must be of type …, none returned
<?php
class Produto
{
    public float $preco = 25.90;
    public function precoComDesconto(): float
    {
        $novo = $this->preco * 0.9;
    }
}
$c = new Produto();
echo $c->precoComDesconto();
$ php sem-retorno.php

Fatal error: Uncaught TypeError: Produto::precoComDesconto(): Return value must be of type float, none returned in /home/ana/loja/sem-retorno.php:8
Stack trace:
#0 /home/ana/loja/sem-retorno.php(11): Produto->precoComDesconto()
#1 {main}
  thrown in /home/ana/loja/sem-retorno.php on line 8

O que significa: o método prometeu devolver um float, mas terminou sem return ("nenhum retornado"). Repare no Stack trace: a linha #0 mostra de onde o método foi chamado (linha 11), útil quando o erro está dentro de um método.

Como consertar: devolva o valor: return $this->preco * 0.9;.

Typed property … must not be accessed before initialization

Você já viu este erro acima: uma propriedade com tipo e sem valor inicial foi lida antes de receber valor. Dê um valor inicial na declaração, atribua antes de ler, ou (o melhor) receba o valor no construtor.


Referência

EscritaO que faz
public tipo $nome = valor;Declara uma propriedade com tipo e valor inicial
public ?tipo $nome = null;Propriedade que aceita null
public tipo $nome;Começa "não inicializada"; ler antes de atribuir é erro
public function nome(params): tipo { … }Declara um método
$this->prop / $this->metodo()Dentro de um método: usa o objeto atual
return $this;Permite encadear chamadas
declare(strict_types=1);Desliga as conversões automáticas de tipo no arquivo
#[\AllowDynamicProperties]Permite propriedades dinâmicas numa classe, sem aviso

Missão final

Missão: complete a classe Carrinho. Ela já tem a propriedade $itens (um array de preços, começando vazio). Escreva os métodos adicionar(float $preco): void (põe o preço no fim de $this->itens), quantidade(): int (devolve count dos itens) e total(): float (devolve array_sum dos itens). A saída deve ser 3 itens e Total: R$ 79,30, sem nenhum aviso.

<?php
class Carrinho
{
    public array $itens = [];

    // escreva os métodos aqui
}

$carrinho = new Carrinho();
$carrinho->adicionar(25.90);
$carrinho->adicionar(49.90);
$carrinho->adicionar(3.50);

echo $carrinho->quantidade() . " itens\n";
echo "Total: R$ " . number_format($carrinho->total(), 2, ",", ".") . "\n";
<?php
class Carrinho
{
    public array $itens = [];

    public function adicionar(float $preco): void
    {
        $this->itens[] = $preco;
    }

    public function quantidade(): int
    {
        return count($this->itens);
    }

    public function total(): float
    {
        return array_sum($this->itens);
    }
}

$carrinho = new Carrinho();
$carrinho->adicionar(25.90);
$carrinho->adicionar(49.90);
$carrinho->adicionar(3.50);

echo $carrinho->quantidade() . " itens\n";
echo "Total: R$ " . number_format($carrinho->total(), 2, ",", ".") . "\n";

Exercícios

1. Propriedade que aceita nulo

Complete para a propriedade $descricao aceitar texto ou null, começando com null:

public $descricao = null;

2. Dentro do método

Complete o método para devolver o estoque deste produto:

return ;

3. Encadear

Complete a última linha do método adicionar para permitir $c->adicionar("a")->adicionar("b"):

return ;

4. A classe tem public int $estoque; (sem valor inicial). O que acontece com echo $p->estoque; logo depois do new?

5. No PHP 8.5, o que acontece com $caneca->cor = "azul"; se a classe não declara $cor (e não tem #[\AllowDynamicProperties])?

6. Dentro de um método, a Ana escreveu return $preco * 2; em vez de $this->preco. O que acontece?

7. Qual é a convenção de nomes para métodos no PHP?


Resumo

  • Propriedades guardam os dados: public tipo $nome = valor;. Declare sempre o tipo.
  • Propriedade tipada sem valor inicial fica não inicializada: lê-la antes de atribuir dá Error: Typed property … must not be accessed before initialization.
  • O PHP converte tipos "seguros" ("7" → 7); com declare(strict_types=1), recusa.
  • Métodos são funções da classe: public function nome(): tipo, em camelCase.
  • $this é o objeto que chamou o método. Propriedades, dentro do método, são sempre $this->prop.
  • return $this; permite encadear chamadas.
  • Propriedade não declarada criada na hora = propriedade dinâmica: Deprecated desde o 8.2. Declare a propriedade (ou, em último caso, #[\AllowDynamicProperties]).