services laravel Guida semplificata alle classi Services di Laravel 12
Illustrazione originale ispirata allo stile della Pop Art anni '80

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:

CaratteristicaTutto nel Controller (Senza Service)Con il Service Layer (Soluzione Migliore)
Punto di accessoSolo richieste HTTP (Web/API).Qualsiasi (HTTP, CLI, Job, Webhook).
ResponsabilitàTroppe (Validazione, DB, Email, HTTP).Una sola (Logica di business specifica).
Codice DuplicatoElevato (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