I am using tags on a form using tagsinput :
This plugin ends-up with a single text field containing tags separated by a comma (eg: tag1,tag2,...)
Those tags are currently managed on a non-mapped form field:
$builder
// ...
->add('tags', 'text', array(
'mapped' => false,
'required' => false,
))
;
And finally, they are stored on an ArrayCollection, as this is a bad practice to store multiple values in a database field:
/**
* #var ArrayCollection[FiddleTag]
*
* #ORM\OneToMany(targetEntity="FiddleTag", mappedBy="fiddle", cascade={"all"}, orphanRemoval=true)
*/
protected $tags;
To map my form to my entity, I can do some code in my controller like this:
$data->clearTags();
foreach (explode(',', $form->get('tags')->getData()) as $tag)
{
$fiddleTag = new FiddleTag();
$fiddleTag->setTag($tag);
$data->addTag($fiddleTag);
}
But this looks the wrong way at first sight.
I am wondering what is the best practice to map my entity to my form, and my form to my entity.
This is tricky since you aren't just embedding a collection of Tag forms that are say, all separate text fields. I suppose you could do that with some trickery, but what about using a data transformer instead? You could convert a comma-separated list of tags to an ArrayCollection and pass that back to the form, and on the flip-side, take the collection and return the tags as a comma-separated string.
Data transformer
FiddleTagsTransformer.php
<?php
namespace Fuz\AppBundle\Transformer;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\DataTransformerInterface;
use Fuz\AppBundle\Entity\FiddleTag;
class FiddleTagTransformer implements DataTransformerInterface
{
public function transform($tagCollection)
{
$tags = array();
foreach ($tagCollection as $fiddleTag)
{
$tags[] = $fiddleTag->getTag();
}
return implode(',', $tags);
}
public function reverseTransform($tags)
{
$tagCollection = new ArrayCollection();
foreach (explode(',', $tags) as $tag)
{
$fiddleTag = new FiddleTag();
$fiddleTag->setTag($tag);
$tagCollection->add($fiddleTag);
}
return $tagCollection;
}
}
Note: you cannot specify ArrayCollection type to public function transform($tagCollection) because your implementation should match the interface.
Form type
The second step is to replace your form field declaration so it will use the data transformer transparently, you'll not even need to do anything in your controller:
FiddleType.php
$builder
// ...
->add(
$builder
->create('tags', 'text', array(
'required' => false,
))
->addModelTransformer(new FiddleTagTransformer())
)
;
Validation
You can use #Assert\Count to limit the number of allowed tags, and #Assert\Valid if your FiddleTag entity has some validation constraints itself.
Fiddle.php
/**
* #var ArrayCollection[FiddleTag]
*
* #ORM\OneToMany(targetEntity="FiddleTag", mappedBy="fiddle", cascade={"all"}, orphanRemoval=true)
* #Assert\Count(max = 5, maxMessage = "You can't set more than 5 tags.")
* #Assert\Valid()
*/
protected $tags;
Further reading
See the Symfony2 doc about data transformers: http://symfony.com/doc/current/cookbook/form/data_transformers.html
See these posts for some other ideas:
Parsing comma separated string into multiple database entries (eg. Tags)
How does Symfony 2 find custom form types?
Related
How can I bind an object to a laminas form using Doctrine Hydrator? The bind function populates the base fieldset, but will not populate my address fieldset which is a mapped entity in the user/member.
This is my form code that pulls in my field sets. All form fields are created in the view, but the bind method doesn't seem to be aware of my address fieldset.
namespace Member\Form;
use Doctrine\Laminas\Hydrator\DoctrineObject as DoctrineHydrator;
use Doctrine\Persistence\ObjectManager;
use Laminas\Form\Element\Collection;
use Laminas\Form\Form;
class MemberProfileForm extends Form
{
/**
* __construct($objectManager)
* #param \Doctrine\Persistence\ObjectManager $objectManager
*/
public function __construct(ObjectManager $objectManager)
{
parent::__construct('member-profile-form');
/**
* Set the hydrator
*/
$this->setHydrator(new DoctrineHydrator($objectManager));
/**
* Set the user base fieldset
*/
$profileFieldset = new \User\Form\ProfileFieldset($objectManager);
$profileFieldset->setUseAsBaseFieldset(true);
$this->add($profileFieldset);
/**
* Set the home address fieldset
*/
$homeAddressFieldset = new \User\Fieldset\AddressFieldset($objectManager);
$this->add($homeAddressFieldset);
/**
* Security & submit
*/
$this->add([
'name' => \Application\Fieldset\SubmitCsrfFieldset::FIELDSET_NAME,
'type' => \Application\Fieldset\SubmitCsrfFieldset::class
]);
}
}
The documentation demonstrates how to do a one to many scenario, but this is a one to one and nothing I try works, leaving the form field blank, though the profile form fields are populated from the base fieldset.
I think your address fieldset should be inside your profile fieldset.
You should have an address property inside your profile entity which should map to your address entity. Doctrine object hydrator should pick this relationship up from your entites.
So the problem is like this:
I am trying to save some data from API and I need to validate them with Symfony validation ex:
private $id;
/**
* #var
* #Assert\Length(max="255")
* #CustomAssert\OrderExternalCode()
* #CustomAssert\OrderShipNoExternalCode()
*/
private $code;
private $someId;
/**
* #var
* #Assert\NotBlank()
* #Assert\Length(max="255")
*/
private $number;
this works well but now I need to add some Assert Constrains dynamically from the controller and that is where I am stuck!
Does anyone knows how to do that or any suggestion that might help?
Currently I did an extra constraint which does extra query in the DB and I don't want to do that and I am not using FormType.
You can use groups and use (or leave out) the extra group you're talking about.
Using the CallbackConstraint should help I think, in your case :
use My\Custom\MyConstraint;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
// This is not tested !
class MyEntity
{
/**
* #Assert\Callback()
*/
public function validateSomeId(ExecutionContextInterface $context)
{
$constraint = new MyConstraint(['code' => $this->code]);
$violations = $context->getValidator()->validate($this->number, $constraint);
foreach ($violations as $violation) {
$context->getViolations()->add($violation);
}
}
}
See https://symfony.com/doc/current/reference/constraints/Callback.html
EDIT : I don't know what you're trying to validate so I just put some random params of your entity in there
So I wanted to dynamically validate the request data based on a condition in the controller.
I specified an extra group for that in the entity like so:
/**
* #var
* #Assert\NotBlank(groups={"extra_check"})
* #Assert\Length(max="255")
*/
private $externalId;
Then in the controller I just did the condition to validate with the extra group or not.
$groups = $order->getExternalCode() != null ? ['Default'] : ['Default', 'extra_check'];
$this->validateRequest($request, null, $groups);
The Default group is the one without group specified and the other one is the group I specified in the field
Im learning symfony 3 & doctrine and i created a form with entity collection. Entities are Post and Tags with manyTomany relation. Main form is Post with collection of tags.
I want to pass only IDs (primary key) of tags in my collection. In result i have only one field in tag form:
$builder->add('tagId');
I created autocomplete for it, thats why i need only primary key.
After saving my form, doctrine create new tag entities with passed ids but i want to find those entities instead of creating new. Have no clue...
I was trying to make it work inside my controller:
$formTags = $form->get('tag');
foreach ($formTags->getData() as $key => $formTag)
{
// here i have new entities with id ;/
if($formTag->getTagId())
{
// so i tryied to find them, and replace it
$formTags->offsetSet($key,
array($this->getDoctrine()->getRepository('BlogBundle:Tag')
->find($formTag->getTagId())));
}
}
But symfony throw me exceptions, also with setData method. Cant change it after form is submitted. I hope you guys can help me!
i was trying to make data transformer. Forget to mention :) Problem Was that my transformer change tagId field to tag object. In result i had tag object with new entity, and instead tagId value - there was another object inside, transformed. So dont work like expected for me. I think i should make transformer for collection field instead of tag id, but have no idea how make it work. I tryied to make "tag" field inside collection and transform it, but doctrine try always to get value from entity based on fields so no getTag() method found :)
You can use Symfony DataTransfomer in your TagType to transform the tagId to a Tag Entity .
From Symfony DataTransformer docs :
Data transformers are used to translate the data for a field into a format that can be displayed in a form (and back on submit).
...
Say you have a many-to-one relation from the Task entity to an Issue entity (i.e. each Task has an optional foreign key to its related Issue). Adding a listbox with all possible issues could eventually get really long and take a long time to load. Instead, you decide you want to add a textbox, where the user can simply enter the issue number.
I made it. With data transformers, but we need to make transformer for collection, not for field inside collection.
So its look like that (works!).
My PostType.php form need to have entity manager (like inside documentation, about data transformers), and data transformer for collection, so i added:
# PostType.php form
namespace BlogBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use BlogBundle\Form\DataTransformer\TagToIdTransformer;
use Doctrine\Common\Persistence\ObjectManager;
class PostType extends AbstractType
{
private $manager;
public function __construct(ObjectManager $manager)
{
// needed for transformer :(
// and we need to register service inside app config for this. Details below
$this->manager = $manager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('tag', CollectionType::class, array(
'entry_type' => TagType::class,
'by_reference' => false,
'allow_add' => true,
))
->add('save', SubmitType::class, array('label' => 'Save'));
$builder->get('tag')
->addModelTransformer(new TagToIdTransformer($this->manager));
}
}
Constructor will trow exception, we need to pass ObjectManager to it. To make it, modify config file inside your bundle:
# src/BlogBundle/Resources/config/services.yml
services:
blog.form.type.tag:
class: BlogBundle\Form\PostType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
Now lets make transformer for a collection! I made it wrong before, because i was trying to make like inside documentation, for one field. For collection we need to transform whole array of tags (its manyToMany collection):
<?php
namespace BlogBundle\Form\DataTransformer;
use BlogBundle\Entity\Tag;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class TagToIdTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms array of objects (Tag) to an array of string (number).
*
* #param array|null $tags
* #return string
*/
public function transform($tags)
{
$result = array();
if (null === $tags) {
return null;
}
foreach ($tags as $tag)
{
$result[] = $tag->getTagId();
}
return $result;
}
/**
* Transforms an array of strings (numbers) to an array of objects (Tag).
*
* #param string $tagsId
* #return Tag|null
* #throws TransformationFailedException if object (Tag) is not found.
*/
public function reverseTransform($tagsId)
{
// no issue number? It's optional, so that's ok
if (!$tagsId) {
return;
}
$result = array();
$repository = $this->manager
->getRepository('BlogBundle:Tag');
foreach ($tagsId as $tagId) {
$tag = $repository->find($tagId);
if (null === $tag) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'An tag with id "%s" does not exist!',
$tagId
));
}
$result[] = $tag;
}
return $result;
}
}
Everything works fine now. I can easy save my entities with autocomplete that populate IDs of tags only
So here is the scenario: I have a radio button group. Based on their value, I should or shouldn't validate other three fields (are they blank, do they contain numbers, etc).
Can I pass all these values to a constraint somehow, and compare them there?
Or a callback directly in the controller is a better way to solve this?
Generally, what is the best practice in this case?
I suggest you to use a callback validator.
For example, in your entity class:
<?php
use Symfony\Component\Validator\Constraints as Assert;
/**
* #Assert\Callback(methods={"myValidation"})
*/
class Setting {
public function myValidation(ExecutionContextInterface $context)
{
if (
$this->getRadioSelection() == '1' // RADIO SELECT EXAMPLE
&&
( // CHECK OTHER PARAMS
$this->getFiled1() == null
)
)
{
$context->addViolation('mandatory params');
}
// put some other validation rule here
}
}
Otherwise you can build your own custom validator as described here.
Let me know you need more info.
Hope this helps.
You need to use validation groups. This allows you to validate an object against only some constraints on that class. More information can be found in the Symfony2 documentation http://symfony.com/doc/current/book/validation.html#validation-groups and also http://symfony.com/doc/current/book/forms.html#validation-groups
In the form, you can define a method called setDefaultOptions, that should look something like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// some other code here ...
$builder->add('SOME_FIELD', 'password', array(
'constraints' => array(
new NotBlank(array(
'message' => 'Password is required',
'groups' => array('SOME_OTHER_VALIDATION_GROUP'),
)),
)
))
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$groups = array('Default');
$data = $form->getData();
if ($data['SOME_OTHER_FIELD']) { // then we want password to be required
$groups[] = 'SOME_OTHER_VALIDATION_GROUP';
}
return $groups;
}
));
}
The following link provides a detailed example of how you can make use them http://web.archive.org/web/20161119202935/http://marcjuch.li:80/blog/2013/04/21/how-to-use-validation-groups-in-symfony/.
Hope this helps!
For anyone that may still care, whilst a callback validator is perfectly acceptable for simpler dependencies an expression validator is shorter to implement.
For example if you've got a field called "Want a drink?" then if yes (true) "How many?" (integer), you could simplify this with:
/**
* #var bool|null $wantDrink
* #ORM\Column(name="want_drink", type="boolean", nullable=true)
* #Assert\NotNull()
* #Assert\Type(type="boolean")
*/
private $wantDrink;
/**
* #var int|null $howManyDrinks
* #ORM\Column(name="how_many_drinks", type="integer", nullable=true)
* #Assert\Type(type="int")
* #Assert\Expression(
* "true !== this.getWantDrink() or (null !== this.getHowManyDrinks() and this.getHowManyDrinks() >= 1)",
* message="This value should not be null."
* )
*/
private $howManyDrinks;
You write the expression in PASS context, so the above is saying that $howManyDrinks must be a non-null integer at least 1 if $wantDrink is true, otherwise we don't care about $howManyDrinks. Make use of the expression syntax, which is sufficient for a broad range of scenarios.
Another scenario I find myself frequently using a expression validator are when I've got two fields to the effect of "date start" and "date end", so that they can each ensure that they are the right way around (so that the start date is before or equal to the end date and the end date is greater or equal to the start date).
This is my error:
The form's view data is expected to be an instance of class
My\Bundle\Entity\Tags, but is an instance of class
Doctrine\Common\Collections\ArrayCollection. You can avoid this error
by setting the "data_class" option to null or by adding a view
transformer that transforms an instance of class
Doctrine\Common\Collections\ArrayCollection to an instance of
My\Bundle\Entity\Tags
and this is my form builder
$builder
->add('name')
->add('tags','collection',array(
'data_class' => 'My\Bundle\Entity\Tags'
)
)
->add('save','submit')
;
I changed data_class to null ( only that ) and I'm getting error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, **but is an instance of class
My\Bundle\Entity\Tags*. You can avoid this error by setting the
"data_class" option to "My\Bundle\Entity\Tags" or by adding a view
transformer that transforms an instance of class My\Bundle\Entity\Tags
to scalar, array or an instance of \ArrayAccess.
I've tried with a transformer, so it looked like this :
$transformer = new TagTransformer($this->entityManager);
$builder
->add(
$builder->create(
'tags','collection',array(
'data_class' => 'My\Bundle\Entity\Tags'
)
)->addModelTransformer($transformer)
);
and transformer:
public function transform($tag)
{
if (null === $tag) {
return "";
}
return $tag->toArray();
}
and changed data_class to null again. What I get:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
My\Bundle\Entity\Tags. You can avoid this error by setting the
"data_class" option to "My\Bundle\Entity\Tags" or by adding a view
transformer that transforms an instance of class My\Bundle\Entity\Tags
to scalar, array or an instance of \ArrayAccess.
When I changed data_class to My\Bundle\Entity\Tags
The form's view data is expected to be an instance of class
My\Bundle\Entity\Tags, but is a(n) array. You can avoid this error by
setting the "data_class" option to null or by adding a view
transformer that transforms a(n) array to an instance of
My\Bundle\Entity\Tags.
Well.. I mean... wtf? What am I doing wrong? How can I change that?
Edit:
My user entity:
class User
{
/**
* #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=255)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Tags", cascade={"persist"})
*/
protected $tags;
// methods, etc..
}
So the reason you're getting errors is because you're using the collection field type a bit incorrect. First of all, the collection field type doesn't support data_class. When you say
->add('tags','collection',array(
'data_class' => 'My\Bundle\Entity\Tags'
)
)
you're basically saying that tags (which is an array collection according to your declaration) is actually a tag. If you look at the documentation for the collection type you'll notice that data_class isn't even a supported option. http://symfony.com/doc/current/reference/forms/types/collection.html
so if you want to render a multiple choice list of tags you're looking for the entity type, however these are tags, and if you have any sort of decent site you'll probably have way more than a multiple choice list would be practical for. design wise you want to just have an auto-completer to show what tags already exist with the typed text as you type and then just have the user press enter to add the tag whether is exists or not. then above the auto completer you'd show the tags already added and have and x next to them that they can press on to remove the tag.
you can cheat by just having tags field in your form be a unmapped text type and use javascript to combine the tags together into a string on form submit, then in your action turn the string into your tags.
Sorry for the delay here but would this work for you ?
$builder
->add('name')
->add('tags','collection',array(
'type' => '**{{ NAME OF THE FORM }}**',
)
)
->add('save','submit');