Embedding chained form to another form - forms

In my application I have two bundles (UserBundle and LocationBundle), there is OneToOne association between them.
LocationFormType has chained country,state,city fields the form works fine independently but when I try to embed the form (LocationFormType) to UserRegistrationForm I don't have access to country object to retrieve related state of country.
Error: Call to a member function getCountry() on a non-object
I like to use the LocationFormType in both mode, embed to another form or independent, Could any body help me to fix my code
//LocationFormType
class LocationFormType extends AbstractType {
protected $em;
function __construct (EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('country', 'entity', array(
'empty_value' => '--Choose--',
'class' => 'AppLocationBundle:Country',
'query_builder' => function(EntityRepository $er)
{
return $er->createQueryBuilder('c')
->where('c.enabled = 1');
},
'label' => 'form.country',
'translation_domain' => 'AppLocationBundle'
));
$builder->addEventSubscriber(new LocationChainedFieldSubscriber($this->em));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'App\LocationBundle\Entity\Location'
));
}
public function getName()
{
return 'app_location_form';
}
}
Location Form Event Subscriber
//EventSubscriber
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormInterface;
use Doctrine\ORM\EntityManager;
use App\LocationBundle\Entity\Country;
use Doctrine\ORM\EntityRepository;
class LocationChainedFieldSubscriber implements EventSubscriberInterface {
protected $em;
function __construct (EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
);
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$location = $event->getData();
// Problem occurs here when try to get data, the event data are null
//How to handle data passed from parent form to child form
//Might have an empty account(when we insert a new country)
$country = $location->getCountry() ? $location->getCountry() : null;
$this->addElement($form, $country);
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
//The data is not yet hydrated into the entity.
$country = $this->em->getRepository('AppLocationBundle:Country')->find($data->getCountry());
$this->addElement($form, $country);
}
protected function addElement(FormInterface $form, Country $country = null)
{
$states = array();
if($country)
{
//fetch the states form specific a country
$repo = $this->em->getRepository('AppLocationBundle:State');
$states = $repo->findByCountry($country, array('name' => 'asc'));
}
//Add the state element
$form->add('state', 'entity', array(
'choices' => $states,
'empty_value' => '--Choose--',
'class' => 'AppLocationBundle:State',
'mapped' => false
));
}
}
Using the Form location as service
services:
app_location.form:
class: App\LocationBundle\Form\Type\LocationFormType
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: form.type, alias: app_location_form }
embed to user registration form
//UserRegistrationForm
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//other fields here
->add('name', 'text')
->add('location', 'app_location_form', array(
'data_class' => 'App\LocationBundle\Entity\Location',
'label' => 'form.location',
'translation_domain' => 'AppUserBundle'
));
public function getName()
{
return 'app_user_profile';
}
}

//UserRegistrationForm
$builder->add('location', 'collection', array('location' => new LocationFormType ()));

Related

How does symfony know what is the type of this object got from form?

I'm using symfony 2.8 with doctrine (and still learning as you will see).
I have this controller in which I create some form and render it.
I don't understand how symfony could possibly know what is the type of the $contract object below.
However those $contract objects end up in my database in the contract table so there must be something (magic?) I don't understand going on.
class MyContractsController extends BaseController
{
public function addAction(Request $request)
{
$form = $this->createForm(MultiNewType::class)->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($form->getData()['contracts'] as $contract) {
$em->persist($contract);
}
$em->flush();
return $this->validResponse();
}
return $this->renderModalFormResponse($form, array(
'formTemplate' => 'MyBundle:Form:Contract/multiNew.html.twig'
));
}
Below the MultiNewType file in which I don't see anything related to contract.
class MultiNewType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('contracts', 'collection', array(
'type' => new NewInlineType(),
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'label' => false
));
}
public function getName()
{
return 'contractMultiNew';
}
}
And the NewInlineType in which I see all the fields of the contract table, but can't see anywhere the contract :
class NewInlineType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('foo', 'bar', array(
'label' => false,
'class' => 'MyBundle:MyClass',
))
->add('format', FormatType::class, array(
'label' => false,
))
->add('version', VersionType::class, array(
'label' => false,
))
->add('debut', 'datePicker', array(
'label' => false,
'placeholder' => 'Début'
))
->add('fin', 'datePicker', array(
'label' => false,
'placeholder' => 'Fin'
));
}
public function getName()
{
return 'contractNew';
}
public function getParent()
{
return 'contractEdit';
}
}
Thanks
Your NewInlineType has a parent form as indicated by the getParent() method.
The parent form in this case will return 'contractEdit' from it's getName() method.
In the parent (or possibly grand-parent) form you should find something like the following:
use AppBundle\Entity\Contract;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Contract::class,
));
}
That pretty much does what it says; setting the default data_class option to your Contract class name.

