Symfony 3.4 inject service inside another fail - symfony

I am doing a service in Symfony and I need to inject my mail service. The mail service depends on a parameter:
app.email:
class: AppBundle\Services\EmailNotificationService
autowire: false
public: true
arguments:
- '#doctrine.orm.entity_manager'
- '#swiftmailer.mailer'
- '#templating.engine.twig'
- '%mailer_user%'
- '#logger'
It works to inject it into one service:
app.booking:
class: AppBundle\Services\BookingService
autowire: false
public: true
arguments:
- '#doctrine'
- '#pay.pay'
- '#app.calendar'
- '#app.email'
- '#app.push'
but when I include it in another:
services:
app.servicios_extras:
class: AppBundle\Services\ServiciosExtrasService
autowire: false
public: true
arguments:
- '#app.email'
...
class ServiciosExtrasService
{
private $mailer;
public function __construct(EmailNotificationService $mailer)
{
$this->mailer = $mailer;
}
it gives me the error:
Cannot autowire service "AppBundle\Services\EmailNotificationService": argument "$mailerUser" of method "__construct()" has no type-hint, you should configure its value explicitly.
I'm missing something?
UPDATE
app/config/services.yml:
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
public: false

I don't normally answer these sorts of questions because there are a number of similar questions with the same answer. But I could not find one that could be considered to be a duplicate.
The short answer is to add the Services directory to the list of excluded directories:
#app/config/services.yml
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests,Services}'
Doing so prevents autowire from trying to create a service with an id of AppBundle\Services\EmailNotificationService and then failing because $emailUser is a string.
The Symfony container has moved away from explicitly naming services with things like app.email. Instead, the class name (AppBundle\Services\EmailNotificationService) is preferred. So autowire finds all classes under src, looks in container to see if a service has been defined with the same class name and, if none is found, creates a new service. Autowire does not link app.email with your email class. Unless you are supporting a legacy project then it might be best to do away with app.* service names.
And, as a bonus, by going with class names you can eliminate all the service configuration except for a bind under the _default section. But it looks like you do have a legacy project in which case you should probably turn off autowire under _default just to preserve your own sanity.

Related

Symfony 4 services local binding in different environments

