Display empty form collection item in Symfony without using javascript - symfony

I'm trying to display a form with a collection. The collection should display an empty sub-form. Due to the projects nature I can't rely on JavaScript to do so.
Googling didn't help and I does not seem to work by adding an empty entity to the collection field.
What I have so far:
public function indexAction($id)
{
$em = $this->getDoctrine()->getManager();
$event = $em->getRepository('EventBundle:EventDynamicForm')->find($id);
$entity = new Booking();
$entity->addParticipant( new Participant() );
$form = $this->createForm(new BookingType(), $entity);
return array(
'event' => $event,
'edit_form' => $form->createView()
);
}
In BookingType.php buildForm()
$builder
->add('Participants', 'collection')
In the Twig template
{{ form_row(edit_form.Participants.0.companyName) }}
If I put the line $entity->addParticipant( new Participant() ); in indexAction() I get an error saying:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Yanic\EventBundle\Entity\Participant. You can avoid this error by
setting the "data_class" option to
"Yanic\EventBundle\Entity\Participant" or by adding a view transformer
that transforms an instance of class
Yanic\EventBundle\Entity\Participant to scalar, array or an instance
of \ArrayAccess.
If I delete the said line Twig complains:
Method "0" for object "Symfony\Component\Form\FormView" does not exist in
/Applications/MAMP/htdocs/symfony-standard-2.1/src/Yanic/EventBundle/Resources/views/Booking/index.html.twig
at line 27
EDIT: The addParticipant is the default methos generated by the doctrine:generate:entities command
/**
* Add Participants
*
* #param \Yanic\EventBundle\Entity\Participant $participants
* #return Booking
*/
public function addParticipant(\Yanic\EventBundle\Entity\Participant $participants)
{
$this->Participants[] = $participants;
return $this;
}
I'm sure that I'm doing something wrong, but can't find the clue :-(

I guess you are a bit lost on Symfony2 form collection, though I think you already read http://symfony.com/doc/current/cookbook/form/form_collections.html.
Here I will just emphasize the doc, help other SO readers, and exercise myself a bit on answering question.. :)
First, you must have at least two entities. In your case, Booking and Participant. In Booking entity, add the following. Because you use Doctrine, Participant must be wrapped in ArrayCollection.
use Doctrine\Common\Collections\ArrayCollection;
class Booking() {
// ...
protected $participants;
public function __construct()
{
$this->participants = new ArrayCollection();
}
public function getParticipants()
{
return $this->participants;
}
public function setParticipants(ArrayCollection $participants)
{
$this->participants = $participants;
}
}
Second, your Participant entity could be anything. Just for example:
class Participant
{
private $name;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
}
Third, your BookingType should contain collection of ParticipantType, something like this:
// ...
$builder->add('participants', 'collection', array('type' => new ParticipantType()));
Fourth, the ParticipantType is straightforward. According to my example before:
// ...
$builder->add('name', 'text', array('required' => true));
Last, in BookingController, add the necessary amount of Participant to create a collection.
// ...
$entity = new Booking();
$participant1 = new Participant();
$participant1->name = 'participant1';
$entity->getParticipants()->add($participant1); // add entry to ArrayCollection
$participant2 = new Participant();
$participant2->name = 'participant2';
$entity->getParticipants()->add($participant2); // add entry to ArrayCollection

think you have to add here the type:
->add('Participants', 'collection', array('type' => 'YourParticipantType'));
Could you also paste in here the declaration of your addParticipant function from the model? Seems that there's something fishy too.

Related

how to visit each Entity's property when `serializer.pre_serialize` event is raised

I would like to visit recursively each property of the serializing Entity, check if a string is set and verify that the metadata property is properly set to string, otherwise change it in order to allow the serialization.
Imagine a users property which is an ArrayCollection, but I force the value to be a string in corner cases.
I set a SerializationSubscriber to catch the serializer.pre_serialize event, but I'm not finding any doc for take advantage of the Visitor and surroundings.
Any hint?
class MyEventSubscriber implements JMS\Serializer\EventDispatcher\EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
array('event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize'),
);
}
public function onPreSerialize(JMS\Serializer\EventDispatcher\PreSerializeEvent $event)
{
/*
* #var YourEntity $object
*/
$object = $event->getObject();
$reflect = new \ReflectionClass($foo);
$props = $reflect->getProperties(\ReflectionProperty::IS_PRIVATE);
foreach ($props as $prop) {
$method = 'get'.ucfirst($prop->getName());
// here is call of methods like getId(), getName() etc,
// depending on name of entity properties
$object->$method();
}
}
}

Symfony custom form weird property access errors

