Service injection fails only on certain function of the same class - symfony

Moving from symfony 3 to 4 I started injecting the service according to this.
Most functions where I inject the "helper" service it works fine, but for some reason it fails on a few of them and I can't figure out what I've done differently... Here's the error:
Could not resolve argument $helper of
"App\Controller\Poslog\OperatorController::historyindividualaction()",
maybe you forgot to register the controller as a service or missed
tagging it with the "controller.service_arguments"?
I've omitted the contents if the functions completely as it doesn't matter what they contain, I can comment out the entire code inside the HistoryIndividualAction() and still get the exact same error message.
The OperatorController::newAction() below works perfectly fine (as well as a number of other functions within the same object), while the OperatorController::HistoryIndividualAction() fails.
use App\Service\globalHelper as globalHelper;
...
class OperatorController extends AbstractController
{
public function newAction(Request $request, ACRGroup $ACRGroup = null, globalHelper $helper)
{
...
}
public function HistoryIndividualAction($operatorId, globalHelper $helper)
{
...
}
}
The routing looks like this
operator_new:
path: /new/{ACRGroup}
defaults: { _controller: App\Controller\Poslog\OperatorController::newAction, ACRGroup: null }
methods: [GET, POST]
operator_history_individual:
path: /history/individual/{operatorId}
defaults: { _controller: App\Controller\Poslog\OperatorController::historyIndividualAction }
methods: GET
Services.yml
# 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
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'
# 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
globalHelper:
class: App\Service\globalHelper
public: false
arguments: ['#service_container', '#doctrine.orm.entity_manager']
GlobalHelper starts out like this:
namespace App\Service;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
//use Doctrine\ORM\EntityManager as EntityManager; //gave deprecation notice, profiler suggested to change to below and the deprecation notice disappeared.
use Doctrine\ORM\EntityManagerInterface as EntityManager;
class globalHelper {
private $container;
private $em;
public function __construct(Container $container, EntityManager $em) {
$this->container = $container;
$this->em = $em;
}

The basic problem is that method names are case sensitive and you had a mismatch.
// Change
public function HistoryIndividualAction(
// To
public function historyIndividualAction(
Normally I would just have called this a typo and moved on but the error is interesting. I would have expected perhaps an unknown method sort of message but Symfony would have dealt with that. The error comes from trying to process the method's arguments.
There is a huge sub-system behind Symfony's ability to inject action arguments. Not as simple as it may look and it involves caching information about the action signatures using the method name. And that is where the case mismatch becomes significant.
So I think it might be worth keeping this answer around just for developers who might encounter a similar error message.

Try sorting your arguments differently in your controller action.
public function newAction(Request $request, ACRGroup, globalHelper $helper, $ACRGroup = null) {
// ...
}
And try to clear the cache:
php bin/console cache:clear

Related

How to override translator in symfony 5.2

I'm trying to override translator class in Symfony 5.2. I tried this:
# config/services.yaml
services:
# ....
App\Translator:
decorates: translator
and this (App\Translator implements TranslatorInterface):
# config/services.yaml
services:
# ....
App\Translator:
arguments:
$translator: '#translator'
Symfony\Contracts\Translation\TranslatorInterface: '#App\Translator'
both methods work well in PHP code, but in development mode in the twig, translator service is still DataCollectorTranslator. So in twig templates the translator service remains not overridden. How can I fix it?
It's possible I am not understanding the question. If something works in one mode but not another then sometimes just deleting the var/cache directory and building a new cache with bin/console cache:clear might work.
Decorating services can be a bit interesting sometimes. I created a fresh 5.2 project and then added:
# src/Translation/Translation.php
namespace App\Translation;
use JetBrains\PhpStorm\Pure;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
// Uses PHP8 constructor promotion
public function __construct(private BaseTranslator $translator)
{
}
#[Pure]
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
//return $this->translator->trans($id,$parameters,$domain,$locale);
return strtoupper($id); // Verify calling this class
}
public function getCatalogue(string $locale = null): MessageCatalogueInterface
{
return $this->translator->getCatalogue($locale);
}
#[Pure]
public function getLocale(): string
{
return $this->translator->getLocale();
}
public function setLocale(string $locale)
{
$this->translator->setLocale($locale);
}
}
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\Translation\Translator:
decorates: translator
# index.html.twig
<li>{{ 'Hello' | trans }}</li>
You can disregard the Pure stuff as well as some of the PHP8 stuff. I was using this as a PHP8 test as well.
But it all seems to work as advertised.

Symfony 3.4 logger service

When I called logger service get this information message in log file it's worked but write this message in the log file:
php.INFO: User Deprecated: The "logger" service
is private, getting it from the container is deprecated since Symfony
3.2 and will fail in 4.0. You should either make the service public, or stop using the container directly and use dependency injection
instead. {"exception":"[object] (ErrorException(code: 0): User
Deprecated: The \"logger\" service is private, getting it from the
container is deprecated since Symfony 3.2 and will fail in 4.0. You
should either make the service public, or stop using the container
directly and use dependency injection instead. at
/home/****/###/PROJECT/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Container.php:275)"}
[]
My symfony version: 3.4.1
As stated in Symfony 3.4, the logger service provided by the MonologBundle and all other services, are set to private by default. [sic]
To workaround the issue, the recommended method is to use Dependency Injection.
http://symfony.com/doc/3.4/logging.html
namespace AppBundle\Controller;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction(LoggerInterface $logger)
{
$logger->info('Your Message');
}
}
Source code Reference: https://github.com/symfony/monolog-bundle/blob/v3.1.0/Resources/config/monolog.xml#L17
For service definitions Dependency Injection is available when autowire is enabled. [sic]
#app/config/services.yml
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 do this, you can override this setting on individual services
public: false
# makes classes in src/AppBundle available to be used as services
# this creates a service per class whose id is the fully-qualified class name
AppBundle\:
resource: '../../src/AppBundle/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
#enables dependency injection in controller actions
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
#all of your custom services should be below this line
#which will override the above configurations
#optionally declare an individual service as public
#AppBundle\Service\MyService:
# public: true
#alternatively declare the namespace explicitly as public
#AppBundle\Service\:
# resource: '../../src/AppBundle/Service/*'
# public: true
Then to Inject the Dependency into the service, you add the type hint for the argument to the constructor.
namespace AppBundle\Service;
use Psr\Log\LoggerInterface;
class MyService
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
}
if autowire is disabled, you can manually define your services to inject the logger alias.
#app/config/services.yml
services:
AppBundle\Service\MyService:
arguments: ['#logger']
public: true
Alternatively, to force the logger alias to be publicly accessible from the container, you can re-declare the service alias in your application services config.
#app/config/services.yml
services:
#...
logger:
alias: 'monolog.logger'
public: true
Instead of overriding the value in the configuration, you can also set logger as a public service in a compiler pass. https://symfony.com/doc/4.4/service_container/compiler_passes.html
Symfony Flex
// src/Kernel.php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container)
{
// in this method you can manipulate the service container:
// for example, changing some container service:
$container->getDefinition('logger')->setPublic(true);
}
}
Symfony Bundle
// src/AppBundle/AppBundle.php
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use AppBundle\DependencyInjection\Compiler\CustomPass;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new CustomPass());
}
}
// src/AppBundle/DependencyInjection/Compiler/CustomPass.php
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CustomPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container->getDefinition('logger')->setPublic(true);
}
}
$this->container->get('logger') fails because logger is now (as of 3.2) marked as a private service, all services are private by default, that means that these services can not be returned from the container, and must instead be dependency injected (The class constructor must take the logger as a parameter and become a property of the class to be accessible), or marked as public in the service configuration, and since the logger is a symfony component, the service configuration is within the symfony project, you'd have to copy the logger configuration from symfony to your project service configuration and add public: true, to access the logger instance from the container.