Symfony2: How call the repository of an entity in the FormType

I tried to call the repository of my entity Category in the class form of my entity BonCommande, but this error ocuured:
Notice: Undefined property: Application\VehiculeBundle\Form\BonCommandeType::$em in C:\wamp\www\Symfony_test\src\Application\VehiculeBundle\Form\BonCommandeType.php line 74
This is my code:
The class BonCommandeType.php:
namespace Application\VehiculeBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Application\VehiculeBundle\Entity\Category;
class BonCommandeType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Name of the user
$builder->add('observation', 'text');
/* Add additional fields... */
$builder->add('save', 'submit');
// Add listeners
$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'));
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
}
protected function addElements(FormInterface $form, Category $categorie = null) {
// Remove the submit button, we will place this at the end of the form later
$submit = $form->get('save');
$form->remove('save');
// Add the province element
$form->add('categorie', 'entity', array(
'data' => $categorie,
'empty_value' => '-- Choose --',
'class' => 'ApplicationVehiculeBundle:Category',
'property' => 'intitule',
'mapped' => false)
);
// Cities are empty, unless we actually supplied a province
$vehicules = array();
if ($categorie) {
// Fetch the cities from specified province
$repo = $this->em->getRepository('ApplicationVehiculeBundle:Vehicule');
$cities = $repo->findByCategory($categorie);
}
// Add the city element
$form->add('vehicule', 'entity', array(
'empty_value' => '-- Select a categorie first --',
'class' => 'ApplicationVehiculeBundle:Vehicule',
'choices' => $vehicules,
));
// Add submit button again, this time, it's back at the end of the form
$form->add($submit);
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// Note that the data is not yet hydrated into the entity.
$categorie = $this->em->getRepository('ApplicationVehiculeBundle:Category')->find($data['categorie']);
$this->addElements($form, $categorie);
}
function onPreSetData(FormEvent $event) {
$account = $event->getData();
$form = $event->getForm();
// We might have an empty account (when we insert a new account, for instance)
$categorie = $account->getVehicule() ? $account->getVehicule()->getCategorie() : null;
$this->addElements($form, $categorie);
}
...
}
This is the instruction that causes the error:
$categorie = $this->em->getRepository('ApplicationVehiculeBundle:Category')->find($data['categorie']);
FormComponent is an independent component and it doesn't provide any entityManager to use. You have to inject it or pass it by $options if you want to use it..
In your case it would be correct if you directly pass it to the type's __construct or pass by $options array or declare your type as a service and inject entity manager to it:
class BonCommandeType extends AbstractType
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
...
}
or
$this->createForm(TYPE, DATA, ['em' => $em]);
From your code I assume you are missing this:
//Somewhere at the begging of your BonCommandeType
protected $em;
...
public function __construct(EntityManager $em)
{
$this->em = $em;
}
Keep in mind that when you create a new form object you should use smth like :
BonCommandeType($this->getDoctrine()->getManager()) // if inside a controller

Display by default the name in an autocomplete field with his id in hidden field

