Update form vich uploader, cannot delete or edit file - symfony

I can't delete or edit my uploaded image with VichUploaderBundle. I have an Annonce and Photo entities with OneToMany (bidirectionnal relation). I try with an attribute setUpdatedAt to call vich prePersist but he doesn't work.
Here is Annonce :
class Annonce
{
// ...
/**
* #ORM\OneToMany(targetEntity="Immo\AnnonceBundle\Entity\Photo", mappedBy="annonce", cascade={"persist", "remove"})
*/
private $photos;
Photo entity with setter/getterImage() :
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Photo
* #Vich\Uploadable
* #ORM\Table()
*/
class Photo
{ // id, etc.
/**
* #Assert\File(
* maxSize="1M",
* mimeTypes={"image/png", "image/jpeg", "image/pjpeg"}
* )
* #Vich\UploadableField(mapping="uploads_image", fileNameProperty="url")
*
* #var File $image
*/
protected $image;
/**
* #ORM\Column(type="string", length=255, name="url")
*
* #var string $url
*/
protected $url;
/**
* #ORM\ManyToOne(targetEntity="Immo\AnnonceBundle\Entity\Annonce", inversedBy="photos")
*/
private $annonce;
/**
* #ORM\Column(type="datetime", nullable=true)
*
* #var \DateTime $updatedAt
*/
protected $updatedAt;
/**
* Set image
*
* #param string $image
* #return Photo
*/
public function setImage($image)
{
$this->image = $image;
if ($this->image instanceof UploadedFile) {
$this->updatedAt = new \DateTime('now');
}
return $this;
}
Here is my config.yml:
knp_gaufrette:
stream_wrapper: ~
adapters:
uploads_adapter:
local:
directory: %kernel.root_dir%/../web/img/uploads
filesystems:
uploads_image_fs:
adapter: uploads_adapter
vich_uploader:
db_driver: orm
twig: true
gaufrette: true
storage: vich_uploader.storage.gaufrette
mappings:
uploads_image:
delete_on_remove: true
delete_on_update: true
inject_on_load: true
uri_prefix: img/uploads
upload_destination: uploads_image_fs
namer: vich_uploader.namer_uniqid
My AnnonceType:
$builder->add('photos', 'collection', array('type' => new PhotoType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
)
)
PhotoType:
$builder->add('image', 'file')
The Controller :
public function updateAnnonceAction($id)
{
$em = $this->getDoctrine()->getManager();
$annonce = $em->getRepository('ImmoAnnonceBundle:Annonce')
->findCompleteAnnonceById($id);
$form = $this->createForm(new AnnonceType, $annonce);
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($annonce);
$em->flush();
$this->get('session')->getFlashBag()->add('success', 'ok');
return $this->redirect($this->generateUrl('immo_admin_annonce_homepage'));
}
}
return $this->render('ImmoAnnonceBundle:Admin/Annonce:update.html.twig', array('annonce' => $annonce,
'form' => $form->createView()
)
);
}
And my template put input file for each Photo in Annonce in html :
{{ form_widget(form.photos) }} // With JS to manage add/delete on each input.
// Return this :
<input type="file" required="required" name="immo_annoncebundle_annonce[photos][2][image]" id="immo_annoncebundle_annonce_photos_2_image">

Add an "updateAt" attribute in your entity
See http://mossco.co.uk/symfony-2/vichuploaderbundle-how-to-fix-cannot-overwrite-update-uploaded-file/ for more informations

I know it is an old thread, but David's answer was not working in this case since OP tries to delete the Photo object within the Annonce object once it gets updated.
I had a similar case where i just had to check if the path (fileName) of the Photo object was null after the request handling (after performing a delete operation from the formular) and then if it is the case, i remove manually the Photo object calling entitymanager to perform the operation.
Looking into the code of VichUploaderBundle (see the remove method of UploadHandler class) shows that an event is being dispatched after requesting the deletion, and you could also bind to this event Events::POST_REMOVE (vich_uploader.post_remove) somewhere to handle the deletion. This solution sounds cleaner although the first one is also working well.

Related

Symfony3 ManyToMany relation and cascade persistance. cascade={"persist"} is configured but error occurs

