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.
Related
Hey Im trying API Platform with Symfony 6.0 (and PHP 8)
Everything was going alright until I needed to make a DataPersister so I can encrypt the user password before saving it
I literally copied the example in the docs (here https://api-platform.com/docs/core/data-persisters/#decorating-the-built-in-data-persisters) since my entity is actually called User:
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\User;
final class UserDataPersister implements ContextAwareDataPersisterInterface
{
private $decorated;
public function __construct(ContextAwareDataPersisterInterface $decorated)
{
$this->decorated = $decorated;
}
public function supports($data, array $context = []): bool
{
return $this->decorated->supports($data, $context);
}
public function persist($data, array $context = [])
{
$result = $this->decorated->persist($data, $context);
return $result;
}
public function remove($data, array $context = [])
{
return $this->decorated->remove($data, $context);
}
}
I just removed the mailer parts cause what Im trying to do has nothing to do with that. Other than that, it is exactly equal to the example
But it wont work. I get this error when I try to persist:
Cannot autowire service "App\DataPersister\UserDataPersister": argument "$decorated" of method "__construct()" references interface "ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface" but no such service exists. Try changing the type-hint to "ApiPlatform\Core\DataPersister\DataPersisterInterface" instead.
I tried doing what the error suggests but it seems to throw the framework in some endless loop or something cause I get a memory error. And in any case, I need a ContextAwareDataPersisterInterface
Am I doing something wrong or missing something here? Or this a bug? The docs says:
"If service autowiring and autoconfiguration are enabled (they are by default), you are done!"
They are both enabled in 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.html#use-parameters-for-application-configuration
parameters:
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 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/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
I works if I explicity define the service in services.yaml:
App\DataPersister\UserDataPersister:
bind:
$decorated: '#api_platform.doctrine.orm.data_persister'
edit: sorry, the documentation actually says we have to do that, I missed it. My bad.
Problem solved
To parse phone number I need to use libphonenumber.phone_number_util in my controller ( Symfony 4) as like as :
$parsed = $this->get('libphonenumber.phone_number_util')->parse($phoneNo);
as we have libphonenumber.phone_number_util in private I wanted to make it public by adding this helper in service as below:
services:
libphonenumber\PhoneNumberUtil:
alias: libphonenumber.phone_number_util
public: true
But this returns Exception and message:
"message": "The \"libphonenumber.phone_number_util\" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.",
"class": "Symfony\\Component\\DependencyInjection\\Exception\\ServiceNotFoundException",
If you are using this in a controller method (which I presume you do based on $this->get(...)), you need to
1) Declare your controller as a service and tag it with controller.service_arguments tag
2) Make sure your util service id matches the class name (I suppose it does already). You don't need it to be public - that's and ancient approach
3) Require the util as a parameter to your controller's action method.
E.g.
services:
libphonenumber\PhoneNumberUtil:
alias: libphonenumber.phone_number_util
AppBundle\Controller\MyController:
tags: ['controller.service_arguments']
and
public function validatePhoneAction(Request $request, PhoneNumberUtil $phoneNumberUtil)
{
...
$phoneNumberUtil->parse($request->request->get('phone_number');
...
}
There is a nice Symfony blog post about these changes in dependency management: https://symfony.com/blog/new-in-symfony-3-4-services-are-private-by-default
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.
In Symfony 4.3 using Monolog, I have created a custom handler to push logs to AWS Firehose. Here is the constructor:
class FirehoseLogHandler extends AbstractProcessingHandler {
public function __construct(
FirehoseClient $firehoseClient,
FormatterInterface $formatter,
$streamName,
$level = Logger::INFO,
$bubble = true
) {
$this->firehoseClient = $firehoseClient;
$this->streamName = $streamName;
$this->formatter = $formatter;
parent::__construct($level, $bubble);
}
And here is my monolog.yaml config:
monolog:
handlers:
firehose_handler:
type: service
id: kinesis_stream_handler
main:
type: stream
handler: firehose_handler
level: error
channels: ["!event"]
services:
kinesis_stream_handler:
class: App\Logger\Handler\FirehoseLogHandler
arguments:
$firehoseClient: '#aws.firehose'
$formatter: '#App\Logger\Formatter\FirehoseLogFormatter'
$streamName: 'firehose-stream-test'
The problem that I am having is that the $level is always set to Logger::INFO (200), or whatever is set in the constructor. It seems to be ignoring what is set in the yaml config.
What am I doing wrong here? I know I could always add $level: 400 to the service declaration but that doesn't seem to make much sense. Appreciate any help in advance.
Handlers defined as type: service are used strictly as-is, since they are instances you have already constructed. They are not passed any arguments from the main monolog configuration; that would only apply to services it is constructing for you. So you do need to add $level as an explicit argument to your custom handler for this reason.
There may be some further confusion stemming from your main handler definition. While handler is a valid configuration key, it does not apply to a stream handler as it only makes sense for handlers that wrap others, such as filter. So that is simply being ignored.
The full list of handler types and what configuration keys actually apply to each can be found in the code here.
I have a service which takes a driver to do the actual work. The driver itself is within the context of Symfony 2 is just another service.
To illustrate a simplified version:
services:
# The driver services.
my_scope.mailer_driver_smtp:
class: \My\Scope\Service\Driver\SmtpDriver
my_scope.mailer_driver_mock:
class: \My\Scope\Service\Driver\MockDriver
# The actual service.
my_scope.mailer:
class: \My\Scope\Service\Mailer
calls:
- [setDriver, [#my_scope.mailer_driver_smtp]]
As the above illustrates, I can inject any of the two driver services into the Mailer service. The problem is of course that the driver service being injected is hard coded. So, I want to parameterize the #my_scope.mailer_driver_smtp.
I do this by adding an entry to my parameters.yml
my_scope_mailer_driver: my_scope.mailer_driver_smtp
I can then use this in my config.yml and assign the parameter to the semantic exposed configuration [1]:
my_scope:
mailer:
driver: %my_scope_mailer_driver%
In the end, in the Configuration class of my bundle I set a parameter onto the container:
$container->setParameter('my_scope.mailer.driver', $config['mailer']['driver'] );
The value for the container parameter my_scope.mailer.driver now equals the my_scope.mailer_driver_smtp that I set in the parameters.yml, which is, as my understanding of it is correct, just a string.
If I now use the parameter name from the container I get an error complaining that there is no such service. E.g:
services:
my_scope.mailer:
class: \My\Scope\Service\Mailer
calls:
- [setDriver, [#my_scope.mailer.driver]]
The above will result in an error:
[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
The service "my_scope.mailer" has a dependency on a non-existent service "my_scope.mailer.driver"
The question now is, what is the correct syntax to inject this container parameter based service?
[1] http://symfony.com/doc/current/cookbook/bundles/extension.html
This question has a similar answer here
I think the best way to use this kind of definition is to use service aliasing.
This may look like this
Acme\FooBundle\DependencyInjection\AcmeFooExtension
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration;
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yml');
$alias = $config['mailer']['driver'];
$container->setAlias('my_scope.mailer_driver', $alias);
}
This will alias the service you've defined in my_scope.mailer.driver with my_scope.mailer_driver, which you can use as any other service
services.yml
services:
my_scope.mailer_driver:
alias: my_scope.mailer_driver_smtp # Fallback
my_scope.mailer_driver_smtp:
class: My\Scope\Driver\Smtp
my_scope.mailer_driver_mock:
class: My\Scope\Driver\Mock
my_scope.mailer:
class: My\Scope\Mailer
arguments:
- #my_scope.mailer_driver
With such a design, the service will change whenever you change the my_scope.mailer_driver parameter in your config.yml.
Note that the extension will throw an exception if the service doesn't exist.
With service container expression language you have access to the following two functions in config files:
service - returns a given service (see the example below);
parameter - returns a specific parameter value (syntax is just like service)
So to convert parameter name into a service reference you need something like this:
parameters:
my_scope_mailer_driver: my_scope.mailer_driver_smtp
services:
my_scope.mailer:
class: \My\Scope\Service\Mailer
calls:
- [setDriver, [#=service(parameter('my_scope_mailer_driver'))]]
At first I thought this was just a question of getting the # symbol passed in properly. But I tried assorted combinations and came to the conclusion that you can't pass an actual service as a parameter. Maybe someone else will chime in and show how to do this.
So then I figured is was just a question of using the service definition and passing it a reference. At first I tried this in the usual extension but the container does not yet contain all the service definitions.
So I used a compiler pass: http://symfony.com/doc/current/cookbook/service_container/compiler_passes.html
The Pass class looks like:
namespace Cerad\Bundle\AppCeradBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class Pass1 implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
// Set in the Extension: my_scope.mailer_driver_smtp
$mailerDriverId = $container->getParameter('my_scope.mailer.driver');
$def = $container->getDefinition('my_scope.mailer');
$def->addMethodCall('setDriver', array(new Reference($mailerDriverId)));
}
}
Take the calls section out of the service file and it should work. I suspect there is an easier way but maybe not.
#my_scope.mailer.driver needs to be a service but not defined as service. To retrieve string parameter named as my_scope.mailer.driver you need to wrap it with %: %my_scope.mailer.driver%.
So you need to pass #%my_scope.mailer.driver% as parameter to a service. Yml parser will replace %my_scope.mailer.driver% with the appropriate value of the parameter and only then it will be called as a service.