Skip to content

📦 PSR-11 Dependency Injection Guide

Master dependency injection and service containers in XOOPS 2026.

Dependency Injection (DI) is a design pattern that removes hard-coded dependencies, making your code more modular, testable, and maintainable. XOOPS 2026 uses PSR-11 compliant containers.


Understanding Dependency Injection

The Problem: Tight Coupling

<?php
// ❌ Bad: Hard-coded dependencies
class ArticleService
{
    public function getArticles(): array
    {
        // Tightly coupled to specific implementation
        $db = new MySQLDatabase();
        $cache = new RedisCache();
        $logger = new FileLogger();

        // Hard to test, hard to change
        return $db->query("SELECT * FROM articles");
    }
}

The Solution: Dependency Injection

<?php
// ✅ Good: Dependencies injected
class ArticleService
{
    public function __construct(
        private readonly DatabaseInterface $db,
        private readonly CacheInterface $cache,
        private readonly LoggerInterface $logger,
    ) {}

    public function getArticles(): array
    {
        return $this->db->query("SELECT * FROM articles");
    }
}

Visualization

flowchart TB
    subgraph Without["❌ Without DI"]
        direction TB
        S1[Service] --> D1[Database]
        S1 --> C1[Cache]
        S1 --> L1[Logger]
        note1[Hard-coded<br/>dependencies]
    end

    subgraph With["✅ With DI"]
        direction TB
        CONT[Container] --> S2[Service]
        CONT --> D2[Database]
        CONT --> C2[Cache]
        CONT --> L2[Logger]
        S2 -.->|injected| D2
        S2 -.->|injected| C2
        S2 -.->|injected| L2
    end

PSR-11 Container Interface

The Standard

<?php

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 found.
     * @throws ContainerExceptionInterface Error while retrieving.
     */
    public function get(string $id): mixed;

    /**
     * Returns true if the container can return an entry for the given identifier.
     *
     * @param string $id Identifier of the entry to look for.
     * @return bool
     */
    public function has(string $id): bool;
}

XOOPS Container Features

flowchart LR
    subgraph Container["XOOPS Container"]
        AUTO[Auto-wiring]
        BIND[Interface Binding]
        FACT[Factories]
        LIFE[Lifecycle Management]
        TAG[Service Tagging]
    end

    AUTO --> |"Resolves dependencies<br/>automatically"| SVC[Services]
    BIND --> |"Maps interfaces<br/>to implementations"| SVC
    FACT --> |"Custom creation<br/>logic"| SVC
    LIFE --> |"Singleton/Transient"| SVC
    TAG --> |"Group related<br/>services"| SVC

Container Configuration

Basic Service Registration

<?php
// config/services.php

declare(strict_types=1);

use Psr\Container\ContainerInterface;
use Xoops\Container\ContainerBuilder;

return static function (ContainerBuilder $container): void {
    // Simple binding: interface to implementation
    $container->bind(
        LoggerInterface::class,
        FileLogger::class
    );

    // With constructor arguments
    $container->bind(CacheInterface::class, RedisCache::class)
        ->constructor('localhost', 6379);

    // Singleton (shared instance)
    $container->singleton(
        DatabaseInterface::class,
        MySQLDatabase::class
    );

    // Factory function for complex creation
    $container->bind(ArticleRepository::class)
        ->factory(function (ContainerInterface $c) {
            return new ArticleRepository(
                $c->get(DatabaseInterface::class),
                $c->get(CacheInterface::class),
                $c->get('config.cache_ttl')
            );
        });
};

Auto-Wiring

The container automatically resolves dependencies based on type hints:

<?php

class ArticleController
{
    // Container automatically injects these
    public function __construct(
        private readonly ArticleService $articleService,
        private readonly LoggerInterface $logger,
    ) {}
}

// Container resolves:
// 1. ArticleService needs ArticleRepository, CacheInterface
// 2. ArticleRepository needs DatabaseInterface
// 3. All dependencies resolved recursively
flowchart TB
    subgraph Resolution["Auto-Wiring Resolution"]
        REQ[Request ArticleController]
        REQ --> AS[Resolve ArticleService]
        AS --> AR[Resolve ArticleRepository]
        AR --> DB[Resolve Database]
        AR --> CACHE[Resolve Cache]
        AS --> LOG[Resolve Logger]
    end

    DB --> INST[Create Instances]
    CACHE --> INST
    LOG --> INST
    INST --> DONE[ArticleController Ready]

Service Definitions

Interface Binding

<?php

// Bind interface to concrete implementation
$container->bind(
    ArticleRepositoryInterface::class,
    XoopsArticleRepository::class
);

// Now any class requesting ArticleRepositoryInterface
// will receive XoopsArticleRepository

Contextual Binding

Different implementations for different consumers:

<?php

// WebArticleController gets HTMLFormatter
$container->when(WebArticleController::class)
    ->needs(FormatterInterface::class)
    ->give(HTMLFormatter::class);

