Introduzione
Quando sviluppiamo in PHP, non tutti i problemi meritano un’eccezione. In molti casi, soprattutto nei flussi applicativi frequenti, è più pulito restituire un risultato esplicito che dica chiaramente se un’operazione è andata a buon fine oppure no. Questo approccio è molto utile per gestire operazioni come autenticazione, calcolo di prezzi, invio email, chiamate a servizi esterni o validazioni di business.
In questo tutorial vediamo un sotto-argomento pratico e spesso sottovalutato: l’uso di un oggetto Result per affiancare le eccezioni nella gestione degli errori. L’idea è semplice: le eccezioni restano riservate a eventi davvero eccezionali, mentre gli errori attesi vengono rappresentati in modo esplicito e leggibile.
Questo stile migliora la manutenibilità del codice, riduce i try-catch sparsi ovunque e rende più chiaro il contratto di ogni metodo. È una soluzione molto utile nei progetti intermedi e si integra bene con architetture moderne, specialmente quando vuoi separare la logica di dominio dalla gestione tecnica degli errori.
Codice completo
<?php
declare(strict_types=1);
/**
* Esempio pratico: gestione degli errori con un oggetto Result.
* Le eccezioni vengono usate solo per errori davvero inattesi.
*/
final class Result
{
private function __construct(
private bool $success,
private mixed $value = null,
private ?string $error = null
) {}
public static function success(mixed $value = null): self
{
return new self(true, $value, null);
}
public static function failure(string $error): self
{
return new self(false, null, $error);
}
public function isSuccess(): bool
{
return $this->success;
}
public function isFailure(): bool
{
return !$this->success;
}
public function getValue(): mixed
{
if ($this->isFailure()) {
throw new RuntimeException(´Impossibile leggere il valore di un Result fallito.´);
}
return $this->value;
}
public function getError(): ?string
{
return $this->error;
}
}
final class Email
{
private function __construct(private string $value) {}
public static function fromString(string $email): Result
{
$email = trim($email);
if ($email === ´´) {
return Result::failure(´L’email non può essere vuota.´);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return Result::failure(´Formato email non valido.´);
}
return Result::success(new self($email));
}
public function value(): string
{
return $this->value;
}
}
final class PasswordPolicy
{
public static function validate(string $password): Result
{
$password = trim($password);
if (strlen($password) < 8) {
return Result::failure(´La password deve contenere almeno 8 caratteri.´);
}
if (!preg_match(´/[A-Z]/´, $password)) {
return Result::failure(´La password deve contenere almeno una lettera maiuscola.´);
}
if (!preg_match(´/[0-9]/´, $password)) {
return Result::failure(´La password deve contenere almeno un numero.´);
}
return Result::success(true);
}
}
final class UserRegistrationService
{
public function register(array $input): Result
{
try {
$emailResult = Email::fromString($input[´email´] ?? ´´);
if ($emailResult->isFailure()) {
return Result::failure($emailResult->getError());
}
$passwordResult = PasswordPolicy::validate($input[´password´] ?? ´´);
if ($passwordResult->isFailure()) {
return Result::failure($passwordResult->getError());
}
$email = $emailResult->getValue();
$password = $input[´password´];
// Simuliamo un errore inatteso di infrastruttura.
if (($input[´simulate_db_error´] ?? false) === true) {
throw new RuntimeException(´Database non raggiungibile.´);
}
// Qui normalmente salveremmo l’utente nel database.
// Per esempio:
// $this->userRepository->save($email, password_hash($password, PASSWORD_DEFAULT));
return Result::success([
´message´ => ´Utente registrato con successo.´,
´email´ => $email->value(),
]);
} catch (Throwable $e) {
// Le eccezioni vengono catturate solo per errori tecnici inattesi.
return Result::failure(´Errore interno durante la registrazione: ´ . $e->getMessage());
}
}
}
// ======================
// Uso del servizio
// ======================
$service = new UserRegistrationService();
$input = [
´email´ => ´[email protected]´,
´password´ => ´Password123´,
];
$result = $service->register($input);
if ($result->isSuccess()) {
echo $result->getValue()[´message´];
} else {
echo ´Errore: ´ . $result->getError();
} Spiegazione
Il cuore di questo approccio è la classe Result. Invece di restituire solo true o false, o di lanciare eccezioni per ogni problema, il metodo restituisce un oggetto che contiene tre informazioni:
- se l’operazione è riuscita;
- il valore prodotto, se disponibile;
- il messaggio di errore, se l’operazione è fallita.
Perché usare Result insieme alle eccezioni?
Le eccezioni sono perfette per problemi inattesi: database non disponibile, file mancanti, bug di programmazione, timeout di rete. Però non sono sempre la scelta migliore per gestire errori di business attesi, come:
- password troppo debole;
- email non valida;
- codice promozionale scaduto;
- utente già registrato.
In questi casi un oggetto Result rende il flusso più leggibile: il chiamante controlla esplicitamente il risultato e decide come reagire.
La classe Email
Il metodo Email::fromString() mostra un buon esempio di validazione orientata al risultato. Se l’input è vuoto o non è un’email valida, non lancia eccezioni: restituisce un fallimento con un messaggio chiaro. Se invece il dato è corretto, restituisce un oggetto Email già pronto all’uso.
La policy sulla password
La classe PasswordPolicy applica regole di business semplici ma realistiche. Anche qui il metodo validate() usa Result per indicare un eventuale errore. Questo è molto utile perché il chiamante può mostrare il messaggio all’utente senza dover gestire eccezioni per una regola prevedibile.
Il servizio di registrazione
UserRegistrationService combina i controlli. Prima valida email e password, poi simula un possibile errore tecnico con una eccezione. Questo passaggio è importante: mostra come il pattern Result non elimini le eccezioni, ma le usi in modo più mirato.
Se qualcosa va storto a livello infrastrutturale, il blocco try-catch intercetta l’errore e lo converte in un Result::failure(). In questo modo il contratto del metodo resta uniforme: chi chiama register() riceve sempre un oggetto Result da analizzare.
Vantaggi pratici
- Flusso più esplicito: non devi indovinare se un metodo lancia eccezioni oppure no.
- Codice più leggibile: gli errori attesi sono gestiti come parte normale del flusso.
- Controllo migliore: puoi distinguere tra errore di validazione e errore tecnico.
- API più prevedibili: il chiamante sa sempre cosa aspettarsi.
Best practice
- Usa le eccezioni per gli errori inattesi, non per ogni caso negativo.
- Usa Result per gli errori di dominio, cioè quelli che possono accadere normalmente durante l’uso dell’applicazione.
- Non restituire messaggi generici: un errore come “Operazione fallita” è poco utile. Meglio spiegare il motivo reale.
- Evita di leggere il valore di un Result fallito: nel codice di esempio,
getValue()lancia una RuntimeException se usato male. È una protezione utile durante lo sviluppo. - Non nascondere eccezioni importanti: se un errore tecnico va loggato o propagato, fallo in modo consapevole.
- Rendi i metodi coerenti: un metodo dovrebbe seguire sempre la stessa strategia di gestione errori, senza mescolare stili in modo casuale.
- Usa tipi e oggetti di dominio: invece di passare stringhe ovunque, incapsula regole e validazioni in classi dedicate.
Riepilogo
Gestire gli errori in PHP non significa usare sempre e solo try-catch. In molti casi, soprattutto quando l’errore è prevedibile, un oggetto Result è una soluzione più chiara e più adatta alla logica applicativa.
In questo tutorial abbiamo visto come:
- distinguere tra errori attesi e inattesi;
- usare una classe Result per rappresentare successo o fallimento;
- validare email e password in modo esplicito;
- limitare le eccezioni ai problemi tecnici reali.
Questo approccio migliora la leggibilità del codice e aiuta a costruire applicazioni più robuste, soprattutto quando il dominio cresce e i casi di errore aumentano.
Approfondisci con risorse ufficiali
- PHP Manual - Exceptions: documentazione ufficiale sulle eccezioni in PHP.
- PHP Manual - Throwable: interfaccia base per errori ed eccezioni catturabili.
- PHP Manual - filter_var(): utile per validazioni semplici e affidabili.
- PHP Manual - password_hash(): funzione consigliata per l’hashing sicuro delle password.
- PSR-3 Logger Interface: standard utile per integrare logging e gestione degli errori.
