Symfony access Entity Manager inside EventSubscriber - php

I have an Employer profile form and I am trying to link the State where Employer is located at with a City this part is working alright but the FormType got so long and this feature will be required at other places of the website so I decided to move this logic inside an EventSubscriber and reuse it where ever I need it.
Problem I am having is I am trying to wrap my head around how to inject EntityManager inside the EventSubscriber class.
I know I can add the following code inside my services.yml and that should do it buts its not working.
app.form.location:
class: AppBundle\Form\EventListener\AddStateFieldSubscriber
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: kernel.event_subscriber }
This is my EmployerProfileType where I am calling my addEventSubscriber which is AddStateFieldSubscriber()
class EmployerProfileType extends AbstractType
{
protected $em;
function __construct(EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->add('companyName', TextType::class)
->add('companyProfile', TextareaType::class)
->add('companyLogo', FileType::class, array(
'data_class' => null
));
$builder->addEventSubscriber(new AddStateFieldSubscriber());
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\EmployerProfile',
));
}
public function getName()
{
return 'app_bundle_emp_profile_type';
}
}
This is my AddStateFieldSubscriber class where I need access to EntityManager
class AddStateFieldSubscriber implements EventSubscriberInterface
{
protected $em;
function __construct(EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that you want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit'
);
}
protected function addElements(FormInterface $form, States $province = null)
{
// Remove the submit button, we will place this at the end of the form later
// Add the province element
$form->add('state', EntityType::class, array(
'data' => $province,
'placeholder' => 'provide_state',
'class' => 'AdminBundle\Entity\States',
'mapped' => false)
);
// Cities are empty, unless we actually supplied a province
$cities = array();
if ($province) {
// Fetch the cities from specified province
$repo = $this->em->getRepository('AdminBundle:Cities');
$cities = $repo->findByStates($province, array('name' => 'asc'));
}
// Add the city element
$form->add('city', EntityType::class, array(
'placeholder' => 'provide_state_first',
'class' => 'AdminBundle\Entity\Cities',
'choices' => $cities,
));
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// Note that the data is not yet hydrated into the entity.
$province = $this->em->getRepository('AdminBundle:States')->find($data['state']);
$this->addElements($form, $province);
}
function onPreSetData(FormEvent $event) {
$account = $event->getData();
$form = $event->getForm();
// We might have an empty account (when we insert a new account, for instance)
$province = $account->getCity() ? $account->getCity()->getStates() : null;
$this->addElements($form, $province);
}
}
The error I get is
Catchable Fatal Error: Argument 1 passed to
AppBundle\Form\EventListener\AddStateFieldSubscriber::__construct()
must be an instance of Doctrine\ORM\EntityManager, none given, called
in
/Users/shairyar/Sites/clickjobboard/src/AppBundle/Form/EmployerProfileType.php
on line 48 and defined
I am injecting EntityManager via service then why do I get this error?
If inside EmployerProfileType I replace
$builder->addEventSubscriber(new AddStateFieldSubscriber();
to
$builder->addEventSubscriber(new AddStateFieldSubscriber($this->em));
then things start working fine.
I thought in Symfony we are supposed to inject the dependencies by creating services? So how can I inject EntityManager inside my AddStateFieldSubscriber() class
What am i doing wrong? may be I am over thinking about it.
Will appreciate any feedback.

I think you need to tag your AddStateFieldSubscriber as kernel.event_subscriber.
Then the binding inside the form in not needed at all.
Instead you need to check for the correct form, and jump out of the event listener method if the form is not one of your forms using that subscriber, since the Event would be triggered on any form then, not only the EmployerProfileType form.

Related

Service argument injection: "Argument 1 passed to entity constructor must be must be an instance of Entity, none given"

I'm working with forms on my Symfony 3 app.
I have a ClassType created to create the form of a specific entity:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currencies = $this->doctrine->getRepository('AppBundle:Currency')->findAll();
$currenciesFormat = array();
foreach($currencies as $currency){
$currenciesFormat += array($currency->getName() .' ('. $currency->getShortName() . ')' => $currency);
}
$cycles = $this->doctrine->getRepository('AppBundle:Cycle')->findAll();
$cyclesFormat = array();
foreach($cycles as $cycle){
$cyclesFormat += array($cycle->getName() => $cycle);
}
$builder
->add('currency', ChoiceType::class, array(
'label' => 'Waluta',
'choices' => $currenciesFormat,
))
->add('cycle', ChoiceType::class, array(
'label' => 'Cykl',
'choices' => $cyclesFormat,
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => CyclicNewsletter::class,
));
}
Also I have a universal POST action ment to handle most of the submitting (regullar add to db type stuff). It uses variable classnames to match many forms at once.
For now, all it does it var_dumps the class it is supposed persist into the database:
/**
* #Route("/form/{url}", name="entity_form_post")
* #Method("POST")
*
*/
public function formPostAction(EntityForm $entityForm, Request $request)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm($entityForm->createType())
->add('Dodaj!', SubmitType::class)
;
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$formData = $form->getData();
$em->persist($formData);
var_dump($formData);
exit;
}
}
Now I want the CyclicNewsletter constructor to by default set the currently logged in user as $user field.
For this I have created a service working for the class:
cyclic_newsletter:
class: AppBundle\Entity\CyclicNewsletter
arguments: ["#security.token_storage"]
And made the entity's constructor make use of the argument:
public function __construct(TokenStorage $tokenStorage)
{
$this->user = $tokenStorage->getToken()->getUser();
$this->date = new DateTime();
}
However the output I am getting when submitting the form is:
Type error: Argument 1 passed to
AppBundle\Entity\CyclicNewsletter::__construct() must be an instance
of AppBundle\Entity\TokenStorage, none given, called in
C:\PHP\Repos\centaur\vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Core\Type\FormType.php
on line 136
I have tried using a separate method inside the class, but it too requires an argument.
Is there any way I can make this work?
ps. All cache have been cleared
Edit:
- Running debug:container displays the service properly.
- Searching to my service in appDevDebugProjectContainer.php returns this:
/**
* Gets the 'cyclic_newsletter' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* #return \AppBundle\Entity\CyclicNewsletter A AppBundle\Entity\CyclicNewsletter instance
*/
protected function getCyclicNewsletterService()
{
return $this->services['cyclic_newsletter'] = new \AppBundle\Entity\CyclicNewsletter($this->get('security.token_storage'));
}

