Symfony3 Factory as Service
Asked Answered
L

2

5

I have been trying to get my head around this

http://symfony.com/doc/current/service_container/factories.html

But there seems to be a piece missing that tires it all together OR I am completely missing the point.

The example has a factory Class

class NewsletterManagerFactory
{
    public static function createNewsletterManager()
    {
        $newsletterManager = new NewsletterManager();

        // ...

        return $newsletterManager;
    }
}

And then that factory is configured via a service to be the factory class and method for NewsletterManager service.

services:
    app.newsletter_manager_factory:
        class: AppBundle\Email\NewsletterManagerFactory

    app.newsletter_manager:
        class:   AppBundle\Email\NewsletterManager
        factory: 'app.newsletter_manager_factory:createNewsletterManager'

So now we have a NewsletterManager Class that is aware of the NewsletterManagerFactory Class via the factory: param in services.yml

Question

How do you make use of this configuration? What is now exposed inside of NewsletterManager that allows me to call createNewsletterManager in the factory Class?

The two services are still completely separate as far as I can tell?

Loony answered 16/12, 2016 at 13:32 Comment(0)
S
12

I used this pattern once. Here is the use case for it.

Imagine that you have multiple widget classes i.e. Acme\Widget1, Acme\Widget2, Acme\WidgetN.

Each widget has an advanced instantiation process so you decide to use a factory. It also has a complex dependency chain that is needed for each widget to be instantiated. I.e. Acme\Dependency1, Acme\Dependency2, Acme\Dependency3.

So what you'd do is to create Acme\WidgetFactory service with dependencies once. Then you need to specify that Acme\WidgetFactory as factory for each widget. In case something changes in the way of widget instantiation you only need to change one class and one service definition. All 1 to N widget services stay the same.

Here is the example...

Typical way of implementation:

acme.widget1:
    class: Acme\Widget1
    factory: ['Acme\Widget1', 'create']
    arguments: ['@acme.dependency1', '@acme.dependency2', '@acme.dependencyN'] 

acme.widget2:
    class: Acme\Widget2
    factory: ['Acme\Widget2', 'create']
    arguments: ['@acme.dependency1', '@acme.dependency2', '@acme.dependencyN'] 

acme.widgetN:
    class: Acme\WidgetN
    factory: ['Acme\WidgetN', 'create']
    arguments: ['@acme.dependency1', '@acme.dependency2', '@acme.dependencyN'] 

Here you have a strong smell of code duplication. If you want to change something you need to do it N times.

Instead here is what you can do.

acme.widget_factory:
    class: Acme\WidgetFactory
    arguments: ['@acme.dependency1', '@acme.dependency2', '@acme.dependencyN']

acme.widget1:
    class: Acme\Widget1
    factory: ['@acme.widget_factory', createWidget]

acme.widget2:
    class: Acme\Widget2
    factory: ['@acme.widget_factory', createWidget]

acme.widgetN:
    class: Acme\WidgetN
    factory: ['@acme.widget_factory', createWidget]

Code duplication is gone.

Appeared small inconvenience... Factory does not know what concrete class to instantiate. I used the following technique for it.

I tagged each widget and then during compiler pass added extra parameter to factory.

acme.widget_factory:
    class: Acme\WidgetFactory
    arguments: ['@acme.dependency1', '@acme.dependency2', '@acme.dependencyN']

acme.widget1:
    class: Acme\Widget1
    factory: ['@acme.widget_factory', createWidget]
    tags:
        - { name: acme.widget }

acme.widget2:
    class: Acme\Widget2
    factory: ['@acme.widget_factory', createWidget]
    tags:
        - { name: acme.widget }

acme.widgetN:
    class: Acme\WidgetN
    factory: ['@acme.widget_factory', createWidget]
    tags:
        - { name: acme.widget }

Then in DepencencyInjection\AcmeDemoExtension.php

class AcmeDemoExtension implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $widgets = $container->findTaggedServiceIds('acme.widget');
        foreach ($widgets as $id => $tags) {
            $definition = $container->getDefinition($id);
            $definition->setArguments([$definition->getClass()]);
        }
    }
}

and finally in the factory...

class AcmeWidgetFactory
{
    //.....
    public static function createWidget($class) 
    {
        //.....
        return new $class(/*  dependencies */);
        //.....
    }
    //.....
}

So at the end when you do $this->get('acme.widget1') the factory method with class name as a parameter is called. Factory already has all dependencies and knows the logic of class instantiation. So it does all the work and return required widget instance.

Shaddock answered 16/12, 2016 at 19:43 Comment(4)
Thanks this is useful and I can see why this is handy. But I am still not clear on why you add the factory param to the service. For instance the acme.widget2 service now has a factory param of ['@acme.widget_factory', createWidget] but how is that used inside of the class Acme\Widget2. Can you show me what the Acme\Widget2 class might look like?Loony
Nothing special in there. Just a class with a constructor. All instantiation logic is moved to a factory. When Symfony wants to instantiate acme.widget2 it takes instance of acme.widget_factory and calls its method createWidget() with widget class name as parameter. All complex logic of instantiating and setting up happens there. At the end createWidget() returns ready to be used instance of Acme\Widget2.Shaddock
Hi! So useful example, but I can't guess how to pass and get dependency arguments from the controller (doing that ->get('service.id')) to the factory. Thank you in advance. Cheers!Asyndeton
Hi, could you please elaborate on how the WidgetFactory's "createWidget()" method gets the dependencies?Allan
C
1

This is simple. If you gonna call

$nm = $container->get('app.newsletter_manager')

then the newsletter manager will be created by factory automatically.

Crampon answered 16/12, 2016 at 13:55 Comment(3)
Ok so the point is that the service returns a new object. Rather than the service itself?Loony
Exactly. The service (the factory) returns an new service or entity or what ever your factory creates.Crampon
@JakeN, no, the return value of the factory method is the service. The factory class itself is just an intermediary that is not directly adressable. You can however write a separate service definition for the factory too, if desireable.Breathless

© 2022 - 2024 — McMap. All rights reserved.