diff --git a/system/Common.php b/system/Common.php index 87f7d7e85d3b..b145db336e58 100644 --- a/system/Common.php +++ b/system/Common.php @@ -636,6 +636,8 @@ function function_usable(string $functionName): bool */ function helper($filenames) { + static $loaded = []; + $loader = Services::locator(true); if (! is_array($filenames)) @@ -659,6 +661,12 @@ function helper($filenames) $filename .= '_helper'; } + // Check if this helper has already been loaded + if (in_array($filename, $loaded)) + { + continue; + } + // If the file is namespaced, we'll just grab that // file and not search for any others if (strpos($filename, '\\') !== false) @@ -671,6 +679,7 @@ function helper($filenames) } $includes[] = $path; + $loaded[] = $filename; } // No namespaces, so search in all available locations @@ -695,6 +704,7 @@ function helper($filenames) else { $localIncludes[] = $path; + $loaded[] = $filename; } } } @@ -704,6 +714,7 @@ function helper($filenames) { // @codeCoverageIgnoreStart $includes[] = $appHelper; + $loaded[] = $filename; // @codeCoverageIgnoreEnd } @@ -714,6 +725,7 @@ function helper($filenames) if (! empty($systemHelper)) { $includes[] = $systemHelper; + $loaded[] = $filename; } } } diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index e9a60a1641a2..19b22c7efb0e 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -171,7 +171,7 @@ public static function locator(bool $getShared = true) static::$instances['locator'] = new FileLocator(static::autoloader()); } - return static::$instances['locator']; + return static::$mocks['locator'] ?? static::$instances['locator']; } return new FileLocator(static::autoloader()); diff --git a/tests/_support/Autoloader/FatalLocator.php b/tests/_support/Autoloader/FatalLocator.php new file mode 100644 index 000000000000..ebc298806cc8 --- /dev/null +++ b/tests/_support/Autoloader/FatalLocator.php @@ -0,0 +1,59 @@ +search('Config/Routes.php'); + * // Assuming PSR4 namespaces include foo and bar, might return: + * [ + * 'app/Modules/foo/Config/Routes.php', + * 'app/Modules/bar/Config/Routes.php', + * ] + * + * @param string $path + * @param string $ext + * @param boolean $prioritizeApp + * + * @return array + */ + public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array + { + $prioritizeApp = $prioritizeApp ? 'true' : 'false'; + throw new RuntimeException("search({$path}, {$ext}, {$prioritizeApp})"); + } +} diff --git a/tests/_support/Helpers/baguette_helper.php b/tests/_support/Helpers/baguette_helper.php new file mode 100644 index 000000000000..2e50fe85e0c1 --- /dev/null +++ b/tests/_support/Helpers/baguette_helper.php @@ -0,0 +1,2 @@ +assertTrue($exception); + Services::reset(); + } + + public function testHelperLoadsOnce() + { + // Load it the first time + helper('baguette'); + + // Replace the locator with one that will fail if it is called + $locator = new FatalLocator(Services::autoloader()); + Services::injectMock('locator', $locator); + + try + { + helper('baguette'); + $exception = false; + } + catch (\RuntimeException $e) + { + $exception = true; + } + + $this->assertFalse($exception); + Services::reset(); + } }