I have to bind parameters with different values in different environments, and having problems with this.
I was trying this:
# config/services.yaml
services:
_defaults:
bind:
$param: 'param for PROD'
# config/services_dev.yaml
services:
_defaults:
bind:
$param: 'param for DEV'
# src/Controller/SomeController.php
class MyController extends AbstractController
{
public function example($param)
{
echo $param;
}
}
But it forces me to have all the services defined in both of services.yaml and services_dev.yaml files, otherwise it does not work.
I would like to have a services.yaml shared for any environment, and only override the custom services/bindings etc, not have two identical files with all services listed in them for changing one binding value.
The real problem is that I have to create two http clients (real and a dummy) with same interface, in production load the real one, and in development load the dummy, Symfony 4-s autowiring allows me to inject the interface in a controller and choose which client to use in binding:
# config/services.yaml
services:
_defaults:
bind:
'ClientInterface': '#real_client'
# More services here...
# config/services_dev.yaml
services:
_defaults:
bind:
'ClientInterface': '#dummy_client'
# Here I don't want to have another copy of the services,
# but it does not work without them
# Controller
public function someMethod(ClientInterface $client)
{
// ...
}
In Symfony 2 I was able to extend services.yml and in services_dev.yml only define the specific values I wanted to override/add, but in Symfony 4 services_dev.yaml can not use services from services.yaml and I have to keep my services identical in two different files which is pain.
Anny suggestions?
Thank you.
I'm updating the post again with a real example:
services.yaml
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: 'en'
app.access_token: '%env(string:APP_ACCESS_TOKEN)%'
app.aws_version: '%env(string:AWS_VERSION)%'
app.aws_profile: '%env(string:AWS_PROFILE)%'
app.aws_region: '%env(string:AWS_REGION)%'
app.aws_queue_url_creation: '%env(string:AWS_QUEUE_URL_CAMPAIGN_CREATION)%'
app.aws_queue_url_edition: '%env(string:AWS_QUEUE_URL_CAMPAIGN_EDITION)%'
app.redis_host: '%env(string:REDIS_HOST)%'
app.redis_port: '%env(string:REDIS_PORT)%'
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.
bind:
App\Service\MessageSenderServiceInterface: '#App\Service\MessageSenderSqsService'
# 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/{DependencyInjection,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
# Authenticators
App\Security\ApiKeyAuthenticator:
arguments:
- "%app.access_token%"
# Clients
App\Client\AwsSqsClient:
arguments:
- "%app.aws_version%"
- "%app.aws_profile%"
- "%app.aws_region%"
App\Client\RedisClient:
arguments:
- "%app.redis_host%"
- "%app.redis_port%"
# Services
App\Service\MessageSenderSqsService:
arguments:
- '#App\Client\AwsSqsClient'
- '#App\Client\RedisClient'
- "%app.aws_queue_url_creation%"
- "%app.aws_queue_url_edition%"
App\Service\MessageSenderRedisService:
arguments:
- '#App\Client\RedisClient'
services_dev.yaml
imports:
- { resource: services.yaml }
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.
bind:
App\Service\MessageSenderServiceInterface: '#App\Service\MessageSenderRedisService'
Controller.php
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class TestController extends AbstractController
{
/**
* #Route("/api/dummy")
*/
public function dummyEndpoint(MessageSenderServiceInterface $messageSender)
{
echo get_class($messageSender); exit;
}
}
And the echo from controller for both envs (prod and dev) is
App\Service\MessageSenderSqsService
But if I copy whole node "services" form services.yaml to services_dev.yaml and only change the binding config, it works fine and says that the injected class is:
App\Service\MessageSenderRedisService
I've just noticed that if I don't touch the "_defaults" node, it works as expected, the problems start only when I want to override the _defaults node of services...
You can define parameters in parameters section of config.yml and overwrite this parameters in config_dev.yml.
# config.yml
imports:
# ...
parameters:
parameter_1: value 1
parameter_2: value 2
# ...
framework:
# ...
# config_dev.yml
imports:
# ...
parameters:
parameter_1: dev value 1
# ...
framework:
# ...
This parameters can be used used in service.yml as:
# service.yml
services:
_defaults:
bind:
$param: '%parameter_1%'
Finally the problem was only in overriding the "_defaults" node (which I was touching in order to have different "bind" configs in the project).
Extending services.yaml without overriding _defaults, everything works as expected. And the solution is to have different configuration for services with their bindings by environment, and have "_defaults" only in services.yaml.
If we override the "_defaults" in other files, we'll have to redefine all the services too.
Thanks everyone for help.
You have some options:
1.Don't use bind and write different service configs for different environments
# services.yaml
App\Controller:
arguments:
- "#client"
# services_dev.yaml
App\Controller:
arguments:
- "#dummy_client"
2.Use bind and create service alias in each environment's services.yaml:
# services.yaml
services:
some.client:
alias: "#client"
# services_dev.yaml
services:
some.client:
alias: "#dummy_client"
3.Just configure only one ClientInterface service per environment:
# services.yaml
App\ClientInterface:
class: App\RealClient
# services_dev.yaml
App\ClientInterface:
class: App\DummyClient
4.Use factory which will create this client depends on environment (but this is not very good practice as for me)
# services.yaml
App\ClientInterface:
factory: ["#App\ClientFactory", create]
arguments:
- '%kernel.environment%'
class ClientFactory
{
public function create(string $env): ClientInterface
{
if ($env === 'dev') {
return new DummyClient();
} else {
return new Client();
}
}
}
5.In your case, when you have so much services and you want to inject same service in all of them, you can use option #3 or you can create one interface for all of them and use _instanceof:
# services.yaml
_instanceof:
App\SomeCommonInterface:
calls:
- method: setSomeService # interface's method
arguments:
- '#service'
# services_dev.yaml
_instanceof:
App\SomeCommonInterface:
calls:
- method: setSomeService
arguments:
- '#dummy_service'

Service autowire don't work with Symfony 4

I try to pass from Symfony 3.4 to Symfony 4.1, but I've a problem with autowire. I've the symfony/swiftmailer-bundle installed, and in an event subscriber I have:
public function __construct(\Swift_Mailer $mailer, EngineInterface $templating, EntityManagerInterface $em, $senderMail, $senderName)
{
$this->mailer = $mailer;
$this->templating = $templating;
$this->em = $em;
$this->senderMail = $senderMail;
$this->senderName = $senderName;
}
In the service.yaml:
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: 'en'
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
# Twig
twig.extension.text:
class: Twig_Extensions_Extension_Text
tags:
- { name: twig.extension }
# Listeners
App\EventListener\ContactNotificationSubscriber:
$senderMail: '%env(MAILER_SENDER_ADDRESS)%'
$senderName: '%env(MAILER_SENDER_NAME)%'
But I've an error:
Cannot autowire service "App\EventListener\ContactNotificationSubscriber": argument "$mailer" of method "__construct()" references class "Swift_Mailer" but no such service exists.
I don't understand why... The component exists, with PhpStorm, I can click on \Swift_Mailer and see the class, but Symfony always return to me an error...
If someone know why :-) Thanks a lot
I was having the same problem. For my case, the bundle was not being included in bundles.php. Adding the following in bundles.php solved it for me:
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
You can change
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
to :
App\:
resource: '../src/*'
exclude: '../src/{Entity,EventListener,Migrations,Tests,Kernel.php}'

monolog.logger.db service has been removed