I tried to use a field city with autocomplete. I use twitter bootstrap style and function typeahead but I do not know if my work is good.
For my example, I have 2 entities: Person and City (with approximately 37,000 communes of France).
The Person entity has a OneToMany relationship with the City field Birthplace.
So I create a hidden field type city_typeahead to add a DataTransformer "CityToIdTransformer" to persist an object from the City id sent by the form.
Until then, the autocomplete works perfectly for the creation and editing.
However, in the edit form, I want to show in the autocomplete field the name of the city registered in my hidden field.And this is where I stuck.
I thought trying with a listener, but I'm not sure of the solution to apply. If someone could guide me I shall be grateful.
Thank you.
Edit : I did similar test with autocomplete field country and I arrived after several test.
I create a listener that checks if the hidden field has a value and from it I get the name and load my field autocomplete.
I do not know if the method is clean, but it works, my autocomplete field now displays the name of the country on edit form.
FormType :
<?php
namespace Myapp\PersonBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Myapp\PersonBundle\Form\EventListener\AddNationalitySearchSubscriber;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstname')
->add('lastname')
->add('nationality', 'country_typeahead', array(
'attr' => array('class' => 'country-toset'),
'required' => false))
->add('nationality_search','text', array(
'attr' => array(
'class' => 'country-tosearch',
'name' => 'term',
'autocomplete' => true,
'required' => false,
'mapped' => false));
$builder->addEventSubscriber(new AddNationalitySearchSubscriber());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Myapp\PersonBundle\Entity\Person'
));
}
public function getName()
{
return 'person_person_type';
}
}
Script js in view :
<script type="text/javascript">
$('.country-tosearch').typeahead({
source: function(query, process){
$.ajax({
url: "/ajax/country",
type: "post",
data: "term="+query,
dataType: "json",
async: false,
success: function(data){
countries = []
mapped = {}
$.map(data, function(country, i){
mapped[country.name] = country;
countries.push(country.name);
});
process(countries);
}
})
},
minLength: 3,
property: 'enriched',
items:15,
updater: function (obj) {
if(mapped[obj].id){
$('.country-toset').val(mapped[obj].id);
}
return mapped[obj].name;
}
});
</script>
DataTransformer :
<?php
namespace Myapp\PersonBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Myapp\GeoBundle\Entity\Country;
class CountryToIdTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforms an object (country) to a string (id).
*
* #param Country|null $country
* #return string
*/
public function transform($country)
{
if (null === $country) {
return "";
}
return $country->getId();
}
/**
* Transforms a string (id) to an object (country).
*
* #param string $id
* #return Country|null
* #throws TransformationFailedException if object (country) is not found.
*/
public function reverseTransform($id)
{
if (!$id) {
return null;
}
$country = $this->om
->getRepository('MyappGeoBundle:Country')->findOneBy(array('id' => $id))
;
if (null === $country) {
throw new TransformationFailedException(sprintf(
'A country with id "%s" does not exist!',
$id
));
}
return $country;
}
}
the listener :
<?php
namespace Myapp\PersonBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AddNationalitySearchSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event)
{
$country = '';
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
if ($data->getNationality()) {
$country = $data->getNationality();
}
$form->add('nationality_search','text', array(
'attr' => array(
'class' => 'country-tosearch',
'name' => 'term',
'autocomplete' => true,
'data' => $country,
'required' => false,
'mapped' => false));
}
}

symfony2 forms from entity

