Introduzione alla programmazione orientata agli oggetti

Il termine “OOP” sta per Object-Oriented Programming, cioè programmazione orientata agli oggetti, un modo di organizzare e strutturare il codice. L'OOP ci permette di vedere un programma come un insieme di oggetti che comunicano tra loro, invece che come una sequenza di comandi e funzioni.

Nell'OOP un “oggetto” è un'unità che contiene dati e funzioni che lavorano su quei dati. Gli oggetti si creano in base alle “classi”, che si possono intendere come progetti o modelli per gli oggetti. Una volta che abbiamo una classe, possiamo crearne un'“istanza”, cioè un oggetto concreto costruito da quella classe.

Vediamo come creare una semplice classe in PHP. Quando definiamo una classe usiamo la parola chiave “class”, seguita dal nome della classe e poi dalle parentesi graffe che racchiudono le funzioni della classe (chiamate “metodi”) e le variabili della classe (chiamate “proprietà” o “attributi”):

class Car
{
	function honk()
	{
		echo 'Bip bip!';
	}
}

In questo esempio abbiamo creato una classe chiamata Car con una funzione (o “metodo”) chiamata honk.

Ogni classe dovrebbe risolvere un solo compito principale. Se una classe fa troppe cose, può essere opportuno dividerla in classi più piccole e specializzate.

Le classi si salvano di solito in file separati, per mantenere il codice ordinato e facile da percorrere. Il nome del file dovrebbe corrispondere al nome della classe, quindi per la classe Car il file si chiamerebbe Car.php.

Nel dare i nomi alle classi conviene rispettare la convenzione “PascalCase”, cioè ogni parola del nome inizia con la lettera maiuscola e non ci sono trattini bassi o altri separatori. I metodi e le proprietà seguono la convenzione “camelCase”, cioè iniziano con la lettera minuscola.

Alcuni metodi in PHP hanno ruoli speciali e sono preceduti da __ (due trattini bassi). Uno dei metodi speciali più importanti è il “costruttore”, indicato come __construct. Il costruttore è un metodo che viene chiamato automaticamente quando si crea una nuova istanza di una classe.

Il costruttore lo usiamo spesso per impostare lo stato iniziale di un oggetto. Per esempio, quando create un oggetto che rappresenta una persona, potete usare il costruttore per impostarne l'età, il nome o altri attributi.

Vediamo come usare un costruttore in PHP:

class Person
{
	private $age;

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

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

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

In questo esempio la classe Person ha una proprietà (variabile) $age e un costruttore che imposta questa proprietà. Il metodo howOldAreYou() fornisce poi l'accesso all'età della persona.

La pseudo-variabile $this si usa dentro la classe per accedere alle proprietà e ai metodi dell'oggetto.

La parola chiave new serve a creare una nuova istanza di una classe. Nell'esempio sopra abbiamo creato una nuova persona di 25 anni.

Potete anche impostare valori predefiniti per i parametri del costruttore, se non vengono indicati quando si crea l'oggetto. Per esempio:

class Person
{
	private $age;

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

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

$person = new Person;  // se non si passa alcun argomento, le parentesi si possono omettere
echo $person->howOldAreYou(); // stampa: 20

In questo esempio, se non indicate l'età quando create un oggetto Person, verrà usato il valore predefinito 20.

La cosa bella è che la definizione della proprietà con la sua inizializzazione tramite il costruttore si può abbreviare e semplificare così:

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

Per completezza: oltre ai costruttori, gli oggetti possono avere anche i distruttori (metodo __destruct), che vengono chiamati prima che l'oggetto venga liberato dalla memoria.

Namespace

I namespace ci permettono di organizzare e raggruppare classi, funzioni e costanti correlate evitando i conflitti di nomi. Potete immaginarli come cartelle su un computer, dove ogni cartella contiene i file relativi a un determinato progetto o argomento.

I namespace tornano particolarmente utili nei progetti più grandi o quando si usano librerie di terze parti, dove possono nascere conflitti tra i nomi delle classi.

Immaginate di avere nel vostro progetto una classe chiamata Car e di volerla collocare in un namespace chiamato Transport. Lo fareste così:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Bip bip!';
	}
}

