How to nest Symfony form classes? - forms

I am having issues figuring out the logic of how to create nested forms for the three entities I have: Films, Actors and Locations. I generated my Symfony entities (+orm.xml) from my database following the instructions in the symfony docs here.
My ultimate goal would to be have one page where the user can perform any of the following actions:
Create a new Films object
Select Films from a dropdown menu, and then create a new Actors object to associate to it
Select Films from a dropdown menu, and then create a new Locations object to associate to it
(Actors and Locations both have a 1-to-many join with the Films table)
However, I've been struggling with the concept of nested forms in Symfony for a long time and in order to "walk before I can run" I'm just trying to put each of the above into separate routes with separate forms:
/newfilm
/newactor
/newlocation
/New-film I can get working without problem. However, with either of the other two, anything I try doesn't seem to work. The below is my code, if someone can explain the "theory" of nested forms in Symfony to avoid keep hitting this wall would be very much appreciated...!
As my problem is the same for both Actor and Location, I'm only putting the code for for Actors (and Films) as I realise it's quite a lot already:
~~~~~Controller~~~~~
It is this second route (/newactor) which has the embedded/nested formType:
class DefaultController extends FOSRestController
{
/**
* #Route("/newfilm", name="new_film")
*/
public function newFilmAction(Request $request)
{
$film = new Films();
$form = $this->CreateFormBuilder($film)
->add('film_title','text',array('label'=>'Film title'))
->add('Save','submit',array('label'=>'Add new film'))
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($film);
$em->flush();
return $this->redirectToRoute('success_addFilm');
}
return $this->render('AppBundle:Default:newfilm.form.html.twig', array(
'form' => $form->createView(),
));
}
/**
* #Route("/newactor", name="new_actor")
*/
public function newActorAction(Request $request)
{
$actor = new Actors();
$form = $this->createForm(new ActorType(), $actor);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($actor);
$em->flush();
return $this->redirectToRoute('success_addActor');
}
return $this->render('AppBundle:Default:newactor.form.html.twig', array(
'form' => $form->createView(),
));
}
}
~~~~~Films~~~~~
Films.php
/**
* Films
*/
class Films
{
/**
* #var integer
*/
private $filmid;
/**
* #var string
*/
private $film_title;
/**
* #var \AppBundle\Entity\Actors
*/
private $actor;
/**
* Get filmid
* #return integer
*/
public function getFilmid()
{
return $this->filmid;
}
/**
* Get film_title
*
* #return string
*/
public function getFilm_title()
{
return $this->film_title;
}
/**
* Set film_title
* #param string $film_title
* #return Films
*/
public function setFilm_title($film_title)
{
$this->film_title = $film_title;
return $this;
}
/**
* Set actor
*
* #param \AppBundle\Entity\Actors $actor
*
* #return Actors
*/
public function setActor(\AppBundle\Entity\Actors $actor = null)
{
$this->actor = $actor;
return $this;
}
/**
* Get actor
*
* #return \AppBundle\Entity\Actors
*/
public function getActor()
{
return $this->actor;
}
}
Films.orm.xml
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="AppBundle\Entity\Films" table="Films">
<indexes>
<index name="fk_Films_actors1_idx" columns="actor_id"/>
</indexes>
<id name="filmid" type="integer" column="filmid">
<generator strategy="IDENTITY"/>
</id>
<field name="film_title" type="text" column="film_title" length="65535" nullable="false">
<options>
<option name="fixed"/>
</options>
</field>
<many-to-one field="actor" target-entity="Actors" fetch="LAZY">
<join-columns>
<join-column name="actor_id" referenced-column-name="actorid"/>
</join-columns>
</many-to-one>
</entity>
</doctrine-mapping>
FilmType.php
class FilmType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('film_title');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class'=>'AppBundle\Entity\Films'
));
}
public function getName()
{
return 'film';
}
}
~~~~~Actors~~~~~
Actors.php
/**
* Entries
*/
class Entries
{
/**
* #var integer
*/
private $actorid;
/**
* #var string
*/
private $actorName;
/**
* Set actorid
*
* #param integer $actorid
*
* #return Actors
*/
public function setActorid($actorid)
{
$this->actorid = $actorid;
return $this;
}
/**
* Get actorid
*
* #return integer
*/
public function getActorid()
{
return $this->actorid;
}
/**
* Set actorName
*
* #param string $actorName
*
* #return Actors
*/
public function setActorName($actorName)
{
$this->actorName = $actorName;
return $this;
}
/**
* Get actorName
*
* #return string
*/
public function getActorName()
{
return $this->actorName;
}
}
Actors.orm.xml
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="AppBundle\Entity\Actors" table="Actors">
<id name="actorid" type="integer" column="actorid">
<generator strategy="IDENTITY"/>
</id>
<field name="actorName" type="text" column="actorName" length="65535" nullable="true">
<options>
<option name="fixed"/>
</options>
</field>
</entity>
</doctrine-mapping>
ActorType
class ActorType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('actorName')
->add('film','entity',array(
'class'=>'AppBundle:Films',
'query_builder'=>function(EntityRepository $er) {
return $er->createQueryBuilder('f')
->orderBy('f.film_title','ASC');
}
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class'=>'\AppBundle\Entity\Actors'
));
}
public function getName()
{
return 'actor';
}
}
My current error message is:
Catchable Fatal Error: Object of class AppBundle\Entity\Films could not be converted to string
500 Internal Server Error - ContextErrorException
I have read answers that say to add in the function to my Films.php:
public function __toString() {
return $this->name;
}
However, when I do that, I then get the error:
Error: Method AppBundle\Entity\Films::__toString() must not throw an exception
Other possible ideas I've come across online (but unfortunately with no success) are:
Setting the Forms as services
Data transformers

