Symfony 3.4 - Autowire not working for bind values? - symfony

I am working on migrating an existing Symfony 2.8 project to Symfony 3.4 and would like to autowire / auto inject the value of the %kernel.environment% parameter into a controller action.
However binding the parameter to a argument name does not work:
// app/config/services.yml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
bind:
$kernelEnvironment: '%kernel.environment%'
MyBundle\:
resource: '../../src/MyBundle/*'
exclude: '../../src/MyBundle/{Entity,Repository,Tests}'
MyBundle\Controller\:
resource: '../../src/MyBundle/Controller'
tags: ['controller.service_arguments']
// src/MyBundle/SomeController.php
public function showPostAction($kernelEnvironment, $post_id) {
...
}
Uncaught PHP Exception RuntimeException: "Controller "MyBundle\Controller\SomeController::showPostAction()" requires that you provide a value for the "$kernelEnvironment" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one."
Why does this not work?
I know that I could specify MyBundle\Controller\SomeController directly within in the services.yml and set the value of $kernelEnvironment in the arguments list. However this solution would mean that I would have to specify the argument for every controller that uses this parameter. This is is not what autorwire is good for. Binding the argument should work, shouldn't it?

Related

Symfony 3.4 inject service inside another fail

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.

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}'

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