Nello sviluppo Laravel moderno, l’adozione del pattern Service Layer è fondamentale per creare applicazioni pulite , scalabili e di facile manutenzione . Spostando la logica di business complessa fuori dai controller e dai modelli, si rispetta il principio di responsabilità unica (SRP), rendendo il codice più testabile e riutilizzabile.
Il perché: la separazione delle responsabilità
L’obiettivo principale di una Service Class è quello di fungere da livello dedicato per la logica di business della tua applicazione.
Il Service Layer Pattern è la soluzione migliore rispetto a inserire tutta la logica nel Controller perché elimina l’accoppiamento forte tra il protocollo di comunicazione (HTTP) e le regole operative della tua applicazione (Business Logic).
Mentre il Controller è per sua natura un “addetto al traffico”, il Service è l’effettivo “esecutore” del lavoro.
Confronto Diretto: Controller vs Service:
| Caratteristica | Tutto nel Controller (Senza Service) | Con il Service Layer (Soluzione Migliore) |
| Punto di accesso | Solo richieste HTTP (Web/API). | Qualsiasi (HTTP, CLI, Job, Webhook). |
| Responsabilità | Troppe (Validazione, DB, Email, HTTP). | Una sola (Logica di business specifica). |
| Codice Duplicato | Elevato (Stessa logica ripetuta in più aree). | Zero (La logica è centralizzata nel Service). |
| Testabilità | Difficile (Richiede simulazione di richieste HTTP). | Facile (È una classe PHP pura). |
| Manutenibilità | Bassa (File enormi e difficili da leggere). | Alta (Classi piccole e isolate). |
Definizione del contratto (l’interfaccia)
Inizieremo definendo un’interfaccia completa che delinei tutte le operazioni CRUD necessarie. Ciò garantisce che il nostro servizio rispetti un contratto prevedibile.
app/Services/Contracts/ProductServiceInterface.php
<?php
namespace App\Services\Contracts;
use App\Models\Product;
use Illuminate\Database\Eloquent\Collection;
interface ProductServiceInterface
{
/**
* Recupera tutti i prodotti.
*
* @return Collection<Product>
*/
public function getAll(): Collection;
/**
* Recupera un singolo prodotto con l'ID.
*
* @param int $id
* @return Product|null
*/
public function find(int $id): ?Product;
/**
* Creare un nuovo prodotto.
*
* @param array $data
* @return Product
*/
public function create(array $data): Product;
/**
* Aggiornamento di un prodotto esistente.
*
* @param Product $product
* @param array $data
* @return bool
*/
public function update(Product $product, array $data): bool;
/**
* Cancellazione di un prodotto.
*
* @param Product $product
* @return bool|null
*/
public function delete(Product $product): ?bool;
}
Implementazione della classe Service
Ora creiamo la classe Service. È qui che risiederanno tutta la logica di business, la manipolazione dei dati e tutte le attività correlate (come la registrazione, l’invio di notifiche o la sincronizzazione con API esterne) per la risorsa Prodotto.
app/Services/ProductService.php
<?php
namespace App\Services;
use App\Models\Product;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
use App\Services\Contracts\ProductServiceInterface;
class ProductService implements ProductServiceInterface
{
// READ (All)
public function getAll(): Collection
{
return Product::latest()->get();
}
// READ (Single)
public function find(int $id): ?Product
{
return Product::find($id);
}
// CREATE
public function create(array $data): Product
{
// 1. Business Logic: trasformiamo l'SKU tutto in maiuscolo
$data['sku'] = strtoupper($data['sku']);
// 2. Persistenza del dato
$product = Product::create($data);
// 3. Log dell'azione appena effettuata
Log::info("Product created: {$product->name} (ID: {$product->id})");
return $product;
}
// UPDATE
public function update(Product $product, array $data): bool
{
// 1. Business Logic: Verifica se il prodotto è aumentato in modo significativo
if (isset($data['price']) && $data['price'] > ($product->price * 1.5)) {
// Log
Log::warning("È stato rilevato un aumento di prezzo significativo per l'ID prodotto: {$product->id}");
}
// 2. Persistenza del dato
$result = $product->update($data);
// 3. Log post scrittura
if ($result) {
Log::info("Prodotto aggiornato: {$product->name}");
}
return $result;
}
// DELETE
public function delete(Product $product): ?bool
{
// 1. Pre-Deletion Check (e.g.,
Impossibile eliminare il prodotto se è presente in un ordine attivo.)
if ($product->orders()->count() > 0) {
throw new \Exception("
Impossibile eliminare il prodotto se è presente in un ordine attivo..");
}
// 2. Persistenza del dato
$result = $product->delete();
// 3. Log dell'azione appena effettuata
if ($result) {
Log::info("Prodotto cancellato: {$product->name} (ID: {$product->id})");
}
return $result;
}
}
Vincolare il Service Provider
Come in precedenza, registriamo la nostra interfaccia e il binding della classe all’interno di un Service Provider.
app/Providers/AppServiceProvider.php(o un dedicato ProductServiceProvider)
<?php
use App\Services\Contracts\ProductServiceInterface;
use App\Services\ProductService;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind the Interface to the concrete Service Class
$this->app->bind(ProductServiceInterface::class, ProductService::class);
}
}
Il controller (snello)
Ora il controller è pulito, di facile lettura e completamente testabile perché delega tutto il lavoro più complesso all’injected service.
app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use App\Services\Contracts\ProductServiceInterface;
use App\Http\Requests\ProductStoreRequest;
use App\Http\Requests\ProductUpdateRequest;
class ProductController extends Controller
{
protected ProductServiceInterface $productService;
// Dependency Injection via constructor
public function __construct(ProductServiceInterface $productService)
{
$this->productService = $productService;
}
// 1. READ (All)
public function index()
{
// Delegate to Service
$products = $this->productService->getAll();
return view('products.index', compact('products'));
}
// 2. READ (Single)
public function show(Product $product)
{
// Possiamo utilizzare il route model binding per il recupero iniziale,
//oppure utilizzare $this->productService->find($id)
//se effettuiamo il recupero solo tramite ID.
return view('products.show', compact('product'));
}
// 3. CREATE (Store)
public function store(ProductStoreRequest $request)
{
// Delega la creazione e la logica aziendale al servizio
$product = $this->productService->create($request->validated());
return redirect()->route('products.show', $product)
->with('success', 'Product created successfully.');
}
// 4. UPDATE
public function update(ProductUpdateRequest $request, Product $product)
{
// Delega la logica di aggiornamento al servizio
$this->productService->update($product, $request->validated());
return redirect()->route('products.show', $product)
->with('success', 'Product updated successfully.');
}
// 5. DELETE
public function destroy(Product $product)
{
// Delega la cancellazione e la pulizia al servizio
$this->productService->delete($product);
return redirect()->route('products.index')
->with('success', 'Prodotto eliminato con successo.');
}
}
Utilizzando questo modello di contratto di servizio, garantisci che la tua applicazione Laravel 12 rimanga robusta, manutenibile e altamente testabile, secondo lo standard professionale per lo sviluppo a livello aziendale.
Aut. Ercole Palmeri

