NEW Add factory_method configuration to Injector

use callable as well as creator
This commit is contained in:
Paweł Suwiński 2022-06-09 03:57:47 +02:00 committed by GitHub
parent 3799bceff3
commit 1c85d151a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 5 deletions

View File

@ -193,9 +193,14 @@ Note: undefined variables will be replaced with null.
## Factories
Some services require non-trivial construction which means they must be created by a factory class. To do this, create
a factory class which implements the [Factory](api:SilverStripe\Framework\Injector\Factory) interface. You can then specify
the `factory` key in the service definition, and the factory service will be used.
Some services require non-trivial construction which means they must be created
by a factory.
### Factory interface
Create a factory class which implements the [Factory](api:SilverStripe\Framework\Injector\Factory)
interface. You can then specify the `factory` key in the service definition,
and the factory service will be used.
An example using the `MyFactory` service to create instances of the `MyService` service is shown below:
@ -224,6 +229,32 @@ class MyFactory implements SilverStripe\Core\Injector\Factory
$instance = Injector::inst()->get('MyService');
```
### Factory method
To use any class that not implements Factory interface as a service factory
specify `factory` and `factory_method` keys.
An example of HTTP Client service with extra logging middleware:
**app/_config/app.yml**
```yml
SilverStripe\Core\Injector\Injector:
LogMiddleware:
factory: 'GuzzleHttp\Middleware'
factory_method: 'log'
constructor: ['%$Psr\Log\LoggerInterface', '%$GuzzleHttp\MessageFormatter', 'info']
GuzzleHttp\HandlerStack:
factory: 'GuzzleHttp\HandlerStack'
factory_method: 'create'
calls:
- [push, ['%$LogMiddleware']]
GuzzleHttp\Client:
constructor:
-
handler: '%$GuzzleHttp\HandlerStack'
```
## Dependency overrides
To override the `$dependency` declaration for a class, define the following configuration file.

View File

@ -605,8 +605,36 @@ class Injector implements ContainerInterface
$constructorParams = [null, DataObject::CREATE_SINGLETON];
}
$factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
$object = $factory->create($class, $constructorParams);
if (isset($spec['factory']) && isset($spec['factory_method'])) {
if (!method_exists($spec['factory'], $spec['factory_method'])) {
throw new InvalidArgumentException(sprintf(
'Factory method "%s::%s" does not exist.',
$spec['factory'],
$spec['factory_method']
));
}
// If factory_method is statically callable, do not instantiate
// factory i.e. just call factory_method statically.
$factory = is_callable([$spec['factory'], $spec['factory_method']])
? $spec['factory']
: $this->get($spec['factory']);
$method = $spec['factory_method'];
$object = call_user_func_array([$factory, $method], $constructorParams);
} else {
$factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
if (!$factory instanceof Factory) {
throw new InvalidArgumentException(sprintf(
'Factory class "%s" does not implement "%s" interface.',
get_class($factory),
Factory::class
));
}
$object = $factory->create($class, $constructorParams);
}
if (!is_object($object)) {
throw new InjectorNotFoundException('Factory does not return an object');
}
// Handle empty factory responses
if (!$object) {

View File

@ -118,6 +118,22 @@ class InjectorTest extends SapphireTest
$injector->create('SomeClass');
}
/**
* Fail creating object by factory that does not implement Factory
* interface.
*/
public function testNotFactoryInterfaceFactory()
{
$this->expectException(\InvalidArgumentException::class);
$injector = new Injector([
'service' => [
'factory' => 'stdClass',
],
]);
$injector->get('service');
}
public function testConfiguredInjector()
{
$injector = new Injector();
@ -906,6 +922,80 @@ class InjectorTest extends SapphireTest
$this->assertInstanceOf(TestObject::class, $injector->get('service'));
}
/**
* Creating object by factory method.
*/
public function testByFactoryMethodObjectCreator()
{
// Dummy service giving DateTime of tommorow.
$injector = new Injector([
'service' => [
'factory' => 'DateTime',
'factory_method' => 'add',
'constructor' => ['%$DateInterval'],
],
'DateInterval' => [
'constructor' => ['P1D'],
],
]);
$this->assertInstanceOf(\DateTime::class, $injector->get('service'));
$this->assertEquals(
(new \DateTime())->add(new \DateInterval('P1D'))->format('%Y%m%d'),
$injector->get('service')->format('%Y%m%d')
);
}
/**
* Creating object by static factory method.
*/
public function testByStaticFactoryMethodObjectCreator()
{
// Dummy service changing any callable to injector service with
// `strtoupper` as default one. Constructor disallows instantiation.
$injector = new Injector([
'service' => [
'factory' => 'Closure',
'factory_method' => 'fromCallable',
'constructor' => ['strtoupper'],
],
]);
$this->assertInstanceOf(\Closure::class, $injector->get('service'));
// Default service.
$this->assertEquals('ABC', $injector->get('service')('abc'));
// Create service with arguments.
$this->assertEquals('abc', $injector->create('service', 'strtolower')('ABC'));
}
public function testFactoryMethodNotReturnsObject()
{
$this->expectException(InjectorNotFoundException::class);
$injector = new Injector([
'service' => [
'factory' => 'DateTime',
'factory_method' => 'getTimeStamp',
],
]);
$injector->get('service');
}
public function testFactoryMethodNotExists()
{
$this->expectException(\InvalidArgumentException::class);
$injector = new Injector([
'service' => [
'factory' => 'stdClass',
'factory_method' => 'method',
],
]);
$injector->get('service');
}
public function testMethods()
{
// do it again but have test object configured as a constructor dependency