Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

Why Laravel Doesn’t Make Me as Happy as Drupal: A Look at Web Route Definitions

By admin, 1 August, 2025

When working with Laravel and Drupal, I noticed something striking: defining web routes feels vastly different between the two. Laravel offers what feels like an endless number of ways to register routes, while Drupal provides a single, standardized approach. Let me break this down.


1. How Laravel Defines Web Routes

In Laravel, routes for the web layer are typically defined in the routes/web.php file. But because of Laravel’s flexibility, there are many ways and places to register them:

  • Classic approach:
Route::get('/articles', [ArticleController::class, 'index']);
  • Using route groups with middleware, prefixes, and namespaces:
Route::prefix('admin')->middleware('auth')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'index']);
});
  • RouteServiceProvider — loading routes from various files or programmatically:
$this->routes(function () {
    Route::middleware('web')
         ->group(base_path('routes/web.php'));
});
  • API routes, single-action controllers, resource controllers, closure routes, fallback routes…

Each method is slightly different, and Laravel doesn’t enforce a single “right” way. For someone coming from Drupal, this can feel overwhelming and inconsistent.


2. How Drupal Defines Routes

Drupal takes a completely different approach: everything is standardized through YAML files. For example:

example.content:
  path: '/example'
  defaults:
    _controller: '\Drupal\example\Controller\ExampleController::content'
    _title: 'Example page'
  requirements:
    _permission: 'access content'
  • All routes live in a MODULE_NAME.routing.yml file.
  • They are automatically loaded when the module is enabled.
  • Middleware and permissions are also defined declaratively, either in the same file or related ones.
  • There aren’t ten ways to do it — just one standard way.

3. Why Laravel Can Be Frustrating

  • Too much freedom: No single “correct” method means every project might use routes differently. Tutorials and packages show varying patterns.
  • Scattered logic: Middleware, service providers, resource routes, and more must all be remembered.
  • Implicit conventions: Laravel relies heavily on naming conventions and “magic.” For example, Route::resource silently creates 7 routes — you need to memorize them.

4. Why Drupal Feels “Happier”

  • Strict standardization: One format (YAML), one place to define routes.
  • Less magic: Open the .routing.yml file, and you immediately see every URL, controller, and permission.
  • Predictable workflow: No surprises; modules and routes follow a consistent pattern.

Conclusion

Laravel’s flexibility is excellent for startups or highly customized solutions — but sometimes, all you want is standardization. Drupal enforces structure, which can feel restrictive, but also predictable and comforting.

If you value consistency and a single source of truth, Drupal’s routing system might just make you happier.


One Route, Many Ways: Why Laravel’s Routing Feels Chaotic Compared to Drupal

Let’s take a simple example: a route /articles that displays a list of articles via ArticleController@index. This is how it looks in both Laravel and Drupal — and why Laravel can feel overwhelming.


In Laravel: Multiple Ways to Define the Same Route

1. Directly in routes/web.php

Route::get('/articles', [ArticleController::class, 'index']);

2. Using an Anonymous Function

Route::get('/articles', function () {
    return App\Models\Article::all();
});

3. Using a Route Group with Middleware

Route::middleware('auth')->group(function () {
    Route::get('/articles', [ArticleController::class, 'index']);
});

4. Using a Prefix

Route::prefix('content')->group(function () {
    Route::get('/articles', [ArticleController::class, 'index']);
});

5. Resource Route (Generates 7 Routes Automatically)

Route::resource('articles', ArticleController::class);
// GET /articles → index
// GET /articles/create → create
// POST /articles → store
// GET /articles/{id} → show
// etc.

6. In RouteServiceProvider

public function boot(): void
{
    $this->routes(function () {
        Route::middleware('web')
             ->group(base_path('routes/custom_routes.php'));
    });
}

7. Using a Custom Route Macro

Route::macro('articles', function () {
    Route::get('/articles', [ArticleController::class, 'index']);
});
Route::articles();

8. Single-Action Controller

// Controller has __invoke()
Route::get('/articles', ArticleController::class);

9. Fallback Route (Last Resort)

Route::fallback(function () {
    return redirect('/articles');
});

