Changelog
This file contains all changes for all versions of the codesaur/router package.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[6.0.1] - 2026-07-03
[6.0.1]: https://github.com/codesaur-php/Router/compare/v6.0.0...v6.0.1
Fixed
generate()replaced the wrong placeholder on partial params. When only some parameters were supplied,preg_replacewith limit 1 substituted the first placeholder of the matching filter type instead of the one with the requested name (e.g./point/{int:x}/{int:y}with['y' => 5]produced/point/5/{int:y}). Substitution now targets the exact named placeholder viastr_replace.- **
generate()interpreted$1/\1in parameter values as regex backreferences.** Values were passed as apreg_replacereplacement string; they are now substituted literally. - Literal dot in static segments acted as a regex wildcard in
match(). A pattern like/files/{int:id}/manifest.jsonalso matched/files/1/manifestXjsonbecauserawurlencodeleaves.and~untouched. Literal segments are now additionally passed throughpreg_quote.
Removed
- Author phone number removed from
composer.json.
[6.0.0] - 2026-05-24
[6.0.0]: https://github.com/codesaur-php/Router/compare/v5.2.0...v6.0.0
Breaking changes
Callbackclass removed.Router::match()now returns a fixed 3-tuplearray{0: callable, 1: array, 2: list<middleware>}|nullinstead of the wrapper object. Consumers destructure with[$callable, $params, $middleware] = $result.Router::merge(Router)removed. Module routes are no longer absorbed into a parent router.codesaur/http-applicationwill delegate to sub-routers directly (delegation chain) in an upcoming release, so the merge step becomes unnecessary.Router::name(string)removed (public method). Route naming now goes through the newRoute::name()fluent API. The Router itself is stateless again - the transient$lastPatternproperty is gone.Router::getNamePatterns()removed. Was only used by the removedmerge(); not part of any production code path.Router::getRoutes()shape changed. Each entry is now a 2-tuple[callable, middleware]rather than the bare callable. The pattern is still the outer key, methods are still the inner key. Name is intentionally not included - naming is a separate concern, usegenerate($name)orpattern($name)for name-based lookup, or access the protected$name_patternsproperty from a subclass for direct introspection.Router::__call()return type changed. Now returns aRoutevalue object instead of$this.$router->GET(...)->name(...)->middleware([...])still works becauseRoute::name()andRoute::middleware()are chainable.- Internal storage merged. Per-route middleware was previously kept in a
separate
$route_middlewarearray; it now lives inside$routesas[pattern => [method => [callable, middleware]]]- method-level granularity, single source of truth. - Storage
$_callable,$_params,$_patternunderscore prefixes removed. PSR-12 sec. 4.3 explicitly forbids_-prefixed properties; they are now$callable,$params,$lastPattern(the last is gone entirely as part of the stateless refactor).
Added
Routeclass. Immutablefinal readonlyvalue object returned byRouter::__call(). Carries the registered pattern and a back-reference to the Router. Exposesname(string)andmiddleware(array)fluent methods.Route::middleware([...]). Per-route middleware, scoped to the (pattern, method) pair the same way Express, Laravel, Slim and Fastify scope theirs - soGET /foomiddleware does not fire onPOST /foo, and the "public read, protected write" pattern works out of the box:$router->GET('/api/users', $list); // public $router->POST('/api/users', $create)->middleware([Auth::class]); // protected // match('/api/users', 'GET')[2] -> [] // match('/api/users', 'POST')[2] -> [Auth::class]Accepts class-strings (PSR-15
MiddlewareInterface), pre-instantiated middleware objects, and Closures. Append semantics - multiple chained calls accumulate. Compound method registrations (GET_POST) fan the middleware out to each constituent method:$router->POST('/users', $cb)->middleware([CsrfMiddleware::class, RBACMiddleware::class]); $router->GET('/admin', $cb)->middleware([Auth::class])->middleware([AdminOnly::class]); $router->GET_POST('/foo', $cb)->middleware([Auth::class]); // attaches to both GET and POST- Strict name conflict detection.
Router::registerName()(and thereforeRoute::name()) throws\LogicExceptionwhen the same name is reassigned to a different pattern. Re-registering the same name on the same pattern stays idempotent (no-op), so post-hoc registration of an already-named route still works. Previously the second registration silently overwrote the first, hiding the original route fromgenerate():$router->GET('/users', $a)->name('users'); $router->GET('/admin', $b)->name('users'); // throws \LogicException RouterInterfaceis now the full contract (match,generate,pattern,getRoutes). Third-party routers (FastRoute, Symfony Routing, AltoRouter, etc.) can be wrapped in adapters that implement this interface, and an upcomingcodesaur/http-applicationrelease will accept them directly. Adapter compatibility is covered bytests/AdapterPatternTest.php.Router::registerName()andRouter::registerMiddleware(). Public@internalsetters thatRoute::name()andRoute::middleware()delegate to.registerMiddleware(string $pattern, string $method, array $middleware)takes the method (compoundGET_POSTaccepted) so middleware lands on the correct (pattern, method) bucket. They are also callable directly for post-hoc registration or for the inheritance pattern documented in the README (auto-attaching middleware via a subclass overriding__call).- HEAD -> GET auto-fallback (RFC 7231 sec. 4.3.2). A
HEADrequest without an explicitHEADroute is automatically dispatched to the registeredGEThandler, inheriting its middleware. ExplicitHEADroutes still win when present. The consumer is responsible for stripping the response body (a forthcomingcodesaur/http-applicationrelease will do this in its dispatcher). - Per-route middleware demo in
example/index.php. Three sample middleware classes (LoggingMiddleware,AuthMiddleware,TimingMiddleware) and four demo routes (/middleware-demo,/admin/secret,/middleware-chain, plus a?token=secretvariant) exercise the onion-model pipeline end-to-end.
Improved
- Public API surface minimised. Three files in
src/:Router.php,Route.php,RouterInterface.php. Public methods:__call,match,generate,pattern,getRoutes,registerName,registerMiddleware(plus the twoRoutemethods). Nothing else is exposed. match()performance. Always-3-element tuple means positional access ($result[2]) - no hash lookup for middleware. Direct destructuring[$callable, $params, $middleware] = $resultis the fastest pattern PHP offers for this shape.- Test suite expanded. From 54 tests / 103 assertions in v5.2.0 to
71 tests / 161 assertions in v6.0.0. New coverage: HEAD fallback (4),
per-route middleware (10, including per-method isolation and
GET_POSTfan-out),Routeobject (6), adapter pattern (4),getRoutes()2-tuple shape (2), strict name conflict detection (2). - PSR-12 sec. 4.3 compliance. All properties renamed off the
_prefix. @inheritDocused consistently. Concrete methods that implement interface contracts no longer duplicate the docblock - one source of truth inRouterInterface.
Migration guide (5.2.0 -> 6.0.0)
// === BEFORE (5.2.0) ===
$callback = $router->match($path, $method);
if ($callback instanceof Callback) {
$callable = $callback->getCallable();
$params = $callback->getParameters();
\call_user_func_array($callable, $params);
}
$mainRouter->merge($moduleRouter);
$patterns = $router->getNamePatterns();
// === AFTER (6.0.0) ===
$result = $router->match($path, $method);
if ($result !== null) {
[$callable, $params, $middleware] = $result;
// ... run middleware pipeline ...
\call_user_func_array($callable, $params);
}
// merge() is gone - an upcoming codesaur/http-application release will
// expose a delegation chain (just $app->use(new ModuleRouter())) so that
// modules can register their own routers without merging.
// getNamePatterns() is gone - getRoutes() no longer exposes names either.
// For naming, call generate($name) directly:
$url = $router->generate('news.view', ['id' => 10]);
// To iterate route definitions:
foreach ($router->getRoutes() as $pattern => $methods) {
foreach ($methods as $method => [$callable, $middleware]) {
echo "$method $pattern\n";
}
}
Removed
src/Callback.php(class deleted)tests/CallbackTest.php(10 tests removed; behaviour now covered byRouterTestagainst the match() 3-tuple)
[5.2.0] - 2026-05-12
[5.2.0]: https://github.com/codesaur-php/Router/compare/v5.1.1...v5.2.0
Added
pattern()method - Returns route pattern with filter prefixes stripped, suitable for client-side substitutionpattern('news-view')->/news/{id}/{slug}(instead of/news/{int:id}/{slug})- Throws
OutOfRangeExceptionif route name not found (consistent withgenerate()) - Required addition to
RouterInterface - Use case: server-rendered template emits the pattern, client-side JS performs substitution via
URL.replace('{id}', value) - Resolves the long-standing issue where
generate('route', ['id' => '_PLACEHOLDER_'])would throwInvalidArgumentExceptionbecause typed parameters ({int:},{uint:},{float:}) reject non-numeric placeholder strings - 4 new unit tests covering filter stripping, all filter types (
int/uint/float/utf8/default), static routes, and unknown-route exception (total suite: 54 tests, 103 assertions, all passing) /pattern-testexample route inexample/index.phpdemonstrating the full client-side workflow:- Table showing
pattern()output for every named route in the example - Side-by-side PHP template snippet (
<?= $router->pattern('hello') ?>) and the rendered JS output it produces - Interactive Run test button that performs JS
.replace()substitution on the emitted patterns andfetch()es the real/hello/Temujin/Khanand/sum/5/7endpoints, printing both status codes and response bodies - Auto-detects the script base path so the demo works under sub-directory installs (e.g.
/Router/example/)
Technical Details
- Single
preg_replaceover the existingFILTERS_REGEXconstant - O(n) on pattern length, no extra state - Zero impact on
match(),generate(), ormerge()- new method is additive - New API summary:
Method Purpose Use case match($path, $method)Find a registered route for an incoming request Request handling generate($name, $params)Build a fully resolved URL with validated parameters Server-rendered <a href="...">pattern($name)(new)Emit a placeholder pattern for client-side substitution JS URL.replace('{id}', value)
[5.1.1] - 2026-03-05
[5.1.1]: https://github.com/codesaur-php/Router/compare/v5.1.0...v5.1.1
Changed
- Removed all emoji characters from documentation and source files
- Replaced Unicode symbols with ASCII
[5.1.0] - 2026-03-04
[5.1.0]: https://github.com/codesaur-php/Router/compare/v5.0.0...v5.1.0
Added
- UTF-8 parameter type restored - Re-introduced
{utf8:param}parameter type (was in v4.0, removed in v5.0.0) - Supports multibyte characters in URL parameters (Cyrillic, CJK, Arabic, etc.)
- Matches both percent-encoded (
%D0%9C%D0%BE...) and raw UTF-8 (Монгол) paths - Works on all PHP servers: Apache, Nginx, LiteSpeed, IIS, Caddy, PHP built-in
- Usage:
$router->GET('/search/{utf8:query}', ...) UTF8_REGEXconstant - Regex pattern for UTF-8 parameters (\x80-\xFFbyte range)- Example route - Added
/unicode/{utf8:string}demo route - Displays Unicode code point, hex value, and byte length for each character
Technical Details
{utf8:}extendsDEFAULT_REGEXwith\x80-\xFFbyte range and space character- Zero impact on existing
{string},{int:},{uint:},{float:}parameters - no code or performance changes - Added
utf8:toFILTERS_REGEX:/{(int:|uint:|float:|utf8:)?(\w+)}/
[5.0.0] - 2026-01-08
[5.0.0]: https://github.com/codesaur-php/Router/compare/v4.0...v5.0.0
Added
- CI/CD workflow - Automated testing using GitHub Actions
- Tests on PHP 8.2, 8.3, 8.4 versions
- Tests on Ubuntu and Windows
- Code coverage measurement
- API documentation - API.md file (auto-generated from PHPDoc)
- Code review report - REVIEW.md file
- Comprehensive PHPDoc - Full documentation for all classes, methods, and properties
@constannotation on all constants- Method return types more specific (
@return static) - Callable types more detailed (
callable|array{class-string, string}) - Parameter type hints with array syntax (
array<string, mixed>) - Return type hints - Added to all methods
match()returnsCallback|nullgenerate()returnsstring(throws exception instead of returning null)- Type safety improvements
- Property type declarations (
protected array $routes = []) - Return type declarations on all methods
- Better type checking in method signatures
- Enhanced merge() method - Now also merges
name_patternsfrom Router instances - Example file improvements
- PHPDoc added to all methods
- Comments made more detailed
- README.md improvements
- Installation guide made more detailed
- More example code added
- Router merge, Matching & Dispatching sections made more detailed
- CI/CD badges added
Improved
- PHP version requirement - Upgraded from PHP 7.2+ to PHP 8.2.1+
- Modern PHP syntax - Switched from
array()to[]array syntax - PHPDoc standard - Fully compliant with PSR-5 standard
- Error handling -
generate()now throwsOutOfRangeExceptioninstead of returning null - Code structure - Better organization and readability
- Documentation - All documentation made more detailed and clear
- Type safety - Callable types made more specific
- Pattern matching - Direct pattern comparison for exact matches (performance improvement)
Removed
- UTF8 parameter support - Removed
utf8:parameter type that was in v4.0 (restored in [5.1.0]) - Legacy code - Removed old PHP 7.2 compatible syntax
[4.0] - 2021-10-06
[4.0]: https://github.com/codesaur-php/Router/compare/v1.0...v4.0
Added
- Callback class - Introduced separate
Callbackclass to wrap callable and parameters - Replaces Route class approach from v1.0
- Stores callable and route parameters separately
- Simplified routing structure - Routes stored as associative array with pattern as key
- Structure:
[pattern => [method => Callback]] - More efficient route lookup
- UTF8 parameter support - Added
utf8:parameter type for UTF-8 encoded strings - Example:
/news/{utf8:title} - Automatically URL decodes UTF-8 parameters
- RouterInterface improvements - Expanded interface with new methods
- Added
getRoutes()method requirement - Added
merge()method requirement - Route naming system - Enhanced name-based routing
name_patternsarray maps route names to patterns- Better reverse routing support
Improved
- Architecture simplification - Removed Route class, simplified to Callback-based approach
- Route matching - Returns
Callbackobject directly instead ofRoute - Parameter parsing - Better type conversion for int, uint, and float parameters
- Pattern regex generation - Improved
getPatternRegex()method - URL encodes static path parts
- Better regex pattern generation
- Method chaining -
__call()returns&$thisfor fluent interface - Error handling - Better exception messages with class context
Removed
- Route class - Completely removed Route class
- Pipe property - Removed
_pipeproperty for route prefix (present in v1.0) - Strict types - Removed
declare(strict_types=1)(was in v1.0) - HTTP method constants - Removed
HTTP_REQUEST_METHODSconstant - Complex route configuration - Simplified route registration
Changed
- match() return type - Now returns
Callback|nullinstead ofRoute|null - generate() behavior - Now throws
OutOfRangeExceptioninstead of returning null - Route storage - Changed from Route objects array to pattern-based associative array
- Interface methods -
RouterInterfacemethods changed signature
[1.0] - 2021-03-02
[1.0]: https://github.com/codesaur-php/Router/releases/tag/v1.0
Added
- Initial release - First stable version of codesaur/router
- Router class - Main routing class with full routing capabilities
- Route class - Separate Route class to encapsulate route information
- Stores methods, pattern, callback, name, params, and filters
- Has getter/setter methods for all properties
- RouterInterface - Interface defining routing contract
- Route prefix support -
_pipeproperty for route prefixes - Allows setting base path prefix for all routes
- Dynamic parameter support - Support for typed route parameters
{int:id}- Integer parameters (supports negative numbers){uint:page}- Unsigned integer parameters (0 and positive){float:price}- Float parameters{string:slug}- String parameters (default)- HTTP method support - Support for all standard HTTP methods
- GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
ANYmethod for all HTTP methods- Multiple methods per route support
- Route matching -
match()method finds routes by path and method - Returns Route object with parameters set
- Automatic parameter type conversion
- Reverse routing -
generate()method creates URLs from route names - Parameter validation and type checking
- Returns null if route not found (logs error in development mode)
- Route naming -
name()method for naming routes - Allows finding routes by name
- Enables reverse routing
- Route merging -
merge()method to combine multiple routers - Useful for modular applications
- Parameter filters - Automatic filter assignment based on parameter type
- Type-specific regex patterns
- Parameter validation during generation
- Strict types - Uses
declare(strict_types=1)for type safety
Technical Details
- PHP version: PHP 7.2+ required
- Array syntax: Uses
array()syntax (pre-PHP 5.4 style) - Type declarations: Basic type hints, no return types
- Route storage: Array of Route objects
- Pattern matching: Regex-based pattern matching with parameter extraction
Architecture
- Object-oriented design - Full OOP with classes and interfaces
- Separation of concerns - Route class separate from Router class
- Extensible - Interface-based design allows custom implementations