From dc326202a37e174840c4bee82fc8bbe43bbcdee3 Mon Sep 17 00:00:00 2001 From: Michel Mendiola Date: Thu, 2 Apr 2026 09:33:53 -0600 Subject: [PATCH 1/7] chore: Split settings and modSettings to be independent of Config.php --- Sources/Config.php | 65 +++++++++++++++++++++++++ Sources/Infrastructure/ServicesList.php | 11 +++++ 2 files changed, 76 insertions(+) diff --git a/Sources/Config.php b/Sources/Config.php index 05fa3155df..a6ba6d30b0 100644 --- a/Sources/Config.php +++ b/Sources/Config.php @@ -2933,4 +2933,69 @@ protected static function getTempDir(): string return Sapi::getTempDir(); } + + /** + * 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; + } } 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, ], From 84b07f324b605226557e665e71c9ec2a5f2c3072 Mon Sep 17 00:00:00 2001 From: Michel Mendiola Date: Thu, 2 Apr 2026 11:04:03 -0600 Subject: [PATCH 2/7] chore: Split settings and modSettings to be independent of Config.php --- .../Contracts/ModSettingsServiceInterface.php | 123 +++ .../Contracts/SettingsServiceInterface.php | 148 ++++ Sources/Services/ModSettingsService.php | 394 ++++++++++ Sources/Services/SettingsService.php | 336 ++++++++ docs/DEPENDENCY_INJECTION_GUIDE.md | 723 ++++++++++++++++++ docs/README.md | 207 +++++ docs/REFACTORED_SERVICES_SUMMARY.md | 486 ++++++++++++ 7 files changed, 2417 insertions(+) create mode 100644 Sources/Services/Contracts/ModSettingsServiceInterface.php create mode 100644 Sources/Services/Contracts/SettingsServiceInterface.php create mode 100644 Sources/Services/ModSettingsService.php create mode 100644 Sources/Services/SettingsService.php create mode 100644 docs/DEPENDENCY_INJECTION_GUIDE.md create mode 100644 docs/README.md create mode 100644 docs/REFACTORED_SERVICES_SUMMARY.md diff --git a/Sources/Services/Contracts/ModSettingsServiceInterface.php b/Sources/Services/Contracts/ModSettingsServiceInterface.php new file mode 100644 index 0000000000..169568c09d --- /dev/null +++ b/Sources/Services/Contracts/ModSettingsServiceInterface.php @@ -0,0 +1,123 @@ + 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 + * @return void + */ + 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) + * @return void + */ + 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 + * @return void + */ + public function delete(string|array $keys): void; + + /** + * Reload mod settings from the database. + * + * This clears the cache and reloads all settings from the database. + * + * @return void + */ + public function reload(): void; + + /** + * Clear the mod settings cache. + * + * @return void + */ + 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..bac448e744 --- /dev/null +++ b/Sources/Services/Contracts/SettingsServiceInterface.php @@ -0,0 +1,148 @@ +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; + } + + /** + * Ensure settings are loaded from database. + * + * @return void + */ + protected function ensureLoaded(): void + { + if (!$this->loaded) { + $this->loadFromDatabase(); + } + } + + /** + * Load settings from database. + * + * @return void + */ + 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. + * + * @return void + */ + 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..f11753d070 --- /dev/null +++ b/Sources/Services/SettingsService.php @@ -0,0 +1,336 @@ +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'] ?? ''; + } + + /** + * Ensure settings are loaded from Settings.php. + * + * @return void + */ + 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. + * + * @return void + */ + 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. + * + * @return void + */ + 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..5dc2a063d6 --- /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 + +#### ⚠️ 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)) { + // ... +} +``` + +### ⚠️ 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..66885d2dd6 --- /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(); +``` + +### ⚠️ 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..9a3f6520a2 --- /dev/null +++ b/docs/REFACTORED_SERVICES_SUMMARY.md @@ -0,0 +1,486 @@ +# 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) + +### Caching + +ModSettingsService uses CacheApi for performance: + +```php +protected function loadFromDatabase(): void +{ + // Try cache first + if (\is_array($temp = CacheApi::get('modSettings', 90))) { + $this->settings = $temp; + return; + } + + // Load from database + // ... + + // Cache the result + CacheApi::put('modSettings', $this->settings, 90); +} +``` + +--- + +## 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 +- **[Config Services Migration Guide](../CONFIG_SERVICES_MIGRATION_GUIDE.md)** - Detailed config services documentation +- **[Config Services Independence Summary](../CONFIG_SERVICES_INDEPENDENCE_SUMMARY.md)** - Architecture and independence analysis +- **[Migration Plan](https://github.com/MissAllSunday/SMF2.1/blob/Dependency-injection-proposal/DEPENDENCY_INJECTION_MIGRATION_PLAN.md)** - Overall migration strategy + +--- + +**Last Updated**: 2026-04-02 +**Status**: 3/3 Core Services Refactored (ErrorHandler, Settings, ModSettings) +**Next**: CacheService, DatabaseService, SessionService + + From 4548c103b9355daad5bff53e56def03940d7768b Mon Sep 17 00:00:00 2001 From: Michel Mendiola Date: Thu, 2 Apr 2026 11:12:36 -0600 Subject: [PATCH 3/7] chore: update docs to remove files that no longer exists --- docs/REFACTORED_SERVICES_SUMMARY.md | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/docs/REFACTORED_SERVICES_SUMMARY.md b/docs/REFACTORED_SERVICES_SUMMARY.md index 9a3f6520a2..87aed72a5c 100644 --- a/docs/REFACTORED_SERVICES_SUMMARY.md +++ b/docs/REFACTORED_SERVICES_SUMMARY.md @@ -428,27 +428,6 @@ protected function loadSettings(): void - ✅ Fast when Config is loaded (typical case) - ✅ Works independently when needed (testing, special cases) -### Caching - -ModSettingsService uses CacheApi for performance: - -```php -protected function loadFromDatabase(): void -{ - // Try cache first - if (\is_array($temp = CacheApi::get('modSettings', 90))) { - $this->settings = $temp; - return; - } - - // Load from database - // ... - - // Cache the result - CacheApi::put('modSettings', $this->settings, 90); -} -``` - --- ## Troubleshooting @@ -473,9 +452,6 @@ protected function loadFromDatabase(): void ## Additional Documentation - **[Dependency Injection Guide](DEPENDENCY_INJECTION_GUIDE.md)** - Complete DI usage guide with examples -- **[Config Services Migration Guide](../CONFIG_SERVICES_MIGRATION_GUIDE.md)** - Detailed config services documentation -- **[Config Services Independence Summary](../CONFIG_SERVICES_INDEPENDENCE_SUMMARY.md)** - Architecture and independence analysis -- **[Migration Plan](https://github.com/MissAllSunday/SMF2.1/blob/Dependency-injection-proposal/DEPENDENCY_INJECTION_MIGRATION_PLAN.md)** - Overall migration strategy --- From c39186d6f22106e263a845f8ad5ca249c6bfd0e0 Mon Sep 17 00:00:00 2001 From: Michel Mendiola Date: Thu, 2 Apr 2026 11:28:24 -0600 Subject: [PATCH 4/7] fix: I always forget to add an index file to new folders... --- Sources/Services/Contracts/index.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Sources/Services/Contracts/index.php diff --git a/Sources/Services/Contracts/index.php b/Sources/Services/Contracts/index.php new file mode 100644 index 0000000000..2844a3b9e7 --- /dev/null +++ b/Sources/Services/Contracts/index.php @@ -0,0 +1,8 @@ + Date: Thu, 2 Apr 2026 11:30:14 -0600 Subject: [PATCH 5/7] fix: apply cs-fixer --- Sources/Config.php | 42 ++++++++++--------- .../Contracts/ModSettingsServiceInterface.php | 10 ++--- .../Contracts/SettingsServiceInterface.php | 6 ++- Sources/Services/ModSettingsService.php | 20 +++++---- Sources/Services/SettingsService.php | 15 +++++-- 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/Sources/Config.php b/Sources/Config.php index a6ba6d30b0..fd4cdc4ba2 100644 --- a/Sources/Config.php +++ b/Sources/Config.php @@ -2914,26 +2914,6 @@ public static function checkCron(): void } } - /************************* - * Internal static methods - *************************/ - - /** - * Wrapper for SMF\Sapi::getTempDir(). - * - * Loads the Sapi class if necessary, and then calls Sapi::getTempDir(). - */ - protected static function getTempDir(): string - { - // Can't rely on autoloading because this method may be - // called before the autoloader exists. - if (!class_exists('\\SMF\\Sapi', false)) { - require_once self::$sourcedir . DIRECTORY_SEPARATOR . 'Sapi.php'; - } - - return Sapi::getTempDir(); - } - /** * Get the SettingsService instance from the container. * @@ -2954,6 +2934,7 @@ public static function getSettingsService(): Services\SettingsService // 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 @@ -2987,6 +2968,7 @@ public static function getModSettingsService(): Services\ModSettingsService // 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 @@ -2998,4 +2980,24 @@ public static function getModSettingsService(): Services\ModSettingsService return $service; } + + /************************* + * Internal static methods + *************************/ + + /** + * Wrapper for SMF\Sapi::getTempDir(). + * + * Loads the Sapi class if necessary, and then calls Sapi::getTempDir(). + */ + protected static function getTempDir(): string + { + // Can't rely on autoloading because this method may be + // called before the autoloader exists. + if (!class_exists('\\SMF\\Sapi', false)) { + require_once self::$sourcedir . DIRECTORY_SEPARATOR . 'Sapi.php'; + } + + return Sapi::getTempDir(); + } } diff --git a/Sources/Services/Contracts/ModSettingsServiceInterface.php b/Sources/Services/Contracts/ModSettingsServiceInterface.php index 169568c09d..96609133e6 100644 --- a/Sources/Services/Contracts/ModSettingsServiceInterface.php +++ b/Sources/Services/Contracts/ModSettingsServiceInterface.php @@ -24,6 +24,10 @@ */ interface ModSettingsServiceInterface { + /**************** + * Public methods + ****************/ + /** * Get a mod setting value. * @@ -55,7 +59,6 @@ public function has(string $key): bool; * * @param string $key The setting key * @param mixed $value The value to set - * @return void */ public function set(string $key, mixed $value): void; @@ -67,7 +70,6 @@ public function set(string $key, mixed $value): void; * @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) - * @return void */ public function update(array $settings, bool $update = false): void; @@ -75,7 +77,6 @@ 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 - * @return void */ public function delete(string|array $keys): void; @@ -84,14 +85,12 @@ public function delete(string|array $keys): void; * * This clears the cache and reloads all settings from the database. * - * @return void */ public function reload(): void; /** * Clear the mod settings cache. * - * @return void */ public function clearCache(): void; @@ -120,4 +119,3 @@ public function hasAny(array $keys): bool; */ public function hasAll(array $keys): bool; } - diff --git a/Sources/Services/Contracts/SettingsServiceInterface.php b/Sources/Services/Contracts/SettingsServiceInterface.php index bac448e744..a56dbfa2d4 100644 --- a/Sources/Services/Contracts/SettingsServiceInterface.php +++ b/Sources/Services/Contracts/SettingsServiceInterface.php @@ -24,6 +24,10 @@ */ interface SettingsServiceInterface { + /**************** + * Public methods + ****************/ + /** * Get a configuration value from Settings.php. * @@ -40,7 +44,6 @@ public function get(string $key, mixed $default = null): mixed; * * @param string $key The configuration key * @param mixed $value The value to set - * @return void */ public function set(string $key, mixed $value): void; @@ -145,4 +148,3 @@ public function getDatabaseName(): string; */ public function getDatabasePrefix(): string; } - diff --git a/Sources/Services/ModSettingsService.php b/Sources/Services/ModSettingsService.php index 25e486b0d7..99639f4d12 100644 --- a/Sources/Services/ModSettingsService.php +++ b/Sources/Services/ModSettingsService.php @@ -30,6 +30,10 @@ */ class ModSettingsService implements ModSettingsServiceInterface { + /********************* + * Internal properties + *********************/ + /** * @var array * @@ -44,12 +48,14 @@ class ModSettingsService implements ModSettingsServiceInterface */ protected bool $loaded = false; + /**************** + * Public methods + ****************/ + /** * Constructor. */ - public function __construct() - { - } + public function __construct() {} public function get(string $key, mixed $default = null): mixed { @@ -283,10 +289,13 @@ public function hasAll(array $keys): bool return true; } + /****************** + * Internal methods + ******************/ + /** * Ensure settings are loaded from database. * - * @return void */ protected function ensureLoaded(): void { @@ -298,7 +307,6 @@ protected function ensureLoaded(): void /** * Load settings from database. * - * @return void */ protected function loadFromDatabase(): void { @@ -360,7 +368,6 @@ protected function loadFromDatabase(): void * * This ensures critical settings have valid values. * - * @return void */ protected function applyDefaults(): void { @@ -391,4 +398,3 @@ protected function applyDefaults(): void } } } - diff --git a/Sources/Services/SettingsService.php b/Sources/Services/SettingsService.php index f11753d070..05332f4867 100644 --- a/Sources/Services/SettingsService.php +++ b/Sources/Services/SettingsService.php @@ -26,6 +26,10 @@ */ class SettingsService implements SettingsServiceInterface { + /********************* + * Internal properties + *********************/ + /** * @var array * @@ -47,6 +51,10 @@ class SettingsService implements SettingsServiceInterface */ protected string $settingsFile; + /**************** + * Public methods + ****************/ + /** * Constructor. * @@ -224,10 +232,13 @@ public function getDatabasePrefix(): string return $this->settings['db_prefix'] ?? ''; } + /****************** + * Internal methods + ******************/ + /** * Ensure settings are loaded from Settings.php. * - * @return void */ protected function ensureLoaded(): void { @@ -242,7 +253,6 @@ protected function ensureLoaded(): void * This method loads settings independently from Config class. * If Config has already loaded settings, we use those for efficiency. * - * @return void */ protected function loadSettings(): void { @@ -308,7 +318,6 @@ protected function loadSettingsFile(string $file): array * * This is used when Config has already loaded settings for efficiency. * - * @return void */ protected function syncFromConfig(): void { From 232d55c898c7467fe027625b9df612c6390c8de3 Mon Sep 17 00:00:00 2001 From: Michel Mendiola Date: Thu, 2 Apr 2026 11:31:31 -0600 Subject: [PATCH 6/7] fix: oh come on! another missing index file --- docs/index.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/index.php 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 @@ + Date: Thu, 2 Apr 2026 11:37:05 -0600 Subject: [PATCH 7/7] chore: Remove not needed emojis --- docs/DEPENDENCY_INJECTION_GUIDE.md | 58 ++++++++++++++--------------- docs/README.md | 38 +++++++++---------- docs/REFACTORED_SERVICES_SUMMARY.md | 42 ++++++++++----------- 3 files changed, 69 insertions(+), 69 deletions(-) diff --git a/docs/DEPENDENCY_INJECTION_GUIDE.md b/docs/DEPENDENCY_INJECTION_GUIDE.md index 5dc2a063d6..8cc8a3e482 100644 --- a/docs/DEPENDENCY_INJECTION_GUIDE.md +++ b/docs/DEPENDENCY_INJECTION_GUIDE.md @@ -179,10 +179,10 @@ return [ ``` **Why this is best:** -- ✅ Testable (inject mocks) -- ✅ Clear dependencies -- ✅ Type-safe -- ✅ IDE autocomplete support +- Testable (inject mocks) +- Clear dependencies +- Type-safe +- IDE autocomplete support #### Option 2: Container Direct Access (For Actions/Controllers) @@ -202,25 +202,25 @@ $result = $myService->performTask(); - One-off service access in procedural code - Bootstrapping/initialization code -#### ⚠️ Facade Pattern (Deprecated - Existing Code Only) +#### 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 +// DEPRECATED - Do not use in new code $settings = \SMF\Config::getSettingsService(); $modSettings = \SMF\Config::getModSettingsService(); -// ⛔ LEGACY - Still works but avoid in new code +// 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 +- Tight coupling to static classes +- Harder to test +- Hidden dependencies +- Not following DI principles **Facades are only maintained for:** - Backward compatibility with existing code @@ -398,10 +398,10 @@ class UserProfileServiceTest extends TestCase ``` **Benefits:** -- ✅ No database needed for tests -- ✅ Complete control over dependencies -- ✅ Fast test execution -- ✅ Test edge cases easily +- No database needed for tests +- Complete control over dependencies +- Fast test execution +- Test edge cases easily ## Best Practices @@ -423,7 +423,7 @@ class MyService { public function doWork() { - // ⛔ Don't do this in new code + // Don't do this in new code $settings = Config::getModSettingsService(); $value = Config::$modSettings['key']; } @@ -514,7 +514,7 @@ return [ ### Phase 1: Backward Compatible Services (Current) -**Status**: ✅ Complete +**Status**: Complete - Create service classes that work alongside static classes - Services can use facades for backward compatibility @@ -522,7 +522,7 @@ return [ ### Phase 2: New Code Uses DI (In Progress) -**Status**: 🔄 Ongoing +**Status**: Ongoing - All new features use dependency injection - Gradually refactor existing code when touched @@ -530,7 +530,7 @@ return [ ### Phase 3: Full Migration (Future) -**Status**: ⏳ Planned +**Status**: Planned - Static facades become thin wrappers around services - All business logic in services @@ -560,17 +560,17 @@ if (Container::has(MyService::class)) { } ``` -### ⚠️ Facade Access (Deprecated - Existing Code Only) +### WARNING: Facade Access (Deprecated - Existing Code Only) **Do NOT use in new code. Only for backward compatibility.** ```php -// ⛔ DEPRECATED - Existing code only +// DEPRECATED - Existing code only $settings = \SMF\Config::getSettingsService(); $modSettings = \SMF\Config::getModSettingsService(); \SMF\ErrorHandler::log('message', 'error'); -// ✅ NEW CODE - Use constructor injection instead +// NEW CODE - Use constructor injection instead public function __construct( private SettingsServiceInterface $settings, private ModSettingsServiceInterface $modSettings, @@ -677,12 +677,12 @@ return [ 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 +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:** @@ -718,6 +718,6 @@ return [ --- -**Remember**: The goal is gradual, non-breaking migration to improve testability and maintainability! 🚀 +**Remember**: The goal is gradual, non-breaking migration to improve testability and maintainability! diff --git a/docs/README.md b/docs/README.md index 66885d2dd6..b53134a50c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ Welcome to the SMF Dependency Injection documentation! This directory contains comprehensive guides for understanding and using the new service-based architecture. -## 📚 Documentation Index +## Documentation Index ### 1. [Dependency Injection Guide](DEPENDENCY_INJECTION_GUIDE.md) **Complete guide for using DI in SMF** @@ -27,7 +27,7 @@ Welcome to the SMF Dependency Injection documentation! This directory contains c **Best for**: Developers who need a quick reference for using existing services. -## 🚀 Quick Start +## Quick Start ### For New Features @@ -85,14 +85,14 @@ $action = Container::get(MyNewAction::class); $action->execute(); ``` -### ⚠️ For Legacy Code Only +### 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 +// DEPRECATED - Only use in existing code $settings = \SMF\Config::getSettingsService(); $modSettings = \SMF\Config::getModSettingsService(); @@ -105,15 +105,15 @@ $enabled = $modSettings->get('feature_enabled', false); - Constructor injection (preferred) - Container direct access (if injection not possible) -## 📊 Refactoring Status +## 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) | +| **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 +## Additional Resources ### In This Repository @@ -124,7 +124,7 @@ $enabled = $modSettings->get('feature_enabled', false); - **[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 +## Common Tasks ### Creating a New Service @@ -162,7 +162,7 @@ $service = new MyService($mock); [Full guide →](DEPENDENCY_INJECTION_GUIDE.md#testing-with-dependency-injection) -## ❓ FAQ +## 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. @@ -179,18 +179,18 @@ A: Yes! Use `'shared' => false` in ServicesList.php. Most services use `'shared' **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 +## 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 +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 +## Documentation Standards When documenting new services: diff --git a/docs/REFACTORED_SERVICES_SUMMARY.md b/docs/REFACTORED_SERVICES_SUMMARY.md index 87aed72a5c..e59a93f9e7 100644 --- a/docs/REFACTORED_SERVICES_SUMMARY.md +++ b/docs/REFACTORED_SERVICES_SUMMARY.md @@ -8,15 +8,15 @@ This document provides a summary of all services that have been refactored to us | 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 | +| **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 +- 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 @@ -31,9 +31,9 @@ This document provides a summary of all services that have been refactored to us - Facade: `Sources/ErrorHandler.php` **Key Features:** -- ✅ Handles PHP errors and exceptions -- ✅ Logging with different severity levels -- ✅ Backward compatible facade +- Handles PHP errors and exceptions +- Logging with different severity levels +- Backward compatible facade **Public Methods:** ```php @@ -75,11 +75,11 @@ ErrorHandler::log('Something went wrong', 'error'); - 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 +- **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 @@ -291,11 +291,11 @@ class MyNewActionTest extends TestCase ``` **Benefits:** -- ✅ No database required -- ✅ No Settings.php file required -- ✅ Fast test execution -- ✅ Complete control over behavior -- ✅ Test edge cases easily +- No database required +- No Settings.php file required +- Fast test execution +- Complete control over behavior +- Test edge cases easily --- @@ -425,8 +425,8 @@ protected function loadSettings(): void ``` **Best of Both Worlds:** -- ✅ Fast when Config is loaded (typical case) -- ✅ Works independently when needed (testing, special cases) +- Fast when Config is loaded (typical case) +- Works independently when needed (testing, special cases) ---