Multiple objects in one Fluid form - typo3

I need to put two objects in one form with fluid.
The background is: I have a userDomain and a ordering object. Both are not persistent. I want to send these two objects to an action without making one of them persistent. The userDomain object is handled by the form, the other is assigned to the view. If I add the ordering object to the arguments section in the Form, Typo3 throws me this error:
Oops, an error occurred!
Could not serialize Domain Object Whmcs\Registration\Domain\Model\Ordering. It is neither an Entity with identity properties set, nor a Value Object.
More information regarding this error might be available online.
This is because the object is non-persistent and does not have any ID.
So the question is, how I do pass these two objects to the next action?

In order to pass non persistent objects between multiple actions, I would suggest that you store the objects serialized in a TYPO3 session variable. Doing so, you can restore the objects in the target action.
Below you find a working example which can also be found here.
<?php
namespace derhansen\ValidationExamplesNew\Controller;
/***************************************************************
* Copyright notice
*
* (c) 2013 Torben Hansen <derhansen#gmail.com>
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Multiple Steps Controller
*
* #package validation_examples_new
* #license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3 or later
*
*/
class MultipleStepsController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
/**
* addressdataRepository
*
* #var \derhansen\ValidationExamplesNew\Domain\Repository\AddressdataRepository
* #inject
*/
protected $addressdataRepository;
/**
* API Service
*
* #var \derhansen\ValidationExamplesNew\Service\ExternalApiService
* #inject
*/
protected $apiService;
/**
* Step1
*
* #param \derhansen\ValidationExamplesNew\Domain\Model\Step1Data $step1data
* #dontvalidate $step1data
*/
public function step1Action(\derhansen\ValidationExamplesNew\Domain\Model\Step1Data $step1data = NULL) {
/* Check if step1data is available in session */
if ($GLOBALS['TSFE']->fe_user->getKey('ses', 'step1data') && $step1data == NULL) {
$step1data = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', 'step1data'));
}
$this->view->assign('step1data', $step1data);
}
/**
* Step1 redirect action
*
* #param \derhansen\ValidationExamplesNew\Domain\Model\Step1Data $step1data
*/
public function step1redirectAction(\derhansen\ValidationExamplesNew\Domain\Model\Step1Data $step1data) {
$GLOBALS['TSFE']->fe_user->setKey('ses', 'step1data', serialize($step1data));
$GLOBALS['TSFE']->fe_user->storeSessionData();
$this->redirect('step2');
}
/**
* Step2
*
* #param \derhansen\ValidationExamplesNew\Domain\Model\Step2Data $step2data
* #dontvalidate $step2data
*/
public function step2Action(\derhansen\ValidationExamplesNew\Domain\Model\Step2Data $step2data = NULL) {
/* Check if step2data is available in session */
if ($GLOBALS['TSFE']->fe_user->getKey('ses', 'step2data') && $step2data == NULL) {
$step2data = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', 'step2data'));
}
/* Set external validations errors if available */
$this->setApiValidationErrors('step2');
$this->view->assign('step2data', $step2data);
}
/**
* Step2 redirect action
*
* #param \derhansen\ValidationExamplesNew\Domain\Model\Step2Data $step2data
*/
public function step2redirectAction(\derhansen\ValidationExamplesNew\Domain\Model\Step2Data $step2data) {
$GLOBALS['TSFE']->fe_user->setKey('ses', 'step2data', serialize($step2data));
$GLOBALS['TSFE']->fe_user->storeSessionData();
$this->redirect('step3');
}
/**
* Step3
*
* #param \derhansen\ValidationExamplesNew\Domain\Model\Step3Data $step3data
* #dontvalidate $step3data
*/
public function step3Action(\derhansen\ValidationExamplesNew\Domain\Model\Step3Data $step3data = NULL) {
/* Check if step3data is available in session */
if ($GLOBALS['TSFE']->fe_user->getKey('ses', 'step3data') && $step3data == NULL) {
$step3data = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', 'step3data'));
}
/* Set external validations errors if available */
$this->setApiValidationErrors('step3');
$this->view->assign('step3data', $step3data);
}
/**
* Step3 redirect action
*
* #param \derhansen\ValidationExamplesNew\Domain\Model\Step3Data $step3data
*/
public function step3redirectAction(\derhansen\ValidationExamplesNew\Domain\Model\Step3Data $step3data) {
$GLOBALS['TSFE']->fe_user->setKey('ses', 'step3data', serialize($step3data));
$GLOBALS['TSFE']->fe_user->storeSessionData();
$this->redirect('create');
}
/**
* Create Action
*
* #return void
*/
public function createAction() {
$addressdata = $this->getAddressdataFromSession();
/* get validation results from API */
$apiresults = $this->apiService->validateMultipleSteps($addressdata);
if (count($apiresults) > 0) {
/* Save results to a session variable */
$GLOBALS['TSFE']->fe_user->setKey('ses', 'apiresults', $apiresults);
$GLOBALS['TSFE']->fe_user->storeSessionData();
/* Redirect to step with validation errors */
if (array_key_exists('step2', $apiresults)) {
$this->redirect('step2');
}
if (array_key_exists('step3', $apiresults)) {
$this->redirect('step3');
}
}
$this->addressdataRepository->add($addressdata);
$this->cleanUpSessionData();
$this->view->assign('message', 'Addressdata has been created');
}
/**
* Collects the addressdata from the multiple steps form stored in session variables
* and returns an addressdata object.
*
* #return \derhansen\ValidationExamplesNew\Domain\Model\Addressdata
*/
protected function getAddressdataFromSession() {
/** #var \derhansen\ValidationExamplesNew\Domain\Model\Step1Data $step1data */
$step1data = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', 'step1data'));
/** #var \derhansen\ValidationExamplesNew\Domain\Model\Step2Data $step2data */
$step2data = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', 'step2data'));
/** #var \derhansen\ValidationExamplesNew\Domain\Model\Step3Data $step3data */
$step3data = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', 'step3data'));
/** #var \derhansen\ValidationExamplesNew\Domain\Model\Addressdata $addressData */
$addressData = $this->objectManager->get('derhansen\ValidationExamplesNew\Domain\Model\Addressdata');
$addressData->setFirstname($step1data->getFirstname());
$addressData->setLastname($step1data->getLastname());
$addressData->setStreet($step2data->getStreet());
$addressData->setStreetnr($step2data->getStreetnr());
$addressData->setZip($step3data->getZip());
$addressData->setCity($step3data->getCity());
return $addressData;
}
/**
* Removes all session variables from the multiple steps form
*
* #return void
*/
protected function cleanUpSessionData() {
$GLOBALS['TSFE']->fe_user->setKey('ses', 'step1data', '');
$GLOBALS['TSFE']->fe_user->setKey('ses', 'step2data', '');
$GLOBALS['TSFE']->fe_user->setKey('ses', 'step3data', '');
$GLOBALS['TSFE']->fe_user->setKey('ses', 'apiresults', '');
$GLOBALS['TSFE']->fe_user->storeSessionData();
}
/**
* Sets validation errors for fields in the given step
*
* #param string $step The step
* #return void
*/
protected function setApiValidationErrors($step) {
$apiresults = $GLOBALS['TSFE']->fe_user->getKey('ses', 'apiresults');
if (array_key_exists($step, $apiresults)) {
/* Set Form Errors manually - get results from property mapper and add new errors */
$result = $this->getControllerContext()->getRequest()->getOriginalRequestMappingResults();
/* Add validation errors */
foreach ($apiresults[$step] as $key => $value) {
$error = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\Error',
$apiresults[$step][$key], time());
$result->forProperty($step . 'data.' . $key)->addError($error);
}
$this->getControllerContext()->getRequest()->setOriginalRequestMappingResults($result);
}
}
}
?>
This example is part of a multiple step form validation example, but uses TYPO3 session variables in order to store the input of each form step.

