Introdução à programação orientada a objetos

O termo “POO” refere-se à programação orientada a objetos, que é uma forma de organizar e estruturar o código. A POO permite-nos ver um programa como um conjunto de objetos que comunicam entre si, em vez de uma sequência de comandos e funções.

Na POO, um “objeto” é uma unidade que contém dados e funções que operam sobre esses dados. Os objetos são criados a partir de “classes”, que podemos entender como projetos ou modelos para objetos. Quando temos uma classe, podemos criar a sua “instância”, que é um objeto específico criado a partir dessa classe.

Vamos mostrar como podemos criar uma classe simples em PHP. Ao definir uma classe, usamos a palavra-chave “class”, seguida pelo nome da classe e, em seguida, chaves que envolvem as funções (chamadas de “métodos”) e variáveis da classe (chamadas de “propriedades”):

class Carro
{
	function buzinar()
	{
		echo 'Bip bip!';
	}
}

Neste exemplo, criamos uma classe chamada Carro com uma função (ou “método”) chamada buzinar.

Cada classe deve resolver apenas uma tarefa principal. Se uma classe faz muitas coisas, pode ser apropriado dividi-la em classes menores e especializadas.

Normalmente, guardamos as classes em arquivos separados para manter o código organizado e fácil de navegar. O nome do arquivo deve corresponder ao nome da classe, portanto, para a classe Carro, o nome do arquivo seria Carro.php.

Ao nomear classes, é bom seguir a convenção “PascalCase”, o que significa que cada palavra no nome começa com uma letra maiúscula e não há sublinhados ou outros separadores entre elas. Métodos e propriedades usam a convenção “camelCase”, o que significa que começam com uma letra minúscula.

Alguns métodos em PHP têm tarefas especiais e são prefixados com __ (dois sublinhados). Um dos métodos especiais mais importantes é o “construtor”, que é marcado como __construct. O construtor é um método que é chamado automaticamente quando você cria uma nova instância da classe.

Frequentemente usamos o construtor para definir o estado inicial de um objeto. Por exemplo, ao criar um objeto que representa uma pessoa, você pode usar o construtor para definir sua idade, nome ou outras propriedades.

Vamos ver como usar um construtor em PHP:

class Pessoa
{
	private $idade;

	function __construct($idade)
	{
		$this->idade = $idade;
	}

	function quantosAnosVoceTem()
	{
		return $this->idade;
	}
}

$pessoa = new Pessoa(25);
echo $pessoa->quantosAnosVoceTem(); // Imprime: 25

Neste exemplo, a classe Pessoa tem uma propriedade (variável) $idade e um construtor que define essa propriedade. O método quantosAnosVoceTem() permite então aceder à idade da pessoa.

A pseudovariável $this é usada dentro da classe para aceder às propriedades e métodos do objeto.

A palavra-chave new é usada para criar uma nova instância da classe. No exemplo acima, criamos uma nova pessoa com 25 anos.

Você também pode definir valores padrão para os parâmetros do construtor, caso não sejam especificados ao criar o objeto. Por exemplo:

class Pessoa
{
	private $idade;

	function __construct($idade = 20)
	{
		$this->idade = $idade;
	}

	function quantosAnosVoceTem()
	{
		return $this->idade;
	}
}

$pessoa = new Pessoa;  // se não passarmos nenhum argumento, os parênteses podem ser omitidos
echo $pessoa->quantosAnosVoceTem(); // Imprime: 20

Neste exemplo, se você não especificar a idade ao criar o objeto Pessoa, o valor padrão 20 será usado.

É conveniente que a definição da propriedade com sua inicialização através do construtor possa ser abreviada e simplificada desta forma:

class Pessoa
{
	function __construct(
		private $idade = 20,
	) {
	}
}

Para completar, além dos construtores, os objetos também podem ter destrutores (método __destruct), que são chamados antes que o objeto seja libertado da memória.

Namespaces

Namespaces (ou “namespaces” em inglês) permitem-nos organizar e agrupar classes, funções e constantes relacionadas, evitando ao mesmo tempo conflitos de nomes. Pode imaginá-los como pastas num computador, onde cada pasta contém arquivos que pertencem a um determinado projeto ou tópico.