Se volete usare la classe Car in un altro file, dovete indicare da quale namespace proviene:

$car = new Transport\Car;

Per semplificare potete indicare all'inizio del file quale classe di un certo namespace volete usare, così potete creare le istanze senza citare l'intero percorso:

use Transport\Car;

$car = new Car;

Ereditarietà

L'ereditarietà è uno strumento della programmazione orientata agli oggetti che permette di creare nuove classi a partire da quelle esistenti, ereditandone proprietà e metodi ed estendendoli o ridefinendoli secondo le necessità. L'ereditarietà garantisce il riuso del codice e la gerarchia delle classi.

In parole semplici: se abbiamo una classe e vogliamo crearne un'altra derivata da essa ma con qualche modifica, possiamo far “ereditare” la nuova classe da quella originale.

In PHP l'ereditarietà si realizza con la parola chiave extends.

La nostra classe Person conserva l'informazione sull'età. Possiamo avere un'altra classe, Student, che estende Person e aggiunge l'informazione sul corso di studi.

Vediamo un esempio:

class Person
{
	private $age;

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

	function printInformation()
	{
		echo "Età: {$this->age} anni\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

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

	function printInformation()
	{
		parent::printInformation();
		echo "Corso di studi: {$this->fieldOfStudy} \n";
	}
}

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

Come funziona questo codice?

  • Abbiamo usato la parola chiave extends per estendere la classe Person, il che significa che la classe Student eredita tutti i metodi e le proprietà di Person.
  • La parola chiave parent:: ci permette di chiamare i metodi della classe genitore. In questo caso abbiamo chiamato il costruttore della classe Person prima di aggiungere la funzionalità propria della classe Student. E allo stesso modo il metodo printInformation() dell'antenato prima di elencare le informazioni sullo studente.

L'ereditarietà è pensata per le situazioni in cui tra le classi c'è una relazione “è un”. Per esempio uno Student è una Person. Un gatto è un animale. Ci permette, nei punti in cui nel codice aspettiamo un oggetto (per esempio “Person”), di usare al suo posto un oggetto derivato (per esempio “Student”).

È fondamentale rendersi conto che lo scopo principale dell'ereditarietà non è evitare la duplicazione del codice. Al contrario, un uso sbagliato dell'ereditarietà può portare a codice complicato e difficile da mantenere. Se tra le classi non c'è una relazione “è un”, dovremmo pensare alla composizione invece che all'ereditarietà.

Notate che i metodi printInformation() nelle classi Person e Student stampano informazioni leggermente diverse. E possiamo aggiungere altre classi (per esempio Employee) che forniranno altre implementazioni di questo metodo. La capacità degli oggetti di classi diverse di rispondere allo stesso metodo in modi diversi si chiama polimorfismo:

$people = [
	new Person(30),
	new Student(20, 'Informatica'),
	new Employee(45, 'Direttore'),
];

foreach ($people as $person) {
	$person->printInformation();
}

Composizione

La composizione è una tecnica in cui, invece di ereditare proprietà e metodi da un'altra classe, usiamo semplicemente una sua istanza nella nostra classe. Questo ci permette di combinare funzionalità e proprietà di più classi senza creare complicate strutture di ereditarietà.

Per esempio abbiamo una classe Engine e una classe Car. Invece di dire “Un'auto è un motore”, diciamo “Un'auto ha un motore”, che è una tipica relazione di composizione.

class Engine
{
	function start()
	{
		echo 'Il motore è in funzione.';
	}
}

class Car
{
	private $engine;

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

	function start()
	{
		$this->engine->start();
		echo 'La macchina è pronta a partire!';
	}
}

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

Qui la Car non ha tutte le proprietà e i metodi di Engine, ma vi accede tramite la proprietà $engine.

Il vantaggio della composizione è una maggiore flessibilità di progettazione e una migliore capacità di adattarsi ai cambiamenti futuri.

Visibilità

In PHP potete definire la “visibilità” di proprietà, metodi e costanti di una classe. La visibilità determina da dove potete accedere a questi elementi.

