Ereditarietà e polimorfismo in PHP: esportazione di report in diversi formati

by theArchitect
SHARE
Ereditarietà e polimorfismo in PHP: esportazione di report in diversi formati
© Guida-HTML5.it

Introduzione

Quando un’applicazione deve generare report, spesso i dati devono essere esportati in formati differenti: ad esempio CSV per un foglio di calcolo, JSON per un’API oppure HTML per una pagina web. Un approccio poco organizzato consiste nel riempire una sola classe di istruzioni if e switch per gestire ogni formato. Con il tempo, però, questa soluzione diventa difficile da estendere e da testare.

In questo tutorial realizzeremo un sistema di esportazione usando ereditarietà e polimorfismo. Creeremo una classe astratta comune, chiamata ReportExporter, e diverse sottoclassi concrete. Ogni sottoclasse saprà trasformare gli stessi dati nel proprio formato.

Il vantaggio principale è che il codice che utilizza gli esportatori non dovrà conoscere i dettagli della conversione. Sarà sufficiente lavorare con il tipo comune ReportExporter. In questo modo potremo aggiungere un nuovo formato senza modificare la logica principale dell’applicazione.

Codice completo

<?php

declare(strict_types=1);

/**
 * Classe astratta di base per tutti gli esportatori.
 */
abstract class ReportExporter
{
    /**
     * Metodo polimorfico: ogni sottoclasse deve implementarlo.
     *
     * @param array<array<string, mixed>> $rows
     */
    abstract public function export(array $rows): string;

    /**
     * Converte un valore in una forma leggibile.
     */
    protected function normalizeValue(mixed $value): string
    {
        if (is_bool($value)) {
            return $value ? ´Sì´ : ´No´;
        }

        return (string) $value;
    }
}

/**
 * Esportazione in formato CSV.
 */