Related

Put request entity linked to some others entity symfony

i'm stucked on something: i have to entity Profile and Job
in the Job entity i have :
/**
* #var string
* #Gedmo\Translatable
* #ORM\Column(name="label", type="string", length=255)
* #Serializer\Groups({"profile_details","profile_put"})
* #Serializer\Type("string")
* #Serializer\Expose()
*/
private $label;
in the Profile entity i have :
* #var Job
*
* #Assert\NotBlank(message="app.profile.job.not_blank")
* #ORM\ManyToOne(targetEntity="App\Entity\Job")
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
* #Serializer\Groups({"profile_details","user_details","corporate_details","profile_put"})
* #Serializer\Type("App\Entity\Job")
* #Serializer\Expose()
*/
private $job;
/**
* #return Job
*/
public function getJob(): Job
{
return $this->job;
}
/**
* #param Job $job
*
* #return Profile
*/
public function setJob(Job $job): Profile
{
$this->job = $job;
return $this;
}
EDIT:
in the controller :
public function putAction(Request $request, Profile $profile): Profile
{
$profile
->setJob($request->get('job'))
$errors = $this->validator->validate($profile);
if (count($errors) > 0) {
throw new UnprocessableEntityHttpException((string) $errors);
}
$this->entityManager->persist($profile);
$this->entityManager->flush();
return $profile;
}
I'm trying to perform a put on the Profile entity to change some normal string fields. I don't know how to set the put request body on postman, i tried to put just the label but i got : "Argument 1 passed to App\\Entity\\Profile::setJob() must be an instance of App\\Entity\\Job, array given, called in /srv/api/src/Controller/ProfileController.php on line 114"

