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:
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:
Automatic
Manual Configuration
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. services :
App\Service\InvoiceService :
arguments :
$document : '@libredte\lib\Core\Package\Billing\Component\Document\DocumentComponent'
You can also manually configure dependencies when needed.
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
Tag strategies in configuration
Each strategy is automatically tagged based on its interface and attributes.
Collect tagged services
The !tagged_iterator directive collects all services with the specified tag.
Inject as collection
The worker receives an indexed array of all strategy implementations.
Select strategy at runtime
The worker selects the appropriate strategy based on document type.
Strategy Examples
Builder Strategies
Parser Strategies
Sender Handlers
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
class MyService
{
public function __construct (
private DocumentComponentInterface $document ,
private ExchangeComponentInterface $exchange
) {}
}
Best practice: Let the container inject dependencies automatically. $document = $app -> getService ( DocumentComponentInterface :: class );
Useful for one-off service access. $billing = $app -> getPackageRegistry () -> getBillingPackage ();
$document = $billing -> getDocumentComponent ();
Provides organized access to components. $container = $app -> getContainer ();
$document = $container -> get ( DocumentComponentInterface :: class );
Avoid direct container access when possible.
Compiler Passes
Custom compiler passes enhance the container during compilation:
ServiceProcessingCompilerPass
Processes services with the libredte.lib. prefix:
$container -> addCompilerPass (
new ServiceProcessingCompilerPass ( 'libredte.lib.' )
);
This enables custom service manipulation before the container is frozen.
ServiceConfigurationCompilerPass
Handles configuration merging and validation:
$container -> addCompilerPass (
new ServiceConfigurationCompilerPass ( 'libredte.lib.' )
);
What are Compiler Passes?
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:
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
}
}
Register in your application's services.yaml
services :
App\Service\CustomInvoiceHandler :
autowire : true
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 );
Development
Production
Testing
// 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