Skip to main content
LibreDTE Core uses Symfony’s Dependency Injection Container to manage services, promoting loose coupling and testability. This guide covers how to leverage the DI container effectively.

Container Basics

The dependency injection container is built during application bootstrap and manages all services throughout the application lifecycle.

Accessing the Container

use libredte\lib\Core\Application;

$app = new Application('dev', true);
$container = $app->getContainer();

// Get a service
$document = $container->get(DocumentComponentInterface::class);
In most cases, you won’t access the container directly. Instead, use constructor injection in your own services.

Service Configuration

All services are defined in config/services.yaml:
services.yaml
parameters:
    libredte.lib.core.project_dir: '%kernel.project_dir%'

services:
    _defaults:
        autowire: true      # Automatic dependency injection
        autoconfigure: true # Automatic tag registration
        public: false       # Services are private by default

    # Package Registry (public service)
    Derafu\Backbone\Contract\PackageRegistryInterface:
        class: libredte\lib\Core\PackageRegistry
        public: true
        arguments:
            $packages:
                billing: '@libredte\lib\Core\Package\Billing\BillingPackage'

    # Billing Package
    libredte\lib\Core\Package\Billing\BillingPackage: ~

    # Document Component
    libredte\lib\Core\Package\Billing\Component\Document\Contract\DocumentComponentInterface:
        class: libredte\lib\Core\Package\Billing\Component\Document\DocumentComponent

Autowiring

Autowiring automatically resolves dependencies based on type hints:
namespace App\Service;

use libredte\lib\Core\Package\Billing\Component\Document\Contract\DocumentComponentInterface;

class InvoiceService
{
    public function __construct(
        private DocumentComponentInterface $document
    ) {}

    public function createInvoice(array $data): void
    {
        $bag = $this->document->bill($data);
        // ...
    }
}
The container automatically injects DocumentComponent based on the type hint.

Interface Binding

Services are bound to their interfaces for flexibility:
# Bind interface to implementation
libredte\lib\Core\Package\Billing\Component\Exchange\Contract\ExchangeComponentInterface:
    class: libredte\lib\Core\Package\Billing\Component\Exchange\ExchangeComponent

libredte\lib\Core\Package\Billing\Component\Identifier\Contract\IdentifierComponentInterface:
    class: libredte\lib\Core\Package\Billing\Component\Identifier\IdentifierComponent
Benefits:
  • Easier testing with mock implementations
  • Ability to swap implementations without changing code
  • Clear contracts between components
  • Better IDE support and type safety
Example:
// Your code depends on the interface
public function __construct(
    private DocumentComponentInterface $document
) {}

// The container provides the concrete implementation
// You can swap implementations without touching this code

Tagged Services

Tagged services enable the Strategy pattern for document-specific logic:
# Define a worker that uses strategies
libredte\lib\Core\Package\Billing\Component\Document\Contract\BuilderWorkerInterface:
    class: libredte\lib\Core\Package\Billing\Component\Document\Worker\BuilderWorker
    arguments:
        $strategies: !tagged_iterator 
            tag: 'billing.document.builder#strategy'
            index_by: 'name'

How Tagged Services Work

1

Tag strategies in configuration

Each strategy is automatically tagged based on its interface and attributes.
2

Collect tagged services

The !tagged_iterator directive collects all services with the specified tag.
3

Inject as collection

The worker receives an indexed array of all strategy implementations.
4

Select strategy at runtime

The worker selects the appropriate strategy based on document type.

Strategy Examples

libredte\lib\Core\Package\Billing\Component\Document\Contract\Builder\Strategy\FacturaAfectaBuilderStrategyInterface:
    class: libredte\lib\Core\Package\Billing\Component\Document\Worker\Builder\Strategy\FacturaAfectaBuilderStrategy

libredte\lib\Core\Package\Billing\Component\Document\Contract\Builder\Strategy\BoletaAfectaBuilderStrategyInterface:
    class: libredte\lib\Core\Package\Billing\Component\Document\Worker\Builder\Strategy\BoletaAfectaBuilderStrategy

Lazy Loading

Some services use lazy loading to avoid circular dependencies:
libredte\lib\Core\Package\Billing\Component\Document\Contract\DocumentBagManagerWorkerInterface:
    class: libredte\lib\Core\Package\Billing\Component\Document\Worker\DocumentBagManagerWorker
    lazy: true  # Necessary for circular reference with BuilderWorker
Lazy services are created as proxies and instantiated only when first accessed. This adds minimal overhead but resolves complex dependency graphs.

Service Retrieval Methods

Compiler Passes

Custom compiler passes enhance the container during compilation:

ServiceProcessingCompilerPass

Processes services with the libredte.lib. prefix:
Application.php
$container->addCompilerPass(
    new ServiceProcessingCompilerPass('libredte.lib.')
);
This enables custom service manipulation before the container is frozen.

ServiceConfigurationCompilerPass

Handles configuration merging and validation:
Application.php
$container->addCompilerPass(
    new ServiceConfigurationCompilerPass('libredte.lib.')
);
Compiler passes run during container compilation (before services are instantiated) and can:
  • Modify service definitions
  • Register tagged services
  • Validate configuration
  • Add service aliases
  • Optimize service graphs
They’re executed in the build phase, not at runtime, so they don’t impact performance.

Extending with Custom Services

You can add your own services to the container:
1

Create your service class

namespace App\Service;

use libredte\lib\Core\Package\Billing\Component\Document\Contract\DocumentComponentInterface;

class CustomInvoiceHandler
{
    public function __construct(
        private DocumentComponentInterface $document
    ) {}

    public function process(array $data): void
    {
        // Your custom logic
    }
}
2

Register in your application's services.yaml

services:
    App\Service\CustomInvoiceHandler:
        autowire: true
3

Use via dependency injection

class MyController
{
    public function __construct(
        private CustomInvoiceHandler $handler
    ) {}
}

Environment-Specific Configuration

The container supports environment-based configuration:
// Development environment with debugging
$app = new Application('dev', true);

// Production environment optimized
$app = new Application('prod', false);
// Enable debugging, no caching
$app = new Application(
    environment: 'dev',
    debug: true
);

Best Practices

Use Constructor Injection

Always inject dependencies via constructor rather than accessing the container directly

Depend on Interfaces

Type-hint interfaces, not concrete classes, for maximum flexibility

Keep Services Stateless

Services should not maintain state between calls

Use Tagged Services for Strategies

Leverage tagged services for the Strategy pattern
Common Pitfalls:
  • Accessing the container directly in application code (service location anti-pattern)
  • Creating services manually with new instead of using DI
  • Making services stateful
  • Circular dependencies without lazy loading

Next Steps

Architecture

Review the overall architecture

Packages & Components

Explore available packages and components

Build docs developers (and LLMs) love