I got error while persisting object saying I need to configure cascade persist option in ManyToMany relation, but it is configured.
A new entity was found through the relationship 'AppBundle\Entity\ShopProducts#shopProductImages' that was not configured to cascade persist operations for entity: AppBundle\Entity\ShopProductImages#000000007d4db89e00000000344e8db2. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'AppBundle\Entity\ShopProductImages#__toString()' to get a clue.
Controller/ShopController.php
$product = new ShopProducts();
$form = $this->createForm(ProductTypeNew::class, $product);
if ($form->isSubmitted() && $form->isValid())
{
$image = new ShopProductImages();
...
$product->addShopProductImages($image);
...
$em->persist($product);
$em->flush();
Entity/ShopProducts.php
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\ShopProductImages",
* mappedBy="shopProducts",
* cascade={"persist"}
* )
*/
private $shopProductImages;
/**
* #return ArrayCollection|ShopProductImages[]
*/
public function getShopProductImages()
{
return $this->shopProductImages;
}
public function addShopProductImages(ShopProductImages $image)
{
if ($this->shopProductImages->contains($image)) {
return;
}
$this->shopProductImages[] = $image;
$image->addImagesProduct($this);
}
public function removeShopProductImages(ShopProductImages $image)
{
if (!$this->shopProductImages->contains($image)) {
return;
}
$this->shopProductImages->removeElement($image);
$image->removeImagesProduct($this);
}
Entity/ShopProductImages.php
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\ShopProducts",
* inversedBy="shopProductImages",
* cascade={"persist"}
* )
* #ORM\JoinTable(name="shop_product_images_has_shop_products"),
* joinColumns={
* #ORM\JoinColumn(name="shop_product_images_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="shop_products_id", referencedColumnName="id")
* }
*/
private $shopProducts;
public function addImagesProduct(ShopProducts $product)
{
if ($this->shopProducts->contains($product)) {
return;
}
$this->shopProducts[] = $product;
$product->addShopProductImages($this);
}
public function removeImagesProduct(ShopProducts $product)
{
if (!$this->shopProducts->contains($product)) {
return;
}
$this->shopProducts->removeElement($product);
$product->removeShopProductImages($this);
}
Form/Type/ProductTypeNew.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('price')
->add('description', TextareaType::class)
->add('quantity')
->add('file', FileType::class, array('label' => 'Zdjęcie'))
In your controller, try adding $em->persist($image); before you flush.
Your issue seems to be that he can't create the values of the shop_product_images_has_shop_products table because the images don't have an Id yet, so you have to persist the $image and the $product entities before you flush.
Also, your cascade={"persist"} should only be in the image entity entity annotations, not on the product ones.
On manyToMany, you don't need cascade={"persist"}
You have your add..() and remove...() It replace your cascade to persist data into the middle table
In your form builder you need : ->add('ShopProductImages', 'collection', array( 'by_reference' => false, ))
And remove cascades from your annotations

StofDoctrineExtensionsBundle Uploadable

I use the StofDoctrineExtensionsBundle Uploadable to upload a picture in User entity.
<?php
namespace Application\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* #ORM\Entity
* #ORM\Table(name="user")
* #Gedmo\Uploadable(pathMethod="getPath", filenameGenerator="SHA1", allowOverwrite=true, maxSize="100000", allowedTypes="image/jpeg,image/pjpeg,image/png,image/x-png")
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
...
/**
* #ORM\Column(name="picture", type="string", length=255, nullable=true)
* #Gedmo\UploadableFilePath
*/
private $picture;
public function getPath()
{
return '/user';
}
public function setPhoto($photo)
{
$this->photo = $photo;
return $this;
}
public function getPhoto()
{
return $this->photo;
}
...
In the controller:
...
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$uploadableManager = $this->get('stof_doctrine_extensions.uploadable.manager');
$uploadableManager->markEntityToUpload($user, $user->getPath());
...
in the FormType:
...
->add('picture', FileType::class, array(
'label' => 'Picture',
'required' => false
))
...
config.yml:
# StofDoctrineExtensionsBundle Configuration
stof_doctrine_extensions:
default_locale: fr_FR
uploadable:
# Default file path: This is one of the three ways you can configure the path for the Uploadable extension
default_file_path: %kernel.root_dir%/../web/uploads
# Mime type guesser class: Optional. By default, we provide an adapter for the one present in the HttpFoundation component of Symfony
mime_type_guesser_class: Stof\DoctrineExtensionsBundle\Uploadable\MimeTypeGuesserAdapter
# Default file info class implementing FileInfoInterface: Optional. By default we provide a class which is prepared to receive an UploadedFile instance.
default_file_info_class: Stof\DoctrineExtensionsBundle\Uploadable\UploadedFileInfo
orm:
default:
uploadable: true
When I test it I get the message:
Unable to create "/user" directory.
Any idea to solve this problem. Thanks
Is your app in a server ? If so, verify the chmod.
Or remove the / at the beginning of (if your folder structure is web/user):
public function getPath()
{
return '/user';
}

