Skip to content

Route Discovery

Laravel Hyper includes an automatic route discovery system that eliminates the need to manually register routes. Inspired by Spatie's Laravel Route Discovery package, Hyper's implementation automatically discovers routes for both controllers and views based on file structure and naming conventions.

Benefits

  • Less Boilerplate: No need to manually register every route
  • Convention Over Configuration: Predictable URL patterns based on file structure
  • Developer Experience: Focus on building features, not managing route files
  • Flexible: Works alongside traditional route registration

Overview

Route discovery works in two ways:

  1. Controller Discovery - Automatically registers routes for controller methods
  2. View Discovery - Automatically creates routes for Blade view files

Both systems can be configured via the configuration file or activated programmatically in your routes file.

Controller Discovery

Getting Started

There are two ways to enable controller discovery:

1. Via Routes File (Recommended for Granular Control)

php
use Dancycodes\Hyper\Routing\Discovery\Discover;

Route::middleware('web')->group(function () {
    Discover::controllers()->in(app_path('Http/Controllers/Discover'));
});

2. Via Configuration File (Set-and-Forget)

php
// config/hyper.php

'route_discovery' => [
    'enabled' => true,

    'discover_controllers_in_directory' => [
        app_path('Http/Controllers/Discover'),
        app_path('Http/Controllers/Admin'),
    ],
],

Route Caching

Route discovery is automatically disabled when routes are cached. Run php artisan route:cache in production for optimal performance.

How Controllers Map to Routes

Controllers are mapped to routes using a predictable convention based on the controller name, namespace, and method names.

Basic Mapping:

php
// app/Http/Controllers/Discover/ContactsController.php
namespace App\Http\Controllers\Discover;

class ContactsController extends Controller
{
    public function index()
    {
        // Route: GET /contacts
        // Name: contacts.index
    }

    public function create()
    {
        // Route: GET /contacts/create
        // Name: contacts.create
    }

    public function store()
    {
        // Route: POST /contacts/store
        // Name: contacts.store
    }
}

URL Generation Rules:

  1. Controller name (without Controller suffix) becomes the base URI segment
  2. Converted to kebab-case: UserProfileuser-profile
  3. Method names become URI segments: editProfileedit-profile
  4. index methods omit the method name from the URI

Examples:

ControllerMethodGenerated RouteRoute Name
ContactsControllerindex()GET /contactscontacts
ContactsControllercreate()GET /contacts/createcontacts.create
ContactsControllereditProfile()GET /contacts/edit-profilecontacts.edit-profile
UserProfileControllerindex()GET /user-profileuser-profile

Nested Controllers

Controllers in subdirectories create nested routes automatically:

php
// app/Http/Controllers/Discover/Admin/UsersController.php
namespace App\Http\Controllers\Discover\Admin;

class UsersController extends Controller
{
    public function index()
    {
        // Route: GET /admin/users
        // Name: admin.users
    }

    public function create()
    {
        // Route: GET /admin/users/create
        // Name: admin.users.create
    }
}

The directory structure directly translates to URL structure:

  • Discover/Admin/Users/admin/users
  • Discover/Settings/Billing/settings/billing

Route Parameters

Hyper discovers route parameters from method signatures:

php
class ContactsController extends Controller
{
    public function show(Contact $contact)
    {
        // Route: GET /contacts/{contact}
        // Automatic route model binding
    }

    public function edit(Contact $contact)
    {
        // Route: GET /contacts/{contact}/edit
    }

    public function update(Contact $contact)
    {
        // Route: POST /contacts/{contact}/update
    }
}

Multiple Parameters:

php
class CommentsController extends Controller
{
    public function edit(Post $post, Comment $comment)
    {
        // Route: GET /comments/{post}/{comment}/edit
    }
}

HTTP Methods

By default, Hyper determines HTTP methods based on naming conventions:

Method Name PatternHTTP Verb
index(), show(), edit(), create()GET
store(), update()POST
OthersGET (default)

Override with #[Route] Attribute:

php
use Dancycodes\Hyper\Routing\Attributes\Route;

class ContactsController extends Controller
{
    #[Route(method: 'delete')]
    public function destroy(Contact $contact)
    {
        // Route: DELETE /contacts/{contact}/destroy
    }

    #[Route(method: ['get', 'post'])]
    public function multiMethod()
    {
        // Responds to both GET and POST
    }
}

Route Attributes

Hyper provides PHP attributes to customize discovered routes:

#[Route] Attribute

The most versatile attribute for customizing route behavior:

Customize HTTP Method:

php
#[Route(method: 'patch')]
public function update(Contact $contact)
{
    // PATCH /contacts/{contact}/update
}

