Architettura Riuso Codice: Manuale vs Automatico

Architettura Riuso Codice: Manuale vs Automatico

🎯 Principio Fondamentale

La business logic vive nei Services, non nei Controller o Jobs.

Controller e Jobs sono solo trigger diversi che chiamano gli stessi Services.

---

πŸ—οΈ Layer Architetturale

``

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PRESENTATION LAYER (Trigger)                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚   CONTROLLER     β”‚              β”‚      JOB         β”‚    β”‚
β”‚  β”‚   (Manuale)      β”‚              β”‚   (Automatico)   β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚           β”‚                                  β”‚              β”‚
β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                          β”‚                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  BUSINESS LOGIC LAYER (Services)                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvScannerService                                      β”‚β”‚
β”‚  β”‚   β†’ scanForNewCsvs()                                    β”‚β”‚
β”‚  β”‚   β†’ registerCsv($file, $machine, $profile)             β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvValidatorService                                    β”‚β”‚
β”‚  β”‚   β†’ validate($csvImport): ValidationResult             β”‚β”‚
β”‚  β”‚   β†’ checkColumns($csv, $profile)                       β”‚β”‚
β”‚  β”‚   β†’ checkDataTypes($csv, $profile)                     β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvProcessorService                                    β”‚β”‚
β”‚  β”‚   β†’ process($csvImport): ProcessingResult              β”‚β”‚
β”‚  β”‚   β†’ importRows($csv, $profile)                         β”‚β”‚
β”‚  β”‚   β†’ handleErrors($errors)                              β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvCleanupService                                      β”‚β”‚
β”‚  β”‚   β†’ cleanup($csvImport)                                β”‚β”‚
β”‚  β”‚   β†’ archive($csvImport)                                β”‚β”‚
β”‚  β”‚   β†’ deleteFile($csvImport)                             β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  DATA LAYER (Models)                                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  CsvImport, Machine, ImportProfile                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
`

---

πŸ’» Esempio Concreto: Validazione

Service (Core Logic - UNICO)

`php namespace App\Services\I40;

