I am using CollectionType Field in my Symfony project, to be able to generate as many similar form items as user needs. The problem I have appears while form is being submitted - it passes all the values directly to db, but the foreign key id (household_app_id) remains null, even though relations seem to be correct.
What can I do to prevent this bug? And why is this happening?
My project code is below:
Both Entities:
<?php //HouseholdApp.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* HouseholdApp
*
* #ORM\Table(name="household_application")
* #ORM\Entity(repositoryClass="AppBundle\Repository\HouseholdAppRepository")
* #Vich\Uploadable
*
*/
class HouseholdApp
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* One Application has One Household.
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Household", inversedBy="householdApp")
* #ORM\JoinColumn(name="household_id", referencedColumnName="id")
*/
private $household;
/**
* #var File
*
* #Vich\UploadableField(mapping="union_register", fileNameProperty="unionRegisterName")
*/
private $unionRegister;
/**
* #var File
*
* #Vich\UploadableField(mapping="apt_owners_decision", fileNameProperty="aptOwnersDecisionName")
*/
private $aptOwnersDecision;
/**
* #var File
*
* #Vich\UploadableField(mapping="mulaptinventory", fileNameProperty="multiAptInventoryName")
*/
private $multiAptInventory;
/**
* #var File
*
* #Vich\UploadableField(mapping="buildtechsurvey", fileNameProperty="buildingTechnicalSurveyName")
*/
private $buildingTechnicalSurvey;
/**
* #var File
*
* #Vich\UploadableField(mapping="defectact", fileNameProperty="defectActName")
*/
private $defectAct;
/**
* #var File
*
* #Vich\UploadableField(mapping="activitycost", fileNameProperty="activityCostName")
*/
private $activityCost;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\DefectAct", mappedBy="householdApp", cascade={"persist"}, orphanRemoval=true)
*/
protected $actOfDefects;
/**
* Set unionRegister
*
* #param File|UploadedFile $unionRegister
*
* #return HouseholdApp
*/
public function setUnionRegister(File $unionRegister = null)
{
$this->unionRegister = $unionRegister;
if ($unionRegister) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->uUpdatedAt = new \DateTime('now');
}
return $this;
}
/**
* Get unionRegister
*
* #return File|UploadedFile
*/
public function getUnionRegister()
{
return $this->unionRegister;
}
/**
* Set aptOwnersDecision
*
* #param File|UploadedFile $aptOwnersDecision
*
* #return HouseholdApp
*/
public function setAptOwnersDecision(File $aptOwnersDecision = null)
{
$this->aptOwnersDecision = $aptOwnersDecision;
if ($aptOwnersDecision) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->aUpdatedAt = new \DateTime('now');
}
return $this;
}
/**
* Get aptOwnersDecision
*
* #return File|UploadedFile
*/
public function getAptOwnersDecision()
{
return $this->aptOwnersDecision;
}
/**
* Set multiAptInventory
*
* #param File|UploadedFile $multiAptInventory
*
* #return HouseholdApp
*/
public function setMultiAptInventory(File $multiAptInventory = null)
{
$this->multiAptInventory = $multiAptInventory;
if ($multiAptInventory) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->mUpdatedAt = new \DateTime('now');
}
return $this;
}
/**
* Get multiAptInventory
*
* #return File|UploadedFile
*/
public function getMultiAptInventory()
{
return $this->multiAptInventory;
}
/**
* Set buildingTechnicalSurvey
*
* #param File|UploadedFile $buildingTechnicalSurvey
*
* #return HouseholdApp
*/
public function setBuildingTechnicalSurvey(File $buildingTechnicalSurvey = null)
{
$this->buildingTechnicalSurvey = $buildingTechnicalSurvey;
if ($buildingTechnicalSurvey) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->bUpdatedAt = new \DateTime('now');
}
return $this;
}
/**
* Get buildingTechnicalSurvey
*
* #return File|UploadedFile
*/
public function getBuildingTechnicalSurvey()
{
return $this->buildingTechnicalSurvey;
}
/**
* Set defectAct
*
* #param File|UploadedFile $defectAct
*
* #return HouseholdApp
*/
public function setDefectAct(File $defectAct = null)
{
$this->defectAct = $defectAct;
if ($defectAct) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->dUpdatedAt = new \DateTime('now');
}
return $this;
}
/**
* Get defectAct
*
* #return File|UploadedFile
*/
public function getDefectAct()
{
return $this->defectAct;
}
/**
* Set activityCost
*
* #param File|UploadedFile $activityCost
*
* #return HouseholdApp
*/
public function setActivityCost(File $activityCost = null)
{
$this->activityCost = $activityCost;
if ($activityCost) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->acUpdatedAt = new \DateTime('now');
}
return $this;
}
/**
* Get activityCost
*
* #return File|UploadedFile
*/
public function getActivityCost()
{
return $this->activityCost;
}
}
<?php //DefectAct.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* DefectAct
*
* #ORM\Table(name="defect_act")
* #ORM\Entity(repositoryClass="AppBundle\Repository\DefectActRepository")
*
*/
class DefectAct
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $defectWork;
/**
* #var string
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $defectDescription;
/**
* #var string
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $preventionDeadline;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\HouseholdApp", inversedBy="actOfDefects")
*/
private $householdApp;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set defectWork
*
* #param string $defectWork
*
* #return DefectAct
*/
public function setDefectWork($defectWork)
{
$this->defectWork = $defectWork;
return $this;
}
/**
* Get defectWork
*
* #return string
*/
public function getDefectWork()
{
return $this->defectWork;
}
/**
* Set defectDescription
*
* #param string $defectDescription
*
* #return DefectAct
*/
public function setDefectDescription($defectDescription)
{
$this->defectDescription = $defectDescription;
return $this;
}
/**
* Get defectDescription
*
* #return string
*/
public function getDefectDescription()
{
return $this->defectDescription;
}
/**
* Set preventionDeadline
*
* #param string $preventionDeadline
*
* #return DefectAct
*/
public function setPreventionDeadline($preventionDeadline)
{
$this->preventionDeadline = $preventionDeadline;
return $this;
}
/**
* Get preventionDeadline
*
* #return string
*/
public function getPreventionDeadline()
{
return $this->preventionDeadline;
}
/**
* Set householdApp
*
* #param \AppBundle\Entity\HouseholdApp $householdApp
*
* #return DefectAct
*/
public function setHouseholdApp(\AppBundle\Entity\HouseholdApp $householdApp = null)
{
$this->householdApp = $householdApp;
return $this;
}
/**
* Get householdApp
*
* #return \AppBundle\Entity\HouseholdApp
*/
public function getHouseholdApp()
{
return $this->householdApp;
}
}
Controller:
/**
* #Security("has_role('ROLE_HOUSEHOLD')")
* #param \Symfony\Component\HttpFoundation\Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function householdApplicationAction(Request $request) {
$application = $this->getUser()->getRhousehold()->getHouseholdApp();
$flag = true;
if(is_null($application)) {
$flag = false;
}
$form = $this->createForm(HouseholdAppType::class, $application, ['disabled' => false]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$appData = $form->getData();
$em = $this->get("doctrine.orm.default_entity_manager");
$household = $this->getUser()->getRhousehold();
$appData->setHousehold($household);
$em->persist($appData);
$em->flush();
return $this->redirectToRoute("householder_app");
}
return $this->render("#App/household_application.html.twig",
['form' => $form->createView(), 'householdapp' => $application]);
}
Forms:
<?php //HouseholdAppType
namespace AppBundle\Form;
use AppBundle\Entity\HouseholdApp;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichFileType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
class HouseholdAppType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('multiapthouseaddress', TextType::class, ['label' => 'form.multiapthouseaddress'])
->add('cadastralnumberofbuilding', TextType::class, ['label' => 'form.cadastralnumberofbuilding'])
->add('totalbuildingarea', TextType::class, ['label' => 'form.totalbuildingarea'])
->add('house_constructive_solution', TextType::class, ['label' => 'form.house_constructive_solution'])
->add('exploitation_start_year', IntegerType::class, ['label' => 'form.exploitation_start_year'])
->add('non_residential_area', TextType::class, ['label' => 'form.non_residential_area'])
->add('has_heat_supply_system', CheckboxType::class, ['label' => 'form.has_heat_supply_system'])
->add('apts_with_individual_heating', TextType::class, ['label' => 'form.apts_with_individual_heating'])
->add('authorized_person_data', TextType::class, ['label' => 'form.authorized_person_data'])
->add('is_vatpayer', CheckboxType::class, ['label' => 'form.is_vatpayer'])
->add('personal_code', TextType::class, ['label' => 'form.personal_code'])
->add('declared_address', TextType::class, ['label' => 'form.declared_address'])
->add('contact_person_data', TextType::class, ['label' => 'form.contact_person_data'])
->add('unionRegister', VichFileType::class, ['required' => false, 'label' => 'form.unionRegister'])
->add('aptOwnersDecision', VichFileType::class, ['required' => false, 'label' => 'form.aptOwnersDecision'])
->add('multiAptInventory', VichFileType::class, ['required' => false, 'label' => 'form.multiAptInventory'])
->add('buildingTechnicalSurvey', VichFileType::class, ['required' => false, 'label' => 'form.buildingTechnicalSurvey'])
->add('defectAct', VichFileType::class, ['required' => false, 'label' => 'form.defectAct'])
->add('activityCost', VichFileType::class, ['required' => false, 'label' => 'form.activityCost'])
->add('actOfDefects', CollectionType::class, [
'label' => true,
'entry_type' => DefectsActCollectionType::class,
'allow_add' => true,
'prototype' => true,
'allow_delete' => true,
'by_reference' => false,
'delete_empty' => true,
'required' => false
])
->add('save', SubmitType::class, [
'label' => 'form.save',
'attr' => [
'class' => 'btn2'
]]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => HouseholdApp::class,
'translation_domain' => 'main',
));
}
}
<?php //DefectActCollectionType
namespace AppBundle\Form;
use AppBundle\Entity\DefectAct;
use AppBundle\Entity\HouseholdApp;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DefectsActCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('defectWork', TextType::class)
->add('defectDescription', TextType::class)
->add('preventionDeadline', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'translation_domain' => 'main',
'data_class' => DefectAct::class
]);
}
public function getBlockPrefix()
{
return 'app_bundle_defects_act_collection_type';
}
}
Template:
{% extends '#App/templates/base.html.twig' %}
{% trans_default_domain "main" %}
{% block main %}
{% include '#App/templates/progressbar.html.twig' %}
{% include '#App/templates/tabs.html.twig' %}
{% include '#App/h_apply_menu.html.twig' %}
<section id='cabinet_household_app'>
<div class='container'>
<div class="tab-content">
<div id="h-apply" class="tab-pane fade in active form_page">
{{ form_start(form)}}
{{ form_row(form._token) }}
<h3>{{ 'form.house_data'|trans }}</h3>
<div class='field'><div class="form-group"> {{ form_row(form.multiapthouseaddress)}}</div></div>
<div class='field'><div class="form-group"> {{ form_row(form.cadastralnumberofbuilding)}}</div></div>
<div class='field'><div class="form-group"> {{ form_row(form.totalbuildingarea)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.house_constructive_solution)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.exploitation_start_year)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.non_residential_area)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.has_heat_supply_system, {'required': false})}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.apts_with_individual_heating)}}</div></div>
<h3>{{ 'form.app_applying_person'|trans }}</h3>
<div class='field'><div class="form-group">{{ form_row(form.authorized_person_data)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.is_vatpayer, {'required': false})}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.personal_code)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.declared_address)}}</div></div>
<div class='field'><div class="form-group">{{ form_row(form.contact_person_data)}}</div></div>
<h3>{{ 'form.apply_documents'|trans }}</h3>
<section id='cabinet_household_inner_app'>
{% if householdapp is null %}
<h4>{{ 'form.unionRegister'|trans }}</h4>
<div class='field'><div class="form-group">{{ form_widget(form.unionRegister) }}</div></div>
<h4>{{ 'form.aptOwnersDecision'|trans }}</h4>
<div class='field'><div class="form-group">{{ form_widget(form.aptOwnersDecision) }}</div></div>
<h4>{{ 'form.multiAptInventory'|trans }}</h4>
<div class='field'><div class="form-group">{{ form_widget(form.multiAptInventory) }}</div></div>
<h4>{{ 'form.buildingTechnicalSurvey'|trans }}</h4>
<div class='field'><div class="form-group">{{ form_widget(form.buildingTechnicalSurvey) }}</div></div>
<h4>{{ 'form.defectAct'|trans }}</h4>
<div class='field'><div class="form-group">{{ form_widget(form.defectAct) }}</div></div>
<h4>{{ 'form.activityCost'|trans }}</h4>
<div class='field'><div class="form-group">{{ form_widget(form.activityCost) }}</div></div>
{% else %}
<h4>{{ 'form.unionRegister'|trans }}</h4>
Download
<h4>{{ 'form.aptOwnersDecision'|trans }}</h4>
Download
<h4>{{ 'form.multiAptInventory'|trans }}</h4>
Download
<h4>{{ 'form.buildingTechnicalSurvey'|trans }}</h4>
Download
<h4>{{ 'form.defectAct'|trans }}</h4>
Download
<h4>{{ 'form.activityCost'|trans }}</h4>
Download
{% endif %}
<div class="container center" data-prototype="{{ form_widget(form.actOfDefects.vars.prototype)|e('html_attr') }}">
{% for actOfDefect in form.actOfDefects %}
<div class="row document">{{ form_row(actOfDefect) }}</div>
{% endfor %}
<div class="row">
<div class="col-xs-12 center">
<div id="add" class="btn2">{{ "common.addFile"|trans }}</div>
</div>
</div>
</div>
</section>
<div class="center">{{ form_row(form.save) }}</div>
{{ form_end(form, {'render_rest': false}) }}
</div>
</div>
</div>
</section>
{% endblock main %}
{% block js_bottom %}
<script>
var $collectionHolder;
$(document).ready(function () {
// Get the ul that holds the collection of tags
$collectionHolder = $('div.container.center');
// add a delete link to all of the existing tag form li elements
$collectionHolder.find('li').each(function() {
addTagFormDeleteLink($(this));
});
// add the "add a tag" anchor and li to the tags ul
//$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$('#add').on('click', function (e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
addTagForm($collectionHolder);
});
});
function addTagFormDeleteLink($tagFormLi) {
var $removeFormA = $('{{ "common.cancel"|trans }}');
$tagFormLi.append($removeFormA);
$removeFormA.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// remove the li for the tag form
$tagFormLi.remove();
});
}
function addTagForm($collectionHolder) {
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormBlock = $('<div class="row document"></div>').append(newForm);
$collectionHolder.append($newFormBlock);
addTagFormDeleteLink($newFormBlock);
}
</script>
{% endblock %}
In your class HouseholdApp, you must have :
public function addActOfDefects(\AppBundle\Entity\DefectAct $actOfDefect)
{
$this->actOfDefects[] = $actOfDefect;
$actOfDefect->setHouseholdApp($this);
return $this;
}
public function removeActOfDefects(\AppBundle\Entity\DefectAct $actOfDefect)
{
$this->actOfDefects->removeElement($actOfDefect);
}
Related
I'm using VichUploaderBundle in my application to upload the files.
Problem is that even if I followed the docs, the file isn't uploaded (even if seems to be all right, as profiles sais), and no rows was make in the entity table.
Here is my vich_uploader.yaml:
vich_uploader:
db_driver: orm
mappings:
media:
uri_prefix: /media
upload_destination: '%kernel.project_dir%/public/media'
inject_on_load: false
delete_on_update: true
delete_on_remove: true
Here is my Media Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity
* #Vich\Uploadable
*/
class Media
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="media", fileNameProperty="imageName", size="imageSize")
*
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="integer")
*
* #var integer
*/
private $imageSize;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $imageFile
*/
public function setImageFile(?File $imageFile = null): void
{
$this->imageFile = $imageFile;
if (null !== $imageFile) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTimeImmutable();
}
}
public function getImageFile(): ?File
{
return $this->imageFile;
}
public function setImageName(?string $imageName): void
{
$this->imageName = $imageName;
}
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageSize(?int $imageSize): void
{
$this->imageSize = $imageSize;
}
public function getImageSize(): ?int
{
return $this->imageSize;
}
}
I just need to store the file in the entity, there is no relations between files and other entities.
Here is my MediaType
<?php
namespace App\Form;
use App\Entity\Media;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;
class MediaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('imageFile', VichImageType::class, [
'required' => false,
'allow_delete' => true,
'download_uri' => true
])
->add('submit', SubmitType::class, [
'attr' => [
'class' => 'btn btn-success'
],
'label' => 'Upload File'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Media::class,
]);
}
}
The Form is handled by this Twig template:
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel">
<div class="panel-body">
{{ form_start(form) }}
{{ form_widget(form.imageFile) }}
</div>
<div class="panel-footer">
{{ form_widget(form.submit) }}
{{ form_end(form) }}
</div>
</div>
</div>
</div>
</div>
I'm developing a king of simple CMS, with Symfony 4.1.
Regarding my question, we have 2 entities:
Post Entity:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post extends BaseEntity
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="text")
*/
private $content;
/**
* #ORM\Column(type="boolean")
*/
private $status;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Category", inversedBy="posts")
*/
private $categories;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Picture", mappedBy="post", orphanRemoval=true, cascade={"persist"})
*/
private $pictures;
/**
* #Assert\All({#Assert\Image(mimeTypes="image/jpeg")})
*
*/
private $pictureFiles;
/**
* Post constructor.
*/
public function __construct()
{
$this->categories = new ArrayCollection();
$this->pictures = new ArrayCollection();
}
/**
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* #return null|string
*/
public function getContent(): ?string
{
return $this->content;
}
/**
* #param string $content
* #return Post
*/
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
/**
* #return bool|null
*/
public function getStatus(): ?bool
{
return $this->status;
}
/**
* #param bool $status
* #return Post
*/
public function setStatus(bool $status): self
{
$this->status = $status;
return $this;
}
/**
* #return Collection|Category[]
*/
public function getCategories(): Collection
{
return $this->categories;
}
/**
* #param Category $category
* #return Post
*/
public function addCategory(Category $category): self
{
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
}
return $this;
}
/**
* #param Category $category
* #return Post
*/
public function removeCategory(Category $category): self
{
if ($this->categories->contains($category)) {
$this->categories->removeElement($category);
}
return $this;
}
/**
* #return Collection|Picture[]
*/
public function getPictures(): Collection
{
return $this->pictures;
}
/**
* #param Picture $picture
* #return Post
*/
public function addPicture(Picture $picture): self
{
if (!$this->pictures->contains($picture)) {
$this->pictures[] = $picture;
$picture->setPost($this);
}
return $this;
}
/**
* #param Picture $picture
* #return Post
*/
public function removePicture(Picture $picture): self
{
if ($this->pictures->contains($picture)) {
$this->pictures->removeElement($picture);
if ($picture->getPost() === $this) {
$picture->setPost(null);
}
}
return $this;
}
/**
* #return mixed
*/
public function getPictureFiles()
{
return $this->pictureFiles;
}
/**
* #param $pictureFiles
* #return Post
*/
public function setPictureFiles($pictureFiles): self
{
foreach ($pictureFiles as $pictureFile) {
/** #var Picture $picture */
$picture = new Picture();
$picture->setImageFile($pictureFile);
$this->addPicture($picture);
}
$this->pictureFiles = $pictureFiles;
return $this;
}
}
Picture Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\File;
/**
* #ORM\Entity(repositoryClass="App\Repository\PictureRepository")
*/
class Picture
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var File|null
* #Assert\Image(mimeTypes="image/jpeg")
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Post", inversedBy="pictures")
* #ORM\JoinColumn(nullable=false)
*/
private $post;
/**
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* #return File|null
*/
public function getImageFile(): ? File
{
return $this->imageFile;
}
/**
* #param File|null $imageFile
* #return Picture
*/
public function setImageFile(? File $imageFile): self
{
$this->imageFile = $imageFile;
return $this;
}
/**
* #return string|null
*/
public function getFilename(): ?string
{
return $this->filename;
}
/**
* #param string $filename
* #return Picture
*/
public function setFilename(string $filename): self
{
$this->filename = $filename;
return $this;
}
/**
* #return Post|null
*/
public function getPost(): ?Post
{
return $this->post;
}
/**
* #param Post|null $post
* #return Picture
*/
public function setPost(?Post $post): self
{
$this->post = $post;
return $this;
}
}
So for adding a Post, I have a PostType:
<?php
namespace App\Form;
use App\Entity\Category;
use App\Entity\Post;
use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class PostType
* #package App\Form
*/
class PostType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('content', CKEditorType::class)
->add('categories', EntityType::class,
[
'class' => Category::class,
'required' => true,
'choice_label' => 'name',
'multiple' => true,
]
)
->add('pictureFiles', FileType::class,
[
'required' => false,
'multiple' => true,
'label' => 'Add files...',
'attr' =>
[
'action' => '%kernel.project_dir%/public/media/posts'
]
]
)
->add('status')
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Post::class,
]);
}
}
The view corresponding to that form:
{% form_theme form '/admin/form/switch_btn_layout.html.twig' %}
{{ form_start(form) }}
{{ form_errors(form) }}
<div class="form-row">
<div class="col-md-6">
{{ form_row(form.name) }}
{{ form_row(form.categories) }}
{{ form_row(form.status) }}
</div>
<div class="col-md-6 dropzone" id="postDropzone">
{{ form_row(form.pictureFiles, {'attr': {'class': 'dropzone'}} ) }}
<div class="dropzone-previews" style="border: 1px solid red"></div>
</div>
</div>
<div class="form-group">
{{ form_row(form.content) }}
</div>
<div class="form-group">
{{ form_row(form.status) }}
</div>
{{ form_rest(form) }}
<button class="btn btn-success btn-lg btn-block" id="postSubmit">
{{ button_label|default('Save') }}
</button>
{{ form_end(form) }}
As you can see, the "input" for files as the dropzone css class.
Indeed, my project include the oneup_uploader bundle, for dropzone.
Here the configuration for oneup_uploader:
oneup_uploader:
mappings:
# This is a mapping example, remove it and create your own mappings.
post_image:
frontend: dropzone
namer: oneup_uploader.namer.uniqid
storage:
directory: '%kernel.project_dir%/public/media/posts'
And my script for Dropzone:
Dropzone.autoDiscover = false;
var postDropzone = new Dropzone('.dropzone', {
url: '%kernel.project_dir%/public/media/posts',
// url: 'file/post',
maxFiles: 10,
addRemoveLinks: true,
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: 100,
});
postDropzone.on("addedfile", function (file) {
file.previewElement.addEventListener("click", function () {
postDropzone.removeFile(file);
})
});
The issue for me is:
no file is save in the folder
the Post entity is save in my DB, but nothing is save for Pictures.
I also tried to not use OneUploaderBundle, and use VichUploader: the saving part in DB is perfect, but I can't link it to dropzone.
Some help guys ?
Thanks a lot !
Might be useful for new visitors.
You can use a library that extends Symfony Form and adds a new type DropzneType.
1.Install the library
composer require emrdev/symfony-dropzone
This way you will have a new form type DropzoneType
2.Use the type in your form like this
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options)
{
// userFiles is OneToMany
$builder->add('userFiles', DropzoneType::class, [
'class' => File::class,
'maxFiles' => 6,
'uploadHandler'=>'uploadHandler', // route name
'removeHandler'=> 'removeHandler'// route name
]);
}
Change the uploadHandler and removeHandler options to your endpoints
3.Route uploadHandler/removeHandler might look something like this
/**
* #Route("/uploadhandler", name="uploadHandler")
*/
public function uploadhandler(Request $request, ImageUploader $uploader) {
$doc = $uploader->upload($request->files->get('file'));
$file = new File();
$file->setSrc($doc['src']);
...
$this->getDoctrine()->getManager()->persist($file);
$this->getDoctrine()->getManager()->flush();
return new JsonResponse($file);
}
/**
* #Route("/removeHandler/{id}", name="removeHandler")
*/
public function removeHandler(Request $request,File $file = null) {
$this->getDoctrine()->getManager()->remove($file);
$this->getDoctrine()->getManager()->flush();
return new JsonResponse(true);
}
note that uploadhandler should return a File object
you should pass upload url instead of upload directory
generate url in twig - {{ oneup_uploader_endpoint('post_image') }}
var postDropzone = new Dropzone('.dropzone', {
url: '{{ oneup_uploader_endpoint('post_image') }}',
// url: '%kernel.project_dir%/public/media/posts',
// url: 'file/post',
maxFiles: 10,
addRemoveLinks: true,
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: 100,
});
Could someone explain me what i am doing wrong. I have an entity with classes Actualite and ActualiteTranslation.
I created ActualiteAdmin class with sonanta:admin:generate.
Everything is working as expected. I debug fields with dump($actualite) in the controller and i see that each fields are well displayed :
array:1 [▼
0 => Actualite {#742 ▼
-id: 8
-date: DateTime {#806 ▶}
-image: null
#translations: PersistentCollection {#709 ▼
-snapshot: array:1 [ …1]
-owner: Actualite {#742}
-association: array:16 [ …16]
-em: EntityManager {#789 …11}
-backRefFieldName: "translatable"
-typeClass: ClassMetadata {#738 …}
-isDirty: false
#collection: ArrayCollection {#662 ▼
-elements: array:1 [▼
"fr" => ActualiteTranslation {#705 ▼
#actucategorie: "categorie"
#alt: "image"
#titre: "Page Facebook"
#article: "mon article"
#id: 4
#locale: "fr"
#translatable: Actualite {#742}
#slug: "page-facebook"
}
]
}
#initialized: true
}
#newTranslations: null
#currentLocale: "fr"
#defaultLocale: "fr"
}
]
I have thir error in Twig Template when using {{actualite.translations.titre}}:
Key "translations" for array with keys "0" does not exist.
This is the code I use :
namespace BDN\ActualitesBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* Actualite
*
* #ORM\Table(name="bdn_actualite")
* #ORM\Entity(repositoryClass="BDN\ActualitesBundle\Repository\ActualiteRepository")
*
*/
class Actualite
{
use ORMBehaviors\Translatable\Translatable;
/**
* #param $method
* #param $args
*
* #return mixed
*/
public function __call($method, $args)
{
if (!method_exists(self::getTranslationEntityClass(), $method)) {
$method = 'get' . ucfirst($method);
}
return $this->proxyCurrentLocaleTranslation($method, $args);
}
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="datetime")
*/
private $date;
/**
* #var string
*
* #ORM\Column(name="image", type="string", length=255, nullable=true)
*/
private $image;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set date
*
* #param \DateTime $date
*
* #return null
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Get date
*
* #return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Set image
*
* #param string $image
*
* #return null
*/
public function setImage($image)
{
$this->image = $image;
return $this;
}
/**
* Get image
*
* #return string
*/
public function getImage()
{
return $this->image;
}
}
namespace BDN\ActualitesBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* ActualiteTranslation
*
* #ORM\Entity
* #ORM\Table(name="bdn_actualite_translation")
*
*/
class ActualiteTranslation
{
use ORMBehaviors\Translatable\Translation;
use ORMBehaviors\Sluggable\Sluggable;
/**
* #var string
*
* #ORM\Column(name="actucategorie", type="string", length=50)
*/
protected $actucategorie;
/**
* #var string
*
* #ORM\Column(name="alt", type="string", length=255)
*/
protected $alt;
/**
* #var string
*
* #ORM\Column(name="titre", type="string", length=255, unique=true)
*/
protected $titre;
public function getSluggableFields()
{
return ['titre'];
}
/**
* #var string
*
* #ORM\Column(name="article", type="text")
*/
protected $article;
/**
* Set actucategorie
*
* #param string $actucategorie
*
* #return ActualiteTranslation
*/
public function setActucategorie($actucategorie)
{
$this->actucategorie = $actucategorie;
return $this;
}
/**
* Get actucategorie
*
* #return string
*/
public function getActucategorie()
{
return $this->actucategorie;
}
/**
* Set alt
*
* #param string $alt
*
* #return ActualiteTranslation
*/
public function setAlt($alt)
{
$this->alt = $alt;
return $this;
}
/**
* Get alt
*
* #return string
*/
public function getAlt()
{
return $this->alt;
}
/**
* Set titre
*
* #param string $titre
*
* #return ActualiteTranslation
*/
public function setTitre($titre)
{
$this->titre = $titre;
return $this;
}
/**
* Get titre
*
* #return string
*/
public function getTitre()
{
return $this->titre;
}
/**
* Set article
*
* #param string $article
*
* #return ActualiteTranslation
*/
public function setArticle($article)
{
$this->article = $article;
return $this;
}
/**
* Get article
*
* #return string
*/
public function getArticle()
{
return $this->article;
}
}
<!-- begin snippet: js hide: false console: true babel: false -->
<?php
namespace BDN\ActualitesBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use A2lix\TranslationFormBundle\Form\Type\TranslationsType;
class ActualiteAdmin extends AbstractAdmin
{
/**
* #param DatagridMapper $datagridMapper
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('date')
->add('image')
;
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add('date')
->add('image')
->add('_action', null, array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
),
))
;
}
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('date')
->add('image')
->add("translations", TranslationsType::class, array(
"fields" => array(
"actucategorie" => [],
"alt" => [],
"titre" => [],
"slug" => ["display" => false,],
"article" => [ "field_type" => "text"],
),
))
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('id')
->add('date')
->add('image')
->add("translations", TranslationsType::class, array(
"required_locales" => ['fr'],
"fields" => array(
"actucategorie" => [],
"alt" => [],
"titre" => [],
"article" => [ "field_type" => "text"],
),
))
;
}
}
{% extends "::base.html.twig" %}
{% block page %}
<section class="row jumbotron">
<div class="col-lg-12">
<div class="row">
<div class="col-xs-9">
<h3>{{ actualite.getTitre() }}</h3>
<p class="categorie">Le {{ actualite.date | date('d/m/Y') }} | Catégorie : {{ actualite.translate.actucategorie }}</p>
</div>
{% if actualite.image is not null %}
<div class="col-xs-3 hidden-xs">
<img src="{{ asset('actualite.image') }}" class="img-thumbnail img-shop" alt="{{ actualite.alt }}">
</div>
{% endif %}
</div>
<div class="row content-actualite">
<div class="col-lg-12">
{{ actualite.translate.article }}
</div>
</div>
</div>
</section>
{% endblock %}
Any help would be very appreciated!
Denis
What should happen here is this:
Only dd/mm/yyyy format will be accepted for DOB.
If different format given then "The DoB field must have a valid format." message should read on the screen BUT this message should be coming from the ENTITY, not form TYPE set with 'invalid_message' attribute.
JFYI: I can define $dob as 'string' in the entity and as 'text' in the form type to make whole process work but it is not good practise. The reason is I don't want varchar field for $dob in database, I want date field.
PERSON ENTITY (Note: This is the only place I want form validation takes place):
namespace Se\HirBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* Person
*
* #ORM\Entity
* #ORM\Table(name="person")
* #ORM\HasLifecycleCallbacks
*/
class Person
{
/**
* #var integer $id
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string $firstname
*
* #Assert\NotBlank(message = "The Firstname field should not be blank.")
* #Assert\Length(max = "100", maxMessage = "The Firstname field cannot be longer than {{ limit }} characters length.")
*
* #ORM\Column(type = "string", length = 100)
*/
protected $firstname;
/**
* #var date $dob
*
* #Assert\NotBlank(message = "The DoB field should not be blank.")
* #Assert\Regex(pattern = "/^(0[1-9]|[12][0-9]|3[01])[\/\-](0[1-9]|1[012])[\/\-]\d{4}$/", message = "The DoB field must have a valid format.")
*
* #ORM\Column(type = "date", length = 10)
*/
protected $dob;
/**
* #var datetime $created
*
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #var datetime $updated
*
* #ORM\Column(type="datetime", nullable = true)
*/
protected $updated;
/**
* Gets triggered only on insert
*
* #ORM\PrePersist
*/
public function onPrePersist()
{
$this->created = new \DateTime("now");
}
/**
* Gets triggered every time on update
*
* #ORM\PreUpdate
*/
public function onPreUpdate()
{
$this->updated = new \DateTime("now");
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set firstname
*
* #param string $firstname
* #return Person
*/
public function setFirstname($firstname)
{
$this->firstname = $firstname;
return $this;
}
/**
* Get firstname
*
* #return string
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* Set dob
*
* #param string $dob
* #return Person
*/
public function setDob($dob)
{
$this->dob = $dob;
return $this;
}
/**
* Get dob
*
* #return string
*/
public function getDob()
{
return $this->dob;
}
}
Form TYPE file:
namespace Se\HirBundle\Form\Type\Person;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setAction($options['action'])
->setMethod('POST')
->add('firstname', 'text',
array('label' => 'Firstname', 'error_bubbling' => true))
->add('dob', 'date',
array('label' => 'DoB', 'widget' => 'single_text',
'format' => 'dd/MM/yyyy', 'input' => 'datetime', 'error_bubbling' => true))
->add('create', 'submit',
array('label' => 'Create Person'));
}
public function getName()
{
return 'personcreate';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Se\HirBundle\Entity\Person'));
}
}
CONTROLLER:
namespace Se\HirBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Se\HirBundle\Entity\Person;
use Se\HirBundle\Form\Type\Person\CreateType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CrudController extends Controller
{
public function createAction()
{
$person = new Person();
$form = $this->createForm(new CreateType(), $person,
array('action' => $this->generateUrl('create_submit')));
return $this->render('SeHirBundle:Default:create.html.twig',
array('page' => 'Create', 'form' => $form->createView()));
}
public function createPersonAction(Request $request)
{
if ($request->getMethod() != 'POST')
{
return new Response('Only POST method is accepted');
}
$person = new Person();
$form = $this->createForm(new CreateType(), $person,
array('action' => $this->generateUrl('create_submit')));
$form->handleRequest($request);
if ($form->isValid())
{
$submission = $form->getData();
$em = $this->getDoctrine()->getManager();
$person = new Person();
$person->setFirstname($submission->getFirstname());
$person->setDob($submission->getDob());
$em->persist($person);
$em->flush();
$this->get('session')->getFlashBag()->add('message', 'Person successfully created!');
return $this->redirect($this->generateUrl('message'));
}
return $this->render('SeHirBundle:Default:create.html.twig',
array('page' => 'Create', 'form' => $form->createView()));
}
}
TWIG:
{% extends '::base.html.twig' %}
{% block title %}{{ page }}{% endblock %}
{% block body %}
<b>{{ page|upper }}</b>
<hr />
{{ form_start(form, {attr: {novalidate:'novalidate'}}) }}
{% if form_errors(form) != '' %}
<div>{{ form_errors(form) }}</div>
{% endif %}
<div>
{{ form_label(form.firstname) }}
{{ form_widget(form.firstname) }}
</div>
<div>
{{ form_label(form.dob) }}
{{ form_widget(form.dob, {'type':'text'}) }}
</div>
<br />
<div>
{{ form_widget(form.create) }}
</div>
{{ form_end(form)}}
{% endblock %}
Here is a solution based on the Symfony2 documentation:
Controller
Start of the file
use Symfony\Component\HttpFoundation\Response;
Function in the controller
This controller will display the dob field, since this is a Datetime object it requires to use format() in order to display it. This is just an example to show that Symfony2 recognizes the date and transform it internally.
Uncommenting the lines starting with // would be sufficient to persist the Person entity with the dob.
public function testAction(Request $request)
{
$person = new Person();
$form = $this->createFormBuilder($person)
->add('dob', 'date',
array(
'label' => 'DoB',
'widget' => 'single_text',
'format' => 'dd/MM/yyyy',
'invalid_message' => 'Validation error goes here',
'error_bubbling' => true,
'input' => 'datetime' # return a Datetime object (*)
)
)
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
# perform some action, such as saving the task to the database
//$em = $this->getDoctrine()->getManager();
//$em->persist($person);
//$em->flush();
return new Response($person->getDob()->format('d-m-Y'));
}
return $this->render(
'YourBundle:Default:test.html.twig',
array(
'form' => $form->createView()
)
);
}
(*): http://symfony.com/fr/doc/master/reference/forms/types/date.html#input
Twig file
{{ form(form) }}
routing.yml
test:
pattern: /test/
defaults: { _controller: YourBundle:Default:test }
SOLUTION in Controller:
$dob = date('Y-m-d', strtotime(str_replace('/', '-', $submission->getDob())));
$person->setDob($dob);
I have a problem with Doctrine2 and two relationed entities.
There is a user-entity that can (not must) have one or a collection of social-entity referenced which contains a social network link.
I do not control Doctrine and I'm still learning relationship.
I want to add a user with/without adding social network link.
After several researches and testing, I am still unable to find a solution.
Here is my user-entity
<?php
//...
/**
* User
*
* #ORM\Table(name="admin_users")
* #ORM\Entity(repositoryClass="UserRepository")
* #ORM\HasLifecycleCallbacks()
*/
class User implements AdvancedUserInterface, \Serializable
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="username", type="string", length=25, unique=true)
*/
private $username;
/**
* #var string
*
* #ORM\Column(name="password", type="string", length=40)
*/
private $password;
/**
*
* #var string
*
* #Assert\NotBlank
*/
private $plainPassword;
//...
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Social", mappedBy="user", cascade={"persist","remove"})
* #ORM\JoinColumn(nullable=true)
*/
private $socials;
public function __construct()
{
$this->socials = new ArrayCollection();
}
//Some getters setters
/**
* Add socials
*
* #param Social $socials
* #return User
*/
public function addSocials(Social $socials)
{
$this->socials[] = $socials;
$socials->setUser($this);
return $this;
}
/**
* Remove socials
*
* #param Social $socials
*/
public function removeSocials(Social $socials)
{
$this->socials->removeElement($socials);
}
/**
* Get socials
*
* #return Collection
*/
public function getSocials()
{
return $this->socials;
}
}
Here is the social-entity
<?php
/**
* Social
*
* #ORM\Table(name="admin_social")
* #ORM\Entity(repositoryClass="SocialRepository")
*/
class Social
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=20, nullable=true)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="url", type="string", length=255, nullable=true)
*/
private $url;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="socials", cascade={"persist","remove"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=true)
*/
private $user;
//getters setters
/**
* Set user
*
* #param User $user
* #return Social
*/
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* #return User
*/
public function getUser()
{
return $this->user;
}
}
The userType code looks like this:
$builder
->add('username', 'text', array(
'attr'=> array('class' => 'span6',),
'label_attr' => array('class' => 'control-label'),
)
)
// ....
->add('sociaux', 'collection', array('type' => new SocialType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,))
;
Finally the controller code :
public function addAction()
{
$user = new User;
// Create the form
$form = $this->createForm( new UserType, $user );
// Gets the request
$request = $this->getRequest();
// Checks if the request have type POST
if ( $request->getMethod() == 'POST' ) {
// Links the request and the form
$form->bind( $request );
// Checks if all input values are correct
if ( $form->isValid() ) {
// Save user object in database
$em = $this->getDoctrine()->getManager();
// Persist entity user
$em->persist( $user );
$em->flush();
//...
}
}
//...
}
When I try to add a user without social-entity I have no error, but in the database I have in social table a row with null values. Please help.
UPDATE
In user-entity I added this :
if( !( $socials->getName() === null && $socials->getUrl() === null ) )
{
$this->socials[] = $socials;
$socials->setUser($this);
}
Now there is no row inserted in social table, but when I try editing the user, I have two collection field (duplicated).
See the screenshot
Here my template file (Twig) :
<div class="widget-body">
{{ form_start(form, { 'action': path('acme_admin_edit_user', {'id': userId}), 'attr': {'class': 'form-horizontal'} }) }}
<div class="control-group">
{{ form_errors(form.username) }}
{{ form_label(form.username) }}
<div class="controls">
{{ form_widget(form.username) }}
</div>
</div>
<!-- ... -->
<div id="acme_adminbundle_useredittype_socials" data-prototype="{{ form_row(form.socials.vars.prototype) | escape }}">
{% for social in form.socials %}
<div>
<label class="required text-primary lead">Lien n°{{ loop.index }}</label>
<div id="acme_adminbundle_useredittype_socials_{{ loop.index0 }}">
<div class="control-group">
{{ form_errors(social.name) }}
{{ form_label(social.name) }}
<div class="controls">
{{ form_widget(social.name) }}
</div>
</div>
<div class="control-group">
{{ form_errors(social.url) }}
{{ form_label(social.url) }}
<div class="controls">
{{ form_widget(social.url) }}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="txt-center well">
<input type="submit" class="auto-margin btn btn-primary btn-large" />
</div>
{{ form_end(form) }}
</div>
Try removing:
#ORM\JoinColumn(nullable=true)
from your User class. #JoinColumn should be defined only on one side of relationship and since Social entity contains name and referencedColumnName it is unnecessary inside the User.