Symfony 4. How to access the service from controller without dependency injection?

后端 未结 3 444
庸人自扰
庸人自扰 2020-12-18 16:07

I have several services: DieselCaseService, CarloanCaseService LvCaseService.

The controller decides which of services to get.

$type = $quickCheck[\         


        
相关标签:
3条回答
  • 2020-12-18 16:29

    I have created a wrapper-service

    <?php
    namespace App;
    
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    class ServiceFactory
    {
    
        /** @var ContainerInterface */
        private $container;
    
    
        public function __construct(ContainerInterface $container)
        {
            $this->container = $container;
        }
    
        public function getService($alias)
        {
            return $this->container->get($alias);
        }
    
        /**
         * @param $alias
         * @return bool
         */
        public function hasService($alias)
        {
            return $this->container->has($alias);
        }
    }
    

    than I inject this service into controller

    public function saveRegisterData(Request $request, AccountService $accountService, ServiceFactory $serviceFactory) 
    {
    .
    .
    .
    
        if (!$serviceFactory->hasService('case_service.'.$type)) {
            throw new \LogicException("no valid case_service found");
        }
    
        /**
        * @var $caseService \App\Service\Cases\CaseInterface
        */
        $caseService = $serviceFactory->getService('case_service.' . $type);
    
    .
    .
    .
    }
    
    
    0 讨论(0)
  • 2020-12-18 16:36

    For any "multiple instances of same type by key" situation, you can use autowired array.

    1. Autodiscovery Services with App\ namespace

    services:
        _defaults:
            autowire: true
    
        App\:
            resource: ../src
    

    2. Require autowired array in Constructor

    <?php
    
    namespace App\Controller\Api;
    
    use App\Service\Cases\DieselCaseService
    
    final class AccountController
    {
        /**
         * @var CaseInterface[]
         */
        private $cases;
    
        /**
         * @param CaseInterface[] $cases
         */
        public function __construct(array $cases)
        {
            foreach ($cases as $case) {
                $this->cases[$case->getName()] = $cases;
            }
        }
    
        public function someAction(): void
        {
            $dieselCase = $this->cases['diesel']; // @todo maybe add validation for exisiting key
            $dieselCase->anyMethod();
        }
    }
    

    3. Register compiler pass in Kernel

    The autowired array functionality is not in Symfony core. It's possible thanks to compiler passes. You can write your own or use this one:

    use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireArrayParameterCompilerPass;
    
    final class AppKernel extends Kernel
    {
        protected function build(ContainerBuilder $containerBuilder): void
        {
            $containerBuilder->addCompilerPass(new AutowireArrayParameterCompilerPass);
        }
    }
    

    That's it! :)

    I use it on all my projects and it works like a charm.


    Read more in post about autowired arrays I wrote.

    0 讨论(0)
  • 2020-12-18 16:40

    Some rainy Sunday afternoon code. This question is basically a duplicate of several other questions but there are enough moving parts that I suppose it is worthwhile to provide some specific solutions.

    Start by defining the cases just to make sure we are all on the same page:

    interface CaseInterface { }
    class DieselCaseService implements CaseInterface {}
    class CarloanCaseService implements CaseInterface{}
    

    The easiest way to answer the question is to add the case services directly to the controller's container:

    class CaseController extends AbstractController
    {
        public function action1()
        {
            $case = $this->get(DieselCaseService::class);
    
            return new Response(get_class($case));
        }
        // https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
        public static function getSubscribedServices()
        {
            return array_merge(parent::getSubscribedServices(), [
                // ...
                DieselCaseService::class => DieselCaseService::class,
                CarloanCaseService::class => CarloanCaseService::class,
            ]);
        }
    }
    

    The only thing I changed for your question is using the class names for service ids instead of something like 'case_service.diesel'. You can of course tweak the code to use your ids but class names are more standard. Note that there is no need for any entries in services.yaml for this to work.

    There are a couple of issues with the above code which may or may not be a problem. The first is that only the one controller will have access to the case services. Which may be all you need. The second potential issue is that you need to explicitly list all your case services. Which again might be okay but it might be nice to automatically pick up services which implement the case interface.

    It's not hard to do but it does require following all the steps.

    Start be defining a case locator:

    use Symfony\Component\DependencyInjection\ServiceLocator;
    
    class CaseLocator extends ServiceLocator
    {
    
    }
    

    Now we need some code to find all the case services and inject them into the locator. There are several approaches but perhaps the most straight forward is to use the Kernel class.

    # src/Kernel.php
    // Make the kernel a compiler pass by implementing the pass interface
    class Kernel extends BaseKernel implements CompilerPassInterface 
    {
        protected function build(ContainerBuilder $container)
        {
            // Tag all case interface classes for later use
            $container->registerForAutoconfiguration(CaseInterface::class)->addTag('case');
        }
        // and this is the actual compiler pass code
        public function process(ContainerBuilder $container)
        {
            // Add all the cases to the case locator
            $caseIds = [];
            foreach ($container->findTaggedServiceIds('case') as $id => $tags) {
                $caseIds[$id] = new Reference($id);
            }
            $caseLocator = $container->getDefinition(CaseLocator::class);
            $caseLocator->setArguments([$caseIds]);
        }
    

    If you follow all of the above steps then you can inject your case locator into any controller (or other service) that happens to need it:

    class CaseController extends AbstractController
    {
        public function action2(CaseLocator $caseLocator)
        {
            $case = $caseLocator->get(CarloanCaseService::class);
    
            return new Response(get_class($case));
        }
    

    And once again, there is no need to make any changes to your services.yaml for all this to work.

    0 讨论(0)
提交回复
热议问题