And this is not all — you can also register routes via API route files, custom providers, dynamically loaded files, and more.


In Drupal: Only One Way

example.routing.yml:

example.articles:
  path: '/articles'
  defaults:
    _controller: '\Drupal\example\Controller\ArticleController::index'
    _title: 'Articles'
  requirements:
    _permission: 'access content'
  • Automatically loaded when the module is enabled.
  • Middleware (permissions) are declared in this file or in .permissions.yml.
  • There are no alternative ways — and that’s actually a good thing for consistency.

Comparison

  • Laravel: Gives you a dozen ways; flexibility can turn into chaos across different projects.
  • Drupal: Enforces a strict YAML standard; predictable and stable, less flexible but easier to maintain.

Digging Deeper: Why Laravel’s Routing Feels Chaotic (and How to Make It More “Drupal-like”)

We’ve already looked at the common ways to define routes in Laravel. But let’s dig even deeper and explore all the remaining ways Laravel lets you register routes — to show exactly why it feels endless. Then, I’ll show you how to restrict it and create a “Drupal-like” structure for sanity.


Additional Routing Methods in Laravel

10. API Routes (routes/api.php)

Typically used for JSON endpoints, but you can also use them for web routes:

Route::middleware('api')->get('/articles', [ArticleController::class, 'index']);

By default, Laravel automatically prefixes them with /api, unless you remove the prefix.


11. Lazy-Loading Routes from Separate Files

You can store routes in module-specific files and load them dynamically:

Route::middleware('web')->group(base_path('modules/Blog/routes.php'));

12. Route Caching / Dynamic Loading

You can dynamically build routes based on database content:

$pages = Page::all();
foreach ($pages as $page) {
    Route::get($page->slug, fn() => view('page', ['page' => $page]));
}

13. Custom RouteServiceProvider

You can create your own service provider that registers routes:

public function map()
{
    Route::middleware('web')->group(base_path('routes/blog.php'));
}

14. Route Model Binding

Automatically inject models into routes:

Route::get('/articles/{article}', function (Article $article) {
    return $article;
});

This is also a routing method — but with hidden “magic.”


15. Subdomain Routing

Route::domain('{account}.example.com')->group(function () {
    Route::get('/articles', [ArticleController::class, 'index']);
});

16. Route Macros via Packages

Packages can add their own routing methods, for example:

Route::crud('articles');

(Common in tools like Backpack or Laravel Nova.)


17. Attribute Routing (Laravel 9+)

Using PHP attributes in controllers:

#[Route('/articles', methods: ['GET'])]
public function index() { ... }

18. Route::controller Grouping

Route::controller(ArticleController::class)->group(function () {
    Route::get('/articles', 'index');
    Route::post('/articles', 'store');
});

Why Drupal Feels Simpler

Drupal doesn’t have this problem: there’s one YAML file, one method, one entry point.

  • No RouteServiceProvider
  • No dynamic file loading
  • No attributes or controller grouping inline
  • Everything is predictable and consistent across every project

How to Make Laravel “Drupal-like”

If you want less chaos and more order in Laravel, here’s what you can do:

  1. Choose one format — for example, always define routes in routes/web.php.
  2. Avoid custom RouteServiceProvider setups — keep everything in one place.
  3. Explicitly define all URLs — skip resource, controller, fallback, api shortcuts.
  4. Create a YAML-like structure (custom loader):

    • Store routes in routes/routes.yml
    • Parse and load them at boot:
    $routes = Yaml::parseFile(base_path('routes/routes.yml'));
    foreach ($routes as $route) {
        Route::{$route['method']}($route['path'], $route['action']);
    }
    

    This mimics Drupal’s approach.

  5. Standardize middleware and prefixes — declare them at the top of the file, not scattered everywhere.

 


Even More Ways: Why Laravel’s Routing Feels “Endlessly Flexible” (vs Drupal’s Simplicity)

We’ve already covered many of Laravel’s routing options, but there are even more exotic and advanced methods found in real-world Laravel projects. Here’s the full picture — and why it makes Laravel feel scattered compared to Drupal.


19. Invokable Controllers with Route::controller()

A hybrid method using Route::controller with an __invoke action:

