From a29ca0afb8bc804bac35a476a2d42002c5df96c3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 26 Oct 2026 20:22:16 -0500 Subject: [PATCH 1/3] [CP-17693] - optimizing router - getindexdata Assisted-by: Claude Code --- phalcon/Mvc/Router/Route.zep | 11 ++- phalcon/Support/Registry.zep | 25 ++---- .../Mvc/Router/Route/GetIndexDataTest.php | 79 +++++++++++++++++++ 3 files changed, 95 insertions(+), 20 deletions(-) diff --git a/phalcon/Mvc/Router/Route.zep b/phalcon/Mvc/Router/Route.zep index 8323d35c2f..e61aed5b96 100644 --- a/phalcon/Mvc/Router/Route.zep +++ b/phalcon/Mvc/Router/Route.zep @@ -584,15 +584,22 @@ class Route implements RouteInterface * Returns the data that the router needs for its method index, with * one call: the HTTP methods, the compiled pattern, the host name, the * compiled host name, the beforeMatch callback and the route id, in - * this order. + * this order. A route with no host name has no compiled host name, so + * there is no getCompiledHostName() call for it. */ public function getIndexData() -> array { + var compiledHostName = null; + + if this->hostname !== null { + let compiledHostName = this->getCompiledHostName(); + } + return [ this->methods, this->compiledPattern, this->hostname, - this->getCompiledHostName(), + compiledHostName, this->beforeMatch, this->routeId ]; diff --git a/phalcon/Support/Registry.zep b/phalcon/Support/Registry.zep index 83340b6b0e..b56c803fb4 100644 --- a/phalcon/Support/Registry.zep +++ b/phalcon/Support/Registry.zep @@ -18,7 +18,7 @@ use Traversable; * available throughout your application. * *```php - * $registry = new \Phalcon\Registry(); + * $registry = new \Phalcon\Support\Registry(); * * // Set value * $registry->something = "something"; @@ -41,24 +41,13 @@ use Traversable; * unset($registry["something"]); *``` * - * In addition to ArrayAccess, Phalcon\Registry also implements Countable - * (count($registry) will return the number of elements in the registry), - * Serializable and Iterator (you can iterate over the registry using a foreach - * loop) interfaces. For PHP 5.4 and higher, JsonSerializable interface is - * implemented. + * Phalcon\Support\Registry extends Phalcon\Support\Collection. It implements + * ArrayAccess, Countable, IteratorAggregate and JsonSerializable. Use + * count($registry) to get the number of elements, and use foreach to iterate + * over the registry. * - * Phalcon\Registry is very fast (it is typically faster than any userspace - * implementation of the registry); however, this comes at a price: - * Phalcon\Registry is a final class and cannot be inherited from. - * - * Though Phalcon\Registry exposes methods like __get(), offsetGet(), count() etc, - * it is not recommended to invoke them manually (these methods exist mainly to - * match the interfaces the registry implements): $registry->__get("property") - * is several times slower than $registry->property. - * - * Internally all the magic methods (and interfaces except JsonSerializable) - * are implemented using object handlers or similar techniques: this allows to - * bypass relatively slow method calls. + * The keys are not case-sensitive. Phalcon\Support\Registry is a final class. + * You cannot extend it. * * @extends Collection */ diff --git a/tests/unit/Mvc/Router/Route/GetIndexDataTest.php b/tests/unit/Mvc/Router/Route/GetIndexDataTest.php index a1ceaeb702..3a00daaac5 100644 --- a/tests/unit/Mvc/Router/Route/GetIndexDataTest.php +++ b/tests/unit/Mvc/Router/Route/GetIndexDataTest.php @@ -13,11 +13,28 @@ namespace Phalcon\Tests\Unit\Mvc\Router\Route; +use Phalcon\Events\Manager as EventsManager; use Phalcon\Mvc\Router\Route; use Phalcon\Talon\PHPUnit\AbstractUnitTestCase; +use Phalcon\Tests\Unit\Mvc\Fake\RouterTrait; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\DataProvider; final class GetIndexDataTest extends AbstractUnitTestCase { + use RouterTrait; + + /** + * @return array + */ + public static function getEventsManagerCases(): array + { + return [ + 'no events manager' => [false], + 'events manager' => [true], + ]; + } + /** * getIndexData() returns the values of the six getters, in order. * @@ -43,6 +60,50 @@ public function testMvcRouterRouteGetIndexData(): void ); } + /** + * getIndexData() does not call getCompiledHostName() for a route with no + * host name. A subclass that overrides getCompiledHostName() gets null. + * + * @author Phalcon Team + * @since 2026-10-26 + */ + public function testMvcRouterRouteGetIndexDataNoHostnameNoCompiledHostNameCall(): void + { + $route = $this->getNoHostnameRoute(); + + $actual = $route->getIndexData(); + + $this->assertNull($actual[3]); + $this->assertSame(0, $route->calls); + } + + /** + * The router does not read the compiled host name of a route with no + * host name: the route matches with any host, with and without an + * events manager. + * + * @author Phalcon Team + * @since 2026-10-26 + */ + #[BackupGlobals(true)] + #[DataProvider('getEventsManagerCases')] + public function testMvcRouterRouteGetIndexDataNoHostnameRoutingUnchanged(bool $events): void + { + $route = $this->getNoHostnameRoute(); + $router = $this->getRouter(false); + if ($events) { + $router->setEventsManager(new EventsManager()); + } + $router->attach($route); + + $_SERVER['HTTP_HOST'] = 'www.other.com'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $router->handle('/users'); + + $this->assertTrue($router->wasMatched()); + $this->assertSame($route, $router->getMatchedRoute()); + } + /** * getIndexData() returns the compiled host name and the beforeMatch * callback, and the compiled host name stays in the route. @@ -72,4 +133,22 @@ public function testMvcRouterRouteGetIndexDataWithHostnameAndBeforeMatch(): void ); $this->assertSame($actual[3], $route->getCompiledHostName()); } + + /** + * A route with no host name. Its getCompiledHostName() counts the calls + * and returns a host regex. + */ + private function getNoHostnameRoute(): Route + { + return new class ('/users', ['controller' => 'users']) extends Route { + public int $calls = 0; + + public function getCompiledHostName(): ?string + { + $this->calls++; + + return '#^api\.example\.com$#i'; + } + }; + } } From f6db2f48d54871ebaca9aea658652f575c4f3cc8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 26 Oct 2026 20:26:54 -0500 Subject: [PATCH 2/3] [CP-17693] - updating changelog Assisted-by: Claude Code --- CHANGELOG-5.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index eb6a3697df..0f483ec6d0 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -15,6 +15,7 @@ All notable changes are documented here. The format is based on [Keep a Changelo ### Changed - Faster item storage, lookup, removal and JSON serialization in `Phalcon\Support\Collection`. [#17683](https://github.com/phalcon/cphalcon/issues/17683) +- Faster route index build in `Phalcon\Mvc\Router` for routes with no host name. [#17693](https://github.com/phalcon/cphalcon/issues/17693) ### Added From 744ead50464336680e715630bfcccf2765cc7e40 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 26 Oct 2026 20:27:35 -0500 Subject: [PATCH 3/3] [CP-17693] - refreshing ext --- ext/phalcon/mvc/router/route.zep.c | 57 +++++++++++++++++------------- ext/phalcon/support/registry.zep.c | 25 ++++--------- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/ext/phalcon/mvc/router/route.zep.c b/ext/phalcon/mvc/router/route.zep.c index 4679c83054..831c78ba8d 100644 --- a/ext/phalcon/mvc/router/route.zep.c +++ b/ext/phalcon/mvc/router/route.zep.c @@ -866,15 +866,17 @@ PHP_METHOD(Phalcon_Mvc_Router_Route, getHttpMethods) * Returns the data that the router needs for its method index, with * one call: the HTTP methods, the compiled pattern, the host name, the * compiled host name, the beforeMatch callback and the route id, in - * this order. + * this order. A route with no host name has no compiled host name, so + * there is no getCompiledHostName() call for it. */ PHP_METHOD(Phalcon_Mvc_Router_Route, getIndexData) { - zval _0, _1; + zval compiledHostName, _0, _1; zephir_method_globals *ZEPHIR_METHOD_GLOBALS_PTR = NULL; zend_long ZEPHIR_LAST_CALL_STATUS; zval *this_ptr = getThis(); + ZVAL_UNDEF(&compiledHostName); ZVAL_UNDEF(&_0); ZVAL_UNDEF(&_1); static zend_string *_zephir_prop_0 = NULL; @@ -883,13 +885,13 @@ PHP_METHOD(Phalcon_Mvc_Router_Route, getIndexData) static zend_string *_zephir_prop_3 = NULL; static zend_string *_zephir_prop_4 = NULL; if (UNEXPECTED(!_zephir_prop_0)) { - _zephir_prop_0 = zend_string_init("methods", 7, 1); + _zephir_prop_0 = zend_string_init("hostname", 8, 1); } if (UNEXPECTED(!_zephir_prop_1)) { - _zephir_prop_1 = zend_string_init("compiledPattern", 15, 1); + _zephir_prop_1 = zend_string_init("methods", 7, 1); } if (UNEXPECTED(!_zephir_prop_2)) { - _zephir_prop_2 = zend_string_init("hostname", 8, 1); + _zephir_prop_2 = zend_string_init("compiledPattern", 15, 1); } if (UNEXPECTED(!_zephir_prop_3)) { _zephir_prop_3 = zend_string_init("beforeMatch", 11, 1); @@ -900,25 +902,30 @@ PHP_METHOD(Phalcon_Mvc_Router_Route, getIndexData) ZEPHIR_METHOD_GLOBALS_PTR = pecalloc(1, sizeof(zephir_method_globals), 0); zephir_memory_grow_stack(ZEPHIR_METHOD_GLOBALS_PTR, __func__); + ZEPHIR_INIT_VAR(&compiledHostName); + ZVAL_NULL(&compiledHostName); + zephir_read_property_cached(&_0, this_ptr, _zephir_prop_0, 1170, PH_NOISY_CC | PH_READONLY); + if (Z_TYPE_P(&_0) != IS_NULL) { + ZEPHIR_CALL_METHOD(&compiledHostName, this_ptr, "getCompiledHostName", NULL, 0); + zephir_check_call_status(); + } zephir_create_array(return_value, 6, 0); - zephir_memory_observe(&_0); - zephir_read_property_cached(&_0, this_ptr, _zephir_prop_0, 1166, PH_NOISY_CC); - zephir_array_fast_append(return_value, &_0); - ZEPHIR_OBS_NVAR(&_0); - zephir_read_property_cached(&_0, this_ptr, _zephir_prop_1, 1171, PH_NOISY_CC); - zephir_array_fast_append(return_value, &_0); - ZEPHIR_OBS_NVAR(&_0); - zephir_read_property_cached(&_0, this_ptr, _zephir_prop_2, 1170, PH_NOISY_CC); - zephir_array_fast_append(return_value, &_0); - ZEPHIR_CALL_METHOD(&_1, this_ptr, "getCompiledHostName", NULL, 0); - zephir_check_call_status(); + zephir_memory_observe(&_1); + zephir_read_property_cached(&_1, this_ptr, _zephir_prop_1, 1166, PH_NOISY_CC); + zephir_array_fast_append(return_value, &_1); + ZEPHIR_OBS_NVAR(&_1); + zephir_read_property_cached(&_1, this_ptr, _zephir_prop_2, 1171, PH_NOISY_CC); + zephir_array_fast_append(return_value, &_1); + ZEPHIR_OBS_NVAR(&_1); + zephir_read_property_cached(&_1, this_ptr, _zephir_prop_0, 1170, PH_NOISY_CC); + zephir_array_fast_append(return_value, &_1); + zephir_array_fast_append(return_value, &compiledHostName); + ZEPHIR_OBS_NVAR(&_1); + zephir_read_property_cached(&_1, this_ptr, _zephir_prop_3, 1168, PH_NOISY_CC); + zephir_array_fast_append(return_value, &_1); + ZEPHIR_OBS_NVAR(&_1); + zephir_read_property_cached(&_1, this_ptr, _zephir_prop_4, 1167, PH_NOISY_CC); zephir_array_fast_append(return_value, &_1); - ZEPHIR_OBS_NVAR(&_0); - zephir_read_property_cached(&_0, this_ptr, _zephir_prop_3, 1168, PH_NOISY_CC); - zephir_array_fast_append(return_value, &_0); - ZEPHIR_OBS_NVAR(&_0); - zephir_read_property_cached(&_0, this_ptr, _zephir_prop_4, 1167, PH_NOISY_CC); - zephir_array_fast_append(return_value, &_0); RETURN_MM(); } @@ -1081,14 +1088,14 @@ PHP_METHOD(Phalcon_Mvc_Router_Route, reConfigure) ZEPHIR_CALL_SELF(&routePaths, "getRoutePaths", NULL, 0, paths); zephir_check_call_status(); if (!(zephir_start_with_str(&pattern_zv, SL("#")))) { - if (zephir_memnstr_str(&pattern_zv, SL("{"), "phalcon/Mvc/Router/Route.zep", 689)) { + if (zephir_memnstr_str(&pattern_zv, SL("{"), "phalcon/Mvc/Router/Route.zep", 696)) { ZEPHIR_CALL_METHOD(&extracted, this_ptr, "extractNamedParams", NULL, 0, &pattern_zv); zephir_check_call_status(); zephir_memory_observe(&pcrePattern); - zephir_array_fetch_long(&pcrePattern, &extracted, 0, PH_NOISY, "phalcon/Mvc/Router/Route.zep", 694); + zephir_array_fetch_long(&pcrePattern, &extracted, 0, PH_NOISY, "phalcon/Mvc/Router/Route.zep", 701); ZEPHIR_INIT_VAR(&_0$$4); zephir_memory_observe(&_1$$4); - zephir_array_fetch_long(&_1$$4, &extracted, 1, PH_NOISY, "phalcon/Mvc/Router/Route.zep", 695); + zephir_array_fetch_long(&_1$$4, &extracted, 1, PH_NOISY, "phalcon/Mvc/Router/Route.zep", 702); zephir_fast_array_merge(&_0$$4, &routePaths, &_1$$4); ZEPHIR_CPY_WRT(&routePaths, &_0$$4); } else { diff --git a/ext/phalcon/support/registry.zep.c b/ext/phalcon/support/registry.zep.c index b78f99966b..8b1dbec3dc 100644 --- a/ext/phalcon/support/registry.zep.c +++ b/ext/phalcon/support/registry.zep.c @@ -32,7 +32,7 @@ * available throughout your application. * *```php - * $registry = new \Phalcon\Registry(); + * $registry = new \Phalcon\Support\Registry(); * * // Set value * $registry->something = "something"; @@ -55,24 +55,13 @@ * unset($registry["something"]); *``` * - * In addition to ArrayAccess, Phalcon\Registry also implements Countable - * (count($registry) will return the number of elements in the registry), - * Serializable and Iterator (you can iterate over the registry using a foreach - * loop) interfaces. For PHP 5.4 and higher, JsonSerializable interface is - * implemented. + * Phalcon\Support\Registry extends Phalcon\Support\Collection. It implements + * ArrayAccess, Countable, IteratorAggregate and JsonSerializable. Use + * count($registry) to get the number of elements, and use foreach to iterate + * over the registry. * - * Phalcon\Registry is very fast (it is typically faster than any userspace - * implementation of the registry); however, this comes at a price: - * Phalcon\Registry is a final class and cannot be inherited from. - * - * Though Phalcon\Registry exposes methods like __get(), offsetGet(), count() etc, - * it is not recommended to invoke them manually (these methods exist mainly to - * match the interfaces the registry implements): $registry->__get("property") - * is several times slower than $registry->property. - * - * Internally all the magic methods (and interfaces except JsonSerializable) - * are implemented using object handlers or similar techniques: this allows to - * bypass relatively slow method calls. + * The keys are not case-sensitive. Phalcon\Support\Registry is a final class. + * You cannot extend it. * * @extends Collection */