Search code examples
phpsymfonydependency-injectionsymfony4dry

Symfony multiple command classes with same dependencies: Avoid code repetition


In a Symfony 4 project, I have two commands (and possibly more coming) that have the same dependencies. Currently, I have some code repetition along my code for the two commands, I know I should be able to make this more DRY and efficient, but not sure how exactly I have to organize it.

This is how I have them so far:

# config/services.yaml

parameters:
    api_client_id: '%env(API_CLIENT_ID)%'
    api_client_secret: '%env(API_CLIENT_SECRET)%'
    api_client_id_sandbox: '%env(API_CLIENT_ID_SANDBOX)%'
    api_client_secret_sandbox: '%env(API_CLIENT_SECRET_SANDBOX)%'
    api_env: '%env(API_ENV)%'

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
        public: false       # Allows optimizing the container by removing unused services; this also means
                            # fetching services directly from the container via $container->get() won't work.
                            # The best practice is to be explicit about your dependencies anyway.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/*'
        exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones


    # Registering 3rd Party API Manager.
    app.api_service_factory:
        class: App\Service\APIServiceFactory
        arguments:
            - "%api_client_id%"
            - "%api_client_secret%"
            - "%api_client_id_sandbox%"
            - "%api_client_secret_sandbox%"
            - "%api_env%"  


    # Collect promotions command.
    App\Command\CollectPromotionsCommand:
        arguments:
            - "@app.api_service_factory"
        tags:
            - { name: 'console.command', command: 'app:collect-promotions' }


    # Processes the reserved topups orders.
    App\Command\ProcessReservedTopupsCommand:
        arguments:
            - "@app.api_service_factory"
        tags:
            - { name: 'console.command', command: 'app:process-reserved-topups' }

<?php
// src/Command/CollectPromotionsCommand.php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Service\APIServiceFactory;
use Doctrine\ORM\EntityManagerInterface;

class CollectPromotionsCommand extends Command
{

  /**
   * @var APIServiceFactory
   */
  protected $apiServiceFactory;

  /**
   * @var EntityManagerInterface
   */
  protected $em;

  protected static $defaultName = 'app:collect-promotions';

  public function __construct(APIServiceFactory $apiServiceFactory, EntityManagerInterface $em) {
    $this->apiServiceFactory  = $apiServiceFactory;
    $this->em                         = $em;

    parent::__construct();
  }

// ...


}

<?php
// src/Command/ProcessReservedTopupsCommand.php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Service\APIServiceFactory;
use Doctrine\ORM\EntityManagerInterface;

class ProcessReservedTopupsCommand extends Command
{

  /**
   * @var APIServiceFactory
   */
  protected $apiServiceFactory;

  /**
   * @var EntityManagerInterface
   */
  protected $em;

  protected static $defaultName = 'app:collect-promotions';

  public function __construct(APIServiceFactory $apiServiceFactory, EntityManagerInterface $em) {
    $this->apiServiceFactory  = $apiServiceFactory;
    $this->em                         = $em;

    parent::__construct();
  }

// ...


}

Any help to make this code look more decent is very appreciated.


Solution

  • Personnaly I wouldn't do it because I prefer clarity even if it implies a bit of duplication.

    But if you are wondering how to avoid code duplication in your serivces' constructors and configuration here is the answer: https://symfony.com/doc/current/service_container/parent_services.html. It is about introducing a base class for your commands. Symfony let you avoid duplication in configuration as well.

    Your example will look like:

    class BaseCommand extends Command
    {
      protected APIServiceFactory $apiServiceFactory;
      protected EntityManagerInterface $em;
    
      public function __construct(APIServiceFactory $apiServiceFactory, EntityManagerInterface $em) {
        parent::__construct();
    
        $this->apiServiceFactory  = $apiServiceFactory;
        $this->em = $em;
      }
    
      // ...
    }
    

    and

    class CollectPromotionsCommand extends BaseCommand
    {
        // ...
    }
    

    and

    class ProcessReservedTopupsCommand extends BaseCommand
    {
        // ...
    }
    

    Configuraiton:

    # Base API command depending on API.
    App\Command\BaseCommand:
        abstract: true
        arguments:
            - "@app.api_service_factory"
    
    # Collect promotions command.
    App\Command\CollectPromotionsCommand:
        parent: App\Command\BaseCommand
        tags:
            - { name: 'console.command', command: 'app:collect-promotions' }
    
    
    # Processes the reserved topups orders.
    App\Command\ProcessReservedTopupsCommand:
        parent: App\Command\BaseCommand
        tags:
            - { name: 'console.command', command: 'app:process-reserved-topups' }