  1. Public: se un elemento è contrassegnato come public, significa che potete accedervi da qualsiasi punto, anche fuori dalla classe.
  2. Protected: un elemento contrassegnato come protected è accessibile solo dentro la classe e tutti i suoi discendenti (le classi che ereditano da essa).
  3. Private: se un elemento è private, potete accedervi solo dall'interno della classe in cui è stato definito.

Se non indicate la visibilità, PHP la imposta automaticamente a public.

Vediamo un codice di esempio:

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

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

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // funziona
// echo $object->protectedProperty;   // provoca un errore
// echo $object->privateProperty;     // provoca un errore

Proseguiamo con l'ereditarietà delle classi:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // funziona
		echo $this->protectedProperty;  // funziona
		// echo $this->privateProperty;   // provoca un errore
	}
}

In questo caso il metodo printProperties() della ChildClass può accedere alle proprietà public e protected, ma non a quelle private della classe genitore.

I dati e i metodi dovrebbero essere il più nascosti possibile e accessibili solo tramite un'interfaccia definita. Questo vi permette di cambiare l'implementazione interna della classe senza influire sul resto del codice.

Parola chiave final

In PHP possiamo usare la parola chiave final se vogliamo impedire che una classe, un metodo o una costante vengano ereditati o ridefiniti. Quando una classe è contrassegnata come final, non si può estendere. Quando un metodo è contrassegnato come final, non si può ridefinire in una sottoclasse.

Sapere che una certa classe o un certo metodo non verranno più modificati ci permette di fare cambiamenti più facilmente, senza preoccuparci di possibili conflitti. Possiamo per esempio aggiungere un nuovo metodo senza temere che un discendente abbia già un metodo con lo stesso nome, il che porterebbe a una collisione. Oppure possiamo cambiare i parametri di un metodo, di nuovo senza il rischio di creare incoerenze con un metodo ridefinito in un discendente.

final class FinalClass
{
}

// il codice seguente provocherà un errore, perché non possiamo ereditare da una classe final.
class ChildOfFinalClass extends FinalClass
{
}

In questo esempio il tentativo di ereditare dalla classe final FinalClass porterà a un errore.

Proprietà e metodi statici

Quando parliamo di elementi “statici” di una classe in PHP intendiamo metodi e proprietà che appartengono alla classe stessa, non a una sua istanza concreta. Questo significa che per accedervi non dovete creare un'istanza della classe. Li chiamate o vi accedete invece direttamente tramite il nome della classe.

Tenete presente che, poiché gli elementi statici appartengono alla classe e non alle sue istanze, dentro i metodi statici non potete usare la pseudo-variabile $this.

L'uso delle proprietà statiche porta a codice offuscato e pieno di trappole, quindi non dovreste mai usarle e qui non ne mostriamo alcun esempio. I metodi statici, invece, sono utili. Ecco un esempio:

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

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

// uso del metodo statico senza creare un'istanza della classe
echo Calculator::add(5, 3); // stampa: 8
echo Calculator::subtract(5, 3); // stampa: 2

In questo esempio abbiamo creato una classe Calculator con due metodi statici. Possiamo chiamarli direttamente senza creare un'istanza della classe, usando l'operatore ::. I metodi statici sono particolarmente utili per operazioni che non dipendono dallo stato di una specifica istanza della classe.

Costanti di classe

Dentro le classi abbiamo la possibilità di definire delle costanti. Le costanti sono valori che non cambiano mai durante l'esecuzione del programma. A differenza delle variabili, il valore di una costante resta lo stesso.