Namespaces são especialmente úteis em projetos maiores ou quando se usam bibliotecas de terceiros, onde podem ocorrer conflitos nos nomes das classes.

Imagine que tem uma classe chamada Carro no seu projeto e quer colocá-la num namespace chamado Transporte. Faria isso da seguinte forma:

namespace Transporte;

class Carro
{
	function buzinar()
	{
		echo 'Bip bip!';
	}
}

Se quiser usar a classe Carro noutro arquivo, precisa especificar de qual namespace a classe vem:

$carro = new Transporte\Carro;

Para simplificar, pode indicar no início do arquivo qual classe de um determinado namespace deseja usar, o que permite criar instâncias sem a necessidade de especificar o caminho completo:

use Transporte\Carro;

$carro = new Carro;

Herança

A herança é uma ferramenta da programação orientada a objetos que permite criar novas classes com base em classes existentes, herdando suas propriedades e métodos e estendendo-os ou redefinindo-os conforme necessário. A herança permite garantir a reutilização de código e a hierarquia de classes.

Simplificando, se tivermos uma classe e quisermos criar outra derivada dela, mas com algumas alterações, podemos “herdar” a nova classe da classe original.

Em PHP, a herança é realizada usando a palavra-chave extends.

Nossa classe Pessoa armazena informações sobre a idade. Podemos ter outra classe Estudante, que estende Pessoa e adiciona informações sobre a área de estudo.

Vejamos um exemplo:

class Pessoa
{
	private $idade;

	function __construct($idade)
	{
		$this->idade = $idade;
	}

	function exibirInformacoes()
	{
		echo "Idade: {$this->idade} anos\n";
	}
}

class Estudante extends Pessoa
{
	private $curso;

	function __construct($idade, $curso)
	{
		parent::__construct($idade);
		$this->curso = $curso;
	}

	function exibirInformacoes()
	{
		parent::exibirInformacoes();
		echo "Curso: {$this->curso} \n";
	}
}

$estudante = new Estudante(20, 'Informática');
$estudante->exibirInformacoes();

Como este código funciona?

  • Usamos a palavra-chave extends para estender a classe Pessoa, o que significa que a classe Estudante herdará todos os métodos e propriedades de Pessoa.
  • A palavra-chave parent:: permite-nos chamar métodos da classe pai. Neste caso, chamamos o construtor da classe Pessoa antes de adicionar a nossa própria funcionalidade à classe Estudante. E da mesma forma, o método exibirInformacoes() do ancestral antes de exibir as informações do estudante.

A herança destina-se a situações em que existe uma relação “é um” entre as classes. Por exemplo, Estudante é uma Pessoa. Gato é um animal. Dá-nos a possibilidade, nos casos em que esperamos um objeto (por exemplo, “Pessoa”) no código, usar em vez disso um objeto herdado (por exemplo, “Estudante”).

É importante notar que o principal propósito da herança não é evitar a duplicação de código. Pelo contrário, o uso incorreto da herança pode levar a código complexo e difícil de manter. Se a relação “é um” entre as classes não existir, devemos considerar a composição em vez da herança.

Note que os métodos exibirInformacoes() nas classes Pessoa e Estudante exibem informações ligeiramente diferentes. E podemos adicionar outras classes (por exemplo, Funcionario), que fornecerão outras implementações deste método. A capacidade de objetos de diferentes classes responderem ao mesmo método de maneiras diferentes é chamada de polimorfismo:

$pessoas = [
	new Pessoa(30),
	new Estudante(20, 'Informática'),
	new Funcionario(45, 'Diretor'), // Supondo que a classe Funcionario existe
];

foreach ($pessoas as $pessoa) {
	$pessoa->exibirInformacoes();
}

Composição

A composição é uma técnica em que, em vez de herdarmos as propriedades e métodos de outra classe, simplesmente usamos a sua instância na nossa classe. Isso permite-nos combinar funcionalidades e propriedades de várias classes sem a necessidade de criar estruturas de herança complexas.

Vejamos um exemplo. Temos uma classe Motor e uma classe Carro. Em vez de dizermos “Carro é um Motor”, dizemos “Carro tem um Motor”, que é uma relação típica de composição.

class Motor
{
	function ligar()
	{
		echo 'Motor a funcionar.';
	}
}

class Carro
{
	private $motor;

