Symfony 4: Form as service, inject service : too few arguments - php

I try to get service in my Form, following official instructions:
https://symfony.com/doc/current/form/form_dependencies.html
this is my 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
class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
App\Service\:
resource: '../src/Service/*'
App\Form\:
resource: '../src/Form/*'
my form look like this :
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\Common\Persistence\ObjectManager;
use App\Entity\Contrat;
use App\Entity\Society;
use App\Entity\Client;
use App\Form\DataTransformer\ClientToNumberTransformer;
use App\Form\DataTransformer\SocietyToNumberTransformer;
class ContratType extends AbstractType{
private $manager;
public function __construct(SocietyToNumberTransformer $manager){ // this is the breakpoint from error
$this->manager = $manager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('...')
...
;
// Old fashion way for testing only ...
$builder->get('client')->addModelTransformer(new ClientToNumberTransformer($this->manager));
$builder->get('society')->addModelTransformer(new SocietyToNumberTransformer($this->manager));
}
public function configureOptions(OptionsResolver $resolver){
$resolver->setDefaults(array(
'data_class' => Contrat::class,
));
}
It's look-like pretty ... When i test this with postman I get this error :
Too few arguments to function App\Form\ContratType::__construct(), 0
passed in /code/vendor/symfony/form/FormRegistry.php on line 92 and
exactly 1 expected
I don't understand, i found my fomr in autowiring debug command :
php bin/console debug:autowiring
App\Form\ContratType
App\Form\DataTransformer\ClientToNumberTransformer
App\Form\DataTransformer\SocietyToNumberTransformer
and the controller :
public function addContrat(Request $request, FormData $formData){
$em = $this->getDoctrine()->getManager('default');
$data = $formData->getRequestData($request);
var_dump($data);
$contrat = new Contrat;
$form = $this->factory->createBuilder(ContratType::class, $contrat)->getForm();
$form->submit($data);
if($form->isValid()){
$em->persist($contrat);
$em->flush();
return $this->response(
$this->serialize(
$contrat,
['group1']
)
);
}
return $this->response('error', 500);
}
thx for your help.

Try to replace
$form = $this->factory->createBuilder(ContratType::class, $contrat)->getForm();
for
$form = $this->createForm(ContratType::class, $contrat);
https://symfony.com/doc/current/forms.html#creating-form-classes
I have got the same problem but as I build my form from inside a service, I m still stuck ;-)
Hope it helps

Related

Symfony6 changing the controller manually using the "kernel.controller" event. How to inject the service container?

The application that I am building is not going to work in a traditional way. All the routes ar going to be stored in the database. And based on the route provided I need to get the correct controller and action to be executed.
As I understand this can be achieved using the "kernel.controller" event listener: https://symfony.com/doc/current/reference/events.html#kernel-controller
I am trying to use the docs provided, but the example here does not exacly show how to set up a new callable controller to be passed. And I have a problem here, because I dont know how to inject the service container to my newly called controller.
At first the setup:
services.yaml
parameters:
db_i18n.entity: App\Entity\Translation
developer: '%env(DEVELOPER)%'
category_directory: '%kernel.project_dir%/public/uploads/category'
temp_directory: '%kernel.project_dir%/public/uploads/temp'
product_directory: '%kernel.project_dir%/public/uploads/product'
app.supported_locales: 'lt|en|ru'
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
App\Translation\DbLoader:
tags:
- { name: translation.loader, alias: db }
App\Extension\TwigExtension:
arguments:
- '#service_container'
tags:
- { name: twig.extension }
App\EventListener\RequestListener:
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onControllerRequest }
The listener:
RequestListener.php
<?php
namespace App\EventListener;
use App\Controller\Shop\HomepageController;
use App\Entity\SeoUrl;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
class RequestListener
{
public ManagerRegistry $doctrine;
public RequestStack $requestStack;
public function __construct(ManagerRegistry $doctrine, RequestStack $requestStack)
{
$this->doctrine = $doctrine;
$this->requestStack = $requestStack;
}
/**
* #throws Exception
*/
public function onControllerRequest(ControllerEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
if(str_contains($this->requestStack->getMainRequest()->getPathInfo(), '/admin')) {
return;
}
$em = $this->doctrine->getManager();
$pathInfo = $this->requestStack->getMainRequest()->getPathInfo();
;
$route = $em->getRepository(SeoUrl::class)->findOneBy(['keyword' => $pathInfo]);
if($route instanceof SeoUrl) {
switch ($route->getController()) {
case 'homepage':
$controller = new HomepageController();
$event->setController([$controller, $route->getAction()]);
break;
default:
break;
}
} else {
throw new Exception('Route not found');
}
}
}
So this is the most basic example. I get the route from the database, if it a "homepage" route, I create the new HomepageController and set the action. However I am missing the container interface that I dont know how to inject. I get this error:
Call to a member function has() on null
on line: vendor\symfony\framework-bundle\Controller\AbstractController.php:216
which is:
/**
* Returns a rendered view.
*/
protected function renderView(string $view, array $parameters = []): string
{
if (!$this->container->has('twig')) { // here
throw new \LogicException('You cannot use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".');
}
return $this->container->get('twig')->render($view, $parameters);
}
The controller is as basic as it gets:
HomepageController.php
<?php
namespace App\Controller\Shop;
use App\Repository\CategoryRepository;
use App\Repository\Shop\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HomepageController extends AbstractController
{
#[Route('/', name: 'index', methods: ['GET'])]
public function index(): Response
{
return $this->render('shop/index.html.twig', [
]);
}
}
So basically the container is not set. If I dump the $event->getController() I get this:
RequestListener.php on line 58:
array:2 [▼
0 => App\Controller\Shop\HomepageController {#417 ▼
#container: null
}
1 => "index"
]
I need to set the container by doing $controller->setContainer(), but what do I pass?
Do not inject the container, controllers are services too and manually instanciating them is preventing you from using constructor dependency injection. Use a service locator which contains only the controllers:
Declared in config/services.yaml:
# config/services.yaml
services:
App\EventListener\RequestListener:
arguments:
$serviceLocator: !tagged_locator { tag: 'controller.service_arguments' }
Then in the event listener, add the service locator argument and fetch the fully configured controllers from it:
# ...
use App\Controller\Shop\HomepageController;
use Symfony\Component\DependencyInjection\ServiceLocator;
class RequestListener
{
# ...
private ServiceLocator $serviceLocator;
public function __construct(
# ...
ServiceLocator $serviceLocator
) {
# ...
$this->serviceLocator = $serviceLocator;
}
public function onControllerRequest(ControllerEvent $event)
{
# ...
if($route instanceof SeoUrl) {
switch ($route->getController()) {
case 'homepage':
$controller = $this->serviceLocator->get(HomepageController::class);
# ...
break;
default:
break;
}
}
# ...
}
}
If you dump any controller you will see that the container is set. Same will go for additionnal service that you autowire from the constructor.