class Car
{
	public const NumberOfWheels = 4;

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

echo Car::NumberOfWheels;  // stampa: 4

In questo esempio abbiamo la classe Car con la costante NumberOfWheels. Quando accediamo alla costante dentro la classe, possiamo usare la parola chiave self al posto del nome della classe.

Interfacce degli oggetti

Le interfacce degli oggetti fanno da “contratto” per le classi. Se una classe deve implementare un'interfaccia, deve contenere tutti i metodi che l'interfaccia definisce. È un ottimo modo per garantire che certe classi rispettino lo stesso “contratto” o la stessa struttura.

In PHP le interfacce si definiscono con la parola chiave interface. Tutti i metodi definiti in un'interfaccia sono pubblici (public). Quando una classe implementa un'interfaccia, usa la parola chiave implements.

interface Animal
{
	function makeSound();
}

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

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

Se una classe implementa un'interfaccia ma non definisce tutti i metodi attesi, PHP genera un errore.

Una classe può implementare più interfacce contemporaneamente, il che è diverso dall'ereditarietà, dove una classe può ereditare solo da una classe:

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Bau';
	}

	public function guardHouse()
	{
		echo 'Il cane fa la guardia alla casa con diligenza';
	}
}

Classi astratte

Le classi astratte fanno da modello di base per altre classi, ma non potete crearne direttamente le istanze. Contengono un misto di metodi completi e metodi astratti che non hanno un contenuto definito. Le classi che ereditano da classi astratte devono fornire le definizioni di tutti i metodi astratti del genitore.

Per definire una classe astratta usiamo la parola chiave abstract.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'Questo è un metodo normale';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'Implementazione del metodo astratto';
	}
}

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

In questo esempio abbiamo una classe astratta con un metodo normale e uno astratto. Poi abbiamo una classe Child che eredita da AbstractClass e fornisce l'implementazione del metodo astratto.

In che cosa differiscono interfacce e classi astratte? Le classi astratte possono contenere sia metodi astratti sia metodi concreti, mentre le interfacce definiscono solo quali metodi la classe deve implementare, senza fornirne l'implementazione. Una classe può ereditare da una sola classe astratta, ma può implementare un numero qualsiasi di interfacce.

Controllo dei tipi

Nella programmazione è fondamentale garantire che i dati con cui lavoriamo siano del tipo corretto. In PHP abbiamo strumenti che offrono questa garanzia. Verificare che i dati siano del tipo corretto si chiama “controllo dei tipi”.

Tipi che possiamo incontrare in PHP:

  1. Tipi di base: comprendono int (numeri interi), float (numeri in virgola mobile), bool (valori booleani), string (stringhe), array (array) e null.
  2. Classi: quando vogliamo che un valore sia un'istanza di una classe specifica.
  3. Interfacce: definiscono un insieme di metodi che una classe deve implementare. Un valore che soddisfa un'interfaccia deve avere questi metodi.
  4. Tipi misti: possiamo indicare che una variabile può avere più tipi consentiti.
  5. Void: questo tipo speciale indica che una funzione o un metodo non restituisce alcun valore.

Vediamo come modificare il codice per aggiungervi i tipi:

class Person
{
	private int $age;

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

	public function printAge(): void
	{
		echo "Questa persona ha {$this->age} anni.";
	}
}

/**
 * Una funzione che accetta un oggetto Person e stampa l'età della persona.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

In questo modo garantiamo che il nostro codice si aspetti e lavori con dati del tipo corretto, il che ci aiuta a prevenire possibili errori.

Alcuni tipi non si possono scrivere direttamente in PHP. In tal caso si indicano nel commento phpDoc, che è il formato standard per documentare il codice PHP: inizia con /** e finisce con */. Permette di aggiungere descrizioni di classi, metodi e così via. E anche di indicare tipi complessi con le cosiddette annotazioni @var, @param e @return. Questi tipi vengono poi usati dagli strumenti di analisi statica del codice, ma non sono controllati da PHP stesso.

class Registry
{
	/** @var array<Person>  indica che si tratta di un array di oggetti Person */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

Confronto e identità

In PHP potete confrontare gli oggetti in due modi:

