How to configure group of services by namespace in Symfony - php

In the default Symfony 5 config, I looking this:
services:
...
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Exception,Migrations,Tests,Kernel.php}'
...
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
It looks very cool. As if I can configure some default settings for a group of services by namespace, like that
...
App\Service\MySpecialTaskServices\:
resource: '../src/Service/MySpecialTaskServices/*'
public: true
... other default params\args for this NS ...
But this stuff not work: configuration loaded correctly, but parameters can not be applied.
May be I doing something wrong? It's bad idea, clogging services.yaml with a bunch of services with the same params.

I found a good reason, with using custom tags functionality. Maybe it help someone else.
Instantiate you services in group from single interface/abstract
Add _instanceof option in services.yml in global services: block
services:
_default:
....
_instanceof:
App\Service\MySpecialTaskServices\MySpecialTaskInterface:
tags: ['app.public_service']
Define a custom compiler pass for support you new tag with this process method
class PublicServicesGroupPass implements CompilerPassInterface
{
...
public function process(ContainerBuilder $container): void
{
$services = $container->findTaggedServiceIds('app.public_service');
foreach (array_keys($services) as $id) {
$definition = $container->findDefinition($id);
$definition->setPublic(true);
// other works with params
}
}
...
}
And register them in your kernel
class Kernel extends BaseKernel
{
...
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new PublicServicesGroupPass());
}
...
}
And this working correct. But I would like to make it easier.

Related

Command not found

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"
}
},
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.

Controller has no container set, did you forget to define it as a service subscriber? Symfony 5

I'm making my first application on Symfony 5
I ran into a problem when creating a controller, and I need a controller, not a service
Uncaught PHP Exception LogicException: ""App\Controller\UploadFileController" has no container set, did you forget to define it as a service subscriber?" at /vendor/symfony/framework-bundle/Controller/ControllerResolver.php line 36
it is my simple Controller
class UploadFileController extends AbstractController
{
/**
* #Route("/test-upload", name="app_test_upload")
*/
public function testAction(Request $request, FileUploader $fileUploader)
{
return 1;
}
}
I tried to clean the cache, it didn't help
this is my services.yaml
services:
App\Controller\UploadFileController:
calls:
- [setContainer, ['#service_container']]
There is no need to add any releated information in your service.yaml file, you only need to edit your code in service.yaml file to be like that:
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']

Passing dynamic arguments to service factory in Symfony

I'm integrating Symfony into an older application having its own dependency container based on PSR-11. Been searching for a solution to merge that DI container to the one Symfony uses, but found nothing. To just make it work, I came with one "hacky" solution which I don't like.
I've created this class. It creates an instance of an old DI container inside of it:
class OldAppServiceFactory
{
private ContainerInterface $container;
public function __construct()
{
$this->container = OldContainerFactory::create();
}
public function factory(string $className)
{
return $this->container->get($className);
}
}
and added proper entries to services.yaml:
oldapp.service_factory:
class: Next\Service\LeonContainer\LeonServiceFactory
OldApp\Repository\Repository1:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\Repository\Repository1'
OldApp\Repository\Repository2:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\Repository\Repository2'
OldApp\configuration\ConfigurationProviderInterface:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\configuration\ConfigurationProviderInterface'
With above hack, putting those classes in service class constructors works. Unfortunately it looks bad and it'll be pain to extend it with more of those repositories (especially when having 50 of them). Is it possible to achieve something like this in services.yaml?
OldApp\Repository\:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- << PASS FQCN HERE >>
This would leave me with only one entry in services.yaml for a single namespace of the old application.
But, maybe there is other solution for my problem? Been trying with configuring Kernel.php and prepareContainer(...) method, but I also ended with nothing as the old dependencies are in one PHP file returning an array:
return array [
RepositoryMetadataCache::class => static fn () => RepositoryMetadataCache::createFromCacheFile(),
EntityCollection::class => autowire(EntityCollection::class),
'Model\Repository\*' => static function (ContainerInterface $container, RequestedEntry $entry) { ... }
];
You could probably accomplish this easily with a custom compiler pass.
First tag all the old repository classes by loading the directory where they exist:
OldApp\Repository\:
resource: '../src/OldApp/Repository/*'
autowire: false
autoconfigure: false
tags: ['oldapp_repository']
(I think that you may need to also exclude src/OldApp from the default automatic service loading. E.g.:
App\:
resource: '../src/*'
exclude: '../src/{OldApp/Repository,DependencyInjection,Entity,Tests,Kernel.php}'
... but I'm not 100% sure, test this one).
Then create a compiler pass to go through the tags and define a factory for each one:
class OldAppRepositoryCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$taggedServices = $container->findTaggedServiceIds('oldapp_repository');
foreach ($taggedServices as $serviceId => $tags) {
$definition = $container->getDefinition($serviceId);
$definition
->setFactory([new Reference('oldapp.service_factory'), 'factory'])
->addArgument($serviceId);
}
}
}
And in your Application Kernel build() method add the compiler pass:
// src/Kernel.php
namespace App;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
// ...
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new OldAppRepositoryCompilerPass());
}
}
Can't test this right at this minute, but this should get you going in the right direction. For additional details check the docs:
Working with service tags
You can check this example repo where the above is implemented and working. On this repo the OldApp namespace is outside of App and src, so no need to exclude it from automatic service loading.