// ApiArticleController gets JSONFormatter
$container->when(ApiArticleController::class)
    ->needs(FormatterInterface::class)
    ->give(JSONFormatter::class);
flowchart LR
    subgraph Contextual["Contextual Binding"]
        WEB[WebController] --> HTML[HTMLFormatter]
        API[ApiController] --> JSON[JSONFormatter]
    end

    FMT[FormatterInterface] -.-> HTML
    FMT -.-> JSON

Lifecycle Management

<?php

// Singleton: Same instance every time
$container->singleton(DatabaseInterface::class, MySQLDatabase::class);

// Transient: New instance every time (default)
$container->bind(RequestValidator::class);

// Scoped: Same instance within a request
$container->scoped(UserContext::class);
flowchart TB
    subgraph Lifecycles["Service Lifecycles"]
        direction LR
        subgraph Singleton["Singleton"]
            S1[Request 1] --> SI[Instance A]
            S2[Request 2] --> SI
            S3[Request 3] --> SI
        end

        subgraph Transient["Transient"]
            T1[Request 1] --> TI1[Instance A]
            T2[Request 2] --> TI2[Instance B]
            T3[Request 3] --> TI3[Instance C]
        end

        subgraph Scoped["Scoped"]
            SC1[Request 1] --> SCI1[Instance A]
            SC1B[Request 1b] --> SCI1
            SC2[Request 2] --> SCI2[Instance B]
        end
    end

Advanced Patterns

Service Providers

Organize related services into providers:

<?php

declare(strict_types=1);

namespace MyModule\Provider;

use Xoops\Container\ServiceProvider;
use Xoops\Container\ContainerBuilder;

final class ArticleServiceProvider extends ServiceProvider
{
    public function register(ContainerBuilder $container): void
    {
        // Repository
        $container->bind(
            ArticleRepositoryInterface::class,
            XoopsArticleRepository::class
        );

        // Services
        $container->bind(ArticleService::class);
        $container->bind(ArticleSearchService::class);

        // Commands
        $container->bind(CreateArticleHandler::class);
        $container->bind(UpdateArticleHandler::class);
        $container->bind(DeleteArticleHandler::class);
    }

    public function boot(ContainerInterface $container): void
    {
        // Run after all providers are registered
        // Good for subscribing to events, etc.
    }
}

Service Tagging

Group related services for bulk operations:

<?php

// Tag multiple services
$container->bind(ArticleCreatedListener::class)
    ->tag('event.listener', ['event' => 'article.created']);

$container->bind(ArticleUpdatedListener::class)
    ->tag('event.listener', ['event' => 'article.updated']);

$container->bind(CacheInvalidator::class)
    ->tag('event.listener', ['event' => 'article.*']);

// Retrieve all tagged services
$listeners = $container->tagged('event.listener');
foreach ($listeners as $listener) {
    $dispatcher->addListener($listener);
}
flowchart TB
    subgraph Tags["Service Tagging"]
        TAG[event.listener tag]
        TAG --> L1[ArticleCreatedListener]
        TAG --> L2[ArticleUpdatedListener]
        TAG --> L3[CacheInvalidator]
        TAG --> L4[SearchIndexer]
    end

    DISP[Event Dispatcher] --> |"Get all tagged"| TAG

Decorators

Wrap services with additional functionality:

<?php

// Original service
$container->bind(ArticleRepositoryInterface::class, XoopsArticleRepository::class);

// Decorate with caching
$container->decorate(
    ArticleRepositoryInterface::class,
    CachedArticleRepository::class
);

// Decorate with logging
$container->decorate(
    ArticleRepositoryInterface::class,
    LoggingArticleRepository::class
);

// Resolution order: Logging → Caching → Original
flowchart LR
    REQ[Request] --> LOG[LoggingRepository]
    LOG --> CACHE[CachedRepository]
    CACHE --> ORIG[XoopsRepository]
    ORIG --> DB[(Database)]

Module Service Configuration

Module services.php

<?php
// modules/mymodule/config/services.php

declare(strict_types=1);

use Xoops\Container\ContainerBuilder;

return static function (ContainerBuilder $container): void {
    // Module-specific services
    $container->bind(
        \MyModule\Repository\ArticleRepositoryInterface::class,
        \MyModule\Infrastructure\XoopsArticleRepository::class
    );

    // Command handlers
    $container->bind(\MyModule\Application\CreateArticleHandler::class);
    $container->bind(\MyModule\Application\UpdateArticleHandler::class);

    // Controllers (auto-wired by default)
    $container->bind(\MyModule\Controller\ArticleController::class);
    $container->bind(\MyModule\Controller\Admin\ArticleAdminController::class);

    // Use XOOPS core services
    $container->alias('db', \Xoops\Database\DatabaseInterface::class);
    $container->alias('cache', \Psr\SimpleCache\CacheInterface::class);
};

Accessing the Container

