PSR-11 Container in XOOPS 4.0
PSR-11 Container
Section titled “PSR-11 Container”Overview
Section titled “Overview”PSR-11 defines a common interface for dependency injection containers. XOOPS 4.0 implements a fully PSR-11 compliant container that manages service instantiation, dependency resolution, and lifecycle management.
PSR-11 Interface
Section titled “PSR-11 Interface”ContainerInterface
Section titled “ContainerInterface”namespace Psr\Container;
interface ContainerInterface{ /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * @return mixed Entry. * @throws NotFoundExceptionInterface No entry was found. * @throws ContainerExceptionInterface Error while retrieving the entry. */ public function get(string $id): mixed;
/** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * @param string $id Identifier of the entry to look for. * @return bool */ public function has(string $id): bool;}Exception Interfaces
Section titled “Exception Interfaces”namespace Psr\Container;
interface ContainerExceptionInterface extends \Throwable {}
interface NotFoundExceptionInterface extends ContainerExceptionInterface {}XOOPS Container Implementation
Section titled “XOOPS Container Implementation”XoopsContainer Class
Section titled “XoopsContainer Class”<?php
declare(strict_types=1);
namespace Xoops\Core\Container;
use Psr\Container\ContainerInterface;use Psr\Container\NotFoundExceptionInterface;
class XoopsContainer implements ContainerInterface{ /** @var array<string, callable|object> */ private array $services = [];
/** @var array<string, object> */ private array $instances = [];
/** @var array<string, string> */ private array $aliases = [];
/** * Register a service factory */ public function set(string $id, callable|object $service): void { $this->services[$id] = $service; unset($this->instances[$id]); // Clear cached instance }
/** * Register a service alias */ public function alias(string $alias, string $id): void { $this->aliases[$alias] = $id; }
/** * Get a service by ID */ public function get(string $id): mixed { // Resolve alias $resolvedId = $this->aliases[$id] ?? $id;
// Return cached instance if available if (isset($this->instances[$resolvedId])) { return $this->instances[$resolvedId]; }
if (!isset($this->services[$resolvedId])) { throw new ServiceNotFoundException( sprintf('Service "%s" not found in container.', $id) ); }
$service = $this->services[$resolvedId];
// If it's a callable (factory), execute it if (is_callable($service)) { $instance = $service($this); $this->instances[$resolvedId] = $instance; return $instance; }
// If it's already an object, cache and return it $this->instances[$resolvedId] = $service; return $service; }
/** * Check if a service exists */ public function has(string $id): bool { $resolvedId = $this->aliases[$id] ?? $id; return isset($this->services[$resolvedId]); }
/** * Get a new instance (bypass cache) */ public function make(string $id): mixed { $resolvedId = $this->aliases[$id] ?? $id;
if (!isset($this->services[$resolvedId])) { throw new ServiceNotFoundException( sprintf('Service "%s" not found in container.', $id) ); }
$service = $this->services[$resolvedId];
if (is_callable($service)) { return $service($this); }
// For objects, create a clone if (is_object($service)) { return clone $service; }
return $service; }}Service Not Found Exception
Section titled “Service Not Found Exception”<?php
declare(strict_types=1);
namespace Xoops\Core\Container;
use Psr\Container\NotFoundExceptionInterface;
class ServiceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface{}Service Registration
Section titled “Service Registration”Service Providers
Section titled “Service Providers”<?php
declare(strict_types=1);
namespace Xoops\Core\Container;
interface ServiceProviderInterface{ /** * Register services with the container */ public function register(ContainerInterface $container): void;
/** * Boot services after all providers are registered */ public function boot(ContainerInterface $container): void;}Core Service Provider
Section titled “Core Service Provider”<?php
declare(strict_types=1);
namespace Xoops\Core\Provider;
use Psr\Container\ContainerInterface;use Xoops\Core\Container\ServiceProviderInterface;use Xoops\Core\Database\Connection;use Xoops\Core\View\SmartyViewRenderer;use Xoops\Core\View\ViewRendererInterface;use Xoops\Core\Http\ApiResponse;use Monolog\Logger;use Monolog\Handler\RotatingFileHandler;
class CoreServiceProvider implements ServiceProviderInterface{ public function register(ContainerInterface $container): void { // Database Connection $container->set('database', function (ContainerInterface $c) { $config = $c->get('config'); return new Connection([ 'host' => $config['db_host'], 'database' => $config['db_name'], 'username' => $config['db_user'], 'password' => $config['db_pass'], 'prefix' => $config['db_prefix'], ]); });
// Alias for type-hint usage $container->alias(Connection::class, 'database');
// Logger $container->set('logger', function (ContainerInterface $c) { $logger = new Logger('xoops'); $logger->pushHandler(new RotatingFileHandler( XOOPS_VAR_PATH . '/logs/xoops.log', 30, Logger::WARNING )); return $logger; });
$container->alias(\Psr\Log\LoggerInterface::class, 'logger');
// View Renderer $container->set(ViewRendererInterface::class, function (ContainerInterface $c) { return new SmartyViewRenderer($c->get('smarty')); });
// API Response Helper $container->set(ApiResponse::class, function () { return new ApiResponse(); });
// Configuration $container->set('config', function () { return require XOOPS_VAR_PATH . '/configs/xoopsconfig.php'; }); }
public function boot(ContainerInterface $container): void { // Boot logic, if needed }}Module Service Provider
Section titled “Module Service Provider”<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher;
use Psr\Container\ContainerInterface;use Xoops\Core\Container\ServiceProviderInterface;
class ModuleServiceProvider implements ServiceProviderInterface{ public function register(ContainerInterface $container): void { // Repository $container->set('publisher.article_repository', function (ContainerInterface $c) { return new Repository\ArticleRepository($c->get('database')); });
$container->alias( Repository\ArticleRepositoryInterface::class, 'publisher.article_repository' );
// Service $container->set('publisher.article_service', function (ContainerInterface $c) { return new Service\ArticleService( $c->get('publisher.article_repository'), $c->get('event_dispatcher') ); });
// Controller $container->set(Controller\ArticleController::class, function (ContainerInterface $c) { return new Controller\ArticleController( $c->get('publisher.article_service'), $c->get(ViewRendererInterface::class), $c->get(ApiResponse::class) ); }); }
public function boot(ContainerInterface $container): void { // Register event listeners, etc. }}Container Bootstrap
Section titled “Container Bootstrap”Bootstrap File
Section titled “Bootstrap File”<?php
declare(strict_types=1);
use Xoops\Core\Container\XoopsContainer;use Xoops\Core\Provider\CoreServiceProvider;
$container = new XoopsContainer();
// Register core services$coreProvider = new CoreServiceProvider();$coreProvider->register($container);
// Discover and register module providers$activeModules = $container->get('module_manager')->getActiveModules();
foreach ($activeModules as $module) { $providerClass = sprintf( 'Xoops\\Module\\%s\\ModuleServiceProvider', ucfirst($module->dirname) );
if (class_exists($providerClass)) { $provider = new $providerClass(); $provider->register($container); }}
// Boot all providers$coreProvider->boot($container);
foreach ($activeModules as $module) { $providerClass = sprintf( 'Xoops\\Module\\%s\\ModuleServiceProvider', ucfirst($module->dirname) );
if (class_exists($providerClass)) { $provider = new $providerClass(); $provider->boot($container); }}
return $container;Service Locator Bridge
Section titled “Service Locator Bridge”For gradual migration from legacy code:
<?php
declare(strict_types=1);
namespace Xoops\Core;
use Psr\Container\ContainerInterface;
/** * Static service locator for legacy code compatibility * * @deprecated Use dependency injection instead */class Xoops{ private static ?ContainerInterface $container = null;
/** * Set the container instance */ public static function setContainer(ContainerInterface $container): void { self::$container = $container; }
/** * Get the container instance */ public static function services(): ContainerInterface { if (self::$container === null) { self::$container = require XOOPS_ROOT_PATH . '/core/bootstrap_container.php'; }
return self::$container; }
/** * Get a service by ID * * @deprecated Use constructor injection instead */ public static function service(string $id): mixed { return self::services()->get($id); }
/** * Check if a service exists */ public static function hasService(string $id): bool { return self::services()->has($id); }}
// Legacy usage (deprecated but supported)$logger = \Xoops::service('logger');$db = \Xoops::service('database');Auto-Wiring
Section titled “Auto-Wiring”Auto-Wiring Container Extension
Section titled “Auto-Wiring Container Extension”<?php
declare(strict_types=1);
namespace Xoops\Core\Container;
use Psr\Container\ContainerInterface;use ReflectionClass;use ReflectionNamedType;
class AutoWiringContainer implements ContainerInterface{ public function __construct( private readonly XoopsContainer $container ) {}
public function get(string $id): mixed { // First, try the regular container if ($this->container->has($id)) { return $this->container->get($id); }
// If not found and it's a class, try auto-wiring if (class_exists($id)) { return $this->autowire($id); }
throw new ServiceNotFoundException("Service '$id' not found"); }
public function has(string $id): bool { return $this->container->has($id) || class_exists($id); }
/** * Auto-wire a class by resolving constructor dependencies */ private function autowire(string $className): object { $reflection = new ReflectionClass($className);
if (!$reflection->isInstantiable()) { throw new \RuntimeException("Class $className is not instantiable"); }
$constructor = $reflection->getConstructor();
if ($constructor === null) { return new $className(); }
$parameters = $constructor->getParameters(); $dependencies = [];
foreach ($parameters as $parameter) { $type = $parameter->getType();
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { if ($parameter->isDefaultValueAvailable()) { $dependencies[] = $parameter->getDefaultValue(); continue; } throw new \RuntimeException( "Cannot resolve parameter '{$parameter->getName()}' for $className" ); }
$dependencies[] = $this->get($type->getName()); }
return new $className(...$dependencies); }}Using the Container
Section titled “Using the Container”In Controllers
Section titled “In Controllers”<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Controller;
use Xoops\Core\View\ViewRendererInterface;use Xoops\Core\Http\ApiResponse;use Xoops\Module\Publisher\Service\ArticleService;
class ArticleController{ // Dependencies injected via constructor public function __construct( private readonly ArticleService $articleService, private readonly ViewRendererInterface $view, private readonly ApiResponse $response ) {}
// Controller methods use injected services public function list(ServerRequestInterface $request): ResponseInterface { $articles = $this->articleService->getPaginated(); return $this->response->html( $this->view->render('@modules/publisher/list', ['articles' => $articles]) ); }}In Services
Section titled “In Services”<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Service;
use Psr\Log\LoggerInterface;use Xoops\Module\Publisher\Repository\ArticleRepositoryInterface;use Psr\EventDispatcher\EventDispatcherInterface;
class ArticleService{ public function __construct( private readonly ArticleRepositoryInterface $repository, private readonly EventDispatcherInterface $eventDispatcher, private readonly LoggerInterface $logger ) {}
public function publish(int $articleId): void { $article = $this->repository->findById($articleId);
if ($article === null) { $this->logger->warning("Article not found: $articleId"); throw new ArticleNotFoundException(); }
$article->publish(); $this->repository->save($article);
$this->eventDispatcher->dispatch( new ArticlePublishedEvent($article->id) );
$this->logger->info("Article published: $articleId"); }}Using PHP-DI
Section titled “Using PHP-DI”For a more feature-rich container, XOOPS supports PHP-DI:
Installation
Section titled “Installation”composer require php-di/php-diConfiguration
Section titled “Configuration”<?php
use DI\ContainerBuilder;
$builder = new ContainerBuilder();
// Enable compilation for productionif (getenv('XOOPS_DEBUG') !== 'true') { $builder->enableCompilation(XOOPS_VAR_PATH . '/cache/container');}
// Add definitions$builder->addDefinitions([ // Using factories 'database' => function (ContainerInterface $c) { return new Connection($c->get('config')['database']); },
// Using DI\create() helper LoggerInterface::class => DI\create(Logger::class) ->constructor('xoops'),
// Auto-wiring by default ArticleController::class => DI\autowire(),
// Interface binding ArticleRepositoryInterface::class => DI\get(ArticleRepository::class),]);
return $builder->build();Testing with Containers
Section titled “Testing with Containers”<?php
use PHPUnit\Framework\TestCase;use Xoops\Core\Container\XoopsContainer;
class ContainerTest extends TestCase{ private XoopsContainer $container;
protected function setUp(): void { $this->container = new XoopsContainer(); }
public function testSetAndGet(): void { $service = new \stdClass(); $this->container->set('test', $service);
$this->assertTrue($this->container->has('test')); $this->assertSame($service, $this->container->get('test')); }
public function testFactoryExecution(): void { $this->container->set('counter', function () { static $count = 0; return ++$count; });
// Factory should only be called once (singleton) $this->assertEquals(1, $this->container->get('counter')); $this->assertEquals(1, $this->container->get('counter')); }
public function testAlias(): void { $this->container->set('original', new \stdClass()); $this->container->alias('aliased', 'original');
$this->assertSame( $this->container->get('original'), $this->container->get('aliased') ); }
public function testNotFoundThrowsException(): void { $this->expectException(ServiceNotFoundException::class); $this->container->get('nonexistent'); }}Best Practices
Section titled “Best Practices”1. Prefer Constructor Injection
Section titled “1. Prefer Constructor Injection”// Good: Constructor injectionclass ArticleService{ public function __construct( private readonly ArticleRepositoryInterface $repository ) {}}
// Avoid: Service locator in methodsclass ArticleService{ public function findAll(): array { // Don't do this $repo = Xoops::service('article_repository'); return $repo->findAll(); }}2. Depend on Interfaces
Section titled “2. Depend on Interfaces”// Good: Depend on interfacepublic function __construct( private readonly ArticleRepositoryInterface $repository) {}
// Avoid: Depend on concrete classpublic function __construct( private readonly ArticleRepository $repository) {}3. Keep Services Stateless
Section titled “3. Keep Services Stateless”// Good: Stateless serviceclass ArticleService{ public function findById(int $id): ?Article { return $this->repository->findById($id); }}
// Avoid: Stateful serviceclass ArticleService{ private ?Article $currentArticle = null;
public function setCurrentArticle(Article $article): void { $this->currentArticle = $article; }}See Also
Section titled “See Also”- PSR Standards Overview
- Architecture Vision
- PSR-15 Middleware
External Resources
Section titled “External Resources”#xoops-4.0 #psr-11 #container #dependency-injection #services