Symfony : Can I return null from Type / Form?

I have an entity form with Symfony :
class MyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'LogicielBundle\Entity\FichierGroup',
'intention' => $this->getName() . '_token'
));
}
But in POST_SUBMIT event, I want to return null (no entity).
I tested this but not working :
$builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
.... my condition ...
$event->setData(null);
});
Can you help me ? Thanks :)
Could you please post you controller code?
Do you pass an object reference to createForm, or do you use $form->getData() ? In your case, you should stick to the second.
Try using SUBMIT event instead of POST_SUBMIT. As Symfony doc states it, "It can be used to change data from the normalized representation of the data.".
To my knowledge you can not change a submit form. (I may be wrong)
But if you want reset a form after submit, you can do it in your controller:
For example:
public function fooAction()
{
...
$form = $this->getEntityForm($entity);
...
$form->handleRequest($request);
if ($form->isSubmitted()) {
// Do something
...
// Reset your form with null as entity
$form = $this->getEntityForm(null);
}
...
return $this->render(
'yourfootwig.html.twig',
[
'form' => $form->createView(),
...
]
);
}
protected function getEntityForm($entity = null)
{
return $this->createForm(
'Foo:MyType',
$entity,
[...]);
}
Of course, you must adapt it with your own code.
Actually, if you want change data passed to form entity, the event should be listened on is FormEvents::PRE_SUBMIT. As described in the official doc.

The form's view data is expected to be an instance of another class error creating form instance of another entity

