Introduzione alla programmazione orientata agli oggetti

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

Nell'OOP, un “oggetto” è un'unità che contiene dati e funzioni che operano su tali dati. Gli oggetti vengono creati sulla base di “classi”, che possono essere intese come progetti o modelli di oggetti. Una volta che abbiamo una classe, possiamo creare la sua “istanza”, che è un oggetto specifico creato a partire da quella classe.

Vediamo come creare una semplice classe in PHP. Quando si definisce una classe, si usa 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 'Beep beep!';
	}
}

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 sono in genere memorizzate in file separati per mantenere il codice organizzato e facile da navigare. Il nome del file deve corrispondere al nome della classe, quindi per la classe Car il nome del file sarà Car.php.

Quando si nominano le classi, è bene seguire la convenzione “PascalCase”, cioè ogni parola del nome inizia con una lettera maiuscola e non ci sono sottolineature o altri separatori. I metodi e le proprietà seguono la convenzione “camelCase”, cioè iniziano con una lettera minuscola.

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

Spesso si usa il costruttore per impostare lo stato iniziale di un oggetto. Per esempio, quando si crea un oggetto che rappresenta una persona, si può usare il costruttore per impostarne l'età, il nome o altri attributi.

Vediamo come utilizzare 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(); // Outputs: 25

In questo esempio, la classe Person ha una proprietà (variabile) $age e un costruttore che imposta questa proprietà. Il metodo howOldAreYou() consente di accedere all'età della persona.

La pseudo-variabile $this viene utilizzata all'interno della classe per accedere alle proprietà e ai metodi dell'oggetto.

La parola chiave new viene utilizzata per creare una nuova istanza di una classe. Nell'esempio precedente, abbiamo creato una nuova persona di 25 anni.

È anche possibile impostare valori predefiniti per i parametri del costruttore, se non sono specificati durante la creazione di un oggetto. Per esempio:

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

In questo esempio, se non si specifica l'età quando si crea un oggetto Person, verrà usato il valore predefinito di 20.

La cosa bella è che la definizione della proprietà con la sua inizializzazione tramite il costruttore può essere accorciata e semplificata in questo modo:

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

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

Spazi dei nomi

Gli spazi dei nomi consentono di organizzare e raggruppare classi, funzioni e costanti correlate, evitando conflitti di denominazione. Si possono considerare come le cartelle di un computer, dove ogni cartella contiene file relativi a un progetto o a un argomento specifico.

Gli spazi dei nomi sono particolarmente utili in progetti di grandi dimensioni o quando si utilizzano librerie di terze parti in cui potrebbero sorgere conflitti di denominazione delle classi.

Immaginate di avere una classe chiamata Car nel vostro progetto e di volerla inserire in uno spazio dei nomi chiamato Transport. Si procederà in questo modo:

namespace Transport;

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

Se si vuole utilizzare la classe Car in un altro file, occorre specificare da quale spazio dei nomi proviene la classe:

$car = new Transport\Car;

Per semplificare, si può specificare all'inizio del file quale classe di un particolare spazio dei nomi si vuole utilizzare, consentendo di creare istanze senza citare il percorso completo:

use Transport\Car;

$car = new Car;

Eredità

L'ereditarietà è uno strumento della programmazione orientata agli oggetti che consente di creare nuove classi basate su quelle esistenti, ereditandone proprietà e metodi ed estendendole o ridefinendole a seconda delle necessità. L'ereditarietà garantisce la riusabilità del codice e la gerarchia delle classi.

In parole povere, se abbiamo una classe e vogliamo crearne un'altra derivata da essa ma con alcune modifiche, possiamo “ereditare” la nuova classe da quella originale.

In PHP, l'ereditarietà viene implementata utilizzando la parola chiave extends.

La nostra classe Person memorizza informazioni sull'età. Possiamo avere un'altra classe, Student, che estende Person e aggiunge informazioni sul campo di studi.

Vediamo un esempio:

class Person
{
	private $age;

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

