diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 6f241e847be3..8a6640d986d0 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -17,7 +17,7 @@ * * @see \CodeIgniter\Autoloader\FileLocatorTest */ -class FileLocator +class FileLocator implements FileLocatorInterface { /** * The Autoloader to use. diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php new file mode 100644 index 000000000000..8bc8d1e85ee9 --- /dev/null +++ b/system/Autoloader/FileLocatorCached.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Autoloader; + +use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; + +/** + * FileLocator with Cache + * + * @see \CodeIgniter\Autoloader\FileLocatorCachedTest + */ +final class FileLocatorCached implements FileLocatorInterface +{ + private FileLocator $locator; + + /** + * @var CacheInterface|FileVarExportHandler + */ + private $cacheHandler; + + /** + * Cache data + * + * [method => data] + * E.g., + * [ + * 'search' => [$path => $foundPaths], + * ] + */ + private array $cache = []; + + /** + * Is the cache updated? + */ + private bool $cacheUpdated = false; + + private string $cacheKey = 'FileLocatorCache'; + + /** + * @param CacheInterface|FileVarExportHandler|null $cache + */ + public function __construct(FileLocator $locator, $cache = null) + { + $this->cacheHandler = $cache ?? new FileVarExportHandler(); + $this->locator = $locator; + + $this->loadCache(); + } + + private function loadCache(): void + { + $data = $this->cacheHandler->get($this->cacheKey); + + if (is_array($data)) { + $this->cache = $data; + } + } + + public function __destruct() + { + $this->saveCache(); + } + + private function saveCache(): void + { + if ($this->cacheUpdated) { + $this->cacheHandler->save($this->cacheKey, $this->cache, 3600 * 24); + } + } + + /** + * Delete cache data + */ + public function deleteCache(): void + { + $this->cacheHandler->delete($this->cacheKey); + } + + public function findQualifiedNameFromPath(string $path): false|string + { + if (isset($this->cache['findQualifiedNameFromPath'][$path])) { + return $this->cache['findQualifiedNameFromPath'][$path]; + } + + $classname = $this->locator->findQualifiedNameFromPath($path); + + $this->cache['findQualifiedNameFromPath'][$path] = $classname; + $this->cacheUpdated = true; + + return $classname; + } + + public function getClassname(string $file): string + { + if (isset($this->cache['getClassname'][$file])) { + return $this->cache['getClassname'][$file]; + } + + $classname = $this->locator->getClassname($file); + + $this->cache['getClassname'][$file] = $classname; + $this->cacheUpdated = true; + + return $classname; + } + + public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array + { + if (isset($this->cache['search'][$path][$ext][$prioritizeApp])) { + return $this->cache['search'][$path][$ext][$prioritizeApp]; + } + + $foundPaths = $this->locator->search($path, $ext, $prioritizeApp); + + $this->cache['search'][$path][$ext][$prioritizeApp] = $foundPaths; + $this->cacheUpdated = true; + + return $foundPaths; + } + + public function listFiles(string $path): array + { + if (isset($this->cache['listFiles'][$path])) { + return $this->cache['listFiles'][$path]; + } + + $files = $this->locator->listFiles($path); + + $this->cache['listFiles'][$path] = $files; + $this->cacheUpdated = true; + + return $files; + } + + public function listNamespaceFiles(string $prefix, string $path): array + { + if (isset($this->cache['listNamespaceFiles'][$prefix][$path])) { + return $this->cache['listNamespaceFiles'][$prefix][$path]; + } + + $files = $this->locator->listNamespaceFiles($prefix, $path); + + $this->cache['listNamespaceFiles'][$prefix][$path] = $files; + $this->cacheUpdated = true; + + return $files; + } + + public function locateFile(string $file, ?string $folder = null, string $ext = 'php'): false|string + { + if (isset($this->cache['locateFile'][$file][$folder][$ext])) { + return $this->cache['locateFile'][$file][$folder][$ext]; + } + + $files = $this->locator->locateFile($file, $folder, $ext); + + $this->cache['locateFile'][$file][$folder][$ext] = $files; + $this->cacheUpdated = true; + + return $files; + } +} diff --git a/system/Autoloader/FileLocatorInterface.php b/system/Autoloader/FileLocatorInterface.php new file mode 100644 index 000000000000..8f7b551bb02f --- /dev/null +++ b/system/Autoloader/FileLocatorInterface.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Autoloader; + +/** + * Allows loading non-class files in a namespaced manner. + * Works with Helpers, Views, etc. + */ +interface FileLocatorInterface +{ + /** + * Attempts to locate a file by examining the name for a namespace + * and looking through the PSR-4 namespaced files that we know about. + * + * @param string $file The relative file path or namespaced file to + * locate. If not namespaced, search in the app + * folder. + * @param string|null $folder The folder within the namespace that we should + * look for the file. If $file does not contain + * this value, it will be appended to the namespace + * folder. + * @param string $ext The file extension the file should have. + * + * @return false|string The path to the file, or false if not found. + */ + public function locateFile(string $file, ?string $folder = null, string $ext = 'php'); + + /** + * Examines a file and returns the fully qualified class name. + */ + public function getClassname(string $file): string; + + /** + * Searches through all of the defined namespaces looking for a file. + * Returns an array of all found locations for the defined file. + * + * Example: + * + * $locator->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', + * ] + */ + public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array; + + /** + * Find the qualified name of a file according to + * the namespace of the first matched namespace path. + * + * @return false|string The qualified name or false if the path is not found + */ + public function findQualifiedNameFromPath(string $path); + + /** + * Scans the defined namespaces, returning a list of all files + * that are contained within the subpath specified by $path. + * + * @return string[] List of file paths + */ + public function listFiles(string $path): array; + + /** + * Scans the provided namespace, returning a list of all files + * that are contained within the sub path specified by $path. + * + * @return string[] List of file paths + */ + public function listNamespaceFiles(string $prefix, string $path): array; +} diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index df28e533a354..bc4d1befc6fb 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -11,7 +11,7 @@ namespace CodeIgniter\CLI; -use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Log\Logger; use ReflectionClass; use ReflectionException; @@ -87,7 +87,7 @@ public function discoverCommands() return; } - /** @var FileLocator $locator */ + /** @var FileLocatorInterface $locator */ $locator = service('locator'); $files = $locator->listFiles('Commands/'); diff --git a/system/Commands/Utilities/Routes/ControllerFinder.php b/system/Commands/Utilities/Routes/ControllerFinder.php index f801d531fa92..1528996968f1 100644 --- a/system/Commands/Utilities/Routes/ControllerFinder.php +++ b/system/Commands/Utilities/Routes/ControllerFinder.php @@ -11,7 +11,7 @@ namespace CodeIgniter\Commands\Utilities\Routes; -use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Config\Services; /** @@ -26,7 +26,7 @@ final class ControllerFinder */ private string $namespace; - private FileLocator $locator; + private FileLocatorInterface $locator; /** * @param string $namespace namespace to search diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index b66b8c9f535e..0a5df0ffec4c 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -13,6 +13,7 @@ use CodeIgniter\Autoloader\Autoloader; use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Cache\ResponseCache; use CodeIgniter\CLI\Commands; @@ -226,7 +227,7 @@ public static function autoloader(bool $getShared = true) * within namespaced folders, as well as convenience methods for * loading 'helpers', and 'libraries'. * - * @return FileLocator + * @return FileLocatorInterface */ public static function locator(bool $getShared = true) { diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index e40a14c88905..ca9b4eb09a79 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -11,7 +11,7 @@ namespace CodeIgniter\Publisher; -use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Files\FileCollection; use CodeIgniter\HTTP\URI; use CodeIgniter\Publisher\Exceptions\PublisherException; @@ -105,7 +105,7 @@ final public static function discover(string $directory = 'Publishers'): array self::$discovered[$directory] = []; - /** @var FileLocator $locator */ + /** @var FileLocatorInterface $locator */ $locator = service('locator'); if ([] === $files = $locator->listFiles($directory)) { diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index ca080152ff28..d38faa41278d 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -12,7 +12,7 @@ namespace CodeIgniter\Router; use Closure; -use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Router\Exceptions\RouterException; use Config\App; use Config\Modules; @@ -245,7 +245,7 @@ class RouteCollection implements RouteCollectionInterface /** * Handle to the file locator to use. * - * @var FileLocator + * @var FileLocatorInterface */ protected $fileLocator; @@ -283,7 +283,7 @@ class RouteCollection implements RouteCollectionInterface /** * Constructor */ - public function __construct(FileLocator $locator, Modules $moduleConfig, Routing $routing) + public function __construct(FileLocatorInterface $locator, Modules $moduleConfig, Routing $routing) { $this->fileLocator = $locator; $this->moduleConfig = $moduleConfig; diff --git a/system/View/Parser.php b/system/View/Parser.php index 120570079d2b..a948579e8f8f 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -11,7 +11,7 @@ namespace CodeIgniter\View; -use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\View\Exceptions\ViewException; use Config\View as ViewConfig; use ParseError; @@ -79,10 +79,15 @@ class Parser extends View /** * Constructor * - * @param FileLocator|null $loader + * @param FileLocatorInterface|null $loader */ - public function __construct(ViewConfig $config, ?string $viewPath = null, $loader = null, ?bool $debug = null, ?LoggerInterface $logger = null) - { + public function __construct( + ViewConfig $config, + ?string $viewPath = null, + $loader = null, + ?bool $debug = null, + ?LoggerInterface $logger = null + ) { // Ensure user plugins override core plugins. $this->plugins = $config->plugins; diff --git a/system/View/View.php b/system/View/View.php index e8fdf5f18f7e..ffb734779de1 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -11,7 +11,7 @@ namespace CodeIgniter\View; -use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Debug\Toolbar\Collectors\Views; use CodeIgniter\Filters\DebugToolbar; use CodeIgniter\View\Exceptions\ViewException; @@ -63,7 +63,7 @@ class View implements RendererInterface * we need to attempt to find a view * that's not in standard place. * - * @var FileLocator + * @var FileLocatorInterface */ protected $loader; @@ -141,8 +141,13 @@ class View implements RendererInterface */ protected $sectionStack = []; - public function __construct(ViewConfig $config, ?string $viewPath = null, ?FileLocator $loader = null, ?bool $debug = null, ?LoggerInterface $logger = null) - { + public function __construct( + ViewConfig $config, + ?string $viewPath = null, + ?FileLocatorInterface $loader = null, + ?bool $debug = null, + ?LoggerInterface $logger = null + ) { $this->config = $config; $this->viewPath = rtrim($viewPath, '\\/ ') . DIRECTORY_SEPARATOR; $this->loader = $loader ?? Services::locator(); diff --git a/tests/system/Autoloader/FileLocatorCachedTest.php b/tests/system/Autoloader/FileLocatorCachedTest.php new file mode 100644 index 000000000000..f3871713d171 --- /dev/null +++ b/tests/system/Autoloader/FileLocatorCachedTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Autoloader; + +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; +use Config\Autoload; +use Config\Modules; + +/** + * @internal + * + * @group Others + */ +final class FileLocatorCachedTest extends FileLocatorTest +{ + private FileVarExportHandler $handler; + protected FileLocatorInterface $locator; + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + // Delete cache file. + $autoloader = new Autoloader(); + $handler = new FileVarExportHandler(); + $fileLocator = new FileLocator($autoloader); + $locator = new FileLocatorCached($fileLocator, $handler); + $locator->deleteCache(); + } + + protected function setUp(): void + { + parent::setUp(); + + $autoloader = new Autoloader(); + $autoloader->initialize(new Autoload(), new Modules()); + $autoloader->addNamespace([ + 'Unknown' => '/i/do/not/exist', + 'Tests/Support' => TESTPATH . '_support/', + 'App' => APPPATH, + 'CodeIgniter' => [ + TESTPATH, + SYSTEMPATH, + ], + 'Errors' => APPPATH . 'Views/errors', + 'System' => SUPPORTPATH . 'Autoloader/system', + 'CodeIgniter\\Devkit' => [ + TESTPATH . '_support/', + ], + 'Acme\SampleProject' => TESTPATH . '_support', + 'Acme\Sample' => TESTPATH . '_support/does/not/exists', + ]); + + $this->handler = new FileVarExportHandler(); + $fileLocator = new FileLocator($autoloader); + $this->locator = new FileLocatorCached($fileLocator, $this->handler); + } + + protected function tearDown(): void + { + $this->locator->__destruct(); + + parent::tearDown(); + } + + public function testDeleteCache() + { + $this->assertNotSame([], $this->handler->get('FileLocatorCache')); + + $this->locator->deleteCache(); + + $this->assertFalse($this->handler->get('FileLocatorCache')); + } +} diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php index 0d084280f520..14c512493d26 100644 --- a/tests/system/Autoloader/FileLocatorTest.php +++ b/tests/system/Autoloader/FileLocatorTest.php @@ -20,10 +20,11 @@ * @internal * * @group Others + * @no-final */ -final class FileLocatorTest extends CIUnitTestCase +class FileLocatorTest extends CIUnitTestCase { - private FileLocator $locator; + protected FileLocatorInterface $locator; protected function setUp(): void { diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index 89910fa84f3d..f7728e7cf9e1 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -51,6 +51,14 @@ Interface Changes Method Signature Changes ======================== +FileLocatorInterface +-------------------- + +- **Router:** The first parameter of the ``RouteCollection`` constructor has been changed + from ``FileLocator`` to ``FileLocatorInterface``. +- **View:** The third parameter of the ``View`` constructor has been changed + from ``FileLocator`` to ``FileLocatorInterface``. + Return Type Changes ------------------- @@ -123,9 +131,12 @@ Helpers and Functions Others ====== -- **Autoloader:** Autoloading performance when using Composer has been improved. - Adding the ``App`` namespace in the ``autoload.psr4`` setting in **composer.json** - may also improve the performance of your app. See :ref:`autoloader-application-namespace`. +- **Autoloader:** + - Autoloading performance when using Composer has been improved. + Adding the ``App`` namespace in the ``autoload.psr4`` setting in **composer.json** + may also improve the performance of your app. See :ref:`autoloader-application-namespace`. + - FileLocator Caching implemented. See :ref:`file-locator-caching` for details. + - ``FileLocatorInterface`` has been added. - **CodeIgniter:** Added a pseudo-variable ``{memory_usage}`` to show your memory usage in your view files, which was supported by CodeIgniter 3. diff --git a/user_guide_src/source/concepts/autoloader.rst b/user_guide_src/source/concepts/autoloader.rst index cc47162e5c0c..62aa0f157cf3 100644 --- a/user_guide_src/source/concepts/autoloader.rst +++ b/user_guide_src/source/concepts/autoloader.rst @@ -140,3 +140,51 @@ autoloader will be the first one to get a chance to locate the file. .. note:: Prior to v4.5.0, if the same namespace was defined in both CodeIgniter and Composer, CodeIgniter's autoloader was the first one to get a chance to locate the file. + +.. _file-locator-caching: + +******************* +FileLocator Caching +******************* + +.. versionadded:: 4.5.0 + +**FileLocator** is responsible for finding files or getting a classname from a file, +which cannot be achieved with PHP autoloading. + +To improve its performance, FileLocator Caching has been implemented. + +How It Works +============ + +- Save the all found data by FileLocator into a cache file when destructing, + if the cache data is updated. +- Restore cached data when instantiating if cached data is available. + +The cached data are used permanently. + +How to Delete Cached Data +========================= + +Once stored, the cached data never expire. + +So if you add or remove files or change existing file paths, or namespaces, old +cached data will be returned and your app may not work properly. + +In that case, you must manually delete the cache file. If you add a CodeIgniter +package via Composer, you also need to delete the cache file. + +You can use the ``spark cache:clear`` command: + +.. code-block:: console + + php spark cache:clear + +Or simply delete the **writable/cache/FileLocatorCache** file. + +How to Enable FileLocator Caching +================================= + +Add the following code in **app/Config/Services.php**: + +.. literalinclude:: autoloader/004.php diff --git a/user_guide_src/source/concepts/autoloader/004.php b/user_guide_src/source/concepts/autoloader/004.php new file mode 100644 index 000000000000..090149e6486c --- /dev/null +++ b/user_guide_src/source/concepts/autoloader/004.php @@ -0,0 +1,25 @@ +