Entity form type in Symfony2 - forms

I'm trying to make a RESTFUL api and when I process PUT, it always responded me as
{
"code": 500,
"message": "The form's view data is expected to be an instance of class My\\Bundle\\Entity\\Post, but is a(n) array. You can avoid this error by setting the \"data_class\" option to null or by adding a view transformer that transforms a(n) array to an instance of My\\Bundle\\Entity\\Post."
}
Here is how I write the PUT action.
public function putPostAction($postKey, Request $request){
$post = $this->getDoctrine()->getManager()->getRepository('MyBundle:Post')
->findPost($postKey);
$form = $form = $this->createFormBuilder($post,
array('data_class' => 'My\Bundle\Entity\Post'))->getForm();
// or use createForm like
// $form = $this->createForm(new PostType(), $post);
$form->submit($request);
if($form->isValid()){
$em = $this->getDoctrine()->getManager();
$em->persist($post);
$em->flush();
return $post;
}
return array(
"form" => $form
);
}
Speaking entity type, it looks like
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('cityKey')
->add('status')
->add('text')
->add('imageKey')
->add('createTime')
->add('updateTime')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'My\Bundle\Entity\Post',
'csrf_protection' => false
));
}
/**
* #return string
*/
public function getName()
{
return '';
}
Regarding the error message, I modified the Resolver as
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'csrf_protection' => false
));
}
This time I got another error,
{
"code": 500,
"message": "EntityManager#persist() expects parameter 1 to be an entity object, array given."
}
Any idea? Thanks in advance.

findPost function is returning an array of objects not an object. You should call instead find($id) which returns only one Post object.
So it should be:
$post = $this->getDoctrine()->getManager()
->getRepository('MyBundle:Post')
->find($postKey);

Related

Symfony - How to call the Service Container object from form events?

How to call the Service Container object from form events?
I created a form where the webmaster can create books. I want to create a Sylius Product when an Book is created through this form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, array(
'attr' => array('class'=>'block1')
))
->addEventListener(
FormEvents::PRE_SET_DATA,
array($this, 'onPreSetData')
)
;
}
public function onPreSetData(FormEvent $event)
{
$book = $event->getData();
$form = $event->getForm();
$productFactory = $this->container->get('sylius.factory.product');
}
For that purpose, I need to reach 'sylius.factory.product' service as explained there: http://docs.sylius.org/en/latest/book/products/products.html
$productFactory = $this->container->get('sylius.factory.product');
I can access it from any Controller, but unfortunately I can't access it from the BookType class I defined.
Here is the error returned by Symfony when I try to access it from the buildForm() or onPreSetData() function:
Notice: Undefined property: FrontendBundle\Form\BookType::$container
Pass the service, to the form, via the options
Actually this process is very well explained in Symfony Documentation: http://symfony.com/doc/current/form/form_dependencies.html
So I updated the way I was calling my form like that:
$editForm = $this->createForm($form, $item, array( //WITH SYLIUS
'sylius_factory_product' => $this->get('sylius.factory.product')
));
Instead:
$editForm = $this->createForm($form, $item); //WIHOUT SYLIUS
Plus, I adapted the configureOptions function like that:
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults(array(
'data_class' => 'FrontendBundle\Entity\Book'
))
->setRequired('sylius_factory_product')
;
}
Instead:
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults(array(
'data_class' => 'FrontendBundle\Entity\Book'
))
;
}
You can now access your service in the buildForm function like so:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$productFactory = $options['sylius_factory_product'];
//...
}

How to generate a 'collection' form in Symfony, generated by each object within an entity?

Having read through the Symfony forms collection/entity types I'm trying to generate a form that only involves these two attributes of an entity (Person) but uses all instances of Person in the database. The purpose of the form is to provide a activation tickbox for every person on a single page so that multiple status' can be flushed to the database on form submission. Eg, the form should look something like:
☑ John Smith
☐ Jane Doe
☑ ...etc
☑ ...etc
My attempts below are not working as they just return an empty page, although I can see that $allpersons is populated
My Person entity:
class Person
{
/**
* #ORM\COLUMN(type="boolean")
*/
protected $active = true;
/**
* #ORM\COLUMN(type="string", length=100)
*/
protected $fullname ;
/* ...many other attributes... */
}
My Controller:
class DefaultController extends BaseController {
public function ​activePersonsAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$persons = $em->getRepository('AppBundle:Person')->findAll();
$form = $this->createForm(AllPersonsType::class, $persons);
$form->handleRequest($request);
if ($form->isSubmitted() && ($form->isValid())) {
$em = $this->getDoctrine()->getManager();
$persons = $form->getData();
foreach ($persons as $person) {
$em->persist($person);
}
$em->flush();
return $this->redirectToRoute('home_user');
}
return $this->render('activatePersons.html.twig', array(
'page_title' => 'Active Persons',
'form' => $form->createView(),
));
}
}
My FormTypes:
class AllPersonsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('persons', CollectionType::class, array(
'entry_type' => ActivatePersonType::class
));
}
public function getName()
{
return 'person';
}
}
class ActivatePersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('active',CheckboxType::class)
->add('fullname', TextType::class);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person',
));
}
public function getName()
{
return 'person';
}
}
You pass $persons collection as form data to AllPersonsType, while the type expects that you pass an array with persons key and persons collection as value.
$form = $this->createForm(AllPersonsType::class, ['persons' => $persons]);

Embedding chained form to another form

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 ()));

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.

symfony2 using validation groups in form

I have a Entity with a property:
/**
* #var string $name
*
* #Assert\NotBlank(groups={"foobar"})
* #ORM\Column(name="name", type="string", length=225, nullable=false)
*/
private $name;
The Form:
class MyType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => '...',
'validation_group' => array('foobar'),
);
}
public function getName()
{
...
}
}
In the Controller I bind the Request and call $form->isValid()
But how to define the validation_group?
From inside your FormType class you can define the validation groups associated to that type by setting your default options:
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\MyBundle\Entity\MyEntity',
'validation_groups' => array('group1', 'group2'),
);
}
I had exactly the same problem. I solved it that way ...
// Entity
$employee = new Employee();
// Form creation
$form = $this->createForm(new EmployeeForm(), $employee, array('validation_groups'=>'registration'));
I hope that helps!
When building the form in the controller, add a 'validation_groups' item to the options array:
$form = $this->createFormBuilder($users, array(
'validation_groups' => array('foobar'),
))->add(...)
;
It is described in the forms page of the symfony2 book: http://symfony.com/doc/current/book/forms.html#validation-groups
For me, on symfony 2.1, i solved it by adding 'Default' in validation_groups like:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\MyBundle\Entity\MyEntity',
'validation_groups' => array('Default', 'registration')
));
}
I made a little blog post related to this problem: http://marcjuch.li/blog/2013/04/21/how-to-use-validation-groups-in-symfony/
In this post I’m going to show how to use validation groups in symfony with the example of an order form which should offer the possibility to use separate billing and shipping adresses. This includes 3 steps:
Group validation contstraints for the shipping related form fields together
Determine which validation contraints are applied, depending on the
checkbox value in the submitted form
Copy data from non-shipping fields to shipping fields if checkbox is
not selected
You can also define validation groups dynamically:
// MyCustomType.php
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
if (Client::TYPE_PERSON == $data->getType()) {
return array('person');
}
return array('company');
},
));
}