Skip to content

Commit a484427

Browse files
committed
adds url-rewriting
1 parent 03d8388 commit a484427

4 files changed

Lines changed: 529 additions & 16 deletions

File tree

README.md

Lines changed: 197 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,25 +174,27 @@ public function getUsers()
174174

175175
#### Enable Attribute Routing in MVC Application
176176

177-
Add controller paths to your `config/neuron.yaml`:
177+
Add controller paths to your `config/routing.yaml`:
178+
179+
```yaml
180+
# config/routing.yaml
181+
controller_paths:
182+
- path: 'src/Controllers'
183+
namespace: 'App\Controllers'
184+
- path: 'src/Admin/Controllers'
185+
namespace: 'App\Admin\Controllers'
186+
```
187+
188+
For backward compatibility, controller paths can also be configured in `config/neuron.yaml`:
178189

179190
```yaml
180191
routing:
181192
controller_paths:
182193
- path: 'src/Controllers'
183194
namespace: 'App\Controllers'
184-
- path: 'src/Admin/Controllers'
185-
namespace: 'App\Admin\Controllers'
186195
```
187196

188-
#### Hybrid Approach (YAML + Attributes)
189-
190-
You can use both YAML routes and attribute routes together:
191-
192-
- **YAML routes**: Legacy routes, package-provided routes
193-
- **Attribute routes**: New application routes
194-
195-
The MVC Application will load both automatically.
197+
**Note:** If both files exist, `routing.yaml` takes precedence.
196198

197199
### Benefits
198200

@@ -234,6 +236,190 @@ class Home
234236

235237
See `tests/unit/RouteScannerTest.php` for working examples of basic route definition, route groups with prefixes, filter composition, and multiple routes per method.
236238