#[Route(method: ['get', 'post'])]
public function flexibleMethod()
{
    // Responds to GET or POST
}

Customize URI:

php
#[Route(uri: 'archive')]
public function softDelete(Contact $contact)
{
    // GET /contacts/{contact}/archive
    // Instead of /contacts/{contact}/soft-delete
}

Full URI Override:

php
#[Route(fullUri: 'api/v1/contacts')]
public function index()
{
    // GET /api/v1/contacts
    // Completely replaces the discovered URI
}

Custom Route Name:

php
#[Route(name: 'contacts.dashboard')]
public function index()
{
    // Name: contacts.dashboard instead of contacts
}

Add Middleware:

php
#[Route(middleware: 'auth')]
public function create()
{
    // Protected by auth middleware
}

#[Route(middleware: ['auth', 'verified'])]
public function adminPanel()
{
    // Multiple middleware
}

Domain Routing:

php
#[Route(domain: 'api.example.com')]
public function apiIndex()
{
    // Only matches on api.example.com subdomain
}

Soft Deleted Models:

php
#[Route(withTrashed: true)]
public function showAll()
{
    // Include soft-deleted models in route model binding
}

Combined Attributes:

php
#[Route(
    method: 'post',
    uri: 'bulk-delete',
    name: 'contacts.bulk-destroy',
    middleware: ['auth', 'verified']
)]
public function destroyBulk()
{
    // Fully customized route
}

#[Where] Attribute

Constrain route parameters with regex patterns:

php
use Dancycodes\Hyper\Routing\Attributes\Where;

class UsersController extends Controller
{
    #[Where('user', Where::uuid)]
    public function show($user)
    {
        // Route: /users/{user}
        // Only matches UUID format
    }

    #[Where('id', Where::numeric)]
    public function edit($id)
    {
        // Only matches numeric IDs
    }

    #[Where('slug', '[a-z\-]+')]
    public function findBySlug($slug)
    {
        // Custom regex pattern
    }
}

Built-in Patterns:

php
Where::alpha          // [a-zA-Z]+
Where::numeric        // [0-9]+
Where::alphanumeric   // [a-zA-Z0-9]+
Where::uuid           // UUID format

Multiple Constraints:

php
#[Where('category', Where::alpha)]
#[Where('id', Where::numeric)]
public function showCategoryItem($category, $id)
{
    // Both parameters are constrained
}

#[DoNotDiscover] Attribute

Prevent specific methods or entire controllers from being discovered:

Exclude a Method:

php
use Dancycodes\Hyper\Routing\Attributes\DoNotDiscover;

class UsersController extends Controller
{
    public function index()
    {
        // This will be discovered
    }

    #[DoNotDiscover]
    public function internalMethod()
    {
        // This will NOT be discovered
        // Use for helper methods
    }
}

Exclude an Entire Controller:

php
#[DoNotDiscover]
class BaseController extends Controller
{
    // No routes will be discovered for this controller
}

Use Case

Use #[DoNotDiscover] for base controllers, trait methods, or internal helpers that shouldn't be accessible via routes.

View Discovery

Getting Started

View discovery automatically creates routes for Blade view files:

Via Routes File:

php
use Dancycodes\Hyper\Routing\Discovery\Discover;

Route::prefix('docs')->group(function () {
    Discover::views()->in(resource_path('views/pages/docs'));
});

Via Configuration:

php
// config/hyper.php

'route_discovery' => [
    'enabled' => true,

    'discover_views_in_directory' => [
        'docs' => resource_path('views/pages/docs'),
        'legal' => resource_path('views/pages/legal'),
    ],
],

The array key (docs, legal) becomes the URL prefix.

How Views Map to Routes

Views are automatically mapped to routes based on file structure:

Directory Structure:

resources/views/pages/docs/
├── index.blade.php
├── getting-started.blade.php
├── installation.blade.php
└── advanced/
    ├── index.blade.php
    ├── configuration.blade.php
    └── deployment.blade.php

Generated Routes:

FileRouteRoute Name
index.blade.phpGET /docsdocs
getting-started.blade.phpGET /docs/getting-starteddocs.getting-started
installation.blade.phpGET /docs/installationdocs.installation
advanced/index.blade.phpGET /docs/advanceddocs.advanced
advanced/configuration.blade.phpGET /docs/advanced/configurationdocs.advanced.configuration

Naming Conventions:

  1. File names are converted to kebab-case URLs
  2. index.blade.php files map to their directory path
  3. Route names use dot notation: / becomes .
  4. Nested directories create nested routes