	function printInformation()
	{
		echo "Age: {$this->age} years\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

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

	function printInformation()
	{
		parent::printInformation();
		echo "Field of study: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Computer Science');
$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à da Person.
  • La parola chiave parent:: consente di richiamare i metodi della classe padre. In questo caso, abbiamo richiamato il costruttore della classe Person prima di aggiungere la nostra funzionalità alla classe Student. E allo stesso modo, il metodo dell'antenato printInformation() prima di elencare le informazioni sugli studenti.

L'ereditarietà è pensata per le situazioni in cui esiste una relazione “is a” tra le classi. Per esempio, un Student è un Person. Un gatto è un animale. Ci consente, nei casi in cui ci aspettiamo un oggetto (ad esempio, “Persona”) nel codice, di utilizzare invece un oggetto derivato (ad esempio, “Studente”).

È essenziale rendersi conto che lo scopo principale dell'ereditarietà non è quello di prevenire la duplicazione del codice. Al contrario, un uso improprio dell'ereditarietà può portare a codice complesso e difficile da mantenere. Se non c'è una relazione “è un” tra le classi, dovremmo considerare la composizione invece dell'ereditarietà.

Si noti che i metodi printInformation() delle classi Person e Student forniscono informazioni leggermente diverse. È possibile aggiungere altre classi (come Employee) che forniranno altre implementazioni di questo metodo. La capacità di oggetti di classi diverse di rispondere allo stesso metodo in modi diversi si chiama polimorfismo:

$people = [
	new Person(30),
	new Student(20, 'Computer Science'),
	new Employee(45, 'Director'),
];

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

Composizione

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

Ad 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 '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();

In questo caso, la classe Car non possiede tutte le proprietà e i metodi della classe Engine, ma vi ha accesso attraverso la proprietà $engine.

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

Visibilità

In PHP, è possibile definire la “visibilità” per le proprietà, i metodi e le costanti delle classi. La visibilità determina dove è possibile accedere a questi elementi.

  1. Se un elemento è contrassegnato come public, significa che è possibile accedervi da qualsiasi punto, anche al di fuori della classe.
  2. Un elemento contrassegnato come protected è accessibile solo all'interno della classe e di tutti i suoi discendenti (classi che ereditano da essa).
  3. Se un elemento è private, è possibile accedervi solo dalla classe in cui è stato definito.

Se non si specifica la visibilità, PHP la imposterà automaticamente a public.

Vediamo un esempio di codice:

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

Continuare con l'ereditarietà delle classi:

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

In questo caso, il metodo printProperties() della classe ChildClass può accedere alle proprietà pubbliche e protette, ma non alle proprietà private della classe padre.

I dati e i metodi devono essere il più possibile nascosti e accessibili solo attraverso un'interfaccia definita. Ciò consente di modificare l'implementazione interna della classe senza influenzare il resto del codice.

Parola chiave finale

In PHP, si può usare la parola chiave final se si vuole impedire che una classe, un metodo o una costante vengano ereditati o sovrascritti. Quando una classe è contrassegnata come final, non può essere estesa. Quando un metodo è contrassegnato come final, non può essere sovrascritto in una sottoclasse.

Essere consapevoli che una certa classe o un certo metodo non saranno più modificati ci permette di apportare modifiche più facilmente senza preoccuparci di potenziali conflitti. Ad esempio, possiamo aggiungere un nuovo metodo senza temere che un discendente abbia già un metodo con lo stesso nome, causando una collisione. Oppure possiamo cambiare i parametri di un metodo, sempre senza il rischio di causare incoerenze con un metodo sovrascritto in un discendente.

final class FinalClass
{
}

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

In questo esempio, il tentativo di ereditare dalla classe finale FinalClass produrrà un errore.

Proprietà e metodi statici

Quando si parla di elementi “statici” di una classe in PHP, si intendono metodi e proprietà che appartengono alla classe stessa, non a un'istanza specifica della classe. Ciò significa che non è necessario creare un'istanza della classe per accedervi. Al contrario, è possibile chiamarli o accedervi direttamente attraverso il nome della classe.

Tenere presente che, poiché gli elementi statici appartengono alla classe e non alle sue istanze, non è possibile utilizzare la pseudo-variabile $this all'interno dei metodi statici.

L'uso di proprietà statiche porta a un codice offuscato e pieno di insidie, quindi non si dovrebbe mai usarle e non ne mostreremo un esempio qui. D'altra parte, i metodi statici 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;
	}
}

// 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

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

Costanti di classe

All'interno delle classi è possibile 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 rimane invariato.

class Car
{
	public const NumberOfWheels = 4;

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

echo Car::NumberOfWheels;  // Output: 4

In questo esempio, abbiamo una classe Car con la costante NumberOfWheels. Quando si accede alla costante all'interno della classe, si può usare la parola chiave self invece del nome della classe.

Interfacce di oggetti

Le interfacce di oggetti agiscono come “contratti” per le classi. Se una classe deve implementare un'interfaccia di oggetti, deve contenere tutti i metodi definiti dall'interfaccia. È un ottimo modo per garantire che alcune classi aderiscano allo stesso “contratto” o struttura.

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

interface Animal
{
	function makeSound();
}

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

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

Se una classe implementa un'interfaccia, ma non tutti i metodi previsti sono definiti, PHP lancia un errore.

Una classe può implementare più interfacce contemporaneamente, a differenza dell'ereditarietà, dove una classe può ereditare solo da una classe:

interface Guardian
{
	function guardHouse();
}

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

