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:
- Controller Discovery - Automatically registers routes for controller methods
- 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)
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)
// 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:
// 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:
- Controller name (without
Controllersuffix) becomes the base URI segment - Converted to kebab-case:
UserProfile→user-profile - Method names become URI segments:
editProfile→edit-profile indexmethods omit the method name from the URI
Examples:
| Controller | Method | Generated Route | Route Name |
|---|---|---|---|
ContactsController | index() | GET /contacts | contacts |
ContactsController | create() | GET /contacts/create | contacts.create |
ContactsController | editProfile() | GET /contacts/edit-profile | contacts.edit-profile |
UserProfileController | index() | GET /user-profile | user-profile |
Nested Controllers
Controllers in subdirectories create nested routes automatically:
// 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/usersDiscover/Settings/Billing→/settings/billing
Route Parameters
Hyper discovers route parameters from method signatures:
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:
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 Pattern | HTTP Verb |
|---|---|
index(), show(), edit(), create() | GET |
store(), update() | POST |
| Others | GET (default) |
Override with #[Route] Attribute:
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:
#[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:
#[Route(uri: 'archive')]
public function softDelete(Contact $contact)
{
// GET /contacts/{contact}/archive
// Instead of /contacts/{contact}/soft-delete
}Full URI Override:
#[Route(fullUri: 'api/v1/contacts')]
public function index()
{
// GET /api/v1/contacts
// Completely replaces the discovered URI
}Custom Route Name:
#[Route(name: 'contacts.dashboard')]
public function index()
{
// Name: contacts.dashboard instead of contacts
}Add Middleware:
#[Route(middleware: 'auth')]
public function create()
{
// Protected by auth middleware
}
#[Route(middleware: ['auth', 'verified'])]
public function adminPanel()
{
// Multiple middleware
}Domain Routing:
#[Route(domain: 'api.example.com')]
public function apiIndex()
{
// Only matches on api.example.com subdomain
}Soft Deleted Models:
#[Route(withTrashed: true)]
public function showAll()
{
// Include soft-deleted models in route model binding
}Combined Attributes:
#[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:
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:
Where::alpha // [a-zA-Z]+
Where::numeric // [0-9]+
Where::alphanumeric // [a-zA-Z0-9]+
Where::uuid // UUID formatMultiple Constraints:
#[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:
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:
#[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:
use Dancycodes\Hyper\Routing\Discovery\Discover;
Route::prefix('docs')->group(function () {
Discover::views()->in(resource_path('views/pages/docs'));
});Via Configuration:
// 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.phpGenerated Routes:
| File | Route | Route Name |
|---|---|---|
index.blade.php | GET /docs | docs |
getting-started.blade.php | GET /docs/getting-started | docs.getting-started |
installation.blade.php | GET /docs/installation | docs.installation |
advanced/index.blade.php | GET /docs/advanced | docs.advanced |
advanced/configuration.blade.php | GET /docs/advanced/configuration | docs.advanced.configuration |
Naming Conventions:
- File names are converted to kebab-case URLs
index.blade.phpfiles map to their directory path- Route names use dot notation:
/becomes. - Nested directories create nested routes
Examples
Marketing Pages:
// 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/reportingDocumentation Site:
'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/endpointsMultiple Directories with Same Prefix:
'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:
// config/hyper.php
'pending_route_transformers' => [
...Dancycodes\Hyper\Routing\Config::defaultRouteTransformers(),
App\Routing\CustomTransformer::class,
],Create a Custom Transformer:
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):
- RejectDefaultControllerMethodRoutes - Removes Laravel default methods (
__construct,middleware, etc.) - HandleDoNotDiscoverAttribute - Excludes routes with
#[DoNotDiscover] - AddControllerUriToActions - Builds URI from controller name
- HandleUrisOfNestedControllers - Handles nested directory structure
- HandleRouteNameAttribute - Processes custom route names
- HandleMiddlewareAttribute - Applies middleware from attributes
- HandleHttpMethodsAttribute - Sets HTTP verbs
- HandleUriAttribute - Processes custom URI segments
- HandleFullUriAttribute - Applies full URI overrides
- HandleWithTrashedAttribute - Configures soft delete handling
- HandleWheresAttribute - Applies parameter constraints
- AddDefaultRouteName - Generates default route names
- HandleDomainAttribute - Configures domain routing
- ValidateOptionalParameters - Ensures optional parameters are last
- MoveRoutesStartingWithParametersLast - Optimizes route matching
Disabling Route Discovery
Globally:
// config/hyper.php
'route_discovery' => [
'enabled' => false,
],For Specific Environments:
// config/hyper.php
'route_discovery' => [
'enabled' => env('HYPER_ROUTE_DISCOVERY', true),
],# .env
HYPER_ROUTE_DISCOVERY=falseTroubleshooting
Routes Not Discovered
Check these common issues:
- Routes are cached:
php artisan route:clear
php artisan route:list # Verify routes- Discovery disabled in config:
// config/hyper.php
'route_discovery' => [
'enabled' => true, // Must be true
],- Controller not in configured directory:
'discover_controllers_in_directory' => [
app_path('Http/Controllers/Discover'), // Ensure path is correct
],- Class not a controller:
// Must extend Controller
class MyController extends Controller
{
//
}- Method is not public:
// Must be public to be discovered
public function index()
{
//
}Route Conflicts
If discovered routes conflict with manual routes:
// 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:
# Always cache routes in production
php artisan route:cacheDiscovery only runs when routes are not cached, making it zero-cost in production.
Learn More
- Blade Directives - Hyper's Blade helpers
- Hyper Response - Building Hyper responses
- Navigation - Client-side navigation with Hyper
- Spatie Route Discovery - Original package inspiration

