An interesting issue occurred regarding email registration with the FOSUserBundle when using Hotmail for account signups.
FOSUserBundle’s registration confirmation was enabled, so the user signing up would receive an email containing a link with a token used to verify and enable their account.
However, when clicking the verification link from any Hotmail email account, a 404 not found page was the result.
The NotFoundHttpException said:
The user with confirmation token “R_jrlvXPyi_qTb3kvbCE_x5Qka89SikV9F1FlL5xWU4” does not exist
There is information online regarding this FOSUserBundle error, but it all seems to relate to users accidentally clicking their link multiple times. In our case, the link was clicked once…and only once.
Checking the database, I could see that the account had been enabled successfully, and the verification token removed. This is exactly what you’d expect to see upon a successful account confirmation.
Looking back at the Hotmail email again, I noticed that it was including a link preview of our site in the email body (similar to what you see when sharing links on Facebook).
I realized that Hotmail was likely sending a bot to our verification link in order to create that preview.
Checking in the server logs confirmed it:
157.55.39.45 – – [30/Dec/2016:14:22:54 -0500] “GET /signup/confirm/R_jrlvXPyi_qTb3kvbCE_x5Qka89SikV9F1FlL5xWU4 HTTP/1.1” 302 4648 “-” “Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b”
A visit from their BingPreview crawler occurred the moment the email was opened.
Considering how FOSUserBundle is set up to work, this might cause some user confusion.
You see, the first time a confirmation link is visited successfully, FOSUserBundle enables the account, and removes the verification token from the database. Any attempts to visit the url again with that token will result in a 404 not found (as the token no longer exists in the database).
The user has no idea their account was already verified by the BingPreview crawler just by opening their email, and due to the 404 error page, will likely believe that something went wrong.
So, what can you do if you want to continue using FOSUserBundle with email verification?
One thing you could do is, set your .htaccess to block the BingPreview user agent from accessing your signup and password reset paths (Your paths may differ from this example):
|
RewriteEngine On RewriteCond %{REQUEST_URI} /signup/confirm/ [OR] RewriteCond %{REQUEST_URI} /resetting/reset/ RewriteCond %{HTTP_USER_AGENT} BingPreview RewriteRule . - [F,L] |
This will stop the BingPreview crawler, but I feel like it is an incomplete solution. You can block as many annoying user agents here as you want, but do you really want to spend all your time hunting down which email providers are providing similar link previews in their emails?
Another thing you can do is to override the way FOSUserBundle handles account confirmation.
If you don’t know how to override FOSUserBundle controllers, read this article: Overriding Default FOSUserBundle Controllers
Once your bundle override is in place, you can do something like suggested by GitHub user dmaicher here in your RegistrationController.php: https://github.com/FriendsOfSymfony/FOSUserBundle/issues/2106#issuecomment-212027852
Make sure that you also
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; in your file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
// src/MyCompany/UserBundle/Controller/RegistrationController.php namespace MyCompany\UserBundle\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use FOS\UserBundle\Controller\RegistrationController as BaseController; class RegistrationController extends BaseController { /** * Redirect to login page when token confirmation fails. */ public function confirmAction(Request $request, $token) { try { return parent::confirmAction($request, $token); } catch (NotFoundHttpException $e) { return $this->redirectToRoute('fos_user_security_login'); } } } |
In that example, if another email provider decides to start visiting your confirmation links, the users would at least be redirected to a login page rather than getting a confusing 404.
You can get creative with it, and I’ll post again if I come up with something better.
Would love to hear others’ opinions on this, and/or if you have had issues with this kind of behavior from email providers before.
This is Part 4 in a series on setting up Tags in a ManyToMany association with an Entity in Symfony. You can read the previous three articles here:
- Managing Tags in ManyToMany association with Symfony
- Setting up Form Collection for Tags in Symfony
- Using a Data Transformer in Symfony to handle duplicate tags
By now, you should have your Product and Tag entities set up, along with your forms, and a data transformer for preventing duplicate tags from being added to the database. At the end of the previous article, we also created a controller which renders the submitProduct.html.twig template that we’ll be using for displaying our form to the world.
As mentioned in the previous article, we are going to need to add some JavaScript to add/remove
tag.name inputs for our form. There are multiple ways you can do this, and the Symfony documention on Allowing “new” Tags with the “Prototype” shows you a good example.
In this article, we will be using the jQuery Tag-it plugin, which allows you to have a tag input box on your page which works something like this:
You can read Tag-it’s documentation for more information on it. For this tutorial, go ahead and download/unzip Tag-it, and copy these files:
css/jquery.tagit.css
css/tagit.ui-zendesk.css
js/tag-it.min.js
Into your Symfony web assets folder:
web/css/jquery.tagit.css
web/css/tagit.ui-zendesk.css
web/js/tag-it.min.js
Make sure you also have jQuery and jQuery-ui included in your template, as they are required by Tag-it.
Ok, now how to set up this plugin with our Symfony form?
Remember in the Symfony example using prototype, that you are replacing __name__ with the index of the next item in the array:
product[tags][__name__][name]
Using a find replace on __name__ you can create new inputs for your form:
|
<input id="product_tags_0_name" name="product[tags][0][name]" type="text"> <input id="product_tags_1_name" name="product[tags][1][name]" type="text"> <input id="product_tags_2_name" name="product[tags][2][name]" type="text"> |
The way that Tag-it works is to add a hidden form input for each tag. It has a fieldName option which allows you to set the name you want to use for your inputs. You might think of doing something like this there:
|
$('#tags').tagit({ fieldName: 'product[tags][][name]' }); |
That will actually work to add your tags, but will not play nicely when you later remove a tag from the middle of a group of tags associated with a Product, and add another to it for example. The index of the item in the array needs to be set in the field name, so that it knows exactly which item to remove/add.
So how to increment the field name’s index in Tag-it?
In the Tag-it documentation, you will notice there is a beforeTagAdded event. We can use this to increment our index, and reset the Tag-it fieldName value each time a new tag is added. Here is a basic example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
<ul id="tags"></ul> <script> $(document).ready(function () { var prototypeFieldName = 'product[tags][__name__][name]'; var fieldIndex = 0; $('#tags').tagit({ fieldName: prototypeFieldName.replace('__name__', fieldIndex), beforeTagAdded: function (event, ui) { fieldIndex++; $(this).tagit({fieldName: prototypeFieldName.replace('__name__', fieldIndex)}); } }); }); </script> |
Test that by typing 3 new tags into your Tag-it input box: car, Japanese, import
And you’ll see that Tag-it adds the hidden inputs to the form just we as we want them:
|
<input value="car" name="product[tags][0][name]" class="tagit-hidden-field" type="hidden"> <input value="Japanese" name="product[tags][1][name]" class="tagit-hidden-field" type="hidden"> <input value="import" name="product[tags][2][name]" class="tagit-hidden-field" type="hidden"> |
If you prefer not to hand code the field name value in, but want to use Symfony’s form prototype instead, you could do something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
{% do form.tags.setRendered %} <ul id="tags" data-prototype-field-name="{{ form.tags.vars.prototype.name.vars.full_name }}"></ul> <script> $(document).ready(function () { var $tags = $('ul#tags'); var prototypeFieldName = $tags.data('prototype-field-name'); var fieldIndex = 0; $tags.tagit({ fieldName: prototypeFieldName.replace(/__name__/g, fieldIndex), beforeTagAdded: function (event, ui) { fieldIndex++; $(this).tagit({fieldName: prototypeFieldName.replace(/__name__/g, fieldIndex)}); } }); }); </script> |
Notice the
{% do form.tags.setRendered %} added in the first line there. You will need this if you are using Symfony’s
{{ form_end(form) }} to end your form. The reason is that
form_end will output anything it thinks is missing from your form. It will believe that
'tags' are missing since we are actually creating the tag inputs ourselves. So we tell it that
form.tags are already rendered to prevent that.
Another option would be to just close the form yourself:
</form> . But don’t forget to render the CSRF token (or anything else your form requires) in that case.
The final result of your form might look something like this (using Bootstrap here):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
{# app/Resources/views/product/submitProduct.html.twig #} <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>New Product</title> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" rel="stylesheet"> <link href="https://code.jquery.com/ui/1.10.4/themes/start/jquery-ui.css" rel="stylesheet"> <link href="{{ asset('css/jquery.tagit.css') }}" rel="stylesheet"> <link href="{{ asset('css/tagit.ui-zendesk.css') }}" rel="stylesheet"> </head> <body> <div class="container"> <h2>New Product</h2> {{ form_start(form) }} <div class="row"> {# Product Name #} <div class="col-sm-3"> {{ form_label(form.name) }} {{ form_widget(form.name, { 'attr': { 'class': 'form-control' } }) }} {{ form_errors(form.name) }} </div> </div> <div class="row"> {# Tags #} <div class="col-sm-6"> {% do form.tags.setRendered %} {{ form_label(form.tags) }} <ul id="tags" data-prototype-field-name="{{ form.tags.vars.prototype.name.vars.full_name }}"></ul> {{ form_errors(form.tags) }} </div> </div> <div class="row"> {# Submit #} <div class="col-sm-12"> <button type="submit" class="btn btn-default">Submit</button> </div> </div> {{ form_end(form) }} </div> <script src="https://code.jquery.com/jquery-1.11.3.min.js"></script> <script src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script> <script src="{{ asset('js/tag-it.min.js') }}"></script> <script> $(document).ready(function () { var $tags = $('ul#tags'); var prototypeField = $tags.data('prototype-field-name'); var fieldIndex = 0; $tags.tagit({ fieldName: prototypeField.replace(/__name__/g, fieldIndex), beforeTagAdded: function (event, ui) { fieldIndex++; $(this).tagit({fieldName: prototypeField.replace(/__name__/g, fieldIndex)}); } }); }); </script> </body> </html> |
If you want to edit the tags of an existing Product, you could pass that Product to the view in your controller, and then output all of its tags inside the unordered list like so:
|
<ul id="tags" data-prototype-field-name="{{ form.tags.vars.prototype.name.vars.full_name }}"> {% if product is defined %} {% for tag in product.getTags() %} <li>{{ tag.name }}</li> {% endfor %} {% endif %} </ul> |
The final result is a setup which meets the requirements outlined in the first article of this tutorial.
I hope that this has given you some ideas on ways you can manage tags for your entities in a ManyToMany association in Symfony. If you have any questions or suggestions, please leave them in the comments!
This is Part 3 in a series on setting up Tags in a ManyToMany association with an Entity in Symfony. You can read the previous two articles here:
- Managing Tags in ManyToMany association with Symfony
- Setting up Form Collection for Tags in Symfony
In the last article, I talked about the problem of duplicate Tags being added when creating a new Product. Here, I will show one way to handle that using a Data Transformer.
First, we will create the data transformer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
// src/AppBundle/Form/DataTransformer/TagsToCollectionTransformer.php namespace AppBundle\Form\DataTransformer; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\DataTransformerInterface; use Doctrine\Common\Collections\ArrayCollection; class TagsToCollectionTransformer implements DataTransformerInterface { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } public function transform($tags) { return $tags; } public function reverseTransform($tags) { $tagCollection = new ArrayCollection(); $tagsRepository = $this->manager ->getRepository('AppBundle:Tag'); foreach ($tags as $tag) { $tagInRepo = $tagsRepository->findOneByName($tag->getName()); if ($tagInRepo !== null) { // Add tag from repository if found $tagCollection->add($tagInRepo); } else { // Otherwise add new tag $tagCollection->add($tag); } } return $tagCollection; } } |
Your data transformer implements the DataTransformerInterface, which requires these two methods:
|
public function transform($value); public function reverseTransform($value); |
transform allows you to alter the data going to your form. We don’t need to change this, so can just return the tags as is.
reverseTransform allows you to alter the data submitted by your form. This is where we want to make our changes.
In the constructor, the
ObjectManager is injected for checking a Tag’s existence in the database.
In the
reverseTransform method, an empty ArrayCollection is created to store all tags we’ll be returning. For each submitted Tag, it checks for its presence in the Tag repository. If present, we replace the form submitted Tag with the one already present in the Tag repository. If not, then we allow the new form submitted Tag into the
$tagCollection .
Finally, the
$tagCollection is returned.
You can do any other filtering you like there. But for the purposes of this tutorial, we’ll keep it simple like that.
The next thing we need to do is make a few changes to our ProductType:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
// src/AppBundle/Form/Type/ProductType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Doctrine\Common\Persistence\ObjectManager; use AppBundle\Form\DataTransformer\TagsToCollectionTransformer; class ProductType extends AbstractType { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name') ->add('tags', CollectionType::class, array( 'entry_type' => TagType::class, 'allow_add' => true, 'allow_delete' => true, 'required' => false )); // Data Transformer $builder ->get('tags') ->addModelTransformer(new TagsToCollectionTransformer($this->manager)); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Product', )); } } |
We’ll be using the
ObjectManager and our newly created
TagsToCollectionTransformer :
|
use Doctrine\Common\Persistence\ObjectManager; use AppBundle\Form\DataTransformer\TagsToCollectionTransformer; |
The
ObjectManager is injected in the constructor. (We’ll create a service for our ProductType class later)
We also get
'tags' from our form builder, add our transformer to it using the
addModelTransformer method, and pass in the
ObjectManager dependency to our
TagsToCollectionTransformer .
Finally, we create a service for our
ProductType class which injects the
ObjectManager dependency to it:
|
# app/config/services.yml services: app.form.type.product: class: AppBundle\Form\Type\ProductType arguments: ["@doctrine.orm.entity_manager"] tags: - { name: form.type } |
Our form is now ready to roll.
Let’s create a controller for submitting new Products:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
// src/AppBundle/Controller/ProductController.php namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Component\HttpFoundation\Request; use AppBundle\Entity\Product; use AppBundle\Form\Type\ProductType; class ProductController extends Controller { /** * @Route("/new/product", name="new_product") */ public function newProductAction(Request $request) { $product = new Product(); $form = $this->createForm(ProductType::class, $product); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($product); $em->flush(); } return $this->render('product/submitProduct.html.twig', array( 'form' => $form->createView() )); } } |
This should be pretty self-explanatory if you’re familiar with controllers, routes, form submissions, and persisting objects to the database in Symfony. Basically we just create our form with a new
Product object, and have the form handle the
Request. If a form was submitted, and is valid, we persist the Product to the database. Finally, we render the submitProduct template which will be created later.
Note that while it’s not actually required to check
$form->isSubmitted() , it is recommended per the Symfony best practices on form submissions for readability:
Second, we recommend using $form->isSubmitted()
in the if
statement for clarity. This isn’t technically needed, since isValid()
first calls isSubmitted()
. But without this, the flow doesn’t read well as it looks like the form is always processed (even on the GET request).
The last thing to do is create the Twig template submitProduct.html.twig which will display our form. Since we’ll be adding and removing Tags from Products, we’ll need some JavaScript to add/remove our
tag.name inputs for us. You could accomplish this using the example in the Symfony documentation, but in the next article, we’ll be using the jQuery Tag-it plugin instead.
Next article: How to use jQuery Tag-it plugin with a Symfony form
This is the second article in a series on setting up a ManyToMany association for tags in Symfony. You can read the previous article where we created our Product and Tag entities here:
- Managing Tags in ManyToMany association with Symfony
Now that our entities have been created, it’s time to create our forms. Basically, what we want here is a Product submission form where you can add/remove a collection of Tags which will also be associated to our Product. First, let’s create a Tag form which will be added to our Product form later to collect tags.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
// src/AppBundle/Form/Type/TagType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class TagType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name'); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Tag', )); } } |
We only need a
'name' for each tag.
Next, we’ll create the Product form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
// src/AppBundle/Form/Type/ProductType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\CollectionType; class ProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name') ->add('tags', CollectionType::class, array( 'entry_type' => TagType::class, 'allow_add' => true, 'allow_delete' => true, 'required' => false )); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Product', )); } } |
We add
'tags' here to the builder as a
CollectionType , and set
'entry_type' to
TagType::class . That is the TagType form class we created above in TagType.php, with ::class to get the fully qualified class name.
We set
'allow_add' and
'allow_delete' to
true because we want to be able to add and delete the associations of Tags to each Product.
Finally, I set
'required' => false , because I want adding Tags to be optional.
We haven’t gotten to the point of setting up anything in our Controller or templates yet, but I want to show you what happens now if we leave things the way they are so far in this example.
Currently, if you added one Product (BMW) with the following tags:
When you check your database, you would see this in your tables:
product
product_tag
tag:
Great. Now imagine adding another Product (Honda) with a car tag again, and see what happens:
Check your tables and you will find:
product
product_tag
tag
As you can see, it is adding duplicate Tags to our tables. However, rather than adding an unnecessary duplicate car tag (tag.id: 3), we want the Honda (product.id: 2) to be associated to the original car tag (tag.id: 1) in the product_tag table. So how can we accomplish this?
The method which I decided on was to setup a Data Transformer which checks if a tag already exists in the database. If so, it replaces the tag submitted by the form with the one already in the database. This way, the Product will be associated with the original tag in the database, and no new duplicate tag will be added.
I cover this in the next article: Using a Data Transformer in Symfony to handle duplicate tags
Symfony: v3.1.3
Doctrine: v2.5.4
One of the things I found challenging when first working with Symfony was form collections. The Symfony documentation on how to embed a collection of forms as well as how to work with Doctrine associations / relations is a great place to get started. However, checking out other use cases people shared online really helped as well, so I’m going to share another example here which I hope might help others.
For the following example, I will be setting up a Product entity, and a Tag entity. One Product can have multiple Tags, and one Tag can have multiple Products (Many To Many). Here’s how I want it to work:
- New Tags can be added when adding/editing a Product.
- No duplicate Tags should be added to the tag table.
- If this Product is deleted later, any Tag which was added with it should remain in the database.
- Use the jQuery Tag-it plugin to add the tags to our form for submission.
Ok, let’s get started with our Product entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Product { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Tag", inversedBy="products", cascade={"persist"}) */ protected $tags; } |
Then, the Tag entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
// src/AppBundle/Entity/Tag.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Tag { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Product", mappedBy="tags") */ protected $products; } |
A quick note about this line in my Product.php file:
@ORM\ManyToMany(targetEntity="Tag", inversedBy="products", cascade={"persist"})
I decided to pick Product as the owning side in this association, as Tags will only be created when a Product is added or edited. As you can see in the Doctrine documentation you can choose the owning side in a ManyToMany association. You do so by setting “inversedBy” to it, while setting “mappedBy” to the inverse side.
targetEntity is the class which is your target:
Tag
inversedBy is the property used in the Tag entity for Products (line 27 in Tag.php):
products
cascade={"persist"} will automatically persist any Tags associated to this Product.
As far as the Tag.php file goes, you set
Product as its
targetEntity , and since Tag will be the inverse side in this association, you set
mappedBy to the property in the Product entity used for Tags (line 27 in Product.php):
tags
With that setup, you can run this to generate your getters and setters:
$ php bin/console doctrine:generate:entities AppBundle
The final result of both entities should look like this:
Product.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
|
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Product { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Tag", inversedBy="products", cascade={"persist"}) */ protected $tags; /** * Constructor */ public function __construct() { $this->tags = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set name * * @param string $name * * @return Product */ public function setName($name) { $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Add tag * * @param \AppBundle\Entity\Tag $tag * * @return Product */ public function addTag(\AppBundle\Entity\Tag $tag) { $this->tags[] = $tag; return $this; } /** * Remove tag * * @param \AppBundle\Entity\Tag $tag */ public function removeTag(\AppBundle\Entity\Tag $tag) { $this->tags->removeElement($tag); } /** * Get tags * * @return \Doctrine\Common\Collections\Collection */ public function getTags() { return $this->tags; } } |
Tag.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
|
// src/AppBundle/Entity/Tag.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Tag { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Product", mappedBy="tags") */ protected $products; /** * Constructor */ public function __construct() { $this->products = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set name * * @param string $name * * @return Tag */ public function setName($name) { $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Add product * * @param \AppBundle\Entity\Product $product * * @return Tag */ public function addProduct(\AppBundle\Entity\Product $product) { $this->products[] = $product; return $this; } /** * Remove product * * @param \AppBundle\Entity\Product $product */ public function removeProduct(\AppBundle\Entity\Product $product) { $this->products->removeElement($product); } /** * Get products * * @return \Doctrine\Common\Collections\Collection */ public function getProducts() { return $this->products; } } |
Note that both the
$tags and
$products properties need to be set as an ArrayCollection in the constructor. This is explained in the Symfony documentation linked to in the beginning of this article on form collections.
Last but not least, we need to update our database by running:
$ php bin/console doctrine:schema:update –force
If all goes well, then you should see a friendly little message like this afterwards:
You should now have 3 tables added to your database:
We will setup our forms in the next article: Setting up Form Collection for Tags in Symfony