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

O termo “OOP” significa Object-Oriented Programming (Programação orientada a objetos), que é uma forma de organizar e estruturar o código. A OOP nos permite ver um programa como uma coleção de objetos que se comunicam entre si, em vez de uma sequência de comandos e funções.

Na OOP, um “objeto” é uma unidade que contém dados e funções que operam com esses dados. Os objetos são criados com base em “classes”, que podem ser entendidas como planos ou modelos para objetos. Quando temos uma classe, podemos criar sua “instância”, que é um objeto específico criado a partir dessa classe.

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

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

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

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

Normalmente, as classes são armazenadas 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 Car, o nome do arquivo seria Car.php.

Ao nomear as classes, é bom seguir a convenção “PascalCase”, o que significa que cada palavra do nome começa com uma letra maiúscula e não há sublinhados ou outros separadores. Os métodos e as propriedades seguem a convenção “camelCase”, ou seja, começam com uma letra minúscula.

Alguns métodos no PHP têm funções especiais e são prefixados com __ (dois sublinhados). Um dos métodos especiais mais importantes é o “construtor”, rotulado como __construct. O construtor é um método que é chamado automaticamente ao criar uma nova instância de uma classe.

Geralmente 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 outros atributos.

Vamos ver como usar um construtor no PHP:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Outputs: 25

Neste exemplo, a classe Person tem uma propriedade $age e um construtor que define essa propriedade. O método howOldAreYou() fornece acesso à idade da pessoa.

A palavra-chave new é usada para criar uma nova instância de uma 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 se eles não forem especificados ao criar um objeto. Por exemplo:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20

Neste exemplo, se você não especificar uma idade ao criar um objeto Person, o valor padrão de 20 será usado.

Por fim, a definição da propriedade com sua inicialização por meio do construtor pode ser encurtada e simplificada da seguinte forma:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

Namespaces

Os namespaces nos permitem organizar e agrupar classes, funções e constantes relacionadas, evitando conflitos de nomes. Você pode pensar neles como pastas em um computador, onde cada pasta contém arquivos relacionados a um projeto ou tópico específico.

Os namespaces são especialmente úteis em projetos maiores ou ao usar bibliotecas de terceiros, onde podem surgir conflitos de nomes de classes.

Imagine que você tenha uma classe chamada Car em seu projeto e queira colocá-la em um namespace chamado Transport. Você faria isso da seguinte forma:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

Se quiser usar a classe Car em outro arquivo, precisará especificar de qual namespace a classe se origina:

$car = new Transport\Car;

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

use Transport\Car;

$car = new Car;

Herança

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

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

No PHP, a herança é implementada usando a palavra-chave extends.

Nossa classe Person armazena informações de idade. Podemos ter outra classe, Student, que estende a Person e acrescenta informações sobre o campo de estudo.

Vamos dar uma olhada em um exemplo:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		echo 'Age of student: ', $this->howOldAreYou();
		echo 'Field of study: ', $this->fieldOfStudy;
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

Como esse código funciona?

  • Usamos a palavra-chave extends para estender a classe Person, o que significa que a classe Student herda todos os métodos e propriedades de Person.
  • A palavra-chave parent:: nos permite chamar métodos da classe pai. Nesse caso, chamamos o construtor da classe Person antes de adicionar nossa própria funcionalidade à classe Student.

A herança é destinada a situações em que há um relacionamento “é um” entre as classes. Por exemplo, um Student é um Person. Um gato é um animal. Ela nos permite, nos casos em que esperamos um objeto (por exemplo, “Person”) no código, usar um objeto derivado (por exemplo, “Student”).

É essencial perceber que o objetivo principal da herança não é evitar a duplicação de código. Pelo contrário, o uso incorreto da herança pode levar a um código complexo e de difícil manutenção. Se não houver uma relação “é um” entre as classes, devemos considerar a composição em vez da herança.

Composição

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

Por exemplo, temos uma classe Engine e uma classe Car. Em vez de dizer “Um carro é um motor”, dizemos “Um carro tem um motor”, que é uma relação de composição típica.

class Engine
{
	function start()
	{
		echo "Engine is running.";
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo "The car is ready to drive!";
	}
}

$car = new Car;
$car->start();

Aqui, o Car não tem todas as propriedades e métodos do Engine, mas tem acesso a eles por meio da propriedade $engine.

A vantagem da composição é a maior flexibilidade de design e a melhor adaptabilidade a mudanças futuras.

Visibilidade

No PHP, você pode definir “visibilidade” para propriedades de classe, métodos e constantes. A visibilidade determina onde você pode acessar esses elementos.

  1. Public: Se um elemento estiver marcado como public, isso significa que você pode acessá-lo de qualquer lugar, mesmo fora da classe.
  2. Protegido: Um elemento marcado como protected é acessível somente dentro da classe e de todos os seus descendentes (classes que herdam dela).
  3. Private: Se um elemento for private, você poderá acessá-lo somente na classe em que ele foi definido.

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

