diff --git a/src/Cli/Commands/ModuleInstallCommand.php b/src/Cli/Commands/ModuleInstallCommand.php index 298bb953..139a4e2c 100644 --- a/src/Cli/Commands/ModuleInstallCommand.php +++ b/src/Cli/Commands/ModuleInstallCommand.php @@ -1,5 +1,6 @@ @@ -14,7 +16,7 @@ interface CommandProviderInterface { * Module explicitly instantiates and registers CommandInterface instances. * No filesystem scanning — all registration is explicit PHP. * - * @param CommandRegistry $registry + * @param \CommandRegistry $registry */ - public function registerCommands(CommandRegistry $registry): void; + public function registerCommands(\CommandRegistry $registry): void; } diff --git a/src/Core/Module/Contract/CronProviderInterface.php b/src/Core/Module/Contract/CronProviderInterface.php index c2cfbbe4..b5de7c4f 100644 --- a/src/Core/Module/Contract/CronProviderInterface.php +++ b/src/Core/Module/Contract/CronProviderInterface.php @@ -1,5 +1,7 @@ diff --git a/src/Core/Module/Contract/RouteProviderInterface.php b/src/Core/Module/Contract/RouteProviderInterface.php index d9c270d2..4a9aecf0 100644 --- a/src/Core/Module/Contract/RouteProviderInterface.php +++ b/src/Core/Module/Contract/RouteProviderInterface.php @@ -1,5 +1,7 @@ @@ -13,9 +15,9 @@ interface ServiceProviderInterface { * * Called once per request during module boot phase. * - * @param ServiceContainer $container + * @param \ServiceContainer $container */ - public function boot(ServiceContainer $container): void; + public function boot(\ServiceContainer $container): void; /** * Return event subscribers declared by this module. diff --git a/src/Core/Module/Contract/StreamMiddlewareProviderInterface.php b/src/Core/Module/Contract/StreamMiddlewareProviderInterface.php index 734c1112..434188f4 100644 --- a/src/Core/Module/Contract/StreamMiddlewareProviderInterface.php +++ b/src/Core/Module/Contract/StreamMiddlewareProviderInterface.php @@ -1,5 +1,7 @@ function (ServiceContainer $c) { $c->get('db')->execute('ALTER TABLE ...'); }, - * '2.0.0' => function (ServiceContainer $c) { $c->get('db')->execute('CREATE TABLE ...'); }, + * '1.1.0' => function (\ServiceContainer $c) { $c->get('db')->execute('ALTER TABLE ...'); }, + * '2.0.0' => function (\ServiceContainer $c) { $c->get('db')->execute('CREATE TABLE ...'); }, * ]; * } * @@ -31,9 +33,9 @@ interface MigratableInterface { * * Keys are semver strings (e.g. "1.1.0"). Values are callables * that perform the schema or data change for that version step. - * Each callable receives the ServiceContainer — use it to access db, settings, etc. + * Each callable receives the \ServiceContainer — use it to access db, settings, etc. * - * @return array + * @return array */ public function getMigrations(): array; } diff --git a/src/Core/Module/ModuleInterface.php b/src/Core/Module/ModuleInterface.php index c7274c6f..ea85611b 100644 --- a/src/Core/Module/ModuleInterface.php +++ b/src/Core/Module/ModuleInterface.php @@ -1,5 +1,11 @@ load($name, $modulePath)) { - throw new ModuleLoadException("ModuleLoader: failed to load module '{$name}'"); + throw new \ModuleLoadException("ModuleLoader: failed to load module '{$name}'"); } $this->manifests[$name] = $discovered[$name]['manifest']; @@ -174,12 +182,12 @@ class ModuleLoader { * Checks each sub-interface via instanceof so modules can implement * only the contracts they need. Core navbar is registered first. * - * @param ServiceContainer $container Service container for dependency injection. + * @param \ServiceContainer $container Service container for dependency injection. * @param Router|null $router Optional router for module route registration. * @param StreamPipeline|null $pipeline Optional stream pipeline for middleware registration. * @return void */ - public function bootAll(ServiceContainer $container, ?Router $router = null, ?StreamPipeline $pipeline = null): void { + public function bootAll(\ServiceContainer $container, ?Router $router = null, ?StreamPipeline $pipeline = null): void { $navbarRegistry = new NavbarRegistry(); (new CoreNavbarProvider())->registerNavbar($navbarRegistry); @@ -209,10 +217,10 @@ class ModuleLoader { * Only calls registerCommands() on modules implementing CommandProviderInterface. * Used in CLI context (console.php). * - * @param CommandRegistry $registry Command registry for registering module commands. + * @param \CommandRegistry $registry Command registry for registering module commands. * @return void */ - public function registerAllCommands(CommandRegistry $registry): void { + public function registerAllCommands(\CommandRegistry $registry): void { foreach ($this->modules as $module) { if ($module instanceof CommandProviderInterface) { $module->registerCommands($registry); @@ -333,11 +341,11 @@ class ModuleLoader { * with their paths and normalized manifest data. * * @param array $jsonFiles Array of full paths to module.json files. - * @param ServerEnvironment $currentEnvironment Current server environment. + * @param \ServerEnvironment $currentEnvironment Current server environment. * @return array Associative array of discovered modules: name => [path, manifest]. - * @throws RuntimeException If manifest has invalid environment value or JSON is malformed. + * @throws \RuntimeException If manifest has invalid environment value or JSON is malformed. */ - protected function discoverModules(array $jsonFiles, ServerEnvironment $currentEnvironment): array { + protected function discoverModules(array $jsonFiles, \ServerEnvironment $currentEnvironment): array { $discovered = []; foreach ($jsonFiles as $jsonFile) { @@ -360,7 +368,7 @@ class ModuleLoader { } if (!in_array($manifest['environment'], ['main', 'lb', 'any'], true)) { - throw new ModuleManifestException("ModuleLoader: invalid environment in module.json for module {$name}"); + throw new \ModuleManifestException("ModuleLoader: invalid environment in module.json for module {$name}"); } // Filter by environment: skip if module is for different environment (skip lb-only on main, etc) @@ -436,11 +444,11 @@ class ModuleLoader { * * Checks SERVER_TYPE constant. Returns LoadBalancer if set to 'lb' (case-insensitive), else Main. */ - protected function getCurrentEnvironment(): ServerEnvironment { + protected function getCurrentEnvironment(): \ServerEnvironment { if (defined('SERVER_TYPE') && strtolower((string) constant('SERVER_TYPE')) === 'lb') { - return ServerEnvironment::LoadBalancer; + return \ServerEnvironment::LoadBalancer; } - return ServerEnvironment::Main; + return \ServerEnvironment::Main; } /** @@ -457,24 +465,24 @@ class ModuleLoader { * @param string $jsonFile Full path to module.json file. * @param string $name Module name (used for error messages). * @return array Normalized manifest. - * @throws RuntimeException If JSON is invalid, dependencies not array, or dependency names not strings. + * @throws \RuntimeException If JSON is invalid, dependencies not array, or dependency names not strings. */ protected function readManifest(string $jsonFile, string $name): array { $raw = @file_get_contents($jsonFile); $manifest = json_decode((string) $raw, true); if (!is_array($manifest)) { - throw new ModuleManifestException("ModuleLoader: invalid JSON in module manifest for module {$name}"); + throw new \ModuleManifestException("ModuleLoader: invalid JSON in module manifest for module {$name}"); } $normalizeDepArray = function (mixed $raw, string $field) use ($name): array { if (!is_array($raw)) { - throw new ModuleManifestException("ModuleLoader: {$field} must be array for module {$name}"); + throw new \ModuleManifestException("ModuleLoader: {$field} must be array for module {$name}"); } $result = []; foreach ($raw as $dep) { if (!is_string($dep) || trim($dep) === '') { - throw new ModuleManifestException("ModuleLoader: {$field} names must be non-empty strings for module {$name}"); + throw new \ModuleManifestException("ModuleLoader: {$field} names must be non-empty strings for module {$name}"); } $result[] = trim($dep); } @@ -505,7 +513,7 @@ class ModuleLoader { * * @param array $discovered Discovered modules (name => [path, manifest]). * @return array Ordered module names: modules are in dependency-order (dependencies first). - * @throws RuntimeException If a cyclic dependency is detected. + * @throws \RuntimeException If a cyclic dependency is detected. */ protected function resolveLoadOrder(array $discovered): array { $order = []; @@ -544,7 +552,7 @@ class ModuleLoader { * @param array &$order Load order being built (appends module names). * @param array $stack Call stack trace (used for cycle error message). * @return void - * @throws RuntimeException If cyclic dependency detected or dependency not found in discovered modules. + * @throws \RuntimeException If cyclic dependency detected or dependency not found in discovered modules. */ protected function visitDependencyNode( string $name, @@ -562,12 +570,12 @@ class ModuleLoader { // Currently visiting = cycle detected $cycle = array_slice($stack, array_search($name, $stack, true) ?: 0); $cycle[] = $name; - throw new ModuleCycleException('ModuleLoader: cyclic module dependency detected: ' . implode(' -> ', $cycle)); + throw new \ModuleCycleException('ModuleLoader: cyclic module dependency detected: ' . implode(' -> ', $cycle)); } } if (!isset($discovered[$name])) { - throw new ModuleLoadException("ModuleLoader: unknown module in dependency graph: {$name}"); + throw new \ModuleLoadException("ModuleLoader: unknown module in dependency graph: {$name}"); } // Mark as currently visiting @@ -577,7 +585,7 @@ class ModuleLoader { // Required dependencies — throw if missing foreach ($discovered[$name]['manifest']['dependencies'] as $dependency) { if (!isset($discovered[$dependency])) { - throw new ModuleNotFoundException("ModuleLoader: module {$name} requires missing dependency {$dependency}"); + throw new \ModuleNotFoundException("ModuleLoader: module {$name} requires missing dependency {$dependency}"); } $this->visitDependencyNode($dependency, $discovered, $state, $order, $stack); } @@ -602,23 +610,23 @@ class ModuleLoader { * 1. getEventSubscribers() — legacy array API: * ['EventClass' => callable] or ['EventClass' => [callable, $priority]] * - * 2. #[ListensTo] attribute — declarative PHP 8.1 attribute on public methods: - * #[ListensTo(SomeEvent::class, priority: 10)] + * 2. #[\ListensTo] attribute — declarative PHP 8.1 attribute on public methods: + * #[\ListensTo(SomeEvent::class, priority: 10)] * public function onSome(SomeEvent $e): void { ... } * * Both paths are additive — using one does not disable the other. * - * If the event class named in a #[ListensTo] attribute does not exist at + * If the event class named in a #[\ListensTo] attribute does not exist at * registration time the listener is silently skipped (graceful degradation). * * @param ServiceProviderInterface $module - * @param ServiceContainer $container + * @param \ServiceContainer $container * @return void */ - private function registerEventSubscribers(ServiceProviderInterface $module, ServiceContainer $container): void { - // Verify the container holds an actual EventDispatcher instance (not just the class name). - // Static calls below route to the same instance via EventDispatcher::getInstance(). - if (!$container->has('events') || !$container->get('events') instanceof EventDispatcher) { + private function registerEventSubscribers(ServiceProviderInterface $module, \ServiceContainer $container): void { + // Verify the container holds an actual \EventDispatcher instance (not just the class name). + // Static calls below route to the same instance via \EventDispatcher::getInstance(). + if (!$container->has('events') || !$container->get('events') instanceof \EventDispatcher) { return; } @@ -627,22 +635,22 @@ class ModuleLoader { foreach ($subscribers as $event => $handler) { if (is_array($handler) && isset($handler[0]) && is_callable($handler[0])) { // [callable, int $priority] tuple — new PSR-14 style - EventDispatcher::listen($event, $handler[0], $handler[1] ?? 0); + \EventDispatcher::listen($event, $handler[0], $handler[1] ?? 0); } else { // Legacy: string event name or class-string, plain callable - EventDispatcher::listen($event, $handler); + \EventDispatcher::listen($event, $handler); } } - // ── 2. #[ListensTo] attribute scan via Reflection ───────────────── + // ── 2. #[\ListensTo] attribute scan via Reflection ───────────────── $reflection = new \ReflectionClass($module); foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - $attributes = $method->getAttributes(ListensTo::class); + $attributes = $method->getAttributes(\ListensTo::class); if (empty($attributes)) { continue; } foreach ($attributes as $attribute) { - /** @var ListensTo $listensTo */ + /** @var \ListensTo $listensTo */ $listensTo = $attribute->newInstance(); // Graceful degradation: skip if the event class is not (yet) loadable. @@ -650,7 +658,7 @@ class ModuleLoader { continue; } - EventDispatcher::listen( + \EventDispatcher::listen( $listensTo->eventClass, [$module, $method->getName()], $listensTo->priority, @@ -721,28 +729,28 @@ class ModuleLoader { } /** - * Resolve the effective ModuleState for a module from its overrides entry. + * Resolve the effective \ModuleState for a module from its overrides entry. * * Reads both the new 'state' key and the legacy 'enabled' bool key so that * existing config/modules.php files continue to work without migration. * * @param string $name Module name or directory name. - * @return ModuleState + * @return \ModuleState */ - private function resolveState(string $name): ModuleState { + private function resolveState(string $name): \ModuleState { $entry = $this->overrides[$name] ?? null; if ($entry === null) { - return ModuleState::Enabled; + return \ModuleState::Enabled; } // New key takes precedence. if (isset($entry['state'])) { - return ModuleState::fromRaw($entry['state']); + return \ModuleState::fromRaw($entry['state']); } // Legacy bool key. if (array_key_exists('enabled', $entry)) { - return ModuleState::fromRaw($entry['enabled']); + return \ModuleState::fromRaw($entry['enabled']); } - return ModuleState::Enabled; + return \ModuleState::Enabled; } /** diff --git a/src/Core/Module/ModuleManager.php b/src/Core/Module/ModuleManager.php index 339ada06..e522adf3 100644 --- a/src/Core/Module/ModuleManager.php +++ b/src/Core/Module/ModuleManager.php @@ -1,5 +1,7 @@ modulesPath = $modulesPath ?: (defined('MAIN_HOME') ? MAIN_HOME . 'Modules' : dirname(__DIR__, 2) . '/Modules'); $this->overridesPath = $overridesPath ?: (defined('CONFIG_PATH') ? CONFIG_PATH . 'modules.php' : dirname(__DIR__, 2) . '/config/modules.php'); @@ -71,7 +73,7 @@ class ModuleManager { * Scans the modules directory for module.json files, merges with * config/modules.php overrides, and returns sorted results. * - * @return array Module list. + * @return array Module list. */ public function listModules(): array { $overrides = $this->readOverrides(); @@ -81,7 +83,7 @@ class ModuleManager { foreach ($jsonFiles as $jsonFile) { $name = basename(dirname($jsonFile)); $meta = json_decode((string) @file_get_contents($jsonFile), true) ?: []; - $state = ModuleState::fromRaw($overrides[$name]['state'] ?? ($overrides[$name]['enabled'] ?? null)); + $state = \ModuleState::fromRaw($overrides[$name]['state'] ?? ($overrides[$name]['enabled'] ?? null)); $items[] = [ 'name' => $name, @@ -117,13 +119,13 @@ class ModuleManager { * * @param string $name Module name (lowercase, alphanumeric + hyphens). * @return void - * @throws RuntimeException If the module cannot be loaded. + * @throws \RuntimeException If the module cannot be loaded. */ public function installModule(string $name, ?string $version = null): void { $name = $this->sanitizeModuleName($name); $module = $this->loadModuleInstance($name); - $this->setState($name, ModuleState::Installing); + $this->setState($name, \ModuleState::Installing); try { $db = $this->getDb(); @@ -136,11 +138,11 @@ class ModuleManager { $module->install(); } } catch (\Throwable $e) { - $this->setState($name, ModuleState::Failed); + $this->setState($name, \ModuleState::Failed); throw $e; } - $this->setState($name, ModuleState::Enabled); + $this->setState($name, \ModuleState::Enabled); // Version priority: explicit $version (platform installs pass the // authoritative SaaS release version) → module.json manifest → the // module's hardcoded getVersion() (which can drift from the manifest). @@ -157,14 +159,14 @@ class ModuleManager { * * @param string $name Module name. * @return void - * @throws RuntimeException If the module cannot be loaded. + * @throws \RuntimeException If the module cannot be loaded. */ public function uninstallModule(string $name): void { $name = $this->sanitizeModuleName($name); $module = $this->loadModuleInstance($name); $module->uninstall(); $this->clearInstalledVersion($name); - $this->setState($name, ModuleState::Disabled); + $this->setState($name, \ModuleState::Disabled); } /** @@ -210,10 +212,10 @@ class ModuleManager { * When state is anything else the string value is persisted as 'state'. * * @param string $name Module name. - * @param ModuleState $state Target lifecycle state. + * @param \ModuleState $state Target lifecycle state. * @return void */ - public function setState(string $name, ModuleState $state): void { + public function setState(string $name, \ModuleState $state): void { $name = $this->sanitizeModuleName($name); $overrides = $this->readOverrides(); @@ -224,7 +226,7 @@ class ModuleManager { // Remove any legacy bool 'enabled' key — we use 'state' now. unset($overrides[$name]['enabled']); - if ($state === ModuleState::Enabled) { + if ($state === \ModuleState::Enabled) { // Enabled is the default: clean up the key so the file stays minimal. unset($overrides[$name]['state']); if (empty($overrides[$name])) { @@ -240,14 +242,14 @@ class ModuleManager { /** * Enable or disable a module in config/modules.php. * - * @deprecated Use setState(name, ModuleState::Enabled / ModuleState::Disabled) instead. + * @deprecated Use setState(name, \ModuleState::Enabled / \ModuleState::Disabled) instead. * * @param string $name Module name. * @param bool $enabled True to enable, false to disable. * @return void */ public function setEnabled(string $name, bool $enabled): void { - $this->setState($name, $enabled ? ModuleState::Enabled : ModuleState::Disabled); + $this->setState($name, $enabled ? \ModuleState::Enabled : \ModuleState::Disabled); } /** @@ -258,21 +260,21 @@ class ModuleManager { * * @param string $zipFilePath Path to the uploaded zip file. * @return string Installed module name. - * @throws RuntimeException If extraction or installation fails. - * @throws InvalidArgumentException If the zip file is not found. + * @throws \RuntimeException If extraction or installation fails. + * @throws \InvalidArgumentException If the zip file is not found. */ public function uploadAndInstall(string $zipFilePath): string { if (!class_exists('ZipArchive')) { - throw new RuntimeException('ZipArchive extension is not available.'); + throw new \RuntimeException('ZipArchive extension is not available.'); } if (!is_file($zipFilePath)) { - throw new InvalidArgumentException('Uploaded zip file not found.'); + throw new \InvalidArgumentException('Uploaded zip file not found.'); } $tempBase = rtrim(sys_get_temp_dir(), '/') . '/xc_module_' . bin2hex(random_bytes(8)); if (!@mkdir($tempBase, 0755, true) && !is_dir($tempBase)) { - throw new RuntimeException('Unable to create temporary directory.'); + throw new \RuntimeException('Unable to create temporary directory.'); } try { @@ -319,15 +321,15 @@ class ModuleManager { */ public function deployFromArchiveFilesOnly(string $zipFilePath): string { if (!class_exists('ZipArchive')) { - throw new RuntimeException('ZipArchive extension is not available.'); + throw new \RuntimeException('ZipArchive extension is not available.'); } if (!is_file($zipFilePath)) { - throw new InvalidArgumentException('Module archive not found.'); + throw new \InvalidArgumentException('Module archive not found.'); } $tempBase = rtrim(sys_get_temp_dir(), '/') . '/xc_module_' . bin2hex(random_bytes(8)); if (!@mkdir($tempBase, 0755, true) && !is_dir($tempBase)) { - throw new RuntimeException('Unable to create temporary directory.'); + throw new \RuntimeException('Unable to create temporary directory.'); } try { @@ -348,7 +350,7 @@ class ModuleManager { $this->storeModuleArchive($zipFilePath, $moduleName, $version); $this->recordInstalledVersion($moduleName, $version); - $this->setState($moduleName, ModuleState::Enabled); + $this->setState($moduleName, \ModuleState::Enabled); $this->hotReloadSafe($moduleName, $targetDir); return $moduleName; @@ -365,10 +367,10 @@ class ModuleManager { $dest = $this->archivePathFor($name, $version); $dir = dirname($dest); if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) { - throw new RuntimeException('Unable to create modules archive directory.'); + throw new \RuntimeException('Unable to create modules archive directory.'); } if (realpath($sourceZip) !== realpath($dest) && !@copy($sourceZip, $dest)) { - throw new RuntimeException('Unable to store module archive.'); + throw new \RuntimeException('Unable to store module archive.'); } @chmod($dest, 0644); } @@ -477,14 +479,14 @@ class ModuleManager { * * Delegates the full download → key-unwrap → extract flow to the * XC_VM C extension, then runs installModule() to register it. - * Fires PackageInstalledEvent and hot-reloads the module into the - * current ServiceContainer without requiring a PHP-FPM restart. + * Fires \PackageInstalledEvent and hot-reloads the module into the + * current \ServiceContainer without requiring a PHP-FPM restart. * * @param string $slug Module slug as listed on the platform. * @param string $version Exact version string (e.g. "1.2.0"), or '' for the latest. * @param string|null $apiKey API key for the SaaS platform. * @return void - * @throws RuntimeException If the C extension is missing, download fails, or install fails. + * @throws \RuntimeException If the C extension is missing, download fails, or install fails. */ public function downloadFromPlatform(string $slug, string $version = '', ?string $apiKey = null): void { $slug = $this->sanitizeModuleName($slug); @@ -512,7 +514,7 @@ class ModuleManager { // installs); the module's module.json/getVersion() may lag behind. $this->installModule($slug, $resolvedVersion); - EventDispatcher::dispatch(new PackageInstalledEvent( + \EventDispatcher::dispatch(new \PackageInstalledEvent( slug: $result['module'], version: $resolvedVersion, path: $modulePath, @@ -536,9 +538,9 @@ class ModuleManager { if ($backupDir !== null) { $this->deleteDirectory($backupDir); } - } catch (Throwable $e) { + } catch (\Throwable $e) { $restored = $this->restoreModuleBackup($slug, $targetDir, $backupDir, $prevVersion); - throw new RuntimeException( + throw new \RuntimeException( "Platform install of '{$slug}' failed" . ($restored ? ' — rolled back to previous version' . ($prevVersion ? " {$prevVersion}" : '') @@ -560,7 +562,7 @@ class ModuleManager { * * @param string $slug Module slug. * @param string|null $apiKey Platform API key. - * @throws RuntimeException If the module was not installed from the store or + * @throws \RuntimeException If the module was not installed from the store or * has no recorded previous version. */ public function rollbackFromPlatform(string $slug, ?string $apiKey = null): void { @@ -569,11 +571,11 @@ class ModuleManager { $entry = $overrides[$slug] ?? []; if (($entry['source'] ?? '') !== 'platform') { - throw new RuntimeException("Module '{$slug}' was not installed from the store; cannot roll back."); + throw new \RuntimeException("Module '{$slug}' was not installed from the store; cannot roll back."); } $previous = $entry['previous_version'] ?? ''; if ($previous === '') { - throw new RuntimeException("No previous version recorded for '{$slug}'."); + throw new \RuntimeException("No previous version recorded for '{$slug}'."); } // Re-installs the previous version (explicit, not "latest"). @@ -736,7 +738,7 @@ class ModuleManager { if ($prevVersion !== null) { $this->recordInstalledVersion($slug, $prevVersion); } - $this->setState($slug, ModuleState::Enabled); + $this->setState($slug, \ModuleState::Enabled); return true; } @@ -757,7 +759,7 @@ class ModuleManager { public function deployFromPlatformFilesOnly(string $slug, string $version, ?string $apiKey = null): void { $result = $this->pullFilesFromPlatform($slug, $version, $apiKey); - EventDispatcher::dispatch(new PackageInstalledEvent( + \EventDispatcher::dispatch(new \PackageInstalledEvent( slug: $result['module'], version: $result['version'], path: $result['path'], @@ -765,7 +767,7 @@ class ModuleManager { )); $this->recordInstalledVersion($slug, (string) ($result['version'] ?: $version)); - $this->setState($slug, ModuleState::Enabled); + $this->setState($slug, \ModuleState::Enabled); $this->hotReloadSafe($slug, $result['path']); } @@ -774,11 +776,11 @@ class ModuleManager { * files from the platform via the C extension. Does NOT run installModule(). * * @return array{ok: bool, module: string, version: string, path: string} - * @throws RuntimeException On a missing extension, registration or download failure. + * @throws \RuntimeException On a missing extension, registration or download failure. */ private function pullFilesFromPlatform(string $slug, string $version, ?string $apiKey): array { if (!class_exists('XC_VM')) { - throw new RuntimeException('XC_VM extension is not loaded. Install xcvm_core.so and enable it in php.ini.'); + throw new \RuntimeException('XC_VM extension is not loaded. Install xcvm_core.so and enable it in php.ini.'); } // Ensure this panel is registered with the platform before installing. @@ -791,14 +793,14 @@ class ModuleManager { $reg = \XC_VM::panel_register($apiKey ?? ''); if (!is_array($reg) || empty($reg['ok'])) { $regReason = $reg['message'] ?? ($reg['reason'] ?? 'unknown'); - throw new RuntimeException("Panel registration with platform failed for module '{$slug}': {$regReason}"); + throw new \RuntimeException("Panel registration with platform failed for module '{$slug}': {$regReason}"); } $result = \XC_VM::module_install($slug, $version, $apiKey ?? ''); if (!is_array($result) || empty($result['ok'])) { $reason = $result['error'] ?? 'unknown'; - throw new RuntimeException("Platform download failed for module '{$slug}': {$reason}"); + throw new \RuntimeException("Platform download failed for module '{$slug}': {$reason}"); } return [ @@ -813,7 +815,7 @@ class ModuleManager { } /** - * Hot-reload a newly installed module into the running ServiceContainer. + * Hot-reload a newly installed module into the running \ServiceContainer. * * Loads and boots the module within the current request so it becomes * immediately usable without a PHP-FPM restart. @@ -851,7 +853,7 @@ class ModuleManager { * @return void */ private function hotReload(string $slug, string $modulePath): void { - $container = ServiceContainer::getInstance(); + $container = \ServiceContainer::getInstance(); $loader = new ModuleLoader(); if (!$loader->load($slug, $modulePath)) { @@ -929,19 +931,19 @@ class ModuleManager { * * @param string $name Module name. * @return object Module instance implementing ModuleInterface. - * @throws RuntimeException If the module cannot be loaded or instantiated. + * @throws \RuntimeException If the module cannot be loaded or instantiated. */ private function loadModuleInstance(string $name) { $name = $this->sanitizeModuleName($name); $loader = new ModuleLoader(); $ok = $loader->load($name, $this->modulesPath . '/' . $name); if (!$ok) { - throw new ModuleNotFoundException('Cannot load module: ' . $name); + throw new \ModuleNotFoundException('Cannot load module: ' . $name); } $module = $loader->getModule($name); if (!$module) { - throw new ModuleNotFoundException('Module instance is not available: ' . $name); + throw new \ModuleNotFoundException('Module instance is not available: ' . $name); } return $module; @@ -952,12 +954,12 @@ class ModuleManager { * * @param string $name Raw module name. * @return string Sanitized module name. - * @throws InvalidArgumentException If the name is invalid. + * @throws \InvalidArgumentException If the name is invalid. */ private function sanitizeModuleName(string $name): string { $name = trim((string) $name); if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $name)) { - throw new ModuleException('Invalid module name.'); + throw new \ModuleException('Invalid module name.'); } return $name; } @@ -984,7 +986,7 @@ class ModuleManager { * * @param array $overrides Module overrides to persist. * @return void - * @throws RuntimeException If the file cannot be written or renamed. + * @throws \RuntimeException If the file cannot be written or renamed. */ private function writeOverrides(array $overrides): void { ksort($overrides); @@ -994,18 +996,18 @@ class ModuleManager { $dir = dirname($this->overridesPath); $tmp = @tempnam($dir, '.modules_tmp_'); if ($tmp === false) { - throw new RuntimeException('Unable to create temporary file for config/modules.php'); + throw new \RuntimeException('Unable to create temporary file for config/modules.php'); } try { if (@file_put_contents($tmp, $content, LOCK_EX) === false) { - throw new RuntimeException('Unable to write config/modules.php (temp stage)'); + throw new \RuntimeException('Unable to write config/modules.php (temp stage)'); } @chmod($tmp, 0644); if (!@rename($tmp, $this->overridesPath)) { - throw new RuntimeException('Unable to atomically replace config/modules.php'); + throw new \RuntimeException('Unable to atomically replace config/modules.php'); } // config/modules.php is read back via require(), which OPcache caches @@ -1032,12 +1034,12 @@ class ModuleManager { * @param string $zipFilePath Path to the zip file. * @param string $destination Extraction target directory. * @return void - * @throws RuntimeException If extraction fails or unsafe entries are detected. + * @throws \RuntimeException If extraction fails or unsafe entries are detected. */ private function extractZipSafely(string $zipFilePath, string $destination): void { - $zip = new ZipArchive(); + $zip = new \ZipArchive(); if ($zip->open($zipFilePath) !== true) { - throw new RuntimeException('Unable to open zip archive.'); + throw new \RuntimeException('Unable to open zip archive.'); } try { @@ -1049,32 +1051,32 @@ class ModuleManager { $entry = str_replace('\\', '/', $entry); if (strpos($entry, '../') !== false || strpos($entry, '..\\') !== false || strpos($entry, ':') !== false) { - throw new RuntimeException('Unsafe zip entry detected.'); + throw new \RuntimeException('Unsafe zip entry detected.'); } $targetPath = rtrim($destination, '/') . '/' . ltrim($entry, '/'); if (substr($entry, -1) === '/') { if (!is_dir($targetPath) && !@mkdir($targetPath, 0755, true)) { - throw new RuntimeException('Unable to create directory while extracting zip.'); + throw new \RuntimeException('Unable to create directory while extracting zip.'); } continue; } $dir = dirname($targetPath); if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { - throw new RuntimeException('Unable to create directory while extracting zip.'); + throw new \RuntimeException('Unable to create directory while extracting zip.'); } $in = $zip->getStream($entry); if (!$in) { - throw new RuntimeException('Unable to read zip entry stream.'); + throw new \RuntimeException('Unable to read zip entry stream.'); } $out = @fopen($targetPath, 'wb'); if (!$out) { fclose($in); - throw new RuntimeException('Unable to write extracted file.'); + throw new \RuntimeException('Unable to write extracted file.'); } while (!feof($in)) { @@ -1100,7 +1102,7 @@ class ModuleManager { * * @param string $tempBase Temporary extraction directory. * @return string Path to the directory containing module.json. - * @throws RuntimeException If module.json is not found or ambiguous. + * @throws \RuntimeException If module.json is not found or ambiguous. */ private function resolveExtractedModuleDir(string $tempBase): string { $rootJson = $tempBase . '/module.json'; @@ -1110,7 +1112,7 @@ class ModuleManager { $jsonFiles = glob($tempBase . '/*/module.json') ?: []; if (count($jsonFiles) !== 1) { - throw new RuntimeException('Zip must contain exactly one module with module.json.'); + throw new \RuntimeException('Zip must contain exactly one module with module.json.'); } return dirname($jsonFiles[0]); @@ -1154,15 +1156,15 @@ class ModuleManager { * @param string $source Source directory path. * @param string $destination Destination directory path. * @return void - * @throws RuntimeException If copying fails. + * @throws \RuntimeException If copying fails. */ private function copyDirectory(string $source, string $destination): void { if (!is_dir($source)) { - throw new RuntimeException('Source directory not found: ' . $source); + throw new \RuntimeException('Source directory not found: ' . $source); } if (!is_dir($destination) && !@mkdir($destination, 0755, true)) { - throw new RuntimeException('Unable to create module directory.'); + throw new \RuntimeException('Unable to create module directory.'); } $items = scandir($source); @@ -1182,7 +1184,7 @@ class ModuleManager { $this->copyDirectory($src, $dst); } else { if (!@copy($src, $dst)) { - throw new RuntimeException('Unable to copy file: ' . $item); + throw new \RuntimeException('Unable to copy file: ' . $item); } } } diff --git a/src/Core/Module/NavbarItem.php b/src/Core/Module/NavbarItem.php index 8d7347e4..deb9db53 100644 --- a/src/Core/Module/NavbarItem.php +++ b/src/Core/Module/NavbarItem.php @@ -1,5 +1,7 @@ beginModuleRegistration(); $moduleLoader->loadAll(); diff --git a/src/config/modules.php b/src/config/modules.php index 3891fd1b..3d4e3a38 100644 --- a/src/config/modules.php +++ b/src/config/modules.php @@ -1,5 +1,7 @@ assertInterfaceMethod($rc, 'registerNavbar', ['NavbarRegistry'], 'void'); + $this->assertInterfaceMethod($rc, 'registerNavbar', ['XcVm\\Core\\Module\\NavbarRegistry'], 'void'); } // ── StreamMiddlewareProviderInterface ───────────────────────── diff --git a/tests/Unit/ListensToAttributeTest.php b/tests/Unit/ListensToAttributeTest.php index 8f8f7983..9b619598 100644 --- a/tests/Unit/ListensToAttributeTest.php +++ b/tests/Unit/ListensToAttributeTest.php @@ -1,5 +1,7 @@