Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 208
ReynierPM Avatar asked Aug 25 '17 18:08

ReynierPM


2 Answers

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.

like image 142
Cerad Avatar answered Sep 28 '22 10:09

Cerad


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.

like image 36
fgamess Avatar answered Sep 28 '22 10:09

fgamess