I have a document
/**
* #ODM\Document
*/
class Result
{
/**
* #var int $id
* #ODM\Id
*/
protected $id;
/**
* #var string $name
* #ODM\Field(type="string")
*/
protected $name;
/**
* #var UserComment[] $userComments
* #ODM\EmbedMany(targetDocument="UserComment")
*/
protected $userComments;
}
/** #ODM\EmbeddedDocument() */
class UserComment {
public $addedBy;
public $createdAt;
public $comment;
}
I want to create a form which allows me to add new user comments. But it would only have the $comment as TextAreaType. The other 2 fields should be added automatically.
I have used the collectiontype like this:
class ResultForm extends AbstractType
{
/**
* buildForm
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('userComments', CollectionType::class, [
'entry_type' => UserCommentType::class,
'required' => false,
'allow_add' => true,
'label' => false,
'delete_empty' => true,
'prototype' => true,
'entry_options' => [
'attr' => [
'class' => 'user-comment-widget'
],
'label' => false,
]
]);
$builder->add('submit', SubmitType::class);
}
And added also a UserCommentType:
class UserCommentType extends AbstractType
{
/**
* #var TokenStorage
*/
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* buildForm
* #param FormBuilderInterface $builder
* #param array $options
* #throws \Exception
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$now = new \DateTime('now');
$builder->add('comment', TextareaType::class, [
'label' => false,
])
->add('addedBy', HiddenType::class, [
'data' => $this->tokenStorage->getToken()->getUser()->getUsername(),
])
->add('createdAt', HiddenType::class, [
'data' => $now->format('Y-m-d H:i:s')
]);
}
After having a bunch of exception/errors etc I figured there must be an easier way to do this. This does not work as the UserCommentType has no access to the original data for some reason even though I use this to initialize the ResultForm:
$form = $this->createForm(ResultForm::class, $result);
To access the values inside a collection form, you have to use listeners. By adding an PRE_SET_DATA listener to the form, you could change those values.
Documentation
$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'));
Now the function called by the listener. This will iterate through each element in the collection, so you can add your code there.
public function onPreSetData(FormEvent $event) {
$entity = $event->getData();
$form = $event->getForm();
if ($entity) {
$form->add('comment', TextareaType::class, [
'label' => false,
])
->add('addedBy', HiddenType::class, [
'data' => $this->tokenStorage->getToken()->getUser()->getUsername(),
])
->add('createdAt', HiddenType::class, [
'data' => $now->format('Y-m-d H:i:s')
]);
}
}
Make sure to add the if clause, if required, as the first iteration will be to create the collection form prototype and you might don't want it to have presetted values.
Related
In a Symfony 4 application that I've been asked to work on I am attempting to enforce a uniqueness constraint on the name of my program (a course of instruction, not software) within a given company. Despite the attempted constraint, the app happily lets me create a program with the same name as one that already exists in the given company.
I've found various contradictory examples of how to set up a composite constraint, and I've read through the many StackOverflow questions on this topic to no avail.
The relevant code for my entity, program.php:
<?php
namespace Domain\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Domain\AdminBundle\Service\Helper\RouteListHelper;
use Domain\CoreBundle\Repository\ProgramRepository as ProgramRepo;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
use JsonSerializable;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* Program
* #ORM\Entity(repositoryClass="Domain\CoreBundle\Repository\ProgramRepository")
* #ORM\Table(name="programs")
* #UniqueEntity(
* fields={"name","company"},
* errorPath = "name",
* message="A program by that name already exists for this company."
* )
* #ORM\HasLifecycleCallbacks()
*/
class Program implements JsonSerializable
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #Assert\NotBlank(message="Program Name should not be empty")
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="Company")
* #ORM\JoinColumn(name="company_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
protected $company;
...
and my addProgramType.php:
<?php
namespace Domain\AdminBundle\Form;
use Domain\CoreBundle\Repository\UserRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
/**
* Class AddProgramType
*/
class AddProgramType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$params = array(
'name' => array(
'label' => 'Program name:',
'attr' => array('class' => 'base-box'),
),
'isEnabled' => array(
'label' => false,
'attr' => array(
'checked' => 'checked',
),
),
'isRoiCalculating' => array(
'label' => false,
),
'duration' => array(
'label' => 'Duration:',
'class' => 'DomainCoreBundle:Duration',
'query_builder' => function (EntityRepository $er) use ($options) {
return $er->getDurationsQb($options['company']);
},
'choice_label' => 'uniqueName',
'attr' => array(
'class' => 'base-box',
),
),
'sessionTypes' => array(
'class' => 'DomainCoreBundle:SessionType',
'query_builder' => function (EntityRepository $er) use($options) {
return $er->getAllSessionTypesQb($options['company']);
},
'choice_label' => 'name',
'multiple' => true,
'label' => 'Session Types:',
'attr' => array(
'class' => 'multiselect-dropdown multiselect-dropdown-session-types',
'required' => 'required',
'multiple' => 'multiple',
),
),
'users' => array(
'required' => false,
'class' => 'DomainCoreBundle:User',
'choices' => $options['userRepo']->findByRoles(
array(UserRepository::ROLE_ADMIN,UserRepository::ROLE_COMPANY_ADMIN),
$options['company'],
false),
'choice_label' => 'getFullName',
'multiple' => true,
'label' => 'Access to admins:',
'attr' => array(
'class' => 'multiselect-dropdown multiselect-dropdown-users',
'multiple' => 'multiple',
),
),
);
$builder
->add('name', null, $params['name'])
->add('isEnabled', CheckboxType::class, $params['isEnabled'])
->add('isRoiCalculating', CheckboxType::class, $params['isRoiCalculating'])
->add('duration', EntityType::class, $params['duration'])
->add('sessionTypes', EntityType::class, $params['sessionTypes'])
->add('users', EntityType::class, $params['users']);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'Domain\CoreBundle\Entity\Program'));
$resolver->setRequired(array('company', 'userRepo'));
}
/**
* Return form name
*
* #return string
*/
public function getBlockPrefix()
{
return 'add_program';
}
}
While the application enforces the NotBlank constraint on the name correctly, it doesn't enforce the uniqueness of name + company.
Any suggestions?
[UPDATE] Looks like I set company after the isValid() call, thanks BoShurik for the catch. Here's the relevant controller code showing my mistake:
/**
* Add new program
*
* #param Request $request
*
* #return Response
*/
public function addNewAction(Request $request)
{
$form = $this->createForm(AddProgramType::class, null, array('company'=>$this->getCurrentCompany(),
'userRepo' =>$this->em->getRepository('DomainCoreBundle:User')));
if ($request->getMethod() === 'POST') {
$form->handleRequest($request);
if ($form->isValid()) {
$company = $this->getCurrentCompany();
$program = $form->getData();
$program->setCreatedDate(new \DateTime());
$program->setCompany($company);
...
If you want to add the same check at database level you should use the #UniqueConstraint annotation in the Table() declaration and give a name to the new index.
Something like:
/**
* Program
* #ORM\Entity(repositoryClass="Domain\CoreBundle\Repository\ProgramRepository")
* #ORM\Table(name="programs", uniqueConstraints={#ORM\UniqueConstraint(name="IDX_PROGRAM_COMPANY", columns={"name", "company_id"})})
* #UniqueEntity(
* fields={"name","company"},
* errorPath = "name",
* message="A program by that name already exists for this company."
* )
* #ORM\HasLifecycleCallbacks()
*/
class Program implements JsonSerializable
```
As a company field is not manager by your form, you need to set its value before form validation:
public function addNewAction(Request $request)
{
$program = new Program();
$program->setCompany($this->getCurrentCompany());
$form = $this->createForm(AddProgramType::class, $program, array('company' => $this->getCurrentCompany(),
'userRepo' => $this->em->getRepository('DomainCoreBundle:User')));
if ($request->getMethod() === 'POST') {
$form->handleRequest($request);
if ($form->isValid()) {
$program = $form->getData();
$program->setCreatedDate(new \DateTime());
}
}
}
I try to validate FormType inside CollectionType with some simple groups rules but It doesn't work, but if i try to make the same without validations groups, it's work fine.
Any idea?
This is a complete and simple exemple that reproduct the error https://github.com/ychakroun/symfony-collection-type-issue
/**
* Sticker
*
* #ORM\Table(name="sticker")
* #ORM\Entity(repositoryClass="App\Repository\StickerRepository")
*/
class Sticker
{
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="App\Entity\Link", mappedBy="sticker", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\OrderBy({"position"="ASC"})
* #Assert\Valid()
*/
private $links;
}
/**
* Link
*
* #ORM\Table(name="link")
* #ORM\Entity(repositoryClass="App\Repository\LinkRepository")
*/
class Link
{
/**
* #var mixed
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string|null
* #Assert\NotBlank()
*
* #ORM\Column(name="title", type="string")
*/
private $title;
/**
* #var bool
*
* #ORM\Column(name="external", type="boolean")
*/
private $external;
/**
*
* #var string|null
*
* #Assert\NotBlank(groups={"isExternal"})
* #Assert\Url(groups={"isExternal"})
* #ORM\Column(name="url", type="text", nullable=true)
*/
private $url;
/**
* #var \App\Entity\PageVersion|null
*
* #Assert\NotBlank(groups={"isInternal"})
* #ORM\ManyToOne(targetEntity="App\Entity\PageVersion")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="page_version_id", referencedColumnName="id", nullable=true)
* })
*/
private $pageVersion;
/**
* #var \App\Entity\Category|null
*
* #Assert\NotBlank(groups={"isInternal"})
* #ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="links")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=true)
* })
*/
private $category;
/**
* #var \App\Entity\Sticker|null
*
* #ORM\ManyToOne(targetEntity="App\Entity\Sticker", inversedBy="links")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="sticker_id", referencedColumnName="id", nullable=true)
* })
*/
private $sticker;
}
And this is the forms i use:
class StickerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('links', CollectionType::class, [
'entry_type' => LinkType::class,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'attr' => [
'class' => 'collection',
],
'by_reference' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Sticker::class,
]);
}
}
class LinkType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'Titre du menu:',
'attr' => [
'input-group' => 'true',
],
])
->add('external', ChoiceType::class, [
'label' => false,
'expanded' => true,
'choices' => [
'Lien interne' => false,
'Lien externe' => true,
],
'choice_attr' => [
'class' => 'link-type',
],
'label_attr' => [
'class' => 'btn-group btn-group-toggle',
'data-toggle' => 'buttons',
]
])
->add('url', UrlType::class, [
'label' => 'SAISISSEZ L\'URL EXTERNE',
'attr' => ['placeholder' => 'https://'],
])
->add('pageVersion', EntityType::class, [
'required' => false,
'class' => Page::class,
'choice_label' => 'name',
])
->add('category', EntityType::class, [
'required' => false,
'class' => Category::class,
'choice_label' => 'title',
'query_builder' => function (CategoryRepository $er) {
return $er->createQueryBuilder('c')->where('c.enabled = 1');
},
])
->add('position', HiddenType::class, [
'attr' => [
'class' => 'my-position',
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Link::class,
'cascade_validation' => true,
'validation_groups' => function (FormInterface $form) {
/** #var Link $link */
$link = $form->getData();
$groups = ['Default'];
if ($link->getExternal()) {
$groups[] = 'isExternal';
} else {
$groups[] = 'isInternal';
}
return $groups;
},
]);
}
}
We can see that the url field is validated and it's blank
If i try to remove groups={"isExternal"} from link entity, the validation will work, like in this image:
I think you need to add the validation groups on the Sticker entity too :
/**
* Sticker
*
* #ORM\Table(name="sticker")
* #ORM\Entity(repositoryClass="App\Repository\StickerRepository")
*/
class Sticker
{
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="App\Entity\Link", mappedBy="sticker", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\OrderBy({"position"="ASC"})
* #Assert\Valid(groups={"isInternal", "isExternal"})
*/
private $links;
}
This option is only valid on the root form and is used to specify which groups will be used by the validator.
This is the response https://github.com/symfony/symfony/issues/31441
Hello we must add an addEventListener
class StickerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('links', CollectionType::class, [
'entry_type' => LinkType::class,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'attr' => [
'class' => 'collection',
],
'by_reference' => false,
])
->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
;
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Sticker::class,
]);
}
public function onPreSubmit(FormEvent $event)
{
if ($event->getData()) {
$data = $event->getData();
$data['links'] = array_values($data['links']);
$event->setData($data);
}
}
}
I am devolping an application in Symfony 2.7, I generated the CRUD from a PRODUCT TABLE (InProducto) which is related OneToMany to another table (InUnidadMedida).
When I open Edit Form, the value in ENTITY FIELD (which is a select field from UNIDAD DE MEDIDA table) always appears the first option of related table (UNIDAD DE MEDIDA). And It suppose to get the value in the field of the table INPRODUCTO
InProductoType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nombre')
->add('descripcion')
->add('unidadMedida', 'entity', array(
'class' => 'NivalInventarioBundle:InUnidadMedida',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->orderBy('u.nombre', 'ASC');
},
'choice_label' => 'nombre',
'by_reference' => false,
'property' => 'type',
'expanded' => false,
'multiple' => false
))
}
Controller:
private function createEditForm(InProducto $entity)
{
$form = $this->createForm(new InProductoType(), $entity, array(
'action' => $this->generateUrl('inproducto_update', array('id' => $entity->getIdProducto())),
'method' => 'PUT',
));
$form->add('submit', 'submit', array('label' => 'Guardar'));
return $form;
}
Producto table (Entity)
/**
* InProducto
*
* #ORM\Table(name="in_producto")
* #ORM\Entity
*/
class InProducto
{
/**
* #ORM\ManyToOne(targetEntity="InSubLinea", inversedBy="InProducto")
* #ORM\JoinColumn(name="id_sub_linea", referencedColumnName="id_sub_linea")
*/
protected $subLinea;
/**
* #ORM\ManyToOne(targetEntity="InUnidadMedida", inversedBy="InProducto")
* #ORM\JoinColumn(name="id_unidad_medida", referencedColumnName="id_unidad_medida")
*/
protected $unidadMedida;
/**
* #var integer
*
* #ORM\Column(name="id_producto", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
Unidad de medida TABLE (entity)
/**
* InUnidadMedida
*
* #ORM\Table(name="in_unidad_medida")
* #ORM\Entity
*/
class InUnidadMedida
{
/**
* #ORM\OneToMany(targetEntity="InProducto", mappedBy="InUnidadMedida")
*/
protected $InProducto;
public function __construct()
{
$this->InProducto = new ArrayCollection();
}
The form type will be guessed automatically by Symfony, if you've mapped these entities correctly.
So make it just
->add('unidadMedida', null, array(
'choice_label' => 'nombre',
'expanded' => false,
'multiple' => false
))
And there is no such option as property. Did you mean property_path?
After hours of scratching head, it was very simple, I add the following properties to my fields:
'by_reference' => true,
'mapped' => true,
Thanks Dmitry Malyshenko for your time.
I try to do a Parents, Children relation on an Entity, but I've a problem when I submit the form. (It's a ManyToMany on himself)
To do it, I've a ManyToMany relation on my entity like it:
class Strain
{
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Strain", inversedBy="children")
*/
private $parents;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Strain", mappedBy="parents")
*/
private $children;
public function __construct()
{
$this->parents = new ArrayCollection();
$this->children = new ArrayCollection();
}
public function addParent(Strain $strain)
{
$this->parents->add($strain);
}
public function removeParent(Strain $strain)
{
$this->parents->removeElement($strain);
}
public function getParents()
{
return $this->parents;
}
public function getChildren()
{
return $this->children;
}
I think it's okay, I've the foreign keys, an intermediate table strain_strain, with 2 columns: strain_source and strain_target.
My FormTypes:
class StrainType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('parents', CollectionType::class, array(
'entry_type' => StrainParentType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
))
;
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain'
));
}
And the second:
class StrainParentType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('gmoStrain', EntityType::class, array(
'class' => 'AppBundle\Entity\Strain',
'choice_label' => 'systematicName',
'placeholder' => '-- select a parent --',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('strain')
->orderBy('strain.systematicName', 'ASC');
}
))
;
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain'
));
}
When I send the form, symfony return this error:
Neither the property "strain" nor one of the methods "getStrain()", "strain()", "isStrain()", "hasStrain()", "__get()" exist and have public access in class "AppBundle\Entity\Strain".
If someone have an idea :/ Because I don't know how to do it.
EDIT:
The problem is in the FormType, because I need a Collection of EntityType, I've do 2 FormType, but I can do it in on FormType and use entry_options to define config of EntityType, like this:
class StrainType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('parents', CollectionType::class, array(
'entry_type' => EntityType::class,
'entry_options' => array(
'class' => 'AppBundle\Entity\Strain',
'choice_label' => 'systematicName',
'placeholder' => '-- select a parent --',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('strain')
->orderBy('strain.systematicName', 'ASC');
}
),
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'required' => false,
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain'
));
}
I have a simple question,
I have two tables in relation many to many, Post and Category,
in an intact form PostType a collection of form CategoryType, but here the problems begin ..
I followed the instructions on the cookbook collection form to persist the data, I just do not get the desired result ..
Here's the code:
class Post
{
/**
*
* #ORM\ManyToMany(targetEntity="Categories", inversedBy="posts", cascade={"persist", "remove"})
* #ORM\JoinTable(name="AnCat",
* joinColumns={
* #ORM\JoinColumn(name="post_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="categories_id", referencedColumnName="id")
* }
* )
**/
protected $categories;
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addCategory($categories)
{
foreach ($categories as $category) {
$category->addPosts($this);
}
$this->categories[] = $categories;
}
class Categories
{
/**
*
* #ORM\ManyToMany(targetEntity="Post", mappedBy="categories")
*/
protected $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
}
/**
*
* #param Post $post
* #return Categories
*/
public function addPosts(Post $posts)
{
// I tried it but I get the same result!
/*if (!$this->posts->contains($posts)) {
$this->posts->add($posts);
}*/
$posts->addCategory($this);
$this->posts[] = $posts;
}
class PostType extends AbstractType
{
->add('Categories', 'collection', array('type' => new CategoriesType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'prototype_name' => '__categ__',
'by_reference' => false
))
class CategoriesType extends AbstractType
{
->add('category', 'entity', array(
'attr' => array('class' => 'cat'),
'class' => 'MyBusinessBundle:Categories',
'property' => 'category',
'label' => 'Categories'
))
The problem is that inserts a new field Category, instead of creating a simple relationship Post-Category.
I don't understand where I'm wrong ..
In your postType, change collection type into entity Type
class PostType extends AbstractType
{
$builder->add('Categories', 'entity',
array( 'label' => 'Categories',
'required' => false,
'expanded' => true,
'class' => 'xxx\xxxBundle\Entity\Categories',
'property' => 'title',
'multiple' => true,
));
In your post creation form you will have checkboxes with categories. If you want a multi select field, change expanded by false