I have an entity AdsList which represents ads and I have linked entity Photos.
When I create or update the entity AdsList a new row is created in the Photos table in the database even when I do not upload a photo and I don't want that to happen. I wish the table to be updated ONLY if there is a photo uploaded.
The AdsList entity:
namespace obbex\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use obbex\AdsBundle\Entity\Photos;
/**
* AdsList
*
* #ORM\Table()
* #ORM\Entity
* #ORM\Entity(repositoryClass="obbex\AdsBundle\Entity\AdsListRepository")
* #ORM\HasLifecycleCallbacks()
*/
class AdsList
{
... several properties
/**
* #ORM\OneToMany(targetEntity="obbex\AdsBundle\Entity\Photos",mappedBy="adslist", cascade={"persist","remove"})
* #ORM\JoinColumn(nullable=true)
*/
protected $photos;
... more getters and setters
And this is the entity Photos
namespace obbex\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Photos
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="obbex\AdsBundle\Entity\PhotosRepository")
* #ORM\HasLifecycleCallbacks
*/
class Photos
{
/**
* #ORM\ManyToOne(targetEntity="obbex\AdsBundle\Entity\AdsList", inversedBy="photos")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="adslist_id", referencedColumnName="id",onDelete="CASCADE")
* })
*/
protected $adslist;
And here is the form AdsListType
class AdsListType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email',TextType::class)
->add('telephone', IntegerType::class,array('required'=>false))
->add('displayPhone',CheckboxType::class,array('required'=>false,'attr'=>array('checked'=>false)))
->add('title',TextType::class)
->add('description', TextareaType::class)
->add('country', EntityType::class,array(
'class' => 'obbexAdsBundle:Countries',
'choice_label'=>'countryName',
'multiple'=>false
))
->add('region',TextType::class)
->add('department',TextType::class)
->add('address',TextType::class, array('required' => false))
->add('city',TextType::class)
->add('zipCode',TextType::class)
->add('statusPro',TextType::class)
->add('publication', CheckboxType::class)
->add('price', IntegerType::class)
->add('photos',CollectionType::class, array('entry_type'=> 'obbex\AdsBundle\Form\PhotosType',
'allow_add' => true,
'allow_delete'=>true,
'data'=>array(new Photos() ),
'required' => false
)
)
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'obbex\AdsBundle\Entity\AdsList',
));
}
}
I guess that there is a procedure in order to avoid persisting the Photos entity but I've tried in the controller:
public function editAdAction(Request $request,AdsList $ads){
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(AdsListEditType::class, $ads);
$form->handleRequest($request);
if ($form->isValid()) {
if($form->get('save')->isClicked()){
//this condition in case no photo is uploaded
if($form->get('photos')->getData()[0]->getFile() === null){
$photos = $ads->getPhotos();
//what do here? or maybe the problem is somewhere else
//I've tried $em->refresh($photo); but it throws the error I gave
}
$em->flush();
$id = $ads->getId();
return $this->redirect($this->generateUrl('ad_edition',array('id'=>$id)));
}
return $this->render('obbexAdsBundle:Default:editAd.html.twig',
array(
'ads'=>$ads,
'form'=>$form->createView()
));
}
But I have the following error
Entity obbex\AdsBundle\Entity\Photos#000000000b2d669200007fb2152337c5
is not managed. An entity is managed if its fetched from the database or
registered as new through EntityManager#persist
Has anyone done this? it seems pretty standard to me. In case no photo is uploaded don't do anything in the database to keep it clean
In the case there is no file set
We should not touch the image property
/**
* Add image
*
* #param \obbex\AdsBundle\Entity\Photo $image
*
* #return Painting
*/
public function addImage(\Art\GeneralBundle\Entity\Photo $photo)
{
if($photos->getFile() !== NULL){
$this->photos[] = $photo;
$image->setAdlist($this);
}
return $this;
}
and then no photo will be persisted if the field file is not filled with information.
Note that I linked the AdsList entity with the Photo Entity via the object AdsList itself not from the controller which was incorrect.
Related
I have the following data model:
MainEntity.php
<?php declare(strict_type=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class MainEntity
{
// $id, etc...
/**
* #ORM\OneToMany(
* targetEntity="App\Entity\RelationEntity",
* mappedBy="mainEntity",
* cascade={"persist", "merge", "remove"},
* orphanRemoval=true
* )
*/
protected Collection $sideRelations;
// getSideRelations(), setSideRelations($relations), addSideRelation($sideRelation), removeSideRelation($sideRelation)...
}
RelationEntity.php
<?php declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(
* name="main_side_relation",
* uniqueConstraints={
* #ORM\UniqueConstraint(name="id_pair", columns={"main_entity_id", "side_entity_id"})
* }
* )
*/
class RelationEntity
{
// $id, etc...
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected ?int $relationValue;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\MainEntity", inversedBy="sideRelations")
* #ORM\JoinColumn(name="main_entity_id", referencedColumnName="id")
*/
protected MainEntity $mainEntity;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\SideEntity", inversedBy="mainRelations")
* #ORM\JoinColumn(name="side_entity_id", referencedColumnName="id")
*/
protected SideEntity $sideEntity;
// getRelationValue(), setRelationValue($relationValue), getMainEntity(), setMainEntity($mainEntity), getSideEntity(), setSideEntity($sideEntity)...
}
SideEntity.php
<?php declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class SideEntity
{
// $id, etc...
/**
* #ORM\OneToMany(
* targetEntity="App\Entity\RelationEntity",
* mappedBy="sideEntity",
* cascade={"persist", "merge", "remove"},
* orphanRemoval=true
* )
*/
protected Collection $mainRelations;
// getMainRelations(), setMainRelations($mainRelations), addMainRelation($mainRelation), removeMainRelation($mainRelation)...
I have existing Symfony form classes which I extended to fit the data model.
RelationEntityType.php
<?php declare(strict_types=1);
namespace App\Form;
class RelationEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('relationValue', HiddenType::class)
->add('mainEntity', HiddenType::class)
->add('sideEntity', HiddenType::class)
;
// Add Transformers to switch between Entity and database ID for input value
}
public function configureOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(['data_class' => RelationEntity::class]);
}
}
MainEntityType.php
<?php declare(strict_types=1);
namespace App\Form;
class MainEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ...
$builder->add('sideRelations', CollectionType::class, [
'required' => false,
'by_reference' => false,
'entry_type' => RelationEntity::class,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true
]);
}
public function configureOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(['data_class' => MainEntity::class]);
}
}
Submission works not as intended, as by using by_reference I explicitely remove and re-add existing unchanged collection entries, but when omitting the option it happens implicitly.
In both cases, UnitOfWork is filled with insertion queries for the already existing collection entries (and throws errors due to the unique constraint). I don't want that.
Symfony's form collection configuration docs doesn't yield valuable information on how to prevent collections from becoming "dirty" although their entries have technically not been changed.
How exactly do I avoid overwriting unchanged existing collection entries?
(If necessary, I can provide the remaining code for an MWE)
Symfony version : 2.8.5
Context: I have an entity Restaurant which has a OneToOne relationship with an entity Coordinates which have several relations with other entities related to Coordinates informations. I my backend I create a form related to Restaurant entity with a custom nested form related to Coordinates.
Nota : I use EasyAdminBundle to generate my backend.
Entities relations scheme :
Restaurant
1 ________ 1 `Coordinates`
* ________ 1 `CoordinatesCountry`
1 ________ 1 `CoordinatesFR`
* ________ 1 `CoordinatesFRLane`
* ________ 1 `CoordinatesFRCity`
Backend view :
At this point I try the following scenario :
I create a new Restaurant, so I fill the fields related form for the first time. I let the Coordinates nested form blank (empty fields). So after form submission, validation messages are displayed (see image below).
I edit the previous form and this time I fill the fields of the Coordinates nested form. After form submission, a new Coordinates entity is hydrated and a relationship is created between Restaurant and Coordinates.
Once again I edit the previous form and this time I clear all the fields of the Coordinates nested form. The validation is not triggered and I get the following error :
Expected argument of type "FBN\GuideBundle\Entity\CoordinatesFRCity",
"NULL" given
I precise that in CoordinatesFRType (see code below), to trigger the validations message the first time I had to use the option empty_data with a closure (like described in the official doc) to instatiate a new CoordinatesFR instance in case of empty datas (all fields blank). But here, in this article (written by the creator of the Symfony form component), it is explained (see empty_data and datta mappers paragraphs) that the empty_data is only called at object creation. So I think this the reason why my validation does not work anymore in case of edition.
Question : why the validation is not effective anymore when editing my form and clearing all embedded form ?
The code (only what is necessary) :
Restaurant entity
use Symfony\Component\Validator\Constraints as Assert;
class Restaurant
{
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", inversedBy="restaurant", cascade={"persist"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #Assert\Valid()
*/
private $coordinates;
}
Coordinates entity
use Symfony\Component\Validator\Constraints as Assert;
class Coordinates
{
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesCountry")
* #ORM\JoinColumn(nullable=false)
*/
private $coordinatesCountry;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFR", inversedBy="coordinates", cascade={"persist"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #Assert\Valid()
*/
private $coordinatesFR;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Restaurant", mappedBy="coordinates")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $restaurant;
}
CoordinatesFR entity
use Symfony\Component\Validator\Constraints as Assert;
class CoordinatesFR extends CoordinatesISO
{
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRLane")
* #ORM\JoinColumn(nullable=true)
* #Assert\NotBlank()
*/
private $coordinatesFRLane;
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRCity")
* #ORM\JoinColumn(nullable=false)
* #Assert\NotBlank()
*/
private $coordinatesFRCity;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", mappedBy="coordinatesFR")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $coordinates;
}
Easy Admin config (equivalent to RestaurantType)
easy_admin:
entities:
Restaurant:
class : FBN\GuideBundle\Entity\Restaurant
form:
fields:
- { property: 'coordinates', type: 'FBN\GuideBundle\Form\CoordinatesType' }
CoordinatesType
class CoordinatesType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('CoordinatesCountry', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesCountry',
'property' => 'country',
))
->add('coordinatesFR', CoordinatesFRType::class)
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FBN\GuideBundle\Entity\Coordinates',
));
}
}
CoordinatesFRType
class CoordinatesFRType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('laneNum', TextType::class)
->add('coordinatesFRLane', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesFRLane',
'property' => 'lane',
'placeholder' => 'label.form.empty_value',
))
->add('laneName', TextType::class)
->add('miscellaneous', TextType::class)
->add('locality', TextType::class)
->add('metro', TextType::class)
->add('coordinatesFRCity', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesFRCity',
'property' => 'display',
'query_builder' => function (CoordinatesFRCityRepository $repo) {
return $repo->getAscendingSortedCitiesQueryBuilder();
},
'placeholder' => 'label.form.empty_value',
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FBN\GuideBundle\Entity\CoordinatesFR',
// Ensures that validation error messages will be correctly displayed next to each field
// of the corresponding nested form (i.e if submission and CoordinatesFR nested form with all fields empty)
'empty_data' => function (FormInterface $form) {
return new CoordFR();
},
));
}
}
I have a OneToMany relationship between Project and Application, and I want to be sure that 2 Applications cannot have the same name inside a Project.
I tried to configure my entity, form type and controller like it should be, but I am getting an Integrity contraint violation for duplicated entry, so I think the validation process is ignored.
Can someone tell me what am I missing ?
My Application entity like this :
namespace App\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use JsonSerializable;
/**
* #ORM\Entity
* #ORM\Table(name="application", uniqueConstraints={#ORM\UniqueConstraint(name="IDX_Unique", columns={"name", "project_id"})})
* #UniqueEntity(
* fields={"name", "project"},
* message="Name already used in this project.",
* groups="application"
* )
*/
class Application implements JsonSerializable {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
* #Assert\NotBlank(
* message = "Name cannot be empty."
* )
* #Assert\Length(
* min = "3",
* max = "50",
* minMessage = "Name is too short. It should have {{ limit }} characters or more.",
* maxMessage = "Name is too long. It should have {{ limit }} characters or less."
* )
*/
protected $name;
// other properties ...
/**
* #ORM\ManyToOne(targetEntity="Project", inversedBy="applications")
* #ORM\JoinColumn(name="project_id", referencedColumnName="id")
*/
protected $project;
// constructor, methods, getters, setters
}
My ApplicationType class looks like this :
namespace App\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ApplicationType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name', 'text', array(
'icon' => 'pencil'
));
$builder->add('description', 'textarea', array(
'required' => false,
'icon' => 'info'
));
$builder->add('url', 'url', array(
'required' => false,
'icon' => 'link'
));
}
public function getName() {
return 'application';
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'App\MainBundle\Entity\Application',
'validation_group' => array('application'),
'cascade_validation' => true
));
}
}
And in my Controller the action looks like this:
/**
* #Route("/project/{id}/application/add",
* name="app_add_application_ajax",
* requirements={"_method" = "post"},
* options={"expose" = true }
* )
* #Secure(roles="ROLE_SUPER_ADMIN")
* #ParamConverter("project", class="AppMainBundle:Project")
*/
public function addApplicationAction(Project $project, Request $request) {
$ajaxResponse = array();
$em = $this->getDoctrine()->getManager();
if ($request->getMethod() == 'POST' && $request->isXmlHttpRequest()) {
$formApp = new Application();
$formApp->setProject($project);
$form = $this->createForm(new ApplicationType(), $formApp);
$form->handleRequest($request);
if ($form->isValid()) {
$application = $form->getData();
$em->persist($application);
$em->flush();
// build ajax response ...
} else {
$ajaxResponse['error'] = $this->getErrorsAsString();
}
}
$response = new Response(json_encode($ajaxResponse));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
Your issue is that you configure a validation_group option in your form type, while the option used by Symfony is validation_groups. You don't get an error about an unknown option because you are setting this in the default options of your form type, and so you are marking the option as defined (but it is a separate one).
So the validator runs with the default group, which will validate different constraints (the constraints on the length of the name property are in the default group).
Note that you also have a second issue, which would appear once you run the constraint.
Your validation constraint does not match the DB constraints you have. You are asking the validator to have a unique name and a unique project, not a unique tuple (name, project). So you would reject too much things (the name will be validated as unique globally, not per project).
This is because you use 2 separate UniqueEntity constraints instead of a constraint asking for a tuple of multiple fields to be unique.
I have one entity "Task" and another "Attachments". I want to store all attachments in their own table associated with their task and user. So I created this entity Class:
<?php
namespace Seotool\MainBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="attachments")
*/
class Attachments {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank
*/
public $name;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $path;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="attachments")
* #ORM\JoinColumn(name="user", referencedColumnName="id")
*/
protected $User;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="attachments")
* #ORM\JoinColumn(name="editor", referencedColumnName="id")
*/
protected $Editor;
/**
* #ORM\ManyToOne(targetEntity="Task", inversedBy="attachments")
* #ORM\JoinColumn(name="task", referencedColumnName="id")
*/
protected $Task;
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
/**
* Sets file.
*
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* Get file.
*
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
public function getAbsolutePath()
{
return null === $this->path
? null
: $this->getUploadRootDir().'/'.$this->path;
}
public function getWebPath()
{
return null === $this->path
? null
: $this->getUploadDir().'/'.$this->path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded
// documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw up
// when displaying uploaded doc/image in the view.
return 'uploads/documents';
}
....
In my Form Type for my Task Form I want to add now the file upload. But how can I do this?
I can't add $builder->add('Attachment', 'file'); because it's not the same entity. So how can I do it, so that I have in my FormType of Entity Task the upload field which stores the uploaded data in the table of Entity Class Attachment??
EDIT
this is my Controller:
/**
#Route(
* path = "/taskmanager/user/{user_id}",
* name = "taskmanager"
* )
* #Template()
*/
public function taskManagerAction($user_id, Request $request)
{
/* #### NEW TASK #### */
$task = new Task();
$attachment = new Attachments();
$task->getAttachments()->add($attachment);
$addTaskForm = $this->createForm(new TaskType(), $task);
$addTaskForm->handleRequest($request);
if($addTaskForm->isValid()):
/* User Object of current Users task list */
$userid = $this->getDoctrine()
->getRepository('SeotoolMainBundle:User')
->find($user_id);
$task->setDone(FALSE);
$task->setUser($userid);
$task->setDateCreated(new \DateTime());
$task->setDateDone(NULL);
$task->setTaskDeleted(FALSE);
$attachment->setTask($task);
$attachment->setUser($userid);
$em = $this->getDoctrine()->getManager();
$em->persist($task);
$em->persist($attachment);
$em->flush();
$this->log($user_id, $task->getId(), 'addTask');
return $this->redirect($this->generateUrl('taskmanager', array('user_id' => $user_id)));
endif;
}
You should rename your entity from Attachments to Attachment as it would be storing data of only one attachment.
In your case you need Symfony2 form collection type to allow adding attachment in task form (TaskType):
$builder->add('attachments', 'collection', array(
'type' => new AttachmentType(),
// 'allow_add' => true,
// 'allow_delete' => true,
// 'delete_empty' => true,
));
You will also need to create AttachmentType form type for single attachment entity.
Doc of collection field type: http://symfony.com/doc/current/reference/forms/types/collection.html
More information about embedding form collection you can find on: http://symfony.com/doc/current/cookbook/form/form_collections.html
Then also read sections:
http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-new-tags-with-the-prototype
http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-tags-to-be-removed
Ok, that's because you have to initialize new instance of TaskType in your controller - there are no attachments at the beginning that are assigned to this task.
public function newAction(Request $request)
{
$task = new Task();
$attachment1 = new Attachment();
$task->getAttachments()->add($attachment1);
$attachment2 = new Attachment();
$task->getAttachments()->add($attachment2);
// create form
$form = $this->createForm(new TaskType(), $task);
$form->handleRequest($request);
...
}
Now there should be 2 file input for new attachments.
I added a new Form Type: AttachmentsType.php
<?php
namespace Seotool\MainBundle\Form\Type;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AttachmentsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text');
$builder->add('file', 'file');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver
->setDefaults(array(
'data_class' => 'Seotool\MainBundle\Entity\Attachments'
));
}
public function getName()
{
return 'attachments';
}
}
And used this for embbing it into my form builder of the TaskType.php
$builder->add('attachments', 'collection', array(
'type' => new AttachmentsType(),
));
But my output only gives me following HTML:
<div class="form-group"><label class="control-label required">Attachments</label><div id="task_attachments"></div></div><input id="task__token" name="task[_token]" class="form-control" value="brHk4Kk4xyuAhST3TrTHaqwlnA03pbJ5RE4NA0cmY-8" type="hidden"></form>
So I'm trying to create a registration form in Symfony 2 which contains my "Person" entity. The person entity has a one-to-many join, and I want the registration form to allow the user to select a single instance of this "Many" side of the join.
The structure is Users and Institutions. A user can have many institutions. I want a user to select a single institution at registration time (but the model allows for more later).
The basic structure is:
RegistrationType -> PersonType -> PersonInstitutionType
…with corresponding models:
Registration (simple model) -> Person (doctrine entity) -> PersonInstitution (doctrine entity, oneToMany relation from Person)
I tried to pre-populate an empty Person & PersonInstitution record in the RegistrationController but it gives me the error:
Expected argument of type "string or Symfony\Component\Form\FormTypeInterface", "TB\CtoBundle\Entity\PersonInstitution" given
(ok above has been fixed).
I've moved the code from my website to here below, trying to remove all the irrelevant bits.
src/TB/CtoBundle/Form/Model/Registration.php
namespace TB\CtoBundle\Form\Model;
use TB\CtoBundle\Entity\Person;
class Registration
{
/**
* #var Person
*/
private $person
private $termsAccepted;
}
src/TB/CtoBundle/Form/RegistrationType.php
namespace TB\CtoBundle\Form;
use TB\CtoBundle\Form\PersonType;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('person', new PersonType());
$builder->add('termsAccepted','checkbox');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TB\CtoBundle\Form\Model\Registration',
'cascade_validation' => true,
));
}
public function getName()
{
return 'registration';
}
}
src/TB/CtoBundle/Entity/Person.php
namespace TB\CtoBundle\Entity;
use TB\CtoBundle\Entity\PersonInstitution
/**
* #ORM\Entity()
*/
class Person
{
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="PersonInstitution", mappedBy="person", cascade={"persist"})
*/
private $institutions;
}
src/TB/CtoBundle/Form/PersonType.php
namespace TB\CtoBundle\Form;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('institutions', 'collection', array('type' => new PersonInstitutionType()))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TB\CtoBundle\Entity\Person',
));
}
/**
* #return string
*/
public function getName()
{
return 'tb_ctobundle_person';
}
}
src/TB/CtoBundle/Entity/PersonInstitution.php
namespace TB\CtoBundle\Entity
/**
* PersonInstitution
*
* #ORM\Table()
* #ORM\Entity
*/
class PersonInstitution
{
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="institutions", cascade={"persist"})
*/
private $person;
/**
* #ORM\ManyToOne(targetEntity="Institution", inversedBy="members")
*/
private $institution;
/**
* #ORM\Column(type="boolean")
*/
private $approved;
}
src/TB/CtoBundle/Form/PersonInstititionType.php
namespace TB\CtoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PersonInstitutionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('approved')
->add('person')
->add('institution')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TB\CtoBundle\Entity\PersonInstitution'
));
}
/**
* #return string
*/
public function getName()
{
return 'tb_ctobundle_personinstitution';
}
}
src/TB/CtoBundle/Controller/Registration.php
namespace TB\CtoBundle\Controller;
class RegisterController extends Controller
{
/**
*
* #param Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function registerAction(Request $request)
{
$registration = new Registration;
$person = new Person();
$institution = new PersonInstitution();
$person->addInstitution($institution);
$registration->setPerson($person);
// this causes error:
// Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
// $institution->setPerson($person);
$form = $this->createForm(new RegistrationType(), $registration);
$form->handleRequest($request);
if($form->isValid()) {
$registration = $form->getData();
$person = $registration->getPerson();
// new registration - account status is "pending"
$person->setAccountStatus("P");
// I'd like to get rid of this if possible
// for each "PersonInstitution" record, set the 'person' value
foreach($person->getInstitutions() as $rec) {
$rec->setPerson($person);
}
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
}
return $this->render('TBCtoBundle:Register:register.html.twig', array('form' => $form->createView()));
}
}
Here is a detailed solution for adding an Collection field to Person entity and formType.
Your complex question with Registration entity can be solved with this.
I suggest you to use this 3 entity related connection if it is really needed. (only because of termsAccepted data!?)
If you won't change your opinion, then use this annotation:
Registration code:
use TB\CtoBundle\Entity\Person;
/**
* #ORM\OneToOne(targetEntity="Person")
* #var Person
*/
protected $person;
Person code:
use TB\CtoBundle\Entity\PersonInstitution;
/**
* #ORM\OneToMany(targetEntity="PersonInstitution", mappedBy = "person")
* #var ArrayCollection
*/
private $institutions;
/* I suggest you to define these functions:
setInstitutions(ArrayCollection $institutions),
getInstitutions()
addInstitution(PersonInstitution $institution)
removeInstitution(PersonInstitution $institution)
*/
PersonInstitution code:
use TB\CtoBundle\Entity\Person;
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="institutions", cascade={"persist"}))
* #var Person
*/
private $person;
PersonType code:
use TB\CtoBundle\Form\PersonInstitutionType;
->add('institutions', 'collection', array(
'type' => new PersonInstitutionType(), // here is your mistake!
// Other options can be selected here.
//'allow_add' => TRUE,
//'allow_delete' => TRUE,
//'prototype' => TRUE,
//'by_reference' => FALSE,
));
PersonController code:
use TB\CtoBundle\Entity\Person;
use TB\CtoBundle\Entity\PersonInstitution;
/**
* ...
*/
public funtcion newAction()
{
$person = new Person;
$institution = new PersonInstitution;
$institution->setPerson($person);
$person->addInstitution($institution);
$form = $this->createForm(new PersonType($), $person); // you can use formFactory too.
// If institution field is required, then you have to check,
// that is there any institution able to chose in the form by the user.
// Might you can redirect to institution newAction in that case.
return array( '...' => $others, 'form' => $form);
}
If you need more help in twig code, then ask for it.