Skip to content

Commit

Permalink
factory_method key
Browse files Browse the repository at this point in the history
  • Loading branch information
PawelSuwinski committed Jun 7, 2022
1 parent cb37869 commit 6b83a87
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 5 deletions.
37 changes: 34 additions & 3 deletions docs/en/02_Developer_Guides/05_Extending/05_Injector.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.
Expand Down
32 changes: 30 additions & 2 deletions src/Core/Injector/Injector.php
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,36 @@ protected function instantiate($spec, $id = null, $type = null)
$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) {
Expand Down
92 changes: 92 additions & 0 deletions tests/php/Core/Injector/InjectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ public function testEmptyFactory()
$injector->create('SomeClass');
}

/**
* Fail creating object by factory that does not implement Factory
* interface.
*/
public function testNotFactoryInterfaceFactory()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageRegExp('/Factory.*not implement.*interface/');

$injector = new Injector([
'service' => [
'factory' => 'stdClass',
],
]);
$injector->get('service');
}

public function testConfiguredInjector()
{
$injector = new Injector();
Expand Down Expand Up @@ -906,6 +923,81 @@ function ($args) {
$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);
$this->expectExceptionMessageRegExp('/Factory method.*not exist/');

$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
Expand Down

0 comments on commit 6b83a87

Please sign in to comment.