Autowiring in abstract classes with DI in Symfony 3.3, is it possible?
Asked Answered
R

4

8

I am moving a Symfony 3.2 project to Symfony 3.3 and I would like to use DI new features. I have read the docs but so far I can make this to work. See the following class definition:

use Http\Adapter\Guzzle6\Client;
use Http\Message\MessageFactory;

abstract class AParent
{
    protected $message;
    protected $client;
    protected $api_count_url;

    public function __construct(MessageFactory $message, Client $client, string $api_count_url)
    {
        $this->message       = $message;
        $this->client        = $client;
        $this->api_count_url = $api_count_url;
    }

    public function getCount(string $source, string $object, MessageFactory $messageFactory, Client $client): ?array
    {
        // .....
    }

    abstract public function execute(string $source, string $object, int $qty, int $company_id): array;
    abstract protected function processDataFromApi(array $entities, int $company_id): array;
    abstract protected function executeResponse(array $rows = [], int $company_id): array;
}

class AChildren extends AParent
{
    protected $qty;

    public function execute(string $source, string $object, int $qty, int $company_id): array
    {
        $url      = $this->api_count_url . "src={$source}&obj={$object}";
        $request  = $this->message->createRequest('GET', $url);
        $response = $this->client->sendRequest($request);
    }

    protected function processDataFromApi(array $entities, int $company_id): array
    {
        // ....
    }

    protected function executeResponse(array $rows = [], int $company_id): array
    {
        // ....
    }
}

This is how my app/config/services.yml file looks like:

parameters:
    serv_api_base_url: 'https://url.com/api/'

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    CommonBundle\:
        resource: '../../src/CommonBundle/*'
        exclude: '../../src/CommonBundle/{Entity,Repository}'

    CommonBundle\Controller\:
        resource: '../../src/CommonBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    # Services that need manually wiring: API related
    CommonBundle\API\AParent:
        arguments:
            $api_count_url: '%serv_api_base_url%'

But I am getting the following error:

AutowiringFailedException Cannot autowire service "CommonBundle\API\AChildren": argument "$api_count_url" of method "__construct()" must have a type-hint or be given a value explicitly.

Certainly I am missing something here or simply this is not possible which leads me to the next question: is this a poor OOP design or it's a missing functionality from the Symfony 3.3 DI features?

Of course I don't want to make the AParent class an interface since I do not want to redefine the methods on the classes implementing such interface.

Also I do not want to repeat myself and copy/paste the same functions all over the children.

Ideas? Clues? Advice? Is this possible?

UPDATE

After read "How to Manage Common Dependencies with Parent Services" I have tried the following in my scenario:

CommonBundle\API\AParent:
    abstract: true
    arguments:
        $api_count_url: '%serv_api_base_url%'

CommonBundle\API\AChildren:
    parent: CommonBundle\API\AParent
    arguments:
        $base_url: '%serv_api_base_url%'
        $base_response_url: '%serv_api_base_response_url%' 

But the error turns into:

Attribute "autowire" on service "CommonBundle\API\AChildren" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly in /var/www/html/oneview_symfony/app/config/services.yml (which is being imported from "/var/www/html/oneview_symfony/app/config/config.yml").

However I could make it to work with the following setup:

CommonBundle\API\AParent:
    arguments:
        $api_count_url: '%serv_api_base_url%'

CommonBundle\API\AChildren:
    arguments:
        $api_count_url: '%serv_api_base_url%'
        $base_url: '%serv_api_base_url%'
        $base_response_url: '%serv_api_base_response_url%'

Is this the right way? Does it makes sense?

UPDATE #2

Following @Cerad instructions I have made a few mods (see code above and see definition below) and now the objects are coming NULL? Any ideas why is that?

// services.yml
services:
    CommonBundle\EventListener\EntitySuscriber:
        tags:
            - { name: doctrine.event_subscriber, connection: default}

    CommonBundle\API\AParent:
        abstract: true
        arguments:
            - '@httplug.message_factory'
            - '@httplug.client.myclient'
            - '%ser_api_base_url%'

// services_api.yml
services:
    CommonBundle\API\AChildren:
        parent: CommonBundle\API\AParent
        arguments:
            $base_url: '%serv_api_base_url%'
            $base_response_url: '%serv_api_base_response_url%'

// config.yml
imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }
    - { resource: services_api.yml }

Why the objects are NULL in the child class?

Raddie answered 25/8, 2017 at 18:58 Comment(0)
P
4

Interesting. It seems you want autowire to understand that AChild extends AParent and then use the AParent service definition. I don't know if this behaviour was intentionally overlooked or is not supported by design. autowire is still in it's infancy and being heavily developed.

I would suggest heading over to the di github repository, checking the issues and then opening one if applicable. The developers will let you know if this is by design or not.

In the meantime, you can use the parent service functionality if you move your child definition to a different service file. It will work because the _defaults stuff only applies to the current service file.