class CsvValidatorService { public function validate(CsvImport $csvImport): ValidationResult { // Logica IDENTICA sia per manuale che automatico $profile = ImportProfile::find($csvImport->profile_id); $filePath = Storage::path($csvImport->file_path); $errors = []; // 1. Verifica file esista if (!Storage::exists($csvImport->file_path)) { return new ValidationResult(false, ['File non trovato']); } // 2. Leggi CSV try { $csv = Reader::createFromPath($filePath); $csv->setHeaderOffset($profile->has_header ? 0 : null); } catch (\Exception $e) { return new ValidationResult(false, ['Errore lettura CSV: ' . $e->getMessage()]); } // 3. Verifica colonne $headers = $csv->getHeader(); $expectedColumns = array_column($profile->columns, 'name'); foreach ($expectedColumns as $col) { if (!in_array($col, $headers)) { $errors[] = "Colonna mancante: {$col}"; } } // 4. Conta righe $rowsCount = count($csv); // 5. Verifica tipi dati (campione prime 10 righe) // ... logica validazione tipi ... return new ValidationResult( isValid: empty($errors), errors: $errors, rowsCount: $rowsCount ); } }

// Value Object per il risultato class ValidationResult { public function __construct( public bool $isValid, public array $errors, public int $rowsCount = 0 ) {} } `

---

Controller (Trigger Manuale)

`php namespace App\Http\Controllers\Admin\I40;

class CsvImportsController extends Controller { public function __construct( protected CsvValidatorService $validator, protected CsvProcessorService $processor ) {} / * Valida CSV manualmente */ public function validate(CsvImport $csvImport) { // Verifica permessi, stato, etc. if (!in_array($csvImport->status, ['pending', 'failed'])) { return back()->withErrors(['error' => 'CSV non in stato validabile']); } // Aggiorna stato $csvImport->update(['status' => 'validating']); try { // ⭐ CHIAMATA AL SERVICE (STESSO CODICE) $result = $this->validator->validate($csvImport); // Aggiorna con risultato $csvImport->update([ 'status' => $result->isValid ? 'validated' : 'failed', 'is_valid' => $result->isValid, 'validation_errors' => json_encode($result->errors), 'rows_total' => $result->rowsCount, 'validated_at' => now(), 'validated_by' => auth()->id() // ← MANUALE ]); $message = $result->isValid ? "CSV validato con successo! {$result->rowsCount} righe." : "Validazione fallita: " . count($result->errors) . " errori trovati."; return redirect() ->route('admin.i40.csv-imports.index') ->with($result->isValid ? 'success' : 'error', $message); } catch (\Exception $e) { $csvImport->update([ 'status' => 'failed', 'error_message' => $e->getMessage() ]); return back()->withErrors(['error' => 'Errore validazione: ' . $e->getMessage()]); } } / * Elabora CSV manualmente */ public function process(CsvImport $csvImport) { // Verifica sia validato if ($csvImport->status !== 'validated') { return back()->withErrors(['error' => 'CSV deve essere validato prima di elaborare']); } // Aggiorna stato $csvImport->update([ 'status' => 'processing', 'processing_started_at' => now(), 'processed_by' => auth()->id() // ← MANUALE ]); try { // ⭐ CHIAMATA AL SERVICE (STESSO CODICE) $result = $this->processor->process($csvImport); // Aggiorna con risultato $csvImport->update([ 'status' => 'completed', 'processing_completed_at' => now(), 'rows_processed' => $result->rowsProcessed, 'rows_failed' => $result->rowsFailed, 'import_result' => json_encode($result->stats) ]); return redirect() ->route('admin.i40.csv-imports.index') ->with('success', "Elaborazione completata! {$result->rowsProcessed} righe importate."); } catch (\Exception $e) { $csvImport->update([ 'status' => 'failed', 'error_message' => $e->getMessage(), 'processing_completed_at' => now() ]); return back()->withErrors(['error' => 'Errore elaborazione: ' . $e->getMessage()]); } } } `

---

Job (Trigger Automatico - FUTURO)

`php namespace App\Jobs\I40;

class ValidateCsvJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( protected int $csvImportId ) {} public function handle(CsvValidatorService $validator) { $csvImport = CsvImport::find($this->csvImportId); if (!$csvImport) { return; // CSV eliminato nel frattempo } // Aggiorna stato $csvImport->update(['status' => 'validating']); // ⭐ CHIAMATA AL SERVICE (STESSO CODICE DEL CONTROLLER!) $result = $validator->validate($csvImport); // Aggiorna con risultato $csvImport->update([ 'status' => $result->isValid ? 'validated' : 'failed', 'is_valid' => $result->isValid, 'validation_errors' => json_encode($result->errors), 'rows_total' => $result->rowsCount, 'validated_at' => now(), 'validated_by' => null // ← AUTOMATICO ]); // Se valido e auto-process abilitato if ($result->isValid && i40_setting('processing.auto_process_validated')) { // Verifica concurrent limit $running = CsvImport::where('status', 'processing')->count(); $limit = i40_setting('processing.concurrent_limit', 5); if ($running < $limit) { dispatch(new ProcessCsvJob($csvImport->id)); } } } } `

---

πŸ”„ Differenze Chiave

| Aspetto | Controller (Manuale) | Job (Automatico) | |---------|---------------------|------------------| | Service Call | $validator->validate($csv) | $validator->validate($csv) | | Logica | βœ… IDENTICA | βœ… IDENTICA | | User Tracking | auth()->id() | null | | Response | redirect()->with() | dispatch(NextJob) | | Esecuzione | Sincrona (attesa) | Asincrona (background) | | Error Handling | return back()->withErrors() | failed() method |

---

🎯 Transizione Manuale β†’ Automatico

FASE 1 (ORA): Tutto Manuale

`php // Controller public function validate($id) { $result = $this->validator->validate($csv); // Redirect } `

FASE 2 (FUTURO): Aggiungi Automazione

`php // Job (NUOVO) public function handle() { $result = $this->validator->validate($csv); // ← STESSO SERVICE! // Dispatch next }

// Controller (INVARIATO) public function validate($id) { $result = $this->validator->validate($csv); // ← STESSO SERVICE! // Redirect } `

Services NON cambiano! Zero duplicazione! πŸŽ‰

---

πŸ“¦ Struttura File (Con Riuso)

`

app/Services/I40/
β”œβ”€β”€ CsvScannerService.php       ← CORE LOGIC
β”œβ”€β”€ CsvValidatorService.php     ← CORE LOGIC ⭐
β”œβ”€β”€ CsvProcessorService.php     ← CORE LOGIC ⭐
└── CsvCleanupService.php       ← CORE LOGIC ⭐</p><p>app/Http/Controllers/Admin/I40/
└── CsvImportsController.php
    β”œβ”€β”€ scanNewCsvs()    β†’ CsvScannerService::scan()
    β”œβ”€β”€ validate($id)    β†’ CsvValidatorService::validate() ⭐
    β”œβ”€β”€ process($id)     β†’ CsvProcessorService::process() ⭐
    └── cleanup($id)     β†’ CsvCleanupService::cleanup() ⭐</p><p>app/Jobs/I40/ (FUTURE)
β”œβ”€β”€ ValidateCsvJob.php   β†’ CsvValidatorService::validate() ⭐
β”œβ”€β”€ ProcessCsvJob.php    β†’ CsvProcessorService::process() ⭐
└── CleanupCsvJob.php    β†’ CsvCleanupService::cleanup() ⭐
`

⭐ = Stesso metodo chiamato da 2 posti diversi!

---

βœ… Vantaggi

1. Zero Duplicazione

`php // ❌ SBAGLIATO (duplicazione) class Controller { public function validate() { // 100 righe logica validazione } } class Job { public function handle() { // STESSE 100 righe duplicate! } }

// βœ… CORRETTO (riuso) class CsvValidatorService { public function validate() { // 100 righe logica validazione UNA SOLA VOLTA } } class Controller { public function validate() { $this->validator->validate(); // ← Chiama service } } class Job { public function handle() { $this->validator->validate(); // ← Chiama STESSO service } } `

2. Manutenzione Semplice