	public function guardHouse()
	{
		echo 'Dog diligently guards the house';
	}
}

Classi astratte

Le classi astratte servono come modelli di base per altre classi, ma non è possibile creare direttamente le loro istanze. Contengono un mix 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 si usa la parola chiave abstract.

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();

In questo esempio, abbiamo una classe astratta con un metodo regolare e uno astratto. Poi abbiamo una classe Child che eredita da AbstractClass e fornisce un'implementazione per il metodo astratto.

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

Verifica del tipo

Nella programmazione è fondamentale assicurarsi che i dati con cui si lavora siano del tipo corretto. In PHP, disponiamo di strumenti che forniscono questa garanzia. Verificare che i dati siano del tipo corretto si chiama “controllo del tipo”.

Tipi che possiamo incontrare in PHP:

  1. Tipi di base: Questi includono int (numeri interi), float (numeri in virgola mobile), bool (valori booleani), string (stringhe), array (array) e null.
  2. Classi: Quando si vuole che un valore sia un'istanza di una classe specifica.
  3. Interfacce: Definisce un insieme di metodi che una classe deve implementare. Un valore che soddisfa un'interfaccia deve avere questi metodi.
  4. Tipi misti: Si può specificare che una variabile può avere più tipi consentiti.
  5. Voide: Questo tipo speciale indica che una funzione o un metodo non restituisce alcun valore.

Vediamo come modificare il codice per includere i tipi:

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();
}

In questo modo, ci assicuriamo che il nostro codice si aspetti e lavori con dati del tipo corretto, aiutandoci a prevenire potenziali errori.

Alcuni tipi non possono essere scritti direttamente in PHP. In questo caso, vengono elencati nel commento phpDoc, che è il formato standard per la documentazione del codice PHP, che inizia con /** e termina con */. Esso consente di aggiungere descrizioni di classi, metodi e così via. E anche di elencare tipi complessi usando le cosiddette annotazioni @var, @param e @return. Questi tipi vengono poi utilizzati dagli strumenti di analisi statica del codice, ma non vengono controllati da PHP stesso.

class Registry
{
	/** @var array<Person>  indicates that it's an array of Person objects */
	private array $persons = [];

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

Confronto e identità

In PHP, è possibile confrontare gli oggetti in due modi:

  1. Confronto tra valori ==: verifica se gli oggetti appartengono alla stessa classe e hanno gli stessi valori nelle loro 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, 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

L'operatore di instanceof

L'operatore instanceof consente di determinare se un dato oggetto è un'istanza di una classe specifica, un discendente di quella classe o se implementa una certa interfaccia.

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

Dai risultati, è evidente che l'oggetto $student è considerato un'istanza di entrambe le classi Student e Person.

Interfacce fluenti

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

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

Vediamo come potrebbe essere un'interfaccia fluente per l'invio di e-mail:

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

In questo esempio, i metodi setFrom(), setRecipient() e setMessage() sono usati per impostare i valori corrispondenti (mittente, destinatario, contenuto del messaggio). Dopo aver impostato ciascuno di questi valori, i metodi restituiscono l'oggetto corrente ($email), consentendoci di concatenare un altro metodo dopo di esso. Infine, chiamiamo il metodo send(), che invia effettivamente l'e-mail.

Grazie alle interfacce fluide, possiamo scrivere codice intuitivo e facilmente leggibile.

Copiare con clone

In PHP, possiamo creare una copia di un oggetto usando l'operatore clone. In questo modo, si ottiene una nuova istanza con contenuti identici.

Se durante la copia di un oggetto è necessario modificarne alcune proprietà, è possibile definire un metodo speciale __clone() nella classe. Questo metodo viene richiamato 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 of ' . $this->name;
	}
}

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

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

In questo esempio, abbiamo una classe Sheep con una proprietà $name. Quando si clona un'istanza di questa classe, il metodo __clone() assicura che il nome della pecora clonata ottenga il prefisso “Clone di”.

Tratti

I tratti in PHP sono uno strumento che consente di condividere metodi, proprietà e costanti tra le classi, evitando la duplicazione del codice. Si può pensare a loro come a un meccanismo di “copia e incolla” (Ctrl-C e Ctrl-V), in cui il contenuto di un tratto viene “incollato” nelle classi. Ciò consente di riutilizzare il codice senza dover creare complicate gerarchie di classi.