Route::controller(ArticleController::class)->group(function () {
    Route::get('/articles', '__invoke');
});

20. Automatic Route Registration via Packages

Packages like Laravel Modules or Spatie Route Discovery can auto-scan controllers and register routes without explicitly declaring them:

// Spatie Route Discovery
// Automatically builds routes from attributes in controllers

21. Route Caching with Custom Preload

Used in large projects: routes are generated at runtime (e.g., from a database) and cached via php artisan route:cache.


22. Middleware-Defined Routes

Sometimes routes are defined inside middleware to handle access dynamically:

public function handle($request, Closure $next)
{
    if ($request->user()->isAdmin()) {
        Route::get('/admin-only', fn() => 'Secret');
    }
    return $next($request);
}

(Considered bad practice, but seen in legacy code.)


23. Route Binding via the Service Container

Routes can be registered inside service providers or packages:

$this->app->booted(function () {
    Route::get('/articles', [ArticleController::class, 'index']);
});

24. Event-Based Routing

Register routes inside event listeners (e.g., when loading modules):

Event::listen('module.loaded', function () {
    Route::get('/dynamic', fn() => 'Loaded dynamically');
});

25. Built-In Broadcast Routes (WebSockets)

Using Broadcast::routes():

Broadcast::routes(['middleware' => ['auth:api']]);

(Technically its own routing system.)


26. Laravel Livewire

Livewire components register routes automatically when mounting components:

Route::get('/articles', \App\Http\Livewire\Articles::class);

27. Custom Route Resolver via Kernel

You can override the RouteServiceProvider and build a custom resolver that loads routes from configs, JSON, or the database instead of files.


28. Multitenancy Routing

Using packages like tenancy/tenancy, routes are scoped per tenant:

Tenancy::tenantBootstrapped(function () {
    Route::get('/dashboard', TenantDashboardController::class);
});

29. Dynamic Routing via Middleware

A highly flexible but confusing approach — building routes on-the-fly based on database content (like a CMS):

Route::get('/{slug}', [DynamicPageController::class, 'show']);

(In Drupal, this is handled automatically via the menu router.)


30. Inertia / SPA Wildcard Routing

For projects using Inertia.js or Vue/React SPA, Laravel routing often boils down to a single wildcard route:

Route::get('/{any}', fn() => Inertia::render('App'))->where('any', '.*');

In Total

Laravel truly offers 30+ ways to define routes. Many overlap or duplicate functionality, and new patterns emerge with each major package or architectural style. This flexibility is powerful — but can feel chaotic.


Drupal in Comparison

  • One method: MODULE_NAME.routing.yml
  • Occasionally, DynamicRouteSubscriber (rare)
  • Transparent: path → controller → permission
  • Even multisite setups are handled at the configuration level, not routing

 


Cheat Sheet: Laravel Routes vs Drupal Routing

1. routes/web.php (Basic Method)

Route::get('/articles', [ArticleController::class, 'index']);

Drupal: YAML:

example.articles:
  path: '/articles'
  defaults:
    _controller: '\Drupal\example\Controller\ArticleController::index'

2. Closure Route

Route::get('/articles', fn() => 'Hello');

Drupal: No equivalent — controller required.


3. Route Group (middleware/prefix)

Route::middleware('auth')->prefix('admin')->group(function () {
    Route::get('/articles', [AdminController::class, 'index']);
});

Drupal: Uses permissions in YAML.


4. Resource Route

Route::resource('articles', ArticleController::class);

Drupal: Each action must be defined separately in YAML.


5. API Routes (routes/api.php)

Route::middleware('api')->get('/articles', [ArticleController::class, 'index']);

Drupal: No API/Web split — all routes defined in a single routing.yml.


6. RouteServiceProvider

$this->routes(function () {
    Route::middleware('web')->group(base_path('routes/custom.php'));
});

Drupal: Routes are auto-loaded from each module’s .routing.yml.


7. Single Action Controller (__invoke)

Route::get('/articles', ArticleController::class);

Drupal: Controllers always use explicit methods.


8. Route Macro

Route::macro('articles', fn() => Route::get('/articles', [ArticleController::class,'index']));
Route::articles();