  • Bug fix? Un solo punto da modificare
  • Nuova feature? Un solo service da aggiornare
  • Testing? Un solo service da testare
  • 3. Transizione Graduale

  • Parti manuale β†’ tutto funziona
  • Aggiungi Jobs β†’ codice rimane uguale
  • Attivi automazione β†’ zero refactoring
  • 4. FlessibilitΓ 

    `php // Utente puΓ² sempre bypassare automazione public function forceValidate($id) { // Anche se settings = auto, permetti validazione manuale $result = $this->validator->validate($csv); } `

    ---

    🎯 Implementazione Coda d'Import

    Ora Facciamo:

    1. βœ… Tabella csv_imports (SQL pronto) 2. βœ… Model CsvImport.php 3. βœ… Services: - CsvScannerService - Trova nuovi CSV - CsvValidatorService - Valida CSV - CsvProcessorService - Elabora CSV 4. βœ… Controller con metodi manuali: - scanNewCsvs() - Pulsante manuale - validate($id) - Pulsante manuale - process($id)` - Pulsante manuale 5. βœ… Vista con Bootstrap Table + pulsanti

    In Futuro (Quando Vorrai Automazione):

    1. βœ… Jobs (copiano i controller ma chiamano STESSI Services) 2. βœ… Queue Worker sul server 3. βœ… Scheduler per scansioni 4. βœ… Settings abilitano automazione

    Services rimangono IDENTICI! Zero refactoring! πŸŽ‰

    ---

    βœ… Conferma

    Approccio ti convince?

  • Services con tutta la logica βœ…
  • Controller come trigger manuali βœ…
  • Future Jobs riusano Services βœ…
  • Zero duplicazione codice βœ…

Procedo con implementazione Coda d'Import? πŸš€

---

Ultimo aggiornamento: 17 Ottobre 2025

Analisi Codice

Blocco 1
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PRESENTATION LAYER (Trigger)                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚   CONTROLLER     β”‚              β”‚      JOB         β”‚    β”‚
β”‚  β”‚   (Manuale)      β”‚              β”‚   (Automatico)   β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚           β”‚                                  β”‚              β”‚
β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                          β”‚                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  BUSINESS LOGIC LAYER (Services)                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvScannerService                                      β”‚β”‚
β”‚  β”‚   β†’ scanForNewCsvs()                                    β”‚β”‚
β”‚  β”‚   β†’ registerCsv($file, $machine, $profile)             β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvValidatorService                                    β”‚β”‚
β”‚  β”‚   β†’ validate($csvImport): ValidationResult             β”‚β”‚
β”‚  β”‚   β†’ checkColumns($csv, $profile)                       β”‚β”‚
β”‚  β”‚   β†’ checkDataTypes($csv, $profile)                     β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvProcessorService                                    β”‚β”‚
β”‚  β”‚   β†’ process($csvImport): ProcessingResult              β”‚β”‚
β”‚  β”‚   β†’ importRows($csv, $profile)                         β”‚β”‚
β”‚  β”‚   β†’ handleErrors($errors)                              β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  CsvCleanupService                                      β”‚β”‚
β”‚  β”‚   β†’ cleanup($csvImport)                                β”‚β”‚
β”‚  β”‚   β†’ archive($csvImport)                                β”‚β”‚
β”‚  β”‚   β†’ deleteFile($csvImport)                             β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  DATA LAYER (Models)                                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  CsvImport, Machine, ImportProfile                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Blocco 2 php
namespace App\Services\I40;

class CsvValidatorService
{
    public function validate(CsvImport $csvImport): ValidationResult
    {
        // Logica IDENTICA sia per manuale che automatico
        
        $profile = ImportProfile::find($csvImport->profile_id);
        $filePath = Storage::path($csvImport->file_path);
        
        $errors = [];
        
        // 1. Verifica file esista
        if (!Storage::exists($csvImport->file_path)) {
            return new ValidationResult(false, ['File non trovato']);
        }
        
        // 2. Leggi CSV
        try {
            $csv = Reader::createFromPath($filePath);
            $csv->setHeaderOffset($profile->has_header ? 0 : null);
        } catch (\Exception $e) {
            return new ValidationResult(false, ['Errore lettura CSV: ' . $e->getMessage()]);
        }
        
        // 3. Verifica colonne
        $headers = $csv->getHeader();
        $expectedColumns = array_column($profile->columns, 'name');
        
        foreach ($expectedColumns as $col) {
            if (!in_array($col, $headers)) {
                $errors[] = "Colonna mancante: {$col}";
            }
        }
        
        // 4. Conta righe
        $rowsCount = count($csv);
        
        // 5. Verifica tipi dati (campione prime 10 righe)
        // ... logica validazione tipi ...
        
        return new ValidationResult(
            isValid: empty($errors),
            errors: $errors,
            rowsCount: $rowsCount
        );
    }
}

// Value Object per il risultato
class ValidationResult
{
    public function __construct(
        public bool $isValid,
        public array $errors,
        public int $rowsCount = 0
    ) {}
}
Blocco 3 php
namespace App\Http\Controllers\Admin\I40;

class CsvImportsController extends Controller
{
    public function __construct(
        protected CsvValidatorService $validator,
        protected CsvProcessorService $processor
    ) {}
    
    /**
     * Valida CSV manualmente
     */
    public function validate(CsvImport $csvImport)
    {
        // Verifica permessi, stato, etc.
        if (!in_array($csvImport->status, ['pending', 'failed'])) {
            return back()->withErrors(['error' => 'CSV non in stato validabile']);
        }
        
        // Aggiorna stato
        $csvImport->update(['status' => 'validating']);
        
        try {
            // ⭐ CHIAMATA AL SERVICE (STESSO CODICE)
            $result = $this->validator->validate($csvImport);
            
            // Aggiorna con risultato
            $csvImport->update([
                'status' => $result->isValid ? 'validated' : 'failed',
                'is_valid' => $result->isValid,
                'validation_errors' => json_encode($result->errors),
                'rows_total' => $result->rowsCount,
                'validated_at' => now(),
                'validated_by' => auth()->id()  // ← MANUALE
            ]);
            
            $message = $result->isValid 
                ? "CSV validato con successo! {$result->rowsCount} righe."
                : "Validazione fallita: " . count($result->errors) . " errori trovati.";
            
            return redirect()
                ->route('admin.i40.csv-imports.index')
                ->with($result->isValid ? 'success' : 'error', $message);
                
        } catch (\Exception $e) {
            $csvImport->update([
                'status' => 'failed',
                'error_message' => $e->getMessage()
            ]);
            
            return back()->withErrors(['error' => 'Errore validazione: ' . $e->getMessage()]);
        }
    }
    
    /**
     * Elabora CSV manualmente
     */
    public function process(CsvImport $csvImport)
    {
        // Verifica sia validato
        if ($csvImport->status !== 'validated') {
            return back()->withErrors(['error' => 'CSV deve essere validato prima di elaborare']);
        }
        
        // Aggiorna stato
        $csvImport->update([
            'status' => 'processing',
            'processing_started_at' => now(),
            'processed_by' => auth()->id()  // ← MANUALE
        ]);
        
        try {
            // ⭐ CHIAMATA AL SERVICE (STESSO CODICE)
            $result = $this->processor->process($csvImport);
            
            // Aggiorna con risultato
            $csvImport->update([
                'status' => 'completed',
                'processing_completed_at' => now(),
                'rows_processed' => $result->rowsProcessed,
                'rows_failed' => $result->rowsFailed,
                'import_result' => json_encode($result->stats)
            ]);
            
            return redirect()
                ->route('admin.i40.csv-imports.index')
                ->with('success', "Elaborazione completata! {$result->rowsProcessed} righe importate.");
                
        } catch (\Exception $e) {
            $csvImport->update([
                'status' => 'failed',
                'error_message' => $e->getMessage(),
                'processing_completed_at' => now()
            ]);
            
            return back()->withErrors(['error' => 'Errore elaborazione: ' . $e->getMessage()]);
        }
    }
}
Blocco 4 php
namespace App\Jobs\I40;

class ValidateCsvJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public function __construct(
        protected int $csvImportId
    ) {}
    
    public function handle(CsvValidatorService $validator)
    {
        $csvImport = CsvImport::find($this->csvImportId);
        
        if (!$csvImport) {
            return; // CSV eliminato nel frattempo
        }
        
        // Aggiorna stato
        $csvImport->update(['status' => 'validating']);
        
        // ⭐ CHIAMATA AL SERVICE (STESSO CODICE DEL CONTROLLER!)
        $result = $validator->validate($csvImport);
        
        // Aggiorna con risultato
        $csvImport->update([
            'status' => $result->isValid ? 'validated' : 'failed',
            'is_valid' => $result->isValid,
            'validation_errors' => json_encode($result->errors),
            'rows_total' => $result->rowsCount,
            'validated_at' => now(),
            'validated_by' => null  // ← AUTOMATICO
        ]);
        
        // Se valido e auto-process abilitato
        if ($result->isValid && i40_setting('processing.auto_process_validated')) {
            // Verifica concurrent limit
            $running = CsvImport::where('status', 'processing')->count();
            $limit = i40_setting('processing.concurrent_limit', 5);
            
            if ($running < $limit) {
                dispatch(new ProcessCsvJob($csvImport->id));
            }
        }
    }
}
Blocco 5 php
// Controller
public function validate($id) {
    $result = $this->validator->validate($csv);
    // Redirect
}
Blocco 6 php
// Job (NUOVO)
public function handle() {
    $result = $this->validator->validate($csv);  // ← STESSO SERVICE!
    // Dispatch next
}

// Controller (INVARIATO)
public function validate($id) {
    $result = $this->validator->validate($csv);  // ← STESSO SERVICE!
    // Redirect
}
Blocco 7
app/Services/I40/
β”œβ”€β”€ CsvScannerService.php       ← CORE LOGIC
β”œβ”€β”€ CsvValidatorService.php     ← CORE LOGIC ⭐
β”œβ”€β”€ CsvProcessorService.php     ← CORE LOGIC ⭐
└── CsvCleanupService.php       ← CORE LOGIC ⭐

app/Http/Controllers/Admin/I40/
└── CsvImportsController.php
    β”œβ”€β”€ scanNewCsvs()    β†’ CsvScannerService::scan()
    β”œβ”€β”€ validate($id)    β†’ CsvValidatorService::validate() ⭐
    β”œβ”€β”€ process($id)     β†’ CsvProcessorService::process() ⭐
    └── cleanup($id)     β†’ CsvCleanupService::cleanup() ⭐

app/Jobs/I40/ (FUTURE)
β”œβ”€β”€ ValidateCsvJob.php   β†’ CsvValidatorService::validate() ⭐
β”œβ”€β”€ ProcessCsvJob.php    β†’ CsvProcessorService::process() ⭐
└── CleanupCsvJob.php    β†’ CsvCleanupService::cleanup() ⭐
Blocco 8 php
// ❌ SBAGLIATO (duplicazione)
class Controller {
    public function validate() {
        // 100 righe logica validazione
    }
}
class Job {
    public function handle() {
        // STESSE 100 righe duplicate!
    }
}

// βœ… CORRETTO (riuso)
class CsvValidatorService {
    public function validate() {
        // 100 righe logica validazione UNA SOLA VOLTA
    }
}
class Controller {
    public function validate() {
        $this->validator->validate(); // ← Chiama service
    }
}
class Job {
    public function handle() {
        $this->validator->validate(); // ← Chiama STESSO service
    }
}
Blocco 9 php
// Utente puΓ² sempre bypassare automazione
public function forceValidate($id)
{
    // Anche se settings = auto, permetti validazione manuale
    $result = $this->validator->validate($csv);
}