PHP OOP: usare il pattern Repository per separare logica e accesso ai dati

by theArchitect
SHARE
PHP OOP: usare il pattern Repository per separare logica e accesso ai dati
© Guida-HTML5.it

Introduzione

Quando un progetto PHP cresce, uno dei problemi più comuni è vedere la logica applicativa mescolata con le query SQL, le chiamate API o l’accesso al filesystem. All’inizio può sembrare comodo, ma nel tempo il codice diventa difficile da testare, da leggere e da modificare.

Un modo molto pratico per migliorare l’architettura orientata agli oggetti in PHP è usare il pattern Repository. Questo pattern introduce una classe dedicata a recuperare e salvare i dati, nascondendo i dettagli dell’archiviazione al resto dell’applicazione. In questo modo il codice di business lavora con oggetti e non con query sparse in giro per il progetto.

In questo tutorial vedremo come costruire un piccolo esempio OOP in PHP con un repository per gestire utenti. L’obiettivo non è creare un framework, ma capire una tecnica concreta, utile e applicabile in progetti reali.

Codice completo

<?php

declare(strict_types=1);

/**
 * Entità di dominio: rappresenta un utente.
 */
class User
{
    public function __construct(
        private int $id,
        private string $name,
        private string $email
    ) {}

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }
}

/**
 * Contratto del repository.
 * Definisce le operazioni disponibili sui dati.
 */
interface UserRepositoryInterface
{
    public function findById(int $id): ?User;

    public function findAll(): array;

    public function save(User $user): void;

    public function delete(int $id): void;
}

/**
 * Implementazione in memoria.
 * Utile per test, demo o prototipi.
 */
class InMemoryUserRepository implements UserRepositoryInterface
{
    /**
     * Simula un archivio dati.
     * Ogni elemento contiene i dati di un utente.
     */
    private array $storage = [
        1 => [´id´ => 1, ´name´ => ´Mario Rossi´, ´email´ => ´[email protected]´],
        2 => [´id´ => 2, ´name´ => ´Laura Bianchi´, ´email´ => ´[email protected]´],
    ];

    public function findById(int $id): ?User
    {
        if (!isset($this->storage[$id])) {
            return null;
        }

        $data = $this->storage[$id];

        return new User($data[´id´], $data[´name´], $data[´email´]);
    }

    public function findAll(): array
    {
        $users = [];

        foreach ($this->storage as $data) {
            $users[] = new User($data[´id´], $data[´name´], $data[´email´]);
        }

        return $users;
    }

    public function save(User $user): void
    {
        $this->storage[$user->getId()] = [
            ´id´ => $user->getId(),
            ´name´ => $user->getName(),
            ´email´ => $user->getEmail(),
        ];
    }

    public function delete(int $id): void
    {
        unset($this->storage[$id]);
    }
}

/**
 * Servizio applicativo:
 * usa il repository senza conoscere i dettagli di persistenza.
 */
class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}

    public function renameUser(int $id, string $newName): bool
    {
        $user = $this->repository->findById($id);

        if ($user === null) {
            return false;
        }

        $user->setName($newName);
        $this->repository->save($user);

        return true;
    }

    public function listUserEmails(): array
    {
        $emails = [];

        foreach ($this->repository->findAll() as $user) {
            $emails[] = $user->getEmail();
        }

        return $emails;
    }
}

// --- ESEMPIO DI USO ---

$repository = new InMemoryUserRepository();
$service = new UserService($repository);

$user = $repository->findById(1);

if ($user !== null) {
    echo "Utente trovato: " . $user->getName() . PHP_EOL;
}

$updated = $service->renameUser(1, ´Mario Verdi´);

if ($updated) {
    echo "Nome aggiornato con successo." . PHP_EOL;
}

$emails = $service->listUserEmails();

foreach ($emails as $email) {
    echo $email . PHP_EOL;
}

Spiegazione

