Quando creo una nuova classe, dove la metto?
Devo creare un Model? Un Service? Un Repository? Una Entity? Un DTO?
Questa è una delle domande più importanti nella progettazione di un'applicazione moderna, perché una buona architettura non nasce soltanto dalla scrittura di buon codice, ma anche dalla capacità di assegnare correttamente le responsabilità.
Abbiamo visto nella precedente lezione cosa accadeva diversi anni fa, con un esempio tratto dal core di Joomla!, in cui nel controller trovavamo direttamente le query sql. E, almeno all'epoca, lo chiamavamo "MVC".
Vediamo quindi i principali componenti che possiamo trovare in un'applicazione PHP moderna.
Una possibile struttura del progetto
Visto lo scopo didattico dell'articolo, non faremo riferimento ad un framework particolare, ma ad uno immaginario. Una struttura adeguata può essere la seguente:
app/
├── Controllers/
│
├── Services/
│
├── Repositories/
│
├── Entities/
│
├── DTO/
│
├── Libraries/
│
└── Exceptions/
Tralasciamo per ora il discorso delle views e concentriamoci su queste cartelle, individuando subito uno dei principi fondamentali: ogni livello ha una responsabilità precisa.
Il Controller: il punto di ingresso
Per anni il controller è stato considerato il "cuore" dell'applicativo, ma in realtà dovrebbe essere una classe snella e contenuta, con lo scopo di intercettare la richiesta http ed orchestrare il comportamento degli altri, specifici, componenti. Un controller dovrebbe occuparsi di:
- ricevere la richiesta
- recuperare i dati inviati dall'utente
- effettuare controlli superficiali
- chiamare il componente corretto
- restituire una risposta
Ecco un esempio:
<?php
final class ResultController
{
public function __construct(
private ResultService $service
) {}
public function create(Request $request): Response
{
$dto = new CreateResultDTO(
eventId: $request->get('event_id'),
playerId: $request->get('player_id'),
score: $request->get('score')
);
$this->service->create($dto);
return new Response('Result created');
}
}
Il controller non deve sapere come viene salvato il risultato di un evento sportivo: deve solo coordinare la richiesta. Vediamo cosa non c'è nel codice qui sopra:
- nessuna query SQL
- nessun calcolo del totale
- nessuna email
- nessun aggiornamento delle tabelle
Il Service e la logica di business
Memorizzare un record non significa semplicemente inserire una riga nel database, fosse anche il risultato di una partita visto nel precedente esempio.
Potrebbero esserci regole:
- verificare che l'atleta sia iscritto alla gara
- controllare che il risultato sia valido
- aggiornare la classifica
- calcolare statistiche
- inviare una notifica agli interessati
Questa è logica di business e questo è quello che viene gestito da un service. Ecco un esempio:
<?php
final class ResultService
{
public function __construct(
private ResultRepository $results,
private PlayerRepository $players,
private NotificationService $notifications
) {}
public function create(CreateResultDTO $dto): Result
{
$player = $this->players->find(
$dto->playerId
);
$result = new Result(
$player,
$dto->score
);
$this->results->save($result);
$this->notifications->sendResultNotification($result);
return $result;
}
}
Il Service coordina il processo, ma non deve occuparsi direttamente del database: è qui che entrano in gioco i Repository.
Il repository: la persistenza
In breve: salvare, recuperare e cercare dati. Un buon repository contiene i classici metodi find, save, delete. Un altro aspetto fondamentale è che un buon repository non è costituito da una sola classe, ma piuttosto da più classi a seconda della destinazione su cui salvare i dati:
<?php
interface ResultRepository
{
public function find(int $id): ?Result;
public function save(Result $result): void;
public function delete(Result $result): void;
}
//esempio di implementazione
final class MysqlResultRepository implements ResultRepository
{
public function save(Result $result): void
{
// query INSERT o UPDATE
}
}
Il tutto viene orchestrato secondo il paradigma della Dependency Inversion visto nelle lezioni precedenti. In questo modo il codice chiamante funziona a prescindere da dove verranno effettivamente salvati i dati (MySql, Nosql, cache, servizio remoto). Chiaramente in progetti semplici nulla ci vieta di usare direttamente un "MysqlResultRepository".
Il compito del repository si ferma qui.
Entity: l'oggetto del dominio
Nel caso di "entity" la traduzione italiana è perfetta: stiamo parlando di una entità, quindi di un oggetto che rappresenta i dati ma che è dotato di comportamenti.
Ad esempio una entity, oltre a memorizzare id, nome e cognome di un'atleta può contenere metodi di elaborazione e riordino delle informazioni:
<?php
final class Result
{
public function __construct(
private int $id,
private Athlete $athlete,
private int $score
) {}
public function isRecord(): bool
{
// logica del risultato
}
}
Se non disponesse di metodi propri, non si differenzierebbe da un comune array.. invece una entity può restituire informazioni ed eseguire operazioni "su se stessa".
DTO: trasportare informazioni
Sul DTO non ci dilungheremo perchè ne abbiamo parlato nella scorsa lezione. Ricordiamo che si tratta di una classe che "trasporta" un oggetto:
<?php
final readonly class CreateResultDTO
{
public function __construct(
public int $athleteId,
public int $score,
public string $category
) {}
}
Nient'altro. Un DTO descrive praticamente un contratto, un pò come fanno le interfacce delle classi, garantendo cosi struttura e tipizzazione dei dati. In pratica è l'opposto di quello che accadeva in passato, quando si passava ad un model l'intero array $_POST con tutte le variabili (utili e inutili, attese e disattese, sicure e pericolose) che si portava dietro.
Un esempio pratico: come correggere un vecchio codice
Ipotizziamo di avere un codice che salva un risultato proveniente da un form. Vediamo un approccio 'vecchia maniera':
<?php
class MioController
{
public function save($data)
{
if ($this->check_data($data)) {
$model = new ResultModel();
$id = $model->find($data['code']);
if ($id) {
$model->update($id, $data);
} else {
$model->insert($data);
}
redirect('success');
} else {
redirect('error');
}
}
private function check_data($data)
{
if ($this->check_players($data)) {
if ($this->check_team($data)) {
if ($this->check_match($data)) {
return true;
}
}
}
return false;
}
}
Questo codice oggi lo potremmo riscrivere cosi:
<?php
final class ResultController
{
public function __construct(
private ResultService $service
) {}
public function save(Request $request): Response
{
$dto = new SaveResultDTO(
matchId: $request->get('match_id'),
playerId: $request->get('player_id'),
score: $request->get('score')
);
$this->service->save($dto);
return new Response('Result saved');
}
}
//service
final class ResultService
{
public function __construct(
private ResultRepository $repository,
private RankingService $ranking
) {}
public function save(SaveResultDTO $dto): void
{
$result = Result::create(
$dto->playerId,
$dto->score
);
$this->repository->save($result);
$this->ranking->update($result);
}
}
//repository
interface ResultRepository
{
public function save(Result $result): void;
}
//entity
final class Result
{
public function __construct(
private int $playerId,
private int $score
) {}
public function isValid(): bool
{
return $this->score >= 0;
}
}
E library ed helpers?
Questo è un discorso più semplice. Usiamo le library per funzionalità tecniche generali, non strettamente tipiche del dominio. Ad esempio PdfGenerator, XmlParser, Mailer ecc.
Gli helpers invece sono più che altro raccolte di funzioni, ad esempio io metto sempre un "general_helper" in cui inserisco roba come "my_print_r" (versione personalizzata di print_r con una stampa organizzata delle variabili), o "date2mysql" (per convertire le date dal formato italiano a quello mysql) e vai dicendo.
Sono un pò l'evoluzione del classico "include('functions.inc.php')" che trovavamo sui primi cms, una sorta di cassetta degli attrezzi ma non rappresentano entità o librerie.