	function __construct()
	{
		$this->motor = new Motor;
	}

	function iniciar()
	{
		$this->motor->ligar();
		echo 'Carro pronto para andar!';
	}
}

$carro = new Carro;
$carro->iniciar();

Aqui, Carro não tem todas as propriedades e métodos de Motor, mas tem acesso a ele através da propriedade $motor.

A vantagem da composição é maior flexibilidade no design e melhor capacidade de modificação no futuro.

Visibilidade

Em PHP, pode definir a “visibilidade” para propriedades, métodos e constantes de uma classe. A visibilidade determina de onde pode aceder a esses elementos.

  1. Public: Se um elemento é marcado como public, significa que pode aceder a ele de qualquer lugar, mesmo fora da classe.
  2. Protected: Um elemento marcado como protected é acessível apenas dentro da classe e de todos os seus descendentes (classes que herdam desta classe).
  3. Private: Se um elemento é private, só pode aceder a ele de dentro da classe em que foi definido.

Se não especificar a visibilidade, o PHP definirá automaticamente como public.

Vejamos um código de exemplo:

class ExemploVisibilidade
{
	public $propriedadePublica = 'Pública';
	protected $propriedadeProtegida = 'Protegida';
	private $propriedadePrivada = 'Privada';

	public function exibirPropriedades()
	{
		echo $this->propriedadePublica;  // Funciona
		echo $this->propriedadeProtegida; // Funciona
		echo $this->propriedadePrivada; // Funciona
	}
}

$objeto = new ExemploVisibilidade;
$objeto->exibirPropriedades();
echo $objeto->propriedadePublica;      // Funciona
// echo $objeto->propriedadeProtegida;  // Lança um erro
// echo $objeto->propriedadePrivada;  // Lança um erro

Continuamos com a herança da classe:

class DescendenteClasse extends ExemploVisibilidade
{
	public function exibirPropriedades()
	{
		echo $this->propriedadePublica;   // Funciona
		echo $this->propriedadeProtegida;  // Funciona
		// echo $this->propriedadePrivada;  // Lança um erro
	}
}

Neste caso, o método exibirPropriedades() na classe DescendenteClasse pode aceder às propriedades públicas e protegidas, mas não pode aceder às propriedades privadas da classe pai.

Dados e métodos devem ser o mais ocultos possível e acessíveis apenas através de uma interface definida. Isso permite que você altere a implementação interna da classe sem afetar o resto do código.

Palavra-chave final

Em PHP, podemos usar a palavra-chave final se quisermos impedir que uma classe, método ou constante seja herdada ou sobrescrita. Quando marcamos uma classe como final, ela não pode ser estendida. Quando marcamos um método como final, ele não pode ser sobrescrito numa classe descendente.

Saber que uma determinada classe ou método não será mais modificado permite-nos fazer alterações mais facilmente, sem ter que nos preocupar com possíveis conflitos. Por exemplo, podemos adicionar um novo método sem medo de que algum descendente já tenha um método com o mesmo nome e ocorra uma colisão. Ou podemos alterar os parâmetros de um método, pois novamente não há risco de causar inconsistência com um método sobrescrito num descendente.

final class ClasseFinal
{
}

// O código a seguir causará um erro, pois não podemos herdar de uma classe final.
class DescendenteClasseFinal extends ClasseFinal
{
}

Neste exemplo, a tentativa de herdar da classe final ClasseFinal causará um erro.

Propriedades e Métodos Estáticos

Quando falamos em PHP sobre elementos “estáticos” de uma classe, referimo-nos a métodos e propriedades que pertencem à própria classe, e não a uma instância específica dessa classe. Isso significa que não precisa criar uma instância da classe para aceder a eles. Em vez disso, chama-os ou acede a eles diretamente através do nome da classe.

Tenha em mente que, como os elementos estáticos pertencem à classe e não às suas instâncias, não pode usar a pseudovariável $this dentro de métodos estáticos.

O uso de propriedades estáticas leva a código confuso cheio de armadilhas, portanto, nunca deve usá-las e nem mostraremos um exemplo de uso aqui. Por outro lado, os métodos estáticos são úteis. Exemplo de uso:

class Calculadora
{
	public static function adicao($a, $b)
	{
		return $a + $b;
	}