cannot load the TranslatorInterface in my formType

I try to introduce some translation into a ChatBundle in order to follow the changes of the _locale of the hosting app in Symfony 4.
So in the formBuilder i try to inject the TranslatorInterface as such:
// lib/ChatBundle/Form/ChatMessageType.php
namespace bornToBeAlive\ChatBundle\Form;
use bornToBeAlive\ChatBundle\Entity\ChatMessage;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\TranslatorInterface;
class ChatMessageType extends AbstractType
{
private $trans;
public function __construct(TranslatorInterface $trans)
{
$this->trans = $trans;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('content', null, [
'attr'=> ['placeholder' => $this->trans->trans('placeholder',[],'chat')]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ChatMessage::class,
]);
}
}
but when I try to run my show action:
public function show(): Response
{
$message = new ChatMessage();
$form = $this->createForm(ChatMessageType::class, $message);
return $this->render('#Chat/show.html.twig', [
'form' => $form->createView(),
]);
}
I get the following error :
Too few arguments to function bornToBeAlive\ChatBundle\Form\ChatMessageType::__construct(), 0 passed in ../vendor/symfony/form/FormRegistry.php on line 92 and exactly 1 expected
I'm surprised because I use this technique when I'm in my host app for the other type. did I do something wrong ?
According to the Symfony 4.4 Form documentation :
If you're using the default services.yaml configuration, this example will already work! Otherwise, create a service for this form class and tag it with form.type.
services:
# default configuration for services in *this* file
_defaults:
autowire: true
autoconfigure: true
public: false
If autowiring is not working as expected, you can define form as service, like this
# config/services.yaml
app.form.corporation_type:
class: bornToBeAlive\ChatBundle\Entity\ChatMessageType
arguments: ["#translator"]
tags:
- { name: form.type }

symfony EventSubscriber ignored in Symfony 3.4