Okay, so I'm using symfony2 and I'm trying to create an option box in this form based off another Entity...
So in my controller I have:
<?php
namespace Ecs\CrmBundle\Controller\Report;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Ecs\CrmBundle\Form\Report\TimeClockReportType;
class TimeClockReportController extends Controller
{
public function indexAction()
{
$request = $this->get('request');
$em = $this->getDoctrine()->getEntityManager();
$start = null;
$end = null;
$dateRangeForm = $this->createForm(new TimeClockReportType());
if ($request->getMethod() == 'POST')
{
$dateRangeForm->bindRequest($request);
$formData = $dateRangeForm->getData();
$start = $formData['dateRange']['startDate'];
$end = $formData['dateRange']['endDate'];
}
return $this->render('EcsCrmBundle:Reports:TimeClockReport.html.twig', array(
'start' => $start,
'end' => $end,
'form' => $dateRangeForm->createView(),
));
}
}
and my TimeClockReportType.php looks like:
<?php
namespace Ecs\CrmBundle\Form\Report;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Ecs\CrmBundle\Form\Parts\DateRangeType;
use Ecs\CrmBundle\Form\Parts\DepartmentSelectionType;
class TimeClockReportType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('dateRange', new DateRangeType())
->add('salesCompany', new DepartmentSelectionType(true));
}
public function getName()
{
return 'ecs_crmbundle_TimeClockReportType';
}
}
and my DepartmentSelectionType looks like:
<?php
namespace Ecs\CrmBundle\Form\Parts;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class DepartmentSelectionType extends AbstractType
{
private $canSeeAll = false;
public function __construct($canSeeAll = false)
{
$this->canSeeAll = $canSeeAll;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('department', 'entity',
array(
'class' => "EcsAgentManagerBundle:EmployeeDepartment",
'required' => false,
'multiple' => true,
'expanded' => true,
'label' => "Department"))
;
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Ecs\AgentManagerBundle\Entity\EmployeeDepartment',
);
}
public function getName()
{
return 'ecs_crmbundle_departmentselectiontype';
}
}
The problem that i'm coming up with, is i'm getting just a blank white screen... But, if i remove the:
->add('salesCompany', new DepartmentSelectionType(true)) from the TimeClockReportType it works just fine... But I can't see any errors or why it doesn't work..
A blank page indicates a parse error / engine error has occurred. I would suggest altering your php.ini file to have display_errors on.
Be sure to restart your webserver after.

Multiple forms built from one select

