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());
}
}
}
Related
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.
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 have the AppBundle\Entity\Comments entity which I want to persist to the database when the user fill the form I can't find information exactly how to save the form data to the database:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="Comments")
*/
class Comments
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Employer")
* #ORM\JoinColumn(name="employer_id", referencedColumnName="id")
*/
private $employer;
/**
* #ORM\ManyToOne(targetEntity="Application\Sonata\UserBundle\Entity\User")
* #ORM\JoinColumn(name="company_id", referencedColumnName="id")
*/
private $company;
/**
* #ORM\ManyToOne(targetEntity="Commenttypes")
* #ORM\JoinColumn(name="comment_id", referencedColumnName="id")
*/
private $comment;
/**
* #ORM\Column(type="date")
*/
private $from_date;
/**
* #ORM\Column(type="date")
*/
private $to_date;
?>
Here is the Form Type:
<?
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class WorkerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('comment', 'entity', array(
'class' => 'AppBundle\Entity\CommentTypes',
'label' => 'Comment:'
))
->add('from_date', 'Symfony\Component\Form\Extension\Core\Type\DateType', array(
'label' => 'From date:',
'input' => 'datetime',
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
'attr' => [ 'class' => 'datepicker' ]
))
->add('to_date', 'Symfony\Component\Form\Extension\Core\Type\DateType', array(
'label' => 'To date:',
'input' => 'datetime',
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
'attr' => [ 'class' => 'datepicker' ]
))
->add('submit', 'Symfony\Component\Form\Extension\Core\Type\SubmitType', array(
'label' => 'Submit',
'attr' => [ 'class' => 'buttonColor' ]
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Comments',
));
}
}
?>
So by now, I'm receiving the data for the fields $from_date and $to_date and $comment (the ID of the comment from the dropdown) from the Form, what I need to to in my controller in order to persist the comment object to the database. Here is my controller:
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Comments;
use AppBundle\Form\Type\WorkerType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class Worker extends Controller
{
/**
* #Route("/worker/{id}", name="worker")
*/
public function indexAction($id, Request $request)
{
$comment = new Comments();
$form = $this->createForm('AppBundle\Form\Type\WorkerType', $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
{{{{ CODE HERE }}}}
}
return $this->render('default/worker.html.twig',array(
'form' => $form->createView()
));
}
}
I exactly want to know what I need to do to persist the comment object if the form is valid, I'm trying with this:
if ($form->isSubmitted() && $form->isValid()) {
$comment->setEmployer($id);
$comment->setCompany($this->getUser());
$comment->setComment($form->get('comment')->getData());
$comment->setfrom_date($form->get('from_date')->getData());
$comment->setto_date($form->get('to_date')->getData());
}
But I get this error:
Catchable Fatal Error: Argument 1 passed to AppBundle\Entity\Comments::setEmployer() must be an instance of AppBundle\Entity\Employer, string given, called in /home1/rabotnici/src/AppBundle/Controller/Worker.php on line 42 and defined
I have an entity Client and this entity needs to have an association to another Client with information about relationship. This entity is ClientRelationship.
I need to have this asociation in one property, because I want to use symfony form with collection type field.
Problem is that I do not know how to write it to have that association in one Client property.
Now I have in Client two asociation properties, but that is my problem. In form on one side is relationship visible, but on other Client its not.
Client entity:
/**
* #ORM\OneToMany(targetEntity="ClientRelationship", mappedBy="leftClient", cascade={"persist", "remove"})
*/
private $leftRelationships;
/**
* #ORM\OneToMany(targetEntity="ClientRelationship", mappedBy="rightClient")
*/
private $rightRelationships;
ClientRelationship entity:
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length = 255)
* #Assert\Length(max = 255)
* #Assert\NotBlank()
*/
private $relationship;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="leftRelationships")
* #ORM\JoinColumn(name="leftClient_id", referencedColumnName="id")
*/
private $leftClient;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="rightRelationships")
* #ORM\JoinColumn(name="rightClient_id", referencedColumnName="id")
*/
private $rightClient;
EDIT: added forms
ClientType form:
$builder->add('leftRelationships', 'bootstrap_collection', array(
'type' => new ClientRelationshipType(),
'label' => 'relationships',
'allow_delete' => true,
'allow_add' => true,
'by_reference' => false,
'add_button_text' => 'add',
'delete_button_text' => 'remove',
'options' => array(
'businessCase' => $options['businessCase'],
'client' => $options['client'],
),
));
ClientRelationshipType form:
$builder->add('rightClient', 'entity', array(
'label' => 'client',
'class' => 'MyProject\CoreBundle\Entity\Client',
'property' => 'name',
'query_builder' => function (ClientRepository $er) use ($options) {
$qb = $er->createQueryBuilder('c');
if ($options['businessCase'] instanceof BusinessCase) {
$qb->andWhere($qb->expr()->in('c.id', ':clients'));
$qb->setParameter(':clients', $options['businessCase']->getClients()->toArray());
}
if ($options['client'] instanceof Client) {
$qb->andWhere($qb->expr()->neq('c.id', ':client'));
$qb->setParameter(':client', $options['client']);
}
return $qb;
},
));
$builder->add('relationship', 'text', array(
'label' => 'relationship',
));
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