Dependency Auto-Injection
In webman, dependency auto-injection is an optional feature and is disabled by default. If you need dependency auto-injection, it is recommended to use php-di. The following describes how to use php-di with webman.
Installation
composer require php-di/php-di:^7.0
Modify the configuration config/container.php. The final content should be as follows:
$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAttributes(true);
return $builder->build();
The
config/container.phpfile must ultimately return a container instance that complies with thePSR-11specification. If you do not wish to usephp-di, you can create and return another PSR-11 compliant container instance here. The default configuration only provides basic webman container functionality.
Constructor Injection
Create a new file app/service/Mailer.php (create the directory if it does not exist) with the following content:
<?php
namespace app\service;
class Mailer
{
public function mail($email, $content)
{
// Send mail code omitted
}
}
The content of app/controller/UserController.php is as follows:
<?php
namespace app\controller;
use support\Request;
use app\service\Mailer;
class UserController
{
public function __construct(private Mailer $mailer)
{
}
public function register(Request $request)
{
$this->mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
Normally, the following code would be needed to instantiate app\controller\UserController:
$mailer = new Mailer;
$user = new UserController($mailer);
When using php-di, developers do not need to manually instantiate Mailer in the controller — webman will do it automatically. If there are other class dependencies during the instantiation of Mailer, webman will also automatically instantiate and inject them. No initialization work is required from the developer.
Note
Only instances created by the framework orphp-disupport dependency auto-injection. Instances created manually withnewcannot use dependency auto-injection. If you need injection, use thesupport\Containerinterface instead ofnew, for example:
use app\service\UserService;
use app\service\LogService;
use support\Container;
// Instances created with new cannot use dependency injection
$user_service = new UserService;
// Instances created with new cannot use dependency injection
$log_service = new LogService($path, $name);
// Instances created with Container can use dependency injection
$user_service = Container::get(UserService::class);
// Instances created with Container can use dependency injection
$log_service = Container::make(LogService::class, [$path, $name]);
Attribute Injection
In addition to constructor dependency auto-injection, you can also use attribute injection. Continuing with the previous example, modify app\controller\UserController as follows:
<?php
namespace app\controller;
use support\Request;
use app\service\Mailer;
use DI\Attribute\Inject;
class UserController
{
#[Inject]
private Mailer $mailer;
public function register(Request $request)
{
$this->mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
This example uses the #[Inject] attribute for injection and automatically injects the instance into the member variable based on the object type. The effect is the same as constructor injection, but the code is more concise.
Note
Webman does not support controller parameter injection before version 1.4.6. For example, the following code is not supported when webman<=1.4.6:
<?php
namespace app\controller;
use support\Request;
use app\service\Mailer;
class UserController
{
// Controller parameter injection is not supported before version 1.4.6
public function register(Request $request, Mailer $mailer)
{
$mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
Custom Constructor Injection
Sometimes the parameters passed to the constructor may not be class instances but strings, numbers, arrays, or other non-object data. For example, the Mailer constructor may need to receive the SMTP server IP and port:
<?php
namespace app\service;
class Mailer
{
private $smtpHost;
private $smtpPort;
public function __construct($smtp_host, $smtp_port)
{
$this->smtpHost = $smtp_host;
$this->smtpPort = $smtp_port;
}
public function mail($email, $content)
{
// Send mail code omitted
}
}
This case cannot use constructor auto-injection directly, because php-di cannot determine the values of $smtp_host and $smtp_port. In such cases, custom injection can be used.
Add the following code to config/dependence.php (create the file if it does not exist):
return [
// ... Other configurations omitted
app\service\Mailer::class => new app\service\Mailer('192.168.1.11', 25);
];
When dependency injection needs to obtain an instance of app\service\Mailer, it will automatically use the app\service\Mailer instance created in this configuration.
Note that config/dependence.php uses new to instantiate the Mailer class. This is fine for this example, but if the Mailer class depends on other classes or uses attribute injection internally, initialization with new will not perform dependency auto-injection. The solution is to use custom interface injection and initialize classes via Container::get(class name) or Container::make(class name, [constructor parameters]).
Custom Interface Injection
In real projects, it is preferable to program against interfaces rather than concrete classes. For example, app\controller\UserController should depend on app\service\MailerInterface instead of app\service\Mailer.
Define the MailerInterface interface:
<?php
namespace app\service;
interface MailerInterface
{
public function mail($email, $content);
}
Define the implementation of MailerInterface:
<?php
namespace app\service;
class Mailer implements MailerInterface
{
private $smtpHost;
private $smtpPort;
public function __construct($smtp_host, $smtp_port)
{
$this->smtpHost = $smtp_host;
$this->smtpPort = $smtp_port;
}
public function mail($email, $content)
{
// Send mail code omitted
}
}
Use MailerInterface instead of the concrete implementation:
<?php
namespace app\controller;
use support\Request;
use app\service\MailerInterface;
use DI\Attribute\Inject;
class UserController
{
#[Inject]
private MailerInterface $mailer;
public function register(Request $request)
{
$this->mailer->mail('hello@webman.com', 'Hello and welcome!');
return response('ok');
}
}
Define the implementation of MailerInterface in config/dependence.php:
use Psr\Container\ContainerInterface;
return [
app\service\MailerInterface::class => function(ContainerInterface $container) {
return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
}
];
When the application needs to use the MailerInterface interface, it will automatically use the Mailer implementation.
The benefit of programming against interfaces is that when you need to replace a component, you do not need to change business code — only the concrete implementation in
config/dependence.php. This is also very useful for unit testing.
Other Custom Injection
Besides defining class dependencies, config/dependence.php can also define other values such as strings, numbers, arrays, etc.
For example, if config/dependence.php is defined as follows:
return [
'smtp_host' => '192.168.1.11',
'smtp_port' => 25
];
You can inject smtp_host and smtp_port into class properties using #[Inject]:
<?php
namespace app\service;
use DI\Attribute\Inject;
class Mailer
{
#[Inject("smtp_host")]
private $smtpHost;
#[Inject("smtp_port")]
private $smtpPort;
public function mail($email, $content)
{
// Send mail code omitted
echo "{$this->smtpHost}:{$this->smtpPort}\n"; // Will output 192.168.1.11:25
}
}
Lazy Loading
Lazy loading is a design pattern that defers the creation or initialization of objects until they are actually needed.
This feature requires an additional dependency. The following package is a fork of ocramius/proxy-manager; the original repository does not support PHP 8.
composer require friendsofphp/proxy-manager-lts
Usage:
<?php
use DI\Attribute\Injectable;
use DI\Attribute\Inject;
#[Injectable(lazy: true)]
class MyClass
{
private string $name;
public function __construct()
{
echo "MyClass instantiated\n";
$this->name = "Lazy Loaded Object";
}
public function getName(): string
{
return $this->name;
}
}
class Controller
{
#[Inject]
public MyClass $myClass;
public function getClass()
{
echo "Proxy class name: " . get_class($this->myClass) . "\n";
echo "name: " . $this->myClass->getName();
}
}
Output:
Proxy class name: ProxyManagerGeneratedProxy\__PM__\app\web\MyClass\Generated98d2817da63e3c088c808a0d4f6e9ae0
MyClass instantiated
name: Lazy Loaded Object
This example shows that when a class declared with the #[Injectable] attribute is injected, a proxy class is first created. The actual class is only instantiated when any of its methods are called.
Circular Dependencies
Circular dependencies occur when multiple classes depend on each other, forming a closed dependency loop.
-
Direct circular dependency
- Module A depends on module B, and module B depends on module A
- Forms a cycle: A → B → A
-
Indirect circular dependency
- Involves multiple modules in a dependency cycle
- For example: A → B → C → A
When using attribute injection, php-di will automatically detect circular dependencies and throw an exception. If needed, use the following approach instead:
class userController
{
// Remove this code
// #[Inject]
// private UserService userService;
public function getUserName()
{
$userService = Container::get(UserService::class);
return $userService->getName();
}
}
More Information
Please refer to the php-di documentation.