I'm trying to use an event subscriber to redirect person registering to a different route, than the standard FOSUser bundle directs them to. Going by some tutorials for Symfony 3.3, but wondered as I have version 3.4 if anything needs changing to make it work, as it currently just goes to the standard page? Thanks
EventSubscriber
namespace eventsBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Routing\RouterInterface;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use FOS\UserBundle\FOSUserEvents;
class RedirectAfterRegistrationSubscriber implements
EventSubscriberInterface
{
use TargetPathTrait;
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onRegistrationSuccess(FormEvent $event)
{
die();
// main is your firewall's name
$url = $this->getTargetPath($event->getRequest()->getSession(),
'main');
if (!$url) {
$url = $this->router->generate('homepage');
}
$response = new RedirectResponse($url);
$event->setResponse($response);
}
public static function getSubscribedEvents()
{
return [
FOSUserEvents::REGISTRATION_SUCCESS =>
['onRegistrationSuccess',-5]
];
}
}
services.yml
services:
_defaults:
autowire: true
autoconfigure: true
eventsBundle\:
resource: '../../src/eventsBundle/*'
exclude: '../../src/eventsBundle/{Entity,Repository,Tests}'
eventsBundle\EventListener\RedirectAfterRegistrationSubscriber:
autowire: true
I added die(); just to make sure it was going to this but, has not effect
The services.yml file to change is the one in my bundle not the main services.yml under app/config, this now picks up the EventSubscriber
Looks like you need to add tag for your subscriber.
eventsBundle\EventListener\RedirectAfterRegistrationSubscriber:
autowire: true
tags:
- { name: kernel.event_subscriber }

Cannot autowire service "App\Service\MatchCarAdService": argument "$templating" of method