  1. Confronto dei valori ==: verifica se gli oggetti sono della stessa classe e hanno gli stessi valori nelle proprietà.
  2. Identità ===: verifica se si tratta della stessa istanza dell'oggetto.
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, perché hanno lo stesso valore
var_dump($car1 === $car2);  // false, perché non sono la stessa istanza
var_dump($car1 === $car3);  // true, perché $car3 è la stessa istanza di $car1

L'operatore instanceof

L'operatore instanceof permette di stabilire se un dato oggetto è un'istanza di una certa classe, un discendente di quella classe, oppure se implementa una certa interfaccia.

Immaginiamo di avere una classe Person e un'altra classe Student, che è discendente di 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, 'Informatica');

// verifichiamo se $student è un'istanza della classe Student
var_dump($student instanceof Student);  // stampa: bool(true)

// verifichiamo se $student è un'istanza della classe Person (perché Student è discendente di Person)
var_dump($student instanceof Person);   // stampa: bool(true)

Dagli output si vede che l'oggetto $student è considerato un'istanza sia della classe Student sia della classe Person.

Interfacce fluent

Una “interfaccia fluent” è una tecnica dell'OOP che permette di concatenare i metodi in un'unica chiamata. Questo spesso semplifica e chiarisce il codice.

L'elemento chiave di un'interfaccia fluent è che ogni metodo della catena restituisce un riferimento all'oggetto corrente. Lo si ottiene usando return $this; alla fine del metodo. Questo stile di programmazione si associa spesso ai metodi chiamati “setter”, che impostano i valori delle proprietà di un oggetto.

Vediamo come può apparire un'interfaccia fluent per l'invio di email:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Ciao, questo è un messaggio.')
		  ->send();
}

In questo esempio i metodi setFrom(), setRecipient() e setMessage() servono a impostare i valori corrispondenti (mittente, destinatario, contenuto del messaggio). Dopo aver impostato ciascuno di questi valori, i metodi restituiscono l'oggetto corrente ($email), il che ci permette di concatenare un altro metodo. Alla fine chiamiamo il metodo send(), che invia davvero l'email.

Grazie alle interfacce fluent possiamo scrivere codice intuitivo e facilmente leggibile.

Copia con clone

In PHP possiamo creare una copia di un oggetto con l'operatore clone. Otteniamo così una nuova istanza con contenuto identico.

Se copiando un oggetto dobbiamo modificarne alcune proprietà, possiamo definire nella classe il metodo speciale __clone(). Questo metodo viene chiamato automaticamente quando l'oggetto viene clonato.

class Sheep
{
	public string $name;

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

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

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

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

In questo esempio abbiamo una classe Sheep con una proprietà $name. Quando cloniamo un'istanza di questa classe, il metodo __clone() fa in modo che il nome della pecora clonata riceva il prefisso “Clone di”.

Trait

I trait in PHP sono uno strumento che permette di condividere metodi, proprietà e costanti tra le classi ed evita la duplicazione del codice. Potete immaginarli come un meccanismo di “copia e incolla” (Ctrl-C e Ctrl-V), in cui il contenuto di un trait viene “incollato” nelle classi. Questo vi permette di riusare il codice senza dover creare complicate gerarchie di classi.

Diamo un'occhiata a un semplice esempio di uso dei trait in PHP:

trait Honking
{
	public function honk()
	{
		echo 'Bip bip!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // stampa 'Bip bip!'

$truck = new Truck;
$truck->honk(); // stampa anch'esso 'Bip bip!'

In questo esempio abbiamo un trait chiamato Honking che contiene un metodo honk(). Poi abbiamo due classi, Car e Truck, che usano entrambe il trait Honking. Di conseguenza entrambe le classi “hanno” il metodo honk() e possiamo chiamarlo sugli oggetti di entrambe.

I trait vi permettono di condividere il codice tra le classi in modo semplice ed efficiente. Non entrano nella gerarchia di ereditarietà, cioè $car instanceof Honking restituirà false.

Eccezioni

Le eccezioni nell'OOP ci permettono di gestire con eleganza gli errori e le situazioni impreviste nel nostro codice. Sono oggetti che portano con sé informazioni su un errore o una situazione insolita.

In PHP abbiamo una classe integrata Exception, che fa da base per tutte le eccezioni. Ha diversi metodi che ci permettono di ottenere maggiori informazioni sull'eccezione, per esempio il messaggio di errore, il file e la riga in cui si è verificato l'errore ecc.

Quando nel codice si verifica un errore, possiamo “lanciare” l'eccezione con la parola chiave throw.

function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('Divisione per zero!');
	}
	return $a / $b;
}

Quando la funzione division() riceve zero come secondo argomento, lancia un'eccezione con il messaggio di errore 'Divisione per zero!'. Perché il programma non si interrompa quando l'eccezione viene lanciata, la catturiamo nel blocco try/catch:

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Eccezione catturata: '. $e->getMessage();
}