Examples

Marketing Pages:

php
// config/hyper.php
'discover_views_in_directory' => [
    '' => resource_path('views/pages/marketing'),
],
views/pages/marketing/
├── index.blade.php          → /
├── about.blade.php          → /about
├── pricing.blade.php        → /pricing
└── features/
    ├── analytics.blade.php  → /features/analytics
    └── reporting.blade.php  → /features/reporting

Documentation Site:

php
'discover_views_in_directory' => [
    'docs' => resource_path('views/docs'),
],
views/docs/
├── index.blade.php              → /docs
├── quickstart.blade.php         → /docs/quickstart
└── api/
    ├── authentication.blade.php → /docs/api/authentication
    └── endpoints.blade.php      → /docs/api/endpoints

Multiple Directories with Same Prefix:

php
'discover_views_in_directory' => [
    'help' => [
        resource_path('views/help/guides'),
        resource_path('views/help/faqs'),
    ],
],

Both directories will be discovered under the /help prefix.

Advanced Configuration

Custom Route Transformers

Route transformers process discovered routes before registration. You can create custom transformers to add your own logic:

php
// config/hyper.php

'pending_route_transformers' => [
    ...Dancycodes\Hyper\Routing\Config::defaultRouteTransformers(),
    App\Routing\CustomTransformer::class,
],

Create a Custom Transformer:

php
namespace App\Routing;

use Dancycodes\Hyper\Routing\PendingRouteTransformers\PendingRouteTransformer;
use Illuminate\Support\Collection;

class CustomTransformer implements PendingRouteTransformer
{
    public function transform(Collection $pendingRoutes): Collection
    {
        return $pendingRoutes->map(function ($route) {
            // Add custom logic
            // Example: Add middleware to all routes
            $route->actions->each(function ($action) {
                $action->middleware[] = 'custom-middleware';
            });

            return $route;
        });
    }
}

Default Transformers

Hyper includes these transformers by default (in execution order):

  1. RejectDefaultControllerMethodRoutes - Removes Laravel default methods (__construct, middleware, etc.)
  2. HandleDoNotDiscoverAttribute - Excludes routes with #[DoNotDiscover]
  3. AddControllerUriToActions - Builds URI from controller name
  4. HandleUrisOfNestedControllers - Handles nested directory structure
  5. HandleRouteNameAttribute - Processes custom route names
  6. HandleMiddlewareAttribute - Applies middleware from attributes
  7. HandleHttpMethodsAttribute - Sets HTTP verbs
  8. HandleUriAttribute - Processes custom URI segments
  9. HandleFullUriAttribute - Applies full URI overrides
  10. HandleWithTrashedAttribute - Configures soft delete handling
  11. HandleWheresAttribute - Applies parameter constraints
  12. AddDefaultRouteName - Generates default route names
  13. HandleDomainAttribute - Configures domain routing
  14. ValidateOptionalParameters - Ensures optional parameters are last
  15. MoveRoutesStartingWithParametersLast - Optimizes route matching

Disabling Route Discovery

Globally:

php
// config/hyper.php
'route_discovery' => [
    'enabled' => false,
],

For Specific Environments:

php
// config/hyper.php
'route_discovery' => [
    'enabled' => env('HYPER_ROUTE_DISCOVERY', true),
],
bash
# .env
HYPER_ROUTE_DISCOVERY=false

Troubleshooting

Routes Not Discovered

Check these common issues:

  1. Routes are cached:
bash
php artisan route:clear
php artisan route:list  # Verify routes
  1. Discovery disabled in config:
php
// config/hyper.php
'route_discovery' => [
    'enabled' => true,  // Must be true
],
  1. Controller not in configured directory:
php
'discover_controllers_in_directory' => [
    app_path('Http/Controllers/Discover'),  // Ensure path is correct
],
  1. Class not a controller:
php
// Must extend Controller
class MyController extends Controller
{
    //
}
  1. Method is not public:
php
// Must be public to be discovered
public function index()
{
    //
}

Route Conflicts

If discovered routes conflict with manual routes:

php
// Option 1: Use #[DoNotDiscover]
#[DoNotDiscover]
public function conflictingMethod()
{
    //
}

// Option 2: Move to a non-discovered directory
// app/Http/Controllers/Manual/MyController.php

// Option 3: Use fullUri to avoid conflict
#[Route(fullUri: 'unique/path')]
public function myMethod()
{
    //
}

Performance Concerns

Route discovery is efficient, but for maximum performance:

bash
# Always cache routes in production
php artisan route:cache

Discovery only runs when routes are not cached, making it zero-cost in production.

Learn More