chore(psr4): phase 3 (Core) — namespace Core/Enum + Core/Exception

- Core/Enum → XcVm\Core\Enum (BootContext, ModuleState, ServerEnvironment).
- Core/Exception → XcVm\Core\Exception (XcVmException) + \Container
  (ContainerException, CircularDependencyException, ServiceCreationException) +
  \Module (ModuleException + the 4 module exceptions). The hierarchy resolves:
  XcVmException extends \RuntimeException; the Container/Module exceptions extend
  XcVmException via use; ContainerException keeps its PSR ContainerExceptionInterface.
- Add use to referrers across src/ and tests/; fix the leading-backslash refs
  qualified in earlier hub commits (\ModuleState, \ServerEnvironment,
  \XcVmException, \ContainerException, \ModuleLoadException, ...) to their FQCNs.
- phpstan-baseline.neon regenerated (292→292).

Verified: php -l clean; PHPStan no errors (first pass); PHPUnit 295/295;
leading-backslash re-sweep clean.
This commit is contained in:
Divarion-D
2026-06-24 22:23:42 +03:00
parent 432c8a010a
commit dc335a8334
21 changed files with 101 additions and 47 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
<?php
namespace XcVm\Core\Container\Psr;
use XcVm\Core\Exception\Container\ContainerException;
/**
* Thrown when a service identifier is not found in the container.
@@ -8,4 +9,4 @@ namespace XcVm\Core\Container\Psr;
* @package XC_VM_Core_Container
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class NotFoundException extends \ContainerException implements NotFoundExceptionInterface {}
class NotFoundException extends \XcVm\Core\Exception\Container\ContainerException implements NotFoundExceptionInterface {}
+11 -7
View File
@@ -1,6 +1,10 @@
<?php
namespace XcVm\Core\Container;
use XcVm\Core\Exception\Container\ServiceCreationException;
use XcVm\Core\Exception\Container\CircularDependencyException;
use XcVm\Core\Exception\Container\ContainerException;
use XcVm\Core\Exception\XcVmException;
use XcVm\Core\Container\Psr\NotFoundException;
use XcVm\Core\Container\Psr\ContainerInterface;
@@ -242,13 +246,13 @@ class ServiceContainer implements ContainerInterface {
*/
public function decorate(string $id, string|callable $decorator, int $priority = 0): static {
if (in_array($id, $this->protectedServices, true)) {
throw new \ContainerException(
throw new \XcVm\Core\Exception\Container\ContainerException(
"ServiceContainer: сервис '{$id}' защищён от декорирования модулями."
);
}
if (!isset($this->factories[$id]) && !array_key_exists($id, $this->resolved)) {
throw new \ContainerException(
throw new \XcVm\Core\Exception\Container\ContainerException(
"ServiceContainer: невозможно декорировать незарегистрированный сервис '{$id}'."
);
}
@@ -299,12 +303,12 @@ class ServiceContainer implements ContainerInterface {
try {
$service = call_user_func($this->factories[$id], $this);
$service = $this->applyDecorators($id, $service);
} catch (\XcVmException $e) {
} catch (\XcVm\Core\Exception\XcVmException $e) {
unset($this->creating[$id]);
throw $e;
} catch (\Exception $e) {
unset($this->creating[$id]);
throw new \ServiceCreationException(
throw new \XcVm\Core\Exception\Container\ServiceCreationException(
"ServiceContainer: ошибка при создании сервиса '{$id}': " . $e->getMessage(),
0,
$e
@@ -489,14 +493,14 @@ class ServiceContainer implements ContainerInterface {
// ─────────────────────────────────────────────────────────
/**
* Throw a \CircularDependencyException describing the resolution chain.
* Throw a \XcVm\Core\Exception\Container\CircularDependencyException describing the resolution chain.
*
* @param string $id Service id whose creation closed the cycle.
* @return never
* @throws \CircularDependencyException Always.
* @throws \XcVm\Core\Exception\Container\CircularDependencyException Always.
*/
private function throwCircularDependency(string $id): never {
throw new \CircularDependencyException(
throw new \XcVm\Core\Exception\Container\CircularDependencyException(
"ServiceContainer: циклическая зависимость при создании сервиса '{$id}'. "
. "Цепочка: " . implode(' → ', array_keys($this->creating)) . " → {$id}"
);
+2
View File
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Enum;
use XcVm\Core\Database\Database;
/**
* Boot context for XC_Bootstrap::boot().
+2
View File
@@ -1,6 +1,8 @@
<?php
declare(strict_types=1);
namespace XcVm\Core\Enum;
use XcVm\Core\Module\ModuleLoader;
/**
+2
View File
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Enum;
use XcVm\Core\Module\ModuleLoader;
/**
* Server deployment environment.
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception\Container;
use XcVm\Core\Container\ServiceContainer;
/**
* Thrown when ServiceContainer detects a circular dependency chain.
@@ -1,5 +1,8 @@
<?php
namespace XcVm\Core\Exception\Container;
use XcVm\Core\Exception\XcVmException;
use XcVm\Core\Container\Psr\ContainerExceptionInterface;
use XcVm\Core\Container\ServiceContainer;
/**
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception\Container;
use XcVm\Core\Container\ServiceContainer;
/**
* Thrown when a service factory throws during ServiceContainer::get().
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception\Module;
use XcVm\Core\Module\ModuleLoader;
/**
* Thrown when ModuleLoader detects a circular dependency between modules.
@@ -1,5 +1,8 @@
<?php
namespace XcVm\Core\Exception\Module;
use XcVm\Core\Exception\XcVmException;
/**
* Base exception for module lifecycle errors.
*
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception\Module;
/**
* Thrown when a module class cannot be instantiated or registered.
*
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception\Module;
/**
* Thrown when a module.json manifest is invalid, malformed, or missing required fields.
*
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception\Module;
/**
* Thrown when a requested module or its dependency cannot be found.
*
+4 -2
View File
@@ -1,5 +1,7 @@
<?php
namespace XcVm\Core\Exception;
/**
* Base exception for the XC_VM framework.
*
@@ -8,7 +10,7 @@
*
* try { ... } catch (XcVmException $e) { ... }
*
* Still extends RuntimeException so existing catch(RuntimeException) blocks
* Still extends \RuntimeException so existing catch(\RuntimeException) blocks
* remain unaffected — no breaking change.
*
* @package XC_VM_Core_Exception
@@ -16,4 +18,4 @@
* @copyright 2025-2026 Vateron Media
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class XcVmException extends RuntimeException {}
class XcVmException extends \RuntimeException {}
+26 -20
View File
@@ -1,6 +1,12 @@
<?php
namespace XcVm\Core\Module;
use XcVm\Core\Exception\Module\ModuleNotFoundException;
use XcVm\Core\Exception\Module\ModuleManifestException;
use XcVm\Core\Exception\Module\ModuleLoadException;
use XcVm\Core\Exception\Module\ModuleCycleException;
use XcVm\Core\Enum\ServerEnvironment;
use XcVm\Core\Enum\ModuleState;
use XcVm\Core\Events\ListensTo;
use XcVm\Core\Events\EventDispatcher;
use XcVm\Core\Container\ServiceContainer;
@@ -100,7 +106,7 @@ class ModuleLoader {
$modulePath = $discovered[$name]['path'];
if (!$this->load($name, $modulePath)) {
throw new \ModuleLoadException("ModuleLoader: failed to load module '{$name}'");
throw new \XcVm\Core\Exception\Module\ModuleLoadException("ModuleLoader: failed to load module '{$name}'");
}
$this->manifests[$name] = $discovered[$name]['manifest'];
@@ -344,11 +350,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 \XcVm\Core\Enum\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.
*/
protected function discoverModules(array $jsonFiles, \ServerEnvironment $currentEnvironment): array {
protected function discoverModules(array $jsonFiles, \XcVm\Core\Enum\ServerEnvironment $currentEnvironment): array {
$discovered = [];
foreach ($jsonFiles as $jsonFile) {
@@ -371,7 +377,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 \XcVm\Core\Exception\Module\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)
@@ -447,11 +453,11 @@ class ModuleLoader {
*
* Checks SERVER_TYPE constant. Returns LoadBalancer if set to 'lb' (case-insensitive), else Main.
*/
protected function getCurrentEnvironment(): \ServerEnvironment {
protected function getCurrentEnvironment(): \XcVm\Core\Enum\ServerEnvironment {
if (defined('SERVER_TYPE') && strtolower((string) constant('SERVER_TYPE')) === 'lb') {
return \ServerEnvironment::LoadBalancer;
return \XcVm\Core\Enum\ServerEnvironment::LoadBalancer;
}
return \ServerEnvironment::Main;
return \XcVm\Core\Enum\ServerEnvironment::Main;
}
/**
@@ -475,17 +481,17 @@ class ModuleLoader {
$manifest = json_decode((string) $raw, true);
if (!is_array($manifest)) {
throw new \ModuleManifestException("ModuleLoader: invalid JSON in module manifest for module {$name}");
throw new \XcVm\Core\Exception\Module\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 \XcVm\Core\Exception\Module\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 \XcVm\Core\Exception\Module\ModuleManifestException("ModuleLoader: {$field} names must be non-empty strings for module {$name}");
}
$result[] = trim($dep);
}
@@ -573,12 +579,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 \XcVm\Core\Exception\Module\ModuleCycleException('ModuleLoader: cyclic module dependency detected: ' . implode(' -> ', $cycle));
}
}
if (!isset($discovered[$name])) {
throw new \ModuleLoadException("ModuleLoader: unknown module in dependency graph: {$name}");
throw new \XcVm\Core\Exception\Module\ModuleLoadException("ModuleLoader: unknown module in dependency graph: {$name}");
}
// Mark as currently visiting
@@ -588,7 +594,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 \XcVm\Core\Exception\Module\ModuleNotFoundException("ModuleLoader: module {$name} requires missing dependency {$dependency}");
}
$this->visitDependencyNode($dependency, $discovered, $state, $order, $stack);
}
@@ -732,28 +738,28 @@ class ModuleLoader {
}
/**
* Resolve the effective \ModuleState for a module from its overrides entry.
* Resolve the effective \XcVm\Core\Enum\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 \XcVm\Core\Enum\ModuleState
*/
private function resolveState(string $name): \ModuleState {
private function resolveState(string $name): \XcVm\Core\Enum\ModuleState {
$entry = $this->overrides[$name] ?? null;
if ($entry === null) {
return \ModuleState::Enabled;
return \XcVm\Core\Enum\ModuleState::Enabled;
}
// New key takes precedence.
if (isset($entry['state'])) {
return \ModuleState::fromRaw($entry['state']);
return \XcVm\Core\Enum\ModuleState::fromRaw($entry['state']);
}
// Legacy bool key.
if (array_key_exists('enabled', $entry)) {
return \ModuleState::fromRaw($entry['enabled']);
return \XcVm\Core\Enum\ModuleState::fromRaw($entry['enabled']);
}
return \ModuleState::Enabled;
return \XcVm\Core\Enum\ModuleState::Enabled;
}
/**
+20 -17
View File
@@ -1,6 +1,9 @@
<?php
namespace XcVm\Core\Module;
use XcVm\Core\Exception\Module\ModuleNotFoundException;
use XcVm\Core\Exception\Module\ModuleException;
use XcVm\Core\Enum\ModuleState;
use XcVm\Core\Events\Module\PackageInstalledEvent;
use XcVm\Core\Events\EventDispatcher;
use XcVm\Core\Container\ServiceContainer;
@@ -76,7 +79,7 @@ class ModuleManager {
* Scans the modules directory for module.json files, merges with
* config/modules.php overrides, and returns sorted results.
*
* @return array<int, array{name: string, description: string, version: string, requires_core: string, environment: string, priority: int, dependencies: array, optional_dependencies: array, has_navbar: bool, has_settings: bool, enabled: bool, state: \ModuleState, path: string, installed_version: string, source: string, previous_version: string}> Module list.
* @return array<int, array{name: string, description: string, version: string, requires_core: string, environment: string, priority: int, dependencies: array, optional_dependencies: array, has_navbar: bool, has_settings: bool, enabled: bool, state: \XcVm\Core\Enum\ModuleState, path: string, installed_version: string, source: string, previous_version: string}> Module list.
*/
public function listModules(): array {
$overrides = $this->readOverrides();
@@ -86,7 +89,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 = \XcVm\Core\Enum\ModuleState::fromRaw($overrides[$name]['state'] ?? ($overrides[$name]['enabled'] ?? null));
$items[] = [
'name' => $name,
@@ -128,7 +131,7 @@ class ModuleManager {
$name = $this->sanitizeModuleName($name);
$module = $this->loadModuleInstance($name);
$this->setState($name, \ModuleState::Installing);
$this->setState($name, \XcVm\Core\Enum\ModuleState::Installing);
try {
$db = $this->getDb();
@@ -141,11 +144,11 @@ class ModuleManager {
$module->install();
}
} catch (\Throwable $e) {
$this->setState($name, \ModuleState::Failed);
$this->setState($name, \XcVm\Core\Enum\ModuleState::Failed);
throw $e;
}
$this->setState($name, \ModuleState::Enabled);
$this->setState($name, \XcVm\Core\Enum\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).
@@ -169,7 +172,7 @@ class ModuleManager {
$module = $this->loadModuleInstance($name);
$module->uninstall();
$this->clearInstalledVersion($name);
$this->setState($name, \ModuleState::Disabled);
$this->setState($name, \XcVm\Core\Enum\ModuleState::Disabled);
}
/**
@@ -215,10 +218,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 \XcVm\Core\Enum\ModuleState $state Target lifecycle state.
* @return void
*/
public function setState(string $name, \ModuleState $state): void {
public function setState(string $name, \XcVm\Core\Enum\ModuleState $state): void {
$name = $this->sanitizeModuleName($name);
$overrides = $this->readOverrides();
@@ -229,7 +232,7 @@ class ModuleManager {
// Remove any legacy bool 'enabled' key — we use 'state' now.
unset($overrides[$name]['enabled']);
if ($state === \ModuleState::Enabled) {
if ($state === \XcVm\Core\Enum\ModuleState::Enabled) {
// Enabled is the default: clean up the key so the file stays minimal.
unset($overrides[$name]['state']);
if (empty($overrides[$name])) {
@@ -245,14 +248,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, \XcVm\Core\Enum\ModuleState::Enabled / \XcVm\Core\Enum\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 ? \XcVm\Core\Enum\ModuleState::Enabled : \XcVm\Core\Enum\ModuleState::Disabled);
}
/**
@@ -353,7 +356,7 @@ class ModuleManager {
$this->storeModuleArchive($zipFilePath, $moduleName, $version);
$this->recordInstalledVersion($moduleName, $version);
$this->setState($moduleName, \ModuleState::Enabled);
$this->setState($moduleName, \XcVm\Core\Enum\ModuleState::Enabled);
$this->hotReloadSafe($moduleName, $targetDir);
return $moduleName;
@@ -741,7 +744,7 @@ class ModuleManager {
if ($prevVersion !== null) {
$this->recordInstalledVersion($slug, $prevVersion);
}
$this->setState($slug, \ModuleState::Enabled);
$this->setState($slug, \XcVm\Core\Enum\ModuleState::Enabled);
return true;
}
@@ -770,7 +773,7 @@ class ModuleManager {
));
$this->recordInstalledVersion($slug, (string) ($result['version'] ?: $version));
$this->setState($slug, \ModuleState::Enabled);
$this->setState($slug, \XcVm\Core\Enum\ModuleState::Enabled);
$this->hotReloadSafe($slug, $result['path']);
}
@@ -941,12 +944,12 @@ class ModuleManager {
$loader = new ModuleLoader();
$ok = $loader->load($name, $this->modulesPath . '/' . $name);
if (!$ok) {
throw new \ModuleNotFoundException('Cannot load module: ' . $name);
throw new \XcVm\Core\Exception\Module\ModuleNotFoundException('Cannot load module: ' . $name);
}
$module = $loader->getModule($name);
if (!$module) {
throw new \ModuleNotFoundException('Module instance is not available: ' . $name);
throw new \XcVm\Core\Exception\Module\ModuleNotFoundException('Module instance is not available: ' . $name);
}
return $module;
@@ -962,7 +965,7 @@ class ModuleManager {
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 \XcVm\Core\Exception\Module\ModuleException('Invalid module name.');
}
return $name;
}
+1
View File
@@ -54,6 +54,7 @@
*/
declare(strict_types=0);
use XcVm\Core\Enum\BootContext;
use XcVm\Core\Events\EventDispatcher;
use XcVm\Core\Container\ServiceContainer;
use XcVm\Core\Database\Database;
+2
View File
@@ -1,5 +1,7 @@
<?php
use XcVm\Core\Enum\ServerEnvironment;
use XcVm\Core\Enum\BootContext;
use PHPUnit\Framework\TestCase;
/**
+9
View File
@@ -1,6 +1,15 @@
<?php
declare(strict_types=1);
use XcVm\Core\Exception\Module\ModuleNotFoundException;
use XcVm\Core\Exception\Module\ModuleManifestException;
use XcVm\Core\Exception\Module\ModuleLoadException;
use XcVm\Core\Exception\Module\ModuleCycleException;
use XcVm\Core\Exception\Module\ModuleException;
use XcVm\Core\Exception\Container\ServiceCreationException;
use XcVm\Core\Exception\Container\CircularDependencyException;
use XcVm\Core\Exception\Container\ContainerException;
use XcVm\Core\Exception\XcVmException;
use XcVm\Core\Container\Psr\NotFoundException;
use XcVm\Core\Container\Psr\NotFoundExceptionInterface;
use XcVm\Core\Container\Psr\ContainerExceptionInterface;
+1
View File
@@ -1,5 +1,6 @@
<?php
use XcVm\Core\Enum\ModuleState;
use XcVm\Core\Events\ListensTo;
use XcVm\Core\Events\EventDispatcher;
use XcVm\Core\Container\ServiceContainer;
+1
View File
@@ -1,5 +1,6 @@
<?php
use XcVm\Core\Enum\ModuleState;
use XcVm\Core\Module\ModuleManager;
use PHPUnit\Framework\TestCase;