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
Related
I've been reading this post : Doctrine - how to check if a collection contains an entity
But I actually don't like the solution, as, doctrine already provide the contains() method, which have the advantage to keep logic directly into the object, and then to not load EXTRA_LAZY collections entirely.
So here a Cart Entity own a CartProduct collection as is :
/**
* ...
* #ORM\Entity(repositoryClass="App\Repository\CartRepository")
*/
abstract class Cart implements InheritanceInterface{
...
/**
* #ORM\OneToMany(targetEntity="CartProduct", mappedBy="cart", fetch="EXTRA_LAZY", cascade={"persist"})
*/
private Collection $cartProducts;
...
public function __construct()
{
$this->cartProducts = new ArrayCollection();
}
...
}
(CartProduct have to be an Entity look at this simplify EA model. That's a standard way to proceed for related entity holding extra fields)
Now I want to add a new ProductCart Entity to my Cart class.
So I'm adding this method (generated by Symfony make:entity) :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->contains($cartProduct)) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
And then I test this code :
public function testAddCartProduct()
{
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
}
But when I run this test, it fail :
Failed asserting that actual size 2 matches expected size 1.
So I check, and the Cart.cartProducts Collection have two product which are exactly the same objects.
As it's an ArrayCollection, I suppose that it just use this method :
namespace Doctrine\Common\Collections;
class ArrayCollection implements Collection, Selectable {
...
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So well, of course in this case it is just return false, And the objects are considered to be different.
So now, I wish I could use PersistentCollection instead of ArrayCollection when implementing the Collection object , because the PersistentCollection.contains() method looks better.
abstract class Cart implements InheritanceInterface{
...
public function __construct()
{
-- $this->cartProducts = new ArrayCollection();
++ $this->cartProducts = new PersistentCollection(...);
}
}
But this require an EntityManager as a parameter, so, seams a little bit overkill to give an EntityManager to an Entity object...
So I finally, I don't know what is the better way to check for a dupplicate entity inside a collection.
Of course, I could implement myself a thing like :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->filter(
function (CartProduct $cp)use($cartProduct){
return $cp->getId() === $cartProduct->getId();
})->count()) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
But it'll require to load every Entity and I really don't like the idea.
Personally I agree with your comment, I don't think the entity itself should have the responsibility to ensure there is no duplicate.
The entity cannot make a request like a repository could, and I don't see how you can be sure there is no duplicate in the database without querying it.
Calling contains will not trigger a fetch in your case, this means the collection will stay as is, which is not what you want anyway because you could have a previously persisted duplicate that will not be part of the collection because you marked it as EXTRA_LAZY.
You also don't want to fetch all the entities of the collection (and transform the results into objects) just to check if you have a collision.
So IMHO you should create a method in the repository of the entity to check for duplicates, a simple SELECT COUNT(id).
Then there is your real problem.
The way you make your test will never find a collision. When you do:
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
You are creating two instances of CartProduct, that's why the call to contains doesn't find anything.
Because contains checks for the object reference, not the content, like you can see in its implementation:
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So in your test case what you're really testing is:
in_array(new CartProduct(), [new CartProduct()], true);
which will always return false.
I know that association property in entity is implements \Doctrine\Common\Collections\Collection. I know that in constructor such properties should be initialized:
$this->collection = new \Doctrine\Common\Collections\ArrayCollection()
I know that I can modify collections using ArrayCollection#add() and ArrayCollection#remove(). However I have a different case.
Suppose I have a new simple array of associative entities. Using existing methods I need to check every element in array: if entity collection has it. If no - add array element to entity collection. In addition to this, I need to check every element in entity collection. If any collection element is absent in new array, then I need to remove it from collection. So much work to do trivial thing.
What I want? To have the setProducts method implemented:
class Entity {
private $products;
// ... constructor
public function setProducts(array $products)
{
// synchronize $products with $this->products
}
}
I tried: $this->products = new ArrayCollection($products). However this makes doctrine remove all products and add those ones from $products parameter. I want similar result but without database queries.
Is there any built in solution in Doctrine for such case?
Edit:
I would like to have a method in ArrayCollection like fromArray which would merge elements in collections removing unneeded. This would just duplicate using add/remove calls for each element in collection argumen manually.
Doctrine collections do not have a "merge"-feature that will add/remove entities from an array or Collection in another Collection.
If you want to "simplify" the manual merge process you describe using add/remove, you could use array_merge assuming both arrays are not numeric, but instead have some kind of unique key, e.g. the entity's spl_object_hash:
public function setProducts(array $products)
{
$this->products = new ArrayCollection(
array_merge(
array_combine(
array_map('spl_object_hash', $this->products->toArray()),
$this->products->toArray()
),
array_combine(
array_map('spl_object_hash', $products),
$products->toArray()
)
)
);
}
You might want to use the product id instead of spl_object_hash as 2 products with the same id, but created as separate entities - e.g. one through findBy() in Doctrine and one manually created with new Product() - will be recognized as 2 distinct products and might cause another insert-attempt.
Since you replace the original PersistentCollection holding your previously fetched products with a new ArrayCollection this might still result in unneeded queries or yield unexpected results when flushing the EntityManager, though. Not to mention, that this approach might be harder to read than explicitly calling addElement/removeElement on the original Collection instead.
I would approach it by creating my own collection class that extends Doctrine array collection class:
use Doctrine\Common\Collections\ArrayCollection;
class ProductCollection extends ArrayCollection
{
}
In the entity itself you would initialise it in the __constructor:
public function __construct()
{
$this->products = new ProductCollection();
}
Here, Doctrine will you use your collection class for product results. After this you could add your own function to deal with your special merge, perhaps something:
public function mergeProducts(ProductCollection $products): ProductCollection
{
$result = new ProductCollection();
foreach($products as $product) {
$add = true;
foreach($this->getIterator() as $p) {
if($product->getId() === $p->getId()) {
$result->add($product);
$add = false;
}
}
if($add) {
$result->add($product);
}
}
return $result;
}
It will return a brand new product collection, that you can replace your other collection in the entity. However, if the entity is attached and under doctrine control, this will render SQL at the other end, if you want to play with the entity without risking database updates you need to detach the entity:
$entityManager->detach($productEntity);
Hopes this helps
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?
I'm using an embed Symfony form to add and remove Tag entities right from the article editor. Article is the owning side on the association:
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"})
*/
private $tags;
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
}
The condition doesn't help here, as it is always true, and if it wasn't, no new tags would be persisted to the database at all. Here is the Tag entity:
class Tag
{
/**
* #Column(unique=true)
*/
private $name
/**
* #ManyToMany(targetEntity="Articles", mappedBy="tags")
*/
private $articles;
public function addArticle(Article $articles)
{
$this->articles[] = $articles;
}
}
I've set $name to unique, because I want to use the same tag every time I enter the same name in the form. But it doesn't work this way, and I get the exception:
Integrity constraint violation: 1062 Duplicate entry
What do I need to change to use article_tag, the default join table when submitting a tag name, that's already in the Tag table?
I have been battling with a similar issue for months and finally found a solution that seems to be working very well in my application. It's a complex application with quite a few many-to-many associations and I need to handle them with maximum efficiency.
The solution is explained in part here: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-constraint-failures-during-em-flush
You were already halfway there with your code:
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
Basically what I have added to this is to set indexedBy="name" and fetch="EXTRA_LAZY" on the owning side of the relationship, which in your case is Article entity (you may need to scroll the code block horizontally to see the addition):
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"}, indexedBy="name" fetch="EXTRA_LAZY")
*/
private $tags;
You can read up about the fetch="EXTRA_LAZY" option here.
You can read up about indexBy="name" option here.
Next, I modified my versions of your addTag() method as follows:
public function addTag(Tag $tags)
{
// Check for an existing entity in the DB based on the given
// entity's PRIMARY KEY property value
if ($this->tags->contains($tags)) {
return $this; // or just return;
}
// This prevents adding duplicates of new tags that aren't in the
// DB already.
$tagKey = $tag->getName() ?? $tag->getHash();
$this->tags[$tagKey] = $tags;
}
NOTE: The ?? null coalesce operator requires PHP7+.
By setting the fetch strategy for tags to EXTRA_LAZY the following statement causes Doctrine to perform a SQL query to check if a Tag with the same name exists in the DB (see the related EXTRA_LAZY link above for more):
$this->tags->contains($tags)
NOTE: This can only return true if the PRIMARY KEY field of the entity passed to it is set. Doctrine can only query for existing entities in the database/entity map based on the PRIMARY KEY of that entity, when using methods like ArrayCollection::contains(). If the name property of the Tag entity is only a UNIQUE KEY, that's probably why it's always returning false. You will need a PRIMARY KEY to use methods like contains() effectively.
The rest of the code in the addTag() method after the if block creates a key for the ArrayCollection of Tags either by the value in the PRIMARY KEY property (preferred if not null) or by the Tag entity's hash (search Google for "PHP + spl_object_hash", used by Doctrine to index entities). So, you are creating an indexed association, so that if you add the same entity twice before a flush, it will just be re-added at the same key, but not duplicated.
Two main solutions
First
Use a data transformer
class TagsTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* used to give a "form value"
*/
public function transform($tag)
{
if (null === $tag) {
//do proper actions
}
return $issue->getName();
}
/**
* used to give "a db value"
*/
public function reverseTransform($name)
{
if (!$name) {
//do proper actions
}
$issue = $this->om
->getRepository('YourBundleName:Tag')
->findOneBy(array('name' => $name))
;
if (null === $name) {
//create a new tag
}
return $tag;
}
}
Second
Use lifecycle callback. In particular you can use prePersist trigger onto your article entity? In that way you can check for pre-existing tags and let your entity manager manage them for you (so he don't need to try to persist causing errors).
You can learn more about prePersist here
HINT FOR SECOND SOLUTION
Make a custom repository method for search and fetch old tags (if any)
In my Application the user can create Custom Fields for some entities and then set the values for this custom fields for each entity object when i display a form.
The implementation is like this:
1º) I created a Interface for the forms, and the forms that i want implement this Interface.
2º) I created a form extension for all forms:
app_core_form_builder.form_extension:
class: App\Core\Bundle\FormBuilderBundle\Form\FormExtension
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
tags:
- { name: form.type_extension, alias: form }
3º) In this extension if the form implements the interface referenced in the step 1 i add a EventSubscriber:
if($formType instanceof \App\Core\Bundle\FormBuilderBundle\Model\IAllowCustomFieldsdInterface){
$builder->addEventSubscriber(new FormSubscriber($this->container, $this->em));
}
4º) This Form Subscriber subscribes the preSetData FormEvent. In this method i get the Entity associated with the form and i get all custom fields created for it.
Then i add this fields to the form with the help of Symfony2 Form Type.
Everything goes well, and when i display my form the custom fields are rendered correct. Just for the record, when i save the form the values inserted in the custom fields also are store well.
public function preSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
// During form creation setData() is called with null as an argument
// by the FormBuilder constructor. You're only concerned with when
// setData is called with an actual Entity object in it (whether new
// or fetched with Doctrine). This if statement lets you skip right
// over the null condition.
if (null === $data) {
return;
}
$formEntity = $form->getConfig()->getType()->getInnerType()->getEntity();
$DbEntity = $this->em->getRepository('AppCoreSchemaBundle:DbEntity')->findOneBy(array('id' => $formEntity));
if ($DbEntity && $DbEntity->getAllowCustomFields()) {
$organization = $this->container->get('app_user.user_manager')->getCurrentOrganization();
if (!$organization) {
throw $this->createNotFoundException('Unable to find Organization entity.');
}
$params = array(
'organization' => $organization,
'entity' => $DbEntity,
);
$entities = $this->em->getRepository('AppCoreSchemaBundle:DbCustomField')->getAll($params);
# RUN BY ALL CUSTOM FIELDS AND ADD APPROPRIATE FIELD TYPES AND VALIDATORS
foreach ($entities as $customField) {
# configurate customfield
FieldConfiguration::configurate($customField, $form);
# THE PROBLEM IS HERE
# IF OBJECT IS NOT NULL THEN MAKE SET DATA FOR APPROPRIATED FIELD
if ($data->getId()) {
$filters = array(
'custom_field' => $customField,
'object' => $data->getId(),
);
$DbCustomFieldValue = $this->em->getRepository('UebCoreSchemaBundle:DbCustomFieldValue')->getFieldValue($filters);
if ($DbCustomFieldValue) {
$form[$customField->getFieldAlias()]->setData($DbCustomFieldValue->getValue());
} else {
$form[$customField->getFieldAlias()]->setData(array());
}
}
}
}
}
The problem is when i try to edit a form. if you look at the part in the code above where says "THE PROBLEM IS HERE" you can understand.
If the object of the form has an ID, then i will get the values stored for the custom fields of that object, and i call $form[field_alias']->setData(value returned from database that is mapped as type Array).
But this dont work, and the Data is not set for the fields. But if in my controller i do the same, the data is set properly.
Does anybody have an idea where the problem can be? Can't i set the data in preSetData Event?
EDITED
The value field from the Entity DbCustomField is mapped in this way:
/**
* #var string
*
* #ORM\Column(name="value", type="array", nullable=true)
*/
protected $value;
`
var_dump($DbCustomFieldValue) -> object(Ueb\Core\Bundle\SchemaBundle\Entity\DbCustomFieldValue)
var_dump(DbCustomFieldValue->getValue())
-> string(11) "bruno valor"
But even if i try something like:
var_dump($customField->getFieldAlias()); = string(21) "testebruno-1383147874"
$form[$customField->getFieldAlias()]->setData('example1'); it doesnt work.
But in my controller if i do the following for the fieldAlias above:
$form['testebruno-1383147874']->setData('example2');
-> it does work
Any idea?
As metalvarez suggested in his/her comment and working as expected, use the postSetData event instead of the preSetData one:
public function postSetData(FormEvent $event) {
// ...
}
The preSetData event method is called before populating the form with default values, then Symfony2 will set the data and it may change from what you set before, thus the use of postSetData instead.
Figure from the doc