diff --git a/Sources/Config.php b/Sources/Config.php index 05fa3155df..fd4cdc4ba2 100644 --- a/Sources/Config.php +++ b/Sources/Config.php @@ -2914,6 +2914,73 @@ public static function checkCron(): void } } + /** + * Get the SettingsService instance from the container. + * + * This method provides access to the SettingsService for code that wants to use + * dependency injection instead of static methods for Settings.php operations. + * + * @return Services\SettingsService The settings service instance. + */ + public static function getSettingsService(): Services\SettingsService + { + static $service = null; + + // Return cached instance if available + if ($service !== null) { + return $service; + } + + // Try to get the service from the container + try { + $service = Infrastructure\Container::get(Services\SettingsService::class); + + return $service; + } catch (\Throwable $e) { + // Container not available or service not registered + // Fall through to manual instantiation + } + + // Fallback: create instance directly + // This ensures config works even during early bootstrap + $service = new Services\SettingsService(); + + return $service; + } + + /** + * Get the ModSettingsService instance from the container. + * + * This method provides access to the ModSettingsService for code that wants to use + * dependency injection instead of static methods for database settings operations. + * + * @return Services\ModSettingsService The mod settings service instance. + */ + public static function getModSettingsService(): Services\ModSettingsService + { + static $service = null; + + // Return cached instance if available + if ($service !== null) { + return $service; + } + + // Try to get the service from the container + try { + $service = Infrastructure\Container::get(Services\ModSettingsService::class); + + return $service; + } catch (\Throwable $e) { + // Container not available or service not registered + // Fall through to manual instantiation + } + + // Fallback: create instance directly + $service = new Services\ModSettingsService(); + + return $service; + } + /************************* * Internal static methods *************************/ diff --git a/Sources/Infrastructure/ServicesList.php b/Sources/Infrastructure/ServicesList.php index 3e5e4e79bc..1197569652 100644 --- a/Sources/Infrastructure/ServicesList.php +++ b/Sources/Infrastructure/ServicesList.php @@ -1,6 +1,8 @@ [ @@ -8,6 +10,15 @@ // 'shared' => true // false will create a new instance everytime //], return [ + // Settings.php configuration service + SettingsService::class => [ + 'shared' => true, + ], + // Database settings service + ModSettingsService::class => [ + 'shared' => true, + ], + // Error handler service ErrorHandlerService::class => [ 'shared' => true, ], diff --git a/Sources/Services/Contracts/ModSettingsServiceInterface.php b/Sources/Services/Contracts/ModSettingsServiceInterface.php new file mode 100644 index 0000000000..96609133e6 --- /dev/null +++ b/Sources/Services/Contracts/ModSettingsServiceInterface.php @@ -0,0 +1,121 @@ + value pairs + */ + public function getAll(): array; + + /** + * Check if a mod setting exists. + * + * @param string $key The setting key + * @return bool True if the setting exists + */ + public function has(string $key): bool; + + /** + * Set a mod setting value (in memory only). + * + * This does not persist to the database. Use update() to persist. + * + * @param string $key The setting key + * @param mixed $value The value to set + */ + public function set(string $key, mixed $value): void; + + /** + * Update mod settings in the database. + * + * @param array $settings Array of setting key => value pairs + * Set value to null to delete a setting + * @param bool $update Whether to use UPDATE instead of REPLACE + * True: Use UPDATE (allows incrementing with true/false values) + * False: Use REPLACE (default, faster for bulk updates) + */ + public function update(array $settings, bool $update = false): void; + + /** + * Delete one or more mod settings from the database. + * + * @param string|array $keys Setting key(s) to delete + */ + public function delete(string|array $keys): void; + + /** + * Reload mod settings from the database. + * + * This clears the cache and reloads all settings from the database. + * + */ + public function reload(): void; + + /** + * Clear the mod settings cache. + * + */ + public function clearCache(): void; + + /** + * Get multiple settings at once. + * + * @param array $keys Array of setting keys + * @param mixed $default Default value for missing keys + * @return array Array of key => value pairs + */ + public function getMultiple(array $keys, mixed $default = null): array; + + /** + * Check if any of the specified settings exist. + * + * @param array $keys Array of setting keys + * @return bool True if at least one setting exists + */ + public function hasAny(array $keys): bool; + + /** + * Check if all the specified settings exist. + * + * @param array $keys Array of setting keys + * @return bool True if all settings exist + */ + public function hasAll(array $keys): bool; +} diff --git a/Sources/Services/Contracts/SettingsServiceInterface.php b/Sources/Services/Contracts/SettingsServiceInterface.php new file mode 100644 index 0000000000..a56dbfa2d4 --- /dev/null +++ b/Sources/Services/Contracts/SettingsServiceInterface.php @@ -0,0 +1,150 @@ +ensureLoaded(); + + return $this->settings[$key] ?? $default; + } + + public function getAll(): array + { + $this->ensureLoaded(); + + return $this->settings; + } + + public function has(string $key): bool + { + $this->ensureLoaded(); + + return isset($this->settings[$key]); + } + + /** + * {@inheritDoc} + */ + public function set(string $key, mixed $value): void + { + $this->ensureLoaded(); + + $this->settings[$key] = $value; + + // Sync to static Config for backward compatibility + Config::$modSettings[$key] = $value; + } + + /** + * {@inheritDoc} + */ + public function update(array $settings, bool $update = false): void + { + if (empty($settings) || !\is_array($settings)) { + return; + } + + $this->ensureLoaded(); + + $to_remove = []; + + // Check if there are any settings to be removed + foreach ($settings as $k => $v) { + if ($v === null) { + unset($settings[$k]); + $to_remove[] = $k; + } + } + + // Delete settings + if (!empty($to_remove)) { + Db::$db->query( + 'DELETE FROM {db_prefix}settings + WHERE variable IN ({array_string:remove})', + [ + 'remove' => $to_remove, + ], + ); + + // Remove from our cache + foreach ($to_remove as $key) { + unset($this->settings[$key]); + } + } + + // Update mode: use UPDATE queries for increment/decrement + if ($update) { + foreach ($settings as $variable => $value) { + Db::$db->query( + 'UPDATE {db_prefix}settings + SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value} + WHERE variable = {string:variable}', + [ + 'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value), + 'variable' => $variable, + ], + ); + + $this->settings[$variable] = $value === true ? ($this->settings[$variable] ?? 0) + 1 : ($value === false ? ($this->settings[$variable] ?? 0) - 1 : $value); + } + + // Clear cache + $this->clearCache(); + + // Sync to Config for backward compatibility + Config::$modSettings = $this->settings; + + return; + } + + // Replace mode: use REPLACE queries + $replace_array = []; + + foreach ($settings as $variable => $value) { + // Don't bother if it's already like that + if (($this->settings[$variable] ?? null) == $value) { + continue; + } + + // If the variable isn't set, but would only be set to nothingness, then don't bother setting it + if (!isset($this->settings[$variable]) && empty($value)) { + continue; + } + + $replace_array[] = [$variable, $value]; + $this->settings[$variable] = $value; + } + + if (empty($replace_array)) { + return; + } + + Db::$db->insert( + 'replace', + '{db_prefix}settings', + ['variable' => 'string-255', 'value' => 'string-65534'], + $replace_array, + ['variable'], + ); + + // Clear cache + $this->clearCache(); + + // Sync to Config for backward compatibility + Config::$modSettings = $this->settings; + } + + /** + * {@inheritDoc} + */ + public function delete(string|array $keys): void + { + $keys = (array) $keys; + $deleteArray = array_fill_keys($keys, null); + + $this->update($deleteArray); + } + + /** + * {@inheritDoc} + */ + public function reload(): void + { + $this->loaded = false; + $this->settings = []; + $this->loadFromDatabase(); + + // Sync to Config for backward compatibility + Config::$modSettings = $this->settings; + } + + /** + * {@inheritDoc} + */ + public function clearCache(): void + { + CacheApi::put('modSettings', null, 90); + } + + /** + * {@inheritDoc} + */ + public function increment(string $key, int $amount = 1): void + { + $this->ensureLoaded(); + + // Use the update method with true to trigger UPDATE query + $this->update([$key => true], true); + } + + /** + * {@inheritDoc} + */ + public function decrement(string $key, int $amount = 1): void + { + $this->ensureLoaded(); + + // Use the update method with false to trigger UPDATE query + $this->update([$key => false], true); + } + + public function getMultiple(array $keys, mixed $default = null): array + { + $this->ensureLoaded(); + + $result = []; + + foreach ($keys as $key) { + $result[$key] = $this->settings[$key] ?? $default; + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function hasAny(array $keys): bool + { + $this->ensureLoaded(); + + foreach ($keys as $key) { + if (isset($this->settings[$key])) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function hasAll(array $keys): bool + { + $this->ensureLoaded(); + + foreach ($keys as $key) { + if (!isset($this->settings[$key])) { + return false; + } + } + + return true; + } + + /****************** + * Internal methods + ******************/ + + /** + * Ensure settings are loaded from database. + * + */ + protected function ensureLoaded(): void + { + if (!$this->loaded) { + $this->loadFromDatabase(); + } + } + + /** + * Load settings from database. + * + */ + protected function loadFromDatabase(): void + { + // If Config has already loaded modSettings, use those for efficiency + if (!empty(Config::$modSettings)) { + $this->settings = Config::$modSettings; + $this->loaded = true; + + return; + } + + // Load cache API if not already loaded + CacheApi::load(); + + // Try to load from cache first + if (\is_array($temp = CacheApi::get('modSettings', 90))) { + $this->settings = $temp; + $this->loaded = true; + + return; + } + + // Load from database + $this->settings = []; + + try { + $request = Db::$db->query( + 'SELECT variable, value + FROM {db_prefix}settings', + [], + ); + + if (!$request) { + ErrorHandler::displayDbError(); + } + + foreach (Db::$db->fetch_all($request) as $row) { + $this->settings[$row['variable']] = $row['value']; + } + Db::$db->free_result($request); + + // Apply default values and validations + $this->applyDefaults(); + + // Cache the settings + if (!empty(CacheApi::$enable)) { + CacheApi::put('modSettings', $this->settings, 90); + } + + $this->loaded = true; + } catch (\Throwable $e) { + // If database is not available, just mark as loaded with empty settings + $this->loaded = true; + } + } + + /** + * Apply default values and validations to settings. + * + * This ensures critical settings have valid values. + * + */ + protected function applyDefaults(): void + { + // Validate defaultMaxTopics + if (empty($this->settings['defaultMaxTopics']) || $this->settings['defaultMaxTopics'] <= 0 || $this->settings['defaultMaxTopics'] > 999) { + $this->settings['defaultMaxTopics'] = 20; + } + + // Validate defaultMaxMessages + if (empty($this->settings['defaultMaxMessages']) || $this->settings['defaultMaxMessages'] <= 0 || $this->settings['defaultMaxMessages'] > 999) { + $this->settings['defaultMaxMessages'] = 15; + } + + // Validate defaultMaxMembers + if (empty($this->settings['defaultMaxMembers']) || $this->settings['defaultMaxMembers'] <= 0 || $this->settings['defaultMaxMembers'] > 999) { + $this->settings['defaultMaxMembers'] = 30; + } + + // Validate defaultMaxListItems + if (empty($this->settings['defaultMaxListItems']) || $this->settings['defaultMaxListItems'] <= 0 || $this->settings['defaultMaxListItems'] > 999) { + $this->settings['defaultMaxListItems'] = 15; + } + + // Parse attachmentUploadDir if it's JSON + if (isset($this->settings['attachmentUploadDir']) && !\is_array($this->settings['attachmentUploadDir'])) { + $attachmentUploadDir = \SMF\Utils::jsonDecode($this->settings['attachmentUploadDir'], true, 512, 0, false); + $this->settings['attachmentUploadDir'] = !empty($attachmentUploadDir) ? $attachmentUploadDir : $this->settings['attachmentUploadDir']; + } + } +} diff --git a/Sources/Services/SettingsService.php b/Sources/Services/SettingsService.php new file mode 100644 index 0000000000..05332f4867 --- /dev/null +++ b/Sources/Services/SettingsService.php @@ -0,0 +1,345 @@ +settingsFile = $settingsFile ?? (\defined('SMF_SETTINGS_FILE') ? SMF_SETTINGS_FILE : ''); + } + + /** + * {@inheritDoc} + */ + public function get(string $key, mixed $default = null): mixed + { + $this->ensureLoaded(); + + return $this->settings[$key] ?? $default; + } + + /** + * {@inheritDoc} + */ + public function set(string $key, mixed $value): void + { + $this->ensureLoaded(); + + $this->settings[$key] = $value; + + // Also update static Config for backward compatibility + if (property_exists(Config::class, $key)) { + Config::${$key} = $value; + } else { + Config::$custom[$key] = $value; + } + } + + /** + * {@inheritDoc} + */ + public function updateFile(array $configVars, bool $keepQuotes = false, bool $rebuild = false): bool + { + // Delegate to static Config method + return Config::updateSettingsFile($configVars, $keepQuotes, $rebuild); + } + + /** + * {@inheritDoc} + */ + public function getBoardUrl(): string + { + $this->ensureLoaded(); + + return $this->settings['boardurl'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getScriptUrl(): string + { + $this->ensureLoaded(); + + // scripturl is derived from boardurl + return ($this->settings['boardurl'] ?? '') . '/index.php'; + } + + /** + * {@inheritDoc} + */ + public function getBoardDir(): string + { + $this->ensureLoaded(); + + return $this->settings['boarddir'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getSourcesDir(): string + { + $this->ensureLoaded(); + + return $this->settings['sourcedir'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getCacheDir(): string + { + $this->ensureLoaded(); + + return $this->settings['cachedir'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getLanguagesDir(): string + { + $this->ensureLoaded(); + + return $this->settings['languagesdir'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function isMaintenanceMode(): bool + { + $this->ensureLoaded(); + + return !empty($this->settings['maintenance']); + } + + /** + * {@inheritDoc} + */ + public function getMaintenanceLevel(): int + { + $this->ensureLoaded(); + + return $this->settings['maintenance'] ?? 0; + } + + /** + * {@inheritDoc} + */ + public function getForumName(): string + { + $this->ensureLoaded(); + + return $this->settings['mbname'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getDatabaseType(): string + { + $this->ensureLoaded(); + + return $this->settings['db_type'] ?? 'mysql'; + } + + /** + * {@inheritDoc} + */ + public function getDatabaseServer(): string + { + $this->ensureLoaded(); + + return $this->settings['db_server'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getDatabaseName(): string + { + $this->ensureLoaded(); + + return $this->settings['db_name'] ?? ''; + } + + /** + * {@inheritDoc} + */ + public function getDatabasePrefix(): string + { + $this->ensureLoaded(); + + return $this->settings['db_prefix'] ?? ''; + } + + /****************** + * Internal methods + ******************/ + + /** + * Ensure settings are loaded from Settings.php. + * + */ + protected function ensureLoaded(): void + { + if (!$this->loaded) { + $this->loadSettings(); + } + } + + /** + * Load settings from Settings.php file. + * + * This method loads settings independently from Config class. + * If Config has already loaded settings, we use those for efficiency. + * + */ + protected function loadSettings(): void + { + // If Config has already loaded settings, use those for efficiency + if (!empty(Config::$boardurl)) { + $this->syncFromConfig(); + $this->loaded = true; + + return; + } + + // Otherwise, load Settings.php ourselves + if (empty($this->settingsFile) || !file_exists($this->settingsFile)) { + // Try to find Settings.php + foreach (get_included_files() as $file) { + if (basename($file) === 'Settings.php') { + $this->settingsFile = $file; + break; + } + } + + if (empty($this->settingsFile) || !file_exists($this->settingsFile)) { + $this->loaded = true; + + return; + } + } + + // Load Settings.php in isolated scope + $this->settings = $this->loadSettingsFile($this->settingsFile); + $this->loaded = true; + } + + /** + * Load settings from a file in an isolated scope. + * + * @param string $file Path to Settings.php file. + * @return array Array of settings. + */ + protected function loadSettingsFile(string $file): array + { + // Create isolated scope to load Settings.php + $loadSettings = function ($settingsFile) { + // Suppress any output or errors from Settings.php + ob_start(); + $result = @include $settingsFile; + ob_end_clean(); + + // Get all defined variables from the included file + return get_defined_vars(); + }; + + $vars = $loadSettings($file); + + // Remove the closure and file path from the variables + unset($vars['settingsFile'], $vars['result']); + + return $vars; + } + + /** + * Sync settings from static Config class. + * + * This is used when Config has already loaded settings for efficiency. + * + */ + protected function syncFromConfig(): void + { + // Get all public static properties from Config + $reflection = new \ReflectionClass(Config::class); + + foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_STATIC) as $property) { + $name = $property->getName(); + + // Skip modSettings and other runtime properties + if (\in_array($name, ['modSettings', 'scripturl', 'loader', 'custom'])) { + continue; + } + + if ($property->isInitialized()) { + $this->settings[$name] = $property->getValue(); + } + } + + // Also include custom settings + if (!empty(Config::$custom)) { + $this->settings = array_merge($this->settings, Config::$custom); + } + } +} diff --git a/docs/DEPENDENCY_INJECTION_GUIDE.md b/docs/DEPENDENCY_INJECTION_GUIDE.md new file mode 100644 index 0000000000..8cc8a3e482 --- /dev/null +++ b/docs/DEPENDENCY_INJECTION_GUIDE.md @@ -0,0 +1,723 @@ +# SMF Dependency Injection Guide + +## Overview + +This guide documents the ongoing migration of SMF's static architecture to a modern Dependency Injection (DI) pattern using service classes. + +## Refactored Services + +### 1. ErrorHandlerService + +**Purpose**: Centralized error handling and logging. + +**Interface**: `SMF\Services\Contracts\ErrorHandlerServiceInterface` +**Implementation**: `SMF\Services\ErrorHandlerService` +**Facade**: `SMF\ErrorHandler` (for backward compatibility) + +**Key Methods:** +- `log(string $message, string $level = 'error'): void` +- `handleError(int $errno, string $errstr, string $errfile, int $errline): bool` +- `handleException(\Throwable $exception): void` + +**Example Usage:** +```php +use SMF\Services\Contracts\ErrorHandlerServiceInterface; + +class MyService { + public function __construct( + private ErrorHandlerServiceInterface $errorHandler + ) {} + + public function doSomething() { + try { + // ... work ... + } catch (\Exception $e) { + $this->errorHandler->log('Failed to do something: ' . $e->getMessage(), 'error'); + } + } +} +``` + +### 2. SettingsService + +**Purpose**: Manage file-based configuration from Settings.php. + +**Interface**: `SMF\Services\Contracts\SettingsServiceInterface` +**Implementation**: `SMF\Services\SettingsService` +**Facade**: `SMF\Config::getSettingsService()` + +**Key Methods:** +- `get(string $key, mixed $default = null): mixed` +- `getBoardUrl(): string` +- `getBoardDir(): string` +- `getSourcesDir(): string` +- `getDatabaseType(): string` +- `getDatabaseServer(): string` +- `getDatabaseName(): string` +- `isMaintenanceMode(): bool` + +**Example Usage:** +```php +use SMF\Services\Contracts\SettingsServiceInterface; + +class FileManager { + public function __construct( + private SettingsServiceInterface $settings + ) {} + + public function getUploadPath(): string { + return $this->settings->getBoardDir() . '/uploads'; + } + + public function isMaintenanceMode(): bool { + return $this->settings->isMaintenanceMode(); + } +} +``` + +### 3. ModSettingsService + +**Purpose**: Manage database-based runtime settings from the settings table. + +**Interface**: `SMF\Services\Contracts\ModSettingsServiceInterface` +**Implementation**: `SMF\Services\ModSettingsService` +**Facade**: `SMF\Config::getModSettingsService()` + +**Key Methods:** +- `get(string $key, mixed $default = null): mixed` +- `getAll(): array` +- `has(string $key): bool` +- `update(array $settings, bool $update = false): void` +- `delete(string|array $keys): void` +- `reload(): void` +- `clearCache(): void` + +**Example Usage:** +```php +use SMF\Services\Contracts\ModSettingsServiceInterface; + +class FeatureManager { + public function __construct( + private ModSettingsServiceInterface $modSettings + ) {} + + public function isFeatureEnabled(string $feature): bool { + return (bool) $this->modSettings->get($feature . '_enabled', false); + } + + +### Step 2: Register Your Service in the DI Container + +Add your service to `Sources/Infrastructure/ServicesList.php`: + +```php + [ + 'shared' => true, // Singleton pattern + ], + ModSettingsService::class => [ + 'shared' => true, + ], + ErrorHandlerService::class => [ + 'shared' => true, + ], + + // Your new service + MyAwesomeService::class => [ + 'arguments' => [ + SettingsService::class, // Will auto-inject SettingsService + ModSettingsService::class, // Will auto-inject ModSettingsService + ErrorHandlerService::class, // Will auto-inject ErrorHandlerService + ], + 'shared' => true, // Use 'false' if you need a new instance each time + ], +]; +``` + +**Registration Options:** + +- **`shared: true`**: Service is a singleton (one instance shared across app) +- **`shared: false`**: New instance created each time it's requested +- **`arguments`**: List of dependencies to inject (in constructor order) + +### Step 3: Retrieve and Use Your Service + +#### Option 1: Constructor Injection (Recommended for New Code) + +**This is the preferred method for all new code.** + +```php +class HigherLevelService +{ + public function __construct( + private MyAwesomeService $myService + ) {} + + public function doWork(): void + { + $this->myService->performTask(); + } +} + +// Register in ServicesList.php +return [ + HigherLevelService::class => [ + 'arguments' => [ + MyAwesomeService::class, // Auto-injected + ], + 'shared' => true, + ], +]; +``` + +**Why this is best:** +- Testable (inject mocks) +- Clear dependencies +- Type-safe +- IDE autocomplete support + +#### Option 2: Container Direct Access (For Actions/Controllers) + +**Use when you can't use constructor injection (e.g., legacy action classes).** + +```php +use SMF\Infrastructure\Container; +use SMF\Services\MyAwesomeService; + +// Get service from container +$myService = Container::get(MyAwesomeService::class); +$result = $myService->performTask(); +``` + +**When to use:** +- Actions that can't easily use constructor injection +- One-off service access in procedural code +- Bootstrapping/initialization code + +#### WARNING: Facade Pattern (Deprecated - Existing Code Only) + +**Do NOT use facades in new code. Only for backward compatibility with existing code.** + +```php +// DEPRECATED - Do not use in new code +$settings = \SMF\Config::getSettingsService(); +$modSettings = \SMF\Config::getModSettingsService(); + +// LEGACY - Still works but avoid in new code +$boardUrl = \SMF\Config::$boardurl; +$setting = \SMF\Config::$modSettings['some_setting']; +``` + +**Why facades are deprecated:** +- Tight coupling to static classes +- Harder to test +- Hidden dependencies +- Not following DI principles + +**Facades are only maintained for:** +- Backward compatibility with existing code +- Gradual migration of legacy code +- Code that hasn't been refactored yet + +## Complete Example: Building a New Feature + +Let's build a complete feature using DI from scratch. + +### 1. Create Service Interface + +`Sources/Services/Contracts/CacheServiceInterface.php`: +```php +errorHandler->log('Cache get failed: ' . $e->getMessage()); + return null; + } + } + + public function put(string $key, mixed $value, int $ttl = 0): bool + { + try { + return CacheApi::put($key, $value, $ttl); + } catch (\Exception $e) { + $this->errorHandler->log('Cache put failed: ' . $e->getMessage()); + return false; + } + } + + public function delete(string $key): bool + { + try { + return CacheApi::put($key, null); + } catch (\Exception $e) { + $this->errorHandler->log('Cache delete failed: ' . $e->getMessage()); + return false; + } + } +} +``` + +### 3. Register in Container + +`Sources/Infrastructure/ServicesList.php`: +```php +use SMF\Services\CacheService; + +return [ + // ... existing services ... + + CacheService::class => [ + 'arguments' => [ + ModSettingsService::class, + ErrorHandlerService::class, + ], + 'shared' => true, + ], +]; +``` + +### 4. Use in Your Code + +```php +use SMF\Services\Contracts\CacheServiceInterface; +use SMF\Services\Contracts\ModSettingsServiceInterface; + +class UserProfileService +{ + public function __construct( + private CacheServiceInterface $cache, + private ModSettingsServiceInterface $modSettings + ) {} + + public function getUserProfile(int $userId): ?array + { + // Try cache first + $cacheKey = 'user_profile_' . $userId; + $cached = $this->cache->get($cacheKey); + + if ($cached !== null) { + return $cached; + } + + // Load from database (simplified) + $profile = $this->loadFromDatabase($userId); + + // Cache for future requests + $ttl = (int) $this->modSettings->get('profile_cache_ttl', 3600); + $this->cache->put($cacheKey, $profile, $ttl); + + return $profile; + } +} +``` + +## Testing with Dependency Injection + +One of the biggest benefits of DI is testability. Here's how to write tests: + +### Example: Unit Test with Mocks + +```php +use PHPUnit\Framework\TestCase; +use SMF\Services\Contracts\ModSettingsServiceInterface; +use SMF\Services\Contracts\CacheServiceInterface; + +class UserProfileServiceTest extends TestCase +{ + public function testGetUserProfileUsesCache(): void + { + // Create mocks + $cacheMock = $this->createMock(CacheServiceInterface::class); + $modSettingsMock = $this->createMock(ModSettingsServiceInterface::class); + + // Set expectations + $cacheMock->expects($this->once()) + ->method('get') + ->with('user_profile_123') + ->willReturn(['id' => 123, 'name' => 'Test User']); + + // Never should hit database if cache works + $cacheMock->expects($this->never()) + ->method('put'); + + // Create service with mocks + $service = new UserProfileService($cacheMock, $modSettingsMock); + + // Test + $profile = $service->getUserProfile(123); + + // Assert + $this->assertEquals('Test User', $profile['name']); + } +} +``` + +**Benefits:** +- No database needed for tests +- Complete control over dependencies +- Fast test execution +- Test edge cases easily + +## Best Practices + +### 1. Use Constructor Injection for New Code + +**Good - Constructor Injection:** +```php +class MyService +{ + public function __construct( + private ModSettingsServiceInterface $modSettings + ) {} +} +``` + +**Bad - Facade/Static Access:** +```php +class MyService +{ + public function doWork() + { + // Don't do this in new code + $settings = Config::getModSettingsService(); + $value = Config::$modSettings['key']; + } +} +``` + +**Why?** Constructor injection makes dependencies explicit and code testable. + +### 2. Always Use Interfaces, Not Implementations + +**Good:** +```php +public function __construct( + private ModSettingsServiceInterface $modSettings // Interface +) {} +``` + +**Bad:** +```php +public function __construct( + private ModSettingsService $modSettings // Concrete class +) {} +``` + +**Why?** Interfaces allow swapping implementations and better mocking in tests. + +### 3. Keep Constructor Simple + +**Good:** +```php +public function __construct( + private SettingsServiceInterface $settings +) {} +``` + +**Bad:** +```php +public function __construct( + private SettingsServiceInterface $settings +) { + // Don't do heavy work here! + $this->loadAllData(); + $this->processEverything(); +} +``` + +**Why?** Heavy work in constructors makes testing difficult and slows down initialization. + +### 4. Use Lazy Loading for Expensive Operations + +```php +class HeavyService +{ + private ?array $data = null; + + public function __construct( + private ModSettingsServiceInterface $modSettings + ) {} + + public function getData(): array + { + if ($this->data === null) { + $this->data = $this->loadExpensiveData(); + } + return $this->data; + } +} +``` + +### 5. Mark Services as Shared When Appropriate + +```php +// ServicesList.php +return [ + // Stateless services should be shared (singleton) + CacheService::class => [ + 'shared' => true, + ], + + // Stateful services might need multiple instances + SessionHandler::class => [ + 'shared' => false, // New instance per request + ], +]; +``` + +## Migration Strategy + +### Phase 1: Backward Compatible Services (Current) + +**Status**: Complete + +- Create service classes that work alongside static classes +- Services can use facades for backward compatibility +- Old code continues to work unchanged + +### Phase 2: New Code Uses DI (In Progress) + +**Status**: Ongoing + +- All new features use dependency injection +- Gradually refactor existing code when touched +- No breaking changes to existing functionality + +### Phase 3: Full Migration (Future) + +**Status**: Planned + +- Static facades become thin wrappers around services +- All business logic in services +- Global state minimized + +## Quick Reference + +### Currently Available Services + +| Service | Interface | Purpose | +|---------|-----------|---------| +| **ErrorHandlerService** | `ErrorHandlerServiceInterface` | Error handling and logging | +| **SettingsService** | `SettingsServiceInterface` | Settings.php configuration | +| **ModSettingsService** | `ModSettingsServiceInterface` | Database settings | + +### Container Methods + +```php +use SMF\Infrastructure\Container; + +// Get a service +$service = Container::get(MyService::class); + +// Check if service exists +if (Container::has(MyService::class)) { + // ... +} +``` + +### WARNING: Facade Access (Deprecated - Existing Code Only) + +**Do NOT use in new code. Only for backward compatibility.** + +```php +// DEPRECATED - Existing code only +$settings = \SMF\Config::getSettingsService(); +$modSettings = \SMF\Config::getModSettingsService(); +\SMF\ErrorHandler::log('message', 'error'); + +// NEW CODE - Use constructor injection instead +public function __construct( + private SettingsServiceInterface $settings, + private ModSettingsServiceInterface $modSettings, + private ErrorHandlerServiceInterface $errorHandler +) {} +``` + +## Common Patterns + +### Pattern 1: Service Factory + +```php +class UserServiceFactory +{ + public function __construct( + private ModSettingsServiceInterface $modSettings + ) {} + + public function createUserService(int $userId): UserService + { + return new UserService($userId, $this->modSettings); + } +} +``` + +### Pattern 2: Optional Dependencies + +```php +class OptionalDepsService +{ + public function __construct( + private SettingsServiceInterface $settings, + private ?CacheServiceInterface $cache = null // Optional + ) {} + + public function doWork(): void + { + if ($this->cache !== null) { + // Use cache if available + } + } +} +``` + +### Pattern 3: Multiple Implementations + +```php +// Development vs Production +interface LoggerInterface { + public function log(string $message): void; +} + +class FileLogger implements LoggerInterface { + public function log(string $message): void { + file_put_contents('log.txt', $message, FILE_APPEND); + } +} + +class ConsoleLogger implements LoggerInterface { + public function log(string $message): void { + echo $message . PHP_EOL; + } +} + +// In ServicesList.php, choose implementation: +return [ + LoggerInterface::class => [ + 'class' => \defined('SMF_DEBUG') ? ConsoleLogger::class : FileLogger::class, + 'shared' => true, + ], +]; +``` + +## Troubleshooting + +### Problem: Service Not Found + +**Error**: `Service not found: MyService` + +**Solution**: Register service in `Sources/Infrastructure/ServicesList.php` + +### Problem: Circular Dependency + +**Error**: `Circular dependency detected` + +**Solution**: Refactor to break the circle: +- Use interfaces +- Extract shared logic to a new service +- Use lazy loading or events + +### Problem: Wrong Dependencies Injected + +**Error**: Type mismatch or unexpected behavior + +**Solution**: Check argument order in `ServicesList.php` matches constructor order + +## Additional Resources + +- **Migration Plan**: See `DEPENDENCY_INJECTION_MIGRATION_PLAN.md` for the full roadmap +- **Config Services Guide**: See `CONFIG_SERVICES_MIGRATION_GUIDE.md` for configuration details +- **Independence Summary**: See `CONFIG_SERVICES_INDEPENDENCE_SUMMARY.md` for architecture details + +## Contributing New Services + +When adding a new service: + +1. Create interface in `Sources/Services/Contracts/` +2. Create implementation in `Sources/Services/` +3. Register in `Sources/Infrastructure/ServicesList.php` +4. Write unit tests +5. Update this documentation +6. Add facade method if needed for backward compatibility + +**Template:** + +```php +// 1. Interface +namespace SMF\Services\Contracts; + +interface YourServiceInterface { + public function doSomething(): void; +} + +// 2. Implementation +namespace SMF\Services; + +class YourService implements Contracts\YourServiceInterface { + public function __construct( + private SettingsServiceInterface $settings + ) {} + + public function doSomething(): void { + // Implementation + } +} + +// 3. Registration (ServicesList.php) +return [ + YourService::class => [ + 'arguments' => [SettingsService::class], + 'shared' => true, + ], +]; +``` + +--- + +**Remember**: The goal is gradual, non-breaking migration to improve testability and maintainability! + + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..b53134a50c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,207 @@ +# SMF Dependency Injection Documentation + +Welcome to the SMF Dependency Injection documentation! This directory contains comprehensive guides for understanding and using the new service-based architecture. + +## Documentation Index + +### 1. [Dependency Injection Guide](DEPENDENCY_INJECTION_GUIDE.md) +**Complete guide for using DI in SMF** + +- Step-by-step tutorial for creating services +- Complete examples with code +- Testing strategies +- Best practices and patterns +- Troubleshooting guide + +**Best for**: Developers who want to create new features using DI or understand how the system works. + +### 2. [Refactored Services Summary](REFACTORED_SERVICES_SUMMARY.md) +**Detailed documentation of all refactored services** + +- Complete list of refactored services +- API reference for each service +- Independence analysis +- Usage examples +- Migration patterns +- Performance considerations + +**Best for**: Developers who need a quick reference for using existing services. + +## Quick Start + +### For New Features + +Create a new service with dependency injection: + +```php +settings->getBoardDir(); + $enabled = $this->modSettings->get('feature_enabled', false); + + // Your logic here... + } +} +``` + +Register in `Sources/Infrastructure/ServicesList.php`: + +```php +use SMF\Actions\MyNewAction; +use SMF\Services\SettingsService; +use SMF\Services\ModSettingsService; + +return [ + MyNewAction::class => [ + 'arguments' => [ + SettingsService::class, + ModSettingsService::class, + ], + 'shared' => false, + ], +]; +``` + +Use your service: + +```php +use SMF\Infrastructure\Container; + +$action = Container::get(MyNewAction::class); +$action->execute(); +``` + +### WARNING: For Legacy Code Only + +**Facades are deprecated for new code. Use constructor injection or Container instead.** + +For existing code that hasn't been refactored yet: + +```php +// DEPRECATED - Only use in existing code +$settings = \SMF\Config::getSettingsService(); +$modSettings = \SMF\Config::getModSettingsService(); + +// Use them +$boardUrl = $settings->getBoardUrl(); +$enabled = $modSettings->get('feature_enabled', false); +``` + +**When refactoring existing code, migrate to:** +- Constructor injection (preferred) +- Container direct access (if injection not possible) + +## Refactoring Status + +| Service | Status | Documentation | +|---------|--------|---------------| +| **ErrorHandlerService** | Complete | [View Details](REFACTORED_SERVICES_SUMMARY.md#1-errorhandlerservice) | +| **SettingsService** | Complete | [View Details](REFACTORED_SERVICES_SUMMARY.md#2-settingsservice) | +| **ModSettingsService** | Complete | [View Details](REFACTORED_SERVICES_SUMMARY.md#3-modsettingsservice) | + +## Additional Resources + +### In This Repository + +- **[CONFIG_SERVICES_MIGRATION_GUIDE.md](../CONFIG_SERVICES_MIGRATION_GUIDE.md)** - Configuration services migration guide +- **[CONFIG_SERVICES_INDEPENDENCE_SUMMARY.md](../CONFIG_SERVICES_INDEPENDENCE_SUMMARY.md)** - Architecture and independence details + +### External Resources + +- **[SMF DI Migration Plan](https://github.com/MissAllSunday/SMF2.1/blob/Dependency-injection-proposal/DEPENDENCY_INJECTION_MIGRATION_PLAN.md)** - Overall migration strategy and roadmap + +## Common Tasks + +### Creating a New Service + +1. Create interface in `Sources/Services/Contracts/` +2. Create implementation in `Sources/Services/` +3. Register in `Sources/Infrastructure/ServicesList.php` +4. Write tests +5. Update documentation + +[Full guide →](DEPENDENCY_INJECTION_GUIDE.md#complete-example-building-a-new-feature) + +### Using Existing Services + +```php +use SMF\Services\Contracts\ModSettingsServiceInterface; + +class MyClass +{ + public function __construct( + private ModSettingsServiceInterface $modSettings + ) {} +} +``` + +[Full guide →](DEPENDENCY_INJECTION_GUIDE.md#using-dependency-injection) + +### Testing with Mocks + +```php +$mock = $this->createMock(ModSettingsServiceInterface::class); +$mock->method('get')->willReturn('test_value'); + +$service = new MyService($mock); +``` + +[Full guide →](DEPENDENCY_INJECTION_GUIDE.md#testing-with-dependency-injection) + +## FAQ + +**Q: Do I need to refactor existing code to use services?** +A: No! Existing code continues to work. Only new code should use DI. Refactor existing code gradually when you touch it. + +**Q: How should I access services in new code?** +A: Use constructor injection (preferred) or Container direct access. **Do NOT use facades in new code.** + +**Q: Can I still use Config::getSettingsService() in existing code?** +A: Yes, but only in existing code that hasn't been refactored yet. This pattern is deprecated for new code. + +**Q: Can I create multiple instances of a service?** +A: Yes! Use `'shared' => false` in ServicesList.php. Most services use `'shared' => true` (singleton) for performance. + +**Q: What if I need a service that doesn't exist yet?** +A: Create it! Follow the guide in [DEPENDENCY_INJECTION_GUIDE.md](DEPENDENCY_INJECTION_GUIDE.md#complete-example-building-a-new-feature) + +## Contributing + +When adding or modifying services: + +1. Follow existing patterns +2. Write comprehensive tests +3. Update this documentation +4. Maintain backward compatibility +5. Use interfaces, not concrete classes +6. Keep constructors simple + +## Documentation Standards + +When documenting new services: + +- Add to [REFACTORED_SERVICES_SUMMARY.md](REFACTORED_SERVICES_SUMMARY.md) +- Include usage examples +- Document all public methods +- Show testing examples + +--- + +**Last Updated**: 2026-04-02 +**Current Version**: SMF 3.0 Alpha 4 +**Services Refactored**: 3 core services complete (ErrorHandler, Settings, ModSettings) + diff --git a/docs/REFACTORED_SERVICES_SUMMARY.md b/docs/REFACTORED_SERVICES_SUMMARY.md new file mode 100644 index 0000000000..e59a93f9e7 --- /dev/null +++ b/docs/REFACTORED_SERVICES_SUMMARY.md @@ -0,0 +1,462 @@ +# Refactored Services Summary + +## Overview + +This document provides a summary of all services that have been refactored to use Dependency Injection (DI) in the SMF codebase. + +## Refactoring Progress + +| Service | Status | Interface | Implementation | Facade | Independent | +|---------|--------|-----------|----------------|--------|-------------| +| **ErrorHandlerService** | Complete | `ErrorHandlerServiceInterface` | `ErrorHandlerService` | `ErrorHandler` | Yes | +| **SettingsService** | Complete | `SettingsServiceInterface` | `SettingsService` | `Config::getSettingsService()` | Yes | +| **ModSettingsService** | Complete | `ModSettingsServiceInterface` | `ModSettingsService` | `Config::getModSettingsService()` | Yes | + + +**Legend:** +- Complete: Fully implemented and tested +- In Progress: Currently being worked on +- Planned: Scheduled for future implementation +- **Independent**: Can load data without depending on legacy static classes + +## Service Details + +### 1. ErrorHandlerService + +**Purpose**: Centralized error handling, logging, and exception management. + +**Files:** +- Interface: `Sources/Services/Contracts/ErrorHandlerServiceInterface.php` +- Implementation: `Sources/Services/ErrorHandlerService.php` +- Facade: `Sources/ErrorHandler.php` + +**Key Features:** +- Handles PHP errors and exceptions +- Logging with different severity levels +- Backward compatible facade + +**Public Methods:** +```php +public function log(string $message, string $level = 'error'): void; +public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool; +public function handleException(\Throwable $exception): void; +public function fatal(string $message, bool $log = true): void; +public function displayDbError(): void; +public function displayLoadAvgError(): void; +``` + +**Usage:** +```php +// Via DI +public function __construct( + private ErrorHandlerServiceInterface $errorHandler +) {} + +$this->errorHandler->log('Something went wrong', 'error'); + +// Via Facade (legacy) +ErrorHandler::log('Something went wrong', 'error'); +``` + +**Backward Compatibility:** +- Static method `ErrorHandler::log()` still works +- All existing code continues to function +- New code should use DI + +--- + +### 2. SettingsService + +**Purpose**: Manage file-based configuration from Settings.php. + +**Files:** +- Interface: `Sources/Services/Contracts/SettingsServiceInterface.php` +- Implementation: `Sources/Services/SettingsService.php` +- Facade: `Sources/Config.php` (via `getSettingsService()`) + +**Key Features:** +- **Fully Independent**: Loads Settings.php directly without Config class +- Lazy loading pattern +- Efficiency optimization (reuses Config data if available) +- Isolated loading scope +- Backward compatible + +**Public Methods:** +```php +public function get(string $key, mixed $default = null): mixed; +public function set(string $key, mixed $value): void; +public function updateFile(array $configVars, bool $keepQuotes = false, bool $rebuild = false): bool; +public function getBoardUrl(): string; +public function getScriptUrl(): string; +public function getBoardDir(): string; +public function getSourcesDir(): string; +public function getCacheDir(): string; +public function getLanguagesDir(): string; +public function isMaintenanceMode(): bool; +public function getMaintenanceLevel(): int; +public function getForumName(): string; +public function getDatabaseType(): string; +public function getDatabaseServer(): string; +public function getDatabaseName(): string; +public function getDatabasePrefix(): string; +``` + +**Data Source**: Settings.php file (file-based configuration) + +**Loading Strategy:** +1. Check if settings already loaded (lazy loading) +2. If `Config::$boardurl` exists, use `syncFromConfig()` for efficiency +3. Otherwise, load Settings.php directly using `loadSettingsFile()` +4. Settings loaded in isolated closure scope + +**Usage:** +```php +// Via DI (recommended) +public function __construct( + private SettingsServiceInterface $settings +) {} + +$boardDir = $this->settings->getBoardDir(); +$isMaintenanceMode = $this->settings->isMaintenanceMode(); + +// Via Facade +$settings = Config::getSettingsService(); +$boardUrl = $settings->getBoardUrl(); + +// Via Config (legacy - still works) +$boardUrl = Config::$boardurl; +``` + +**Backward Compatibility:** +- `Config::$boardurl`, `Config::$boarddir`, etc. still work +- `set()` method syncs to Config static properties +- All existing code continues to function + +--- + +### 3. ModSettingsService + +**Purpose**: Manage database-based runtime settings from the settings table. + +**Files:** +- Interface: `Sources/Services/Contracts/ModSettingsServiceInterface.php` +- Implementation: `Sources/Services/ModSettingsService.php` +- Facade: `Sources/Config.php` (via `getModSettingsService()`) + +--- + +## Container Registration + +All services are registered in `Sources/Infrastructure/ServicesList.php`: + +--- + +## Quick Start Guide + +### For New Features (Recommended) + +Use Dependency Injection from the start: + +```php +settings->getBoardDir(); + $enabled = $this->modSettings->get('feature_enabled', false); + + if (!$enabled) { + throw new \Exception('Feature not enabled'); + } + + // Do work... + + } catch (\Exception $e) { + $this->errorHandler->log('Action failed: ' . $e->getMessage(), 'error'); + } + } +} +``` + +**Register in ServicesList.php:** +```php +use SMF\Actions\MyNewAction; + +return [ + MyNewAction::class => [ + 'arguments' => [ + SettingsService::class, + ModSettingsService::class, + ErrorHandlerService::class, + ], + 'shared' => false, // New instance per use + ], +]; +``` + +**Use the Action:** +```php +use SMF\Infrastructure\Container; +use SMF\Actions\MyNewAction; + +$action = Container::get(MyNewAction::class); +$action->execute(); +``` + +### For Legacy Code (Transitional) + +Use facade methods to access services: + +```php +getBoardUrl(); +$enabled = $modSettings->get('feature_enabled', false); + +// Old static methods still work too +$boardUrl = \SMF\Config::$boardurl; +$enabled = \SMF\Config::$modSettings['feature_enabled'] ?? false; +``` + +--- + +## Testing Examples + +### Testing with Dependency Injection + +One of the main benefits of DI is easy testing: + +```php +use PHPUnit\Framework\TestCase; +use SMF\Services\Contracts\SettingsServiceInterface; +use SMF\Services\Contracts\ModSettingsServiceInterface; + +class MyNewActionTest extends TestCase +{ + public function testExecuteWhenFeatureDisabled(): void + { + // Create mocks + $settingsMock = $this->createMock(SettingsServiceInterface::class); + $modSettingsMock = $this->createMock(ModSettingsServiceInterface::class); + $errorHandlerMock = $this->createMock(ErrorHandlerServiceInterface::class); + + // Set expectations + $modSettingsMock->expects($this->once()) + ->method('get') + ->with('feature_enabled', false) + ->willReturn(false); // Feature is disabled + + // Expect error to be logged + $errorHandlerMock->expects($this->once()) + ->method('log') + ->with( + $this->stringContains('Feature not enabled'), + 'error' + ); + + // Create action with mocked dependencies + $action = new MyNewAction( + $settingsMock, + $modSettingsMock, + $errorHandlerMock + ); + + // Execute + $action->execute(); + + // Assertions are in the expects() calls above + } +} +``` + +**Benefits:** +- No database required +- No Settings.php file required +- Fast test execution +- Complete control over behavior +- Test edge cases easily + +--- + +## Migration Patterns + +### Pattern 1: Gradual Class Migration + +**Step 1:** Original static class +```php +class FeatureManager +{ + public static function isEnabled(): bool + { + return !empty(Config::$modSettings['feature_enabled']); + } +} +``` + +**Step 2:** Add instance method with DI +```php +class FeatureManager +{ + private ?ModSettingsServiceInterface $modSettings = null; + + public function __construct(?ModSettingsServiceInterface $modSettings = null) + { + $this->modSettings = $modSettings; + } + + // New instance method + public function isEnabled(): bool + { + if ($this->modSettings === null) { + $this->modSettings = Config::getModSettingsService(); + } + return (bool) $this->modSettings->get('feature_enabled', false); + } + + // Keep static method for backward compatibility + public static function isEnabledStatic(): bool + { + return !empty(Config::$modSettings['feature_enabled']); + } +} +``` + +**Step 3:** Deprecate static method +```php +class FeatureManager +{ + public function __construct( + private ModSettingsServiceInterface $modSettings + ) {} + + public function isEnabled(): bool + { + return (bool) $this->modSettings->get('feature_enabled', false); + } + + /** + * @deprecated Use instance method via DI + */ + public static function isEnabledStatic(): bool + { + trigger_error('FeatureManager::isEnabledStatic() is deprecated, use DI', E_USER_DEPRECATED); + return !empty(Config::$modSettings['feature_enabled']); + } +} +``` + +### Pattern 2: Wrapper Service + +Create a service that wraps existing static functionality: + +```php +class LegacyWrapperService +{ + public function __construct( + private ModSettingsServiceInterface $modSettings, + private SettingsServiceInterface $settings + ) {} + + public function doLegacyThing(): void + { + // Old way (still works) + // SomeStaticClass::doSomething(); + + // New way (using services) + $value = $this->modSettings->get('some_setting'); + // ... modern implementation ... + } +} +``` + +--- + +## Performance Considerations + +### Lazy Loading + +Both services use lazy loading to avoid unnecessary work: + +```php +public function getBoardUrl(): string +{ + $this->ensureLoaded(); // Only loads if not already loaded + return $this->settings['boardurl'] ?? ''; +} +``` + +### Efficiency Optimization + +Services check if Config has already loaded data: + +```php +protected function loadSettings(): void +{ + // If Config already loaded, reuse that data (fast!) + if (!empty(Config::$boardurl)) { + $this->syncFromConfig(); + return; + } + + // Otherwise, load independently (slower, but works) + $this->settings = $this->loadSettingsFile($this->settingsFile); +} +``` + +**Best of Both Worlds:** +- Fast when Config is loaded (typical case) +- Works independently when needed (testing, special cases) + +--- + +## Troubleshooting + +### Issue: Service Not Found in Container + +**Symptom**: `Container::get(MyService::class)` throws exception + +**Solution**: Register service in `Sources/Infrastructure/ServicesList.php` + +### Issue: Circular Dependency + +**Symptom**: Error about circular dependencies + +**Solution**: Refactor to break the circle: +- Use interfaces instead of concrete classes +- Extract shared logic to a new service +- Use lazy loading or optional dependencies + +--- + +## Additional Documentation + +- **[Dependency Injection Guide](DEPENDENCY_INJECTION_GUIDE.md)** - Complete DI usage guide with examples + +--- + +**Last Updated**: 2026-04-02 +**Status**: 3/3 Core Services Refactored (ErrorHandler, Settings, ModSettings) +**Next**: CacheService, DatabaseService, SessionService + + diff --git a/docs/index.php b/docs/index.php new file mode 100644 index 0000000000..2844a3b9e7 --- /dev/null +++ b/docs/index.php @@ -0,0 +1,8 @@ +