Hi I'm creating a service. This is the code,
namespace App\Service;
use Symfony\Component\DependencyInjection\ContainerInterface;
use App\Entity\CarAd;
class MatchCarAdService {
protected $mailer;
protected $templating;
public function __construct(ContainerInterface $container, \Swift_Mailer $mailer, $templating) {
$this->container = $container;
$this->mailer = $mailer;
$this->templating = $templating;
}
public function sendMail() {
$message = (new \Swift_Message('Hello Email'))
->setFrom('vimuths#yahoo.com')
->setTo('vimuths#yahoo.com')
->setBody(
$this->templating->render(
// templates/emails/matching-cars.html.html.twig
'emails/matching-cars.html.html.twig', []
), 'text/html'
);
$this->mailer->send($message);
This is services.yml
MatchCarAdService:
class: App\Service\MatchCarAdService
arguments: ['#mailer','#templating']
But I'm getting this error,
Cannot resolve argument $matchService of
"App\Controller\Api\SearchController()": Cannot autowire service
"App\Service\MatchCarAdService": argument "$templating" of method
"__construct()" has no type-hint, you should configure its value
explicitly.
Right now your constructor has 3 parameters but in arguments you are putting 2 only.
So there are two possible solutions:
Configure in your yaml
MatchCarAdService:
class: App\Service\MatchCarAdService
arguments: ['#container', '#mailer','#templating']
Use auto wiring with type hint
There it depends on your Symfony version, but change constructor to
public function __construct(ContainerInterface $container, \Swift_Mailer $mailer, Symfony\Component\Templating\EngineInterface; $teplating) {
$this->container = $container;
$this->mailer = $mailer;
$this->templating = $templating;
}
And you may have to composer require symfony/templating in order to get the Symfony\Bundle\FrameworkBundle\Templating\EngineInterface service.
Also the following configuration has to be added under framework:
templating:
enabled: true
engines: ['twig']
Symfony 3.3+ Answer
Answer by #M. Kebza solves your situation. But you can make this even simpler and bug-proof. Just use Symfony 3.3+ features.
A. Use Autowiring
services:
_defaults:
autowire: true
App\Service\MatchCarAdService: ~
App\Service\CleaningService: ~
App\Service\RentingService: ~
B. Use Autowiring + Autodiscovery - Even better!
services:
_defaults:
autowire: true
App\:
resource: ../src
This load all services in App\ namespace from ../src directory by PSR-4 convention.
You can see more examples in How to refactor to new Dependency Injection features in Symfony 3.3 post.

How to call Entity Manager in a constructor?

I've been trying to call Entity Manager in a constructor:
function __construct()
{
$this->getDoctrine()->getEntityManager();
...
but, as I've seen in this answer: Stackoverflow question, it can't be done.
So I wonder if there is a way to achieve it, as I have to call it often, and want to do some stuff in the constructor after getting the repository.
Edit:
I've tried with #MKhalidJunaid answer:
//src/MSD/HomeBundle/Resources/config/services.yml
services:
imageTransController.custom.service:
class: MSD\HomeBundle\Controller\ImageTransController
arguments:
EntityManager: "#doctrine.orm.entity_manager"
-
//app/config/config.php
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: doctrine_extensions.yml }
- { resource: "#MSDHomeBundle/Resources/config/services.yml" }
-
//src/MSD/HomeBundle/Controller/ImageTransController.php
namespace MSD\HomeBundle\Controller;
use Doctrine\ORM\EntityManager;
use MSD\HomeBundle\Entity\Imagen as Imagen;
use MSD\HomeBundle\Controller\HomeController as HomeController;
class ImageTransController extends HomeController
{
protected $em ;
function __construct(EntityManager $entityManager)
{
...
but I'm getting this error:
Catchable Fatal Error: Catchable Fatal Error: Argument 1 passed to MSD\HomeBundle\Controller\ImageTransController::__construct() must be an instance of Doctrine\ORM\EntityManager, none given, called in /home/manolo/MiServer/itransformer/app/cache/dev/jms_diextra/controller_injectors/MSDHomeBundleControllerImageTransController.php on line 13 and defined in /home/manolo/MiServer/itransformer/src/MSD/HomeBundle/Controller/ImageTransController.php line 38 (500 Internal Server Error)
New attempt:
I've also tried with #praxmatig answer:
//services.yml
parameters:
msd.controller.imagetrans.class: MSD\HomeBundle\Controller\ImageTransController
services:
msd.imagetrans.controller:
class: "%msd.controller.imagetrans.class%"
arguments: [ #doctrine.orm.entity_manager ]
-
//ImageTransController.php
namespace MSD\HomeBundle\Controller;
use Doctrine\ORM\EntityManager;
class ImageTransController
{
protected $em ;
function __construct(EntityManager $em)
{
$this->em = $em;
}
...
-
//routing.yml
msd_home_cambiardimensiones:
pattern: /cambiardimensiones
defaults: { _controller: MSDHomeBundle:msd.imagetrans.controller:cambiardimensionesAction }
but I get this error:
Unable to find controller "MSDHomeBundle:msd.imagetrans.controller" - class "MSD\HomeBundle\Controller\msd.imagetrans.controllerController" does not exist. (500 Internal Server Error)
You need to make a service for your class and pass the doctrine entity manager as the argument doctrine.orm.entity_manager.Like in services.yml
services:
test.cutom.service:
class: Test\YourBundleName\Yourfoldernameinbundle\Test
#arguments:
arguments: [ #doctrine.orm.entity_manager ]
#entityManager: "#doctrine.orm.entity_manager"
You must import your services.yml in config.yml
imports:
- { resource: "#TestYourBundleName/Resources/config/services.yml" }
Then in your class's constructor get entity manager as argument
use Doctrine\ORM\EntityManager;
Class Test {
protected $em;
public function __construct(EntityManager $entityManager)
{
$this->em = $entityManager;
}
}
Hope this makes sense
Don't extend the base controller class when you register controller as a service. There is a documentation about it here
class ImageTestController
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function someAction()
{
// do something with $this->em
}
}
// services.yml
services:
acme.controller.image_test:
class: Acme\SomeBundle\Controller\ImageTestController
// routing.yml
acme:
path: /
defaults: { _controller: acme.controller.image_test:someAction }
Why do you want to grab the Doctrine 2 EntityManager in the constructor of a controller?
Why not simply do $em = $this->getDoctrine()->getManager(); (or $em = $this->getDoctrine()->getEntityManager(); in Symfony 2.0) in the action(s) you need it? This saves you from the overhead of initializing the EntityManager when you don't need it.
If you really do want to do this, there are clear instructions on How to define Controllers as Services. Basically it looks like this:
# src/MSD/HomeBundle/Controller/ImageTransController.php
namespace MSD\HomeBundle\Controller;
use Doctrine\ORM\EntityManager;
class ImageTransController
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function indexAction()
{
// use $this->em
}
}
# src/MSD/HomeBundle/Resources/config/services.yml
parameters:
msd.controller.image_trans.class: MSD\HomeBundle\Controller\ImageTransController
services:
msd.controller.image_trans:
class: "%msd.controller.image_trans.class%"
arguments: ["#doctrine.orm.default_entity_manager"]
# app/config/routing.yml
msd_home_cambiardimensiones:
path: /cambiardimensiones
defaults: { _controller: msd.controller.image_trans:indexAction }
You have to add
use Doctrine\ORM\EntityManager;
in your controller
I see that you are trying to get the entity manager in the constructor of the controller, which is not the way to do so , unless you plan to define your controller as a service.
which on this case, you need to use dependency injection to inject the service entity manager.
But in general the common way to use entity manager in a controller is simply by getting it using the following code:
$entityManager = $this->container->get('doctrine.orm.entity_manager');
I think you are in the right direction, I would take the second option:
For the second option I think that the definition inside routing.yml is wrong
//routing.yml
msd_home_cambiardimensiones:
pattern: /cambiardimensiones
defaults: { _controller: msd.imagetrans.controller:cambiardimensionesAction }
Here just remove MSDHomeBundle from the _controller inside defaults
For the first option:
Does HomeController has its own constructor?
//src/MSD/HomeBundle/Resources/config/services.yml
services:
imageTransController.custom.service:
class: MSD\HomeBundle\Controller\ImageTransController
arguments: [#doctrine]
it could help then inside the constructor
__construct(Registry $doctrine)
$this->doctrine = $doctrine;
or
$this->em = $doctrine->getManager();

Categories