I'm trying to refactor some Symfony 3 code to Symfony 4.
I am getting the following error when attempting to log:
The "monolog.logger.db" service or alias has been removed or inlined
when the container was compiled. You should either make it public, or
stop using the conta iner directly and use dependency injection
instead.
My logging code:
$logger = $container->get('monolog.logger.db');
$logger->info('Import command triggered');
Monolog config:
monolog:
channels: ['db']
handlers:
db:
channels: ['db']
type: service
id: app.monolog.db_handler
app.monolog.db_handler config (Note, I tried public: true here and it had no affect:
app.monolog.db_handler:
class: App\Util\MonologDBHandler
arguments: ['#doctrine.orm.entity_manager']
How can I get this wired up correctly in Symfony 4?
By default all services in Symfony 4 are private (and is the recommended pratice) so you need to "inject" in each Controller each needed service (personally I use a custom CommonControllerServiceClass).
You can also create a public service "alias" to continue accessing the service as you did, but it's not the best pratice to follow (also because I guess you will have many other services to fix).
mylogger.db:
alias: monolog.logger.db
public: true
then you can get the service from the container:
$logger = $container->get('mylogger.db');
Alister's answer is a good start, but you can utilise service arguments binding instead of creating a new service for each logger:
services:
_defaults:
autowire: true
bind:
$databaseLogger: '#monolog.logger.db'
Then just change the argument name in your class:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $databaseLogger = null) {...}
It appears that App\Util\MonologDBHandler may be the only thing that is actively using monolog.logger.db - via a container->get('...') call. (If not, you will want to use this technique to tag the specific sort of logger into more services).
You would be better to allow the framework to build the app.monolog.db_handler service itself, and use the container to help to build it. Normally, to inject a logger service, you will just need to type-hint it:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $logger = null) {...}
However, that will, by default, setup with the default #logger, so you need to add an extra hint in the service definition of the handler that you want a different type of logger:
services:
App\Log\CustomLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: db }
Now, the logger in CustomLogger should be what you had previously known as monolog.logger.db.
You can also alias a different interface (similar to how the LoggerInterface is aliased to inject '#logger') to the allow for the tagging.

SymfonyBundle: How to pass to a controller as a service the full bundle configuration

I've created a bundle completely separate from the main app, so I install it via Composer.
This bundle requires some sort of configuration:
# app/config/config.yml
shq_mybundle:
node1:
node1_1:
...
node1_2:
...
node2:
node2_1:
...
node2_2:
...
My bundle also has a controller MyBundleAController and this controller has a ––construct() with this signature:
class MyBundleAController
{
public function __construct(EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher, array $config)
{
$this->entityManager = $entityManager;
$this->eventDispatcher = $eventDispatcher;
$this->config = $config;
}
}
My bundle also loads a services.yml file that uses autowiring to configure controllers:
services:
# default configuration for services in *this* file
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to d
SerendipityHQ\Bundle\MyBundle\Controller\:
resource: '../../Controller/*'
public: false
tags: ['controller.service_arguments']
Obviously, the way MyBundleAController is configured throws an error, as the $config argument is unknown by the autowiring functionality that requires the argument be typehinted or explicitly set:
Cannot autowire service
"SerendipityHQ\Bundle\MyBundle\Controller\MyBundleAController":
argument "$config" of method "__construct()" must have a type-hint or
be given a value explicitly.
And here we arrive to my question: the $config parameter is the one that someone sets in its app/config/config.yml, so this one:
# app/config/config.yml
shq_mybundle:
node1:
node1_1:
...
node1_2:
...
node2:
node2_1:
...
node2_2:
...
How can I pass shq_mymodule configuration to the autowired controller?
In a first attempt, I've tried to do something like this
SerendipityHQ\Bundle\MyBundle\Controller\ConnectController:
arguments:
$config: "%shq_mybundle%"
But obviously this doesn't work.
To make it work I should do something like this in MyBundleExtension:
$container->setParameter('shq_mybundle', $config);
This way I transform it in a parameter accessible from the services.yml file that can use it to autowire the MyBundleAController controller.
But this seems to me like an hack: is there a more elegant way to do this?

Symfony 3.3.6 change to service auto loading from 3.2

I updated to 3.3.6 yesterday and in setting up a new service I have created in AppBundle. Old style of service registration works - when I try the new approach it is not.
I have a service
AppBundle/Service/SomeService.php
namespace AppBundle\Service;
class SomeService {
}
services.yml
services:
# THIS WORKS
#app.some_service:
# class: AppBundle\Service\SomeService
# arguments: []
_defaults:
autowire: true
autoconfigure: true
public: false
AppBundle\:
resource: '../../src/AppBundle/*'
Inside my action I try and use the service:
$someService = $this->get(SomeService::class);
// THIS WORKS
//$someService = $this->get('app.some_service');
I get the following error:
You have requested a non-existent service
"AppBundle\Service\SomeService".
What am I missing or not understanding about the changes made to 3.3?
I feel it's likely something I missed due to upgrading from 3.2 to 3.3+
Ideas?

Resources