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.
- 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
Deprecateddo 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ção | Aceita |
|---|---|
public string $nome | Texto |
public int $estoque | Número inteiro |
public float $preco | Número com casas decimais (e inteiros, que viram float) |
public bool $ativo | true ou false |
public array $tags | Um array |
public ?string $descricao | Texto ou null (o ? quer dizer "pode ser nulo") |
public Cliente $dono | Um objeto da classe Cliente |
public $antiga | Qualquer coisa (sem tipo). Começa com null. Estilo antigo: prefira sempre declarar o tipo. |
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ção | Valor no objeto novo | Ler antes de atribuir |
|---|---|---|
public float $preco = 0; | 0.0 | Funciona: dá 0.0 |
public ?float $preco = null; | null | Funciona: dá null |
public float $preco; | não inicializada | Erro (must not be accessed before initialization) |
public $preco; (sem tipo) | null | Funciona: 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.
: voidquer dizer "não devolve nada". - $this->estoque
- A propriedade
estoquedo 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:
- Declare a propriedade na classe:
public string $cor = "";. É o certo em 99% dos casos. - Se você precisa mesmo de "chaves livres", use um array:
public array $extras = [];e$caneca->extras["cor"] = "azul";. - 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
| Forma | Exemplo | Quando usar |
|---|---|---|
| Valor inicial na declaração | public 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 new | new Produto("Caneca", 25.90) | O mais usado para dados obrigatórios. É a próxima aula: Construtores. |
Quando dá errado
$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.)
<?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.
<?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.
<?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;.
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
| Escrita | O 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:
2. Dentro do método
Complete o método para devolver o estoque deste produto:
3. Encadear
Complete a última linha do método adicionar para permitir $c->adicionar("a")->adicionar("b"):
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); comdeclare(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:
Deprecateddesde o 8.2. Declare a propriedade (ou, em último caso,#[\AllowDynamicProperties]).