# services.yml
AppBundle\Service\AParent:
    abstract: true
    arguments:
        $api_count_url: '%serv_api_base_url%'

# services2.yml NOTE: different file, add to config.yml
AppBundle\Service\AChild:
    parent: AppBundle\Service\AParent
    arguments:
        $base_url: 'base url'

And one final slightly off-topic note: There is no need for public: false unless you fool around with the auto config stuff. By default, all services are defined as private unless you specifically declare them to be public.

Update - A comment mentioned something about objects being null. Not exactly sure what that means but I went and added a logger to my test classes. So:

use Psr\Log\LoggerInterface;

abstract class AParent
{
    protected $api_count_url;

    public function __construct(
        LoggerInterface $logger, 
        string $api_count_url)
    {
        $this->api_count_url = $api_count_url;
    }
}    
class AChild extends AParent
{
    public function __construct(LoggerInterface $logger, 
        string $api_count_url, string $base_url)
    {
        parent::__construct($logger,$api_count_url);
    }

And since there is only one psr7 logger implementation, the logger is autowired and injected without changing the service definition.

Update 2 I updated to S3.3.8 and started getting:

[Symfony\Component\DependencyInjection\Exception\RuntimeException]                                                                         
Invalid constructor argument 2 for service "AppBundle\Service\AParent": argument 1 must be defined before. Check your service definition.  

Autowire is still under heavy development. Not going to spend the effort at this point to figure out why. Something to do with the order of the arguments. I'll revist once the LTS version is released.

Pabulum answered 25/8, 2017 at 19:41 Comment(5)
Yup, I just figure it out now, everything is private by design! :D I have it removed from OP to avoid any confusion. I am assuming that in the second file I need to define extra parameters, right?Raddie
Yep. You can leave api_count_url in the parent definition and just add additional arguments to the child definition. Things have really changed dramatically since Ms Rowling joined the Symfony team.Pabulum
can you take a look to my 2nd upd? I have opened an issue in Symfony BTW (didn't find a repo for DI)Raddie
Might be time to start a new question. In particular show the constructor of your child object. Rest of the stuff is irrelevant.Pabulum
See here he says I need to define the services explicitly but I am not following him, could you improve your answer?Raddie
P
2

Well, I went through the same issue. For some reasons, I had to create a Symfony 3.3.17 project for starting, because in my company I had to use 2 of our bundles which are still stuck in SF 3.3 (I know what you will say about this, but I am intending to upgrade all of them soon).

My problem was a little bit different that's why Cerad's solution was a little bit complicated to use in my case. But I can bring (maybe) also a solution for the issue mentioned in the question. In my case, I am using Symfony Flex to manage my application even if it is SF 3.3. So, those who know flex are aware that config.yml does not exist anymore and there is instead a config folder at the root of the Application. Inside it, you just have a services.yaml file. So, if you want to add a services2.yaml file, you will see that it is not detected unless you rename it into services_2.yaml. But in this case, this means that you have another environment called 2 like dev or test. Not good, right?

I found the solution from xabbuh's answer in this issue.

So, in order to be able to use autowiring in abstract classes with DI in Symfony 3.3 you can still keep your services definition in one file:

CommonBundle\API\AParent:
    abstract: true
    autoconfigure: false
    arguments:
        $api_count_url: '%serv_api_base_url%'

CommonBundle\API\AChildren:
    parent: CommonBundle\API\AParent
    autoconfigure: false
    autowire: true
    public: false
    arguments:
        $base_url: '%serv_api_base_url%'
        $base_response_url: '%serv_api_base_response_url%'

The point here is that

you need to be a bit more explicit

as xabbuh said. You cannot inherit from _default to set public, autowire and autoconfigure if you are using parent key in your service declaration. Setting autoconfigure value in your child service to false is important because, if it is not set you will have

The service "CommonBundle\API\AChildren" cannot have a "parent" and also have "autoconfigure". Make sense in fact...

And one last thing, if the problem was related to Symfony Console command (like in my case), don't forget to use

tags:
    - { name: console.command }

for your child service because autoconfigure is set to false.

Phenoxide answered 24/7, 2018 at 9:53 Comment(0)
O
0

I think the answer is right there in the error message and nothing to do with using an abstract class. Do you have autowiring set on in such way that AChildren will get wired without your explicit instruction? If so, you probably need to specify that constructor argument for each child class. See https://symfony.com/doc/current/service_container/parent_services.html for possible help

Ossicle answered 25/8, 2017 at 19:7 Comment(1)
I do not think this is completely true: "you probably need to specify that constructor argument for each child class" as per docs "having a parent service implies that the arguments and method calls of the parent service should be used for the child services"Raddie
S
0

It seems that autowiring doesn't work with abstract services in Symfony 3.3.8. I create an issue on Github for this.

In the meantime, personally I remove the abstract option and it works fine.

Schiedam answered 13/9, 2017 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.