<?php

// In a controller (injected automatically)
class ArticleController
{
    public function __construct(
        private readonly ArticleService $service,
    ) {}
}

// Manual access (avoid when possible)
$container = \Xoops\Core\Kernel::getContainer();
$service = $container->get(ArticleService::class);

// In legacy code (bridge)
$service = xoops_getService(ArticleService::class);

Configuration Values

Binding Configuration

<?php

// Bind scalar values
$container->bind('config.cache_ttl', 3600);
$container->bind('config.items_per_page', 10);
$container->bind('config.upload_path', XOOPS_UPLOAD_PATH . '/articles');

// Bind arrays
$container->bind('config.allowed_extensions', ['jpg', 'png', 'gif', 'webp']);

// Use in services
class ImageUploader
{
    public function __construct(
        #[Inject('config.upload_path')]
        private readonly string $uploadPath,

        #[Inject('config.allowed_extensions')]
        private readonly array $allowedExtensions,
    ) {}
}

Environment-Based Configuration

<?php

$container->bind('config.debug', fn() => getenv('APP_DEBUG') === 'true');

$container->bind(CacheInterface::class)
    ->factory(function (ContainerInterface $c) {
        if ($c->get('config.debug')) {
            return new ArrayCache(); // In-memory for development
        }
        return new RedisCache();     // Redis for production
    });

Testing with DI

Mocking Dependencies

<?php

declare(strict_types=1);

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

final class ArticleServiceTest extends TestCase
{
    public function testGetPublishedArticles(): void
    {
        // Create mock
        $repository = $this->createMock(ArticleRepositoryInterface::class);
        $repository->method('findPublished')
            ->willReturn([
                new Article(1, 'Title 1'),
                new Article(2, 'Title 2'),
            ]);

        $cache = $this->createMock(CacheInterface::class);
        $logger = $this->createMock(LoggerInterface::class);

        // Inject mocks
        $service = new ArticleService($repository, $cache, $logger);

        // Test
        $articles = $service->getPublishedArticles();

        $this->assertCount(2, $articles);
    }
}

Test Container

<?php

declare(strict_types=1);

namespace Tests;

use Xoops\Container\TestContainer;

final class ArticleIntegrationTest extends TestCase
{
    private TestContainer $container;

    protected function setUp(): void
    {
        $this->container = new TestContainer();

        // Override specific services for testing
        $this->container->bind(
            CacheInterface::class,
            ArrayCache::class // Use in-memory cache
        );

        $this->container->bind(
            DatabaseInterface::class,
            SQLiteDatabase::class // Use SQLite for tests
        );
    }

    public function testArticleCreation(): void
    {
        $handler = $this->container->get(CreateArticleHandler::class);

        $result = $handler(new CreateArticle(
            title: 'Test Article',
            content: 'Test content',
            authorId: 1,
        ));

        $this->assertInstanceOf(ArticleId::class, $result);
    }
}

Best Practices

Do's ✅

<?php

// ✅ Use constructor injection
class ArticleService
{
    public function __construct(
        private readonly ArticleRepositoryInterface $repository,
    ) {}
}

// ✅ Depend on abstractions
public function __construct(
    private readonly LoggerInterface $logger, // Interface
) {}

// ✅ Use readonly properties
public function __construct(
    private readonly CacheInterface $cache,
) {}

// ✅ Keep constructors simple
public function __construct(
    private readonly Database $db,
    private readonly Cache $cache,
) {} // Just assignment, no logic

Don'ts ❌

<?php

// ❌ Don't use service locator pattern
class BadService
{
    public function doSomething(): void
    {
        $db = Container::get(Database::class); // Hidden dependency
    }
}

// ❌ Don't inject the container itself
class BadController
{
    public function __construct(
        private readonly ContainerInterface $container, // Anti-pattern
    ) {}
}

// ❌ Don't do work in constructor
class BadService
{
    public function __construct(Database $db)
    {
        $this->data = $db->query("SELECT * FROM config"); // Side effect
    }
}

// ❌ Don't have too many dependencies
class BadService
{
    public function __construct(
        $a, $b, $c, $d, $e, $f, $g, $h, $i, $j // Too many!
    ) {} // Consider refactoring
}

Debugging

Container Dump

# Dump all registered services
php xoops_cli.php container:debug

# Filter by pattern
php xoops_cli.php container:debug --filter=Article

# Show service details
php xoops_cli.php container:debug ArticleService --verbose

Visualization

flowchart TB
    subgraph Debug["Container Debug Output"]
        direction TB
        SVC[Service: ArticleService]
        SVC --> TYPE[Type: Singleton]
        SVC --> DEPS[Dependencies:]
        DEPS --> D1[ArticleRepository]
        DEPS --> D2[CacheInterface]
        DEPS --> D3[LoggerInterface]
        SVC --> TAGS[Tags: service.article]
    end


psr-11 #dependency-injection #container #services #xoops-2026