Vediamo un semplice esempio di utilizzo dei tratti in 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!'

In questo esempio, abbiamo un tratto chiamato Honking che contiene un metodo honk(). Abbiamo poi due classi: Car e Truck, che utilizzano entrambe il tratto Honking. Di conseguenza, entrambe le classi “possiedono” il metodo honk() e possiamo chiamarlo su oggetti di entrambe le classi.

I tratti consentono di condividere facilmente ed efficacemente il codice tra le classi. Non entrano nella gerarchia ereditaria, cioè $car instanceof Honking restituirà false.

Eccezioni

Le eccezioni nell'OOP ci permettono di gestire con grazia gli errori e le situazioni inaspettate nel nostro codice. Sono oggetti che contengono informazioni su un errore o una situazione insolita.

In PHP, abbiamo una classe incorporata Exception, che serve come base per tutte le eccezioni. Questa classe ha diversi metodi che ci permettono di ottenere ulteriori informazioni sull'eccezione, come il messaggio di errore, il file e la riga in cui si è verificato l'errore, ecc.

Quando si verifica un errore nel codice, si può “lanciare” l'eccezione usando la parola chiave throw.

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

Quando la funzione division() riceve null come secondo argomento, lancia un'eccezione con il messaggio di errore 'Division by zero!'. Per evitare che il programma si blocchi quando viene lanciata l'eccezione, la intrappoliamo nel blocco try/catch:

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

Il codice che può lanciare un'eccezione viene avvolto in un blocco try. Se l'eccezione viene lanciata, l'esecuzione del codice passa a un blocco catch, dove è possibile gestire l'eccezione (ad esempio, scrivere un messaggio di errore).

Dopo i blocchi try e catch, si può aggiungere un blocco opzionale finally, che viene sempre eseguito, indipendentemente dal fatto che l'eccezione sia stata lanciata o meno (anche se si utilizzano return, break, o continue nel blocco try o catch ):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// Code that is always executed whether the exception has been thrown or not
}

Possiamo anche creare le nostre classi di eccezioni (gerarchia) che ereditano dalla classe Exception. A titolo di esempio, si consideri una semplice applicazione bancaria che consente 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('Not enough funds in the account.');
		}

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

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

È possibile specificare più blocchi catch per un singolo blocco try se si prevedono diversi tipi di eccezioni.

$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.';
}

In questo esempio, è importante notare l'ordine dei blocchi catch. Poiché tutte le eccezioni ereditano da BankingException, se avessimo questo blocco per primo, tutte le eccezioni verrebbero catturate in esso, senza che il codice raggiunga i successivi blocchi catch. Pertanto, è importante che le eccezioni più specifiche (cioè quelle che ereditano da altre) si trovino più in alto nell'ordine dei blocchi catch rispetto alle loro eccezioni padre.

Iterazioni

In PHP, è possibile eseguire un ciclo di oggetti utilizzando il ciclo foreach, proprio come si fa con un array. Perché funzioni, l'oggetto deve implementare un'interfaccia speciale.

La prima opzione è 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 passa all'inizio e valid() che controlla se siamo già alla fine.

L'altra opzione è implementare un'interfaccia IteratorAggregate, che ha un solo metodo getIterator(). Questo metodo può restituire un oggetto segnaposto che fornirà l'attraversamento, oppure può essere un generatore, cioè una funzione speciale che utilizza yield per restituire chiavi e valori in sequenza:

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 "Age: {$person->age} years\n";
}

Migliori pratiche

Una volta acquisiti i principi di base della programmazione orientata agli oggetti, è fondamentale concentrarsi sulle best practice dell'OOP. Queste vi aiuteranno a scrivere codice non solo funzionale, ma anche leggibile, comprensibile e facilmente manutenibile.

  1. Separazione delle preoccupazioni: Ogni classe dovrebbe avere una responsabilità chiaramente definita e dovrebbe occuparsi di un solo compito primario. Se una classe fa troppe cose, potrebbe essere opportuno dividerla in classi più piccole e specializzate.
  2. Incapsulamento: I dati e i metodi devono essere il più possibile nascosti e accessibili solo attraverso un'interfaccia definita. Ciò consente di modificare l'implementazione interna di una classe senza influenzare il resto del codice.
  3. Iniezione di dipendenze: Invece di creare dipendenze direttamente all'interno di una classe, è opportuno “iniettarle” dall'esterno. Per una comprensione più approfondita di questo principio, si consiglia di leggere i capitoli sulla Dependency Injection.
versione: 4.0