final class CsvReportExporter extends ReportExporter
{
    public function export(array $rows): string
    {
        if ($rows === []) {
            return ´´;
        }

        $handle = fopen(´php://temp´, ´r+´);

        if ($handle === false) {
            throw new RuntimeException(´Impossibile creare il buffer CSV.´);
        }

        // La prima riga contiene i nomi delle colonne.
        fputcsv($handle, array_keys($rows[0]));

        foreach ($rows as $row) {
            $values = array_map(
                fn (mixed $value): string => $this->normalizeValue($value),
                $row
            );

            fputcsv($handle, $values);
        }

        rewind($handle);
        $csv = stream_get_contents($handle);
        fclose($handle);

        return $csv === false ? ´´ : $csv;
    }
}

/**
 * Esportazione in formato JSON.
 */
final class JsonReportExporter extends ReportExporter
{
    public function export(array $rows): string
    {
        $json = json_encode(
            $rows,
            JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
        );

        if ($json === false) {
            throw new RuntimeException(
                ´Errore durante la generazione del JSON: ´
                . json_last_error_msg()
            );
        }

        return $json;
    }
}

/**
 * Esportazione in una tabella HTML.
 */
final class HtmlReportExporter extends ReportExporter
{
    public function export(array $rows): string
    {
        if ($rows === []) {
            return ´<p>Nessun dato disponibile.</p>´;
        }

        $html = ´<table border="1"><thead><tr>´;

        foreach (array_keys($rows[0]) as $column) {
            $html .= ´<th>´
                . htmlspecialchars((string) $column, ENT_QUOTES, ´UTF-8´)
                . ´</th>´;
        }

        $html .= ´</tr></thead><tbody>´;

        foreach ($rows as $row) {
            $html .= ´<tr>´;

            foreach ($row as $value) {
                $html .= ´<td>´
                    . htmlspecialchars(
                        $this->normalizeValue($value),
                        ENT_QUOTES,
                        ´UTF-8´
                    )
                    . ´</td>´;
            }

            $html .= ´</tr>´;
        }

        return $html . ´</tbody></table>´;
    }
}

/**
 * Questa funzione riceve qualsiasi esportatore compatibile
 * con la classe astratta ReportExporter.
 *
 * @param array<array<string, mixed>> $rows
 */
function generateReport(ReportExporter $exporter, array $rows): string
{
    return $exporter->export($rows);
}

// Dati recuperati, per esempio, da un database.
$orders = [
    [
        ´id´ => 101,
        ´cliente´ => ´Azienda Alfa´,
        ´totale´ => 249.90,
        ´evaduto´ => true,
    ],
    [
        ´id´ => 102,
        ´cliente´ => ´Studio Beta´,
        ´totale´ => 89.50,
        ´evaduto´ => false,
    ],
];

// Polimorfismo: la stessa funzione lavora con oggetti diversi.
$exporters = [
    new CsvReportExporter(),
    new JsonReportExporter(),
    new HtmlReportExporter(),
];

foreach ($exporters as $exporter) {
    echo generateReport($exporter, $orders);
    echo PHP_EOL . PHP_EOL;
}

Spiegazione

ReportExporter è una classe astratta. Non può essere istanziata direttamente, perché rappresenta un concetto generale: un oggetto capace di esportare dati. Il metodo export() è dichiarato astratto e obbliga ogni sottoclasse a fornire una propria implementazione.

Le classi CsvReportExporter, JsonReportExporter e HtmlReportExporter ereditano da ReportExporter. Grazie all’ereditarietà, condividono il metodo protetto normalizeValue(). Questo metodo evita di duplicare la conversione dei valori booleani in tutte le classi.

Il polimorfismo appare nella funzione generateReport(). Il parametro è dichiarato come ReportExporter, ma può contenere un oggetto di qualsiasi sottoclasse. Quando viene eseguito $exporter->export($rows), PHP richiama automaticamente il metodo corretto dell’oggetto reale.

Il ciclo finale dimostra il vantaggio pratico di questo approccio. La funzione non contiene controlli come “se il formato è CSV” oppure “se il formato è JSON”. Riceve semplicemente tre oggetti compatibili con la classe astratta e li utilizza allo stesso modo.

Per aggiungere il formato XML, ad esempio, sarebbe sufficiente creare una nuova classe XmlReportExporter che estende ReportExporter e implementa export(). Il codice della funzione generateReport() resterebbe invariato.

Best practice

  • Usa una classe astratta quando esiste comportamento condiviso. In questo esempio normalizeValue() è comune a più esportatori.
  • Mantieni le responsabilità separate. Ogni esportatore deve occuparsi soltanto della conversione nel proprio formato.
  • Tipizza parametri e valori restituiti. La dichiarazione declare(strict_types=1) aiuta a individuare conversioni non desiderate.
  • Valida gli errori delle funzioni native. json_encode(), fopen() e stream_get_contents() possono fallire e non dovrebbero essere ignorati.
  • Proteggi l’output HTML. htmlspecialchars() riduce il rischio di inserire codice HTML o JavaScript proveniente dai dati.
  • Preferisci l’estensione alla modifica. Se nasce un nuovo formato, aggiungi una nuova classe invece di modificare una lunga catena di condizioni.
  • Usa classi finali quando non prevedi ulteriori estensioni. In questo caso le implementazioni concrete sono marcate final, rendendo più esplicita l’architettura.

Riepilogo

L’ereditarietà permette a più classi di condividere struttura e comportamento, mentre il polimorfismo consente di usare oggetti diversi attraverso un tipo comune. Nel nostro esempio, tutti gli esportatori sono trattati come ReportExporter, ma ciascuno produce un risultato specifico.

Questa tecnica rende il codice più leggibile, testabile ed estendibile. In particolare, evita di concentrare tutta la logica in una sola classe e permette di aggiungere nuovi formati senza alterare il flusso principale dell’applicazione.

Approfondisci con risorse ufficiali

SHARE