I'm trying to make a nested Form, embed a collection of forms inside a form.
i have problem with the buildForm.
I'm trying to add a formType in a other form type in my case to add educations to my CurriculumVitae form.
One CurriculumVitae has Many Educations.
Many Education have One CurriculumVitae.
im getting this error message:
Expected argument of type "array or (\Traversable and \ArrayAccess)",
"string" given
CurriculumVitae Entity:
/**
* CurriculumVitae
*
* #ORM\Table(name="curriculum_vitae")
* #ORM\Entity(repositoryClass="GE\CandidatBundle\Repository\CurriculumVitaeRepository")
*/
class CurriculumVitae
{
/**
* #ORM\OneToMany(targetEntity="GE\CandidatBundle\Entity\Education", mappedBy="curriculumVitae", cascade={"persist", "remove"})
* #ORM\Column(name="id_education")
*/
protected $educations;
/**
* Add education
*
* #param \GE\CandidatBundle\Entity\Education $education
*
* #return CurriculumVitae
*/
public function addEducation(\GE\CandidatBundle\Entity\Education $education)
{
$this->educations[] = $education;
$education->setCurriculumVitae($this);
return $this;
}
/**
* Remove education
*
* #param \GE\CandidatBundle\Entity\Education $education
*/
public function removeEducation(\GE\CandidatBundle\Entity\Education $education)
{
$this->educations->removeElement($education);
}
}
Education Entity:
/**
* Education
*
* #ORM\Table(name="education")
* #ORM\Entity(repositoryClass="GE\CandidatBundle\Repository\EducationRepository")
*/
class Education
{
...
/**
* Many Education have One CurriculumVitae.
* #ORM\ManyToOne(targetEntity="GE\CandidatBundle\Entity\CurriculumVitae", inversedBy="educations")
* #ORM\JoinColumn(name="curriculumVitae_id", referencedColumnName="id")
*/
private $curriculumVitae;
}
CurriculumVitaeType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('educations',
CollectionType::class, [
'entry_type' => EducationType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'label' => 'Educations:'
]
)
;
}
CurriculumVitaeController:
class CurriculumVitaeController extends Controller
{
...
public function editAction(Request $request, CurriculumVitae $curriculumVitae)
{
$deleteForm = $this->createDeleteForm($curriculumVitae);
$editForm = $this->createForm('GE\CandidatBundle\Form\CurriculumVitaeType', $curriculumVitae);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('cv_edit', array('id' => $curriculumVitae->getId()));
}
return $this->render('curriculumvitae/edit.html.twig', array(
'curriculumVitae' => $curriculumVitae,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
}
I don't know if it's a problem with the constructor or what?
In my CurriculumVitae Entity:
/**
* Constructor
*/
public function __construct()
{
$this->objectifs = new \Doctrine\Common\Collections\ArrayCollection();
$this->educations = new \Doctrine\Common\Collections\ArrayCollection();
$this->experiences = new \Doctrine\Common\Collections\ArrayCollection();
$this->formations = new \Doctrine\Common\Collections\ArrayCollection();
$this->competences = new \Doctrine\Common\Collections\ArrayCollection();
$this->dateAjout = new \Datetime();
$this->etat = false;
$this->disponibilite = false;
}
May be that the problem is here, when we need to nest a single form more than once i mean with which relationships?
Sorry,
I found my problem.
1) $candidatId should be $candidat. we are not storing ids in the entity but rather a reference to $candidat.
2) There is no column for education ids in CurriculumVitae. The relationship is stored in each individual Education entity.
3) There is no need to #ORM\Column(name="id_education").
and i added a JoinColumn to $idCurriculumVitae
/**
* #var CurriculumVitae
* #ORM\ManyToOne(targetEntity="GE\CandidatBundle\Entity\CurriculumVitae")
*#ORM\JoinColumn(name="curriculumVitae_id",referencedColumnName="id",onDelete="SET NULL")
*/
private $idCurriculumVitae;
I have a many to one related entities and every time I create a new CurriculumVitae entity which is related to Candidat entity the column candidat_id_id is null.
Only the entity CurriculumVitae is successfully created and persisted except the Candidat id in the data datatable.
CurriculumVitae table:
id | titre | candidat_id_id | id_education
candidat_id_id is: null
id_education has this value: Doctrine\Common\Collections\ArrayCollection#000000003d585f990000000052235238
Education table is empty
id | description | id_curriculum_vitae_id
My problem is with the id_curriculum_vitae_id and the id_education.
Related
It's simple im doing a permission table from Companies and Users where i have to store the user id and the company id right now i have this and i get this error
Expected value of type "App\Entity\CompanyUserPermissionMap" for association field "App\Entity\User#$companyUserPermissionMaps", got "App\Entity\Company" instead.
User Entity
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $userId;
/**
* #ORM\Column(type="string", length=12)
*/
private $code;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* #var CompanyUserPermissionMap[]
* #ORM\OneToMany(targetEntity="App\Entity\CompanyUserPermissionMap", mappedBy="user", orphanRemoval=true)
*/
private $companyUserPermissionMaps;
public function __construct()
{
$this->companyUserPermissionMaps = new ArrayCollection();
}
public function getCompanyUserPermissionMaps(): Collection
{
return $this->companyUserPermissionMaps;
}
public function addCompanyUserPermissionMaps(CompanyUserPermissionMaps $permission): self
{
if (!$this->companyUserPermissionMaps->contains($permission)) {
$this->companyUserPermissionMaps[] = $permission;
$permission->setUser($this);
}
return $this;
}
Company Entity
#########################
## PROPERTIES ##
#########################
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $companyId;
/**
* #ORM\Column(type="string", length=6, unique=true)
*/
private $code;
/**
* #var CompanyUserPermissionMap[]
* #ORM\OneToMany(targetEntity="App\Entity\CompanyUserPermissionMap", mappedBy="company", orphanRemoval=true)
*/
private $companyUserPermissionMaps;
public function __construct()
{
$this->companyUserPermissionMaps = new ArrayCollection();
}
/**
* #return Collection|CompanyUserPermissionMaps[]
*/
public function getCompanyUserPermissionMaps(): Collection
{
return $this->companyUserPermissionMaps;
}
public function addCompanyUserPermissionMaps(AccountingBankPermission $permission): self
{
if (!$this->companyUserPermissionMaps->contains($permission)) {
$this->companyUserPermissionMaps[] = $permission;
$permission->setAccount($this);
}
return $this;
}
public function removeCompanyUserPermissionMaps(AccountingBankPermission $permission): self
{
if ($this->companyUserPermissionMaps->contains($permission)) {
$this->companyUserPermissionMaps->removeElement($permission);
// set the owning side to null (unless already changed)
if ($permission->getAccount() === $this) {
$permission->setAccount(null);
}
}
return $this;
}
relation table
Column(type="integer")
private $companyUserPermissionId;
/**
* #var User
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="companyUserPermissionMaps")
* #ORM\JoinColumn(referencedColumnName="user_id", nullable=false)
*/
private $user;
/**
* #var Company
* #ORM\ManyToOne(targetEntity="App\Entity\Company", inversedBy="companyUserPermissionMaps")
* #ORM\JoinColumn(referencedColumnName="company_id", nullable=true)
*/
private $company;
/**
* #return int|null
*/
public function getCompanyUserPermissionId(): ?int
{
return $this->companyUserPermisionId;
}
/**
* #return User
*/
public function getUser(): ?User
{
return $this->user;
}
/**
* #param User $user
* #return AccountingBankPermission
*/
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
/**
* #return Company
*/
public function getCompany(): ?Company
{
return $this->company;
}
/**
* #param array $company
* #return CompanyUserPermissionMap
*/
public function setCompany(?array $company): self
{
$this->company = $company;
return $this;
}
Form type
$builder
->add('roles' ,ChoiceType::class ,[
'required' => true,
'choices' => $this->roles,
'multiple' => true,
'expanded' => true,
'label_attr' => [
'class' => 'custom-control-label',
],
'choice_attr' => function($val, $key, $index) {
return ['class' => 'custom-control-input'];
},
'attr'=>['class' =>'custom-checkbox custom-control']
])
->add('email', EmailType::class, [
'label' => "E-Mail"
])
->add('firstName', TextType::class, [
'label' => "First Name"
])
->add('lastName', TextType::class, [
'label' => "Last Name"
])
->add('companyUserPermissionMaps' ,EntityType::class ,[
'required' => true,
'class' => Company::class,
'label' => 'Compañia',
'multiple' => true,
'expanded' => false,
'choice_label' => 'legalName',
'mapped'=>false
])
->add('save', SubmitType::class, [
'label' => "Save"
])
and my controller function looks like this
$user = new User();
$originalRoles = $this->getParameter('security.role_hierarchy.roles');
$options=['roles' => $originalRoles ];
$form = $this->createForm(UserType::class, $user, $options);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$tempPassword = "some pass";
$user->setPassword($encoder->encodePassword(
$user,
$tempPassword
));
$companies=[];
$companiesForm=$form->get('companyUserPermissionMaps')->getData();
foreach ($companiesForm as $value) {
$companies[] = $value->getCompanyId();
}
// Save object to database
$entityManager = $this->getDoctrine()->getManager();
/** #var CompanyRepository $companyRepository */
$companyRepository = $this->getDoctrine()->getRepository(Company::class);
$companiesArr =$companyRepository->findCompanyByArray($companies);
$companyUserPermissionMap = new CompanyUserPermissionMap();
$companyUserPermissionMap->setUser($user);
$companyUserPermissionMap->setCompany($companiesArr);
$entityManager->persist($user);
$entityManager->persist($companyUserPermissionMap);
update
Okay, so I neglected to mention the problem you're actually facing. The form component, smart as it is, will try to call getters and setters when loading/modifying the form data. Since your form contains ->add('companyUserPermissionMaps',..., the form component will call getCompanyUserPermissionMaps on your entity (which will return a currently probably empty collection) and will try to write back to the entity via either setCompanyUserPermissionMaps or add/remove instead of set, if they are present.
Since your field actually behaves as if it holds a collection of Company objects, the setting of those on your User object will obviously fail with the error message you encountered:
Expected value of type "App\Entity\CompanyUserPermissionMap" for association field "App\Entity\User#$companyUserPermissionMaps", got "App\Entity\Company" instead.
which absolutely makes sense. So, this problem can be fixed in different ways. The one way which you apparently already tried was setting mapped to false, but instead of false you used 'false' (notice the quotes), which evaluates to true ... ironically. So to use your approach, you would have to remove the quotes. However, I propose a different approach, which I would much prefer!
end update
My general advice would be to hide stuff you don't want to show. So, in your form builder instead of
->add('companyUserPermissionMaps', EntityType::class, [
'required' => true,
'class' => Company::class,
'label' => 'Compañia',
'multiple' => true,
'expanded' => false,
'choice_label' => 'legalName',
'mapped'=>'false'
])
which obviously already is not a field that handles CompanyUserPermissionMaps but companies instead - so apparently you suspected this is semantically something different, you should go back to the User entity and give it a function getCompanies instead
public function getCompanies() {
return array_map(function ($map) {
return $map->getCompany();
}, $this->getCompanyUserPermissionsMaps());
}
public function addCompany(Company $company) {
foreach($this->companyUserPermissionMaps->toArray() as $map) {
if($map->getCompany() === $company) {
return;
}
}
$new = new CompanyUserPermissionMap();
$new->setCompany($company);
$new->setUser($this);
$this->companyUserPermissionMaps->add($new);
}
public function removeCompany(Company $company) {
foreach($this->companyUserPermissionMaps as $map) {
if($map->getCompany() == $company) {
$this->companyUserPermissionMaps->removeElement($map);
}
}
}
you would then call the field companies (so ->add('companies', ...)) and act on Company entities instead of those pesky maps. (I also don't really like exposing ArrayCollection and other internals to the outside. But hey, that's your decision.)
however, if your Maps are going to hold more values at some point, you actually have to work with the maps in your form, and not just with Company entities.
I am trying to persist two entities that have OneToMany and ManyToOne relationships.
I'm trying to embed a collection of forms inside a form.
I have a many to one related entities and everytime I create a new CurriculumVitae which is related to Candidat the column candidat_id_id is null.
Only the entity CurriculumVitae is successfully created and persisted except the Candidat id in the data datatable.
CurriculumVitae Table
id | titre | candidat_id_id | id_education
candidat_id_id is: null
id_education has this value: Doctrine\Common\Collections\ArrayCollection#000000003d585f990000000052235238
Education Table is empty
id | description | id_curriculum_vitae_id
My problem is with the id_curriculum_vitae_id and the id_education.
CurriculumVitae Entity:
/**
* CurriculumVitae
*
* #ORM\Table(name="curriculum_vitae")
* #ORM\Entity(repositoryClass="GE\CandidatBundle\Repository\CurriculumVitaeRepository")
*/
class CurriculumVitae
{
/**
* #var Candidat
*
* #ORM\ManyToOne(targetEntity="GE\CandidatBundle\Entity\Candidat")
*/
protected $candidatId;
/**
* #var Education
* #ORM\OneToMany(targetEntity="GE\CandidatBundle\Entity\Education", mappedBy="idCurriculumVitae", cascade={"persist", "remove"})
* #ORM\Column(name="id_education")
*/
protected $educations;
/**
* Add education
*
* #param \GE\CandidatBundle\Entity\Education $education
*
* #return CurriculumVitae
*/
public function addEducation(\GE\CandidatBundle\Entity\Education $education)
{
$this->educations[] = $education;
$education->setCurriculumVitae($this);
return $this;
}
/**
* Remove education
*
* #param \GE\CandidatBundle\Entity\Education $education
*/
public function removeEducation(\GE\CandidatBundle\Entity\Education $education)
{
$this->educations->removeElement($education);
}
}
Education Entity:
/**
* Education
*
* #ORM\Table(name="education")
* #ORM\Entity(repositoryClass="GE\CandidatBundle\Repository\EducationRepository")
*/
class Education
{
...
/**
* #var CurriculumVitae
* #ORM\ManyToOne(targetEntity="GE\CandidatBundle\Entity\CurriculumVitae")
*/
private $idCurriculumVitae;
}
CurriculumVitaeController :
class CurriculumVitaeController extends Controller
{
/**
* Creates a new curriculumVitae entity.
*
* #Route("/candidat/cv/ajouter", name="cv_new")
* #Method({"GET", "POST"})
*/
public function newAction(Request $request)
{
$curriculumVitae = new Curriculumvitae();
$form = $this->createForm('GE\CandidatBundle\Form\CurriculumVitaeType', $curriculumVitae);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($curriculumVitae);
$em->flush();
$request->getSession()
->getFlashBag()
->add('infoCv', 'Votre cv a été bien enregistrée.');
return $this->redirectToRoute('cv_show', array('id' => $curriculumVitae->getId()));
}
return $this->render('curriculumvitae/new.html.twig', array(
'curriculumVitae' => $curriculumVitae,
'form' => $form->createView(),
));
}
}
CurriculumVitaeType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('candidat', EntityType::class, array(
'class' => 'GECandidatBundle:Candidat',
'choice_label' => 'id',
'multiple' => false,
))
->add('educations',
CollectionType::class, [
'entry_type' => EducationType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'label' => 'Educations:'
]
)
;
}
Try this code and update your database's schema
/**
* #var CurriculumVitae
* #ORM\ManyToOne(targetEntity="GE\CandidatBundle\Entity\CurriculumVitae")
*#ORM\JoinColumn(name="candidatId",referencedColumnName="id",onDelete="SET NULL")
*/
private $idCurriculumVitae;
Sorry, i found my problem.
the id must be defined in the Controller.
$curriculumVitae->setCandidat($candidat);
I have some entites with common relations and attributes. So, I want to simplify my schema using inheritance mapping.
I created a BaseData mappedsuperclass, and make my other entities expand it. This BaseData class has the common relations I need in each entity.
It works with many-to-one relation, like
/**
* #ORM\MappedSuperclass
*/
class BaseData
{
/**
* #ORM\ManyToOne(targetEntity="Service")
* #ORM\JoinColumn(name="service_id", referencedColumnName="id")
*/
protected $service;
But it become a little bit more tricky with self-referencing.
For instance, since I want to create a parent reference, I tried that :
/**
* #ORM\MappedSuperclass
*/
class BaseData
{
/**
* #ORM\ManyToOne(targetEntity="BaseData")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
protected $parent;
Obviously, it lead to a TableNotFoundException when I try to query this entity : QLSTATE[42S02]: Base table or view not found: 1146 Table 'project.base_data' doesn't exist.
So, I tried AssociationOverrides, but it seems that doesn't allow to change the Target Entity.
So, is there a way to build some self-reference on a MappedSuperclass ? And by the way, does it even make sense ?
Many thanks in advance !
Update
Here is the anwser :
I defined the protected $parent and protected $children in my BaseData mappedSuperClass as planned. I annotated them with other information I need. eg :
/**
* #ORM\MappedSuperclass
*/
class BaseData
{
/**
* #Datagrid\Column(field="parent.id", title="datagrid.parent_id", visible=false, safe=false)
* #Serializer\Expose
* #Serializer\Groups({"foo"})
*/
protected $parent;
/**
* #Serializer\Expose
* #Serializer\Groups({"elastica"})
*/
protected $children;
Then, I add the ORM relation with the event loadClassMetadata.
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
// the $metadata is all the mapping info for this class
$classMetadata = $eventArgs->getClassMetadata();
$reflObj = new \ReflectionClass($classMetadata->name);
if($reflObj) {
if ($reflObj->isSubclassOf('CoreBundle\Entity\BaseData')) {
$fieldMapping = array(
'targetEntity' => $classMetadata->name,
'fieldName' => 'parent',
'inversedBy' => 'children',
'JoinColumn' => array(
'name' => 'parent_id',
'referencedColumnName' => 'id',
'nullable' => true,
'onDelete' => 'SET NULL',
),
);
$classMetadata->mapManyToOne($fieldMapping);
$fieldMapping = array(
'fieldName' => 'children',
'targetEntity' => $classMetadata->name,
'mappedBy' => 'parent',
);
$classMetadata->mapOneToMany($fieldMapping);
}
}
}
Register the event, and that's it.
Now, every class which extends the BaseData superClass get the relation. For instance, php app/console doctrine:generate:entities MyBundle will generates the following code inside the SubClass entity :
/**
* Set parent
*
* #param \MyBundle\Entity\Subclass $parent
*
* #return Subclass
*/
public function setParent(\MyBundle\Entity\Subclass $parent = null)
{
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \MyBundle\Entity\Subclass
*/
public function getParent()
{
return $this->parent;
}
/**
* Add child
*
* #param \MyBundle\Entity\Subclass $child
*
* #return Subclass
*/
public function addChild(\MyBundle\Entity\Subclass $child)
{
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \MyBundle\Entity\Subclass $child
*/
public function removeChild(\MyBundle\Entity\Subclass $child)
{
$this->children->removeElement($child);
}
/**
* Get children
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChildren()
{
return $this->children;
}
You can remove the mapping #ORM\ManyToOne(targetEntity="BaseData") and create an event listener on the event loadClassMetadata. (I didn't test the following code, this is just a starting point) Something like this:
class TestEvent
{
public function loadClassMetadata(\Doctrine\ORM\Event\LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
$class = $classMetadata->getName();
$fieldMapping = array(
'fieldName' => 'parent',
'targetEntity' => $class,
);
$classMetadata->mapManyToOne($fieldMapping);
}
}
One important thing to notice is that a listener will be listening for all entities in your application.
See Doctrine docs about events
And How to register event listener in symfony2
Context : I am building my little TodoList bundle (which is a good exercice to go deep progressively with Symfony2), the difficulty comes with recursivity : each Task can has children and parent, so I used Gedmo Tree.
I have a collection of tasks each having a sub collection of children, children collection has prototype enabled so I can display a new sub task form when clicking "add sub task".
I wanted the default name of the subtask to be "New Sub Task" instead of "New Task" set in Task constructor, so I figured out how to pass a custom instance for the prototype and took some care for preventing infinite loop.
So I am almost done and my new task is added with the name I set when saving...
Problem : I am not able to persist the parent task to the new sub task, the new task persist the name well, but not the parentId, I probably forgot somewhere with Doctrine, here is some relevant parts :
// Entity Task
/**
* #Gedmo\Tree(type="nested")
* #ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="task")
*/
class Task {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank(message="Name must be not empty")
*/
protected $name = 'New Task';
//....
/**
* #Gedmo\TreeLeft
* #ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* #Gedmo\TreeLevel
* #ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* #Gedmo\TreeRight
* #ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* #Gedmo\TreeRoot
* #ORM\Column(name="root", type="integer", nullable=true)
*/
private $root;
/**
* #Gedmo\TreeParent
* #ORM\ManyToOne(targetEntity="Task", inversedBy="children")
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent = null;//
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $parentId = null;
/**
* #Assert\Valid()
* #ORM\OneToMany(targetEntity="Task", mappedBy="parent", cascade={"persist", "remove"})
* #ORM\OrderBy({"status" = "ASC", "created" = "DESC"})
*/
private $children;
public function __construct(){
$this->children = new ArrayCollection();
}
/**
* Set parentId
*
* #param integer $parentId
* #return Task
*/
public function setParentId($parentId){
$this->parentId = $parentId;
return $this;
}
/**
* Get parentId
*
* #return integer
*/
public function getParentId(){
return $this->parentId;
}
/**
* Set parent
*
* #param \Dmidz\TodoBundle\Entity\Task $parent
* #return Task
*/
public function setParent(\Dmidz\TodoBundle\Entity\Task $parent = null){
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \Dmidz\TodoBundle\Entity\Task
*/
public function getParent(){
return $this->parent;
}
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $child
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \Dmidz\TodoBundle\Entity\Task $child
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children->removeElement($child);
}
}
// TaskType
class TaskType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->add('name', null, ['label' => false])
->add('notes', null, ['label' => 'Notes'])
->add('status', 'hidden')
->add('parentId', 'hidden')
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($builder){
$record = $event->getData();
$form = $event->getForm();
if(!$record || $record->getId() === null){// if prototype
$form->add('minutesEstimated', null, ['label' => 'Durée', 'attr'=>['title'=>'Durée estimée en minutes']]);
}elseif($record && ($children = $record->getChildren())) {
// this is where I am able to customize the prototype default values
$protoTask = new Task();
$protoTask->setName('New Sub Task');
// here I am loosely trying to set the parentId I want
// so the prototype form input has the right value
// BUT it goes aways when INSERT in mysql, the value is NULL
$protoTask->setParentId($record->getId());
$form->add('sub', 'collection', [// warn don't name the field 'children' or it will conflict
'property_path' => 'children',
'type' => new TaskType(),
'allow_add' => true,
'by_reference' => false,
// this option comes from a form type extension
// allowing customizing prototype default values
// extension code : https://gist.github.com/jumika/e2f0a5b3d4faf277307a
'prototype_data' => $protoTask
]);
}
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$resolver->setDefaults([
'data_class' => 'Dmidz\TodoBundle\Entity\Task',
'label' => false,
]);
}
public function getParent(){ return 'form';}
}
// my controller
/**
* #Route("/")
* #Template("DmidzTodoBundle:Task:index.html.twig")
*/
public function indexAction(Request $request){
$this->request = $request;
$repo = $this->doctrine->getRepository('DmidzTodoBundle:Task');
$em = $this->doctrine->getManager();
//__ list of root tasks (parent null)
$query = $repo->createQueryBuilder('p')
->select(['p','FIELD(p.status, :progress, :wait, :done) AS HIDDEN field'])
->addOrderBy('field','ASC')
->addOrderBy('p.id','DESC')
->andWhere('p.parent IS NULL')
->setParameters([
'progress' => Task::STATUS_PROGRESS,
'wait' => Task::STATUS_WAIT,
'done' => Task::STATUS_DONE
])
->setMaxResults(20)
->getQuery();
$tasks = $query->getResult();
//__ form building : collection of tasks
$formList = $this->formFactory->createNamed('list_task', 'form', [
'records' => $tasks
])
->add('records', 'collection', [
'type'=>new TaskType(),
'label'=>false,
'required'=>false,
'by_reference' => false,
])
;
//__ form submission
if ($request->isMethod('POST')) {
$formList->handleRequest($request);
if($formList->isValid()){
// persist tasks
// I thought persisting root tasks will persist their children relation
foreach($tasks as $task){
$em->persist($task);
}
$em->flush();
return new RedirectResponse($this->router->generate('dmidz_todo_task_index'));
}
}
return [
'formList' => $formList->createView(),
];
}
As mentionned in the comments in TaskType, the form prototype of the new sub task has the right value for parentId which is posted, BUT the value is gone and NULL on INSERT in db (looking at the doctrine log).
So do you think it is the right way of doing, and then what thing I forgot for persisting correctly the parent task of the new sub task ?
On your child setting you should set the parent when adding, like so..
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->add($children);
$children->setParent($this);
return $this;
}
/**
* Remove children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->removeElement($children);
$children->setParent(null);
}
When your prototype adds and deletes a row it calls addChild and removeChild but it doesn't call the setParent in the associated child.
This way any child that is added or removed/deleted get automatically set in the process.
Also you could change the $children to $child as it makes grammatical sense and it's really bugging me because I am a child(ren).
It seems weird to me that you try using the parentId field as a simple column, whereas it is a relation column. Theoretically, you should not:
$task->getParentId(); //fetching a DB column's value
but instead:
$task->getParent()->getId(); //walking through relations to find an object's attribute
However, if you really need this feature to avoid loading the full parent object and just get its ID, your setParentId method should be transparent (although, as mentionned, I'm not sure using the same DB field is valid):
public function setParent(Task $t = null) {
$this->parent = $t;
$this->parentId = null === $t ? null : $t->getId();
return $this;
}
Back to your issue: in the TaskType class, you should call:
$protoTask->setParent($record);
instead of:
$protoTask->setParentId($record->getId());
The reason:
you tell Doctrine parentId is a relation field (in the $parent attribute declaration), therefore Doctrine expects an object of the proper type
you also tell Doctrine to map this relation field directly to an attribute (the $parentId attribute declaration), I'm neither convinced this is valid, nor convinced this is good practice, but I guess you did some research before going for this structure
you set $parentId, but $parent has not been set (i.e. null), so Doctrine must erase the $parentId value with the $parent value: your code is proof that Doctrine handles attributes first, then computes relations ;)
Keep in mind Doctrine is an Object Relational Mapper, not a simple query helper: mapper is what it does (mapping persistence layer with your code), relational is how it does it (one-to-many and the like), object is what it does it on (therefore not directly using IDs).
Hope this helps!
I have extended the SonataAdmin class for FOSUser and added 2 custom fields (choice type from external data source): Company and Sector
I'd like to make Sector dependent on Company, so if the user selects a Company it filters the available Sectors.
I though about using FormEvents for filtering at page load, but I don't even know how to get the Company value of the current form.
Here is a part of my custom SectorType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA
, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
// Need to get the company value here if set
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'choices' => $this->getSectors(),
));
}
public function getSectors()
{
$sects = array();
// Need to pass the selected company value to my getList
// (which gets the list of sector as you can imagine)
if (($tmp_sects = $this->ssrs->getList('Sector'))) {
foreach ($tmp_sects as $sect) {
$label = $sect['id'] ? $sect['label'] : '';
$sects[$sect['id']] = $label;
}
}
return $sects;
}
So the question is:
How to get the selected Company from my custom SectorType ?
After that I'll need to be able to refresh the Sector with Ajax, but that will be another question
I had a similar problem. I needed to create a sale entity that needed to be associated in a many to one relationship with an enterprise entity and a many to many relationship with services entities. Here is the Sale Entity:
The thing is that services where available depending on the companies chosen. For instance services a and b could only be provided to company x. And services b and c could only be provided to company y. So in my admin, depending on the chosen company I had to display the available services. For these I needed to do 2 things:
First create a dynamic form with my sale admin, so that on the server side I could get the right services available for the company specified in my sale record. And second, I had to create a custom form type for my company form element, so that when it was changed by the user on the client side, It would send an ajax request to get the right services for the company chosen.
For my first problem, I did something similar to what you were trying to achieve, but instead of creating an specific custom type for my services element, I added de event listener directly in the admin.
Here is the Sale entity:
/**
*
* #ORM\Table(name="sales")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Sale
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
public $id;
/**
* #ORM\ManyToOne(targetEntity="Branch")
* #ORM\JoinColumn(name="branch_id", referencedColumnName="id", nullable = false)
* #Assert\NotBlank(message = "Debe especificar una empresa a la cual asignar el precio de este exámen!")
*/
private $branch;
/** Unidirectional many to many
* #ORM\ManyToMany(targetEntity="Service")
* #ORM\JoinTable(name="sales_services",
* joinColumns={#ORM\JoinColumn(name="sale_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="service_id", referencedColumnName="id")}
* )
* #Assert\Count(min = "1", minMessage = "Debe especificar al menos un servicio a realizar!")
*/
private $services;
public function __construct() {
$this->services = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set branch
*
* #param Astricom\NeurocienciasBundle\Entity\Branch $branch
*/
//default value always have to be null, because when validation constraint is set to notblank,
//if default is not null, before calling the validation constraint an error will pop up explaining
//that no instance of Branch was passed to the $branch argument.
public function setBranch(\Astricom\NeurocienciasBundle\Entity\Branch $branch = null)
{
$this->branch = $branch;
}
/**
* Get branch
*
* #return Astricom\NeurocienciasBundle\Entity\Branch
*/
public function getBranch()
{
return $this->branch;
}
/**
* Add service
*
* #param \Astricom\NeurocienciasBundle\Entity\Service|null $service
*/
public function addServices(\Astricom\NeurocienciasBundle\Entity\Service $service = null)
{
$this->services[] = $service;
}
/**
* Get services
*
* #return Doctrine\Common\Collections\Collection
*/
public function getServices()
{
return $this->services;
}
/**
* Sets the creation date
*
* #param \DateTime|null $createdAt
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
}
/**
* Returns the creation date
*
* #return \DateTime|null
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Sets the last update date
*
* #param \DateTime|null $updatedAt
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
}
So then in the Admin form builder:
protected function configureFormFields(FormMapper $formMapper) {
$em = $this->container->get('doctrine')->getEntityManager();
$branchQuery = $em->createQueryBuilder();
$branchQuery->add('select', 'b')
->add('from', 'Astricom\NeurocienciasBundle\Entity\Branch b')
->add('orderBy', 'b.name ASC');
$formMapper
->with('Empresa/Sucursal')
->add('branch','shtumi_ajax_entity_type',array('required' => true, 'label'=>'Empresa/Sucursal','error_bubbling' => true, 'empty_value' => 'Seleccione una empresa/sucursal', 'empty_data' => null, 'entity_alias'=>'sale_branch', 'attr'=>array('add_new'=>false), 'model_manager' => $this->getModelManager(), 'class'=>'Astricom\NeurocienciasBundle\Entity\Branch', 'query' => $branchQuery))
->end()
;
$builder = $formMapper->getFormBuilder();
$factory = $builder->getFormFactory();
$sale = $this->getSubject();
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function(DataEvent $event) use ($sale,$factory, $em) {
$form = $event->getForm();
$servicesQuery = $em->createQueryBuilder();
$servicesQuery->add('select','s')
->add('from','Astricom\NeurocienciasBundle\Entity\Service s');
if (!$sale || !$sale->getId()) {
$servicesQuery
->where($servicesQuery->expr()->eq('s.id', ':id'))
->setParameter('id', 0);
}
else {
$servicesQuery
->join('s.branch', 'b')
->where($servicesQuery->expr()->eq('b.id', ':id'))
->setParameter('id', $sale->getBranch()->getId());
}
$form->add($factory->createNamed('services','entity',null,array('required' => true, 'label'=>'Servicios','error_bubbling' => true, 'attr'=>array('show_value_label'=>true),'class'=>'Astricom\NeurocienciasBundle\Entity\Service','multiple'=>true,'expanded'=>true,'query_builder'=>$servicesQuery)));
}
);
}
The trick thing was to pass the forms data. It doesn't work to use evet->getData() in the event listener's function. Instead I passed it through the admin->getSubject() method. Then instead of adding a sonataadmin form type, inside the event listener's function, I had to use a plain symfony form type.
The Ajax part as you mentioned is another question. All the weird things on the branch add method in the form builder is related to a customized field type for this matter. Don't worry about it.