I get the following error when trying to create a form from another entity to pass through to my view.
I have two entities in this context CourseGuide and CourseGuideRow and I would like to pass through a form view of CourseGuideRowType to my view - how can I do this?
The form's view data is expected to be an instance of class
CRMPicco\CourseBundle\Entity\CourseGuide, but is an instance of class
CRMPicco\CourseBundle\Entity\CourseGuideRow. You can avoid this error
by setting the "data_class" option to null or by adding a view
transformer that transforms an instance of class
CRMPicco\CourseBundle\Entity\CourseGuideRow to an instance of
CRMPicco\CourseBundle\Entity\CourseGuide.
This is my controller:
// CourseGuideController.php
public function viewAction(Request $request)
{
if (!$courseId = $request->get('id')) {
throw new NotFoundHttpException('No Course ID provided in ' . __METHOD__);
}
$resource = $this->get('crmpicco.repository.course_guide_row')->createNew();
$form = $this->getForm($resource);
// ...
}
My Symfony FormBuilder class:
// CourseGuideRowType.php
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\Form\FormBuilderInterface;
class CourseGuideRowType extends AbstractResourceType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('channel', 'crmpicco_channel_choice', array('data_class' => null))
->add('name', 'text')
->add('courses', 'text')
;
}
/**
* #return string name
*/
public function getName()
{
return 'crmpicco_course_guide_row';
}
}
I have tried the data_class => null suggestion mentioned elsewhere, but this has no effect.
If I pass through the data_class like this:
$form = $this->getForm($resource, array('data_class' => 'CRMPicco\CourseBundle\Entity\CourseGuideRow'));
I then get this:
Neither the property "translations" nor one of the methods
"getTranslations()", "translations()", "isTranslations()",
"hasTranslations()", "__get()" exist and have public access in class
"CRMPicco\CourseBundle\Entity\CourseGuideRow".
Why is this? There are translations attached to the CourseGuide entity but not the CourseGuideRow.
try to add this function in your FormType:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'YourBundle\Entity\YourEntity',
));
}
And don't forget the specific use:
use Symfony\Component\OptionsResolver\OptionsResolver;
EDIT
In native Symfony (with the Form component):
public function showAction()
{
/.../
$entity = new YourEntity();
$form = $this->createForm('name_of_your_form_type', $entity);
# And the response:
return $this->render('your_template.html.twig', ['form' => $form->createView()]);
}

symfony2 - adding choices from database

I am looking to populate a choice box in symfony2 with values from a custom query. I have tried to simplify as much as possible.
Controller
class PageController extends Controller
{
public function indexAction()
{
$fields = $this->get('fields');
$countries = $fields->getCountries(); // returns a array of countries e.g. array('UK', 'France', 'etc')
$routeSetup = new RouteSetup(); // this is the entity
$routeSetup->setCountries($countries); // sets the array of countries
$chooseRouteForm = $this->createForm(new ChooseRouteForm(), $routeSetup);
return $this->render('ExampleBundle:Page:index.html.twig', array(
'form' => $chooseRouteForm->createView()
));
}
}
ChooseRouteForm
class ChooseRouteForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// errors... ideally I want this to fetch the items from the $routeSetup object
$builder->add('countries', 'choice', array(
'choices' => $this->routeSetup->getCountries()
));
}
public function getName()
{
return 'choose_route';
}
}
You could pass the choices to your form using..
$chooseRouteForm = $this->createForm(new ChooseRouteForm($routeSetup), $routeSetup);
Then in your form..
private $countries;
public function __construct(RouteSetup $routeSetup)
{
$this->countries = $routeSetup->getCountries();
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('countries', 'choice', array(
'choices' => $this->countries,
));
}
Updated (and improved) for 2.8+
Firstly you don't really need to pass in the countries as part of the route object unless they are going to be stored in the DB.
If storing the available countries in the DB then you can use an event listener. If not (or if you don't want to use a listener) you can add the countries in the options area.
Using Options
In the controller..
$chooseRouteForm = $this->createForm(
ChooseRouteForm::class,
// Or the full class name if using < php 5.5
$routeSetup,
array('countries' => $fields->getCountries())
);
And in your form..
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('countries', 'choice', array(
'choices' => $options['countries'],
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('countries', null)
->setRequired('countries')
->setAllowedTypes('countries', array('array'))
;
}
Using A Listener (If the countries array is available in the model)
In the controller..
$chooseRouteForm = $this->createForm(
ChooseRouteForm::class,
// Or the full class name if using < php 5.5
$routeSetup
);
And in your form..
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
$form = $event->getForm();
/** #var RouteSetup $routeSetup */
$routeSetup = $event->getData();
if (null === $routeSetup) {
throw new \Exception('RouteSetup must be injected into form');
}
$form
->add('countries', 'choice', array(
'choices' => $routeSetup->getCountries(),
))
;
})
;
}
I can't comment or downvote yet, so I'll just reply to Qoop's answer here:
What you proposed will work unless you start using your form type class as a service.
You should generally avoid adding data to your form type object through constructor.
Think of form type class like a Class - it's a kind of description of your form. When you make an instance of form (by building it) you get the Object of form that is build by the description in form type and then filled with data.
Take a look at this: http://www.youtube.com/watch?v=JAX13g5orwo - this situation is described around 31 minute of the presentaion.
You should use form event FormEvents::PRE_SET_DATA and manipulate fields when the form is injected with data.
See: http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#customizing-your-form-based-on-the-underlying-data
I got it working by calling getData on the builder
FormBuilderInterface $builder
// Controller
$myCountries = $this->myRepository->all(['continent' => 'Africa']);
$form = $this->createForm(CountriesType::class, $myCountries);
//FormType
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('pages', ChoiceType::class, [
'choices' => $builder->getData()
])
;
}