	public static function subtracao($a, $b)
	{
		return $a - $b;
	}
}

// Uso de método estático sem criar uma instância da classe
echo Calculadora::adicao(5, 3); // Resultado: 8
echo Calculadora::subtracao(5, 3); // Resultado: 2

Neste exemplo, criamos uma classe Calculadora com dois métodos estáticos. Podemos chamar esses métodos diretamente sem criar uma instância da classe usando o operador ::. Métodos estáticos são especialmente úteis para operações que não dependem do estado de uma instância específica da classe.

Constantes de Classe

Dentro das classes, temos a opção de definir constantes. Constantes são valores que nunca mudam durante a execução do programa. Ao contrário das variáveis, o valor de uma constante permanece sempre o mesmo.

class Carro
{
	public const NumeroDeRodas = 4;

	public function exibirNumeroDeRodas(): int
	{
		echo self::NumeroDeRodas;
	}
}

echo Carro::NumeroDeRodas;  // Saída: 4

Neste exemplo, temos a classe Carro com a constante NumeroDeRodas. Quando queremos aceder à constante dentro da classe, podemos usar a palavra-chave self em vez do nome da classe.

Interfaces de Objeto

Interfaces de objeto funcionam como “contratos” para classes. Se uma classe deve implementar uma interface de objeto, ela deve conter todos os métodos que essa interface define. É uma ótima maneira de garantir que certas classes sigam o mesmo “contrato” ou estrutura.

Em PHP, uma interface é definida com a palavra-chave interface. Todos os métodos definidos na interface são públicos (public). Quando uma classe implementa uma interface, ela usa a palavra-chave implements.

interface Animal
{
	function emitirSom();
}

class Gato implements Animal
{
	public function emitirSom()
	{
		echo 'Miau';
	}
}

$gato = new Gato;
$gato->emitirSom();

Se uma classe implementa uma interface, mas nem todos os métodos esperados são definidos nela, o PHP lançará um erro.

Uma classe pode implementar várias interfaces ao mesmo tempo, o que é uma diferença em relação à herança, onde uma classe pode herdar apenas de uma classe:

interface Guarda
{
	function guardarCasa();
}

class Cachorro implements Animal, Guarda
{
	public function emitirSom()
	{
		echo 'Au au';
	}

	public function guardarCasa()
	{
		echo 'O cachorro guarda a casa atentamente';
	}
}

Classes Abstratas

Classes abstratas servem como modelos básicos para outras classes, mas não pode criar instâncias delas diretamente. Elas contêm uma combinação de métodos completos e métodos abstratos, que não têm conteúdo definido. Classes que herdam de classes abstratas devem fornecer definições para todos os métodos abstratos do ancestral.

Para definir uma classe abstrata, usamos a palavra-chave abstract.

abstract class ClasseAbstrata
{
	public function metodoComum()
	{
		echo 'Este é um método comum';
	}

	abstract public function metodoAbstrato();
}

class Descendente extends ClasseAbstrata
{
	public function metodoAbstrato()
	{
		echo 'Esta é a implementação do método abstrato';
	}
}

$instancia = new Descendente;
$instancia->metodoComum();
$instancia->metodoAbstrato();

Neste exemplo, temos uma classe abstrata com um método comum e um método abstrato. Em seguida, temos a classe Descendente, que herda de ClasseAbstrata e fornece a implementação para o método abstrato.

Qual a diferença entre interfaces e classes abstratas? Classes abstratas podem conter métodos abstratos e concretos, enquanto interfaces apenas definem quais métodos uma classe deve implementar, mas não fornecem nenhuma implementação. Uma classe pode herdar apenas de uma classe abstrata, mas pode implementar qualquer número de interfaces.

Verificação de Tipo

Na programação, é muito importante ter certeza de que os dados com os quais trabalhamos são do tipo correto. Em PHP, temos ferramentas que nos garantem isso. A verificação se os dados têm o tipo correto é chamada de “verificação de tipo” ou “type hinting”.

