Introduzione
L’ereditarietà e il polimorfismo sono due concetti fondamentali della programmazione orientata agli oggetti in PHP. In pratica, permettono di creare codice più ordinato, riutilizzabile e facile da estendere. In questo tutorial vedremo un caso molto utile e realistico: la gestione di diversi tipi di documenti in un sistema gestionale.
Immagina un’applicazione che deve trattare documenti diversi, ad esempio:
- fatture
- preventivi
- contratti
Questi oggetti hanno comportamenti comuni, come il calcolo del totale o la generazione di un riepilogo, ma ciascuno ha anche regole specifiche. Qui entrano in gioco ereditarietà e polimorfismo: una classe base definisce la struttura comune, mentre le classi figlie personalizzano il comportamento.
Questo approccio è molto pratico quando vuoi evitare duplicazione di codice e mantenere il sistema semplice da evolvere. Se domani aggiungi un nuovo tipo di documento, non devi riscrivere tutto: ti basta creare una nuova classe che estende la base.
Codice completo
<?php
declare(strict_types=1);
/**
* Classe base astratta per tutti i documenti.
* Definisce dati e comportamenti comuni.
*/
abstract class Documento
{
protected string $numero;
protected string $cliente;
protected array $righe = [];
public function __construct(string $numero, string $cliente)
{
$this->numero = $numero;
$this->cliente = $cliente;
}
public function aggiungiRiga(string $descrizione, float $importo): void
{
$this->righe[] = [
´descrizione´ => $descrizione,
´importo´ => $importo
];
}
public function calcolaTotale(): float
{
$totale = 0.0;
foreach ($this->righe as $riga) {
$totale += $riga[´importo´];
}
return $totale;
}
// Ogni documento deve descriversi in modo specifico.
abstract public function getTipo(): string;
// Ogni documento può avere una logica diversa per il testo finale.
abstract public function generaRiepilogo(): string;
public function stampaBase(): string
{
return "Documento: " . $this->getTipo() . PHP_EOL .
"Numero: " . $this->numero . PHP_EOL .
"Cliente: " . $this->cliente . PHP_EOL .
"Totale: € " . number_format($this->calcolaTotale(), 2, ´,´, ´.´) . PHP_EOL;
}
}
/**
* Fattura: può aggiungere IVA al totale.
*/
class Fattura extends Documento
{
private float $aliquotaIva;
public function __construct(string $numero, string $cliente, float $aliquotaIva = 22.0)
{
parent::__construct($numero, $cliente);
$this->aliquotaIva = $aliquotaIva;
}
public function getTipo(): string
{
return ´Fattura´;
}
public function calcolaTotaleConIva(): float
{
$totale = $this->calcolaTotale();
return $totale + ($totale * $this->aliquotaIva / 100);
}
public function generaRiepilogo(): string
{
return $this->stampaBase() .
"IVA: {$this->aliquotaIva}%". PHP_EOL .
"Totale con IVA: € " . number_format($this->calcolaTotaleConIva(), 2, ´,´, ´.´) . PHP_EOL;
}
}
/**
* Preventivo: mostra un totale indicativo e uno sconto opzionale.
*/
class Preventivo extends Documento
{
private float $scontoPercentuale;
public function __construct(string $numero, string $cliente, float $scontoPercentuale = 0.0)
{
parent::__construct($numero, $cliente);
$this->scontoPercentuale = $scontoPercentuale;
}
public function getTipo(): string
{
return ´Preventivo´;
}
public function calcolaTotaleScontato(): float
{
$totale = $this->calcolaTotale();
return $totale - ($totale * $this->scontoPercentuale / 100);
}
public function generaRiepilogo(): string
{
return $this->stampaBase() .
"Sconto: {$this->scontoPercentuale}%". PHP_EOL .
"Totale stimato: € " . number_format($this->calcolaTotaleScontato(), 2, ´,´, ´.´) . PHP_EOL;
}
}
/**
* Contratto: può includere una durata e una clausola aggiuntiva.
*/
class Contratto extends Documento
{
private int $durataMesi;
public function __construct(string $numero, string $cliente, int $durataMesi)
{
parent::__construct($numero, $cliente);
$this->durataMesi = $durataMesi;
}
public function getTipo(): string
{
return ´Contratto´;
}
public function generaRiepilogo(): string
{
return $this->stampaBase() .
"Durata: {$this->durataMesi} mesi" . PHP_EOL .
"Nota: documento contrattuale da verificare con attenzione." . PHP_EOL;
}
}
/**
* Funzione polimorfica: accetta qualunque Documento.
* Il comportamento cambia in base alla classe concreta passata.
*/
function mostraDocumento(Documento $documento): void
{
echo "============================" . PHP_EOL;
echo $documento->generaRiepilogo();
}
// Creazione dei documenti
$fattura = new Fattura(´FAT-2025-001´, ´Alfa Srl´, 22);
$fattura->aggiungiRiga(´Sviluppo sito web´, 1200);
$fattura->aggiungiRiga(´Assistenza tecnica´, 300);
$preventivo = new Preventivo(´PRE-2025-010´, ´Beta Studio´, 10);
$preventivo->aggiungiRiga(´Analisi requisiti´, 500);
$preventivo->aggiungiRiga(´Prototipo interfaccia´, 800);
$contratto = new Contratto(´CON-2025-003´, ´Gamma Spa´, 12);
$contratto->aggiungiRiga(´Consulenza annuale´, 2400);
// Uso polimorfico
$documenti = [$fattura, $preventivo, $contratto];
foreach ($documenti as $documento) {
mostraDocumento($documento);
echo PHP_EOL;
} Spiegazione
Il codice usa una classe astratta Documento come base comune. Questo significa che non la useremo direttamente per creare oggetti, ma la useremo per definire ciò che tutte le classi figlie devono condividere.
1. La classe base astratta
La classe Documento contiene proprietà comuni come numero, cliente e righe. Inoltre fornisce metodi già pronti:
- aggiungiRiga(): inserisce una voce nel documento
- calcolaTotale(): somma gli importi delle righe
- stampaBase(): costruisce una parte comune del riepilogo
In più definisce due metodi astratti:
- getTipo()
- generaRiepilogo()
Essendo astratti, questi metodi obbligano le classi figlie a implementarli. Questo è molto utile perché garantisce un contratto minimo: ogni documento deve sapere come identificarsi e come presentarsi.
2. Le classi figlie
Le classi Fattura, Preventivo e Contratto estendono Documento e aggiungono logiche specifiche.
- Fattura: calcola anche il totale con IVA
- Preventivo: applica uno sconto e mostra un totale stimato
- Contratto: aggiunge la durata in mesi e una nota descrittiva
Ogni classe figlia eredita i metodi comuni e può riutilizzarli senza riscriverli. Questo è il vero vantaggio dell’ereditarietà: centralizzare ciò che è comune e specializzare solo ciò che cambia.
3. Il polimorfismo
La parte più interessante è la funzione mostraDocumento(Documento $documento). Questa funzione accetta un oggetto di tipo Documento, ma in realtà può ricevere qualsiasi istanza di una classe figlia.
Quando chiamiamo $documento->generaRiepilogo(), PHP esegue il metodo corretto in base alla classe concreta dell’oggetto. Questo comportamento è il polimorfismo: stessa chiamata, risultati diversi a seconda del tipo reale dell’oggetto.
Nel ciclo finale, il vettore $documenti contiene oggetti diversi, ma li trattiamo tutti allo stesso modo. La funzione non ha bisogno di sapere se sta lavorando con una fattura, un preventivo o un contratto. Questo rende il codice più flessibile e più facile da mantenere.
4. Perché questo esempio è utile
Questo scenario è molto vicino a casi reali: software gestionali, CRM, ERP, portali amministrativi e sistemi di documentazione. Ogni documento ha una struttura comune, ma regole diverse di calcolo e presentazione.
Con ereditarietà e polimorfismo puoi:
- ridurre la duplicazione del codice
- separare le responsabilità
- aggiungere nuovi documenti senza modificare il codice esistente
- scrivere funzioni più generiche e riutilizzabili
Best practice
Quando usi ereditarietà e polimorfismo in PHP, conviene seguire alcune regole pratiche per evitare classi troppo rigide o difficili da mantenere.
- Usa l’ereditarietà solo quando esiste una vera relazione “is-a”: una fattura è un documento, quindi ha senso estendere Documento.
- Non mettere troppa logica nella classe base: la classe astratta deve fornire ciò che è davvero comune, non diventare un contenitore di tutto.
- Preferisci metodi astratti per i comportamenti obbligatori: così costringi le classi figlie a implementare la logica necessaria.
- Evita gerarchie troppo profonde: più livelli aggiungi, più il sistema diventa difficile da capire e testare.
- Tipizza i parametri e i ritorni: in PHP moderno, i type hint aiutano a prevenire errori e migliorano la leggibilità.
- Usa il polimorfismo per semplificare il codice client: chi usa gli oggetti non deve conoscere i dettagli interni di ogni classe.
Un altro consiglio importante: se noti che una classe figlia usa solo una piccola parte della classe base, forse l’ereditarietà non è la soluzione migliore. In quei casi, spesso è più adatto il composizionamento. Tuttavia, per modelli come questo, l’ereditarietà è una scelta naturale e chiara.
Riepilogo
In questo tutorial abbiamo visto come usare ereditarietà e polimorfismo in PHP per gestire una gerarchia di documenti. La classe astratta Documento definisce la struttura comune, mentre le classi Fattura, Preventivo e Contratto implementano comportamenti specifici.
I punti chiave da ricordare sono:
- l’ereditarietà permette di riutilizzare codice comune
- i metodi astratti obbligano le classi figlie a rispettare un contratto
- il polimorfismo consente di trattare oggetti diversi in modo uniforme
- un buon design OOP rende il codice più estendibile e leggibile
Se vuoi progettare applicazioni PHP più pulite, questo pattern è una base eccellente da conoscere e applicare in scenari reali.
Approfondisci con risorse ufficiali
- PHP Manual - OOP: https://www.php.net/manual/it/language.oop5.php
- PHP Manual - Classi astratte: https://www.php.net/manual/it/language.oop5.abstract.php
- PHP Manual - Ereditarietà: https://www.php.net/manual/it/language.oop5.inheritance.php
- PHP Manual - Type declarations: https://www.php.net/manual/it/language.types.declarations.php