VichUploaderBundle : image_name can't be null error

I'm using Symfony3.1 and I have just installed VichUploaderBundle.
I have followed the documentation but I get this error means that Field 'image_name' can not be empty (null)
SQLSTATE[23000]: Integrity constraint violation: 1048 Le champ 'image_name' ne peut être vide (null)
Config
vich_uploader:
db_driver: orm
mappings:
product_image:
uri_prefix: /images/actualites
upload_destination: kernel.root_dir/../web/images/actualites
inject_on_load: false
delete_on_update: true
delete_on_remove: true
the entity
/**
*
* #Vich\UploadableField(mapping="actualite_image", fileNameProperty="imageName")
*
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*
* #return Actualite
*/
public function setImageFile(File $image = null)
{
$this->imageFile = $image;
if ($image) {
// 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 \DateTime('now');
}
return $this;
}
/**
* #return File
*/
public function getImageFile()
{
return $this->imageFile;
}
/**
* #param string $imageName
*
* #return Actualite
*/
public function setImageName($imageName)
{
$this->imageName = $imageName;
return $this;
}
/**
* #return string
*/
public function getImageName()
{
return $this->imageName;
}
FormType:
->add('imageFile', FileType::class)
twig
<form class="form-horizontal" method="post" enctype="multipart/form-data">
//...
{{ form_widget(form.imageFile) }}
Action
public function creerAction(Request $request)
{
$entity = new Actualite();
$form = $this->createForm(BaseType::class, $entity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->redirectToRoute('backend_actualite_liste');
}
return $this->render('Backend/Actualite/creer.html.twig',array(
'form' => $form->createView(),
)
);
}
So, actually according to what hous stated, this is the change required to the app/config/config.yml file:
vich_uploader:
db_driver: orm
mappings:
actualite_image:
uri_prefix: /images/actualites
upload_destination: kernel.root_dir/../web/images/actualites
Note to hous: You don't need those last 3 options in the config.yml file, since they are the default
Solved
The error is that the upload mapping name in the config and the mapping name in the entity are different but they must be the same.
In the config file is product_image and in the entity is actualite_image.
Now I give them the same name and it works good now.
Just as a note, there is different Symfony3 setup here, which might be helpful.
I think for your setup, it's different for Symfony3.
Try this:
->add('imageFile', VichImageType::class, array(
'required' => false,
))
Also, you need this change too:
# app/config/config.yml
twig:
form_themes:
# other form themes
- 'VichUploaderBundle:Form:fields.html.twig'

How to use mimeType Assert with VichUploader?