Tipos que podemos encontrar em PHP:

  1. Tipos básicos: Incluem int (números inteiros), float (números decimais), bool (valores booleanos), string (cadeias de caracteres), array (arrays) e null.
  2. Classes: Se quisermos que o valor seja uma instância de uma classe específica.
  3. Interfaces: Define um conjunto de métodos que uma classe deve implementar. Um valor que satisfaz a interface deve ter esses métodos.
  4. Tipos mistos (Union Types): Podemos especificar que uma variável pode ter vários tipos permitidos.
  5. Void: Este tipo especial indica que a função ou método não retorna nenhum valor.

Vamos ver como modificar o código para incluir tipos:

class Pessoa
{
	private int $idade;

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

	public function exibirIdade(): void
	{
		echo "Esta pessoa tem {$this->idade} anos.";
	}
}

/**
 * Função que recebe um objeto da classe Pessoa e exibe a idade da pessoa.
 */
function exibirIdadePessoa(Pessoa $pessoa): void
{
	$pessoa->exibirIdade();
}

Desta forma, garantimos que nosso código espera e trabalha com dados do tipo correto, o que nos ajuda a prevenir erros potenciais.

Alguns tipos não podem ser escritos diretamente em PHP. Nesse caso, eles são indicados num comentário phpDoc, que é um formato padrão para documentar código PHP começando com /** e terminando com */. Permite adicionar descrições de classes, métodos, etc. E também indicar tipos complexos usando as chamadas anotações @var, @param e @return. Esses tipos são então usados por ferramentas de análise estática de código, mas o próprio PHP não os verifica.

class Lista
{
	/** @var array<Pessoa>  a notação diz que é um array de objetos Pessoa */
	private array $pessoas = [];

	public function adicionarPessoa(Pessoa $pessoa): void
	{
		$this->pessoas[] = $pessoa;
	}
}

Comparação e Identidade

Em PHP, pode comparar objetos de duas maneiras:

  1. Comparação de valores ==: Verifica se os objetos são da mesma classe e têm os mesmos valores em suas propriedades.
  2. Identidade ===: Verifica se se trata da mesma instância do objeto.
class Carro
{
	public string $marca;

	public function __construct(string $marca)
	{
		$this->marca = $marca;
	}
}

$carro1 = new Carro('Skoda');
$carro2 = new Carro('Skoda');
$carro3 = $carro1;

var_dump($carro1 == $carro2);   // true, porque têm o mesmo valor
var_dump($carro1 === $carro2);  // false, porque não são a mesma instância
var_dump($carro1 === $carro3);  // true, porque $carro3 é a mesma instância que $carro1

Operador instanceof

O operador instanceof permite verificar se um determinado objeto é uma instância de uma classe específica, um descendente dessa classe, ou se implementa uma determinada interface.

Imaginemos que temos uma classe Pessoa e outra classe Estudante, que é descendente da classe Pessoa:

class Pessoa
{
	private int $idade;

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

class Estudante extends Pessoa
{
	private string $curso;

	public function __construct(int $idade, string $curso)
	{
		parent::__construct($idade);
		$this->curso = $curso;
	}
}

$estudante = new Estudante(20, 'Informática');

// Verificar se $estudante é uma instância da classe Estudante
var_dump($estudante instanceof Estudante);  // Saída: bool(true)

// Verificar se $estudante é uma instância da classe Pessoa (porque Estudante é descendente de Pessoa)
var_dump($estudante instanceof Pessoa);     // Saída: bool(true)

Das saídas, é evidente que o objeto $estudante é considerado simultaneamente uma instância de ambas as classes – Estudante e Pessoa.

Interfaces Fluentes

“Interface fluente” (em inglês “Fluent Interface”) é uma técnica em POO que permite encadear métodos juntos numa única chamada. Isso muitas vezes simplifica e torna o código mais claro.

O elemento chave de uma interface fluente é que cada método na cadeia retorna uma referência ao objeto atual. Conseguimos isso usando return $this; no final do método. Este estilo de programação é frequentemente associado a métodos chamados “setters”, que definem os valores das propriedades do objeto.

Vamos mostrar como uma interface fluente pode parecer num exemplo de envio de e-mails:

public function sendMessage()
{
	$email = new Email;
	$email->setRemetente('sender@example.com')
		  ->setDestinatario('admin@example.com')
		  ->setMensagem('Olá, esta é uma mensagem.')
		  ->enviar();
}

Neste exemplo, os métodos setRemetente(), setDestinatario() e setMensagem() servem para definir os valores correspondentes (remetente, destinatário, conteúdo da mensagem). Após definir cada um desses valores, os métodos retornam o objeto atual ($email), o que nos permite encadear outro método a seguir. Finalmente, chamamos o método enviar(), que realmente envia o e-mail.

Graças às interfaces fluentes, podemos escrever código que é intuitivo e fácil de ler.

Cópia usando clone

Em PHP, podemos criar uma cópia de um objeto usando o operador clone. Desta forma, obtemos uma nova instância com conteúdo idêntico.

Se precisarmos modificar algumas propriedades ao copiar um objeto, podemos definir um método especial __clone() na classe. Este método é chamado automaticamente quando o objeto é clonado.

class Ovelha
{
	public string $nome;

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

