Laravel Route Refactoring

Laravel Route Refactoring

Un codice ordinato è un codice più semplice da leggere e da mantenere. E da dove partire allora con il riordino se non da quello che potremmo definire l’indice del nostro progetto, ovvero le routes?

Questo articolo è stato scritto molto tempo fa. Potrebbe essere stato superato dalla tecnologia.

Premessa

In questo articolo ti mostrerò qualche buona regola da seguire per tenere in ordine le routes, soprattutto su progetti complessi, e renderle quindi più leggibili.

Resource route

Parto da un concetto molto semplice e che spero conoscerai e userai già. Usa per il più possibile i Resource Controller e dove non puoi usa il più possibile il suo modello, ovvero le sue 7 azioni: index, create, store, show, edit, update e destroy.

  • Index: solitamente è la rotta per la lista dei modelli presenti
  • Create: la vista per la creazione di un nuovo modello
  • Store: il metodo impiegato per salvare il nuovo record
  • Show: la vista per la visualizzazione di un singolo record
  • Edit: la vista per la modifica di un record
  • Update: il metodo per gestire il salvataggio delle modifiche ad un record
  • Destroy: il metodo per eliminare un record

Se in un controller usi un metodo che non risponde a una di queste azioni o ne stai usando più di 7, probabilmente dovresti estrarre dei metodi in un nuovo controller per rifarti alle solite “magnifiche 7”.

Vediamo un po’ di codice di esempio:

// Processes
// Come vedi ci sono le 7 azioni... o meglio 6, manca il destroy 
// ma ne abbiamo una "estranea": dashboard
Route::get('/admin/processes/', [ProcessController::class, 'index'])
	->name('processes.index');
Route::get('/admin/processes/create', [ProcessController::class, 'create'])
	->name('processes.create');
Route::post('/admin/processes/', [ProcessController::class, 'store'])
	->name('processes.store');
Route::get('/admin/processes/{process}', [ProcessController::class, 'show'])
	->name('processes.show');
Route::get('/admin/processes/{process}/edit', [ProcessController::class, 'edit'])
	->name('processes.edit');
Route::put('/admin/processes/{process}', [ProcessController::class, 'update'])
	->name('processes.update');

// Azione non standard
Route::get('/admin/processes/{process}/dashboard', [ProcessController::class, 'dashboard'])
	->name('processes.dashboard')

Vediamo come potremmo riscriverle:

// Processes
// Le 6 azioni standard le raggruppiamo in un resource controller
Route::resource('/admin/processes', ProcessController::class)->except(['destroy']);

// La rotta estranea la estraiamo in un nuovo controller con il solo index come metodo
Route::get('/admin/processes/dashboard', [ProcessDashboardController::class, 'index']);

Inutile dire che nel seguente modo è molto più leggibile. Visto che la rotta destroy non esiste, andiamo ad escluderla con il metodo except(). Esiste anche il suo opposto only() nel caso avessimo solo poche azioni. Per maggiori info, vi rimando alla documentazione ufficiale.

L'azione non standard l'ho estratta in un secondo controller ad hoc ProcessDashboardController dove esisterà il metodo index. Se esiste solo un metodo, potremmo semplificare ancora di più usando un controller invokable, così:

Route::resource('/admin/processes', ProcessController::class)->except(['destroy']);
Route::get('/admin/processes/dashboard', ProcessDashboardController::class);

// e il controller userà il magic method __invoke()
class ChooseLinkController extends Controller
{
    public function __invoke()
    {
        // code
    }
}

Route group, route prefix e middleware

Per il solito principio DRY, evita di ripeterti anche nelle rotte. Dove si fanno ripetizioni di solito? Sui prefissi per gli URL, sull’assegnazione dei middleware, sui nomi delle routes etc. Laravel mette a disposizione dei metodi ad hoc. Vediamoli nel pratico:

Route::get('/admin/processes/', [ProcessController::class, 'index'])
	->middleware('auth')
	->name('processes.index');
Route::get('/admin/processes/create', [ProcessController::class, 'create'])
	->middleware('auth')
	->name('processes.create');
Route::post('/admin/processes/', [ProcessController::class, 'store'])
	->middleware('auth')
	->name('processes.store');
...
Route::get('/admin/users/', [UserController::class, 'index'])
	->middleware('auth')
	->name('users.index');
Route::get('/admin/users/create', [UserController::class, 'create'])
	->middleware('auth')
	->name('users.create');
Route::post('/admin/users/', [UserController::class, 'store'])
	->middleware('auth')
	->name('users.store');
...

Come puoi vedere le ripetizioni sono parecchie. Vediamo come evitarle:

Route::middleware('auth')->prefix('admin')->group(function() {
	Route::prefix('process')->name('processes.')->group(function () {
		Route::get('/', [ProcessController::class, 'index'])->name('index');
		Route::get('/create', [ProcessController::class, 'create'])->name('create');
		Route::post('/', [ProcessController::class, 'store'])->name('store');
		...
	});

	Route::prefix('users')->name('users.')->group(function () {
		Route::get('/', [UserController::class, 'index'])->name('index');
		Route::get('/create', [UserController::class, 'create'])->name('create');
		Route::post('/', [UserController::class, 'store'])->name('store');
		...
	});
	...
});

Unendo i due "trucchi" appena visti otteniano questo codice:

Route::middleware('auth')->prefix('admin')->group(function() {
	Route::resource('/processes', ProcessController::class)->only(['index', 'create', 'store']);
	...
	Route::resource('/users', UserController::class)->only(['index', 'create', 'store']);
	...
});

Anche qui per informazioni più specifiche su group(), middleware(), prefix() etc, vi rimando alla documentazione ufficiale.

Split in più file

