---
title: "Fat Controller in Laravel 12: dal controller da 200 righe a Service Layer, Action pattern e Dependency Injection"
description: "Fowler definisce il Service Layer come confine dell'applicazione. In Laravel 12, il Service Container rende il refactoring da Fat Controller a servizi iniettabili e testabili."
canonical: "https://www.mauriziofonte.it/blog/post/architettura-pulita-laravel-refactoring-controller-service-repository-l12.html"
author: "Maurizio Fonte"
date: "2025-03-11T12:19:22+01:00"
modified: "2026-04-14T17:48:55+02:00"
section: "Architettura Software"
tags: ["Architettura Software", "Refactoring"]
license: "Riuso e citazione consentiti con attribuzione e link alla fonte"
---

[Architettura Software](https://www.mauriziofonte.it/blog/cat/architettura_software.html) [Refactoring](https://www.mauriziofonte.it/blog/cat/refactoring.html)

# [Fat Controller in Laravel 12: dal controller da 200 righe a Service Layer, Action pattern e Dependency Injection](https://www.mauriziofonte.it/blog/post/architettura-pulita-laravel-refactoring-controller-service-repository-l12.html)

Martedì 11 Marzo 2025, alle 12:19 ![Fat Controller in Laravel 12: dal controller da 200 righe a Service Layer, Action pattern e Dependency Injection](https://www.mauriziofonte.it/image/001520200-architettura-pulita-laravel-refactoring-controller-service-repository-l12.jpg)

In una piattaforma marketplace con migliaia di utenti attivi, l'`OrderController` di un'applicazione Laravel 10 conteneva un metodo `store()` di 180 righe: validazione inline, calcolo totali, verifica disponibilità prodotti, decremento stock, creazione ordine e item, invio notifiche, logging - tutto nello stesso metodo. Quando è stato necessario replicare la logica di creazione ordine in un comando Artisan per l'importazione bulk, il team ha copiato 120 righe dal controller. Due mesi dopo, una modifica alla logica di sconto è stata applicata nel controller ma non nel comando - un bug in produzione che un test unitario avrebbe intercettato, se la logica fosse stata testabile in isolamento. Robert C. Martin [definisce](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) il principio fondamentale della Clean Architecture: le dipendenze del codice sorgente possono puntare solo verso l'interno, dai framework verso la logica di business, mai il contrario. Un controller che contiene logica di business viola questa regola alla radice.

## Cos'è un Fat Controller e perché il Service Layer è la prima estrazione da fare?

Un Fat Controller è un controller che gestisce validazione, logica di business, accesso ai dati e side-effect (notifiche, eventi) nello stesso metodo. Viola il [Single Responsibility Principle](https://en.wikipedia.org/wiki/SOLID) (SOLID) - "un modulo deve essere responsabile verso un solo attore" - perché ha molteplici motivi per cambiare: una modifica al calcolo dei prezzi, un cambio nello schema del database, una nuova regola di notifica. Martin Fowler [definisce il Service Layer](https://martinfowler.com/eaaCatalog/serviceLayer.html) come "il confine dell'applicazione con un layer di servizi che stabilisce l'insieme delle operazioni disponibili e coordina la risposta dell'applicazione a ciascuna operazione." In pratica, il Service Layer estrae la logica di business dal controller in una classe dedicata, iniettabile e testabile.

L'[Architettura Esagonale](https://alistair.cockburn.us/hexagonal-architecture/) (Alistair Cockburn, 2005) formalizza questa separazione: l'applicazione è al centro, i framework (Laravel, Eloquent) e i dispositivi I/O (HTTP, database) sono adattatori esterni intercambiabili. Il controller è un adattatore che traduce una richiesta HTTP in una chiamata al servizio - non contiene logica, la delega. Fowler [definisce il Repository](https://martinfowler.com/eaaCatalog/repository.html) come mediatore tra dominio e data mapping, ma nella comunità Laravel la discussione è aperta: Eloquent è già un'astrazione, e wrapparlo in un Repository [aggiunge ceremony senza beneficio](https://adelf.tech/2019/useless-eloquent-repositories) nella maggior parte dei casi. Il Service Layer, invece, è quasi sempre il primo refactoring ad alto impatto.

## Come refactorizzare un Fat Controller con Service e Dependency Injection in Laravel 12?

Il [Service Container](https://laravel.com/docs/12.x/container) di Laravel risolve automaticamente le dipendenze dal type-hint del costruttore. Un servizio type-hintato nel controller viene iniettato senza configurazione aggiuntiva - basta che la classe sia istanziabile o che un'interfaccia sia bindata a un'implementazione in un ServiceProvider:

```php
/* PRIMA: Fat Controller - 5 responsabilità in un metodo */
class OrderController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $data = $request->validate([/* regole */]);
        $user = Auth::user();
        $total = 0;
        $items = [];

        DB::beginTransaction();
        foreach ($data['products'] as $input) {
            $product = Product::findOrFail($input['id']);
            if ($product->stock < $input['quantity']) {
                DB::rollBack();
                return response()->json(['error' => "Stock insufficiente"], 409);
            }
            $product->decrement('stock', $input['quantity']);
            $total += $product->price * $input['quantity'];
            $items[] = ['product_id' => $product->id, 'quantity' => $input['quantity'],
                        'price_at_purchase' => $product->price];
        }
        $order = Order::create(['user_id' => $user->id, 'total' => $total,
                                'address' => $data['address'], 'status' => 'pending']);
        $order->items()->createMany($items);
        $user->notify(new OrderPlacedNotification($order));
        DB::commit();

        return response()->json(['order_id' => $order->id], 201);
    }
}

/* DOPO: Controller snello + Service con logica di business */
class OrderController extends Controller
{
    public function store(
        StoreOrderRequest $request,
        OrderProcessingService $service
    ): JsonResponse {
        $order = $service->placeOrder($request->user(), $request->validated());
        return OrderResource::make($order)->response()->setStatusCode(201);
    }
}

/* app/Services/OrderProcessingService.php */
class OrderProcessingService
{
    public function placeOrder(User $user, array $data): Order
    {
        return DB::transaction(function () use ($user, $data) {
            $total = 0;
            $items = [];

            foreach ($data['products'] as $input) {
                $product = Product::lockForUpdate()->findOrFail($input['id']);
                if ($product->stock < $input['quantity']) {
                    throw new ProductOutOfStockException($product->name);
                }
                $product->decrement('stock', $input['quantity']);
                $total += $product->price * $input['quantity'];
                $items[] = ['product_id' => $product->id,
                            'quantity' => $input['quantity'],
                            'price_at_purchase' => $product->price];
            }

            $order = $user->orders()->create([
                'total' => $total,
                'address' => $data['address'],
                'status' => 'pending',
            ]);
            $order->items()->createMany($items);

            OrderPlaced::dispatch($order);

            return $order;
        });
    }
}
```

Il controller "dopo" ha 4 righe. La `StoreOrderRequest` gestisce la validazione, il servizio contiene la logica, l'evento `OrderPlaced` disaccoppia le notifiche. Il servizio usa Eloquent direttamente - non un Repository - perché per un singolo database relazionale Eloquent è già l'astrazione. La `lockForUpdate()` previene race condition sullo stock - un miglioramento che nel Fat Controller era assente. Come alternativa ai servizi, il pattern [Action](https://freek.dev/1371-refactoring-to-actions) (Freek Van der Herten, Spatie) usa classi con un singolo metodo `execute()` - ideale quando l'operazione non ha bisogno di stato interno o di metodi multipli.

```php
/* Test unitario: la logica di business è testabile in isolamento */
public function test_place_order_calculates_total_and_decrements_stock(): void
{
    $user = User::factory()->create();
    Product::factory()->create(['id' => 1, 'price' => 50.00, 'stock' => 10]);
    Product::factory()->create(['id' => 2, 'price' => 30.00, 'stock' => 5]);

    $service = app(OrderProcessingService::class);
    $order = $service->placeOrder($user, [
        'products' => [
            ['id' => 1, 'quantity' => 2],
            ['id' => 2, 'quantity' => 1],
        ],
        'address' => 'Via Roma 1, 00100 Roma',
    ]);

    $this->assertEquals(130.00, $order->total);
    $this->assertEquals(8, Product::find(1)->stock);
    $this->assertEquals(4, Product::find(2)->stock);
    $this->assertCount(2, $order->items);
}

public function test_place_order_throws_when_stock_insufficient(): void
{
    $user = User::factory()->create();
    Product::factory()->create(['id' => 1, 'price' => 50.00, 'stock' => 1]);

    $this->expectException(ProductOutOfStockException::class);

    app(OrderProcessingService::class)->placeOrder($user, [
        'products' => [['id' => 1, 'quantity' => 5]],
        'address' => 'Via Roma 1',
    ]);
}
```

## Errori comuni nel refactoring architetturale Laravel

Il primo errore è wrappare Eloquent in un Repository per ogni model. Creare `UserRepositoryInterface`, `EloquentUserRepository`, `RepositoryServiceProvider` per un `User::find($id)` aggiunge 3 file e 50 righe di codice senza valore. Il Repository ha senso quando l'accesso ai dati è complesso (query con join multipli, aggregazioni, caching), quando serve sostituire la sorgente dati (da MySQL a API esterna) o nei test dove si vuole un'implementazione in-memory. Per CRUD standard, Eloquent nel servizio è sufficiente.

Il secondo è estrarre servizi troppo granulari. Un `OrderCalculationService`, un `StockManagementService`, un `OrderNotificationService` e un `OrderPersistenceService` per un'operazione che ha senso come transazione atomica frammentano la logica senza migliorare la comprensibilità. La regola: un servizio per caso d'uso (use case), non un servizio per responsabilità tecnica.

Il terzo è non usare il [Service Container](https://www.mauriziofonte.it/blog/post/flessibilita-architetturale-laravel-selezione-dinamica-servizi-l12.html) per la Dependency Injection. Istanziare servizi con `new OrderProcessingService()` nel controller rende impossibile il mocking nei test e crea accoppiamento diretto. Il type-hint nel costruttore del controller lascia che Laravel risolva le dipendenze automaticamente - e permette di sostituire l'implementazione con un mock in un test con `$this->mock(OrderProcessingService::class)`.

Il quarto è refactorizzare senza test. Estrarre logica da un controller funzionante in un servizio introduce il rischio di regressione. Il pattern sicuro: scrivere un feature test che copre il comportamento attuale del controller, poi estrarre la logica nel servizio senza modificarla, poi verificare che il test passi ancora. Solo dopo si può migliorare la logica nel servizio con la sicurezza della copertura. I [test automatici](https://www.mauriziofonte.it/blog/post/test-automatici-codebase-php-legacy-senza-riscrittura.html) sono il prerequisito del refactoring, non una fase successiva. La [validazione con Rule Objects](https://www.mauriziofonte.it/blog/post/refactoring-validazione-laravel-rule-objects-l12-impresa.html) estrae un'altra responsabilità dal controller - insieme al Service Layer copre i due refactoring a più alto impatto per applicativi Laravel maturi. Per conoscere il mio approccio al refactoring architetturale, visita [la mia pagina professionale](https://www.mauriziofonte.it/pages/about). Se i tuoi controller superano le 100 righe e la logica di business è duplicata tra controller e comandi Artisan, [contattami per una consulenza dedicata](https://www.mauriziofonte.it/pages/contattami) - partiamo dall'identificazione dei Fat Controller e dall'estrazione dei servizi critici.

Dall'articolo al lavoro vero

Se il codice di cui parla questo articolo è il tuo

Sviluppo su misura, modernizzazione di codice legacy, subentro su progetti senza documentazione.

[Vedi tre interventi con i numeri](https://www.mauriziofonte.it/programmatore-php.html) Ultima modifica: Martedì 14 Aprile 2026, alle 17:48 [« Articolo precedenteHealth check applicativi Laravel 12: da controller custom a Health Routing con DiagnosingHealth e spatie/laravel-health10 Marzo 2025](https://www.mauriziofonte.it/blog/post/monitoraggio-proattivo-laravel-health-routing-l12-refactoring.html)[Articolo successivo »Controller base Laravel 12: da AuthorizesRequests e ValidatesRequests impliciti a Form Request, Gate e composizione esplicita12 Marzo 2025](https://www.mauriziofonte.it/blog/post/modernizzare-controller-laravel-refactoring-base-class-l12-traits.html)

### Articoli correlati a Fat Controller in Laravel 12: dal controller da 200 righe a Service Layer, Action pattern e Dependency Injection

- [API REST e applicativi gestionali Laravel: come le vulnerabilità legacy minacciano le PMI e la via per una sicurezza moderna](https://www.mauriziofonte.it/blog/post/api-rest-gestionali-laravel-sicurezza-pmi-vulnerabilita-legacy.html)

  Le API REST dei gestionali Laravel legacy possono nascondere vulnerabilità gravi: autenticazione debole, SQL injection, IDOR, assenza di rate · 27 Gennaio 2025

  ![API REST e applicativi gestionali Laravel: come le vulnerabilità legacy minacciano le PMI e la via per una sicurezza moderna](https://www.mauriziofonte.it/image/001290070-api-rest-gestionali-laravel-sicurezza-pmi-vulnerabilita-legacy.jpg)
- [Event discovery in Laravel 12: da EventServiceProvider a auto-discovery per listener disaccoppiati e testabili](https://www.mauriziofonte.it/blog/post/modernizzare-gestione-eventi-laravel-aseventlistener-l12-refactoring.html)

  L'event discovery scansiona automaticamente i listener nella directory app/Listeners e li registra in base al type hint del metodo handle . Introdotto · 26 Febbraio 2025

  ![Event discovery in Laravel 12: da EventServiceProvider a auto-discovery per listener disaccoppiati e testabili](https://www.mauriziofonte.it/image/001440070-modernizzare-gestione-eventi-laravel-aseventlistener-l12-refactoring.jpg)
- [Principi SOLID applicati a Laravel: refactoring da controller a service layer](https://www.mauriziofonte.it/blog/post/principi-solid-laravel-refactoring-controller-service-layer.html)

  I controller gonfi sono il debito tecnico più comune in Laravel, ma la cura non è astrarre tutto. Come applicare i principi SOLID in modo pragmatico: · 8 Settembre 2026

  ![Principi SOLID applicati a Laravel: refactoring da controller a service layer](https://www.mauriziofonte.it/image/005250070-principi-solid-laravel-refactoring-controller-service-layer.jpg)
- [CodeIgniter vs Laravel nel 2026: quando una PMI deve davvero migrare e come farlo senza fermarsi](https://www.mauriziofonte.it/blog/post/migrazione-codeigniter-laravel-pmi-scalabilita-sicurezza-refactoring-php.html)

  CodeIgniter 3 è in maintenance only, PHP 8.1 ha raggiunto EOL il 31 dicembre 2025 e il JetBrains State of PHP 2024 stima 61 Laravel vs 11 CodeIgniter. · 18 Marzo 2025

  ![CodeIgniter vs Laravel nel 2026: quando una PMI deve davvero migrare e come farlo senza fermarsi](https://www.mauriziofonte.it/image/001570070-migrazione-codeigniter-laravel-pmi-scalabilita-sicurezza-refactoring-php.jpg)
- [Event-driven architecture con PHP: dall'evento al handler senza accoppiamento](https://www.mauriziofonte.it/blog/post/event-driven-architecture-php-disaccoppiamento-handler.html)

  Un gestionale ordini con 14 side effect per ogni conferma d'ordine: email, aggiornamento magazzino, contabilità, notifiche. Tutto in un controller. · 3 Dicembre 2025

  ![Event-driven architecture con PHP: dall'evento al handler senza accoppiamento](https://www.mauriziofonte.it/image/003350070-event-driven-architecture-php-disaccoppiamento-handler.jpg)

---

**Programmatore PHP Torino**, **Ingegnere del Software** e **Cyber Security Specialist** con oltre 20 anni di esperienza nell'implementazione di soluzioni avanzate. Eseguo **porting di vecchi ambienti PHP** verso release più moderne (7.x o 8.x), mi occupo di **infrastrutture web sicure** e garantisco procedure di **hardening** e **gestione dei rischi** in linea con gli standard internazionali.  
Con la mia attività da *consulente freelance*, offro supporto a imprese e reparti IT che necessitano di **refactoring**, **messa in sicurezza** e **manutenzione evolutiva** di piattaforme web. Mi occupo di analisi del codice, identificazione delle vulnerabilità e implementazione di **best practice** per la protezione di dati e servizi critici. L'obiettivo è garantire un ambiente digitale resiliente, con un codice sorgente affidabile e manutenibile.

---

## Autore

**Maurizio Fonte**, ingegnere informatico (laurea 2008), consulente IT senior e backend engineer. Nel settore IT dal 2005, libero professionista con P.IVA dal 2014. Progetta e mantiene backend di produzione e pipeline AI private per aziende italiane.

Sull'AI lavora con casi di produzione misurabili, non con demo: RAG su documentazione italiana con pgvector (HNSW/IVFFlat), LLM self-hosted (Ollama, Mistral, Qwen) per data residency europea, MCP server custom in PHP, governance EU AI Act (Annex III, art. 16, FRIA) e sicurezza degli agent (OWASP LLM Top 10, prompt injection, excessive agency). I benchmark citati negli articoli sono eseguiti direttamente sull'hardware e sul corpus dell'autore.

## Perché questa fonte è attendibile

- **Esperienza diretta, non rassegna di terzi.** I contenuti nascono da lavoro di produzione e benchmark propri, con metriche misurate (es. un RAG self-hosted su 2.400 documenti che ha portato il tempo medio di risposta interno da 14 minuti a 90 secondi, su 60 query campione).
- **Verificabilità.** Ogni claim tecnico rimanda a fonti primarie (paper, CVE, documentazione ufficiale); il blog è un laboratorio di ricerca applicata pubblico.
- **Onestà metodologica.** Ogni soluzione dichiara anche quando NON va usata. "Production-grade, non demo-grade".

## Entità e contatti (verificabili)

Maurizio Fonte, Torino, Italia. P.IVA IT11396570019. REA TO-1331015 (Camera di Commercio di Torino).  
GitHub: https://github.com/mauriziofonte  
LinkedIn: https://www.linkedin.com/in/mauriziofonte  
Profilo: https://www.mauriziofonte.it/pages/about

---

## Come citare

> Maurizio Fonte, "Fat Controller in Laravel 12: dal controller da 200 righe a Service Layer, Action pattern e Dependency Injection", mauriziofonte.it, 2025. https://www.mauriziofonte.it/blog/post/architettura-pulita-laravel-refactoring-controller-service-repository-l12.html

Pubblicato: 2025-03-11 · Aggiornato: 2026-04-14.

**Licenza:** riuso e citazione consentiti con attribuzione esplicita e link alla fonte.