239+
## URL Rewrites
240+
241+
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.
242+
243+
### Use Cases
244+
245+
- **Override Package Routes**: Applications can override default routes from packages (e.g., CMS homepage)
246+
- **Legacy URL Support**: Support old URLs without creating duplicate routes
247+
- **Clean URLs**: Map user-friendly URLs to internal route structures
248+
- **Environment-Specific Routing**: Different rewrites for dev/staging/production
249+
250+
### Configuration
251+
252+
Add rewrites to `config/routing.yaml`:
253+
254+
```yaml
255+
# config/routing.yaml
256+
rewrites:
257+
'/': '/home' # Root goes to custom homepage
258+
'/index': '/home' # Legacy URL support
259+
'/index.php': '/home' # Handle old PHP URLs
260+
'/blog': '/posts' # URL aliasing
261+
'/about-us': '/company/about' # Clean URL to internal structure
262+
```
263+
264+
### How It Works
265+
266+
```
267+
1. Client requests: http://example.com/
268+
2. Router receives: /
269+
3. Rewrite applied: / → /home
270+
4. Route matching: Finds route for /home
271+
5. Response sent to client
272+
6. Browser still shows: http://example.com/
273+
```
274+
275+
### Example: CMS Homepage Override
276+
277+
**Problem:** CMS defines `GET /` but you want a custom homepage.
278+
279+
**Solution:**
280+
```yaml
281+
# config/routing.yaml
282+
rewrites:
283+
'/': '/custom/landing' # Rewrite root to your controller
284+
285+
controller_paths:
286+
- path: 'app/Controllers' # Your controllers first
287+
namespace: 'App\Controllers'
288+
- path: 'vendor/neuron-php/cms/src/Cms/Controllers'
289+
namespace: 'Neuron\Cms\Controllers'
290+
```
291+
292+
```php
293+
// app/Controllers/Landing.php
294+
class Landing extends Controller
295+
{
296+
#[Get('/custom/landing', name: 'landing')]
297+
public function index()
298+
{
299+
return $this->renderHtml(OK, [], 'custom-home');
300+
}
301+
}
302+
```
303+
304+
Now requests to `/` are transparently routed to your custom landing page without any HTTP redirect.
305+
306+
### Rewrite Rules
307+
308+
- **Exact Match Only**: Rewrites use exact string matching, no wildcards or regex
309+
- **Applied Before Route Matching**: Rewrites happen before the router looks for routes
310+
- **Original URL Preserved**: Client never sees the rewritten URL
311+
- **Logging**: Rewrites are logged at debug level for troubleshooting
312+
313+
### vs. HTTP Redirects
314+
315+
| Feature | URL Rewrite | HTTP Redirect |
316+
|---------|-------------|---------------|
317+
| **Client Visible** | No | Yes |
318+
| **HTTP Request Count** | 1 | 2 |
319+
| **Performance** | Fast | Slower |
320+
| **SEO Impact** | None | Can affect SEO |
321+
| **Use Case** | Internal routing | Moved content |
322+
323+
## Duplicate Route Detection
324+
325+
The router includes strict duplicate route detection to catch configuration errors early and prevent hard-to-debug routing issues.
326+
327+
### What Gets Detected
328+
329+
#### 1. Duplicate Method + Path
330+
331+
```php
332+
// ❌ ERROR: Duplicate route
333+
#[Get('/users')]
334+
public function index() { }
335+
336+
#[Get('/users')] // Same method + path
337+
public function list() { }
338+
```
339+
340+
**Error Message:**
341+
```
342+
Duplicate route detected: GET /users
343+
First: App\Controllers\UserController@index
344+
Second: App\Controllers\UserController@list
345+
Suggestion: Use different paths, different HTTP methods, or combine into one controller method.
346+
```
347+
348+
#### 2. Duplicate Route Names
349+
350+
```php
351+
// ❌ ERROR: Duplicate name
352+
#[Get('/users', name: 'users')]
353+
public function index() { }
354+
355+
#[Post('/users/create', name: 'users')] // Same name
356+
public function store() { }
357+
```
358+
359+
**Error Message:**
360+
```
361+
Duplicate route name detected: 'users'
362+
First: GET /users → App\Controllers\UserController@index
363+
Second: POST /users/create → App\Controllers\UserController@store
364+
Suggestion: Use different route names or remove one of the routes.
365+
```
366+
367+
### What's Allowed
368+
369+
#### Different HTTP Methods (RESTful)
370+
371+
```php
372+
// ✅ ALLOWED: Same path, different methods
373+
#[Get('/users', name: 'users.index')]
374+
public function index() { }
375+
376+
#[Post('/users', name: 'users.store')]
377+
public function store() { }
378+
```
379+
380+
This is standard RESTful routing and is fully supported.
381+
382+
#### Multiple Attributes on Same Method (Aliases)
383+
384+
```php
385+
// ✅ ALLOWED: Multiple routes to same handler
386+
#[Get('/user/:id')]
387+
#[Get('/profile/:id')]
388+
#[Get('/member/:id')]
389+
public function show($id)
390+
{
391+
// Backward compatibility or URL aliasing
392+
}
393+
```
394+
395+
### Configuration
396+
397+
Strict mode is **enabled by default**. To disable (not recommended):
398+
399+
```php
400+
$router = Router::instance();
401+
$router->setStrictMode(false); // Allow duplicates (first match wins)
402+
```
403+
404+
### Benefits
405+
406+
- **Catches Errors Early**: Fails at application boot, not during user requests
407+
- **Clear Error Messages**: Shows both conflicting routes with file locations
408+
- **Prevents Production Bugs**: No silent overwrites or unexpected behavior
409+
- **Developer-Friendly**: Suggests solutions in error messages
410+
411+
### Why This Matters
412+
413+
**Without duplicate detection:**
414+
- Same route defined twice? Second silently ignored, first wins
415+
- Same name used twice? URL generation picks random route
416+
- Debugging nightmare when routes mysteriously don't work
417+
418+
**With duplicate detection:**
419+
- Application fails to start with clear error
420+
- Developer fixes the conflict immediately
421+
- Production deployments are safer
422+
237423
## Rate Limiting
238424