What is the best practice for building choices list

I have a form with a choice type element. I need to populate it with data. As I know there are 3 methods.
1. Controller:
// Controller
public function myAction()
{
$choices = ...; // create choices array
$form = $this->createForm(new MyFormType($dm), null, array(
'choices' => $choices,
));
}
// Form
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('cars', 'choice', array(
'choices' => $options['choices']
));
}
}
2. Form class + repository
// Controller
public function myAction()
{
$dm = $this->get('doctrine')->getManager();
$form = $this->createForm(new MyFormType($dm));
}
// Form
class MyFormType extends AbstractType
{
private $dm;
public function __construct($dm)
{
$this->dm = $dm;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('cars', 'choice', array(
'choices' => $options['choices']
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$list = array();
foreach($this->dm->getRepository('MyBundle:Cars')->findAll() as $car) {
$list[$car->getName()] = $car->getName();
}
$resolver->setDefaults(array(
'choices' => $list,
));
}
}
3. Form class + custom service
// Controller
public function myAction()
{
$dm = $this->get('doctrine')->getManager();
$form = $this->createForm(new MyFormType(), null, array(
'myservice' => $this->get('myservice'),
));
}
// Form
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('cars', 'choice', array(
'choices' => $options['myservice']->getCars()
));
}
}
// Service
class MyService
{
const ENTITY_CAR = 'MyBundle:Cars';
/** #var DocumentManager */
private $dm;
public function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
public function getCars()
{
return $this->dm->getRepository("MyBundle:Cars")->findAll();
}
}
I'll express my thoughts.
The 1st option is not the best practice. Especially when complicated logic is involved. Controllers should be as tiny as possible.
The 2nd is much better. But it exposes entity name and problems may occur if I decide to rename it.
The 3rd is the best option, imho. Entity names are concentrated in one place, better IDE type hinting, centralized entity management (search, save, remove...). The main disadvantage is a possible over-engineered class as it's becoming responsible for many read/write operations. On the other hand it can be divided into pieces.
What do you think about it?
The third option is good if you have to reuse that service elsewhere in your code (and if that service will grown in comparison of that you've wrote, we'll see it later). In that way, as you said, "manager" of that entity is one and contains itself the name of repo,a const, and so on.
BUT
If this service is use only as a "pusher" for reach your repository by hiding its name, I don't think that this solution is still much good as it seems.
Obviously if that service is thought for have multiple persistance options and multiple retrieve option (base on what ORM you've selected), in that case this could be the best practice.
In other cases, I suppose that the second one is always the better.
The first isn't practicable unless you want to ignore all good practices
I suggest a fourth solution : use an entity field as it is designed to be a choice field with options loaded from DB !
Here is the official doc http://symfony.com/doc/master/reference/forms/types/entity.html
And how you may use it :
// Form
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('cars', 'entity', array(
'class' => 'MyBundle:Cars',
'property' => 'name',
//Optionnal if you need to condition the selection
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')->orderBy('u.username', 'ASC');
},
));
}
}

Categories