I've got this strange problem, here is example usage of my custom ThingType class.
->add('photos', 'namespace\Form\Type\ThingType', [
'required' => false,
])
if the field name is photos everything works as expected, but if I change my entity field to let's say photosi, run generate entities, and change the form field name, this error is thrown:
Neither the property "photosi" nor one of the methods
"addPhotosus()"/"removePhotosus()", "setPhotosi()", "photosi()",
"__set()" or "__call()" exist and have public access in class
"AppBundle\Entity\Product".
I guess the problem comes from Symfony trying to generate getter method name for my entity. Why is this addPhotosus method name generated? How can I solve this?
EDIT:
I'm using model transformer when showing the data to the user.
$builder->addModelTransformer(new CallbackTransformer(
function ($imagesAsText) {
if (!$imagesAsText) {
return null;
}
$newImages = [];
foreach($imagesAsText as $img) {
$newImages[] = $img->getID();
}
return implode(',', $newImages);
},
function ($textAsImages) use ($repo) {
$images = [];
foreach(explode(',', $textAsImages) as $imgID) {
$img = $repo->findOneById($imgID);
if ($img) {
$images[] = $img;
}
}
return $images;
}
));
The actual field is TextType::class with entity ids in it for example 1,10,32,51. The model transformer transforms this data to entities. Setting 'data_class' to my form type seems irrelevant, because the actual form type is a part of entity. I mean I have Product entity and Photo entity, photos is array of photo entity. So in my ThingType, what data_class should I use, photo or product?
Thanks
The fist parameter of the add method for a form, should be one of the mapped attributes of the data_class of the form, usually selected inside the form as
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
That isn't related to the form name. So , you are trying to access to a "photosi" attribute inside your Product class.
Hope this help you.
Ok so for the first point you need to remember that Symfony is looking for setXX() and getXX()method in your entity for each entry of your form.
If you change your variable name you need to update the form :
->add('newName', XXType::class, [
'required' => false,
])
and you're entity by changing the variable
class Entity
{
/**
* #ORM\Column(type="string", length=255)
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName(oldName){
$this->oldName = $oldName;
return $this
}
}
then run the command
php bin/console make:entity --regenerate
and symfony will upload your entity by itself
class Entity
{
/**
* #ORM\Column(type="string", length=255)
* #SerializedName("title")
* #Groups({"calendar"})
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName($oldName){
$this->oldName = $oldName;
return $this
}
public function getNewName(){
return $this->newName;
}
public function setNewName($newName){
$this->newName = $newName;
return $this
}
note that the old get and set method are not deleted by the script
note as well that in your specific case of photosi, symfonyguess that the "i" is a plural mark and look for addPhotosus() methods
For the edit it looks very unclear and has nothing to do with the first question. Consider reading : doc on collectionType

Symfony 2.7: Automatically Populate Form Field from Within Form Builder

I have an entity named HoursSpecial with a foreign key relationship to an entity called HoursArea. Each HoursSpecial belongs to an HoursArea. When I create a new HoursSpecial via my HoursSpecialType, I want the form field to automatically populate the HoursArea field.
I know what you're thinking, just do something like this in my controller's method:
$form->add('area', 'hidden', array('data'=>$area));
That would be fine except I need to make a DataTransformer to switch between the area's ID and the actual area entity. So I have to declare my HoursArea field within my HoursSpecialType with the transformer:
$builder
...
->add('area', 'hidden')
;
$builder->get('area')->addModelTransformer(new HoursAreaToIntTransformer($this->manager));
Now, I can't simply feed my HoursArea entity into the form. Is there an effective way to make this happen?
I've thumbed through Symfony's documentation on How to Dynamically Modify Forms Using Form Events, but I can't make heads or tails of how I would pass in that HoursArea entity dynamically from outside of the form builder. Maybe I'm just missing something?
UPDATE
Following the recommendation of the answer (Recommendation #1) below from #Ryan, I have created the custom type HiddenHoursAreaType:
// AppBundle\Form\Type\HideenHoursAreaType.php
class HiddenHoursAreaType extends AbstractType
{
//need to instantiate HoursAreaToIntTransformer
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new HoursAreaToIntTransformer($this->manager);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'compound' => true //this should be FALSE as there are no children
));
}
/**
*
* the return value of the getParent function indicates that you're extending the choice field type.
* This means that, by default, you inherit all of the logic and rendering of that field type.
*/
public function getParent()
{
return 'hidden';
}
public function getName()
{
return 'app_hoursArea';
}
I have added my transformer into the custom type class. Here is the transformer class:
// AppBundle\Form\DataTransformer;
class HoursAreaToIntTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms an object (HoursArea) to a string (number).
*
* #param Issue|null $issue
* #return string
*/
public function transform($area)
{
if (null === $area) {
return '';
}
return $area->getId();
}
/**
* Transforms a string (number) to an object (HoursArea).
*
* #param string $areaId
* #return HoursArea|null
* #throws TransformationFailedException if object (HoursArea) is not found.
*/
public function reverseTransform($areaId)
{
// no area number? It's optional, so that's ok
if (!$areaId) {
return;
}
$area = $this->manager
->getRepository('AppBundle:HoursArea')
// query for the issue with this id
->find($areaId)
;
if (null === $area) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'An area with number "%s" does not exist!',
$areaId
));
}
return $area;
}
}
Within my controller I create the form with the custom type field:
$form = $this->createForm(new HoursSpecialType($this->getDoctrine()->getManager()), $entity, array(
'action' => $this->generateUrl('hoursspecial_postcreate'),
'method' => 'POST',
));
$form->add('eventDate', 'hidden', array('data'=>$dateString));
$form->add('area', new \AppBundle\Form\Type\HiddenHoursAreaType($this->getDoctrine()->getManager()), array(
'data'=>$area,
'invalid_message'=>'Area field not converted proerly'
));
$form->add('submit', 'submit', array('label' => 'Create'));
Thanks to the transformer and the custom type, the form now correctly converts the HoursArea entity to an integer for population in the hidden field.
The problem now is that upon form submission, the integer is not converted back into an HoursArea object. I know this because I get the 'invalid_message' upon submission.
Final Update
The reason the HoursArea id wasn't being inserted properly had something to do with the
'compound' => true
setting I had in my custom type. I assume it was looking for child fields and wasn't finding any...which it shouldn't have because there were none!
You could create a custom type for it and add the addModelTransformer() call in the buildForm() of your custom type, but still pass the data in explicitly. So your $form->add('area', 'hidden', array('data'=>$area)) would become $form->add('area', new HiddenHoursAreaType(), array('data'=>$area)) where HiddenHoursAreaType::getParent() would be the hidden type.
You could set the data in a POST_SET_DATA listener.
You could get the $options['data'] value in buildForm() and explicitly pass in the HoursArea ID.
/** #var HoursSpecial $hoursSpecial Prepopulated in controller */
$hoursSpecial = $options['data']
$builder->add('area', 'hidden', ['data' => $hoursSpecial->getHoursArea()->getId()])

