The neuron router is a lightweight router/dispatcher is the vein of Ruby's Sinatra or Python's Flask. It allows for a very quick method for creating an app using restful routes or to add them to an existing application.
- Easily map restful http requests to functions.
- Extract one or many variables from routes using masks.
- Create custom 404 responses.
Install php composer from https://getcomposer.org/
Install the neuron routing component:
composer require neuron-php/routing
This example .htaccess file shows how to get and pass the route to the example application.
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?route=$1 [L,QSA]
Here is an example of a fully functional application that processes several routes including one with a variable.
<?php
require_once 'vendor/autoload.php';
Route::get( '/',
function()
{
echo 'Home Page';
}
);
Route::get( '/about',
function()
{
echo 'About Page';
}
);
Route::get( '/test/:name',
function( $parameters )
{
echo "Name = $parameters[name]";
}
);
Route::get( '/404',
function( $parameters )
{
echo "No route found for $parameters[route]";
}
);
$Get = new \Neuron\Data\Filter\Get();
$Server = new \Neuron\Data\Filter\Server();
Route::dispatch(
[
'route' => $Get->filterScalar( 'route' ),
'type' => $Server->filterScalar( 'METHOD' )
]
);
If present, the extra element is merged into the parameters array before it is passed to the routes closure.
Modern PHP 8+ attribute-based routing allows you to define routes directly on controller methods using PHP attributes, providing a modern alternative to YAML configuration files.
useNeuron\Routing\Attributes\Get;
class HomeController
{
#[Get('/')]
publicfunctionindex()
{
return'Hello World';
}
}useNeuron\Routing\Attributes\Get;
useNeuron\Routing\Attributes\Post;
useNeuron\Routing\Attributes\Put;
useNeuron\Routing\Attributes\Delete;
class UsersController
{
#[Get('/users')]
publicfunctionindex() { }
#[Get('/users/:id')]
publicfunctionshow(int$id) { }
#[Post('/users')]
publicfunctionstore() { }
#[Put('/users/:id')]
publicfunctionupdate(int$id) { }
#[Delete('/users/:id')]
publicfunctiondestroy(int$id) { }
}#[Get('/admin/users', name: 'admin.users.index', filters: ['auth'])]
publicfunctionindex() { }
#[Post('/admin/users', name: 'admin.users.store', filters: ['auth', 'csrf'])]
publicfunctionstore() { }Apply common settings to all routes in a controller:
useNeuron\Routing\Attributes\RouteGroup;
useNeuron\Routing\Attributes\Get;
useNeuron\Routing\Attributes\Post;
#[RouteGroup(prefix: '/admin', filters: ['auth'])]
class AdminController
{
#[Get('/dashboard')] // Becomes /admin/dashboard with 'auth' filterpublicfunctiondashboard() { }
#[Post('/users', filters: ['csrf'])] // Becomes /admin/users with ['auth', 'csrf'] filterspublicfunctioncreateUser() { }
}#[Get('/api/v1/users')]
#[Get('/api/v2/users')]
publicfunctiongetUsers()
{
// Handle both API versions
}Add controller paths to your config/routing.yaml:
# config/routing.yamlcontroller_paths:
- path: 'src/Controllers'namespace: 'App\Controllers'
- path: 'src/Admin/Controllers'namespace: 'App\Admin\Controllers'For backward compatibility, controller paths can also be configured in config/neuron.yaml:
routing:
controller_paths:
- path: 'src/Controllers'namespace: 'App\Controllers'Note: If both files exist, routing.yaml takes precedence.
- Co-location: Routes live with controller logic
- Type Safety: IDE autocomplete and validation
- Refactor-Friendly: Routes update when controllers change
- No Sync Issues: Can't have orphaned routes
- Modern Standard: Used by Symfony, Laravel, ASP.NET, Spring Boot
- Self-Documenting: Route definition IS the documentation
Route scanning uses PHP Reflection, which could be slow. For production:
- Routes are scanned once during application initialization
- The Router caches RouteMap objects in memory
- No reflection happens during request handling
- Future: Add route caching to file for zero-cost production routing
Before (YAML):
# routes.yamlhome:
method: GETroute: /controller: App\Controllers\Home@indexAfter (Attributes):
class Home
{
#[Get('/', name: 'home')]
publicfunctionindex() { }
}See tests/unit/RouteScannerTest.php for working examples of basic route definition, route groups with prefixes, filter composition, and multiple routes per method.
URL rewrites provide transparent URL rewriting before route matching. Unlike HTTP redirects (301/302), rewrites are internal and invisible to the client—the browser URL stays the same while the application routes to a different path.
- Override Package Routes: Applications can override default routes from packages (e.g., CMS homepage)
- Legacy URL Support: Support old URLs without creating duplicate routes
- Clean URLs: Map user-friendly URLs to internal route structures
- Environment-Specific Routing: Different rewrites for dev/staging/production
Add rewrites to config/routing.yaml:
# config/routing.yamlrewrites:
'/': '/home'# Root goes to custom homepage'/index': '/home'# Legacy URL support'/index.php': '/home'# Handle old PHP URLs'/blog': '/posts'# URL aliasing'/about-us': '/company/about'# Clean URL to internal structure1. Client requests: http://example.com/
2. Router receives: /
3. Rewrite applied: / → /home
4. Route matching: Finds route for /home
5. Response sent to client
6. Browser still shows: http://example.com/
Problem: CMS defines GET / but you want a custom homepage.
Solution:
# config/routing.yamlrewrites:
'/': '/custom/landing'# Rewrite root to your controllercontroller_paths:
- path: 'app/Controllers'# Your controllers firstnamespace: 'App\Controllers'
- path: 'vendor/neuron-php/cms/src/Cms/Controllers'namespace: 'Neuron\Cms\Controllers'// app/Controllers/Landing.phpclass Landing extends Controller
{
#[Get('/custom/landing', name: 'landing')]
publicfunctionindex()
{
return$this->renderHtml(OK, [], 'custom-home');
}
}Now requests to / are transparently routed to your custom landing page without any HTTP redirect.
- Exact Match Only: Rewrites use exact string matching, no wildcards or regex
- Applied Before Route Matching: Rewrites happen before the router looks for routes
- Original URL Preserved: Client never sees the rewritten URL
- Logging: Rewrites are logged at debug level for troubleshooting
| Feature | URL Rewrite | HTTP Redirect |
|---|---|---|
| Client Visible | No | Yes |
| HTTP Request Count | 1 | 2 |
| Performance | Fast | Slower |
| SEO Impact | None | Can affect SEO |
| Use Case | Internal routing | Moved content |
The router includes strict duplicate route detection to catch configuration errors early and prevent hard-to-debug routing issues.
// ❌ ERROR: Duplicate route
#[Get('/users')]
publicfunctionindex() { }
#[Get('/users')] // Same method + pathpublicfunctionlist() { }Error Message:
Duplicate route detected: GET /users
First: App\Controllers\UserController@index
Second: App\Controllers\UserController@list
Suggestion: Use different paths, different HTTP methods, or combine into one controller method.
// ❌ ERROR: Duplicate name
#[Get('/users', name: 'users')]
publicfunctionindex() { }
#[Post('/users/create', name: 'users')] // Same namepublicfunctionstore() { }Error Message:
Duplicate route name detected: 'users'
First: GET /users → App\Controllers\UserController@index
Second: POST /users/create → App\Controllers\UserController@store
Suggestion: Use different route names or remove one of the routes.
// ✅ ALLOWED: Same path, different methods
#[Get('/users', name: 'users.index')]
publicfunctionindex() { }
#[Post('/users', name: 'users.store')]
publicfunctionstore() { }This is standard RESTful routing and is fully supported.
// ✅ ALLOWED: Multiple routes to same handler
#[Get('/user/:id')]
#[Get('/profile/:id')]
#[Get('/member/:id')]
publicfunctionshow($id)
{
// Backward compatibility or URL aliasing
}Strict mode is enabled by default. To disable (not recommended):
$router = Router::instance();
$router->setStrictMode(false); // Allow duplicates (first match wins)- Catches Errors Early: Fails at application boot, not during user requests
- Clear Error Messages: Shows both conflicting routes with file locations
- Prevents Production Bugs: No silent overwrites or unexpected behavior
- Developer-Friendly: Suggests solutions in error messages
Without duplicate detection:
- Same route defined twice? Second silently ignored, first wins
- Same name used twice? URL generation picks random route
- Debugging nightmare when routes mysteriously don't work
With duplicate detection:
- Application fails to start with clear error
- Developer fixes the conflict immediately
- Production deployments are safer
The routing component includes a powerful rate limiting system with multiple storage backends and flexible configuration options.
useNeuron\Routing\Router;
useNeuron\Routing\Filters\RateLimitFilter;
useNeuron\Routing\RateLimit\RateLimitConfig;
$router = Router::instance();
// Create rate limit configuration$config = newRateLimitConfig([
'enabled' => true,
'storage' => 'redis', // Options: redis, file, memory (testing only)'requests' => 100, // Max requests per window'window' => 3600// Time window in seconds (1 hour)
]);
// Create and register the filter$rateLimitFilter = newRateLimitFilter($config);
$router->registerFilter('rate_limit', $rateLimitFilter);
// Apply globally to all routes$router->addFilter('rate_limit');
// Or apply to specific routes$router->get('/api/data', $handler, 'rate_limit');// Array configuration$config = newRateLimitConfig([
'enabled' => true,
'storage' => 'redis',
'requests' => 100,
'window' => 3600,
'redis_host' => '127.0.0.1',
'redis_port' => 6379,
'file_path' => 'cache/rate_limits'
]);Best for distributed systems and high-traffic applications:
$config = newRateLimitConfig([
'storage' => 'redis',
'redis_host' => '127.0.0.1',
'redis_port' => 6379,
'redis_database' => 0,
'redis_prefix' => 'rate_limit_',
'redis_auth' => 'password', // Optional'redis_persistent' => true// Use persistent connections
]);Simple solution for single-server deployments:
$config = newRateLimitConfig([
'storage' => 'file',
'file_path' => 'cache/rate_limits'// Directory for rate limit files
]);For unit tests and development:
$config = newRateLimitConfig([
'storage' => 'memory'// Data lost when PHP process ends
]);$filter = newRateLimitFilter(
$config,
'ip',
['192.168.1.100', '10.0.0.1'], // Whitelist - no limits
['45.67.89.10'] // Blacklist - stricter limits (1/10th)
);Rate limit exceeded responses include appropriate headers:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remainingX-RateLimit-Reset: Unix timestamp when limit resetsRetry-After: Seconds until retry allowed
The response format (JSON/HTML) is automatically determined from the Accept header.
// Different limits for different endpoints$publicConfig = newRateLimitConfig([
'enabled' => true,
'storage' => 'redis',
'requests' => 10,
'window' => 60// 10 requests per minute
]);
$apiConfig = newRateLimitConfig([
'enabled' => true,
'storage' => 'redis',
'requests' => 1000,
'window' => 3600// 1000 requests per hour
]);
$router->registerFilter('public_limit', newRateLimitFilter($publicConfig));
$router->registerFilter('api_limit', newRateLimitFilter($apiConfig));
// Apply different limits$router->get('/public/search', $searchHandler, 'public_limit');
$router->get('/api/users', $usersHandler, 'api_limit');You can read more about the Neuron components at neuronphp.com