This assert is passing Symfony's form validation when uploading any file with VichUploaderBundle:
/**
* #Vich\UploadableField(mapping="product_media", fileNameProperty="path")
* #Assert\File(
* mimeTypes = {"image/jpeg", "image/gif", "image/png", "video/mp4", "video/quicktime", "video/avi"},
* mimeTypesMessage = "Wrong file type (jpg,gif,png,mp4,mov,avi)"
* )
* #var File $pathFile
*/
protected $pathFile;
I cannot see what the problem is with the assert. How can I validate file types with VichUploader?
You can use validation callback to solve this issue.
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\Repository\EntityRepository")
* #ORM\Table(name="entity")
* #Assert\Callback(methods={"validate"})
* #Vich\Uploadable
*/
class Entity
{
/**
* #Assert\File(maxSize="10M")
* #Vich\UploadableField(mapping="files", fileNameProperty="fileName")
*
* #var File $file
*/
protected $file;
/**
* #ORM\Column(type="string", length=255, name="file_name", nullable=true)
*
* #var string $fileName
*/
protected $fileName;
...
/**
* #param ExecutionContextInterface $context
*/
public function validate(ExecutionContextInterface $context)
{
if (! in_array($this->file->getMimeType(), array(
'image/jpeg',
'image/gif',
'image/png',
'video/mp4',
'video/quicktime',
'video/avi',
))) {
$context
->buildViolation('Wrong file type (jpg,gif,png,mp4,mov,avi)')
->atPath('fileName')
->addViolation()
;
}
}
}
For Symfony 4.0 you'll need to import the Validator component
composer require validator
Now in your Entity class you can use the #Assert annotation.
// src/Entity/Author.php
// ...
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* #Assert\NotBlank()
*/
public $name;
}
You might need to add some configuration in your config/packages/framework.yaml file. Anyway, all this is perfectly explained on the official Symfony documentation.
http://symfony.com/doc/current/validation.html
To check the mime type you'll need to use the File constraint http://symfony.com/doc/current/reference/constraints/File.html
Here is a working exemple
/**
* #ORM\Column(type="string", length=255)
* #var string
*/
private $cvFilename;
/**
* #Assert\File(
* maxSize = "2048k",
* mimeTypes = {"application/pdf", "application/x-pdf"},
* mimeTypesMessage = "Please upload a valid PDF"
* )
* #Vich\UploadableField(mapping="cv", fileNameProperty="cvFilename")
* #var File
*/
private $cvFile;
Now it's true that there is a mime and size option inside the #Vich\UploadableField anotation as described here https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/usage.md#step-2-link-the-upload-mapping-to-an-entity
but I couldn't get this to work.
The #Assert annotation will generate Forms errors, that you can retrieve them in Twig to give a feedback.
The key is to use : form_errors(candidature_form.cvFile)
here is a working example :
{% set error_flag = form_errors(candidature_form.cvFile) %}
<label class=" {% if error_flag %}has-error{% endif %}">
Curriculum Vitae (PDF)
</label>
{{ form_widget(candidature_form.cvFile) }}
{% if error_flag %}
<div class="has-error">
{{ form_errors(candidature_form.cvFile) }}
</div>
{% endif %}
For Symfony 4.x, this solution not working. i don't know why assert or event validator never call ...
I've find this solution : Validation doesn't work on relation fields
use Symfony\Component\Validator\Constraints\File;
/* ... */
->add('ba_file', VichFileType::class, [
'label' => 'Bon d\'adhésion (PDF file)',
'required' => false,
'constraints' => [
new File([
'maxSize' => '5M',
'mimeTypes' => [
'image/jpeg',
'image/gif',
'image/png',
]
])
]
])
For Symfony 3.0+, only 2 things need to be done:
Add use statement to import ExecutionContextInterface.
The callback annotation has to be added directly to the method/function instead of the class.
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* #Assert\File(maxSize="2M")
* #Vich\UploadableField(mapping="profile_image", fileNameProperty="avatar")
* #var File
*/
private $imageFile;
/**
* #ORM\Column(length=255, nullable=true)
* #var string $avatar
*/
protected $avatar;
/**
* #Assert\Callback
* #param ExecutionContextInterface $context
*/
public function validate(ExecutionContextInterface $context, $payload)
{
// do your own validation
if (! in_array($this->imageFile->getMimeType(), array(
'image/jpeg',
'image/gif',
'image/png'
))) {
$context
->buildViolation('Wrong file type (only jpg,gif,png allowed)')
->atPath('imageFile')
->addViolation();
}
}
Symfony 6 - PHP 8:
Entity class:
#[Vich\UploadableField(mapping: 'avatars', fileNameProperty: 'avatarName')]
#[Assert\File(maxSize: '1024k', mimeTypes: ['image/jpeg', 'image/png'])]
private ?File $avatarFile = null;
FormType class:
'attr' => [
'accept' => 'image/jpeg, image/png',
],
'constraints' => [
new Assert\File([
'maxSize' => '1024k',
'mimeTypes' => ['image/jpeg', 'image/png'],
]),
],

Collection Prototype and Doctrine Persistance ManyToOne Relation

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!

Resources