Handle filesUpload in a single table Symfony

Thank you for your interest,
SHORT
I want to manage all my uploads (Image, PDF, Video etc...) in a single entity, so I use entity Inheritance to get various "types" and OneToOne relations to link parent entity with correct upload. I didn't found any bundle to do this and face problems:
Constraints use
Setting uploaded file and not upload entity
Get uploaded file and not upload entity (edition)
LONG
Instead of having 1 file management in each table (which is quiet verbose) I preferred to have only one table Uploads to handle every Uploads. Then I just have to do OneToOne relations to get my file, plus using inheritance I can apply various treatment depending on Image or PDF for example.
I have at least 4 entities that needs image, so I think that 1to1 relation is a good choice.
But I face problems doing things like this :
Constraints aren't taking into account
Edition of $file should set $file->file (it doesn't send the entity from Uploads/Image but the file to create this entity
The Uploaded file isn't loaded on entity edition and should be reuploaded each time I edit entity
Does anyone did this ? I can't find out how to achieve this correctly.
Looking at the assert problem I tried to:
Define asserts on Image (this doesn't work as expected as the form target the $file of WithImage)
Using annotation #Assert\Image()
Using loadValidatorMetadata
Using annotation #Assert\Callback()
Define assert on form field 'constraints' => array(new Assert\Image()), this works but need to be defined everywhere I use it...
Looking at the setter misused I found a workaround, but this is quiet ugly:
public function setFile($file = null)
{
if ($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
$tmpfile = new Image();
$tmpfile->setFile($file);
$file = $tmpfile;
}
$this->file = $file;
return $this;
}
(PS: I read about traits to avoid copy/paste of code, I have checked the SonataMediaBundle but this doesn't seems to apply to my case)
CODE
So I designed my classes as follow:
Entity\Uploads.php To handle all the life of a file from upload to remove (and access, move, edit, possibly thumbnail etc ...)
<?php
namespace Acme\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Acme\CoreBundle\Utils\UUID;
/**
* Uploads
*
* #ORM\Table(name="uploads")
* #ORM\Entity(repositoryClass="Acme\CoreBundle\Repository\UploadsRepository")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="class", type="string")
* #ORM\DiscriminatorMap({"image" = "Image"})
* #ORM\HasLifecycleCallbacks
*/
abstract class Uploads
{
protected $file;
private $tempFileName;
/**
* #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="fileName", type="string", length=36, unique=true)
*/
private $fileName; // UUID
/**
* #var string
*
* #ORM\Column(name="extension", type="string", length=4)
*/
private $extension;
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set date.
*
* #param \DateTime $date
*
* #return uploads
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Get date.
*
* #return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Set fileName.
*
* #param string $fileName
*
* #return uploads
*/
public function setFileName($fileName)
{
$this->fileName = $fileName;
return $this;
}
/**
* Get fileName.
*
* #return string
*/
public function getFileName()
{
return $this->fileName;
}
/**
* Set extension
*
* #param string $extension
*
* #return string
*/
public function setExtension($extension)
{
$this->extension = $extension;
return $this;
}
/**
* Get extension
*
* #return string
*/
public function getExtension()
{
return $this->extension;
}
public function getFileNameExt()
{
return $this->getFileName().'.'.$this->getExtension();
}
public function setFile(UploadedFile $file)
{
$this->file = $file;
if (null !== $this->getId()) {
$this->tempFileName = $this->getFileNameExt();
$this->fileName = null;
$this->extension = null;
}
}
public function getFile()
{
return $this->file;
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null === $this->file) {
return;
}
$this->extension = $this->file->guessExtension();
$this->fileName = UUID::v4();
$this->preUpdateFile();
}
protected function preUpdateFile(){} // To define if specific treatment
/**
* #ORM\PrePersist()
*/
public function prePersistDate()
{
$this->date = new \DateTime();
return $this;
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
if (null !== $this->tempFileName) {
$oldFile = $this->getUploadRootDir().$this->tempFileName;
if (file_exists($oldFile)) {
unlink($oldFile);
}
}
$this->file = $this->file->move(
$this->getUploadRootDir(),
$this->getFileNameExt()
);
$this->postUpdateFile();
}
protected function postUpdateFile(){} // To define if specific treatment
/**
* #ORM\PreRemove()
*/
public function preRemoveUpload()
{
// On sauvegarde temporairement le nom du fichier
$this->tempFileName = $this->getFileNameExt();
$this->preRemoveFile();
}
protected function preRemoveFile(){} // To define if specific treatment
/**
* #ORM\PostRemove()
*/
public function removeUpload()
{
$oldFile = $this->getUploadRootDir().$this->tempFileName;
if (file_exists($oldFile)) {
unlink($oldFile);
}
$this->postRemoveFile();
}
protected function postRemoveFile(){} // To define if specific treatment
public function getFileUri()
{
return $this->getUploadDir().$this->getFileNameExt();
}
public function getUploadDir()
{
return 'uploads/';
}
protected function getUploadRootDir()
{
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
public function __toString() {
return $this->getFileNameExt();
}
}
Entity\Image.php A specific type of upload with its own constraints and file management
<?php
namespace Acme\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Image
*
* #ORM\Entity(repositoryClass="Acme\CoreBundle\Repository\ImageRepository")
*/
class Image extends Uploads
{
}
Entity\WithImage.php An entity which needs an Image
<?php
namespace Acme\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* WithImage
*
* #ORM\Table(name="with_image")
* #ORM\Entity(repositoryClass="Acme\CoreBundle\Repository\WithImageRepository")
*/
class WithImage
{
/**
* #ORM\OneToOne(targetEntity="Acme\CoreBundle\Entity\Image", cascade={"persist", "remove"})
*/
protected $file;
}
Some thoughts come to my mind to help you achieve what you want.
First, you have to upload the files in a form, and the constraints should be in a property in an entity (unless you want to have the pain of writing your constraints in every form, which is not very mantainable). So, for every entity that's going to have files, define a file property (not ORM anotated) and write your constraints there. Also add the respective getters and setters.
/**
* #var UploadedFile
* #Assert\NotBlank(groups={"New"})
* #Assert\File(mimeTypes={"text/html", "text/markdown", "text/plain"})
*/
private $file;
Second, you might ask ¿but how do I save them to a different Entity? This is when I would recommend you to use a Doctrine Event Subscriber. Basically, is a service that is defined with a doctrine.event_subscriber tag which class implements the Doctrine\Common\EventSubscriber Interface. You can subscribe to events like preUpdate, postLoad, and the interesting for you: prePersist.
My take on this would be that you subscribe to the prePersist event. The event will pass you the entity (with that file non-orm property we created, that has the UploadedFile instance holding your file info).
Then, using the info for that file, create a new Upload entity, pass all the info you want, and then set that in the real file orm mapped property that holds your file relationship with your desired entity. For this to work you will have to enable persist cascade if I recall correctly.
The benefits of this:
1. You can define your constraints in your Entities.
2. You can have the Uploads Entity you desire.
The only major problem is that you will have to do all the retrieval, storing and updating of the Uploads entity through a listener. But's the only thing I can think of to help you.

error on feuser edit: is not integer

I extend feuser with custom fields. All work good. If I type integer value it was save good. But if I type string value I get error - 1332933658: "" is no integer. You can see this on the follow screenshots.
enter image description here
ext_tables.php
#
# Table structure for table 'fe_users'
#
CREATE TABLE fe_users (
aboutmyself varchar(255) DEFAULT '' NOT NULL,
aboutmypartner varchar(255) DEFAULT '' NOT NULL,
tx_extbase_type varchar(255) DEFAULT '0' NOT NULL,
);
Model:
<?php
namespace Fhk\Feusersplus\Domain\Model;
/***************************************************************
*
* Copyright notice
*
* (c) 2017
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* User
*/
class User extends \In2code\Femanager\Domain\Model\User
{
/**
* aboutmyself
*
* #var int
*/
protected $aboutmyself = '';
/**
* aboutmypartner
*
* #var int
*/
protected $aboutmypartner = '';
/**
* Returns the aboutmyself
*
* #return int $aboutmyself
*/
public function getAboutmyself()
{
return (string)$this->aboutmyself;
}
/**
* Returns the aboutmypartner
*
* #return int $aboutmypartner
*/
public function getAboutmypartner()
{
return (string)$this->aboutmypartner;
}
/**
* Sets the aboutmyself
*
* #return void
*/
public function setAboutmyself($aboutmyself)
{
$this->aboutmyself = (string)$aboutmyself;
}
/**
* Sets the aboutmypartner
*
* #return void
*/
public function setAboutmypartner($aboutmypartner)
{
$this->aboutmypartner = (string)$aboutmypartner;
}
/**
* __construct
*/
public function __construct()
{
//Do not remove the next line: It would break the functionality
$this->initStorageObjects();
}
/**
* Initializes all ObjectStorage properties
* Do not modify this method!
* It will be rewritten on each save in the extension builder
* You may modify the constructor of this class instead
*
* #return void
*/
protected function initStorageObjects()
{
}
}
The type hint for your fields aboutmyself and aboutmypartner is defined as
#var int
but you store a VARCHAR. Try changing the hint to
#var string
and clear all caches (also, emptying the typo3temp folder can't hurt).

How to implement multiple file upload in TYPO3 Front End Extension

My requirement is to implement a multiple fileupload field in TYPO3 Front-end Extension. Here is what I've used for a single file upload.
My Fields in Model
/**
* angebotuploaden
*
* #var \TYPO3\CMS\Extbase\Domain\Model\FileReference
*/
protected $angebotuploaden = NULL;
/**
* Returns the angebotuploaden
*
* #return \TYPO3\CMS\Extbase\Domain\Model\FileReference $angebotuploaden
*/
public function getAngebotuploaden() {
return $this->angebotuploaden;
}
/**
* Sets the angebotuploaden
*
* #param \TYPO3\CMS\Extbase\Domain\Model\FileReference $angebotuploaden
* #return void
*/
public function setAngebotuploaden(\TYPO3\CMS\Extbase\Domain\Model\FileReference $angebotuploaden) {
$this->angebotuploaden = $angebotuploaden;
}
Now I face issues in implementing multiple file-uploads for this field. Please help me to sort it out.
Use ObjectStorage to get an 1:n-Relation to the FileReference model. In your model that could look like this:
/**
* uploadFiles
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference>
* #cascade remove
*/
protected $uploadFiles = NULL;
/**
* __construct
*/
public function __construct() {
//Do not remove the next line: It would break the functionality
$this->initStorageObjects();
}
/**
* Initializes all ObjectStorage properties
* Do not modify this method!
* It will be rewritten on each save in the extension builder
* You may modify the constructor of this class instead
*
* #return void
*/
protected function initStorageObjects() {
$this->uploadFiles = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
}
/**
* Adds a UploadFile
*
* #param \TYPO3\CMS\Extbase\Domain\Model\FileReference $uploadFile
* #return void
*/
public function addUploadFile(\TYPO3\CMS\Extbase\Domain\Model\FileReference $uploadFile) {
$this->uploadFiles->attach($uploadFile);
}
/**
* Removes a UploadFile
*
* #param \TYPO3\CMS\Extbase\Domain\Model\FileReference $uploadFileToRemove The UploadFile to be removed
* #return void
*/
public function removeUploadFile(\TYPO3\CMS\Extbase\Domain\Model\FileReference $uploadFileToRemove) {
$this->uploadFiles->detach($uploadFileToRemove);
}
/**
* Returns the uploadFiles
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> $uploadFiles
*/
public function getUploadFiles() {
return $this->uploadFiles;
}
/**
* Sets the uploadFiles
*
* #param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> $uploadFiles
* #return void
*/
public function setUploadFiles(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $uploadFiles) {
$this->uploadFiles = $uploadFiles;
}
There're still more things to do, especially in TCA, but I don't know them in detail because I didn't use that yet. See Hemult Hummels Upload Example an this question for more detailed information.

Symfony2, field type entity, "many-to-many" relation with extra fields, problems with the relation/ArrayCollection/multipe checkboxes in form

I want to create a form for a simple entry management.
I got the entities Entry, EntryUser, and User.
Tables in database: entry, entry_user and user.
I think i am close to be successful, but i got some problems here.
In my form, the author can check the users he want to add to the entry via checkboxes. Symfony/Doctrine should do the relation work for me and add rows into entry_user. One row for one selected checkbox.
Entry.php
/**
* #ORM\Entity
* #ORM\Table(name="entry")
*/
class Entry {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="EntryUser", mappedBy="entry", cascade={"all"}, orphanRemoval=true)
*/
protected $users;
function __construct() {
$this->users = new ArrayCollection();
}
/**
* Add user
*
* #param User $user
* #return $this
*
*/
public function addUser(User $user)
{
if (!$this->users->contains($user)) {
$this->users->add($user);
}
return $this;
}
/**
* Remove user
*
* #param User $user
* #return $this
*/
public function removeUser(User $user)
{
if ($this->users->contains($user)) {
$this->users->removeElement($user);
}
return $this;
}
/**
* #return array
*/
public function getUsers()
{
return $this->users->toArray();
}
/**
* Returns the true ArrayCollection of Users.
* #return Doctrine\Common\Collections\ArrayCollection
*/
public function getUsersCollection()
{
return $this->users;
}
}
User.php
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User implements AdvancedUserInterface, EquatableInterface, \Serializable {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
*/
protected $name;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return User
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
// Many other methods for user sign in, roles and so on...
}
EntryUser.php
/**
* #ORM\Entity
* #ORM\Table(name="entry_user")
*/
class EntryUser {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var integer
*
* #ORM\Column(name="user_id", type="integer", nullable=true)
*/
protected $user_id;
/**
* #var integer
*
* #ORM\Column(name="entry_id", type="integer", nullable=true)
*/
protected $entry_id;
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $user;
/**
* #var Entry
*
* #ORM\ManyToOne(targetEntity="Entry", inversedBy="users")
* #ORM\JoinColumn(name="entry_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $entry;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set user_id
*
* #param integer $userId
* #return EntryUser
*/
public function setUserId($userId)
{
$this->user_id = $userId;
return $this;
}
/**
* Get user_id
*
* #return integer
*/
public function getUserId()
{
return $this->user_id;
}
/**
* Set entry_id
*
* #param integer $entryId
* #return EntryUser
*/
public function setEntryId($entryId)
{
$this->entry_id = $entryId;
return $this;
}
/**
* Get entry_id
*
* #return integer
*/
public function getEntryId()
{
return $this->entry_id;
}
/**
* Set user
*
* #param User $user
* #return EntryUser
*/
public function setUser(User $user = null)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* #return User
*/
public function getUser()
{
return $this->user;
}
/**
* Set entry
*
* #param Entry $entry
* #return EntryUser
*/
public function setEntry(Entry $entry = null)
{
$this->entry = $entry;
return $this;
}
/**
* Get entry
*
* #return Entry
*/
public function getEntry()
{
return $this->entry;
}
}
I use ManyToOne relationships here because i want to use a file named EntryUser.php to add some custom fields later. I need this because i must store some additional data there in entry_user.
Because:
And it's the right thing to do. Create a new entity with the new fields, and if you need it, create a custom repository to add the methods you need.
A <--- Many to many with field ---> B
would become
A --One to many--> C (with new fields) <-- One to many--B
and of course, C has ManyToOne relationships with both A and B.
See the comments in: ManyToMany relationship with extra fields in symfony2 orm doctrine
My EntryType.php form definition includes the following, to create the checkboxes for the template:
$builder->add('users', 'entity', array(
'class' => 'MyCompMyAppBundle:User',
'multiple' => true,
'expanded' => true,
'property' => 'name',
'label' => 'Freunde',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')->select('a')
->from('MyComp\MyAppBundle\Entity\User', 'a')
->where('EXISTS (
SELECT b
FROM MyComp\MyAppBundle\Entity\UserFriend b
WHERE b.created_by = :my_user_id
AND b.friend_user_id = a.id
)')
->andWhere('EXISTS (
SELECT c
FROM MyComp\MyAppBundle\Entity\UserFriend c
WHERE c.created_by = a.id
AND c.friend_user_id = :my_user_id
)')
->setParameter('my_user_id', $this->user->getId());
},
'required' => true,
));
As you can see, i load User objects here for the form field (type: entity).
UserFriend is another entity (table: user_friend). A friend list is saved there. Here all the friends gets loaded. They will be shows as the checkboxes.
Now, if i go to my form in my browser, and check some users for the entry und if i submit the form, i get this error:
ORMException: Found entity of type MyComp\MyAppBundle\Entity\User on association MyComp\MyAppBundle\Entity\Entry#friends, but expecting MyComp\MyAppBundle\Entity\EntryUser
So it is very confusing.
How can i make this work?
How can i make symfony and doctrine work to insert data automatically into entry and entry_user?
Important: I want to make it possible that the author can edit the entry later. So the checkboxes should be selected by default as it is saved in entry_user.
I just try to understand this and got a little bit confused. But i dont know how to make this work.
I found an awesome solution in the web. I was searching for many days now, but this guy made my day! :-)
You can read how it works and learn from an awesome example here!
Happy coding!
For this case you should check "form collections" in Symfony: http://symfony.com/doc/current/cookbook/form/form_collections.html
With this technique you will add a form type to crate a single "EntryUser" and after that you can add a collection of that form to the parent form. Quite easy and well explained in the liked article.