PHP Luminova: Dynamic URI Placeholders
Dynamic URI placeholders capture and validate URL segments by data type, passing them to controllers. Predefined patterns also support optional PHP datatypes for flexible routing.
Dynamic URI placeholders provide predefined patterns to capture URL segments when routing to controller methods, whether using attribute-based or method-based routing.
They allow you to define routes without writing raw regular expressions or using curly-brace named placeholders, making route definitions cleaner and easier to maintain. Placeholders can be used to match URL segments or CLI command parameters dynamically.
The predefined placeholders are simple, reliable, and ideal for consistent route definitions. Each placeholder validates input based on a PHP-like datatype and also supports optional placeholders, similar to optional PHP arguments.
Placeholders
These placeholders are both readable and type-safe, reducing errors while keeping your routing definitions concise.
| Placeholder | Description |
|---|---|
(:base) | Matches the base path and anything after it. Commonly used for catch-all routes. |
(:root) | Matches a single root-level path segment and everything after it. |
(:any) | Matches any characters, including slashes. Use with care. |
(:int) | Matches a positive integer value. |
(:integer) | Same as (:int). Alias for readability. |
(:mixed) | Matches zero or more characters except /. Can be empty. |
(:string) | Matches one or more characters except /. Cannot be empty. |
(:optional) | Matches an optional single path segment. |
(:alphabet) | Matches alphabet letters only (a–z, A–Z). |
(:alphanumeric) | Matches letters and numbers only. |
(:username) | Matches typical usernames (letters, numbers, dots, underscores, hyphens). |
(:number) | Matches integers or decimal numbers, including negative values. |
(:version) | Matches version numbers like 1.0, 2.1.3. |
(:double) | Matches integer or decimal numbers. Similar to (:number). |
(:float) | Matches decimal numbers only (must include a dot). |
(:path) | Matches nested paths with folders and a final file or segment. |
(:uuid) | Matches a standard UUID string. |
Optional Datatype Placeholders
These placeholders behave like their main versions but are optional.If not present in the URL, the parameter resolves to null.
| Placeholder | Description |
|---|---|
(:?int) | Optional integer value. |
(:?integer) | Optional integer (alias of (:?int)). |
(:?string) | Optional string value (single path segment). |
(:?number) | Optional integer or decimal number. |
(:?double) | Optional integer or decimal number. |
(:?float) | Optional decimal number only. |
Usages
Dynamic User Route
Handles a specific user based on ID and username:
https://example.com/user/42/john-doe→$id = 42,$username = 'john-doe'
#[Route('/user/(:int)/(:username)', methods: ['GET'])]
public function user(int $id, string $username): int
{
return $this->view('user', [
'id' => $id,
'username' => $username
]);
}Optional Segment
The both URI version will be handled in one method:
https://example.com/users→$userIdisnullhttps://example.com//users/12→$userIdis12
#[Route('/users/(:?integer)', methods: ['GET'])]
public function users(?int $userId = null): int
{
if($userId === null){
return $this->view('users');
}
return $this->view('user', [
'id' => $userId
]);
}Defining Custom Placeholders
Luminova allows you to define custom pattern placeholders for dynamic routes. This is useful when:
- You have repeated patterns across multiple routes.
- You need long or complex regex patterns.
- You want to override or extend default placeholders.
Custom placeholders can be defined in your Application class (onCreate or onPreCreate) or globally in /app/Global.php before routing starts.
Note:If defining globally, make sure
feature.app.dev.functionsis enabled in your.envfor autoloading.
// /app/Application.php
namespace App;
use Luminova\Routing\Router;
use Luminova\Foundation\Core\Application as CoreApplication;
class Application extends CoreApplication
{
protected function onPreCreate(): void
{
// Simple pattern → automatically captured
Router::pattern('slug', '[a-z0-9-]+');
// Non-capturing → (?:[a-z0-9-]+)
Router::pattern('slug', '[a-z0-9-]+', 0);
// Capturing → ([a-z0-9-]+)
Router::pattern('slug', '[a-z0-9-]+', 1);
// Already grouped → stays unchanged
Router::pattern('custom', '([a-z]+)-(\d+)', 1);
// Example custom placeholders
Router::pattern('my-int', '(\d+)');
Router::pattern('my-username', '([a-zA-Z0-9._-]+)');
Router::pattern('my-version', '([+-]?\d+(?:\.\d+)?)');
}
}Using Custom Placeholders with Attribute-Based Routing
// /app/Controllers/Http/WebController.php
namespace App\Controllers\Http;
use Luminova\Base\Controller;
use Luminova\Attributes\Route;
class WebController extends Controller
{
#[Route('/user/(:my-int)/(:my-username)', methods: ['GET'])]
public function user(int $id, string $username): int
{
// Matches: GET /user/42/john-doe
}
#[Route('/file/(:my-version)', methods: ['GET'])]
public function file(string $version): int
{
// Matches: GET /file/1.5
}
}Using Custom Placeholders with Method-Based Routing
// /routes/web.php
Router::get('/user/(:my-int)/(:my-username)', 'WebController::user');
Router::get('/file/(:my-version)', 'WebController::file');Once defined, custom placeholders can be used in any route within the application, making route definitions cleaner, consistent, and easier to maintain.
Default Placeholders Description
root
Matches everything after the first / in the URL, regardless of how many segments follow.
It behaves like :any, but is intended specifically for root-level capture, where the full remaining path is needed from a known starting point.
Example:
- Pattern:
?(?:/[^/].*)? - Route:
/(:root) - URI:
/about/company - Matched Value:
about/company
Use Case:
Use
:rootwhen you need to capture the entire remaining path, such as for middleware,controller-level routes, or grouped routes that should handle all sub-paths.
base
Matches the base route with or without any trailing path.It allows the route to resolve whether additional segments are present or not, without capturing or enforcing them.
This placeholder is permissive by design and works well as a fallback or wrapper route.
Example:
- Pattern:
?(?:/.*)? - Route:
/dashboard(:base) - URI:
/dashboard - Matched Value: (none)
- URI:
/dashboard/stats/2024 - Matched Value: (ignored — route still matches)
Use Case:
Use
:basewhen you want a route to match its base path regardless of what comes after it,such as controllers level layout prefixing usingLuminova\Attributes\Prefix, entry points,or feature modules that handle their own internal routing.
mixed
Matches zero or more characters except /.Because it allows empty matches, the segment may be missing entirely. The match is lazy, so it stops as soon as possible.
Example:
- Pattern:
([^/]*?) - Route:
/user/(:mixed) - URI:
/user/ - Matched Value: (empty string)
- URI:
/user/1234 - Matched Value:
1234
Use Case:
Use when a segment may exist or be empty, but must never cross a
/.
string
Matches one or more characters except /.Unlike :mixed, this segment cannot be empty. Matching is lazy and stops at /.
Example:
- Pattern:
([^/]+?) - Route:
/profile/(:string) - URI:
/profile/user_123 - Matched Value:
user_123
Use Case:
Use when a segment must exist and may contain letters, numbers, or symbols, but should not include
/.
integer
Matches whole numbers only, using one or more digits.Both :int and :integer refer to the same pattern.
Alias:
:int and :integer are interchangeable.
Example:
- Pattern:
(\d+) - Route:
/user/(:int) - URI:
/user/42 - Matched Value:
42
Use Case:
Use when the route must accept positive integers only.
float
Matches floating-point numbers only, with a required decimal point and optional sign.
Example:
- Pattern:
([+-]?\d+\.\d+) - Route:
/discount/(:float) - URI:
/discount/-25.50 - Matched Value:
-25.50
Use Case:
Use when a decimal value is required and integers should not be accepted.
double
Matches decimal numbers, with optional sign.Allows integers and decimals, but does not allow a trailing decimal point.
Example:
- Pattern:
([+-]?\d+(\.\d+)?) - Route:
/temperature/(:double) - URI:
/temperature/98.6 - Matched Value:
98.6
Key Difference Between double and float:
floatrequires a decimal point.doubleallows both integers and decimals.- The naming reflects intent more than enforcement, mirroring common language precision concepts.
Use Case:
Use when handling decimal values intended for double-precision calculations.
number
Matches integers or decimal numbers, with optional + or - signs.
Example:
- Pattern:
([+-]?\d+(?:\.\d+)?) - Route:
/price/(:number) - URI:
/price/49.99 - Matched Value:
49.99
Use Case:
Use for numeric values such as prices, measurements, or version-like numbers where decimals are allowed.
uuid
Matches a standard UUID (36 characters, hexadecimal with hyphens).The pattern is version-agnostic and accepts valid UUID v1–v5 formats.
Example:
- Pattern:
([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}) - Route:
/resource/(:uuid) - URI:
/resource/550e8400-e29b-41d4-a716-446655440000 - Matched Value:
550e8400-e29b-41d4-a716-446655440000
Use Case:
Use when a route parameter must be a valid UUID, such as resource IDs or API tokens.
username
Matches letters, numbers, dots (.), underscores (_), and hyphens (-) only.This is stricter than :string and avoids unsafe characters.
Example:
- Pattern:
([a-zA-Z0-9._-]+) - Route:
/profile/(:username) - URI:
/profile/user_123 - Matched Value:
user_123
Use Case:
Use for usernames, slugs, or identifiers where a controlled character set is required.
version
Matches numeric version strings made of numbers separated by dots, such as 1.0.0 or 2.1.
Example:
- Pattern:
(\d+(?:\.\d+)+) - Route:
/package/(:version) - URI:
/package/3.6.6 - Matched Value:
3.6.6
Use Case:
Use when enforcing proper version formats for APIs, packages, or software releases.
any
Matches any character, including /, with no restrictions.
Example:
- Pattern:
(.*) - Route:
/blog/(:any) - URI:
/blog/posts/2024/09/title - Matched Value:
posts/2024/09/title
Use Case:
Use when you need to capture everything after a route point, including nested paths.
optional
Matches a single segment if present, or nothing at all.It never crosses / boundaries and allows routes to remain valid even if the segment is missing.
Example:
- Pattern:
?(?:/([^/]*))? - Route:
/product/(:optional) - URI:
/product - Matched Value: (no segment)
Use Case:
Use when part of a route is optional, such as an ID that may or may not be provided.
See PHP-like optional datatype placeholders, above.
alphanumeric
Matches letters and numbers only, with no symbols or spaces.
Example:
- Pattern:
([a-zA-Z0-9]+) - Route:
/category/(:alphanumeric) - URI:
/category/electronics123 - Matched Value:
electronics123
Use Case:
Use for clean, URL-safe identifiers or slugs.
alphabet
Matches letters only (A–Z, case-insensitive).Numbers and symbols are not allowed.
Example:
- Pattern:
([a-zA-Z]+) - Route:
/name/(:alphabet) - URI:
/name/Peter - Matched Value:
Peter
Use Case:
Use when a segment must contain alphabetic characters only.
path
Matches full paths with multiple /-separated segments.Useful for nested directories or file paths.
Example:
- Pattern:
((.+)/([^/]+)+) - Route:
/files/(:path) - Input:
/files/images/2024/summer/beach.png - Matched Value:
/images/2024/summer/beach.png
Use Case:
Use when capturing file paths or deeply nested route segments without breaking on
/.