This document outlines a comprehensive, gradual migration strategy to move SMF3's most-used static variables and global state into proper services using Dependency Injection (DI) with the already-included League\Container package.
Current State Analysis:
Utils::$context- 5,733 usages (highest priority)Config::$modSettings- 2,270 usages (high priority)Config::$sourcedirand other path variables - ~30 usages (medium priority)Utils::$smcFunc- 8 usages (low priority, already being phased out)
Migration Benefits:
- Improved testability through dependency injection
- Better code organization and separation of concerns
- Reduced global state and hidden dependencies
- Easier to track data flow and debug issues
- Maintains backward compatibility during transition
Current Usage:
Config::$sourcedir
Config::$boarddir
Config::$packagesdir
Config::$vendordir
Config::$languagesdir
Config::$cachedir
Config::$scripturl
Config::$boardurl
Config::$db_prefix
Config::$db_type
// ... and many moreProposed Service Interface:
namespace SMF\Services;
interface ConfigServiceInterface
{
// Path getters
public function getSourceDir(): string;
public function getBoardDir(): string;
public function getPackagesDir(): string;
public function getVendorDir(): string;
public function getLanguagesDir(): string;
public function getCacheDir(): string;
// URL getters
public function getScriptUrl(): string;
public function getBoardUrl(): string;
// Database config
public function getDbPrefix(): string;
public function getDbType(): string;
public function getDbServer(): string;
public function getDbName(): string;
// Cache config
public function getCacheAccelerator(): string;
public function getCacheEnable(): int;
// Maintenance
public function getMaintenanceMode(): int;
public function isInMaintenance(): bool;
}Implementation Strategy:
- Create
ConfigServiceclass implementingConfigServiceInterface - Initially, methods will simply return
Config::$propertyvalues - Register as shared service in
Container::init() - Gradually refactor code to inject
ConfigServiceInterfaceinstead of accessingConfig::directly
Priority: Medium (30 usages, but foundational for other services)
Current Usage:
Config::$modSettings['setting_name']
Config::$modSettings['cache_enable']
Config::$modSettings['registration_method']
// ... 2,270+ usages throughout codebaseProposed Service Interface:
namespace SMF\Services;
interface ModSettingsServiceInterface
{
// Generic getter
public function get(string $key, mixed $default = null): mixed;
// Type-safe getters for common settings
public function getString(string $key, string $default = ''): string;
public function getInt(string $key, int $default = 0): int;
public function getBool(string $key, bool $default = false): bool;
public function getArray(string $key, array $default = []): array;
// Check existence
public function has(string $key): bool;
// Setter (for admin operations)
public function set(string $key, mixed $value): void;
// Bulk operations
public function getAll(): array;
public function setMultiple(array $settings): void;
}Implementation Strategy:
- Create
ModSettingsServiceimplementingModSettingsServiceInterface - Initially wraps
Config::$modSettingsarray access - Register as shared service in
Container::init()
Priority: HIGH (2,270 usages)
Current Usage: 5,733+ usages throughout codebase
Proposed Service Breakdown:
interface AssetServiceInterface {
public function addJavaScriptFile(string $file, array $options = []): void;
public function addCssFile(string $file, array $options = []): void;
public function addHtmlHeader(string $header): void;
}interface PageContextServiceInterface {
public function getPageTitle(): string;
public function setPageTitle(string $title): void;
public function getCurrentAction(): string;
public function getLinktree(): array;
}Priority: HIGHEST (5,733 usages)
interface PathServiceInterface {
public function getSourcePath(string $relativePath = ''): string;
public function getBoardPath(string $relativePath = ''): string;
public function getCachePath(string $relativePath = ''): string;
}interface UrlServiceInterface {
public function action(string $action, array $params = []): string;
public function profile(int $userId): string;
public function topic(int $topicId): string;
}| Variable | Current Location | Usage Count | Proposed Service | Priority |
|---|---|---|---|---|
$context |
Utils::$context |
5,733 | PageContextService, AssetService, TemplateContextService |
HIGHEST |
$modSettings |
Config::$modSettings |
2,270 | ModSettingsService |
HIGH |
$sourcedir |
Config::$sourcedir |
~30 | PathService |
MEDIUM |
$boarddir |
Config::$boarddir |
~25 | PathService |
MEDIUM |
$scripturl |
Config::$scripturl |
~200 | UrlService |
MEDIUM-HIGH |
$boardurl |
Config::$boardurl |
~50 | UrlService |
MEDIUM |
| Variable | Current Location | Proposed Service | Notes |
|---|---|---|---|
$cachedir |
Config::$cachedir |
PathService |
Cache path management |
$packagesdir |
Config::$packagesdir |
PathService |
Package management |
$languagesdir |
Config::$languagesdir |
PathService |
Language files |
$db_prefix |
Config::$db_prefix |
ConfigService |
Database config |
$db_type |
Config::$db_type |
ConfigService |
Database config |
$maintenance |
Config::$maintenance |
ConfigService |
Site status |
| Variable | Current Location | Status | Notes |
|---|---|---|---|
$smcFunc |
Utils::$smcFunc |
8 usages | Already being phased out |
$user_info |
User::$me |
Aliased | Use User::$me directly |
$user_profile |
User::$profiles |
Aliased | Use User::$profiles directly |
$txt |
Lang::$txt |
Active | Consider LangService in future |
- Create service interfaces in
Sources/Services/ - Implement
ConfigService - Implement
ModSettingsService - Update
Container::init()to register new services - Create service provider pattern for cleaner registration
- Implement
AssetService - Implement
PageContextService - Implement
TemplateContextService - Create facade layer in
Utils::$contextto delegate to services - Update 2-3 high-traffic Actions to use DI
- Implement
PathService - Implement
UrlService - Refactor file inclusion patterns
- Refactor URL generation patterns
- Migrate Admin actions (high value, contained scope)
- Migrate Profile actions
- Migrate Board/Topic actions
- Update templates to use service-provided data
- Comprehensive unit tests for all services
- Integration tests for DI container
- Performance benchmarking
- Documentation updates
Current State (Sources/Container.php):
public static function init(): LeagueContainer
{
$container = new LeagueContainer();
// Enable auto-wiring
$container->delegate(new ReflectionContainer());
// Register core services
$container->add(DatabaseServiceInterface::class, DatabaseService::class)->setShared(true);
self::$instance = $container;
return $container;
}Proposed Enhanced Configuration:
public static function init(): LeagueContainer
{
$container = new LeagueContainer();
// Enable auto-wiring
$container->delegate(new ReflectionContainer());
// Register service providers
$container->addServiceProvider(new CoreServiceProvider());
$container->addServiceProvider(new ContextServiceProvider());
$container->addServiceProvider(new ConfigServiceProvider());
self::$instance = $container;
return $container;
}Create Sources/Services/Providers/CoreServiceProvider.php:
<?php
namespace SMF\Services\Providers;
use League\Container\ServiceProvider\AbstractServiceProvider;
use SMF\Services\{
ConfigService,
ConfigServiceInterface,
ModSettingsService,
ModSettingsServiceInterface,
PathService,
PathServiceInterface,
UrlService,
UrlServiceInterface
};
class CoreServiceProvider extends AbstractServiceProvider
{
protected $provides = [
ConfigServiceInterface::class,
ModSettingsServiceInterface::class,
PathServiceInterface::class,
UrlServiceInterface::class,
];
public function register(): void
{
// Config Service - Shared singleton
$this->getContainer()
->add(ConfigServiceInterface::class, ConfigService::class)
->setShared(true);
// ModSettings Service - Shared singleton
$this->getContainer()
->add(ModSettingsServiceInterface::class, ModSettingsService::class)
->setShared(true);
// Path Service - Shared singleton
$this->getContainer()
->add(PathServiceInterface::class, PathService::class)
->setShared(true)
->addArgument(ConfigServiceInterface::class);
// URL Service - Shared singleton
$this->getContainer()
->add(UrlServiceInterface::class, UrlService::class)
->setShared(true)
->addArgument(ConfigServiceInterface::class);
}
}Before:
class MyAction implements ActionInterface
{
public function execute(): void
{
$setting = Config::$modSettings['some_setting'];
$path = Config::$sourcedir . '/SomeFile.php';
Utils::$context['page_title'] = 'My Page';
}
}After:
class MyAction implements ActionInterface
{
public function __construct(
private ModSettingsServiceInterface $modSettings,
private PathServiceInterface $paths,
private PageContextServiceInterface $pageContext
) {}
public function execute(): void
{
$setting = $this->modSettings->get('some_setting');
$path = $this->paths->getSourcePath('SomeFile.php');
$this->pageContext->setPageTitle('My Page');
}
}Helper Function (Sources/Services/helpers.php):
<?php
use SMF\Container;
use SMF\Services\ModSettingsServiceInterface;
if (!function_exists('modSettings')) {
function modSettings(?string $key = null, mixed $default = null): mixed
{
$service = Container::getInstance()->get(ModSettingsServiceInterface::class);
if ($key === null) {
return $service;
}
return $service->get($key, $default);
}
}
if (!function_exists('config')) {
function config(?string $key = null): mixed
{
$service = Container::getInstance()->get(ConfigServiceInterface::class);
if ($key === null) {
return $service;
}
return match($key) {
'sourcedir' => $service->getSourceDir(),
'boarddir' => $service->getBoardDir(),
'scripturl' => $service->getScriptUrl(),
default => null
};
}
}Usage in Legacy Code:
// Old way (still works during transition)
$setting = Config::$modSettings['some_setting'];
// New helper function way
$setting = modSettings('some_setting');
// Or get the service
$modSettings = modSettings();
$setting = $modSettings->get('some_setting');Sources/Services/ModSettingsService.php:
<?php
namespace SMF\Services;
use SMF\Config;
class ModSettingsService implements ModSettingsServiceInterface
{
private array $settings;
public function __construct()
{
// Initially, wrap the static array
$this->settings = &Config::$modSettings;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->settings[$key] ?? $default;
}
public function getString(string $key, string $default = ''): string
{
return (string) ($this->settings[$key] ?? $default);
}
public function getInt(string $key, int $default = 0): int
{
return (int) ($this->settings[$key] ?? $default);
}
public function getBool(string $key, bool $default = false): bool
{
return !empty($this->settings[$key]);
}
public function getArray(string $key, array $default = []): array
{
$value = $this->settings[$key] ?? $default;
return is_array($value) ? $value : $default;
}
public function has(string $key): bool
{
return isset($this->settings[$key]);
}
public function set(string $key, mixed $value): void
{
$this->settings[$key] = $value;
}
public function getAll(): array
{
return $this->settings;
}
public function setMultiple(array $settings): void
{
foreach ($settings as $key => $value) {
$this->settings[$key] = $value;
}
}
}Both old and new patterns work simultaneously:
// Old way - still works
$value = Config::$modSettings['setting'];
// New way - preferred
$value = $this->modSettings->get('setting');Add deprecation notices to static access:
class Config
{
public static function __get($name)
{
if ($name === 'modSettings') {
trigger_error(
'Direct access to Config::$modSettings is deprecated. Use ModSettingsService instead.',
E_USER_DEPRECATED
);
return self::$modSettings;
}
}
}Remove static properties, keep only service-based access.
Tests/Services/ModSettingsServiceTest.php:
<?php
namespace SMF\Tests\Services;
use PHPUnit\Framework\TestCase;
use SMF\Services\ModSettingsService;
class ModSettingsServiceTest extends TestCase
{
private ModSettingsService $service;
protected function setUp(): void
{
$this->service = new ModSettingsService();
}
public function testGetReturnsValue(): void
{
$this->service->set('test_key', 'test_value');
$this->assertEquals('test_value', $this->service->get('test_key'));
}
public function testGetReturnsDefault(): void
{
$this->assertEquals('default', $this->service->get('nonexistent', 'default'));
}
public function testGetIntReturnsInteger(): void
{
$this->service->set('number', '42');
$this->assertSame(42, $this->service->getInt('number'));
}
public function testGetBoolReturnsBool(): void
{
$this->service->set('enabled', 1);
$this->assertTrue($this->service->getBool('enabled'));
$this->service->set('disabled', 0);
$this->assertFalse($this->service->getBool('disabled'));
}
}Tests/Integration/ContainerTest.php:
<?php
namespace SMF\Tests\Integration;
use PHPUnit\Framework\TestCase;
use SMF\Container;
use SMF\Services\ModSettingsServiceInterface;
use SMF\Services\ConfigServiceInterface;
class ContainerTest extends TestCase
{
public function testContainerResolvesModSettingsService(): void
{
$container = Container::getInstance();
$service = $container->get(ModSettingsServiceInterface::class);
$this->assertInstanceOf(ModSettingsServiceInterface::class, $service);
}
public function testServicesAreSingletons(): void
{
$container = Container::getInstance();
$service1 = $container->get(ModSettingsServiceInterface::class);
$service2 = $container->get(ModSettingsServiceInterface::class);
$this->assertSame($service1, $service2);
}
}The overhead of dependency injection is minimal when properly implemented:
- Constructor injection: Negligible overhead (services resolved once)
- Container resolution: Small overhead (acceptable for non-hot paths)
- Helper functions: Slight overhead (convenience vs performance trade-off)
Recommendation: Use constructor injection for hot paths, helper functions for legacy code.
- Lazy Loading: Services are only instantiated when needed
- Shared Instances: All services registered as
setShared(true) - Avoid Container in Loops: Inject dependencies in constructor
- Cache Service References: Don't resolve from container repeatedly
- Define interface in
Sources/Services/ - Implement service class
- Write unit tests
- Register in service provider
- Update container initialization
- Create helper functions (if needed)
- Document usage in this plan
- Migrate 2-3 example usages
- Performance benchmark
- Code review
- Identify all static/global dependencies
- Add constructor parameters for services
- Update instantiation to use container
- Replace static calls with service calls
- Update tests to inject mocks
- Verify functionality
- Check performance impact
Problem: ServiceA needs ServiceB, ServiceB needs ServiceA
Solution:
- Refactor to remove circular dependency
- Use events/hooks for decoupling
- Introduce a third service that both depend on
Problem: Service tries to access Config before it's loaded
Solution:
- Use lazy initialization in service constructor
- Defer heavy operations to first method call
- Use factory pattern for complex initialization
Problem: Templates can't use constructor injection
Solution:
// In Action class
public function execute(): void
{
Utils::$context['page_title'] = $this->pageContext->getPageTitle();
Utils::$context['services'] = [
'assets' => $this->assets,
'urls' => $this->urls,
];
}
// In template
echo $context['services']['urls']->action('profile', ['u' => $user_id]);- Significantly reduce static property access
- Increase test coverage
- Zero new static property additions
- All new Actions use DI
- Minimal page load time impact
- Minimal memory usage increase
- No degradation in database query performance
- Complete onboarding documentation
- IDE autocomplete works for all services
- Clear migration examples for each pattern
- Positive feedback on new patterns
This migration plan provides a structured, gradual approach to modernizing SMF3's architecture by:
- Prioritizing high-impact changes (Utils::$context, Config::$modSettings)
- Maintaining backward compatibility throughout the transition
- Using proven patterns (Service Provider, Constructor Injection)
- Providing clear examples for each migration scenario
- Ensuring testability with comprehensive test coverage
The migration can be executed incrementally, with each phase building on the previous one. The end result will be a more maintainable, testable, and modern codebase that's easier to extend and debug.
Next Steps:
- Review this plan
- Create initial service interfaces
- Implement first service (ModSettingsService)
- Migrate one Action as proof of concept
- Iterate and refine based on learnings