Vamos dar uma olhada em um exemplo de código:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		echo $this->privateProperty;    // Works
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Continuando com a herança de classes:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

Nesse caso, o método printProperties() no ChildClass pode acessar as propriedades públicas e protegidas, mas não pode acessar as propriedades privadas da classe principal.

Os dados e os métodos devem ser tão ocultos quanto possível e acessíveis somente por meio de uma interface definida. Isso permite que você altere a implementação interna da classe sem afetar o restante do código.

Palavra-chave final

No PHP, podemos usar a palavra-chave final se quisermos impedir que uma classe, um método ou uma constante seja herdado ou substituído. Quando uma classe é marcada como final, ela não pode ser estendida. Quando um método é marcado como final, ele não pode ser substituído em uma subclasse.

O fato de saber que uma determinada classe ou método não será mais modificado nos permite fazer alterações com mais facilidade sem nos preocuparmos com possíveis conflitos. Por exemplo, podemos adicionar um novo método sem medo de que um descendente já tenha um método com o mesmo nome, levando a uma colisão. Ou podemos alterar os parâmetros de um método, novamente sem o risco de causar inconsistência com um método substituído em um descendente.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

Neste exemplo, a tentativa de herdar da classe final FinalClass resultará em um erro.

Propriedades e métodos estáticos

Quando falamos de elementos “estáticos” de uma classe no PHP, queremos dizer métodos e propriedades que pertencem à própria classe, não a uma instância específica da classe. Isso significa que não é necessário criar uma instância da classe para acessá-los. Em vez disso, você os chama ou acessa diretamente por meio do nome da classe.

Lembre-se de que, como os elementos estáticos pertencem à classe e não às suas instâncias, você não pode usar a pseudovariável $this dentro dos métodos estáticos.

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

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

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

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

Neste exemplo, criamos uma classe Calculator com dois métodos estáticos. Podemos chamar esses métodos diretamente sem criar uma instância da classe usando o operador ::. Os 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

Nas 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 o mesmo.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): int
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Output: 4

Neste exemplo, temos uma classe Car com a constante NumberOfWheels. Ao acessar a constante dentro da classe, podemos usar a palavra-chave self em vez do nome da classe.

Interfaces de objeto

As interfaces de objetos funcionam como “contratos” para as classes. Se uma classe tiver que implementar uma interface de objeto, ela deverá conter todos os métodos que a interface define. Essa é uma ótima maneira de garantir que determinadas classes sigam o mesmo “contrato” ou estrutura.

No PHP, as interfaces são definidas usando a palavra-chave interface. Todos os métodos definidos em uma interface são públicos (public). Quando uma classe implementa uma interface, ela usa a palavra-chave implements.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

Se uma classe implementar uma interface, mas nem todos os métodos esperados estiverem definidos, o PHP lançará um erro. Uma classe pode implementar várias interfaces de uma vez, o que é diferente da herança, em que uma classe só pode herdar de uma classe.

Classes abstratas

As classes abstratas servem como modelos de base para outras classes, mas você não pode criar suas instâncias diretamente. Elas contêm uma mistura de métodos completos e métodos abstratos que não têm um conteúdo definido. As classes que herdam de classes abstratas devem fornecer definições para todos os métodos abstratos da classe pai.

Usamos a palavra-chave abstract para definir uma classe abstrata.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

Neste exemplo, temos uma classe abstrata com um método regular e um método abstrato. Em seguida, temos uma classe Child que herda de AbstractClass e fornece uma implementação para o método abstrato.

Verificação de tipo

Na programação, é fundamental garantir que os dados com os quais trabalhamos sejam do tipo correto. No PHP, temos ferramentas que oferecem essa garantia. A verificação de que os dados são do tipo correto é chamada de “verificação de tipo”.

Tipos que podemos encontrar no PHP:

  1. Tipos básicos: Incluem int (inteiros), float (números de ponto flutuante), bool (valores booleanos), string (strings), array (arrays) e null.
  2. Classes: Quando queremos que um 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 atende a uma interface deve ter esses métodos.
  4. Tipos mistos: Podemos especificar que uma variável pode ter vários tipos permitidos.
  5. Void: Esse tipo especial indica que uma função ou método não retorna nenhum valor.

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

class Person
{
	private int $age;

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

