Search code examples
phpsymfonysymfony-2.3

Symfony Tagged Service vs Factory


I am new to Symfony2 and a bit confused. Sorry if this question sounds silly.

Both creating service with factory and Tagged Services are used to create a factory hypothetically.

As per the documentation in above links, the differences I could figure out are:

For Tagged services:

you need to write a compiler pass and then define a tag for each service.

Then write a factory( any class. Should this be called a factory or not?), which will take objects of all the tagged services from compiler pass. Also create a getter method here which will return the object based upon some criteria.

In case of creating service with factory, you can create only one service and its object will be returned to you by calling a static method.

So, I guess, in tagged service, you may choose from a number of services and in case of creating-service-by-factory, you can only create a single service. I think tagged service is already serving the purpose of a factory. Why do we need to have factory-services when they can create a single object only? May be I have a misconception here, but are tagged services better, as compiler passes are run on cache warmup and the tagged service is stored there itself, so it will be faster. But, services are also cached, so there should not be much of a difference. But I am not sure if this conceptualization is right.

Please make me understand the concept of both and make me feel enlightened.


Solution

  • Factory service and tagged services have completely different roles. I will try to illustrate you through examples some common cases when you might want to use one or the other.

    Factory

    It is commonly used when some of parameters required for construction of a service are not available in service container, or if some preparation is needed before your service can be instantiated. Symfony will automatically check whether your service has it's own factory or is constructed directly from container parameters.

    Real world usecase

    You have a service that handles PayPal payments. To construct it, you need to pass it in your PayPal credentials and API endpoint URL.

    class PayPalPaymentService
    {
        // ...
    
        public function __construct(PayPalCredentials $credentials, $apiEndpoint)
        {
            // ...
        }
    
        // ...
    }
    

    This works well for you, but you realize that you need to have two environments: Live and Sandbox. Both your credentials and API endpoint URL is different depending on which environment you're in.

    Symfony has a couple of ways to handle this for you, but one of them is through factory service: you create a factory which will instantiate PayPalPaymentService according to your current local environment.

    class PayPalPaymentServiceFactory
    {
        const SANDBOX_ENDPOINT = '...';
        const LIVE_ENDPOINT = '...';
    
        private $livePublic;
        // ...
    
        public function __construct($livePublic, $liveSecret, $sandboxPublic, $sandboxSecret, $kernelEnvironment)
        {
            $this->livePublic = $livePublic;
            // ...
        }
    
        public function create()
        {
            if ('prod' === $kernelEnvironment) {
                $credentials = new PayPalCredentials($this->livePublic, $this->liveSecret);
            } else {
                $credentials = new PayPalCredentials($this->sandboxPublic, $this->sandboxSecret);
            }
            $url = 'prod' === $kernelEnvironment ? self::LIVE_ENDPOINT : self::SANDBOX_ENDPOINT;
    
            return new PayPalPaymentService($credentials, $url);
        }
    }
    

    Now, whenever you inject your PayPalPaymentService, container will first ask your PayPalPaymentServiceFactory to create it, and then inject it.

    Tagged services

    These are completely a different story. Tagged services are used throughout Symfony codebase: for FormTypes, Twig Extensions, Validators etc. Concept of tagged services is a really powerful extension mechanism. It allows you to flag your service as something that's used for a specific purpose. As I said, there are already examples of them in Symfony codebase, but here is a really simplified example to see what you can do with them:

    You have payments on your E-shop and you have a list of supported payment processors. Every payment processor has it's own checkout page. So you decide to make it modular and let others create new payment processors for it.

    On your cart page, you need to show links to all available payment processor checkout pages, to do that, you have a service which gives you all available checkout links:

    class CheckoutLinksProvider
    {
        /**
         * @var ProcessorAdapter[]
         */
        private $adapters;
    
        public function registerProcessor(ProcessorAdapter $adapter)
        {
            $this->adapters = $adapter;
        }
    
        public function getCheckoutLinks()
        {
            $links = [];
            foreach ($this->adapters as $adapter) {
                $links[] = $adapter->getCheckoutLink();
            }
            return $links;
        }
    }
    
    interface ProcessorAdapter
    {
        /**
         * @return string Checkout URL link
         */
        public function getCheckoutLink();
    
        // ...
    }
    

    Now, anybody can implement ProcessorAdapter and share it as a bundle on Github! All they have to do is mark their ProcessorAdapter implementation with some tag that you decide, for example: 'my_eshop_processor_adapter'.

    In your core E-shop system compiler pass, you can now pick up all ProcessorAdapter implementations and register them in your CheckoutLinksProvider service. Voila! You've got a fully modular payment sistem! Anybody can create support for custom processors, you can just download their bundles and register them in your Kernel and checkout links will appear!