diff --git a/.gitignore b/.gitignore index 787214c..93a8507 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ /.env Engine/storage/views/ +storage/cache/* +!storage/cache/.gitkeep \.idea/ diff --git a/README.md b/README.md index da97790..d090230 100644 --- a/README.md +++ b/README.md @@ -429,6 +429,16 @@ List discovered modules: ./shift module:list ``` +Cache discovered modules for production: + +```sh +./shift cache:modules +./shift cache:status +./shift cache:clear +``` + +The module cache is stored in `storage/cache/modules.php`. Without that file, ShiftPHP discovers modules from `application/modules` on each run. After changing module boundaries, module config, or module command mappings in production, rebuild the cache. + Run database migrations: ```sh diff --git a/REFACTORING.md b/REFACTORING.md index c97d8d9..3640504 100644 --- a/REFACTORING.md +++ b/REFACTORING.md @@ -34,6 +34,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled - [x] CLI help listing and command-specific usage. - [x] Centralized CLI command registry shared by the dispatcher and help command. - [x] Database migrations with create, migrate, status, and rollback commands. +- [x] Module discovery cache for production. - [x] Removal of view storage and example page assets from runtime. - [x] Removal of legacy `application/controllers` and `application/routes.php`. - [x] Domain-oriented framework namespaces: @@ -160,6 +161,5 @@ Internal errors return a generic `500` message unless `display_errors` is enable ## Next - [ ] Structured logging for exceptions. -- [ ] Module discovery cache for production. - [ ] CLI command aliases and richer command metadata. - [ ] Basic package-quality checks, for example static analysis and coding style. diff --git a/docs/index.html b/docs/index.html index 5fdb6e3..689131f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -167,6 +167,7 @@
Shift\Modules\ModuleLoader discovers modules by convention from application/modules/*/Module.php. Module config can be returned from getConfig() or from a module-level config.php file. Merged config is available through $modules->getConfig() and the container singleton modules.config.
For production, discovered module metadata can be cached in storage/cache/modules.php with ./shift cache:modules. Clear it with ./shift cache:clear after changing module boundaries or module config.
Create commands scaffold modules and module-owned classes:
diff --git a/src/Console/Commands/CacheClear.php b/src/Console/Commands/CacheClear.php new file mode 100644 index 0000000..9e34597 --- /dev/null +++ b/src/Console/Commands/CacheClear.php @@ -0,0 +1,33 @@ +clearCache(); + + if ($cleared) { + $cli->success('Module cache cleared.'); + return; + } + + $cli->info('Module cache is already empty.'); + } + + public function getHelp(): string + { + return 'Usage: ./shift cache:clear'; + } + + public function getDescription(): string + { + return 'Clear framework cache files.'; + } +} diff --git a/src/Console/Commands/CacheModules.php b/src/Console/Commands/CacheModules.php new file mode 100644 index 0000000..964f72e --- /dev/null +++ b/src/Console/Commands/CacheModules.php @@ -0,0 +1,34 @@ +cache(); + $cacheFile = $loader->getCacheFile(); + + $cli->success('Cached modules: ' . $count); + + if ($cacheFile !== null) { + $cli->debug('Cache file: ' . $cacheFile); + } + } + + public function getHelp(): string + { + return 'Usage: ./shift cache:modules'; + } + + public function getDescription(): string + { + return 'Cache discovered modules for production.'; + } +} diff --git a/src/Console/Commands/CacheStatus.php b/src/Console/Commands/CacheStatus.php new file mode 100644 index 0000000..fc420ca --- /dev/null +++ b/src/Console/Commands/CacheStatus.php @@ -0,0 +1,31 @@ +getCacheFile(); + $exists = $loader->isCached(); + + (new Cli())->table(['Cache', 'Status', 'Path'], [ + ['modules', $exists ? 'cached' : 'empty', $cacheFile ?? 'disabled'], + ]); + } + + public function getHelp(): string + { + return 'Usage: ./shift cache:status'; + } + + public function getDescription(): string + { + return 'Show framework cache status.'; + } +} diff --git a/src/Modules/ModuleLoader.php b/src/Modules/ModuleLoader.php index ed8c25e..c1f8bd4 100644 --- a/src/Modules/ModuleLoader.php +++ b/src/Modules/ModuleLoader.php @@ -11,41 +11,159 @@ class ModuleLoader private array $modules = []; private array $config = []; - public function __construct(private readonly string $modulesPath = APP_PATH . '/modules') - { + public function __construct( + private readonly string $modulesPath = APP_PATH . '/modules', + private readonly ?string $cacheFile = APP_ROOT . '/storage/cache/modules.php' + ) { } public function load(): self { - if (!is_dir($this->modulesPath)) { - return $this; + $this->modules = []; + $this->config = []; + + if ($this->cacheFile !== null && is_file($this->cacheFile)) { + return $this->loadFromCache(); + } + + return $this->loadFromSnapshot($this->discover()); + } + + public function cache(): int + { + $snapshot = $this->discover(); + $this->writeCache($snapshot); + $this->modules = []; + $this->config = []; + $this->loadFromSnapshot($snapshot); + + return count($snapshot['modules']); + } + + public function clearCache(): bool + { + if ($this->cacheFile === null || !is_file($this->cacheFile)) { + return false; } - foreach (glob($this->modulesPath . '/*/Module.php') ?: [] as $moduleFile) { - require_once $moduleFile; + return unlink($this->cacheFile); + } + + public function isCached(): bool + { + return $this->cacheFile !== null && is_file($this->cacheFile); + } - $modulePath = dirname($moduleFile); - $moduleName = basename($modulePath); - $moduleClass = 'Modules\\' . $moduleName . '\\Module'; + public function getCacheFile(): ?string + { + return $this->cacheFile; + } - if (!class_exists($moduleClass)) { + /** + * @return array{generated_at: string, modules_path: string, modules: list