diff --git a/docs/en/02_Developer_Guides/05_Extending/05_Injector.md b/docs/en/02_Developer_Guides/05_Extending/05_Injector.md index e22421680..763746e22 100644 --- a/docs/en/02_Developer_Guides/05_Extending/05_Injector.md +++ b/docs/en/02_Developer_Guides/05_Extending/05_Injector.md @@ -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. diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index f5b4b87c8..7136a605b 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -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) { diff --git a/tests/php/Core/Injector/InjectorTest.php b/tests/php/Core/Injector/InjectorTest.php index 3b8cd1850..b06b0fc87 100644 --- a/tests/php/Core/Injector/InjectorTest.php +++ b/tests/php/Core/Injector/InjectorTest.php @@ -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