FosRest : Date change is impossible with put method - symfony

I'm working on a PUT method which allows user to update his profile. My problem is when I try to update the birthDate field, in the user entity:
/**
* #var \DateTime
*
* #ORM\Column(name="birth_date", type="date", nullable=true)
*
* #Expose
*/
private $birthDate;
With getters and setters:
/**
* Set birthDate
*
* #param \DateTime $birthDate
* #return User
*/
public function setBirthDate(\DateTime $birthDate)
{
$this->birthDate = $birthDate;
return $this;
}
/**
* Get birthDate
*
* #return \DateTime
*/
public function getBirthDate()
{
return $this->birthDate;
}
Here is my controller code sample:
$user = $this->getDoctrine()->getRepository('FTUserBundle:User')->find($user_id);
if(null == $user) {
$view = View::create("User not found", Codes::HTTP_NOT_FOUND);
return $this->handleView($view);
}
$form = $this->createForm(new UserProfileType(), $user, array('method' => 'PUT'));
The error is right here, when createForm is called:
{"error":{"code":500,"message":"Internal Server Error","exception":[{"message":"Unable to transform value for property path \"birthDate\": datefmt_format: string '' is not numeric, which would be required for it to be a valid date: U_ILLEGAL_ARGUMENT_ERROR"
Here's my UserProfileType:
$builder->add('birthDate', 'date', array(
'widget' => 'single_text',
'format' => 'dd-MM-yyyy'
));
The birthDate is correctly set in the database. If I replace the return of the birthDate getter by a static DateTime object, it works fine. When I call the $user->getBirthDate(), before building the form, it returns me a correct DateTime object.

My mistake was in my UserProfileType(). The birthDate field type must be "datetime", and not "date".

Related

Symfony formbuilder DateTime shows wrong date

I want to set the default value of date as current date, but I'm getting 2013.01.01 instead of current date.
$builder->add(
'date',
DateTimeType::class,
[
'input' => 'datetime',
'empty_data'=> new \DateTime(),
'date_format' => 'y-M-d H:i',
'required' => true,])
How to set default date value as the current date when I submit the form? and if I want to edit the form, set that date to the existing value in my entity?
Try to construct new \DateTime() in your entity itself. Since you are not manually entering the current date value, you don't need to add the 'date' field in your form builder.
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="datetime", nullable=false)
*/
private $date;
/**
* YourEntity constructor.
*
*/
public function __construct()
{
$this->setdate(new \DateTime());
}
/**
* Set date
*
* #param \DateTime $date
*
* #return YourEntity
*/
public function setdate($date)
{
$this->date = $date;
return $this;
}

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!

Symfony2: do not update a form field if not provided

I have a form for my "Team" entity. This entity has an "image" field. This field is required on creation process, but not required on edit process. But right now, during edit process, if I don't provide any image in the file input, the empty input is still persisted, and so my database field is emptied during the process.
How can I do to avoid persistence of this field, if nothing is provided in the form file input? So the entity keeps its old value for this field. Of course, if a file is provided, I want him to erase the old one.
My controller looks like this:
if ($request->getMethod() == 'POST') {
$form->bind($request);
if ($form->isValid()) {
$em->persist($team);
$em->flush();
...
}
}
and part of my entity, dealing with image (I'm pretty sure I have to do something in here, but don't know what exactly):
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function uploadImage() {
// the file property can be empty if the field is not required
if (null === $this->image) {
return;
}
if(!$this->id){
$this->image->move($this->getTmpUploadRootDir(), $this->image->getClientOriginalName());
}else{
$this->image->move($this->getUploadRootDir(), $this->image->getClientOriginalName());
}
$this->setImage($this->image->getClientOriginalName());
}
EDIT
Ok, I did some changes to this answer's code code, because apparently the event listener asks for a FormEvent instance in his callback, not a FormInterface instance.
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
// Retrieve submitted data
$form = $event->getForm();
$item = $event->getData();
// Test if upload image is null (maybe adapt it to work with your code)
if (null !== $form->get('image')->getData()) {
var_dump($form->get('image')->getData());
die('image provided');
$item->setImage($form->get('image')->getData());
}
});
When I provide an image, the script goes into the test, and die(), as expected. When I do not provide any file, the script doesn't go into the test if(), but my field in the database is still erased with an empty value. Any idea?
And as asked below, here is the Form
// src/Van/TeamsBundle/Form/TeamEditType.php
namespace Van\TeamsBundle\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TeamEditType extends TeamType // Ici, on hérite de ArticleType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// On fait appel à la méthode buildForm du parent, qui va ajouter tous les champs à $builder
parent::buildForm($builder, $options);
// On supprime celui qu'on ne veut pas dans le formulaire de modification
$builder->remove('image')
->add('image', 'file', array(
'data_class' => null,
'required' => false
))
;
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
// Retrieve submitted data
$form = $event->getForm();
$item = $event->getData();
// Test if upload image is null (maybe adapt it to work with your code)
if (null !== $form->get('image')->getData()) {
var_dump($form->get('image')->getData());
die('image provided');
$item->setImage($form->get('image')->getData());
}
});
}
// On modifie cette méthode car les deux formulaires doivent avoir un nom différent
public function getName()
{
return 'van_teamsbundle_teamedittype';
}
}
and the whole Team entity:
<?php
namespace Van\TeamsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Team
*
* #ORM\Table()
* #ORM\HasLifecycleCallbacks
* #ORM\Entity
* #ORM\Entity(repositoryClass="Van\TeamsBundle\Entity\TeamRepository") #ORM\Table(name="van_teams")
*/
class Team
{
/**
* #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=100)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="countryCode", type="string", length=2)
*/
private $countryCode;
/**
* #ORM\ManyToOne(targetEntity="Van\TeamsBundle\Entity\Game")
* #ORM\JoinColumn(nullable=false)
*/
private $game;
/**
* #ORM\ManyToOne(targetEntity="Van\TeamsBundle\Entity\Statut")
* #ORM\JoinColumn(nullable=false)
*/
private $statut;
/**
* #var string $image
* #Assert\File( maxSize = "1024k", mimeTypesMessage = "Please upload a valid Image")
* #ORM\Column(name="image", type="string", length=255)
*/
private $image;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Team
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set countryCode
*
* #param string $countryCode
* #return Team
*/
public function setCountryCode($countryCode)
{
$this->countryCode = $countryCode;
return $this;
}
/**
* Get countryCode
*
* #return string
*/
public function getCountryCode()
{
return $this->countryCode;
}
/**
* Set image
*
* #param string $image
* #return Team
*/
public function setImage($image)
{
$this->image = $image;
return $this;
}
/**
* Get image
*
* #return string
*/
public function getImage()
{
return $this->image;
}
/**
* Set game
*
* #param \Van\TeamsBundle\Entity\Game $game
* #return Team
*/
public function setGame(\Van\TeamsBundle\Entity\Game $game)
{
$this->game = $game;
return $this;
}
/**
* Get game
*
* #return \Van\TeamsBundle\Entity\Game
*/
public function getGame()
{
return $this->game;
}
/**
* Set statut
*
* #param \Van\TeamsBundle\Entity\Statut $statut
* #return Team
*/
public function setStatut(\Van\TeamsBundle\Entity\Statut $statut)
{
$this->statut = $statut;
return $this;
}
/**
* Get statut
*
* #return \Van\TeamsBundle\Entity\Statut
*/
public function getStatut()
{
return $this->statut;
}
public function getFullImagePath() {
return null === $this->image ? null : $this->getUploadRootDir(). $this->image;
}
protected function getUploadRootDir() {
// the absolute directory path where uploaded documents should be saved
// return $this->getTmpUploadRootDir();
return __DIR__ . '/../../../../web/uploads/';
}
protected function getTmpUploadRootDir() {
// the absolute directory path where uploaded documents should be saved
return __DIR__ . '/../../../../web/uploads_tmp/';
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function uploadImage() {
// the file property can be empty if the field is not required
if (null === $this->image) {
return;
}
if(!$this->id){
$this->image->move($this->getTmpUploadRootDir(), $this->image->getClientOriginalName());
}else{
$this->image->move($this->getUploadRootDir(), $this->image->getClientOriginalName());
}
$this->setImage($this->image->getClientOriginalName());
}
/**
* #ORM\PostPersist()
*/
public function moveImage()
{
if (null === $this->image) {
return;
}
if(!is_dir($this->getUploadRootDir())){
mkdir($this->getUploadRootDir());
}
copy($this->getTmpUploadRootDir().$this->image, $this->getFullImagePath());
unlink($this->getTmpUploadRootDir().$this->image);
}
/**
* #ORM\PreRemove()
*/
public function removeImage()
{
unlink($this->getFullImagePath());
rmdir($this->getUploadRootDir());
}
}
EDIT 2
I did that. When I provide an image, it is saved in the image field in the database and redirection to my index page occurs. When I don't provide any image, redirection doesn't occur, and the following message appears above my file input in my form: "The file could not be found." In my TeamEditType class, I did the following, so the image should not be required.
$builder->remove('image')
->add('image', 'file', array(
'data_class' => null,
'required' => false
))
;
As of Symfony 2.3, you can simply use the PATCH http method, as documented here.
$form = $this->createForm(FooType::class, $foo, array(
'action' => $this->generateUrl('foo_update', array('id' => $foo->getId())),
'method' => 'PATCH',
));
It's an easy way to make a partial update of an entity using your main form, without rendering all the fields.
One approach in Symfony 2.4 (further information in Symfony2 coockbook) :
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder->add() ...
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormInterface $form) {
// Retrieve submitted data
$form = $event->getForm();
$image = $form->getData();
// Test if upload image is null (maybe adapt it to work with your code)
if (null !== $form->get('uploadImage')->getData()) {
$image->setUploadImage($form->get('uploadImage')->getData());
}
});
}
Edit
It seems that you already test your prepersit data. Try the following :
public function setImage($image)
{
if($image !== null) {
$this->image = $image;
return $this;
}
}
I'm using symfony 3.3 and faced the same issue, below is the solution how I overcome it.
You need to create a extra property for image uploading in your entity other than image property, something like $file;
Product.php
/**
* #var string
*
* #ORM\Column(name="image", type="string")
*
*/
private $image;
/**
*
* #Assert\File(mimeTypes={ "image/jpeg", "image/jpg", "image/png" })
*/
private $file;
public function setFile($file)
{
$this->file = $file;
return $this;
}
public function getFile()
{
return $this->file;
}
// other code i.e image setter getter
...
ProductType.php
$builder->add('file', FileType::class, array(
'data_class' => null,
'required'=>false,
'label' => 'Upload Image (jpg, jpeg, png file)')
);
Form.html.twig
<div class="form-group">
{{ form_label(form.file) }}
{{ form_widget(form.file, {'attr': {'class': 'form-control'}}) }}
</div>
Finally ProductController.php
...
if ($form->isSubmitted() && $form->isValid()) {
$file = $item->getFile();
if($file instanceof UploadedFile) {
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move(
$this->getParameter('upload_directory'),
$fileName
);
$item->setImage($fileName);
}
...
}
...
Little more about upload_directory
app/config/config.html
parameters:
upload_directory: '%kernel.project_dir%/web/uploads'
Create a uploads directory in web dir.
I resolved this issue by using PHP Reflection API. My approach was to browse the attributes of the entity class and replace the values not provided in the query with those already saved.
/**
* #param \ReflectionClass $reflectionClass
* #param $entity
* #param Request $request
* #return Request
*/
public function formatRequest($class, $entity, Request $request){
$reflectionClass = new \ReflectionClass('AppBundle\Entity\\'.$class);
foreach ($reflectionClass->getProperties() as $attribut)
{
$attribut->setAccessible(true); // to avoid fatal error when trying to access a non-public attribute
if($request->request->get($attribut->getName()) == null) { // if the attribute value is not provided in the request
$request->request->set($attribut->getName(), $attribut->getValue($entity));
}
}
return $request;
}
And then I use it like this :
$request = $this->formatRequest("EntityName", $entity, $request);
This is really generic.

