0

I want to migrate a symfony2.8 api to symfony5.4. I transferred all my Symfony commands, changed containerAwareCommand to \Symfony\Component\Console\Command\Command and changed the command configuration to match the configuration of the new version .

However, my commands are still not visible in the bin/console list and I cannot use them without declaring them in the services.yml. I have more than thirty commands to transfer and adding a paragraph in the service.yml seems to me tedious and not necessary. Did I forget something?

Second question, how can I access the service container directly in the commands? On Symfony 2.8 I was accessing it with $this->getContainer()->get('toto') but it doesn't seem to work anymore on version 5.

services.yml:

    tags:
        - { name: 'console.command', command: 'word:manager:expiration_email' }
    arguments:
        - '@service_container'
        - '@logger'

Command:

protected function configure()
{
    $this
        ->setName('word:manager:expiration_email')
        ->setDescription('Alert email : Send an email x days before the expiration.')
        ->addOption(
            'days',
            'd',
            InputOption::VALUE_REQUIRED,
            'How many days before the expiration',
            1
        );
}

/**
 * {@inheritdoc}
 */
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $days = intval($input->getOption('days'));

    if ($days > 0) {
        try {
            $this->getManager()->sendExpirationEmail($days);
        } catch (\Exception $e) {
            $this->container->get('logger')->error(sprintf(
                'aemCommand: an error occurred in file "%s" (L%d): "%s"',
                $e->getFile(),
                $e->getLine(),
                $e->getMessage()
            ));
        }
    }
    return 0;
}

/**
 * @return aem
 */
private function getManager()
{
    return $this->container->get('word.manager.alert_employee');
}

services.yml:

imports:

    - { resource: "@WORDCoreBundle/Resources/config/services.yml" }
    - { resource: "@WORDAlertBundle/Resources/config/services.yml" }
    - { resource: "@WORDEmployeeBundle/Resources/config/services.yml" }
    - { resource: "@WORDTimeManagementBundle/Resources/config/services.yml" }
    - { resource: "@WORDUserBundle/Resources/config/services.yml" }
    - { resource: "@WORDFileBundle/Resources/config/services.yml" }
    - { resource: "@WORDLocalisationBundle/Resources/config/services.yml" }
    - { resource: "@WORDPlatformBundle/Resources/config/services.yml" }
    - { resource: "@WORDCompanyBundle/Resources/config/services.yml" }
    - { resource: "@WORDContractBundle/Resources/config/services.yml" }
    - { resource: "@WORDFolderBundle/Resources/config/services.yml" }
    - { resource: "@WORDTrainingBundle/Resources/config/services.yml" }
    - { resource: "@WORDExpenseReportBundle/Resources/config/services.yml" }
    - { resource: "@WORDBookStoreBundle/Resources/config/services.yml" }
    - { resource: "@WORDPaymentBundle/Resources/config/services.yml" }
    - { resource: "@WORDInvoiceBundle/Resources/config/services.yml" }
    - { resource: "@WORDAgendaBundle/Resources/config/services.yml" }
    - { resource: "@WORDAlertBundle/Resources/config/services.yml" }
    - { resource: "@WORDAnnualReviewBundle/Resources/config/services.yml" }
    - { resource: "@WORDProReviewBundle/Resources/config/services.yml" }
    - { resource: "@WORDWidgetBundle/Resources/config/services.yml" }
    - { resource: "@WORDCommentBundle/Resources/config/services.yml"}
    - { resource: "@WORDFrontBundle/Resources/config/services.yml" }
    - { resource: "@WORDNotificationBundle/Resources/config/services.yml" }

   


    services:

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'
    _defaults:
        autowire: true      
        autoconfigure: true 



            
    WORD\CoreBundle\Controller\ApiController:
        calls:
            - method: setContainer
              arguments: ['@service_container']
    WORD\PlatformBundle\Controller\PlatformRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']
    WORD\UserBundle\Controller\UserRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']
    WORD\AlertBundle\Controller\AlertRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']
              
    WORD\EmployeeBundle\Controller\EmployeeRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']
              
    WORD\CompanyBundle\Controller\CompanyRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']      
      
    WORD\CompanyBundle\Controller\ContactRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']     
    WORD\CompanyBundle\Controller\ServiceRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']               
              
    WORD\CompanyBundle\Controller\ChartRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']   
              
    WORD\BookStoreBundle\Controller\InfoRESTController:
        calls:
            - method: setContainer
              arguments: ['@service_container']  

    
    WORD.user.manager.user:
        class: 'WORD\UserBundle\Service\UserManager'
        public: false
        lazy: true
        arguments:
            - '@fos_user.util.password_updater'
            - '@fos_user.util.canonical_fields_updater'
            - '@fos_user.object_manager'
            - 'WORD\UserBundle\Entity\User'

    
    WORD\AgendaBundle\DataFixtures\AgendaFixtures:
        tags: [doctrine.fixture.orm]

    WORD\UserBundle\DataFixtures\UserFixtures:
        tags: [doctrine.fixture.orm]
        arguments:
            $fos_manager: '@fos_user.user_manager'
    WORD\LocalisationBundle\DataFixtures\CountryFixtures:
        tags: [doctrine.fixture.orm]

    
    App\DataFixtures\Processor\UserProcessor:
        tags: [ { name: fidry_alice_data_fixtures.processor } ]



    WORD\AlertBundle\Command\AlertExpirationEmailCommand:
        tags:
            - { name: 'console.command', command: 'WORD:alert:expiration_email' }
        arguments:
            - '@service_container'
            - '@logger'