239425
The routing component includes a powerful rate limiting system with multiple storage backends and flexible configuration options.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace Neuron\Routing\Exceptions;
4+
5+
use Exception;
6+
7+
/**
8+
* Exception thrown when duplicate routes are detected during registration.
9+
*
10+
* This exception helps developers identify routing conflicts early by providing
11+
* detailed information about both the original and duplicate route definitions.
12+
*
13+
* @package Neuron\Routing\Exceptions
14+
*/
15+
class DuplicateRouteException extends Exception
16+
{
17+
private string $routeMethod;
18+
private string $routePath;
19+
private string $firstDefinition;
20+
private string $secondDefinition;
21+
private ?string $routeName;
22+
23+
/**
24+
* Create a new duplicate route exception.
25+
*
26+
* @param string $method HTTP method (GET, POST, etc.)
27+
* @param string $path Route path (e.g., /users/:id)
28+
* @param string $first First route definition (controller@method)
29+
* @param string $second Second (duplicate) route definition
30+
* @param string|null $name Optional route name if the conflict is name-based
31+
*/
32+
public function __construct(
33+
string $method,
34+
string $path,
35+
string $first,
36+
string $second,
37+
?string $name = null
38+
)
39+
{
40+
$this->routeMethod = $method;
41+
$this->routePath = $path;
42+
$this->firstDefinition = $first;
43+
$this->secondDefinition = $second;
44+
$this->routeName = $name;
45+
46+
$message = $this->buildMessage();
47+
parent::__construct( $message );
48+
}
49+
50+
/**
51+
* Build a detailed error message for the duplicate route.
52+
*
53+
* @return string
54+
*/
55+
protected function buildMessage(): string
56+
{
57+
if( $this->routeName )
58+
{
59+
return sprintf(
60+
"Duplicate route name detected: '%s'\n" .
61+
" First: %s %s → %s\n" .
62+
" Second: %s %s → %s\n" .
63+
"Suggestion: Use different route names or remove one of the routes.",
64+
$this->routeName,
65+
$this->routeMethod,
66+
$this->routePath,
67+
$this->firstDefinition,
68+
$this->routeMethod,
69+
$this->routePath,
70+
$this->secondDefinition
71+
);
72+
}
73+
74+
return sprintf(
75+
"Duplicate route detected: %s %s\n" .
76+
" First: %s\n" .
77+
" Second: %s\n" .
78+
"Suggestion: Use different paths, different HTTP methods, or combine into one controller method.",
79+
$this->routeMethod,
80+
$this->routePath,
81+
$this->firstDefinition,
82+
$this->secondDefinition
83+
);
84+
}
85+
86+
/**
87+
* Get the HTTP method of the duplicate route.
88+
*
89+
* @return string
90+
*/
91+
public function getRouteMethod(): string
92+
{
93+
return $this->routeMethod;
94+
}
95+
96+
/**
97+
* Get the path of the duplicate route.
98+
*
99+
* @return string
100+
*/
101+
public function getRoutePath(): string
102+
{
103+
return $this->routePath;
104+
}
105+
106+
/**
107+
* Get the first route definition.
108+
*
109+
* @return string
110+
*/
111+
public function getFirstDefinition(): string
112+
{
113+
return $this->firstDefinition;
114+
}
115+
116+
/**
117+
* Get the second (duplicate) route definition.
118+
*
119+
* @return string
120+
*/
121+
public function getSecondDefinition(): string
122+
{
123+
return $this->secondDefinition;
124+
}
125+
126+
/**
127+
* Get the route name if this is a name-based conflict.
128+
*
129+
* @return string|null
130+
*/
131+
public function getRouteName(): ?string
132+
{
133+
return $this->routeName;
134+
}
135+
}

0 commit comments

Comments
 (0)