Sonata User Admin - Custom field dependency

I have extended the SonataAdmin class for FOSUser and added 2 custom fields (choice type from external data source): Company and Sector
I'd like to make Sector dependent on Company, so if the user selects a Company it filters the available Sectors.
I though about using FormEvents for filtering at page load, but I don't even know how to get the Company value of the current form.
Here is a part of my custom SectorType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA
, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
// Need to get the company value here if set
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'choices' => $this->getSectors(),
));
}
public function getSectors()
{
$sects = array();
// Need to pass the selected company value to my getList
// (which gets the list of sector as you can imagine)
if (($tmp_sects = $this->ssrs->getList('Sector'))) {
foreach ($tmp_sects as $sect) {
$label = $sect['id'] ? $sect['label'] : '';
$sects[$sect['id']] = $label;
}
}
return $sects;
}
So the question is:
How to get the selected Company from my custom SectorType ?
After that I'll need to be able to refresh the Sector with Ajax, but that will be another question
I had a similar problem. I needed to create a sale entity that needed to be associated in a many to one relationship with an enterprise entity and a many to many relationship with services entities. Here is the Sale Entity:
The thing is that services where available depending on the companies chosen. For instance services a and b could only be provided to company x. And services b and c could only be provided to company y. So in my admin, depending on the chosen company I had to display the available services. For these I needed to do 2 things:
First create a dynamic form with my sale admin, so that on the server side I could get the right services available for the company specified in my sale record. And second, I had to create a custom form type for my company form element, so that when it was changed by the user on the client side, It would send an ajax request to get the right services for the company chosen.
For my first problem, I did something similar to what you were trying to achieve, but instead of creating an specific custom type for my services element, I added de event listener directly in the admin.
Here is the Sale entity:
/**
*
* #ORM\Table(name="sales")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Sale
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
public $id;
/**
* #ORM\ManyToOne(targetEntity="Branch")
* #ORM\JoinColumn(name="branch_id", referencedColumnName="id", nullable = false)
* #Assert\NotBlank(message = "Debe especificar una empresa a la cual asignar el precio de este exámen!")
*/
private $branch;
/** Unidirectional many to many
* #ORM\ManyToMany(targetEntity="Service")
* #ORM\JoinTable(name="sales_services",
* joinColumns={#ORM\JoinColumn(name="sale_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="service_id", referencedColumnName="id")}
* )
* #Assert\Count(min = "1", minMessage = "Debe especificar al menos un servicio a realizar!")
*/
private $services;
public function __construct() {
$this->services = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set branch
*
* #param Astricom\NeurocienciasBundle\Entity\Branch $branch
*/
//default value always have to be null, because when validation constraint is set to notblank,
//if default is not null, before calling the validation constraint an error will pop up explaining
//that no instance of Branch was passed to the $branch argument.
public function setBranch(\Astricom\NeurocienciasBundle\Entity\Branch $branch = null)
{
$this->branch = $branch;
}
/**
* Get branch
*
* #return Astricom\NeurocienciasBundle\Entity\Branch
*/
public function getBranch()
{
return $this->branch;
}
/**
* Add service
*
* #param \Astricom\NeurocienciasBundle\Entity\Service|null $service
*/
public function addServices(\Astricom\NeurocienciasBundle\Entity\Service $service = null)
{
$this->services[] = $service;
}
/**
* Get services
*
* #return Doctrine\Common\Collections\Collection
*/
public function getServices()
{
return $this->services;
}
/**
* Sets the creation date
*
* #param \DateTime|null $createdAt
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
}
/**
* Returns the creation date
*
* #return \DateTime|null
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Sets the last update date
*
* #param \DateTime|null $updatedAt
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
}
So then in the Admin form builder:
protected function configureFormFields(FormMapper $formMapper) {
$em = $this->container->get('doctrine')->getEntityManager();
$branchQuery = $em->createQueryBuilder();
$branchQuery->add('select', 'b')
->add('from', 'Astricom\NeurocienciasBundle\Entity\Branch b')
->add('orderBy', 'b.name ASC');
$formMapper
->with('Empresa/Sucursal')
->add('branch','shtumi_ajax_entity_type',array('required' => true, 'label'=>'Empresa/Sucursal','error_bubbling' => true, 'empty_value' => 'Seleccione una empresa/sucursal', 'empty_data' => null, 'entity_alias'=>'sale_branch', 'attr'=>array('add_new'=>false), 'model_manager' => $this->getModelManager(), 'class'=>'Astricom\NeurocienciasBundle\Entity\Branch', 'query' => $branchQuery))
->end()
;
$builder = $formMapper->getFormBuilder();
$factory = $builder->getFormFactory();
$sale = $this->getSubject();
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function(DataEvent $event) use ($sale,$factory, $em) {
$form = $event->getForm();
$servicesQuery = $em->createQueryBuilder();
$servicesQuery->add('select','s')
->add('from','Astricom\NeurocienciasBundle\Entity\Service s');
if (!$sale || !$sale->getId()) {
$servicesQuery
->where($servicesQuery->expr()->eq('s.id', ':id'))
->setParameter('id', 0);
}
else {
$servicesQuery
->join('s.branch', 'b')
->where($servicesQuery->expr()->eq('b.id', ':id'))
->setParameter('id', $sale->getBranch()->getId());
}
$form->add($factory->createNamed('services','entity',null,array('required' => true, 'label'=>'Servicios','error_bubbling' => true, 'attr'=>array('show_value_label'=>true),'class'=>'Astricom\NeurocienciasBundle\Entity\Service','multiple'=>true,'expanded'=>true,'query_builder'=>$servicesQuery)));
}
);
}
The trick thing was to pass the forms data. It doesn't work to use evet->getData() in the event listener's function. Instead I passed it through the admin->getSubject() method. Then instead of adding a sonataadmin form type, inside the event listener's function, I had to use a plain symfony form type.
The Ajax part as you mentioned is another question. All the weird things on the branch add method in the form builder is related to a customized field type for this matter. Don't worry about it.

Symfony 2: ManyToMany Relation and Unique Objects

I'm learning SF2 - really impressed with the job done, faced my first real issue I can't solve myself.
I have two entities: Post and Tag. Shortened code below:
class Tag
{
/**
* #ORM\ManyToMany(targetEntity="Post", mappedBy="tags", cascade={"persist"})
*/
private $posts;
public function __construct()
{
$this->posts = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param \My\AppBundle\Entity\Snippet $posts
* #return Tag
*/
public function addSnippet(\My\AppBundle\Entity\Post $posts)
{
$this->posts[] = $posts;
return $this;
}
/**
* #param \My\AppBundle\Entity\Snippet $snippets
*/
public function removeSnippet(\My\AppBundle\Entity\Post $posts)
{
$this->posts->removeElement($posts);
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getSnippets()
{
return $this->posts;
}
}
class Post
{
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="posts", cascade={"persist"})
* #ORM\JoinTable(name="posts_tags",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id", unique=true, onDelete="cascade")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id", unique=true, onDelete="cascade")}
* )
*/
private $tags;
public function __construct()
{
$this->tags = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param \My\AppBundle\Entity\Tag $tags
* #return Snippet
*/
public function addTag(\My\AppBundle\Entity\Tag $tags)
{
$this->tags[] = $tags;
return $this;
}
/**
* #param \My\AppBundle\Entity\Tag $tags
*/
public function removeTag(\My\AppBundle\Entity\Tag $tags)
{
$this->tags->removeElement($tags);
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
As you can see I have M:M relation between two entities.
I have also a form to add Post with embedded Tag collection:
$builder
->add('title')
->add('tags', 'collection', array(
'type' => new \My\AppBundle\Form\TagType(),
'allow_add' => true,
'by_reference' => false,
'prototype' => true
))
;
TagType form class:
$builder->add('name');
Everything works as expected. Except one thing: if there's a Tag object with the following name, I'm getting SQLSTATE[23000]: Integrity constraint violation MySQL error which is obvious. If I apply unique validation constraint I can add a tag to post (if it already exists in database).
It's obvious I need to check if following tag does exist in database and add it only if does not, but... how to do it Symfony way?
Any suggestions appreciated!
You can use UniqueEntity to handle this. I can't see your annotations on your tags class, or your declaration of 'name' but if you add something like the below it should give you a unique validation constraint based on name with an optional message to throw back.
/**
* #ORM\Entity
* #UniqueEntity(fields="name", message="This tag name already exists")
*/
class Tag...
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
protected $name;

Resources