Override a symfony service tag with a compiler pass - symfony

I'm trying to override a tag in a symfony service definition with a compiler pass. The service as an example would be data_collector.translation.
The goal is to deactivate the data collector service to disable the element in the symfony web developer toolbar. To do this, I have to set the priority of the data_collector tag to 0.
I could also override it in my own service definition:
services:
data_collector.translation:
class: 'Symfony\Component\Translation\DataCollector\TranslationDataCollector'
tags:
- {name: 'data_collector', priority: '0'}
arguments: [#translator.data_collector]
But as I want to do this for a few of the data collectors, I would need to know the mandatory arguments for the data collector definition. The priority works the same for all collectors and therefore I would only need the name of the collector to disable it.
So I wrote the following compiler pass:
class DataCollectorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('data_collector.translation')) {
return;
}
$definition = $container->getDefinition('data_collector.translation');
$tags = $definition->getTags();
$tags['data_collector'][0]['priority'] = 0;
$definition->setTags($tags);
$container->setDefinition('data_collector.translation', $definition);
}
}
To make things more wired: When I run this command:
$ php app/console container:debug --show-private --tag='data_collector'
I get the following output:
data_collector.translation #WebProfiler/Collector/translation.html.twig translation 0 Symfony\Component\Translation\DataCollector\TranslationDataCollector
So the priority even in the debugger is set to 0.
But for which reason ever the element is still shown in the toolbar.
What did I do wrong here? Is there another mechanism for overwriting a tag within a compiler pass?
The compiler pass does run (tested it with printing out stuff)
I'm using Symfony 2.7.1

Turns out the code does work, the only problem is, that the CompilerPass is run after the ProfilerPass which is part of the FrameworkBundle. Putting my bundle with the CompilerPass before the FrameworkBundle in the AppKernel solves the problem (more information here). For not even initiating the data collectors it's better to remove all tags instead of just setting the priority to 0.
That's what the final solution looks like:
class DataCollectorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$collectorsToRemove = [
'data_collector.form',
'data_collector.translation',
'data_collector.logger',
'data_collector.ajax',
'data_collector.twig'
];
foreach($collectorsToRemove as $dataCollector) {
if (!$container->hasDefinition($dataCollector)) {
continue;
}
$definition = $container->getDefinition($dataCollector);
$definition->clearTags();
}
}
}

Can you try this?
if (!$container->hasDefinition('data_collector.form')) {
return;
}
$definition = $container->getDefinition('data_collector.form');
$definition->clearTags();
$container->setDefinition('data_collector.form', $definition);

Why not use your compiler pass to manipulate directly the service Definition of the service holding all these collectors ?
If I look at the compiler pass responsible for loading the data collector, it seems that they are all injected using a method call injection.
You could use your compiler pass to rewrite the method call array using methods like setMethodCalls, removeMethodCall, ... of the Definition entity.
The method call manipulation documentation : link

Related

Symfony override autowired services

I'm writing a Symfony 4 bundle and inside, in a compiler pass, I create multiple service definitions based on an abstract one (also enabling autowiring based on the argument name):
$managerDefinition = new ChildDefinition(Manager::class);
$managerDefinition->replaceArgument(0, $managerName);
...
$container->registerAliasForArgument($managerId, Manager::class, $managerName . 'Manager');
And this is the abstract service definition:
services:
MyBundle\Manager:
abstract: true
arguments:
- # manager name
So, in my App controller I can have this and it works correctly:
public function __construct(MyBundle\Manager $barManager)
{
// $barManager is MyBundle\Manager
}
Now, let's say at some point I decide to extend the Manager class in my App with additional methods:
class MyManager extends \MyBundle\Manager
{
public function newMethod() {
...
}
}
I override the bundle's abstract service like this:
services:
MyBundle\Manager:
class: App\Manager
abstract: true
arguments:
- # manager name
Everything still works as expected:
public function __construct(MyBundle\Manager $barManager)
{
// $barManager is App\Manager
$barManager->newMethod(); // Works
}
However, the IDE complains that newMethod() does not exist, as it doesn't exist in the typehinted MyBundle\Manager.
So, it seems more correct to change my constructor definition to let it know the actual class it's going to receive:
public function __construct(App\Manager $barManager)
However, I can't write this, as auto-wiring no longer works.
I suppose I could write a compiler pass in my App that registers autowiring for my custom App\Manager, but that seems like an overkill.
I can't shake the feeling that I'm doing something fundamentally wrong.
I guess my question is, what would be the best way to allow easy overriding of the abstract Manager definition in the bundle?

