6.0.0-beta6
3/25/26
Last Modified 3/21/26 by Ralf Lang

Dependency Injection with Horde\Injector

Horde\Injector is a lightweight PSR-11 dependency injection container for managing object dependencies and wiring. It allows loose coupling between application components. You can inject dependencies into the constructor or setter methods and don't need to use global state or singletons. The injector supports autowiring via reflection, declarative configuration through PHP 8 attributes and explicit binding for complex scenarios. For legacy scenarios it supports using annotations rather than attributes.

Configuring the Injector

The injector resolves dependencies in order of precedence: 1) Explicit bindings override attributes. 2) Attributes override autowiring. Choose which approach matches your use case.

Autowiring

Autowiring means zero configuration. All necessary facts can be derived from type signatures in the constructor.

The injector automatically creates instances of classes with typed constructor parameters.
The code example is fictional - a class UserInterface? might exist in your code but not in Horde 6.

<?php

use Horde\Db\Adapter as DbAdapter;
use Psr\Logger\LoggerInterface;
class UserService
{
    public function __construct(
        private DbAdapter $db,
        private LoggerInterface $logger
    ) {}
}

$injector = new Horde\Injector\Injector(new Horde\Injector\TopLevel());
$service = $injector->get(UserService::class); // Dependencies auto-resolved
?>

Works when:

  • All constructor parameters are type-hinted
  • All dependencies are concrete classes or already bound
  • No special construction logic required

Factory Attributes (Recommended for Factories)

Use PHP 8 #[Factory] attributes to specify factory classes and methods. No manual binding calls are required:

<?php

use Horde\Injector\Attribute\Factory;

class UserFactory
{
    public function createUser(Horde\Injector\Injector $injector): User
    {
        return new User(
            $injector->get(Database::class),
            $injector->get('config.user.default_role')
        );
    }
}

#[Factory(factory: UserFactory::class, method: 'createUser')]
class User
{
    public function __construct(
        private Database $db,
        private string $defaultRole
    ) {}
}

// No binding needed - factory auto-discovered

$user = $injector->get(User::class);
?>

Use attributes when:

  • Factory logic is complex or needs configuration
  • You want self-documenting, type-safe factory registration
  • Refactoring tools should track factory relationships

Setter Injection Attributes (Legacy)

Mark optional dependencies for setter injection using @inject doc blocks (H5 compatibility) or #[Inject] attributes (H6):

<?php

use Horde\Injector\Attribute\Inject;

class Service
{
    private ?CacheInterface $cache = null;

    #[Inject]
    public function setCache(CacheInterface $cache): void
    {
        $this->cache = $cache;
    }
}

$injector->bindImplementation(Service::class, Service::class);
?>

Note: Setter injection happens after constructor. This means injected properties are null during __construct().
This is a pattern for retrofitting code which cannot fix constructors for BC reasons. It's generally something to avoid otherwise.

Explicit Binding (Full Control)

For complex scenarios you can explicitly bind interfaces to implementations, factories, or closures:

Factory binding:
<?php

use Horde\Cache\Storage as CacheStorage;

$injector->bindFactory(CacheStorage::class', 'CacheFactory', 'create');

class CacheFactory
{
    public function create(Horde\Injector\Injector $injector)
    {
        $conf = $injector->getInstance('Horde_Registry')->get('cache');
        return new Horde\Cache\Storage\File($conf['dir']);
    }
}
?>
Implementation binding:
<?php

$injector->bindImplementation('Psr\Log\LoggerInterface', 'Horde\Log\Logger');
?>
Closure binding:
<?php

$injector->bindClosure('DatabaseConfig', function($inj) {
    return new DatabaseConfig($inj->getInstance('Horde_Registry')->get('sql'));
});
?>

Explicit bindings always override attributes and autowiring.

Instance Registration (Rare)

Only use setInstance() for pre-constructed objects or the injector itself:

<?php

$injector->setInstance('Horde\Injector\Injector', $injector);
$injector->setInstance('legacy_db', $existingDbConnection);
?>

Warning: Avoid setInstance() unless you really need it objects. Use factories instead to maintain lazy instantiation.

Using the Injector

Getting Instances

Retrieve objects from the container. Instances are cached (singleton per injector scope):

<?php

// PSR-11 interface (preferred in H6)
$service = $injector->get('ServiceInterface');

// H5 compatibility
$service = $injector->getInstance('ServiceInterface');
?>

Creating New Instances

Force creation of a new instance (not cached):

<?php

$newService = $injector->createInstance('ServiceClass');
?>

Dependencies may still be reused from the instance cache.

Child Injectors (Scoping)

Create isolated scopes for modules or request handling without polluting global scope:

<?php

$appInjector->bindFactory('Logger', 'LoggerFactory', 'create');

$moduleInjector = $appInjector->createChildInjector();
$moduleInjector->bindImplementation('Logger', 'ModuleSpecificLogger');

// Child sees parent bindings
$x = $moduleInjector->get('Logger'); // Uses ModuleSpecificLogger

// Parent doesn't see child bindings
$y = $appInjector->get('Logger'); // Uses LoggerFactory
?>

Use child injectors for:

  • Per-module configuration
  • Request-scoped objects
  • Test isolation

Checking Availability

Test if the injector can provide an interface:

<?php

if ($injector->has('OptionalService')) {
    $service = $injector->get('OptionalService');
}
?>

has() returns true if:

  • An instance exists
  • A binding is registered
  • The class is autowireable (concrete with satisfiable dependencies)

Migration from H5

H5 (Horde_Injector):
<?php

$injector = new Horde_Injector(new Horde_Injector_TopLevel());
$injector->bindFactory('User', 'UserFactory', 'create');
$user = $injector->getInstance('User');
?>
H6 (Horde\Injector with attributes):
<?php

use Horde\Injector\Attribute\Factory;

#[Factory(factory: UserFactory::class, method: 'create')]
class User { }

$injector = new Horde\Injector\Injector(new Horde\Injector\TopLevel());
$user = $injector->get(User::class); // PSR-11, factory auto-discovered
?>

Key changes:

  • getInstance() ? get() (PSR-11 compliance)
  • Manual bindFactory() ? #[Factory] attribute (optional)
  • Horde_Injector ? Horde\Injector\Injector (namespaced)

Best Practices

  • Prefer autowiring - Let type hints drive dependency resolution
  • Use attributes for factories - Self-documenting, refactoring-safe
  • Avoid setInstance() for business objects - Breaks lazy loading
  • Use child injectors for scopes - Module/request isolation
  • Constructor injection over setter injection - Dependencies available immediately
  • Type-hint everything - Enables autowiring and IDE support