Search code examples
phpsymfonydependency-injectionsymfony-3.3symfony3.x

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?


Solution

  • 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.