Composer.json

    "autoload": {
    "psr-4": {
        "App\\": "src/",
        "WORD\\": "WORD"
    }
},
Will B.
  • 17,883
  • 4
  • 67
  • 69
oracle972
  • 77
  • 10
  • [**Please Never** post images of or off site links to code, data or error messages](https://meta.stackoverflow.com/a/285557/2310830). Please edit your question and include copy/paste the text into the question, formatted. This is so that we can try to reproduce the problem without having to re-type everything, and your question can be properly indexed or read by screen readers. – RiggsFolly Oct 17 '22 at 11:30
  • maybe a caching issue? You could try `APP_ENV=dev bin/console list` alternatively you could check, are other classes from your custom namespace autoloaded? – john Smith Oct 17 '22 at 11:34
  • Hello, I launched a cache:clear as well as an APP_ENV=dev bin/console list and I have no change. I even deleted the cache folder. Yes my other classes of the same namespace are well autoloaded – oracle972 Oct 17 '22 at 11:45
  • Please post **one** problem per question. not multiple ones. Additionally: "how can I access the service container directly in the commands" - please don't do that. Inject the services you need to the commands, not the whole container – Nico Haase Oct 17 '22 at 11:47
  • Finally migrating directly from Symfony 2.8 to Symfony 5 is a lot of work. Why not do this step by step, or use Rector? – Nico Haase Oct 17 '22 at 11:48
  • 1
    Injecting the `Container` into a command or service doesn't work like it used to in Symfony <= 3.4 and is [highly discourage](https://symfony.com/doc/3.4/components/dependency_injection.html#avoiding-your-code-becoming-dependent-on-the-container). Instead you [should utilize autowiring with the desired services](https://symfony.com/doc/current/service_container.html#explicitly-configuring-services-and-arguments) to be injected in the constructor explicitly type-hinted. eg: `__construct(LoggerInterface $logger)`. – Will B. Oct 17 '22 at 11:49
  • @NicoHaase I wondered for a long time to perform the update and I concluded that it was much easier to start over on a blank symfony 5 project because of the many obsolete bundles that I had to replace or modify. I use Rector to adjust the depreciations on my various files. – oracle972 Oct 17 '22 at 11:56
  • Please post your full `services.yaml` file content. – Will B. Oct 17 '22 at 11:58
  • @WillB. ok thank you i will use the autowriting with the desired services. So that means that I have to declare my 36 orders in the services.yml? Ok I updated the post with my services.yml – oracle972 Oct 17 '22 at 12:06
  • 1
    @oracle972 I am in favor of jumping directly to your final version instead of the incremental approach but you can't do things half way. Run `bin/console make:command DummyCommand` to see what a 5.4 command looks like and then convert yours. With autowire properly setup you should not need any command specific configuration in services.yaml. And as long as you are taking the time to go to 5.4 you should probably at least consider 6.x. The 6.4 LTS version is only a year away. – Cerad Oct 17 '22 at 12:11
  • 1
    No autowiring (auto-wire) will wire the services together for you, if you're using the `_defaults` as in the [Symfony default services.yaml file](https://symfony.com/doc/current/service_container.html#service-container-services-load-example) - I saw the comment, but you had services declared above it, which is a common issue when migrating. All your custom services should be below the `resource: '../src/'` declaration, which attempts to create a service for every file under `src/` See: [Commands as Services](https://symfony.com/doc/current/console/commands_as_services.html) – Will B. Oct 17 '22 at 12:11
  • @Cerad indeed, very good idea, I will test this solution and I will make the necessary modifications. Some of my bundles are not compatible with symfony 6, that's why I'm staying on version 5.4 for the moment – oracle972 Oct 17 '22 at 12:19
  • @WillB. also have all the services of my different bundles which are declared in my services.yml thanks to imports. I actually have the _default config in my services.yml as well. I'm going to find out about this feature because I didn't understand everything – oracle972 Oct 17 '22 at 12:27
  • Yea, your `services.yaml` is borked and it doesn't look like you finished [migrating the bundles to the new directory structure](https://symfony.com/doc/5.4/bundles.html) Generally speaking, it will take multiple days to fully migrate and test out all of the changes. This is why some people find it easier to incrementally upgrade, because of the many many many changes to how things work and things being removed. – Will B. Oct 17 '22 at 12:27
  • Please change the services example in the question, Use select all and copy/paste the services.yaml into a single code block, so that we have a ***verbatim copy***, because the order of operations matter. Also what namespace are the Commands under? Since you are using Bundles, you will most likely want to migrate from `config/services.yaml` to [loading each Bundle's services using an Extension](https://symfony.com/doc/5.4/bundles/extension.html), each with their own `services: { _defaults: autowire: true, autoconfigure: true }` declaration, due to the way Symfony Flex (4.0+) loads services. – Will B. Oct 17 '22 at 12:52
  • ok i updated the service file in the post and copied all the content. My command is in the WORD\AlertBundle\Command namespace; – oracle972 Oct 17 '22 at 13:16
  • Is the `WORD` namespace assigned to the `src/` directory or elsewhere? Additionally, please upload your composer.json autoload configuration, so we can see how your mappings are supposed to look. eg: `"psr-4": { "WORD\\": "src/WORD" }` or `"psr-4": { "WORD\\": "src/" }`, please include the `classmap` if it is used. – Will B. Oct 17 '22 at 13:25
  • I think that's where the problem really comes from. I had added WORD in the composer.json. I had not added my WORD directory in the services.yml so the auto wiring did not take it into account – oracle972 Oct 17 '22 at 13:33

1 Answers1

4

Namespace PSR-4 mapping

In order for composer to properly map out your files to their associated namespaces, the namespace and path must be declared in the autoload.psr-4 and/or autoload.classmap composer.json configuration.

composer.json autoload mappings

    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "W2D\\": "W2D/",
            "WORD\\": "WORD/"
        },
        "classmap": {
            "src/",
            "W2D/",
            "WORD/"
        }
    },