all.
I would like to ask if it is possible to have multiple forms (now per select option) on one page instead of multiple select field.
The situation is: I have User with #ManyToMany bi-relation to Services and 'user_services' relation storage table, but extended with additional fields like min_price, max_price, etc. with UserService Doctrine Entity class.
I think that the better user experience in my particular case is to have a table layout with checkboxes, service names and price fields with one save button, but I can't get how to create multiple forms in which each form corresponds to one option from select list for example and followed by additional fields for this option.
Thanks.
In this case, I think you should use the collection type, which can be used to handle many to many relations.
You want to handle a list of user_services for a given user, if I understand well.
You then have to make something like this:
1, create a UserServiceType that will represent the relation between a user and a service (plus its min_price, max_price, etc):
class UserServiceType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('min_price', 'currency')
->add('max_price', 'currency')
;
}
public function getDefaultOptions()
{
return array('data_class' => 'Entity\UserService');
}
}
2, register it as a form_type:
// app/config/config.yml (for example)
services:
user_service_type:
class: UserServiceType
tags:
- { name: form.type, alias: user_service }
3, configure your UserType to handle this collection:
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('services', 'collection', array(
'type' => 'user_service', // this is the id of the form type registered above
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
))
;
}
public function getDefaultOptions()
{
return array('data_class' => 'Entity\User');
}
}
Imagine that you have a bunch of services and you want to display them
What would you do?
My Solution:
Controller:
class DefaultController extends Controller
{
public function indexAction(Request $request)
{
/** #var $em \Doctrine\ORM\EntityManager */
$em = $this->get('doctrine.orm.entity_manager');
$services = $em->getRepository('ThestudioscheduleProfileBundlesServiceBundle:Service')->findAll();
$user = $this->get('security.context')->getToken()->getUser();
$form = $this->createForm(new UserServiceType($services, $user->getServiceDetails()));
if ('POST' == $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$data = $form->getData();
$em->getConnection()->beginTransaction();
try {
foreach ($data['services'] as $serviceDetails) {
// if service is selected
if ($serviceDetails['id']) {
$serviceDetails['details']->setUser($user);
$serviceDetails['details']->setService($em->getRepository('ThestudioscheduleProfileBundlesServiceBundle:Service')->find($serviceDetails['service']));
$serviceDetails['details'] = $em->merge($serviceDetails['details']);
} else {
// if the entity is exist but user unchecked it - delete it
if ($serviceDetails['details']->getId()) {
$serviceDetails['details'] = $em->merge($serviceDetails['details']);
$em->remove($serviceDetails['details']);
}
}
}
$em->flush();
$em->getConnection()->commit();
// TODO: display success message to user
return $this->redirect($this->generateUrl('ThestudioscheduleProfileBundlesServiceBundle_homepage'));
} catch (\Exception $e) {
$em->getConnection()->rollback();
$em->close();
var_export($e->getMessage());die;
// TODO: log exception
// TODO: display something to user about error
}
}
}
return $this->render('ThestudioscheduleProfileBundlesServiceBundle:Default:index.html.twig', array(
'form' => $form->createView()
));
}
}
UserServiceType:
class UserServiceType extends AbstractType
{
private $services;
private $userServiceDetails;
public function __construct($services, $userServiceDetails)
{
$this->services = $services;
$this->userServiceDetails = $userServiceDetails;
}
/**
* #param \Symfony\Component\Form\FormBuilder $builder
* #param array $options
* #return void
*/
public function buildForm(FormBuilder $builder, array $options)
{
// all application services
$builder->add('services', 'collection');
foreach ($this->services as $key => $service) {
$serviceType = new ServiceType($service, null);
foreach ($this->userServiceDetails as $details) {
if ($service == $details->getService()) {
$serviceType->setUserServiceDetails($details);
}
}
$builder->get('services')->add('service_' . $key, $serviceType);
}
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'profile_user_service';
}
}
ServiceType:
class ServiceType extends AbstractType
{
private $service;
private $userServiceDetails;
public function __construct($service, $userServiceDetails)
{
$this->service = $service;
$this->userServiceDetails = $userServiceDetails;
}
/**
* #param \Symfony\Component\Form\FormBuilder $builder
* #param array $options
* #return void
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('id', 'checkbox', array(
'label' => $this->service->getName(),
'required' => false))
->add('service', 'hidden')
->add('details', new ServiceDetailsType($this->userServiceDetails));
$values = array('service' => $this->service->getId());
if (null !== $this->userServiceDetails) {
$values = array_merge($values, array('id' => true));
}
$builder->setData($values);
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'profile_service';
}
public function setUserServiceDetails($details)
{
$this->userServiceDetails = $details;
}
}
ServiceDetailsType:
class ServiceDetailsType extends AbstractType
{
private $details;
public function __construct($details)
{
$this->details = $details;
}
/**
* #param \Symfony\Component\Form\FormBuilder $builder
* #param array $options
* #return void
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('id', 'hidden')
->add('minPrice', 'money', array('required' => false))
->add('maxPrice', 'money', array('required' => false))
->add('unit', 'choice', array(
'choices' => array(
'hour' => 'Hours',
'photo' => 'Photos'
),
'required' => false
))
->add('unitsAmount', null, array('required' => false));
if (!empty($this->details)) {
$builder->setData($this->details);
}
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Thestudioschedule\ProfileBundles\ServiceBundle\Entity\UserService',
'csrf_protection' => false
);
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
function getName()
{
return 'profile_service_details';
}
}
The most amazing thing that all this works! Many thanks to Florian for spending his time on me and for keeping me thinking about the solution and my apologise for unclear question (if it is). I think, Symfony docs should be updated with more different form embedding/collection examples like this.
Cheers,
Dima.