Symfony2 Forms - How to use parametrized constructors in form builders

I am learning to use Symfony2 and in the documentation I have read, all entities being used with Symfony forms have empty constructors, or none at all. (examples)
http://symfony.com/doc/current/book/index.html Chapter 12
http://symfony.com/doc/current/cookbook/doctrine/registration_form.html
I have parametrized constructors in order to require certain information at time of creation. It seems that Symfony's approach is to leave that enforcement to the validation process, essentially relying on metadata assertions and database constraints to ensure that the object is properly initialized, forgoing constructor constraints to ensure state.
Consider:
Class Employee {
private $id;
private $first;
private $last;
public function __construct($first, $last)
{ .... }
}
...
class DefaultController extends Controller
{
public function newAction(Request $request)
{
$employee = new Employee(); // Obviously not going to work, KABOOM!
$form = $this->createFormBuilder($employee)
->add('last', 'text')
->add('first', 'text')
->add('save', 'submit')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
Should I not be using constructor arguments to do this?
Thanks
EDIT : Answered Below
Found a solution:
Looking into the API for the Controllers "createForm()" method I found something that is not obvious from the examples. It seems that the second argument is not necessarily an object:
**Parameters**
string|FormTypeInterface $type The built type of the form
mixed $data The initial data for the form
array $options Options for the form
So rather than pass in an instance of the Entity, you can simply pass in an Array with the appropriate field values:
$data = array(
'first' => 'John',
'last' => 'Doe',
);
$form = $this->createFormBuilder($data)
->add('first','text')
->add('last', 'text')
->getForm();
Another option (which may be better), is to create an empty data set as a default option in your Form Class.
Explanations here and here
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first');
$builder->add('last');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => new Employee('John', 'Doe'),
));
}
//......
}
class EmployeeFormController extends Controller
{
public function newAction(Request $request)
{
$form = $this->createForm(new EmployeeType());
}
//.........
}
Hope this saves others the head scratching.

Passing custom variable to form validation

I try to pass my variable to constraint in form validator, but can't.
i'm doing that:
$payForm = $this->createForm(new CableTVPayType(), null, array('balance' => $balance));
And in CableTVPayType:
public function getDefaultOptions(array $options)
{
$maxSumm = $options['balance'] - 100;
[...]
It works fine, my maxSumm is what i want, but Symfiony checks $options array. 'balance' isn't a default option, and complain about this:
The option "balance" does not exist
Is there another, more right way to pass custom variable to validation?
Use the constructor for stuff to be used by all instances of a type. For example, your type might need an entity manager for it to work. It will be reused across all the form instances.
For instance specific stuff use options. If you use the constructor for instance specific stuff, all the instances will get the value you pass to the constructor of the first instance.
/**
* #FormType
*/
class PayType extends AbstractType {
private $someService;
/**
* #InjectParams
*/
public function __construct(SomeService $someService)
{
$this->someService = $someService;
}
public function getDefaultOptions(array $options)
{
return array(
'balance' => 0
);
}
public function getName()
{
return 'pay';
}
}
$form = $this->createForm('pay', null, array('balance' => $balance));
Note that the #FormType annotation registers the type as a service. It allows you to use the type's name instead of creating an instance manually. It gets even more convenient when a type needs a service to be injected into it. You use just the name — pay in this case — instead of something like this:
$form = $this->createForm(new PayType($this->get('some_service')), null, array(
'balance' => $balance
));
Done with this!
Crate variable for a class, and passing value to it through construct method
class CableTVPayType extends AbstractType {
private $maxSumm;
public function __construct($maxSumm) {
$this->maxSumm = $maxSumm;
}
Create form with argument
$payForm = $this->createForm(new CableTVPayType($someValue));
Now i can use this variable as i want in my form.

Resources