	public function __clone()
	{
		$this->nome = 'Clone ' . $this->nome;
	}
}

$original = new Ovelha('Dolly');
echo $original->nome . "\n";  // Imprime: Dolly

$clone = clone $original;
echo $clone->nome . "\n";      // Imprime: Clone Dolly

Neste exemplo, temos a classe Ovelha com uma propriedade $nome. Quando clonamos uma instância desta classe, o método __clone() garante que o nome da ovelha clonada receba o prefixo “Clone”.

Traits

Traits em PHP são uma ferramenta que permite compartilhar métodos, propriedades e constantes entre classes e evitar a duplicação de código. Pode imaginá-los como um mecanismo de “copiar e colar” (Ctrl-C e Ctrl-V), onde o conteúdo do trait é “colado” nas classes. Isso permite reutilizar código sem a necessidade de criar hierarquias de classes complicadas.

Vamos mostrar um exemplo simples de como usar traits em PHP:

trait BuzinarTrait
{
	public function buzinar()
	{
		echo 'Bip bip!';
	}
}

class Carro
{
	use BuzinarTrait;
}

class Caminhao
{
	use BuzinarTrait;
}

$carro = new Carro;
$carro->buzinar(); // Imprime 'Bip bip!'

$caminhao = new Caminhao;
$caminhao->buzinar(); // Também imprime 'Bip bip!'

Neste exemplo, temos um trait chamado BuzinarTrait, que contém um método buzinar(). Em seguida, temos duas classes: Carro e Caminhao, ambas usando o trait BuzinarTrait. Graças a isso, ambas as classes “têm” o método buzinar(), e podemos chamá-lo em objetos de ambas as classes.

Traits permitem compartilhar código entre classes de forma fácil e eficiente. Ao mesmo tempo, eles não entram na hierarquia de herança, ou seja, $carro instanceof BuzinarTrait retornará false.

Exceções

Exceções em POO permitem-nos lidar elegantemente com erros e situações inesperadas no nosso código. São objetos que carregam informações sobre o erro ou situação incomum.

Em PHP, temos a classe integrada Exception, que serve como base para todas as exceções. Ela tem vários métodos que nos permitem obter mais informações sobre a exceção, como a mensagem de erro, o arquivo e a linha onde o erro ocorreu, etc.

Quando ocorre um erro no código, podemos “lançar” uma exceção usando a palavra-chave throw.

function divisao(float $a, float $b): float
{
	if ($b === 0.0) { // Comparação estrita para float
		throw new Exception('Divisão por zero!');
	}
	return $a / $b;
}

Quando a função divisao() recebe zero como segundo argumento, ela lança uma exceção com a mensagem de erro 'Divisão por zero!'. Para evitar que o programa falhe ao lançar uma exceção, capturamo-la num bloco try/catch:

try {
	echo divisao(10, 0);
} catch (Exception $e) {
	echo 'Exceção capturada: '. $e->getMessage();
}

O código que pode lançar uma exceção é envolvido num bloco try. Se uma exceção for lançada, a execução do código move-se para o bloco catch, onde podemos processar a exceção (por exemplo, exibir uma mensagem de erro).

Após os blocos try e catch, podemos adicionar um bloco opcional finally, que será executado sempre, quer uma exceção tenha sido lançada ou não (mesmo no caso de usarmos a instrução return, break ou continue no bloco try ou catch):

try {
	echo divisao(10, 0);
} catch (Exception $e) {
	echo 'Exceção capturada: '. $e->getMessage();
} finally {
	// Código que será executado sempre, quer a exceção tenha sido lançada ou não
}

Também podemos criar nossas próprias classes (hierarquia) de exceções que herdam da classe Exception. Como exemplo, imaginemos uma aplicação bancária simples que permite fazer depósitos e saques:

class ExcecaoBancaria extends Exception {}
class ExcecaoSaldoInsuficiente extends ExcecaoBancaria {}
class ExcecaoLimiteExcedido extends ExcecaoBancaria {}

class ContaBancaria
{
	private int $saldo = 0;
	private int $limiteDiario = 1000;

