From 3ca79aac5d813571638a88a41ede3bda63c5c4d2 Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 19:17:32 +0100 Subject: [PATCH 01/13] Implement addMatchTypes method in Routes.php Added a method to allow custom match types for AltoRouter. --- Routes.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Routes.php b/Routes.php index 32e6732..565ffa5 100755 --- a/Routes.php +++ b/Routes.php @@ -72,6 +72,24 @@ public static function map($route, $callback, $name = '') { $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', untrailingslashit($route), $callback, $name); } + /** + * Wrapper for AltoRouter's addMatchTypes function. See AltoRouter documentation for more details. + * + * @api + * @link https://dannyvankooten.github.io/AltoRouter//usage/mapping-routes.html + * + * @param array $match_types An array of custom match types to add to AltoRouter. + * Keys are type names and values are regex patterns. + * ex: Routes::addMatchTypes(['hex' => '[0-9A-Fa-f]+']); + */ + public static function addMatchTypes($match_types) + { + global $upstatement_routes; + if (isset($upstatement_routes->router)) { + $upstatement_routes->router->addMatchTypes($match_types); + } + } + /** * @return string A string in a format for AltoRouter * ex: [:my_param] From 138c0327f6c69a69a538ea272e537ae4d3bfdd79 Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 19:23:11 +0100 Subject: [PATCH 02/13] Document addMatchTypes method in README Added documentation for the addMatchTypes method in README.md. --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 501f0f9..d3f09c0 100644 --- a/README.md +++ b/README.md @@ -125,3 +125,26 @@ The query you want to use, it can accept a string or array just like `Timber::ge `$status_code` Send an optional status code. Defaults to 200 for 'Success/OK' + +## addMatchTypes +This method makes it possbile to add custom matchtypes in Routes. + +```php + '@[0-9]++', +]); + +Routes::map( + '[oldID:id]/[:slug]', + function ($params) { + $old_id = $params['id']; + $slug = $params['slug']; + + /* the rest as normal... */ + Timber::render('single.php', $context); + } +); +``` From c6049675ae0428567107b0becb12361fed6495bd Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 21:25:03 +0100 Subject: [PATCH 03/13] Update Routes.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Routes.php b/Routes.php index 565ffa5..4e3f051 100755 --- a/Routes.php +++ b/Routes.php @@ -76,7 +76,7 @@ public static function map($route, $callback, $name = '') { * Wrapper for AltoRouter's addMatchTypes function. See AltoRouter documentation for more details. * * @api - * @link https://dannyvankooten.github.io/AltoRouter//usage/mapping-routes.html + * @link https://dannyvankooten.github.io/AltoRouter/usage/mapping-routes.html * * @param array $match_types An array of custom match types to add to AltoRouter. * Keys are type names and values are regex patterns. From 8b9cad653bf5397896993e57f950f792893772d2 Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 21:25:42 +0100 Subject: [PATCH 04/13] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d3f09c0..39e6181 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ The query you want to use, it can accept a string or array just like `Timber::ge Send an optional status code. Defaults to 200 for 'Success/OK' ## addMatchTypes -This method makes it possbile to add custom matchtypes in Routes. +This method makes it possbile to add custom match types in Routes. ```php Date: Mon, 23 Mar 2026 22:15:43 +0100 Subject: [PATCH 05/13] refactor: update Routes class to implement singleton pattern and add match types functionality --- Routes.php | 148 ++++++++++++++++++++++++++++--------------- tests/RoutesTest.php | 119 +++++++++++++++++++++++----------- 2 files changed, 178 insertions(+), 89 deletions(-) diff --git a/Routes.php b/Routes.php index 1e3df4c..8255e59 100755 --- a/Routes.php +++ b/Routes.php @@ -7,7 +7,7 @@ * Author: Jared Novack + Upstatement * Author URI: http://www.upstatement.com * Text Domain: routes - * Version: 0.9.2 + * Version: 0.9.2. */ /** @@ -15,44 +15,80 @@ * It uses the AltoRouter library to match the current request to the defined routes, * and to call the appropriate callback function when a route is matched. * It also provides a method for loading a template file and sending data to it, which can be used in the callback functions for the routes defined with the map() method. - * - * @package Routes */ class Routes { /** * The AltoRouter instance used to match the current request to the defined routes. - * - * @var AltoRouter */ - protected $router; + protected ?AltoRouter $router = null; /** - * Constructor. + * The singleton instance of the Routes class. + */ + private static ?self $instance = null; + + /** + * Private constructor to enforce the singleton pattern. * * Adds the match_current_request function to the init and wp_loaded hooks, * which will check if the current request matches any of the routes defined in this plugin, * and if so, will call the appropriate callback function. */ - public function __construct() + private function __construct() { add_action('init', [$this, 'match_current_request']); add_action('wp_loaded', [$this, 'match_current_request']); } + /** + * Returns the singleton instance, creating it if it does not yet exist. + */ + public static function get_instance(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Initializes the AltoRouter instance if it has not been created yet. + * Called lazily by map() and add_match_types(). + */ + private function ensure_router(): void + { + if (null !== $this->router) { + return; + } + $this->router = new AltoRouter(); + $site_url = get_bloginfo('url'); + $site_url_parts = explode('/', $site_url); + $site_url_parts = array_slice($site_url_parts, 3); + $base_path = implode('/', $site_url_parts); + if (!$base_path) { + $base_path = '/'; + } else { + $base_path = '/' . $base_path . '/'; + } + // Clean any double slashes that have resulted + $base_path = str_replace('//', '/', $base_path); + $this->router->setBasePath($base_path); + } + /** * Checks if the current request matches any of the routes defined in this plugin, * and if so, calls the appropriate callback function. * * @internal */ - public static function match_current_request() + public function match_current_request() { - global $upstatement_routes; - if (isset($upstatement_routes->router)) { - $route = $upstatement_routes->router->match(); + if (null !== $this->router) { + $route = $this->router->match(); - unset($upstatement_routes->router); + $this->router = null; if ($route && isset($route['target'])) { if (isset($route['params'])) { @@ -64,16 +100,34 @@ public static function match_current_request() } } + /** + * Wrapper for AltoRouter's addMatchTypes function. See AltoRouter documentation for more details. + * + * @api + * + * @link https://dannyvankooten.github.io/AltoRouter/usage/mapping-routes.html + * + * @param array $match_types An array of custom match types to add to AltoRouter. + * Keys are type names and values are regex patterns. + * ex: Routes::add_match_types(['hex' => '[0-9A-Fa-f]+']); + */ + public static function add_match_types($match_types) + { + $instance = self::get_instance(); + $instance->ensure_router(); + $instance->router->addMatchTypes($match_types); + } + /** * Maps a route to a callback function. * * @api - * @param string $route A string to match (ex: 'myfoo'). - * @param callable $callback A callback function to call when the route is matched. - * This can be a string for a function name, - * an array for a class method, or an anonymous function. - * @param string $name An optional name for the route, which can be used to generate URLs with the url() method. - * @return void + * + * @param string $route a string to match (ex: 'myfoo') + * @param callable $callback A callback function to call when the route is matched. + * This can be a string for a function name, + * an array for a class method, or an anonymous function. + * @param string $name an optional name for the route, which can be used to generate URLs with the url() method * * @example * ```php @@ -86,38 +140,25 @@ public static function match_current_request() */ public static function map($route, $callback, $name = '') { - global $upstatement_routes; - if (!isset($upstatement_routes->router)) { - $upstatement_routes->router = new AltoRouter(); - $site_url = get_bloginfo('url'); - $site_url_parts = explode('/', $site_url); - $site_url_parts = array_slice($site_url_parts, 3); - $base_path = implode('/', $site_url_parts); - if (!$base_path || str_starts_with($route, $base_path)) { - $base_path = '/'; - } else { - $base_path = '/' . $base_path . '/'; - } - // Clean any double slashes that have resulted - $base_path = str_replace('//', '/', $base_path); - $upstatement_routes->router->setBasePath($base_path); - } + $instance = self::get_instance(); + $instance->ensure_router(); $route = self::convert_route($route); - $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', trailingslashit($route), $callback, $name); - $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', untrailingslashit($route), $callback, $name); + $instance->router->map('GET|POST|PUT|DELETE|HEAD', trailingslashit($route), $callback, $name); + $instance->router->map('GET|POST|PUT|DELETE|HEAD', untrailingslashit($route), $callback, $name); } /** - * * Used internally to convert a route string with :param style parameters * to the format used by AltoRouter, which is [:param]. * If the route string already contains [ and ] characters, * it is assumed to be in the correct format and is returned unchanged. * * @internal - * @param string $route_string A route string with :param style parameters (ex: 'myfoo/:my_param'). - * @return string A string in a format for AltoRouter - * ex: [:my_param] + * + * @param string $route_string a route string with :param style parameters (ex: 'myfoo/:my_param') + * + * @return string A string in a format for AltoRouter + * ex: [:my_param] */ public static function convert_route($route_string) { @@ -132,6 +173,7 @@ public static function convert_route($route_string) if (str_starts_with($route_string, '/')) { $route_string = substr($route_string, 1); } + return $route_string; } @@ -140,12 +182,13 @@ public static function convert_route($route_string) * to load a specific template file when a route is matched, and to send data to that template file. * * @api - * @param string $template A php file to load (ex: 'single.php'). - * @param array|bool $tparams An array of data to send to the php file. Inside the php file this data can be accessed via: `global $params;`. - - * @param WP_Query $query Use a WP_Query object in the template file instead of the default query. - * @param int $status_code A code for the status (ex: 200). - * @param int $priority The priority used by the "template_include" filter. + * + * @param string $template A php file to load (ex: 'single.php'). + * @param array|bool $tparams An array of data to send to the php file. Inside the php file this data can be accessed via: `global $params;`. + * @param WP_Query $query use a WP_Query object in the template file instead of the default query + * @param int $status_code a code for the status (ex: 200) + * @param int $priority the priority used by the "template_include" filter + * * @return bool */ public static function load($template, $tparams = false, $query = false, $status_code = 200, $priority = 10) @@ -163,8 +206,8 @@ public static function load($template, $tparams = false, $query = false, $status 'status_header', function ($status_header, $header, $text, $protocol) use ($status_code) { $text = get_status_header_desc($status_code); - $header_string = "$protocol $status_code $text"; - return $header_string; + + return "{$protocol} {$status_code} {$text}"; }, 10, 4 @@ -214,17 +257,18 @@ function () use ($query) { if ($template) { add_filter( 'template_include', - fn() => $template, + fn () => $template, $priority ); + return true; } + return false; } } -global $upstatement_routes; -$upstatement_routes = new Routes(); +Routes::get_instance(); if ( file_exists($composer_autoload = __DIR__ . '/vendor/autoload.php') diff --git a/tests/RoutesTest.php b/tests/RoutesTest.php index 4460d81..219f7f0 100644 --- a/tests/RoutesTest.php +++ b/tests/RoutesTest.php @@ -2,41 +2,45 @@ use Mantle\Testkit\Integration_Test_Case; +/** + * @internal + * + * @coversNothing + */ class RoutesTest extends Integration_Test_Case { - - function testThemeRoute() + public function testThemeRoute() { $template = Routes::load(__DIR__ . '/Supports/single.php'); $this->assertTrue($template); } - function testThemeRouteDoesntExist() + public function testThemeRouteDoesntExist() { $template = Routes::load('singlefoo.php'); $this->assertFalse($template); } - function testFullPathRoute() + public function testFullPathRoute() { $hello = WP_CONTENT_DIR . '/plugins/hello.php'; $template = Routes::load($hello); $this->assertTrue($template); } - function testFullPathRouteDoesntExist() + public function testFullPathRouteDoesntExist() { $hello = WP_CONTENT_DIR . '/plugins/hello-foo.php'; $template = Routes::load($hello); $this->assertFalse($template); } - function testRouterClass() + public function testRouterClass() { $this->assertTrue(class_exists('AltoRouter')); } - function testAppliedRoute() + public function testAppliedRoute() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -56,7 +60,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testRouteWithVariable() + public function testRouteWithVariable() { $post_name = 'ziggy'; $post = $this->factory->post->create( @@ -70,7 +74,7 @@ function testRouteWithVariable() $phpunit = $this; Routes::map( 'mything/:slug', - function ($params) use ($phpunit) { + function ($params) { global $matches; $matches = []; if ('ziggy' == $params['slug']) { @@ -83,7 +87,7 @@ function ($params) use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testRouteWithAltoVariable() + public function testRouteWithAltoVariable() { $post_name = 'ziggy'; $post = $this->factory->post->create( @@ -97,7 +101,7 @@ function testRouteWithAltoVariable() $phpunit = $this; Routes::map( 'mything/[*:slug]', - function ($params) use ($phpunit) { + function ($params) { global $matches; $matches = []; if ('ziggy' == $params['slug']) { @@ -110,18 +114,18 @@ function ($params) use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testRouteWithMultiArguments() + public function testRouteWithMultiArguments() { $phpunit = $this; Routes::map( 'artist/[:artist]/song/[:song]', - function ($params) use ($phpunit) { + function ($params) { global $matches; $matches = []; - if ($params['artist'] == 'smashing-pumpkins') { + if ('smashing-pumpkins' == $params['artist']) { $matches[] = true; } - if ($params['song'] == 'mayonaise') { + if ('mayonaise' == $params['song']) { $matches[] = true; } } @@ -132,19 +136,19 @@ function ($params) use ($phpunit) { $this->assertEquals(2, count($matches)); } - function testRouteWithMultiArgumentsOldStyle() + public function testRouteWithMultiArgumentsOldStyle() { $phpunit = $this; global $matches; Routes::map( 'studio/:studio/movie/:movie', - function ($params) use ($phpunit) { + function ($params) { global $matches; $matches = []; - if ($params['studio'] == 'universal') { + if ('universal' == $params['studio']) { $matches[] = true; } - if ($params['movie'] == 'brazil') { + if ('brazil' == $params['movie']) { $matches[] = true; } } @@ -154,7 +158,7 @@ function ($params) use ($phpunit) { $this->assertEquals(2, count($matches)); } - function testRouteAgainstPostName() + public function testRouteAgainstPostName() { $post_name = 'jared'; $post = $this->factory->post->create( @@ -180,7 +184,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testVerySimpleRoute() + public function testVerySimpleRoute() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -188,7 +192,7 @@ function testVerySimpleRoute() $phpunit = $this; Routes::map( 'crackers', - function () use ($phpunit) { + function () { global $matches; $matches = []; $matches[] = true; @@ -199,7 +203,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testVerySimpleRouteTrailingSlash() + public function testVerySimpleRouteTrailingSlash() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -207,7 +211,7 @@ function testVerySimpleRouteTrailingSlash() $phpunit = $this; Routes::map( 'bip/', - function () use ($phpunit) { + function () { global $matches; $matches = []; $matches[] = true; @@ -218,7 +222,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testVerySimpleRouteTrailingSlashInRequest() + public function testVerySimpleRouteTrailingSlashInRequest() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -226,7 +230,7 @@ function testVerySimpleRouteTrailingSlashInRequest() $phpunit = $this; Routes::map( 'bopp', - function () use ($phpunit) { + function () { global $matches; $matches = []; $matches[] = true; @@ -237,8 +241,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - - function testVerySimpleRouteTrailingSlashInRequestAndMapping() + public function testVerySimpleRouteTrailingSlashInRequestAndMapping() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -246,7 +249,7 @@ function testVerySimpleRouteTrailingSlashInRequestAndMapping() $phpunit = $this; Routes::map( 'zappers', - function () use ($phpunit) { + function () { global $matches; $matches = []; $matches[] = true; @@ -257,7 +260,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testVerySimpleRoutePreceedingSlash() + public function testVerySimpleRoutePreceedingSlash() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -265,7 +268,7 @@ function testVerySimpleRoutePreceedingSlash() $phpunit = $this; Routes::map( '/gobbles', - function () use ($phpunit) { + function () { global $matches; $matches = []; $matches[] = true; @@ -276,7 +279,7 @@ function () use ($phpunit) { $this->assertEquals(1, count($matches)); } - function testFailedRoute() + public function testFailedRoute() { $_SERVER['REQUEST_METHOD'] = 'GET'; global $matches; @@ -295,7 +298,7 @@ function () use ($phpunit) { $this->assertEquals(0, count($matches)); } - function testRouteWithClassCallback() + public function testRouteWithClassCallback() { Routes::map('classroute', ['RoutesTest', '_testCallback']); $this->get(home_url('classroute')); @@ -304,13 +307,55 @@ function testRouteWithClassCallback() $this->assertEquals(1, count($matches_class_test)); } - function matchRoutes() + public function testAddMatchTypes() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + Routes::add_match_types(['hex' => '[0-9A-Fa-f]+']); + Routes::map( + 'color/[hex:color]', + function ($params) { + global $matches; + $matches = []; + if ('ff5733' === $params['color']) { + $matches[] = true; + } + } + ); + $this->get(home_url('/color/ff5733')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + public function testAddMatchTypesBeforeMap() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + // Calling add_match_types before map() should still work + Routes::add_match_types(['word' => '\w+']); + Routes::map( + 'tag/[word:name]', + function ($params) { + global $matches; + $matches = []; + if ('hello' === $params['name']) { + $matches[] = true; + } + } + ); + $this->get(home_url('/tag/hello')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + public function matchRoutes() { - global $upstatement_routes; - $upstatement_routes->match_current_request(); + Routes::get_instance()->match_current_request(); } - static function _testCallback() + public static function _testCallback() { global $matches_class_test; $matches_class_test[] = true; From 310e86dbd9e3be9ec431c50e008831e1400d654b Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 22:18:02 +0100 Subject: [PATCH 06/13] docs: add documentation for add_match_types method in README --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 0db8c11..076ef03 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,27 @@ The query you want to use, it can accept a string or array just like `Timber::ge `$status_code` Send an optional status code. Defaults to 200 for 'Success/OK' + +## add_match_types + +This method makes it possible to add custom match types in Routes. + +```php + '@[0-9]++', +]); + +Routes::map( + '[oldID:id]/[:slug]', + function ($params) { + $old_id = $params['id']; + $slug = $params['slug']; + + /* the rest as normal... */ + Timber::render('single.php', $context); + } +); +``` From 9251a83cb172ce81aeb21d87ac05228c2d1ce9f4 Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 23:06:23 +0100 Subject: [PATCH 07/13] fix: simplify base path handling and improve router null check in match_current_request method --- Routes.php | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/Routes.php b/Routes.php index 88580b5..b459d36 100755 --- a/Routes.php +++ b/Routes.php @@ -67,13 +67,7 @@ private function ensure_router(): void $site_url_parts = explode('/', $site_url); $site_url_parts = array_slice($site_url_parts, 3); $base_path = implode('/', $site_url_parts); - if (!$base_path) { - $base_path = '/'; - } else { - $base_path = '/' . $base_path . '/'; - } - // Clean any double slashes that have resulted - $base_path = str_replace('//', '/', $base_path); + $base_path = $base_path ? '/' . trim($base_path, '/') . '/' : '/'; $this->router->setBasePath($base_path); } @@ -85,18 +79,15 @@ private function ensure_router(): void */ public function match_current_request() { - if (null !== $this->router) { - $route = $this->router->match(); + if (null == $this->router) { + return; + } - $this->router = null; + $route = $this->router->match(); + $this->router = null; - if ($route && isset($route['target'])) { - if (isset($route['params'])) { - call_user_func($route['target'], $route['params']); - } else { - call_user_func($route['target']); - } - } + if ($route && isset($route['target'])) { + call_user_func($route['target'], ...array_filter([$route['params'] ?? null])); } } From d7057976931f1709f53cfd0504e2b7b862a165c5 Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 23:09:26 +0100 Subject: [PATCH 08/13] fix: update match_current_request method calls to use instance context --- Routes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Routes.php b/Routes.php index b459d36..5f23ab4 100755 --- a/Routes.php +++ b/Routes.php @@ -37,8 +37,8 @@ class Routes */ private function __construct() { - add_action('init', [self::class, 'match_current_request']); - add_action('wp_loaded', [self::class, 'match_current_request']); + add_action('init', [$this, 'match_current_request']); + add_action('wp_loaded', [$this, 'match_current_request']); } /** From c84cf1ad27fd9fc196cdbf73740ba55329151216 Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 23:14:16 +0100 Subject: [PATCH 09/13] fix: reorder class properties for clarity in Routes class --- Routes.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Routes.php b/Routes.php index 5f23ab4..b5444d7 100755 --- a/Routes.php +++ b/Routes.php @@ -19,14 +19,14 @@ class Routes { /** - * The AltoRouter instance used to match the current request to the defined routes. + * The singleton instance of the Routes class. */ - protected ?AltoRouter $router = null; + private static ?self $instance = null; /** - * The singleton instance of the Routes class. + * The AltoRouter instance used to match the current request to the defined routes. */ - private static ?self $instance = null; + protected ?AltoRouter $router = null; /** * Private constructor to enforce the singleton pattern. From c39436ccc822aaf2d97816de10267b929ac295ff Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 23:25:17 +0100 Subject: [PATCH 10/13] refactor: clean up plugin header comments and remove unused autoload checks --- Routes.php | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/Routes.php b/Routes.php index b5444d7..490eceb 100755 --- a/Routes.php +++ b/Routes.php @@ -1,16 +1,8 @@ Date: Mon, 23 Mar 2026 23:26:39 +0100 Subject: [PATCH 11/13] docs: update installation instructions in README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 076ef03..9a6c84e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,22 @@ Simple routing for WordPress. Designed for usage with [Timber](https://github.co [![PHP unit tests](https://github.com/Upstatement/routes/actions/workflows/php-unit-tests.yml/badge.svg?branch=2.x)](https://github.com/Upstatement/routes/actions/workflows/php-unit-tests.yml?query=branch:2.x) [![Latest Stable Version](https://img.shields.io/packagist/v/Upstatement/routes.svg?style=flat-square)](https://packagist.org/packages/Upstatement/routes) +## Installation + +Install via [Composer](https://getcomposer.org/): + +```bash +composer require upstatement/routes +``` + +Then make sure Composer's autoloader is included in your project. In a standard WordPress setup this is typically done in `functions.php` or your plugin's main file: + +```php +require_once __DIR__ . '/vendor/autoload.php'; +``` + +If you're using a WordPress-specific Composer setup (e.g. [Bedrock](https://roots.io/bedrock/)), the autoloader is usually already loaded for you. + ### Basic Usage ```php From eeb5e36554764a3eed5e09d3850e341b4928aa0b Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Mon, 23 Mar 2026 23:52:29 +0100 Subject: [PATCH 12/13] docs: update README for version 1.0.0 breaking changes and new features --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ Routes.php | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a6c84e..3044ddc 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,56 @@ require_once __DIR__ . '/vendor/autoload.php'; If you're using a WordPress-specific Composer setup (e.g. [Bedrock](https://roots.io/bedrock/)), the autoloader is usually already loaded for you. +## Upgrading to 1.x + +Version 1.0 introduces several breaking changes. If you're upgrading from 0.x, read through the following sections to make sure your code is compatible. + +### PHP 8.2+ required + +Routes now requires PHP 8.2 or higher. + +### Singleton — no direct instantiation + +The class is now a singleton. You can no longer instantiate it directly: + +```php +// Before (0.x) — no longer works +$routes = new Routes(); + +// After (1.x) +$instance = Routes::get_instance(); +``` + +In practice most applications never instantiated `Routes` directly, so this is unlikely to affect you. + +### No more global `$upstatement_routes` + +The global variable `$upstatement_routes` used internally in 0.x has been removed. If your code accessed called `$upstatement_routes->match_current_request()` directly, update it: + +```php +// Before (0.x) +global $upstatement_routes; +$upstatement_routes->match_current_request(); + +// After (1.x) +Routes::get_instance()->match_current_request(); +``` + +### New `add_match_types()` method + +Custom AltoRouter match types can now be registered via `Routes::add_match_types()` and may be called before or after `Routes::map()`: + +```php +Routes::add_match_types(['hex' => '[0-9A-Fa-f]+']); +Routes::map('color/[hex:color]', function($params) { + // $params['color'] is guaranteed to be a hex string +}); +``` + +### Class is no longer auto-instantiated on include + +In 0.x, including `Routes.php` immediately instantiated the class and registered WordPress hooks. In 1.x, the singleton is created lazily on the first call to `Routes::map()` or `Routes::add_match_types()`. No action is required as long as you call `Routes::map()` before `wp_loaded` fires, which is the standard usage pattern. + ### Basic Usage ```php diff --git a/Routes.php b/Routes.php index 490eceb..151e1f8 100755 --- a/Routes.php +++ b/Routes.php @@ -18,7 +18,7 @@ class Routes /** * The version of the library. */ - public static $version = '0.9.2'; // x-release-please-version + public static $version = '1.0.0'; // x-release-please-version /** * The AltoRouter instance used to match the current request to the defined routes. */ From 1ce481bfea3a385c19aa2e84daedb47eb4ea895b Mon Sep 17 00:00:00 2001 From: Erik van der Bas Date: Wed, 15 Apr 2026 08:21:21 +0200 Subject: [PATCH 13/13] docs: add section on base path handling for subdirectory installations in README --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 3044ddc..20a8609 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,47 @@ Routes::map('color/[hex:color]', function($params) { In 0.x, including `Routes.php` immediately instantiated the class and registered WordPress hooks. In 1.x, the singleton is created lazily on the first call to `Routes::map()` or `Routes::add_match_types()`. No action is required as long as you call `Routes::map()` before `wp_loaded` fires, which is the standard usage pattern. +### Base path handling for subdirectory installs + +**⚠️ Potential breaking change for WordPress subdirectory installations** + +In 0.x, the base path logic was route-dependent and would detect if you included the subdirectory in your route pattern. In 1.x, the base path is always calculated from your site URL regardless of your route patterns. + +If your WordPress site is installed in a subdirectory (e.g., `https://example.com/blog/`) and your routes **included** that subdirectory in the pattern, those routes will break in 1.x. + +**Example of code that will break:** + +```php +// Site URL: https://example.com/blog/ +// WordPress installed in /blog/ subdirectory + +// This route WILL BREAK in 1.x: +Routes::map('blog/my-page', function($params) { + // This won't match anymore because AltoRouter strips /blog/ + // and then tries to match the remainder against 'blog/my-page' +}); +``` + +**How to fix it:** + +Simply remove the subdirectory prefix from your route patterns: + +```php +// Site URL: https://example.com/blog/ +// WordPress installed in /blog/ subdirectory + +// ✅ Correct way to define routes in 1.x: +Routes::map('my-page', function($params) { + // This will correctly match https://example.com/blog/my-page +}); + +Routes::map('my-users/:userid/edit', function($params) { + // This will correctly match https://example.com/blog/my-users/123/edit +}); +``` + +**Note:** If your site is **not** in a subdirectory, or if your routes never included the subdirectory prefix, this change does not affect you. + ### Basic Usage ```php