Il codice è composto da quattro parti principali: l’entità User, l’interfaccia UserRepositoryInterface, l’implementazione concreta InMemoryUserRepository e il servizio UserService.

1. L’entità User

La classe User rappresenta un oggetto di dominio. Contiene i dati fondamentali dell’utente: id, nome ed email. Notare che non contiene alcuna logica di accesso ai dati. Questo è importante: un oggetto di dominio deve rappresentare il concetto del problema, non sapere dove o come viene salvato.

2. L’interfaccia del repository

UserRepositoryInterface definisce il contratto. In pratica dice: “chiunque implementi questo repository deve sapere come cercare, salvare e cancellare utenti”.

Questo approccio porta due vantaggi enormi:

  • il servizio dipende da un contratto, non da una classe concreta;
  • in futuro puoi cambiare sorgente dati senza toccare la logica applicativa.

3. Il repository in memoria

InMemoryUserRepository è una versione semplificata che salva i dati in un array interno. È perfetta per studiare il pattern o per scrivere test senza database. In un progetto reale, la stessa interfaccia potrebbe essere implementata da una classe che usa PDO, MySQL, PostgreSQL o anche una API esterna.

Il punto chiave è che il resto dell’applicazione non deve sapere nulla di questa scelta tecnica.

4. Il servizio applicativo

UserService contiene la logica di business. Il metodo renameUser() cerca l’utente, modifica il nome e salva il risultato. Il servizio non costruisce query, non gestisce array di storage e non si occupa del formato dei dati persistiti. Si limita a coordinare le operazioni.

Questa separazione rende il codice più pulito e più semplice da testare. Ad esempio, nei test potresti sostituire il repository reale con una versione finta o in memoria.

Perché il pattern Repository è utile in OOP

  • Riduce l’accoppiamento tra logica applicativa e persistenza.
  • Migliora la testabilità, perché puoi simulare i dati facilmente.
  • Rende il codice più leggibile, spostando le query in un punto preciso.
  • Favorisce la manutenzione, perché cambiare database o sorgente dati richiede meno modifiche.

Best practice

Il pattern Repository è molto efficace, ma va usato con criterio. Ecco alcune pratiche consigliate per applicarlo bene in PHP.

  • Usa interfacce: definire un contratto separato dalla classe concreta ti permette di sostituire facilmente l’implementazione.
  • Non mischiare responsabilità: il repository deve occuparsi dei dati, il servizio della logica di business.
  • Evita repository troppo generici: una classe tipo GenericRepository spesso diventa difficile da mantenere. Meglio repository specifici per dominio, come UserRepository o OrderRepository.
  • Trasforma sempre i dati in oggetti: non lasciare array sparsi nel codice applicativo se puoi usare entità.
  • Prepara il codice ai test: il repository in memoria è un ottimo strumento per testare i servizi senza dipendenze esterne.
  • Usa tipi stretti: con declare(strict_types=1) e type hint chiari riduci errori e ambiguità.

Un errore comune è usare il repository come contenitore di tutta la logica possibile. In realtà il repository deve restare concentrato sull’accesso ai dati. Se inizi a inserire calcoli complessi o regole di business al suo interno, perdi il vantaggio della separazione dei ruoli.

Riepilogo

Il pattern Repository è uno strumento molto utile nella programmazione orientata agli oggetti in PHP, soprattutto quando vuoi scrivere codice più ordinato, testabile e facile da evolvere. L’idea è semplice: spostare l’accesso ai dati in una classe dedicata, lasciando alla logica applicativa il compito di orchestrare le operazioni.

Nel nostro esempio:

  • User rappresenta l’entità di dominio;
  • UserRepositoryInterface definisce il contratto;
  • InMemoryUserRepository fornisce una prima implementazione concreta;
  • UserService usa il repository senza conoscere i dettagli di persistenza.

Se impari a separare questi livelli, i tuoi progetti PHP diventeranno più robusti e più facili da mantenere nel tempo.

Approfondisci con risorse ufficiali

SHARE