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.
use Neuron\Routing\Attributes\Get;
class HomeController
{
#[Get('/')]
public function index()
{
return 'Hello World';
}
}use Neuron\Routing\Attributes\Get;
use Neuron\Routing\Attributes\Post;
use Neuron\Routing\Attributes\Put;
use Neuron\Routing\Attributes\Delete;
class UsersController
{
#[Get('/users')]
public function index() { }
#[Get('/users/:id')]
public function show(int $id) { }
#[Post('/users')]
public function store() { }
#[Put('/users/:id')]
public function update(int $id) { }
#[Delete('/users/:id')]
public function destroy(int $id) { }
}#[Get('/admin/users', name: 'admin.users.index', filters: ['auth'])]
public function index() { }
#[Post('/admin/users', name: 'admin.users.store', filters: ['auth', 'csrf'])]
public function store() { }Apply common settings to all routes in a controller:
use Neuron\Routing\Attributes\RouteGroup;
use Neuron\Routing\Attributes\Get;
use Neuron\Routing\Attributes\Post;
#[RouteGroup(prefix: '/admin', filters: ['auth'])]
class AdminController
{
#[Get('/dashboard')] // Becomes /admin/dashboard with 'auth' filter
public function dashboard() { }
#[Post('/users', filters: ['csrf'])] // Becomes /admin/users with ['auth', 'csrf'] filters
public function createUser() { }
}#[Get('/api/v1/users')]
#[Get('/api/v2/users')]
public function getUsers()
{
// Handle both API versions
}Add controller paths to your config/routing.yaml:
# config/routing.yaml
controller_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.yaml
home:
method: GET
route: /
controller: App\Controllers\Home@indexAfter (Attributes):
class Home
{
#[Get('/', name: 'home')]
public function index() { }
}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.yaml
rewrites:
'/': '/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.yaml
rewrites:
'/': '/custom/landing' # Rewrite root to your controller
controller_paths:
- path: 'app/Controllers' # Your controllers first
namespace: 'App\Controllers'
- path: 'vendor/neuron-php/cms/src/Cms/Controllers'
namespace: 'Neuron\Cms\Controllers'// app/Controllers/Landing.php
class Landing extends Controller
{
#[Get('/custom/landing', name: 'landing')]
public function index()
{
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')]
public function index() { }
#[Get('/users')] // Same method + path
public function list() { }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')]
public function index() { }
#[Post('/users/create', name: 'users')] // Same name
public function store() { }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')]
public function index() { }
#[Post('/users', name: 'users.store')]
public function store() { }This is standard RESTful routing and is fully supported.
// ✅ ALLOWED: Multiple routes to same handler
#[Get('/user/:id')]
#[Get('/profile/:id')]
#[Get('/member/:id')]
public function show($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.
use Neuron\Routing\Router;
use Neuron\Routing\Filters\RateLimitFilter;
use Neuron\Routing\RateLimit\RateLimitConfig;
$router = Router::instance();
// Create rate limit configuration
$config = new RateLimitConfig([
'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 = new RateLimitFilter($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 = new RateLimitConfig([
'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 = new RateLimitConfig([
'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 = new RateLimitConfig([
'storage' => 'file',
'file_path' => 'cache/rate_limits' // Directory for rate limit files
]);For unit tests and development:
$config = new RateLimitConfig([
'storage' => 'memory' // Data lost when PHP process ends
]);$filter = new RateLimitFilter(
$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 = new RateLimitConfig([
'enabled' => true,
'storage' => 'redis',
'requests' => 10,
'window' => 60 // 10 requests per minute
]);
$apiConfig = new RateLimitConfig([
'enabled' => true,
'storage' => 'redis',
'requests' => 1000,
'window' => 3600 // 1000 requests per hour
]);
$router->registerFilter('public_limit', new RateLimitFilter($publicConfig));
$router->registerFilter('api_limit', new RateLimitFilter($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