Drupal: No equivalent.


9. Fallback Route

Route::fallback(fn() => redirect('/'));

Drupal: Uses page not found handler via event subscriber.


10. Lazy Loading (Separate Files)

Route::group([], base_path('routes/blog.php'));

Drupal: Not applicable — YAML is per module.


11. Dynamic Routes from Database

Page::all()->each(fn($p) => Route::get($p->slug, fn() => view('page', compact('p'))));

Drupal: Achieved via menu_links and entity system.


12. Custom RouteServiceProvider

class BlogRouteProvider extends ServiceProvider {
    public function boot() {
        Route::get('/blog', [BlogController::class, 'index']);
    }
}

Drupal: Rarely via DynamicRouteSubscriber.


13. Route Model Binding

Route::get('/articles/{article}', fn(Article $article) => $article);

Drupal: Automatic parameter binding via entity type.


14. Subdomain Routing

Route::domain('{account}.example.com')->group(function () {
    Route::get('/dashboard', DashboardController::class);
});

Drupal: Multisite configuration, not route-level.


15. Attribute Routing (PHP 8+)

#[Route('/articles', methods: ['GET'])]
public function index() { ... }

Drupal: Not available — YAML required.


16. Route::controller Grouping

Route::controller(ArticleController::class)->group(function () {
    Route::get('/articles', 'index');
});

Drupal: No equivalent.


17. Broadcast Routes (WebSockets)

Broadcast::routes(['middleware' => ['auth:api']]);

Drupal: No direct equivalent.


18. Livewire Routes

Route::get('/articles', \App\Http\Livewire\Articles::class);

Drupal: Achieved via custom entity + AJAX, not automatic.


19. Event-Based Routing

Event::listen('booted', fn() => Route::get('/dynamic', fn() => 'Hello'));

Drupal: Rare, via kernel events.


20. Middleware-Defined Routes

public function handle($request, Closure $next)
{
    Route::get('/secret', fn() => 'Secret');
    return $next($request);
}

Drupal: Poor practice; possible via subscriber.


21. Multi-Tenancy Routes

Tenancy::tenantBootstrapped(function () {
    Route::get('/dashboard', TenantDashboardController::class);
});

Drupal: Multisite + domain access module.


22. Inertia/SPA Wildcard

Route::get('/{any}', fn() => Inertia::render('App'))->where('any', '.*');

Drupal: Single controller + catch-all path.


23. Package-Based Auto Discovery (Spatie)

// Automatically scans controllers with attributes

Drupal: Not supported.


24. Route Binding via Services

$this->app->booted(fn() => Route::get('/bind', [Ctrl::class, 'method']));

Drupal: Always through YAML and container injection.


25. Route Caching

php artisan route:cache

Drupal: Automatic menu/route cache.


26. Route Discovery from Modules

(via laravel-modules package)

Module::loadRoutes('blog');

Drupal: Each module loads its YAML automatically.


27. SPA JSON Routes

Route::get('/articles', fn() => response()->json(Article::all()));

Drupal: JSON API module automatically generates endpoints.


28. Route::view Shortcut

Route::view('/welcome', 'welcome');

Drupal: Twig template via controller.


29. Route Constraints

Route::get('/user/{id}', fn($id) => $id)->whereNumber('id');

Drupal: Constraints via YAML requirements:

requirements:
  id: \d+

30. Fallback Wildcard

Route::any('{any}', fn() => view('spa'))->where('any', '.*');

Drupal: page not found handler.

 


Comparison with Drupal

  • Laravel: 30+ methods → flexibility, but chaos. Every project may define routes differently.
  • Drupal: One main method (YAML), occasionally DynamicRouteSubscriber. Minimal variations, strict structure.

 

Conclusion

Laravel offers unparalleled flexibility — over 30 ways to define routes. But this freedom can create inconsistency and cognitive load. Drupal, on the other hand, sticks to one YAML-based method, making projects more predictable and uniform.

What about you — what would make you happier?
Do you value Laravel’s freedom or Drupal’s clarity?

Write to me via the contact page →
(I’d love to hear your perspective and continue the conversation.)

 

Tags

  • #Drupal Planet

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Powered by Drupal