Introduzione alla programmazione orientata agli oggetti
Il termine “OOP” si riferisce alla programmazione orientata agli oggetti, che è 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, invece che come una sequenza di comandi e funzioni.
Nell'OOP, un “oggetto” è un'unità che contiene dati e funzioni che operano su questi dati. Gli oggetti sono creati secondo delle “classi”, che possiamo considerare come progetti o modelli per gli oggetti. Quando abbiamo una classe, possiamo creare una sua “istanza”, che è un oggetto specifico creato secondo quella classe.
Vediamo come possiamo 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 (chiamate “metodi”) e le variabili della classe (chiamate “proprietà”):
class Automobile
{
function suonaClacson()
{
echo 'Bip bip!';
}
}
In questo esempio, abbiamo creato una classe chiamata Automobile
con una funzione (o “metodo”) chiamata
suonaClacson
.
Ogni classe dovrebbe risolvere solo un compito principale. Se una classe fa troppe cose, potrebbe essere opportuno dividerla in classi più piccole e specializzate.
Le classi vengono solitamente salvate in file separati per mantenere il codice organizzato e facile da navigare. Il nome del
file dovrebbe corrispondere al nome della classe, quindi per la classe Automobile
, il nome del file sarebbe
Automobile.php
.
Nel nominare le classi, è buona norma seguire la convenzione “PascalCase”, il che significa che ogni parola nel nome inizia con una lettera maiuscola e non ci sono trattini bassi o altri separatori tra di esse. Metodi e proprietà usano la convenzione “camelCase”, cioè iniziano con una lettera minuscola.
Alcuni metodi in PHP hanno compiti speciali e sono contrassegnati dal prefisso __
(due trattini bassi). Uno dei
metodi speciali più importanti è il “costruttore”, contrassegnato come __construct
. Il costruttore è un metodo
che viene chiamato automaticamente quando si crea una nuova istanza della classe.
Il costruttore viene spesso utilizzato per impostare lo stato iniziale dell'oggetto. Ad esempio, quando si crea un oggetto che rappresenta una persona, è possibile utilizzare il costruttore per impostare la sua età, il nome o altre proprietà.
Vediamo come utilizzare un costruttore in PHP:
class Persona
{
private $eta;
function __construct($eta)
{
$this->eta = $eta;
}
function quantiAnniHai()
{
return $this->eta;
}
}
$persona = new Persona(25);
echo $persona->quantiAnniHai(); // Stampa: 25
In questo esempio, la classe Persona
ha una proprietà (variabile) $eta
e un costruttore che imposta
questa proprietà. Il metodo quantiAnniHai()
consente quindi 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 della classe. Nell'esempio precedente, abbiamo
creato una nuova persona con un'età di 25 anni.
È anche possibile impostare valori predefiniti per i parametri del costruttore, nel caso in cui non vengano specificati durante la creazione dell'oggetto. Ad esempio:
class Persona
{
private $eta;
function __construct($eta = 20)
{
$this->eta = $eta;
}
function quantiAnniHai()
{
return $this->eta;
}
}
$persona = new Persona; // se non si passa alcun argomento, le parentesi possono essere omesse
echo $persona->quantiAnniHai(); // Stampa: 20
In questo esempio, se non si specifica l'età durante la creazione dell'oggetto Persona
, verrà utilizzato il
valore predefinito 20.
È piacevole sapere che la definizione di una proprietà con la sua inizializzazione tramite il costruttore può essere abbreviata e semplificata in questo modo:
class Persona
{
function __construct(
private $eta = 20,
) {
}
}
Per completezza, oltre ai costruttori, gli oggetti possono avere anche distruttori (metodo __destruct
), che
vengono chiamati prima che l'oggetto venga rilasciato dalla memoria.
Namespace
I namespace (o “namespaces” in inglese) ci permettono di organizzare e raggruppare classi, funzioni e costanti correlate, evitando al contempo conflitti di nomi. Potete immaginarli come cartelle sul vostro computer, dove ogni cartella contiene file che appartengono a un progetto o argomento specifico.
I namespace sono particolarmente utili in progetti più grandi o quando si utilizzano librerie di terze parti, dove potrebbero sorgere conflitti nei nomi delle classi.
Immaginate di avere una classe chiamata Automobile
nel vostro progetto e di volerla inserire in un namespace
chiamato Trasporti
. Lo fareste in questo modo:
namespace Trasporti;
class Automobile
{
function suonaClacson()
{
echo 'Bip bip!';
}
}
Se volete utilizzare la classe Automobile
in un altro file, dovete specificare da quale namespace proviene la
classe:
$auto = new Trasporti\Automobile;
Per semplificare, potete indicare all'inizio del file quale classe di un dato namespace volete utilizzare, il che permette di creare istanze senza dover specificare l'intero percorso:
use Trasporti\Automobile;
$auto = new Automobile;
Ereditarietà
L'ereditarietà è uno strumento della programmazione orientata agli oggetti che consente di creare nuove classi basate su classi già esistenti, ereditandone proprietà e metodi e estendendoli o ridefinendoli secondo necessità. L'ereditarietà permette di garantire la riutilizzabilità del codice e una gerarchia di classi.
In parole povere, se abbiamo una classe e vorremmo crearne un'altra derivata da essa, ma con alcune modifiche, possiamo “ereditare” la nuova classe dalla classe originale.
In PHP, l'ereditarietà si realizza tramite la parola chiave extends
.
La nostra classe Persona
memorizza informazioni sull'età. Possiamo avere un'altra classe Studente
,
che estende Persona
e aggiunge informazioni sul corso di studi.
Vediamo un esempio:
class Persona
{
private $eta;
function __construct($eta)
{
$this->eta = $eta;
}
function stampaInformazioni()
{
echo "Età: {$this->eta} anni\n";
}
}
class Studente extends Persona
{
private $corsoDiStudi;
function __construct($eta, $corsoDiStudi)
{
parent::__construct($eta);
$this->corsoDiStudi = $corsoDiStudi;
}
function stampaInformazioni()
{
parent::stampaInformazioni();
echo "Corso di studi: {$this->corsoDiStudi} \n";
}
}
$studente = new Studente(20, 'Informatica');
$studente->stampaInformazioni();
Come funziona questo codice?
- Abbiamo usato la parola chiave
extends
per estendere la classePersona
, il che significa che la classeStudente
eredita tutti i metodi e le proprietà daPersona
. - La parola chiave
parent::
ci permette di chiamare metodi dalla classe genitore. In questo caso, abbiamo chiamato il costruttore dalla classePersona
prima di aggiungere la nostra funzionalità alla classeStudente
. E allo stesso modo, il metodostampaInformazioni()
del genitore prima di stampare le informazioni sullo studente.
L'ereditarietà è destinata a situazioni in cui esiste una relazione “è un” tra le classi. Ad esempio, uno
Studente
è una Persona
. Un gatto è un animale. Ci dà la possibilità, nei casi in cui nel codice ci
aspettiamo un oggetto (ad es. “Persona”), di utilizzare al suo posto un oggetto ereditato (ad es. “Studente”).
È importante rendersi conto che lo scopo principale dell'ereditarietà non è evitare la duplicazione del codice. Al contrario, un uso improprio dell'ereditarietà può portare a codice complesso e difficile da mantenere. Se non esiste una relazione “è un” tra le classi, dovremmo considerare la composizione invece dell'ereditarietà.
Notate che i metodi stampaInformazioni()
nelle classi Persona
e Studente
stampano
informazioni leggermente diverse. E possiamo aggiungere altre classi (ad esempio Impiegato
) 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:
$persone = [
new Persona(30),
new Studente(20, 'Informatica'),
new Impiegato(45, 'Direttore'), // Supponendo che esista una classe Impiegato
];
foreach ($persone as $persona) {
$persona->stampaInformazioni();
}
Composizione
La composizione è una tecnica in cui, invece di ereditare proprietà e metodi da un'altra classe, utilizziamo semplicemente la sua istanza nella nostra classe. Questo ci permette di combinare funzionalità e proprietà di più classi senza la necessità di creare strutture ereditarie complesse.
Vediamo un esempio. Abbiamo una classe Motore
e una classe Automobile
. Invece di dire “Automobile
è un Motore”, diciamo “Automobile ha un Motore”, che è una tipica relazione di composizione.
class Motore
{
function accendi()
{
echo 'Motore acceso.';
}
}
class Automobile
{
private $motore;
function __construct()
{
$this->motore = new Motore;
}
function avvia()
{
$this->motore->accendi();
echo 'Automobile pronta a partire!';
}
}
$auto = new Automobile;
$auto->avvia();
Qui Automobile
non ha tutte le proprietà e i metodi di Motore
, ma ha accesso ad esso tramite la
proprietà $motore
.
Il vantaggio della composizione è una maggiore flessibilità nel design e una migliore possibilità di modifiche future.
Visibilità
In PHP, è possibile definire la “visibilità” per proprietà, metodi e costanti di una classe. La visibilità determina da dove è possibile accedere a questi elementi.
- Public: Se un elemento è contrassegnato come
public
, significa che è possibile accedervi da qualsiasi luogo, anche al di fuori della classe. - Protected: Un elemento contrassegnato come
protected
è accessibile solo all'interno della classe data e di tutti i suoi discendenti (classi che ereditano da questa classe). - Private: Se un elemento è
private
, è possibile accedervi solo dall'interno della classe in cui è stato definito.
Se non si specifica la visibilità, PHP la imposta automaticamente su public
.
Vediamo un codice di esempio:
class EsempioVisibilita
{
public $proprietaPubblica = 'Pubblica';
protected $proprietaProtetta = 'Protetta';
private $proprietaPrivata = 'Privata';
public function stampaProprieta()
{
echo $this->proprietaPubblica; // Funziona
echo $this->proprietaProtetta; // Funziona
echo $this->proprietaPrivata; // Funziona
}
}
$oggetto = new EsempioVisibilita;
$oggetto->stampaProprieta();
echo $oggetto->proprietaPubblica; // Funziona
// echo $oggetto->proprietaProtetta; // Genera un errore
// echo $oggetto->proprietaPrivata; // Genera un errore
Continuiamo con l'ereditarietà della classe:
class FiglioClasse extends EsempioVisibilita
{
public function stampaProprieta()
{
echo $this->proprietaPubblica; // Funziona
echo $this->proprietaProtetta; // Funziona
// echo $this->proprietaPrivata; // Genera un errore
}
}
In questo caso, il metodo stampaProprieta()
nella classe FiglioClasse
può accedere alle proprietà
pubbliche e protette, ma non può accedere alle proprietà private della classe genitore.
Dati e metodi dovrebbero essere il più nascosti possibile e accessibili solo tramite un'interfaccia definita. Ciò consente di modificare l'implementazione interna della classe senza influenzare il 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 sovrascritti. Quando contrassegniamo una classe come final
, non può essere estesa. Quando
contrassegniamo un metodo come final
, non può essere sovrascritto in una classe figlia.
Sapere che una determinata classe o metodo non verrà ulteriormente modificato ci consente di apportare modifiche più facilmente senza doverci preoccupare di possibili conflitti. Ad esempio, possiamo aggiungere un nuovo metodo senza preoccuparci che un suo discendente abbia già un metodo con lo stesso nome, causando una collisione. Oppure possiamo modificare i parametri di un metodo, poiché ancora una volta non c'è rischio di causare un'incompatibilità con un metodo sovrascritto in un discendente.
final class ClasseFinale
{
}
// Il seguente codice genererà un errore, perché non possiamo ereditare da una classe final.
class FiglioClasseFinale extends ClasseFinale
{
}
In questo esempio, il tentativo di ereditare dalla classe finale ClasseFinale
genererà un errore.
Proprietà e metodi statici
Quando in PHP parliamo di elementi “statici” di una classe, intendiamo metodi e proprietà che appartengono alla classe stessa, e non a un'istanza specifica di quella classe. Ciò significa che non è necessario creare un'istanza della classe per accedervi. Invece, li chiamate o accedete ad essi direttamente tramite il nome della classe.
Tenete presente che, poiché gli elementi statici appartengono alla classe e non alle sue istanze, non potete usare la
pseudo-variabile $this
all'interno dei metodi statici.
L'uso di proprietà statiche porta a codice poco chiaro e pieno di insidie, quindi non dovreste mai usarle e non mostreremo nemmeno un esempio di utilizzo qui. Al contrario, i metodi statici sono utili. Esempio di utilizzo:
class Calcolatrice
{
public static function addizione($a, $b)
{
return $a + $b;
}
public static function sottrazione($a, $b)
{
return $a - $b;
}
}
// Utilizzo del metodo statico senza creare un'istanza della classe
echo Calcolatrice::addizione(5, 3); // Risultato: 8
echo Calcolatrice::sottrazione(5, 3); // Risultato: 2
In questo esempio, abbiamo creato una classe Calcolatrice
con due metodi statici. Possiamo chiamare questi metodi
direttamente senza creare un'istanza della classe usando l'operatore ::
. I metodi statici sono particolarmente utili
per operazioni che non dipendono dallo stato di un'istanza specifica della classe.
Costanti di classe
All'interno delle classi, abbiamo la possibilità di definire costanti. Le costanti sono valori che non cambiano mai durante l'esecuzione del programma. A differenza delle variabili, il valore di una costante rimane sempre lo stesso.
class Automobile
{
public const NumeroRuote = 4;
public function mostraNumeroRuote(): int
{
echo self::NumeroRuote;
}
}
echo Automobile::NumeroRuote; // Output: 4
In questo esempio, abbiamo una classe Automobile
con la costante NumeroRuote
. Quando vogliamo
accedere alla costante all'interno della classe, possiamo usare la parola chiave self
invece del nome della
classe.
Interfacce di oggetti
Le interfacce di oggetti funzionano come “contratti” per le classi. Se una classe deve implementare un'interfaccia di oggetto, deve contenere tutti i metodi definiti da quell'interfaccia. È un ottimo modo per garantire che determinate classi aderiscano allo stesso “contratto” o struttura.
In PHP, un'interfaccia viene definita con la parola chiave interface
. Tutti i metodi definiti nell'interfaccia
sono pubblici (public
). Quando una classe implementa un'interfaccia, utilizza la parola chiave
implements
.
interface Animale
{
function emettiSuono();
}
class Gatto implements Animale
{
public function emettiSuono()
{
echo 'Miao';
}
}
$gatto = new Gatto;
$gatto->emettiSuono();
Se una classe implementa un'interfaccia, ma non tutti i metodi attesi sono definiti al suo interno, PHP genererà un errore.
Una classe può implementare più interfacce contemporaneamente, il che è una differenza rispetto all'ereditarietà, dove una classe può ereditare solo da una classe:
interface Guardiano
{
function sorvegliaCasa();
}
class Cane implements Animale, Guardiano
{
public function emettiSuono()
{
echo 'Bau';
}
public function sorvegliaCasa()
{
echo 'Il cane sorveglia attentamente la casa';
}
}
Classi astratte
Le classi astratte fungono da modelli di base per altre classi, ma non è possibile crearne istanze direttamente. Contengono una combinazione di metodi completi e metodi astratti, che non hanno un contenuto definito. Le classi che ereditano da classi astratte devono fornire definizioni per tutti i metodi astratti del genitore.
Per definire una classe astratta, usiamo la parola chiave abstract
.
abstract class ClasseAstratta
{
public function metodoComune()
{
echo 'Questo è un metodo comune';
}
abstract public function metodoAstratto();
}
class Figlio extends ClasseAstratta
{
public function metodoAstratto()
{
echo 'Questa è l\'implementazione del metodo astratto';
}
}
$istanza = new Figlio;
$istanza->metodoComune();
$istanza->metodoAstratto();
In questo esempio, abbiamo una classe astratta con un metodo comune e un metodo astratto. Poi abbiamo una classe
Figlio
, che eredita da ClasseAstratta
e fornisce l'implementazione per il metodo astratto.
Qual è la differenza tra interfacce e classi astratte? Le classi astratte possono contenere sia metodi astratti che concreti, mentre le interfacce definiscono solo quali metodi una classe deve implementare, ma non forniscono alcuna implementazione. Una classe può ereditare solo da una classe astratta, ma può implementare un numero qualsiasi di interfacce.
Controllo dei tipi
Nella programmazione, è molto importante essere sicuri che i dati con cui lavoriamo siano del tipo corretto. In PHP, abbiamo strumenti che ci garantiscono questo. La verifica che i dati abbiano il tipo corretto si chiama “controllo dei tipi” (type hinting).
Tipi che possiamo incontrare in PHP:
- Tipi di base: Includono
int
(numeri interi),float
(numeri decimali),bool
(valori booleani),string
(stringhe),array
(array) enull
. - Classi: Se vogliamo che un valore sia un'istanza di una classe specifica.
- Interfacce: Definisce un insieme di metodi che una classe deve implementare. Un valore che soddisfa un'interfaccia deve avere questi metodi.
- Tipi misti: Possiamo specificare che una variabile può avere più tipi consentiti.
- Void: Questo tipo speciale indica che una funzione o un metodo non restituisce alcun valore.
Vediamo come modificare il codice per includere i tipi:
class Persona
{
private int $eta;
public function __construct(int $eta)
{
$this->eta = $eta;
}
public function stampaEta(): void
{
echo "Questa persona ha {$this->eta} anni.";
}
}
/**
* Funzione che accetta un oggetto della classe Persona e stampa l'età della persona.
*/
function stampaEtaPersona(Persona $persona): void
{
$persona->stampaEta();
}
In questo modo, abbiamo assicurato che il nostro codice si aspetti e lavori con dati del tipo corretto, il che ci aiuta a prevenire potenziali errori.
Alcuni tipi non possono essere scritti direttamente in PHP. In tal caso, vengono indicati nel commento phpDoc, che è un
formato standard per documentare il codice PHP che inizia con /**
e termina con */
. Permette di
aggiungere descrizioni a classi, metodi e così via. E anche di specificare tipi complessi tramite le cosiddette annotazioni
@var
, @param
e @return
. Questi tipi vengono poi utilizzati dagli strumenti per l'analisi
statica del codice, ma PHP stesso non li controlla.
class Elenco
{
/** @var array<Persona> la notazione indica che si tratta di un array di oggetti Persona */
private array $persone = [];
public function aggiungiPersona(Persona $persona): void
{
$this->persone[] = $persona;
}
}
Confronto e identità
In PHP, è possibile confrontare oggetti in due modi:
- Confronto di valori
==
: Verifica se gli oggetti sono della stessa classe e hanno gli stessi valori nelle loro proprietà. - Identità
===
: Verifica se si tratta della stessa istanza dell'oggetto.
class Automobile
{
public string $marca;
public function __construct(string $marca)
{
$this->marca = $marca;
}
}
$auto1 = new Automobile('Skoda');
$auto2 = new Automobile('Skoda');
$auto3 = $auto1;
var_dump($auto1 == $auto2); // true, perché hanno lo stesso valore
var_dump($auto1 === $auto2); // false, perché non sono la stessa istanza
var_dump($auto1 === $auto3); // true, perché $auto3 è la stessa istanza di $auto1
Operatore instanceof
L'operatore instanceof
consente di verificare se un dato oggetto è un'istanza di una determinata classe, un
discendente di quella classe, o se implementa una determinata interfaccia.
Immaginiamo di avere una classe Persona
e un'altra classe Studente
, che è un discendente della
classe Persona
:
class Persona
{
private int $eta;
public function __construct(int $eta)
{
$this->eta = $eta;
}
}
class Studente extends Persona
{
private string $corsoDiStudi;
public function __construct(int $eta, string $corsoDiStudi)
{
parent::__construct($eta);
$this->corsoDiStudi = $corsoDiStudi;
}
}
$studente = new Studente(20, 'Informatica');
// Verifica se $studente è un'istanza della classe Studente
var_dump($studente instanceof Studente); // Output: bool(true)
// Verifica se $studente è un'istanza della classe Persona (perché Studente è un discendente di Persona)
var_dump($studente instanceof Persona); // Output: bool(true)
Dagli output è evidente che l'oggetto $studente
è considerato contemporaneamente un'istanza di entrambe le
classi – Studente
e Persona
.
Interfacce fluenti
L'“interfaccia fluente” (in inglese “Fluent Interface”) è una tecnica in OOP che consente di concatenare metodi insieme in una singola chiamata. Questo spesso semplifica e rende più chiaro il codice.
L'elemento chiave di un'interfaccia fluente è che ogni metodo nella catena restituisce un riferimento all'oggetto corrente.
Otteniamo questo usando return $this;
alla fine del metodo. Questo stile di programmazione è spesso associato a
metodi chiamati “setter”, che impostano i valori delle proprietà dell'oggetto.
Vediamo come può apparire un'interfaccia fluente nell'esempio dell'invio di email:
public function inviaMessaggio()
{
$email = new Email; // Supponendo che esista una classe Email
$email->setFrom('mittente@example.com')
->setRecipient('destinatario@example.com')
->setMessage('Ciao, questo è un messaggio.')
->send(); // Supponendo che esista un metodo 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 ci restituiscono l'oggetto corrente ($email
), il che ci permette di concatenare un altro metodo
dopo di esso. Infine, chiamiamo il metodo send()
, che invia effettivamente l'email.
Grazie alle interfacce fluenti, possiamo scrivere codice intuitivo e facilmente leggibile.
Copia tramite clone
In PHP, possiamo creare una copia di un oggetto usando l'operatore clone
. In questo modo, otteniamo una nuova
istanza con contenuto identico.
Se abbiamo bisogno di modificare alcune proprietà durante la copia di un oggetto, possiamo definire nella classe un metodo
speciale __clone()
. Questo metodo viene chiamato automaticamente quando l'oggetto viene clonato.
class Pecora
{
public string $nome;
public function __construct(string $nome)
{
$this->nome = $nome;
}
public function __clone()
{
$this->nome = 'Clone ' . $this->nome;
}
}
$originale = new Pecora('Dolly');
echo $originale->nome . "\n"; // Stampa: Dolly
$clone = clone $originale;
echo $clone->nome . "\n"; // Stampa: Clone Dolly
In questo esempio, abbiamo una classe Pecora
con una proprietà $nome
. Quando cloniamo un'istanza di
questa classe, il metodo __clone()
si assicura che il nome della pecora clonata ottenga il prefisso “Clone”.
Trait
I trait in PHP sono uno strumento che consente di condividere metodi, proprietà e costanti tra classi e prevenire la duplicazione del codice. Potete immaginarli come un meccanismo di “copia e incolla” (Ctrl-C e Ctrl-V), in cui il contenuto del trait viene “incollato” nelle classi. Ciò consente di riutilizzare il codice senza la necessità di creare gerarchie di classi complicate.
Vediamo un semplice esempio di come utilizzare i trait in PHP:
trait SuonareClacson
{
public function suonaClacson()
{
echo 'Bip bip!';
}
}
class Automobile
{
use SuonareClacson;
}
class Camion
{
use SuonareClacson;
}
$auto = new Automobile;
$auto->suonaClacson(); // Stampa 'Bip bip!'
$camion = new Camion;
$camion->suonaClacson(); // Stampa anche 'Bip bip!'
In questo esempio, abbiamo un trait chiamato SuonareClacson
, che contiene un metodo suonaClacson()
.
Poi abbiamo due classi: Automobile
e Camion
, che entrambe usano il trait SuonareClacson
.
Grazie a questo, entrambe le classi “hanno” il metodo suonaClacson()
, e possiamo chiamarlo sugli oggetti di
entrambe le classi.
I trait vi permettono di condividere codice tra classi in modo facile ed efficiente. Tuttavia, non entrano nella gerarchia
ereditaria, cioè $auto instanceof SuonareClacson
restituirà false
.
Eccezioni
Le eccezioni in OOP ci permettono di gestire elegantemente errori e situazioni inaspettate nel nostro codice. Sono oggetti che trasportano informazioni sull'errore o sulla situazione insolita.
In PHP, abbiamo una classe incorporata Exception
, che funge da base per tutte le eccezioni. Ha diversi metodi che
ci permettono di ottenere maggiori 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, possiamo “lanciare” un'eccezione usando la parola chiave throw
.
function divisione(float $a, float $b): float
{
if ($b === 0.0) { // Confronto più sicuro per i float
throw new Exception('Divisione per zero!');
}
return $a / $b;
}
Quando la funzione divisione()
riceve zero come secondo argomento, lancia un'eccezione con il messaggio di errore
'Divisione per zero!'
. Per evitare che il programma si blocchi quando viene lanciata un'eccezione, la catturiamo in
un blocco try/catch
:
try {
echo divisione(10, 0);
} catch (Exception $e) {
echo 'Eccezione catturata: '. $e->getMessage();
}
Il codice che può lanciare un'eccezione è racchiuso in un blocco try
. Se viene lanciata un'eccezione,
l'esecuzione del codice si sposta al blocco catch
, dove possiamo gestire l'eccezione (ad esempio, stampare un
messaggio di errore).
Dopo i blocchi try
e catch
, possiamo aggiungere un blocco finally
opzionale, che verrà
eseguito sempre, indipendentemente dal fatto che sia stata lanciata o meno un'eccezione (anche nel caso in cui usiamo
l'istruzione return
, break
o continue
nel blocco try
o
catch
):
try {
echo divisione(10, 0);
} catch (Exception $e) {
echo 'Eccezione catturata: '. $e->getMessage();
} finally {
// Codice che viene eseguito sempre, indipendentemente dal fatto che sia stata lanciata un'eccezione o meno
}
Possiamo anche creare le nostre classi (gerarchia) di eccezioni che ereditano dalla classe Exception. Come esempio, immaginiamo una semplice applicazione bancaria che consente di effettuare depositi e prelievi:
class EccezioneBancaria extends Exception {}
class EccezioneFondiInsufficienti extends EccezioneBancaria {}
class EccezioneLimiteSuperato extends EccezioneBancaria {}
class ContoBancario
{
private int $saldo = 0;
private int $limiteGiornaliero = 1000;
public function deposita(int $importo): int
{
$this->saldo += $importo;
return $this->saldo;
}
public function preleva(int $importo): int
{
if ($importo > $this->saldo) {
throw new EccezioneFondiInsufficienti('Fondi insufficienti sul conto.');
}
if ($importo > $this->limiteGiornaliero) {
throw new EccezioneLimiteSuperato('È stato superato il limite giornaliero per i prelievi.');
}
$this->saldo -= $importo;
return $this->saldo;
}
}
Per un singolo blocco try
, è possibile specificare più blocchi catch
, se ci si aspetta diversi tipi
di eccezioni.
$conto = new ContoBancario;
$conto->deposita(500);
try {
$conto->preleva(1500);
} catch (EccezioneLimiteSuperato $e) {
echo $e->getMessage();
} catch (EccezioneFondiInsufficienti $e) {
echo $e->getMessage();
} catch (EccezioneBancaria $e) {
echo 'Si è verificato un errore durante l\'esecuzione dell\'operazione.';
}
In questo esempio, è importante notare l'ordine dei blocchi catch
. Poiché tutte le eccezioni ereditano da
EccezioneBancaria
, se avessimo questo blocco per primo, catturerebbe tutte le eccezioni senza che il codice raggiunga
i blocchi catch
successivi. Pertanto, è importante avere eccezioni più specifiche (cioè quelle che ereditano da
altre) nel blocco catch
più in alto nell'ordine rispetto alle loro eccezioni genitore.
Iterazione
In PHP, è possibile iterare sugli oggetti usando il ciclo foreach
, in modo simile a come si itera sugli array.
Affinché 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 si sposta al valore successivo,
rewind()
che si sposta all'inizio e valid()
che verifica se non siamo ancora alla fine.
La seconda opzione è implementare l'interfaccia IteratorAggregate
, che ha solo un metodo
getIterator()
. Questo restituisce o un oggetto sostitutivo che gestirà l'iterazione, oppure può rappresentare un
generatore, che è una funzione speciale in cui si usa yield
per restituire progressivamente chiavi e valori:
class Persona
{
public function __construct(
public int $eta,
) {
}
}
class Elenco implements IteratorAggregate
{
private array $persone = [];
public function aggiungiPersona(Persona $persona): void
{
$this->persone[] = $persona;
}
public function getIterator(): Generator
{
foreach ($this->persone as $persona) {
yield $persona;
}
}
}
$elenco = new Elenco;
$elenco->aggiungiPersona(new Persona(30));
$elenco->aggiungiPersona(new Persona(25));
foreach ($elenco as $persona) {
echo "Età: {$persona->eta} anni \n";
}
Buone pratiche
Una volta compresi i principi fondamentali della programmazione orientata agli oggetti, è importante concentrarsi sulle buone pratiche in OOP. Queste vi aiuteranno a scrivere codice che non sia solo funzionale, ma anche leggibile, comprensibile e facilmente manutenibile.
- Separazione delle responsabilità (Separation of Concerns): Ogni classe dovrebbe avere una responsabilità chiaramente definita e dovrebbe risolvere solo un compito principale. Se una classe fa troppe cose, potrebbe essere opportuno dividerla in classi più piccole e specializzate.
- Incapsulamento (Encapsulation): Dati e metodi dovrebbero essere il più nascosti possibile e accessibili solo tramite un'interfaccia definita. Ciò consente di modificare l'implementazione interna della classe senza influenzare il resto del codice.
- Iniezione delle dipendenze (Dependency Injection): Invece di creare dipendenze direttamente nella classe, dovreste “iniettarle” dall'esterno. Per una comprensione più approfondita di questo principio, consigliamo i capitoli sulla Dependency Injection.