how to get cache directory from a service: $this->container->getParameter('kernel.cache_dir')

So far I was getting the cache directory from some controller. but as I want to set it in a specific service I would like to know which dependancy injection I should do to access it from a service.
Of course I could inject the container (as I settled here below as an exemple) but I guess there is some more spécific dependancy injection that I could use.
Here my code so far in my service
class mycache
{
private $container;
public function __construct($container){
$this->container = $container;
}
public function transf($text, $code)
{
$filename = $this->container->getParameter('kernel.cache_dir') . '/MyCACHE/langue.txt';
}
}
// service configuration
service
cache_langue:
class: MySite\BlogBundle\Services\mycache
arguments: ["#service_container"]
You can inject the kernel.cache_dir parameter as follows:
services:
cache_langue:
class: MySite\BlogBundle\Services\mycache
arguments: ["%kernel.cache_dir%"]

Inject service in symfony2 Controller

How can I inject a service (the service that I created) into my Controller?
A setter injection would do.
<?php
namespace MyNamespace;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
public function setMyService(MyService $myService)
{
$this->myService = $myService;
}
public function indexAction()
{
//Here I cannot access $this->myService;
//Because the setter is not called magically!
}
}
And my route settings :
// Resources/routing.yml
myController_index:
pattern: /test
defaults: { _controller: "FooBarBundle:MyController:index" }
I'm setting the service in another bundle :
// Resources/services.yml
parameters:
my.service.class: Path\To\My\Service
services:
my_service:
class: %my.service.class%
When the route is resolved, the service is not injected ( I know it shouldn't ).
I suppose somewhere in a yml file, I have to set:
calls:
- [setMyService, [#my_service]]
I am not using this Controller as a service, it's a regular Controller that serves a Request.
Edit: At this point in time, I am getting the service with $this->container->get('my_service'); But I need to inject it.
If you want to inject services into your controllers, you have to define controllers as services.
You could also take a look at JMSDiExtraBundle's special handling of controllers — if that solves your problem. But since I define my controllers as services, I haven't tried that.
When using the JMSDiExtraBundle you DON'T have to define your controller as a service (unlike #elnur said) and the code would be:
<?php
namespace MyNamespace;
use JMS\DiExtraBundle\Annotation as DI;
use Path\To\My\Service;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
/**
* #var $myService Service
*
* #DI\Inject("my_service")
*/
protected $myService;
public function indexAction()
{
// $this->myService->method();
}
}
I find this approach very nice because you avoid writing a __construct() method.
Since it's 2017 ending now and there is no tag for Symfony 3 or upcoming Symfony 4 (and I think there should not be), this problem is solvable in a native much better way.
If you are still struggling and somehow ended up on this page and not in Symfony docs, then you should know, that you do not need to declare controller as service, as it is already registered as one.
What you need to do, is check you services.yml:
# app/config/services.yml
services:
# default configuration for services in *this* file
_defaults:
# ...
public: false
Change public: false to public:true if you want all services to be public.
Or explicitly add a service and declare it public:
# app/config/services.yml
services:
# ... same code as before
# explicitly configure the service
AppBundle\Service\MessageGenerator:
public: true
And then in your controller you can get the service:
use AppBundle\Service\MessageGenerator;
// accessing services like this only works if you extend Controller
class ProductController extends Controller
{
public function newAction()
{
// only works if your service is public
$messageGenerator = $this->get(MessageGenerator::class);
}
}
Read more:
Public vs Private services
Services in Container
If you don't want to define your controller as a service, you can add a listener to the kernel.controller event to configure it just before it is executed. This way, you can inject the services you need inside your controller using setters.
http://symfony.com/doc/current/components/http_kernel/introduction.html#component-http-kernel-kernel-controller

Symfony2, autoload service in service

Question is simple but...
So we have main service:
class ManagerOne {}
and have several another services we want to use in main service:
class ServiceOne{}
class ServiceTwo{}
class ServiceThree{}
class ServiceFour{}
...
Each named as (in services.yml)
service.one
service.two
service.three
service.four
...
Locations of services is different, not in one folder (but I don't think it's a huge trouble for custom autoloader).
Regarding manual we can inject them via __construct() in main service (ManagerOne) but what if we got 20 such services need to be injected? Or use only that we need. Describe them in services as simple inject? O.o I think it's not good idea so.... Also we can inject container and that's it. BUT! Everywhere people saying that inject container worst solution.
What I want. I need method for ManagerOne service which will load service i need by 'service.name' or 'path' with checker 'service exist'.
You could use service tagging and tag each service you want to use in your ManagerOne class. And either use constructor dependency injection or method injection.
Example:
First of all you need a compiler pass to collect your tagged services:
namespace ...\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ExamplePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition("manager.one")) {
return;
}
$services = array();
foreach ($container->findTaggedServiceIds('managed_service') as $serviceId => $tag) {
$alias = isset($tag[0]['alias'])
? $tag[0]['alias']
: $serviceId;
// Flip, because we want tag aliases (= type identifiers) as keys
$services[$alias] = new Reference($serviceId);
}
$container->getDefinition('manager.one')->replaceArgument(0, $services);
}
}
Then you need to add the compiler pass to your bundle class:
namespace Example\ExampleBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use ...\DependencyInjection\Compiler\ExamplePass;
class ExampleBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ExamplePass());
}
}
Now you can use your services:
# services.yml
manager.one:
class: ManagerClass
arguments:
- [] # will be replaced by our compiler pass
services.one:
class: ServiceOne
tags:
- { name: managed_service, alias: service_one }
services.two:
class: ServiceTwo
tags:
- { name: managed_service, alias: service_two }
But caution if you get your manager, all service classes will be automatically created. If this is a performance drawback for you could pass only the service ids (not the Reference) to your management class. Add the #service_container as second argument and create the service as needed.
Since 2017, Symfony 3.3 and Symplify\PackageBuilder this gets even easier.
Thanks to this package, you can:
drop tags
have simple 5 line CompilerPass using strict types over strings
Let's get to your example
Suppose you have
1 manager - UpdateManager class
many updaters - a class that implements UpdaterInterface
1. Service Config using PSR-4 autodiscovery
# app/config/services.yml
services:
_defaults:
autowire: true
App\:
resource: ../../src/App
2. Collecting Compiler Pass
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symplify\PackageBuilder\Adapter\Symfony\DependencyInjection\DefinitionCollector;
final class CollectorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder)
{
DefinitionCollector::loadCollectorWithType(
$containerBuilder,
UpdateManager::class,
UpdaterInterface::class,
'addUpdater'
);
}
}
It collect all services of UpdaterInterface type
and adds them via addUpdater() method to UpdateManager.
3. Register Compiler Pass in Bundle
namespace App;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class UpdaterBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CollectorCompilerPass);
}
}
And that's all!
How to add new updater?
Just create class, that implements UpdaterInterface and it will be loaded to UpdateManager.
no tagging
no manual service registration
no boring work
Enjoy!

Resources