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 + pulsantiIn 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?
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);
}