Symfony4: inject service into controller __construct (constructor)

I am having trouble accessing an injected service in the constructor of one of my controllers.
Per http://symfony.com/doc/current/service_container/injection_types.html I believe I have done the injection correctly, however when I try to load a view from the controller, I get the following error:
Argument 1 passed to Regions\AnalyticsBundle\Controller\PatternsController::__construct()
must be an instance of Regions\AnalyticsBundle\Controller\PatternCacheService, instance of
Regions\AnalyticsBundle\Service\PatternCacheService given, called
in /var/tmp/symfony/cache/dev/ContainerLoHUcSH/getPatternsControllerService.php on line 9
It seems like the error indicates that the type hinting in the constructor is trying making it look for an instance in the *\Controller\* namespace instead of the *\Services\* namespace - what am I doing wrong or not seeing here?
Details of my setup are as follows...
Symfony 4.1.0, PHP 7.2.5
services.yaml
services:
...
pattern_cache_service:
class: Regions\AnalyticsBundle\Service\PatternCacheService
public: true
Regions\AnalyticsBundle\Controller\PatternsController:
arguments: ['#pattern_cache_service']
Controller:
namespace Regions\AnalyticsBundle\Controller;
class PatternsController extends BaseController
{
private $pcs;
public function __construct(PatternCacheService $pcs)
{
$this->pcs = $pcs;
}
}
You forgot a use in your Controller, making PHP think that your service is in the same namespace as your controller.
<?php
namespace Regions\AnalyticsBundle\Controller;
use Regions\AnalyticsBundle\Service\PatternCacheService;
class PatternsController extends BaseController
{
private $pcs;
public function __construct(PatternCacheService $pcs)
{
$this->pcs = $pcs;
}
}
This was actually raised as part of your error message
Argument 1 passed to Regions\AnalyticsBundle\Controller\PatternsController::__construct()
must be an instance of Regions\AnalyticsBundle\Controller\PatternCacheService
When what you expected was your controller to need an instance of Regions\AnalyticsBundle\Service\PatternCacheService
The class PatternCacheService cannot be found in the namespace Regions\AnalyticsBundle\Controller.
Add an import:
<?php
namespace Regions\AnalyticsBundle\Controller;
use Regions\AnalyticsBundle\Service\PatternCacheService;
class PatternsController extends BaseController
{
private $pcs;
public function __construct(PatternCacheService $pcs)
{
$this->pcs = $pcs;
}
}
For reference, see
http://php.net/manual/en/language.namespaces.importing.php
You don't need a service definition for pattern_cache_service. It should autowire your service if autowire: true is set.
PatternCacheService should be private as you don't want to access it from within container. Suggested practise!
You don't need a service definition for PatternsController either.
Note: You should not use "bundles" anymore in Symfony 4 so I would get rid of AnalyticsBundle.
Note: Better organise your configuration files as show here: Organising route, service and parameter configuration files in symfony 4 applications.
This should suffice:
services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,....so on .....,Kernel.php}'
App\Controller\:
resource: '../../src/Regions/AnalyticsBundle/Controller'
tags: ['controller.service_arguments']
PatternsController
namespace Regions\AnalyticsBundle\Controller;
use Regions\AnalyticsBundle\Service\PatternCacheService;
class PatternsController
{
private $pcs;
public function __construct(PatternCacheService $pcs)
{
$this->pcs = $pcs;
}
}

:q!Symfony4 Autowiring not wofking for own Utils

I'm trying to access TokenStorageInterface inside a Utils Class, since I need to reuse code and I don't want to mess with internal logic inside the controllers.
But I get the following error when accessing the TokenStorage Interface from my class.
Type error: Too few arguments to function App\Utils\Ability::__construct(), 0 passed
Here is my class (I'll post just the __construct since that's the relevant part) :
<?php
namespace App\Utils;
use Doctrine\ORM\EntityManagerInterface;
class Ability
{
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
}
Any clues?
PS: Autowiring is enabled
Adding config\services.yaml :
parameters:
locale: 'en'
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']

Categories