	public function depositar(int $quantia): int
	{
		$this->saldo += $quantia;
		return $this->saldo;
	}

	public function sacar(int $quantia): int
	{
		if ($quantia > $this->saldo) {
			throw new ExcecaoSaldoInsuficiente('Saldo insuficiente na conta.');
		}

		if ($quantia > $this->limiteDiario) {
			throw new ExcecaoLimiteExcedido('O limite diário para saques foi excedido.');
		}

		$this->saldo -= $quantia;
		return $this->saldo;
	}
}

Para um bloco try, podem ser fornecidos vários blocos catch, se esperar diferentes tipos de exceções.

$conta = new ContaBancaria;
$conta->depositar(500);

try {
	$conta->sacar(1500);
} catch (ExcecaoLimiteExcedido $e) {
	echo $e->getMessage();
} catch (ExcecaoSaldoInsuficiente $e) {
	echo $e->getMessage();
} catch (ExcecaoBancaria $e) {
	echo 'Ocorreu um erro ao realizar a operação.';
}

Neste exemplo, é importante notar a ordem dos blocos catch. Como todas as exceções herdam de ExcecaoBancaria, se tivéssemos este bloco primeiro, todas as exceções seriam capturadas nele, sem que o código chegasse aos blocos catch seguintes. Portanto, é importante ter exceções mais específicas (ou seja, aquelas que herdam de outras) no bloco catch mais acima na ordem do que suas exceções pai.

Iteração

Em PHP, pode percorrer objetos usando o loop foreach, semelhante a como percorre arrays. Para que isso funcione, o objeto deve implementar uma interface especial.

A primeira opção é implementar a interface Iterator, que possui os métodos current() que retorna o valor atual, key() que retorna a chave, next() que move para o próximo valor, rewind() que move para o início e valid() que verifica se ainda não chegamos ao fim.

A segunda opção é implementar a interface IteratorAggregate, que possui apenas um método getIterator(). Este retorna um objeto substituto que garantirá a iteração, ou pode representar um gerador, que é uma função especial onde yield é usado para retornar chaves e valores sequencialmente:

class Pessoa
{
	public function __construct(
		public int $idade,
	) {
	}
}

class Lista implements IteratorAggregate
{
	private array $pessoas = [];

	public function adicionarPessoa(Pessoa $pessoa): void
	{
		$this->pessoas[] = $pessoa;
	}

	public function getIterator(): Generator
	{
		foreach ($this->pessoas as $pessoa) {
			yield $pessoa;
		}
	}
}

$lista = new Lista;
$lista->adicionarPessoa(new Pessoa(30));
$lista->adicionarPessoa(new Pessoa(25));

foreach ($lista as $pessoa) {
	echo "Idade: {$pessoa->idade} anos \n";
}

Boas Práticas

Depois de dominar os princípios básicos da programação orientada a objetos, é importante focar nas boas práticas em POO. Elas ajudarão a escrever código que não é apenas funcional, mas também legível, compreensível e fácil de manter.

  1. Separação de Responsabilidades (Separation of Concerns): Cada classe deve ter uma responsabilidade claramente definida e deve resolver apenas uma tarefa principal. Se uma classe faz muitas coisas, pode ser apropriado dividi-la em classes menores e especializadas.
  2. Encapsulamento (Encapsulation): Dados e métodos devem ser o mais ocultos possível e acessíveis apenas através de uma interface definida. Isso permite que você altere a implementação interna da classe sem afetar o resto do código.
  3. Injeção de Dependência (Dependency Injection): Em vez de criar dependências diretamente na classe, deve “injetá-las” de fora. Para uma compreensão mais profunda deste princípio, recomendamos os capítulos sobre Injeção de Dependência.
versão: 4.0