	public function printAge(): void
	{
		echo "This person is " . $this->age . " years old.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

Dessa forma, garantimos que nosso código espera e trabalha com dados do tipo correto, o que nos ajuda a evitar possíveis erros.

Comparação e identidade

No PHP, você 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 é a mesma instância do objeto.
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

O operador do instanceof

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

Imagine que temos uma classe Person e outra classe Student, que é descendente de Person:

class Person
{
	private int $age;

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

class Student extends Person
{
	private string $major;

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

$student = new Student(20, 'Computer Science');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

A partir dos resultados, fica evidente que o objeto $student é considerado uma instância das classes Student e Person.

Interfaces fluentes

Uma “Interface Fluente” é uma técnica em OOP que permite encadear métodos em uma única chamada. Isso geralmente simplifica e esclarece o código.

O principal elemento de uma interface fluente é que cada método na cadeia retorna uma referência ao objeto atual. Isso é obtido com o uso do endereço return $this; no final do método. Esse estilo de programação é frequentemente associado a métodos chamados “setters”, que definem os valores das propriedades de um objeto.

Vamos ver como seria uma interface fluente para o envio de e-mails:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

Neste exemplo, os métodos setFrom(), setRecipient() e setMessage() são usados para definir os valores correspondentes (remetente, destinatário, conteúdo da mensagem). Depois de definir cada um desses valores, os métodos retornam o objeto atual ($email), o que nos permite encadear outro método depois dele. Por fim, chamamos o método send(), que de fato envia o e-mail.

Graças às interfaces fluentes, podemos escrever códigos intuitivos e de fácil leitura.

Copiando com clone

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

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

class Sheep
{
	public string $name;

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

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

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Outputs: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Outputs: Clone of Dolly

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

Características

As características no PHP são uma ferramenta que permite o compartilhamento de métodos, propriedades e constantes entre classes e evita a duplicação de código. Você pode pensar nelas como um mecanismo de “copiar e colar” (Ctrl-C e Ctrl-V), em que o conteúdo de uma característica é “colado” nas classes. Isso permite que você reutilize o código sem precisar criar hierarquias de classe complicadas.

Vamos dar uma olhada em um exemplo simples de como usar características no PHP:

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Outputs 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

Neste exemplo, temos uma característica chamada Honking que contém um método honk(). Em seguida, temos duas classes: Car e Truck, ambas as quais usam a característica Honking. Como resultado, ambas as classes “têm” o método honk() e podemos chamá-lo em objetos de ambas as classes.

As características permitem o compartilhamento fácil e eficiente de código entre classes. Elas não entram na hierarquia de herança, ou seja, $car instanceof Honking retornará false.

Exceções

As exceções em OOP nos permitem tratar e gerenciar erros que possam surgir durante a execução do nosso código. Basicamente, elas são objetos projetados para registrar erros ou situações inesperadas no programa.

No PHP, temos a classe Exception integrada para esses objetos. 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 em que o erro ocorreu etc.

Quando surge um problema, podemos “lançar” uma exceção (usando throw). Se quisermos “capturar” e processar essa exceção, usaremos os blocos try e catch.

Vamos ver como isso funciona:

try {
	throw new Exception('Message explaining the reason for the exception');

	// This code won't execute
	echo 'I am a message that nobody will read';

} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

É importante observar que uma exceção pode ser lançada mais profundamente, durante a chamada para outros métodos.

Para um bloco try, vários blocos catch podem ser especificados se você espera diferentes tipos de exceções.

Também podemos criar uma hierarquia de exceções, em que cada classe de exceção herda da anterior. Como exemplo, considere um aplicativo bancário simples que permite depósitos e saques:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

Nesse exemplo, é importante observar a ordem dos blocos catch. Como todas as exceções são herdadas de BankingException, se tivéssemos esse bloco primeiro, todas as exceções seriam capturadas nele sem que o código chegasse aos blocos catch subsequentes. Portanto, é importante que as exceções mais específicas (ou seja, aquelas que herdam de outras) estejam em uma posição mais alta na ordem do bloco catch do que suas exceções pai.

Melhores práticas

Depois de aprender os princípios básicos da programação orientada a objetos, é fundamental concentrar-se nas práticas recomendadas de OOP. Elas o ajudarão a escrever códigos que não sejam apenas funcionais, mas também legíveis, compreensíveis e de fácil manutenção.

  1. Separação de preocupações: Cada classe deve ter uma responsabilidade claramente definida e deve tratar apenas de uma tarefa principal. Se uma classe fizer muitas coisas, pode ser apropriado dividi-la em classes menores e especializadas.
  2. Encapsulamento: Os dados e métodos devem ser tão ocultos quanto possível e acessíveis somente por meio de uma interface definida. Isso permite que você altere a implementação interna de uma classe sem afetar o restante do código.
  3. Injeção de dependência: Em vez de criar dependências diretamente em uma classe, você deve “injetá-las” do lado de fora. Para entender melhor esse princípio, recomendamos os capítulos sobre Injeção de Dependência.
versão: 4.0