217 lines
6.4 KiB
Markdown
217 lines
6.4 KiB
Markdown
|
# Dependency Injection
|
||
|
|
||
|
We are **deprecating** the current service registry (`oat\oatbox\service\ServiceManager`) in favor of a
|
||
|
faster and more modern [PSR-11](https://www.php-fig.org/psr/psr-11/) compliant solution based on
|
||
|
[Symfony Dependency Injection documentation](https://symfony.com/doc/current/components/dependency_injection.html).
|
||
|
|
||
|
- For now on, all the new services MUST be created as explained bellow.
|
||
|
- Whenever is possible, old services SHOULD be migrated to the new container.
|
||
|
|
||
|
## How to add new services to container?
|
||
|
|
||
|
1) In _any_ tao extension, create a `Container Service Provider`. Example:
|
||
|
|
||
|
```php
|
||
|
<?php
|
||
|
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||
|
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
|
||
|
use function Symfony\Component\DependencyInjection\Loader\Configurator\env;
|
||
|
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
|
||
|
|
||
|
class MyContainerServiceProvider implements ContainerServiceProviderInterface
|
||
|
{
|
||
|
public function __invoke(ContainerConfigurator $configurator): void
|
||
|
{
|
||
|
$services = $configurator->services();
|
||
|
$parameters = $configurator->parameters();
|
||
|
|
||
|
$parameters->set('someParam', 'someValue');
|
||
|
|
||
|
$services->set(MyService::class, MyService::class)
|
||
|
->public()
|
||
|
->args(
|
||
|
[
|
||
|
service(MyOtherService::class),
|
||
|
service(MyLegacyService::SERVICE_ID),
|
||
|
env('MY_ENV_VAR'),
|
||
|
param('someParam'),
|
||
|
]
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
**RECOMMENDATION:** Avoid inflating the `ContainerServiceProvider` with too many services/params/etc.
|
||
|
Be wise and use common sense to group your services within different `Container Service Providers` classes.
|
||
|
|
||
|
For more information read the [Symfony Dependency Injection documentation](https://symfony.com/doc/current/components/dependency_injection.html).
|
||
|
|
||
|
2) Add the new `Container Service Provider` to the `manifest.php` file of the extension.
|
||
|
|
||
|
```php
|
||
|
<?php
|
||
|
return [
|
||
|
// other manifest configs
|
||
|
'containerServiceProviders' => [
|
||
|
MyContainerServiceProvider::class
|
||
|
]
|
||
|
];
|
||
|
```
|
||
|
|
||
|
## How is the container started?
|
||
|
|
||
|
To start the container, we need to use the ContainerBuilder. Example:
|
||
|
|
||
|
```php
|
||
|
use oat\oatbox\service\ServiceManager;
|
||
|
use oat\generis\model\DependencyInjection\ContainerStarter;
|
||
|
|
||
|
$container = (new ContainerStarter(ServiceManager::getServiceManager()))->getContainer();
|
||
|
```
|
||
|
|
||
|
**IMPORTANT**: This is already done on `ServiceManager->getContainer()`, so you do not need to do it.
|
||
|
|
||
|
## Accessing the container inside a legacy controller
|
||
|
|
||
|
You just need to use this method `$this->getPsrContainer()`.
|
||
|
|
||
|
```php
|
||
|
use oat\tao\model\http\Controller;
|
||
|
use Zend\ServiceManager\ServiceLocatorAwareInterface;
|
||
|
use Zend\ServiceManager\ServiceLocatorAwareTrait;
|
||
|
|
||
|
class MyController extends Controller implements ServiceLocatorAwareInterface
|
||
|
{
|
||
|
use ServiceLocatorAwareTrait;
|
||
|
|
||
|
public function myMethod(): void
|
||
|
{
|
||
|
$service = $this->getPsrContainer()->get(MyService::class);
|
||
|
// Other logic...
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Using legacy configuration on new services
|
||
|
|
||
|
**ATTENTION!** It is **NOT RECOMMENDED** to use the `ServiceOptions` class. New services on
|
||
|
container should rely on **ENVIRONMENT VARIABLES**, but when this is _really not possible_
|
||
|
and ObjectOriented techniques such as Proxy, Factory, Strategy, etc cannot solve the
|
||
|
issue, then _maybe_ you should consider using it.
|
||
|
|
||
|
1) Registering new configs
|
||
|
|
||
|
In a new _migrations file_ or _installation script_, register the new options. Example:
|
||
|
|
||
|
```php
|
||
|
use oat\generis\model\DependencyInjection\ServiceOptions;
|
||
|
|
||
|
$serviceOptions = $serviceLocator->get(ServiceOptions::SERVICE_ID);
|
||
|
$serviceOptions->save(MyService::class, 'foo', 'bar');
|
||
|
|
||
|
$serviceLocator->register(ServiceOptions::SERVICE_ID, $serviceOptions);
|
||
|
```
|
||
|
|
||
|
2) Inject the `ServiceOptions` in your service through the ServiceProvider:
|
||
|
|
||
|
```php
|
||
|
<?php
|
||
|
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||
|
use oat\generis\model\DependencyInjection\ServiceOptions;
|
||
|
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
|
||
|
|
||
|
class MyContainerServiceProvider implements ContainerServiceProviderInterface
|
||
|
{
|
||
|
public function __invoke(ContainerConfigurator $configurator): void
|
||
|
{
|
||
|
$configurator->services()->set(MyService::class, MyService::class)->args(
|
||
|
[
|
||
|
service(ServiceOptions::SERVICE_ID)
|
||
|
]
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
3) Call it inside the new service like:
|
||
|
|
||
|
```php
|
||
|
use oat\generis\model\DependencyInjection\ServiceOptionsInterface;
|
||
|
|
||
|
class MyService
|
||
|
{
|
||
|
/** @var ServiceOptionsInterface */
|
||
|
private $options;
|
||
|
|
||
|
public function __construct(ServiceOptionsInterface $options)
|
||
|
{
|
||
|
$this->options = $options;
|
||
|
}
|
||
|
|
||
|
public function foo()
|
||
|
{
|
||
|
$bar = $this->options->get(self::class, 'foo'); // Will get "bar" as response
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Warming up container cache
|
||
|
|
||
|
The cache warmup happens when we run `taoUpdate.php`, but if you need to warmup the cache only, run:
|
||
|
|
||
|
````shell
|
||
|
php index.php 'oat\generis\scripts\tools\ContainerCacheWarmup'
|
||
|
````
|
||
|
|
||
|
## Avoid caching / Debug mode
|
||
|
|
||
|
To avoid container caching (useful on dev mode), please add the following variable on your `.env` file.
|
||
|
|
||
|
```shell
|
||
|
DI_CONTAINER_DEBUG=true
|
||
|
```
|
||
|
|
||
|
## Testing container inside Legacy Services (ConfigurableService)
|
||
|
|
||
|
While in a legacy class (extending ConfigurableService) and calling the container inside,
|
||
|
you can mock the services with `getServiceLocatorMock` (The same way we do for `ServiceManager`). Example:
|
||
|
|
||
|
Having a class:
|
||
|
|
||
|
```php
|
||
|
use oat\oatbox\service\ConfigurableService;
|
||
|
use oat\generis\persistence\PersistenceManager;
|
||
|
|
||
|
class MyLegacyServiceTest extends ConfigurableService
|
||
|
{
|
||
|
public function something()
|
||
|
{
|
||
|
return get_class($this->getServiceLocator()->getContainer()->get(PersistenceManager::SERVICE_ID));
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
In your test:
|
||
|
|
||
|
```php
|
||
|
use oat\generis\persistence\PersistenceManager;
|
||
|
use oat\generis\test\TestCase;
|
||
|
|
||
|
class MyLegacyServiceTest extends TestCase
|
||
|
{
|
||
|
public function testSomething(): void
|
||
|
{
|
||
|
$sut = new MyLegacyService();
|
||
|
$sut->setServiceLocator(
|
||
|
$this->getServiceLocatorMock(
|
||
|
[
|
||
|
PersistenceManager::SERVICE_ID => $this->createMock(PersistenceManager::class),
|
||
|
]
|
||
|
)
|
||
|
);
|
||
|
|
||
|
echo $sut->something(); // Will return the $persistenceManager class
|
||
|
}
|
||
|
}
|
||
|
```
|