Read from parameters in Symfony 3.4 getParameter null

I can't read a parameter from parameters.yml in my controller.
I want to do this:
//My Controller
class ExampleController extends Controller
{
function someMethod($argument)
{
dump($this->getParameter('free_proxy'));die();
and in parameters.yml I got:
parameters:
free_proxy: "http://xxx:8080"
I get an error: Call to a member function getParameter() on null
I've tested some solutions like adding some services and using get and stuff but nothing works.
EDIT: also, I tried this:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
bind:
$freeProxy: '%free_proxy%'
Then using:
$this->container->getParameter('free_proxy');
But I got an error: Unused binding "$freeProxy" in service...
So there are two mysteries here. First is why is the container not being injected which in turn causes getParameter to fail. And second, why does bind generate that unused binding error.
You did not show your routing but I suspect that somewhere along the line you actually have:
$exampleController = new ExampleController();
If so then this explains why getParameter is failing. You really need to let Symfony create the controller based on the route. Otherwise the container is not injected and other controller magic is skipped.
I installed a fresh 3.4 app with the old directory structure and added a parameter
composer create-project symfony/framework-standard-edition s34
# app/config/parameters.yml
parameters:
free_proxy: "http://xxx:8080"
I then tweaked the default controller using the default route annotation:
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
$freeProxy = $this->getParameter('free_proxy');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.project_dir')).DIRECTORY_SEPARATOR.$freeProxy,
]);
}
}
And everything worked as expected. The Symfony request handler takes care of injecting the container and thus gives you access to the parameters. If you cannot get this working then please update your question with your routing information.
I then took a look at the bind issue. You really want to inject these parameters instead of pulling them. I updated services.yml
# app/config/services.yml
services:
bind:
$freeProxy: '%free_proxy%'
And started getting those unused binding errors. It turns out that bind does not work for action injection. Not really sure why. I don't use it much but I really would have expected that just adding $freeProxy to your action method would work. In any event, here is a working example of the proper way to do things.
class ExampleController extends Controller
{
private $freeProxy;
public function __construct($freeProxy)
{
$this->freeProxy = $freeProxy;
}
/**
* #Route("/example", name="example")
*/
function someMethod()
{
dump($this->freeProxy);
dump($this->getParameter('free_proxy'));die();
}
}
I then went to a fresh 4.2 project and tried action injection:
class IndexController extends AbstractController
{
public function index($freeProxy)
{
return new Response("Index $freeProxy");
}
}
Action injection works as expected for 4.2 but not 3.4. Constructor injection works fine in either version.
documentation show like this :
parameters.yml :
parameters:
mailer.transport: sendmail
to set :
$container->setParameter('mailer.transport', 'sendmail');
to get :
$container->getParameter('mailer.transport');

Symfony Tactician-bundle Typehints = Missing handler method error