Add 'choice_label' => 'film_title', to your form builder. That or you can implement a __toString() function within your Film entity that returns the film title.
Solution:
$builder
->add('actorName')
->add('film','entity',array(
'class'=>'AppBundle:Films',
'choice_label' => 'film_title',
'query_builder'=>function(EntityRepository $er) {
return $er->createQueryBuilder('f')
->orderBy('f.film_title','ASC');
}
));
Also, you may want to keep your entity names singular (eg: Film, not Films; Actor not Actors) as this may cause unnecessary issues when dealing with x-to-many entity relationships.

Related

How to get data in a form event within a CollectionType?

I have a problem with Symfony 4 on an issue already identified and described on Github (here: https://github.com/symfony/symfony/issues/5694#issuecomment-110398953) but I can't find a way to apply this answer.
When I try to use a POST_SET_DATA form event in a ChildType form, the function getData() gives me a null value because the "allow_add" option is set on true in the ParentType form which is a CollectionType.
I have 3 collections: Page, Moduling and Module. The Moduling document is used to embed a collection of Module forms. The purpose is to be able to add multiple forms to the Page collection with one request, following this Symfony article: https://symfony.com/doc/current/form/form_collections.html.
I have 2 different embedded documents: Tag and Task. Both of them are embedded in the Module document (EmbedOne). What I want to do is to be able to custom the ModuleType field with a form event listener so that I just need to set the title of the Module in the controller and then Symfony knows it needs to use the TaskType or the TagType within the ModuleType.
So first, here is my controller
class TaskingController extends Controller
{
/**
* The controller from which I set the module title, "task" here
*
* #Route("/{slug}/task/create", name="tasking_create")
*
* #ParamConverter("page", options={"mapping": {"slug": "slug"}})
*
* #return Response
*/
public function createTasking(DocumentManager $dm, $id, Module $module, Moduling $moduling)
{
$page = $dm->find(Page::class, $id);
$module->setTitle('task');
$moduling->addModule($module);
$page->addModuling($moduling);
$form = $this->createForm(ModulingType, $moduling);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted() {
// Form validation then redirection
}
// Render form template}
}
}
Now, here are my three collections: pages, moduling and modules
/**
* My page document
*
* #MongoDB\Document(collection="pages")
*/
class Page
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
protected $id;
/**
* #MongoDB\ReferenceMany(targetDocument="App\Document\Moduling")
*
* #var Moduling
*/
protected $moduling = array();
public function __construct()
{
$this->moduling = new ArrayCollection();
}
/**
* Get the value of id
*/
public function getId()
{
return $this->id;
}
/**
* #return Collection $moduling
*/
public function getModuling()
{
return $this->moduling;
}
/**
* #param Moduling $moduling
*/
public function addModuling(Moduling $moduling)
{
$this->moduling[] = $moduling;
}
/**
* #param Moduling $moduling
*/
public function removeModuling(Moduling $moduling)
{
$this->moduling->removeElement($moduling);
}
}
/**
* #MongoDB\Document(collection="moduling")
*/
class Moduling
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
protected $id;
/**
* #MongoDB\ReferenceOne(targetDocument="App\Document\Page", storeAs="id")
*
* #var Page
*/
protected $parentPage;
/**
* #MongoDB\ReferenceMany(targetDocument="App\Document\Module", mappedBy="moduling")
*/
protected $module = array();
public function __construct()
{
$this->module = new ArrayCollection();
}
/**
* Get the value of id
*/
public function getId()
{
return $this->id;
}
public function getModule()
{
return $this->module;
}
public function addModule(Module $module): self
{
$this->module[] = $module;
}
public function removeModule(Module $module)
{
$this->module->removeElement($module);
}
/**
* Get the value of parentPage
*
* #return Page
*/
public function getParentPage()
{
return $this->parentPage;
}
/**
* Set the value of parentPage
*
* #param Page $parentPage
*
* #return self
*/
public function setParentPage(Page $parentPage)
{
$this->parentPage = $parentPage;
return $this;
}
}
/**
* #MongoDB\Document(collection="modules")
*/
class Module
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
public $id;
/**
* #MongoDB\Field(type="string")
*/
public $title;
/**
* #MongoDB\ReferenceOne(targetDocument="App\Document\Moduling", inversedBy="module", storeAs="id")
*/
public $moduling;
/**
* #MongoDB\EmbedOne(targetDocument="App\Document\Task", strategy="set")
* #Assert\Valid
*/
public $task;
public function getTitle()
{
return $this->title;
}
/**
* #return self
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTask()
{
return $this->task;
}
public function setTask(Task $task = null)
{
$this->task = $task;
}
}
My embedded document Task. The Tag document has the same structure.
/**
* #MongoDB\EmbeddedDocument
*/
class Task
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
protected $id;
public function getId()
{
return $this->id;
}
}
My ModulingType, which is a collection of ModuleType
class ModulingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('module', CollectionType::class, [
'entry_type' => ModuleType::class,
'entry_options' => [
'label' => false,
],
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Moduling::class
]);
}
}
class ModuleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
$module = $event->getData();
$form = $event->getForm();
if ('task' == $module->getTitle()) {
$form->add('task', TaskType::class);
}
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Module::class
]);
}
}
So I have identified the problem. When I try to make this work, Symfony sends me this error message: "Call to a member function getTitle() on null". It seems the getData() doesn't get anything.
Actually, after reading few posts on Github I've realized that the "allow_add" option set on "true" was the origin of this issue. And indeed, when I set it on "false" I don't have any error message. But the consequence of this is that my JQuery doesn't allow me to duplicate the form if I want to, the "allow_add" option is necessary to do that.
In the Github post I uploaded, they say that the solution is to write this code first in the ModuleType:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
if (null != $event->getData()) {
}
}
It's what I did but it doesn't change anything. I wrote this, followed by the code written in the ModuleType but I still have the same error message... Perhaps I don't know how to insert it correctly in the ModuleType.
I hope someone has a solution. I know I can still add the Tag and Task types directly in the ModulingType but I would have more collections.
Thanks a lot for helping me, I hope I've been clear enough!
Cheers
Did you tried this:
if (!is_null($module) && 'task' == $module->getTitle()) {
$form->add('task', TaskType::class);
}
So actually I found a solution, I was really closed, but I got a new problem...
class ModuleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$module = $event->getData();
$form = $event->getForm();
if (null != $event->getData()) {
if ('task' == $module->getTitle()) {
$form->add('task', TaskType::class);
}
}
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Module::class
]);
}
}
This was the solution so as you can see it was not that complicated BUT the fact of using a form event in the ModuleType creates a new issue.
In my ModulingType, I add an option
'allow_add' => true,
This really useful tool allows to automatically add a "data-prototype" in my form so that I can copy/past some jQuery lines available here (https://symfony.com/doc/current/form/form_collections.html) and then be able to duplicate or delete my form. However, when using a form event, the data-prototype doesn't register anything as it is created before my TaskType.
So after spending hours reading discussions on Github and trying to find the solution, I came to the conclusion I had to create a TaskingType and a TagingType, which look like this:
class TaskingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('task', TaskType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Module::class
]);
}
}
So ok, this solution is not perfect and I have some code duplication. But at least it allows me to only have 3 collections: Page, Moduling and Module.
If someone finds an elegant way to manage everything with one form without deleting the content available in data-prototype, please keep me posted :)