Il frammento di codice di prima potrebbe far parte di un file routes molto più arzigogolato e complesso. Ad esempio, potrebbe avere due sezioni principali, una per il frontend ed una per il backend. Come ad esempio:

/*
|--------------------------------------------------------------------------
| Backend
|--------------------------------------------------------------------------
*/

// Processes
Route::get('/admin/processes/', [ProcessController::class, 'index'])
	->name('processes.index');
Route::get('/admin/processes/create', [ProcessController::class, 'create'])
	->name('processes.create');
Route::post('/admin/processes/', [ProcessController::class, 'store'])
	->name('processes.store');
Route::get('/admin/processes/{process}', [ProcessController::class, 'show'])
	->name('processes.show');
Route::get('/admin/processes/{process}/edit', [ProcessController::class, 'edit'])
	->name('processes.edit');
Route::put('/admin/processes/{process}', [ProcessController::class, 'update'])
	->name('processes.update');
Route::delete('/admin/processes/{process}', [ProcessController::class, 'destroy'])
	->name('processes.destroy');

// Azione non standard
Route::get('/admin/processes/{process}/dashboard', [ProcessController::class, 'dashboard'])
	->name('processes.dashboard');

// Users
Route::get('/admin/users/', [UserController::class, 'index'])
	->name('users.index');
Route::get('/admin/users/create', [UserController::class, 'create'])
	->name('users.create');
Route::post('/admin/users/', [UserController::class, 'store'])
	->name('users.store');
Route::get('/admin/users/{user}', [UserController::class, 'show'])
	->name('users.show');
Route::get('/admin/users/{user}/edit', [UserController::class, 'edit'])
	->name('users.edit');
Route::put('/admin/users/{user}', [UserController::class, 'update'])
	->name('users.update');
Route::delete('/admin/users/{user}', [UserController::class, 'destroy'])
	->name('users.destroy');
...

/*
|--------------------------------------------------------------------------
| Frontend
|--------------------------------------------------------------------------
*/
Route::get('/', [PagesController::class, 'homepage'])
	->name('home');
Route::get('/contact-us', [PagesController::class, 'contact'])
	->name('contact');
Route::post('/contact-us', [PagesController::class, 'sendContact'])
	->name('contact');
Route::get('/thank-you', [PagesController::class, 'thanks'])
	->name('thankyou');

Route::get('/company/', [PagesController::class, 'about'])
	->name('about-us');
Route::get('/company/partners', [PagesController::class, 'getPartners'])
	->name('partner');
Route::get('/company/environment', [PagesController::class, 'getSocial'])
	->name('environment');
Route::get('/company/privacy', [PagesController::class, 'privacy'])
	->name('privacy');
Route::get('/company/cookie', [PagesController::class, 'cookie'])
	->name('cookie');
Route::get('/company/terms', [PagesController::class, 'terms'])
	->name('terms');

Route::get('/works/', [WorksController::class, 'index'])
	->name('works.index');
Route::get('/works/{work}', [WorksController::class, 'show'])
	->name('works.show');
...

Cosa fare quindi quando, per trovare una rotta, inizi ad usare la funzione cerca del tuo editor? Splittalo in più file!

Per prima cosa creiamo due file all’interno della cartella routes: uno frontend.php e uno backend.php e spostiamo le relative rotte nei file corrispondenti.

Fatto questo, andiamo a far sapere a Laravel che ad ogni richiesta, dovrà guardare anche in questi due nuovi file. Come si fa? Modificando il RouteServiceProvider.

RouteServiceProvider

All’interno del file situato, come tutti i providers, in app\Http\Providers, andiamo a creare due metodi simili a mapWebRoutes, il metodo che si occupa delle routes presenti in routes/web.php. Uno per ogni nuovo gruppo che abbiamo creato, ovvero frontend e backend.

/**
 * Define the "frontend" routes for the application.
 *
 * @return void
 */
protected function mapWebFrontendRoutes()
{
    Route::middleware('web')
        ->namespace($this->namespace)
        ->group( base_path('routes/frontend.php') );
}

/**
 * Define the "backend" routes for the application.
 *
 * @return void
 */
protected function mapWebBackendRoutes()
{
    Route::prefix('/admin')
    	->middleware(['web', 'auth'])
        ->namespace($this->namespace)
        ->group( base_path('routes/backend.php') );
}

Come vedi, nel metodo ho definito anche i middleware e il group che varrà per tutte le rotte del file ed il prefisso per le rotte del backend. Ovviamente le relative istruzioni all’interno dei file a questo punto andranno rimosse in quanto ridondanti. Vediamo come esempio il nostro backend.php:

/*
|--------------------------------------------------------------------------
| Backend
|--------------------------------------------------------------------------
*/

Route::resource('/processes', ProcessController::class)->except(['destroy']);
Route::get('/processes/dashboard', ProcessDashboardController::class);

Route::resource('/users', UserController::class);
...

Una volta sistemati i file e creati i due nuovi metodi, andiamo a registrarli nel metodo map() del service provider.

/**
 * Define the routes for the application.
 *
 * @return void
 */
public function map()
{
    $this->mapApiRoutes();

    $this->mapWebRoutes();
    $this->mapWebFrontendRoutes();
    $this->mapWebBackendRoutes();
}

Bene, questo è quanto. Spero ti sia utile e, se hai dubbi o altri metodi da consigliare per ottimizzare le nostre routes, scrivili nei commenti qui sotto!

Ultima modifica: mercoledì 19 gennaio 2022

Ancora nessun commento presente

Che ne dici di essere il primo?

Aggiungi il tuo commento

Iscriviti alla mia newsletter

Resterai informato sugli ultimi post, appena verranno pubblicati