symfony 3 console command list in controller - symfony

As I am not sure how to describe it, I didn't found any results in google or stack.
I would like to list all available console commands (which are callable by using the bin/console) with a Controller-Action so that I can forward a list of all commands to twig.
How can I realize this ?

Interesting question. You can of course just run the console command itself and capture the list of commands. Might actually be the best way.
However, there is a service called console.command_loader which has a method called getNames which does indeed return a list of command names. It implements CommandLoaderInterface.
Originally I tried to create an alias so it could be injected into an action method:
services:
Symfony\Component\Console\CommandLoader\CommandLoaderInterface:
alias: console.command_loader
But I kept getting console.command_loader not found which was puzzling since debug:container shows it. The service was tagged with container.no_preload which might have something to do with it. Not sure.
So I went and just defined the controller service:
services:
App\Controller\CommandController:
tags:
- 'controller.service_arguments'
arguments:
- '#console.command_loader'
And somewhat to my surprise it worked.
class CommandController extends AbstractController
{
public function __construct(private CommandLoaderInterface $cl)
{
}
#[Route('/commands', name: 'app_commands')]
public function commands(): Response
{
$names = $this->cl->getNames();
dump($names);
// I happen to have a command called app:init
$initCommand = $this->cl->get('app:init');
dump($initCommand->getDescription());
//return $this->render('default/index.html.twig', [
// 'controller_name' => 'DefaultController ' . 'Commands',
//]);
}
}
This was all done in Symfony 6. Did not happen to have a Symfony 3 app handy. Your first step would be to confirm that Symfony 3 also has the service with bin/console debug:container console.command_loader. If it does not have such a service then poke around a bit and see if it has something similar.

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?

symfony 3 tagging services for using in autowiring service

I tried to create an interface to create tagged services that can be injected into another service based on the documentation here https://symfony.com/doc/3.4/service_container/tags.html
I created an interface like
namespace AppBundle\Model;
interface PurgeInterface {
//put your code here
public function purge ();
}
put the definition into the service.yml:
_instanceof:
AppBundle\Model\PurgeInterface:
tags: ['app.purge']
and create services on this interface.
console debug:container shows my services as properly tagged.
I created another service which should work with the tagged services but this do not work.
services.yml:
purge_manager:
class: AppBundle\Service\PurgeManager
arguments: [!tagged app.purge]
The service looks like:
namespace AppBundle\Service;
use AppBundle\Model\PurgeInterface;
class PurgeManager {
public function __construct(iterable $purgers) {
dump($purgers);
}
}
If I test this I get:
Type error: Too few arguments to function AppBundle\Service\PurgeManager::__construct(), 0 passed in /.....Controller.php on line 21 and exactly 1 expected
I havenĀ“t tried to create a compiler pass because I just want to understand why this is not working as it should based on the documentation
Thanks in advance
Sebastian
You can use tags, manual service definition and _instanceof in config. It's one of Symfony ways, but it requires a lot of YAML coding. What are other options?
Use Autowired Array
I've answered it here, but you use case much shorter and I'd like to answer with your specific code.
The simplest approach is to autowire arguments by autowired array.
no tag
support PSR-4 autodiscovery
no coding outside the service
1 compiler pass
Example
namespace AppBundle\Service;
use AppBundle\Model\PurgeInterface;
class PurgeManager
{
/**
* #param PurgeInterface[] $purgers
*/
public function __construct(iterable $purgers) {
dump($purgers);
}
}
This is also called collector pattern.
How to Integrate
Read a post with an example about this here
Or use the Compiler pass
If there are some incompatible classes, exclude them in the constructor of compiler pass:
$this->addCompilerPass(new AutowireArrayParameterCompilerPass([
'Sonata\CoreBundle\Model\Adapter\AdapterInterface'
]);

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.

Getting a list of tagged services in my controller

What i want is to add services to the service container that i want to use later in my controller or service.
So i created two services with my custom tag fbeen.admin
here they are:
services:
app.test:
class: AppBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
fbeen.admin.test:
class: Fbeen\AdminBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
Now i want to use all the services with the tag fbeen.admin in my controller but i dont know how.
I followed the How to work with service tags tutorial but i get stuck on this rule:
$definition->addMethodCall('addTransport', array(new Reference($id)));
On some way the addTransport method of the TransportChain class should be called but it seems that it isn't been called.
And even if it would be called then i still do not have a list of services with the fbeen.admin tag into my controller.
I am sure that i am missing something but who can explain me what it is?
p.s. I know that compilerPass runs at buildtime but for example sonata admin knows all admin classes and twig knows all twig extensions. How do they know?
Thank you for reading this :-)
Symfony 3.3
Container gets compiled once (in debug more often, but in production only once). What you manage with addMethodCall... is that once you request your service from container, which you are storing in $definition (that in this case is controller). Then container will call method addMethodCall('method'.. during initialising your service.
What it will look in container:
// This is pseudo content of compiled container
$service = new MyController();
// This is what compiler pass addMethodCall will add, now its your
// responsibility to implement method addAdmin to store admins in for
// example class variable. This is as well way which sonata is using
$service->addAdmin(new AppBundle\Admin\TestAdmin());
$service->addAdmin(new AppBundle\Admin\TestAdmin());
return $service; // So you get fully initialized service
Symfony 3.4+
What you can do is this:
// Your services.yaml
services:
App/MyController/WantToInjectSerivcesController:
arguments:
$admins: !tagged fbeen.admin
// Your controller
class WantToInjectSerivcesController {
public function __construct(iterable $admins) {
foreach ($admins as $admin) {
// you hot your services here
}
}
}
Bonus autotagging of your services. Lets say all your controllers implements interface AdminInterface.
// In your extension where you building container or your kernel build method
$container->registerForAutoconfiguration(AdminInterface::class)->addTag('fbeen.admin');
This will tag automatically all services which implement your interface with tag. So you don't need to set tag explicitly.
The thing to note here is this: The CompilerPass doesn't run the 'addTransport' (or whatever you may call it) in the compiler-pass itself - just says 'when the time is right - run $definition->addTransport(...) class, with this data'. The place to look for where that happens is in your cache directory (grep -R TransportChain var/cache/), where it sets up the $transportChain->addTransport(...).
When you come to use that service for the first time - only then is the data filled in as the class is being instantiated from the container.
This worked for me:
extend the TransportChain class with a getTransports method:
public function getTransports()
{
return $this->transports;
}
and use the TransportChain service in my controller:
use AppBundle\Mail\TransportChain;
$transportChain = $this->get(TransportChain::class);
$transports = $transportChain->getTransports();
// $transports is now an array with all the tagged services
Thank you Alister Bulman for pushing me forwards :-)

Override a symfony service tag with a compiler pass

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

Resources