Il codice che può lanciare un'eccezione si racchiude in un blocco try. Se l'eccezione viene lanciata, l'esecuzione del codice passa al blocco catch, dove possiamo gestire l'eccezione (per esempio stampare un messaggio di errore).

Dopo i blocchi try e catch possiamo aggiungere un blocco opzionale finally, che viene sempre eseguito, sia che l'eccezione sia stata lanciata sia che non lo sia stata (anche se nel blocco try o catch usiamo return, break oppure continue):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Eccezione catturata: '. $e->getMessage();
} finally {
	// codice che viene sempre eseguito, sia che l'eccezione sia stata lanciata sia che non lo sia stata
}

Possiamo anche creare classi di eccezioni nostre (una gerarchia) che ereditano dalla classe Exception. Come esempio prendiamo una semplice applicazione bancaria che permette depositi e prelievi:

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('Fondi insufficienti sul conto.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Superato il limite giornaliero di prelievo.');
		}

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

Per un solo blocco try si possono indicare più blocchi catch, se vi aspettate tipi diversi di eccezione.

$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 'Si è verificato un errore.';
}

In questo esempio è importante fare attenzione all'ordine dei blocchi catch. Poiché tutte le eccezioni ereditano da BankingException, se avessimo messo per primo quel blocco, tutte le eccezioni verrebbero catturate lì e il codice non arriverebbe ai blocchi catch successivi. È perciò importante che le eccezioni più specifiche (cioè quelle che ereditano dalle altre) stiano più in alto nell'ordine dei blocchi catch rispetto alle loro eccezioni genitore.

Iterazioni

In PHP potete percorrere gli oggetti con il ciclo foreach, in modo simile a come percorrete un array. Perché funzioni, l'oggetto deve implementare un'interfaccia speciale.

La prima possibilità è implementare l'interfaccia Iterator, che ha i metodi current() che restituisce il valore corrente, key() che restituisce la chiave, next() che passa al valore successivo, rewind() che torna all'inizio e valid() che verifica se non siamo già alla fine.

L'altra possibilità è implementare l'interfaccia IteratorAggregate, che ha un solo metodo getIterator(). Esso restituisce un oggetto sostitutivo che si occuperà dell'attraversamento, oppure può essere un generatore, cioè una funzione speciale che con yield restituisce via via chiavi e valori:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

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

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Età: {$person->age} anni\n";
}

Best practice

Una volta che avete in mano i principi di base della programmazione orientata agli oggetti, è fondamentale concentrarsi sulle best practice dell'OOP. Vi aiuteranno a scrivere codice non solo funzionante, ma anche leggibile, comprensibile e facilmente manutenibile.

  1. Separazione delle responsabilità: ogni classe dovrebbe avere una responsabilità chiaramente definita e occuparsi di un solo compito principale. Se una classe fa troppe cose, può essere opportuno dividerla in classi più piccole e specializzate.
  2. Incapsulamento: i dati e i metodi dovrebbero essere il più nascosti possibile e accessibili solo tramite un'interfaccia definita. Questo vi permette di cambiare l'implementazione interna di una classe senza influire sul resto del codice.
  3. Dependency Injection: invece di creare le dipendenze direttamente dentro una classe, dovreste “iniettarle” dall'esterno. Per capire più a fondo questo principio consigliamo i capitoli sulla Dependency Injection.
versione: 4.x