How to extend register form by additional fields in FOSUserBundle and Symfony3

I'm learning Symfony3 framework. At now I've build project that contain two Entities: User and Catalog. Catalog entity is for additional Company data (company name, address, and so on) - it's like a business card. Single User (but not all) is connected only with one Business Card (Catalog entity) and that's why I've decided to use two separate entities. Some users have access e.g. to backend and other ones have possibility to add and manage their one Business Card.
I want to allow user for fill in his Company details while he is registering. I'm using FOSUserBundle for User entity. At now I have registration form working but I'm stuck and need Your help with embed my CatalogType form.
CatalogType form:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CatalogType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('address')
->add('city')
->add('province')
->add('postcode')
->add('telephone')
->add('fax')
->add('mobile')
->add('email')
->add('webpage')
->add('misc')
;
}
public function getParent()
{
return 'FOS\UserBundle\Form\Type\RegistrationFormType';
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'app_user_registration';
}
}
User entity:
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User extends BaseUser
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var
*
* #ORM\OneToMany(targetEntity="Comment", mappedBy="user")
*/
private $comments;
/**
* #var
*
* #ORM\OneToOne(targetEntity="Catalog", mappedBy="user")
*/
private $catalog;
public function __construct()
{
parent::__construct();
$this->comments = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Add comment
*
* #param \AppBundle\Entity\Comment $comment
*
* #return User
*/
public function addComment(\AppBundle\Entity\Comment $comment)
{
$this->comments[] = $comment;
return $this;
}
/**
* Remove comment
*
* #param \AppBundle\Entity\Comment $comment
*/
public function removeComment(\AppBundle\Entity\Comment $comment)
{
$this->comments->removeElement($comment);
}
/**
* Get comments
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments()
{
return $this->comments;
}
/**
* Set catalog
*
* #param \AppBundle\Entity\Catalog $catalog
*
* #return User
*/
public function setCatalog(\AppBundle\Entity\Catalog $catalog = null)
{
$this->catalog = $catalog;
return $this;
}
/**
* Get catalog
*
* #return \AppBundle\Entity\Catalog
*/
public function getCatalog()
{
return $this->catalog;
}
}
Catalog entity
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Catalog
*
* #ORM\Table(name="catalog")
* #ORM\Entity(repositoryClass="AppBundle\Repository\CatalogRepository")
*/
class Catalog
{
// ... definition a lot of private variables ...
/**
* #var
*
* #ORM\OneToOne(targetEntity="User", inversedBy="catalog")
* #ORM\JoinColumn(name="user_id", nullable=true)
*/
private $user;
// ...
/**
* Set user
*
* #param \AppBundle\Entity\User $user
*
* #return Catalog
*/
public function setUser(\AppBundle\Entity\User $user = null)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* #return \AppBundle\Entity\User
*/
public function getUser()
{
return $this->user;
}
Services config file:
// app/config/services.yml
services:
app.form.registration:
class: AppBundle\Form\CatalogType
tags:
- { name: form.type, alias: app_user_registration }
When I'm trying to display register form under http://localhost:8000/register/
I'm getting an error:
Neither the property "name" nor one of the methods "getName()",
"name()", "isName()", "hasName()", "__get()" exist and have public
access in class "AppBundle\Entity\User".
I know where is the problem, but I don't know how to properly solve it so it would be great if somebody can help me where I should looking for solutions or how it should be solved.
Thanks.
CatalogType should not extend RegistrationFormType. It should be form type for AppBundle\Entity\Catalog.
You should make form type, that is based on User class (subclass of FOS\UserBundle\Model\User), and that embeds CatalogType form.
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('...', ...)
->add('tasks', CollectionType::class, array(
'entry_type' => CatalogType::class,
...
));
...
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User',
));
}
public function getBlockPrefix()
{
return 'app_user_registration';
}
}
class CatalogType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('address')
->add('city')
->add('province')
->add('postcode')
->add('telephone')
->add('fax')
->add('mobile')
->add('email')
->add('webpage')
->add('misc')
;
}
...
}
More on embedding forms: https://symfony.com/doc/current/form/form_collections.html
More on overriding FosUserBundle forms: http://symfony.com/doc/current/bundles/FOSUserBundle/overriding_forms.html