Convert Commands to be Lazy Loaded

First to reduce configuration and instantiation overhead, make all of your commands lazy loaded by declaring the name using protected static $defaultName = '.....'.

Repeat the changes for each command in the namespace.

Command changes

// /WORD/AlertBundle/Command/AlertExpirationEmailCommand.php
namespace WORD\AlertBundle\Command;

class AlertExpirationEmailCommand extends Command
{
    protected static $defaultName = 'WORD:alert:expiration_email';

    protected function configure()
    {
        $this
            /* ->setName('WORD:alert:expiration_email') */
            // ...
    }
}

Fix Service Loading

All services in Symfony 3.4+ are public: false by default (including the Logger) making them inaccessible by using container->get('service'). Additionally if a service is not explicitly injected in a service configuration or by autowire, the services are removed from the Container to reduce overhead. Because of this, you should never inject the entire container into a service - since the service you may be looking for may have inadvertently been removed.

Due to the way services are handled, _defaults should be the first line of any services declaration. It is also important to note, that _defaults will not be inherited/cascaded by any other file or configuration imports.

Since App\ is the default namespace being autowired, you will need to replace it with yours to ensure that the appropriate namespace(s) are being loaded as services for autowire.

Application services autowire

# /config/services.yaml
services:
# default configuration for services in *this* file
    _defaults:
        autowire: true      
        autoconfigure: true

    # makes classes in App/ 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/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # makes classes in WORD/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    WORD\: # <--- root namespace to autowire eg: WORD\Command\MyCommand
        resource: '../WORD/' # location of the namespace
        exclude:
            - '../WORD/DependencyInjection/'
            - '../WORD/Entity/'
            - '../WORD/*Bundle/' # Never autowire Bundles as they should be self-contained

    # declare manually wired and overridden services below this line
    # ...

Fix Bundle Configurations to be self-contained

With the introduction of autowiring and changes to order of operations in Symfony Flex (4.0+), loading Bundle services in the main application config/services.yaml will often cause conflicts in loading. Because of this it is best to use an Extension for each bundle to load the services configurations, until you migrate away from the Bundle file hierarchy in favor of using a flat application configuration.