I've used the thephpleague/tactician-bundle with Symfony before, but this is the first time I've used it with Symfony 4.* (specifically 4.1.4) and attempted to use a single handler Class for my Application Service.
When I execute a command in the Controller
public function postAction(Request $request, CommandBus $commandBus)
{
$form = $this->createForm(VenueType::class);
$form->submit($request->request->all(), true);
$data = $form->getData();
if($form->isValid()) {
$command = new CreateVenueCommand($data);
$commandBus->handle($command);
return $form->getData();
}
return $form;
}
... I get the following error:
"error": {
"code": 500,
"message": "Internal Server Error",
"exception": [
{
"message": "Could not invoke handler for command App\\Application\\Command\\CreateVenueCommand for reason: Method 'handle' does not exist on handler",
"class": "League\\Tactician\\Exception\\CanNotInvokeHandlerException",
"trace": [
I've seemingly followed the installation documents for the tactician-bundle and installed it using Flex. As far as I can tell everything is configured correctly, so I'm unsure what I'm missing in my implementation.
Implementation
As per the thephpleague/tactician-bundle installation guide I've installed using Flex and the bundle is registered and the config package installed:
tactician:
commandbus:
default:
middleware:
- tactician.middleware.locking
- tactician.middleware.doctrine
- tactician.middleware.command_handler
After creating the DTO Command Class 'CreateVenueCommand', I created the handler Class:
use App\Infrastructure\Domain\Model\VenueRepositoryInterface;
use App\Application\Command\CreateVenueCommand;
use App\Domain\Entity\Venue;
class VenueApplicationService
{
private $venueRepository;
public function __construct(VenueRepositoryInterface $venueRepository)
{
$this->venueRepository = $venueRepository;
}
/**
* #param CreateVenueCommand $aCommand
* #throws \Exception
*/
public function createVenue(CreateVenueCommand $aCommand)
{
$aVenue = new Venue($aCommand->getData())
if ($aVenue === null) {
throw new \LogicException('Venue not created');
}
$this->venueRepository->add($aVenue);
}
Then I registered the handler Class as a Service taking advantage of Symfony's autowiring and Tacticians typehints:
App\Application\VenueApplicationService:
arguments:
- '#App\Infrastructure\Persistence\Doctrine\DoctrineVenueRepository'
tags:
- { name: tactician.handler, typehints: true }
So according to the installation documents, typehints work if:
The method must be public.
The method must accept only one parameter.
The parameter must be typehinted with a class name.
Also, and this is specific to my use case:
If you have multiple commands going into a single handler, they will all be detected, provided they follow the rules above. The actual name of the method is NOT important.
So when I invoke the commandbus in the Controller Class, I'm unsure why I'm getting the error above.
If I change the Command Handler method to:
public function handle(CreateVenueCommand $aCommand)
{
... then it works fine. This would seem to suggest that the typehints aren't working as documented.
It seems in this case that the actual name of the method IS important. ... or I've made some form of error in my implementation ... or I'm misunderstanding the multiple commands going into a single handler use case??
Any assistance would be greatly appreciated.
Solution
With a big thanks to kunicmarko20 for pointing me in the right direction.
Specifically for my use case I simply needed to use one of Tacticians MethodNameInflector classes, configured in Symfony thus:
tactician:
commandbus:
default:
middleware:
- tactician.middleware.locking
- tactician.middleware.doctrine
- tactician.middleware.command_handler
method_inflector: tactician.handler.method_name_inflector.handle_class_name
... then it was simply a matter of naming each Handler method in my Application Service class 'handle{whateverYouLike}Command
Here under 1. is explained how the naming works, if you want to use a different name than in this table you can implement MethodNameInflector Interface and provide a name of the method.

symfony 3.1 Check if a bundle is installed

I'm developing a bundle who has a dependency on another one.
In order to handle the case that the base bundle has not been installed I'll like to perform a "bundle_exists()" function inside a controller.
The question is: How can I have a list of installed bundles or How can I check for the name (eventually also the version) of a bundle.
Thanks.
In addition to #Rooneyl's answer:
The best place to do such a check is inside your DI extension (e.g. AcmeDemoExtension). This is executed once the container is build and dumped to cache. There is no need to check such thing on each request (the container doesn't change while it's cached anyway), it'll only slow down your cache.
// ...
class AcmeDemoExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$bundles = $container->getParameter('bundles');
if (!isset($bundles['YourDependentBundle'])) {
throw new \InvalidArgumentException(
'The bundle ... needs to be registered in order to use AcmeDemoBundle.'
);
}
}
}
Your class needs to have access to the container object (either by extending or DI).
Then you can do;
$this->container->getParameter('kernel.bundles');
This will give you a list of bundles installed.
Update;
If you are in a controller that extends the Symfony\Bundle\FrameworkBundle\Controller\Controller or in a command class that extends Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand, you can just get the parameter.
$this->getParameter('kernel.bundles').
Else #Wouter J's answer is your best answer.
You can get a list of all Bundles from the Kernel like this:
public function indexAction ()
{
$arrBundles = $this->get("kernel")->getBundles();
if (!array_key_exists("MyBundle", $arrBundles))
{
// bundle not found
}
}
From Andrey at this question: How do I get a list of bundles in symfony2?
If you want to call a non static method of registered bundle object (not class) then you can do the following:
$kernel = $this->container->get('kernel');
$bundles = $kernel->getBundles();
$bundles['YourBundleName']->someMethod();
Where 'YourBundleName' is the name of your bundle, which you can get by calling from console:
php app/console config:dump-reference

Symfony2 custom console command not working

I created a new Class in src/MaintenanceBundle/Command, named it GreetCommand.php and put the following code in it:
<?php
namespace SK2\MaintenanceBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('maintenance:greet')
->setDescription('Greet someone')
->addArgument('name', InputArgument::OPTIONAL, 'Who do you want to greet?')
->addOption('yell', null, InputOption::VALUE_NONE, 'If set, the task will yell in uppercase letters')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
if ($name) {
$text = 'Hello '.$name;
} else {
$text = 'Hello';
}
if ($input->getOption('yell')) {
$text = strtoupper($text);
}
$output->writeln($text);
}
}
?>
And tried to call it via
app/console maintenance:greet Fabien
But i always get the following error:
[InvalidArgumentException]
There are no commands defined in the "maintenance" namespace.
Any ideas?
I had this problem, and it was because the name of my PHP class and file didn't end with Command.
Symfony will automatically register commands which end with Command and are in the Command directory of a bundle. If you'd like to manually register your command, this cookbook entry may help: http://symfony.com/doc/current/cookbook/console/commands_as_services.html
I had a similar problem and figured out another possible solution:
If you override the default __construct method the Command will not be auto-registered by Symfony, so you have to either take the service approach as mentioned earlier or remove the __construct override and make that init step in the execute method or in the configure method.
Does actually anyone know a good best practice how to do init "stuff" in Symfony commands?
It took me a moment to figure this out.
I figured out why it was not working: I simply forgot to register the Bundle in the AppKernel.php. However, the other proposed answers are relevant and might be helpful to resolve other situations!
By convention: the commands files need to reside in a bundle's command directory and have a name ending with Command.
in AppKernel.php
public function registerBundles()
{
$bundles = [
...
new MaintenanceBundle\MaintenanceBundle(),
];
return $bundles;
}
In addition to MonocroM's answer, I had the same issue with my command and was silently ignored by Symfony only because my command's constructor had 1 required argument.
I just removed it and call the parent __construct() method (Symfony 2.7) and it worked well ;)
If you are over-riding the command constructor and are using lazy-loading/autowiring, then your commands will not be automatically registered. To fix this you can add a $defaultName variable:
class SunshineCommand extends Command
{
protected static $defaultName = 'app:sunshine';
// ...
}
Link to the Symfony docs.
I think you have to call parent::configure() in your configure method
I had this same error when I tried to test my command execution with PHPUnit.
This was due to a wrong class import :
use Symfony\Component\Console\Application;
should be
use Symfony\Bundle\FrameworkBundle\Console\Application;
cf. Other stack thread
In my case it was complaining about the "workflow" namespace although the WorkflowDumpCommand was correctly provided by the framework.
However, it was not available to run because I have not defined any workflows so the isEnabled() method of the command returned false.
I tried to use a service passed via constructor inside the configure method:
class SomeCommand extends Command {
private $service;
public function __construct(SomeService $service) {
$this->service = $service;
}
protected function configure(): void {
$this->service->doSomething(); // DOES NOT WORK
}
}
Symfony uses Autoconfiguration that automatically inject dependencies into your services and register your services as Command, event,....
So first just make sure that you have services.yaml in your config folder. with autoconfigure:true.
this is the default setting
Then Make sure That All your files are exactly the same name as Your Class.
so if you have SimpleClass your file must be SimpleClass.php
If you have a problem because of a __constructor,
go to services.yml and add something like this:
app.email_handler_command:
class: AppBundle\Command\EmailHandlerCommand
arguments:
- '#doctrine.orm.entity_manager'
- '#app.email_handler_service'
tags:
- { name: console.command }
For newer Symfony-Version (5+) commands must be registered as services.
What I do frequently forget while setting it up, is to tag it properly:
<service id="someServiceCommand">
<tag name="console.command"/>
</service>
Without this litte adaptation, your command name will not be displayed and therefore not accessible.

Resources