FormType and FormHandler as services in Symfony3

So I'm trying to use a FormType with a FormHandler through their respective services to get a pretty clean Form flow.
Now the problem is that I get an error in Symfony 3.0:
Expected argument of type "string", "AppBundle\Form\Type\CurrencyType" given
I'm not really sure how to work with this in the new version.
The FormType:
/**
* Class CurrencyType
* #package AppBundle\Form\Type
*/
class CurrencyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('displayName', TextType::class)
->add('symbol', TextType::class)
->add('save', SubmitType::class, array('label' => 'Voeg Currency toe'))
;
}
public function setDefaultOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Currency'
));
}
}
The Form Handler:
class CurrencyFormHandler
{
/**
* #var Request
*/
protected $requestStack;
/**
* #var CurrencyManager
*/
protected $currencyManager;
/**
* #var FormInterface
*/
protected $form;
/**
* CurrencyFormHandler constructor.
* #param FormInterface $form
* #param RequestStack $requestStack
* #param CurrencyManager $currencyManager
*/
public function __construct(FormInterface $form, RequestStack $requestStack, CurrencyManager $currencyManager)
{
$this->form = $form;
$this->requestStack = $requestStack;
$this->currencyManager = $currencyManager;
}
/**
* Process the form
*
* #param Currency|null $currency
* #return bool
*/
public function process(Currency $currency = null)
{
if (null === $currency) {
$holiday = $this->currencyManager->create();
}
$this->form->setData($currency);
$this->form->handleRequest($this->requestStack->getCurrentRequest());
if ($this->form->isValid()) {
$this->onSuccess($currency);
return true;
}
return false;
}
/**
* When the form is succesfully posted
*
* #param Currency $currency
*/
public function onSuccess(Currency $currency)
{
$this->currencyManager->save($currency);
}
}
The services:
<service id="app.currency.form" class="Symfony\Component\Form\Form">
<argument>app_currency</argument>
<argument type="service" id="app.currency.form.type" />
<factory method="createNamed" service="form.factory" />
</service>
<service id="app.currency.form.type" class="AppBundle\Form\Type\CurrencyType">
<tag name="form.type" alias="currencyform"/>
</service>
<service id="app.currency.form.handler"
class="AppBundle\Form\Handler\CurrencyFormHandler">
<argument type="service" id="app.currency.form" />
<argument type="service" id="request_stack" />
<argument type="service" id="app.currency.manager"/>
</service>
And the controller:
/**
* Class AddController
* #package AppBundle\Controller\Currency
*/
class AddController extends Controller
{
/**
* #Route("/currency/add", name="currency_add")
* #param Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function indexAction(Request $request)
{
$form = $this->get('app.currency.form');
$formHandler = $this->get('app.currency.form.handler');
$process = $formHandler->process();
if ($process) {
$this->get('session')->getFlashBag()->add('success', 'Currency succesvol toegevoegd');
return $this->redirect($this->generateUrl('currency_overview'));
}
return $this->render(':currency:add.html.twig', [
'form' => $form->createView(),
]);
}
}
So what exactly is wrong here? This used to work all the time in Symfony 2
Your Form handler doesn't look right.
You've declared:
/**
* #var CurrencyManager
*/
protected $currencyManager;
But this I believe by default is a string.
Then you do this:
public function onSuccess(Currency $currency)
{
$this->currencyManager->save($currency);
}
Trying to set it to a "Currency" instead of a "string"!

Symfony2 (2.7) Form Entity Data Transformer

I'm trying to customize a selection list's text while using the entity's ID. This is because I want the list options to be specific to the authenticated user. The database text values are Full Name, By City and State, and Anonymous, but I want it to actually display the user's full name (John Smith), User in Denver, CO, and Anonymous. I'm attempting to use a view data transformer to achieve this, but with no luck. I'd rather not use Javascript to achieve this if possible.
Here's my main form type:
<?php
namespace Members\MessagesBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\SecurityContext;
class MessageType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('viewability', 'viewability_entity', array(
'class' => 'MessagesBundle:Viewability',
'property' => 'name',
'required' => true,
))
->add('body', new MessageBodyType())
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Members\MessagesBundle\Entity\Message',
));
}
/**
* #return string
*/
public function getName()
{
return 'members_messages_message';
}
}
Here's my custom form type for Viewability (the entity which I would like to transform):
<?php
namespace Members\MessagesBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\SecurityContext;
use Members\MessagesBundle\Form\DataTransformer\MessageNameTransformer;
class ViewabilityType extends AbstractType
{
private $context;
/**
* #param SecurityContext $context
*/
public function __construct(SecurityContext $context)
{
$this->context = $context;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new MessageNameTransformer($this->context);
$builder->addViewTransformer($transformer);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'The selected issue does not exist',
));
}
/**
* #return string
*/
public function getParent()
{
return 'entity';
}
/**
* #return string
*/
public function getName()
{
return 'viewability_entity';
}
}
Here's my service which defines the Viewability Type:
members.messages.form.type.viewability_entity:
class: Members\MessagesBundle\Form\ViewabilityType
tags:
- { name: form.type, alias: viewability_entity }
arguments: [#security.context]
Here's my Viewability Entity:
<?php
namespace Members\MessagesBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class Viewability
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
public function __construct()
{
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
}
Finally, here's my data transformer:
<?php
namespace Members\MessagesBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Members\MessagesBundle\Entity\Viewability;
use Symfony\Component\Security\Core\SecurityContext;
class MessageNameTransformer implements DataTransformerInterface
{
private $user;
/**
* #param SecurityContext $context
*/
public function __construct(SecurityContext $context)
{
$this->user = $context->getToken()->getUser();
}
/**
* #param Viewability|null $viewability
* #return string
*/
public function transform($viewability)
{
if (null === $viewability) {
return '';
}
if($viewability === 'Full Name')
return sprintf('%s %s', $this->user->getInfo()->getFirstName(), $this->user->getInfo()->getLastName());
if($viewability === 2)
return sprintf('Lawyer in %s, %s', $this->user->getInfo()->getAddress()->getCity(), $this->user->getInfo()->getAddress()->getState());
if($viewability === 3)
return 'Anonymous';
}
/**
* #param Viewability $viewability
* #return Viewability
*/
public function reverseTransform($viewability)
{
return $viewability;
}
}
The data passed into transform() always seems to be null or "" (empty string).
Thanks for any help.
So I ended up taking a different approach to solving this. Originally I was trying to transform data coming from an entity. Fortunately this entity didn't really need to be a database entity after all and a simple choice type sufficed. This doesn't solve the specific issue of transforming an entity list, but it allows me to customize the drop down list values.
The viewability entity was removed and the relationship in the Message entity was changed to an integer field.
My main type is now as follows:
class MessageType extends AbstractType
{
private $user;
public function __construct($user)
{
$this->user = $user;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('body', new MessageBodyType())
->add('viewability', 'choice', array(
'choices' => array(
1 => $user->getFirstName(),
2 => $user->getAddress()->getCity(),
3 => 'Anonymous',
),
'multiple' => false,
'label' => 'Send Message As',
'data' => 0,
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Members\MessagesBundle\Entity\Message',
));
}
/**
* #return string
*/
public function getName()
{
return 'members_messages_message';
}
}

How to work correctly with Forms, FOS Rest Bundle and many to many relations in Symfony2

I'm working with Symfony2 Forms and FOSRestBundle.
I'm trying to save in the database, an entity with a many to many relationship.
I create a Form with a collection field (http://symfony.com/doc/master/cookbook/form/form_collections.html) like this:
class MainType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('description');
$builder->add('others', 'collection', array(
'type' => new OtherType()
));
}
public function getName()
{
return '';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\SearchBundle\Entity\Main',
'csrf_protection' => false
));
}
}
class OtherType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('id');
}
public function getName()
{
return '';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\SearchBundle\Entity\Other',
'csrf_protection' => false
));
}
}
The collection of objects of type "Other" is stored in the database. And I don't want to store more objects of that type, only read and relate them to the main object.
When I process the form I use this function:
private function processForm(Main $main, $new = false)
{
$new = true;
$statusCode = $new ? 201 : 204;
$form = $this->createForm(new MainType(), $main);
$form->bind($this->getRequest());
if ($form->isValid()) {
$mainValidated = $form->getData();
// I should store the collection of objects of type other
// in the database
$em = $this->getDoctrine()->getEntityManager();
$em->persist($mainValidated);
$em->flush();
return $this->view($new ? $mainValidated : null, $statusCode);
}
return $this->view($form, 400);
}
The code json I send from a client Backbone.js is:
{"others":[{"id":1}, {"id":2}]}
Entities:
Main
Xml:
<entity name="Acme\SearchBundle\Entity\Main" table="main">
<id name="id type="integer" column="id">
<generator strategy="IDENTITY"/>
</id>
<field name="name" type="integer" column="name" nullable="true"/>
<field name="description" type="integer" column="description" nullable="true"/>
<many-to-many field="others" target-entity="Other" inversed-by="mains">
<cascade>
<cascade-persist/>
</cascade>
<join-table name="main_has_other">
<join-columns>
<join-column name="main" referenced-column-name="id"/>
</join-columns>
<inverse-join-columns>
<join-column name="other" referenced-column-name="id"/>
</inverse-join-columns>
</join-table>
</many-to-many>
</entity>
Entity:
<?php
namespace Acme\SearchBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\Expose;
class Main
{
/**
* #Type("integer")
* #Groups({"admin"})
*
* #var integer
*
*/
private $id;
/**
*
* #Type("string")
* #Groups({"manage"})
*
* #var string
*/
private $name;
/**
* #Type("string")
* #Groups({"manage"})
*
* #var string
*/
private $description;
/**
* #Type("ArrayCollection<Acme\SearchBundle\Entity\Other>")
* #Groups({"manage"})
*
* #var \Doctrine\Common\Collections\Collection
*/
private $others;
/**
* Constructor
*/
public function __construct()
{
$this->others = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Set name
*
* #param string $name
* #return Main
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Main
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Add others
*
* #param \Acme\SearchBundle\Entity\Other $other
* #return Main
*/
public function addOthers(\Acme\SearchBundle\Entity\Other $other)
{
$this->others[] = $other;
return $this;
}
/**
* Remove others
*
* #param \Acme\SearchBundle\Entity\Other $other
*/
public function removeOthers(\Acme\SearchBundle\Entity\Other $other)
{
$this->others->removeElement($other);
}
/**
* Get others
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getOthers()
{
return $this->others;
}
}
Other
Xml:
<entity name="Acme\SearchBundle\Entity\Other" table="other">
<id name="id" type="integer" column="id">
<generator strategy="IDENTITY"/>
</id>
<field name="name" type="string" column="name" length="255" nullable="true"/>
<field name="description" type="string" column="name" length="255" nullable="true"/>
<many-to-many field="mains" target-entity="Main" mapped-by="others">
<cascade>
<cascade-persist/>
</cascade>
</many-to-many>
</entity>
Entity:
<?php
namespace Acme\SearchBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\Groups;
class Other
{
/**
* #Type("integer")
* #Groups({"manage"})
*
* #var integer
*/
private $id;
/**
* #Type("string")
* #Groups({"manage"})
*
* #var string
*/
private $name;
/**
* #Type("string")
* #Groups({"manage"})
*
* #var string
*/
private $description;
/**
* #Type("Acme\SearchBundle\Entity\Main")
* #Groups({"admin"})
*
* #var \Doctrine\Common\Collections\Collection
*/
private $mains;
/**
* Constructor
*/
public function __construct()
{
$this->mains = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Set name
*
* #param string $name
* #return Other
*/
public function setName($name)
{
$this->name = $name
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Other
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set id
*
* #param integer $id
* #return Other
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Add main
*
* #param \Acme\SearchBundle\Entity\Main $main
* #return Other
*/
public function addMains(\Acme\SearchBundle\Entity\Main $main)
{
$this->mains[] = $main;
return $this;
}
/**
* Remove main
*
* #param \Acme\SearchBundle\Entity\Main $main
*/
public function removeMains(\AcmeSearchBundle\Entity\Main $main)
{
$this->mains->removeElement($main);
}
/**
* Get mains
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMains()
{
return $this->mains;
}
}
When I persist the object of type "main" in the database, the collection is not persited in the table of many to many relationship. I have to save the collection manually when persist the "main" object.
I'm looking for a way to save the collection of objects automatically as easy as possible.
I had a similiar issue, I think you just need to configure the form to expect extra items in your collection.
'allow_add' => true
This way "this form should not contain extra fields" error will not rise, as the form will be expecting these extra fields. So, the code should be
$builder->add('others', 'collection', array(
'type' => new OtherType(),
'allow_add' => true
));