Again repeat the changes for each registered Bundle.

Ultimately it is best-practice to explicitly define all services within the Bundles and NOT to use autowire: true. However, considering that this is a migration from 2.8 to 5.x, you will want to refactor away from using Bundles.

Bundle services.yaml and autowire

# /WORD/AlertBundle/Resources/config/services.yml
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.

    # makes classes in WORD/AlertBundle/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    WORD\AlertBundle\:
        resource: '../../*'
        exclude:
            - '../../DependencyInjection/'
            - '../../Entity/'

    # Bundle specific services below this line
    # ...

Bundle Extension

/* /WORD/AlertBundle/DependencyInjection/WORDAlertExtension.php */
namespace WORD\AlertBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class WORDAlertExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__ . '/../Resources/config')
        );
        $loader->load('services.yml');
    }
}

Manual Bundle Extension Loading
When not using the Bundle file hierarchy naming conventions, you may need to manually instantiate the Bundle's extension in the Bundle::getContainerExtension() method.

/* /WORD/AlertBundle/WordAlertBundle.php */
namespace WORD\AlertBundle;

// ...
use WORD\AlertBundle\DependencyInjection\AlertExtension;

class WordAlertBundle extends Bundle
{
    public function getContainerExtension()
    {
        return new AlertExtension();
    }
}

Container Access and Autowire

As mentioned above, as of Symfony 3.4+ you should never inject the entire container directly into a service.
Instead, the desired services should be explicitly type-hinted in the __construct() method of the class, which will be injected with the autowire: true services configuration.

# EXAMPLE ONLY
#
# /WORD/AlertBundle/Resources/config/services.yml
services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true

    # example autowire and autoconfig
    WORD\AlertBundle\:
        resource: '../../*'

    # EXAMPLE - Service Override Declaration
    WORD\AlertBundle\Service\EmployeeManager:
        arguments:
            - '@fos_user.util.password_updater'
            - '@fos_user.util.canonical_fields_updater'
            - '@fos_user.object_manager'
            - 'WORD\UserBundle\Entity\User'

    # EXAMPLE - declaration of alias
    word.manager.alert_employee:
        alias: WORD\AlertBundle\Service\EmployeeManager
        public: true # to prevent deletion of service when not used

    # EXAMPLE OVERRIDE - manually declare all files under Command as a command service
    WORD\AlertBundle\Command\:
        resource: '../../Command/*'
        tags:
            - { name: 'console.command' } # force as a command
// /WORD/AlertBundle/Command/AlertExpirationEmailCommand.php
namespace WORD\AlertBundle\Command;
// ...

use Psr\Log\LoggerInterface;
use WORD\AlertBundle\Service\EmployeeManager;

class AlertExpirationEmailCommand extends Command
{
    protected static $defaultName = 'WORD:alert:expiration_email';

    private LoggerInterface $logger;

    private EmployeeManager $manager;
     
    public function __construct(LoggerInterface $logger, EmployeeManager $manager) 
    {
        $this->logger = $logger;
        $this->manager = $manager;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $days = intval($input->getOption('days'));

        if ($days > 0) {
            try {
                $this->getManager()->sendExpirationEmail($days);
            } catch (\Exception $e) {
                $this->logger->error(sprintf(
                    'aemCommand: an error occurred in file "%s" (L%d): "%s"',
                    $e->getFile(),
                    $e->getLine(),
                    $e->getMessage()
                ));
            }
        }

        return 0;
    }

    /**
     * @return EmployeeManager
     */
    private function getManager(): EmployeeManager
    {
        return $this->manager;
    }
}

Moving Forward

Go through each of the service classes that uses the container directly and refactor them to use the explicit service type-hints or manual wiring in their respective services.yml configurations.

You should aim to migrate away from the 2.x - 3.4 Bundle file hierarchy and naming conventions and port to a flat configuration such as moving WORD\AlertBundle to WORD\Alert\... or WORD\Command\Alert\.... This way you have a single cohesive application, as opposed to several mini-applications as Bundles, significantly reducing code complexity, overhead, page loads, and cache warm-up times.

Alternatively, if each Bundle provides functionality shared across several of your applications, move each Bundle to their own repositories, following the Bundle best-practices, so they are segregated from the main application. Which will require another directory refactoring.

Will B.
  • 17,883
  • 4
  • 67
  • 69
  • 1
    Thank you very much for all these explanations, I understand much better how it works. I still have a lot of work to do but I'm on the right path now – oracle972 Oct 18 '22 at 07:22