If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe've got more work to do! So head back to /admin/genus
. Leave the "Years Studied" field empty for one of the GenusScientist
forms and submit.
Explosion!
UPDATE genus_scientist SET years_studied = NULL
This field is not allowed to be null in the database. That's on purpose... but we're missing validation! Lame!
But no problem, right? We'll just go into the Genus
class, copy the as Assert
use statement, paste it into GenusScientist
and then - above yearsStudied
- add @Assert\NotBlank
:
... lines 1 - 5 | |
use Symfony\Component\Validator\Constraints as Assert; | |
... lines 7 - 11 | |
class GenusScientist | |
{ | |
... lines 14 - 32 | |
/** | |
... line 34 | |
* @Assert\NotBlank() | |
*/ | |
private $yearsStudied; | |
... lines 38 - 72 | |
} |
Cool! Now, the yearsStudied
field will be required.
Go try it out: refresh the page, empty out the field again, submit and... What!? It still doesn't work!?
It's as if Symfony doesn't see the new validation constraint! Why? Here's the deal: our form is bound to a Genus
object:
... lines 1 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\OneToMany( | |
* targetEntity="GenusScientist", | |
* mappedBy="genus", | |
* fetch="EXTRA_LAZY", | |
* orphanRemoval=true, | |
* cascade={"persist"} | |
* ) | |
*/ | |
private $genusScientists; | |
... lines 82 - 208 | |
} |
That's the top-level object that we're modifying. And by default, Symfony reads all of the validation annotations from the top-level class... only. When it sees an embedded object, or an array of embedded objects, like the genusScientists
property, it does not go deeper and read the annotations from the GenusScientist
class. In other words, Symfony only validates the top-level object.
Double-lame! What the heck Symfony?
No no, it's cool, it's on purpose. You can easily activate embedded validation by adding a unique annotation above that property: @Assert\Valid
:
... lines 1 - 6 | |
use Symfony\Component\Validator\Constraints as Assert; | |
... lines 8 - 14 | |
class Genus | |
{ | |
... lines 17 - 71 | |
/** | |
* @ORM\OneToMany( | |
* targetEntity="GenusScientist", | |
* mappedBy="genus", | |
* fetch="EXTRA_LAZY", | |
* orphanRemoval=true, | |
* cascade={"persist"} | |
* ) | |
* @Assert\Valid() | |
*/ | |
private $genusScientists; | |
... lines 83 - 209 | |
} |
That's it! Now refresh. Validation achieved!
But there's one other problem. I know, I always have bad news. Set one of the users to aquanaut3. Well, that's actually a duplicate of this one... and it doesn't really make sense to have the same user listed as two different scientists. Whatever! Save right now: it's all good: aquanaut3 and aquanaut3. I want validation to prevent this!
No problem! In GenusScientist
add a new annotation above the class: yep, a rare constraint that goes above the class instead of a property: @UniqueEntity
. Make sure to auto-complete that to get a special use
statement for this:
... lines 1 - 5 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
... lines 7 - 8 | |
/** | |
... lines 10 - 11 | |
* @UniqueEntity( | |
... lines 13 - 14 | |
* ) | |
*/ | |
class GenusScientist | |
{ | |
... lines 19 - 77 | |
} |
This takes a few options, like fields={"genus", "user"}
:
... lines 1 - 5 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
... lines 7 - 8 | |
/** | |
... lines 10 - 11 | |
* @UniqueEntity( | |
* fields={"genus", "user"}, | |
... line 14 | |
* ) | |
*/ | |
class GenusScientist | |
{ | |
... lines 19 - 77 | |
} |
This says:
Don't allow there to be two records in the database that have the same genus and user.
Add a nice message, like:
This user is already studying this genus.
... lines 1 - 5 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
... lines 7 - 8 | |
/** | |
... lines 10 - 11 | |
* @UniqueEntity( | |
* fields={"genus", "user"}, | |
* message="This user is already studying this genus" | |
* ) | |
*/ | |
class GenusScientist | |
{ | |
... lines 19 - 77 | |
} |
Great!
Ok, try this bad boy! We already have duplicates, so just hit save. Validation error achieved! But... huh... there are two errors and they're listed at the top of the form, instead of next to the offending fields.
First, ignore the two messages - that's simply because we allowed our app to get into an invalid state and then added validation. That confused Symfony. Sorry! You'll normally only see one message.
But, having the error message way up on top... that sucks! The reason why this happens is honestly a little bit complex: it has to do with the CollectionType
and something called error_bubbling
. The more important thing is the fix: after the message
option, add another called errorPath
set to user
:
... lines 1 - 5 | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
... lines 7 - 8 | |
/** | |
... lines 10 - 11 | |
* @UniqueEntity( | |
* fields={"genus", "user"}, | |
* message="This user is already studying this genus", | |
* errorPath="user" | |
* ) | |
*/ | |
class GenusScientist | |
{ | |
... lines 20 - 78 | |
} |
In a non embedded form, the validation error message from UniqueEntity
normally shows at the top of the form... which makes a lot of sense in that situation. But when you add this option, it says:
Yo! When this error occurs, I want you to attach it to the user field.
So refresh! Error is in place! And actually, let me get us out of the invalid state: I want to reset my database to not have any duplicates to start. Now if we change one back to a duplicate, it looks great... and we don't have two errors anymore.
There is one small bug left with our validation! And it's tricky! To see it: add 2 new scientists, immediately remove the first, leave the yearsStudied
field blank, and then submit. We should see a validation error appearing below the yearsStudied
field. Instead, it appears no the top of the form! This is actually caused by a bug in Symfony, but we can fix it easily! The following code block shows the fix and has more details:
... lines 1 - 14 | |
use Symfony\Component\Form\FormEvent; | |
use Symfony\Component\Form\FormEvents; | |
... lines 17 - 20 | |
class GenusFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
... lines 25 - 56 | |
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit')); | |
} | |
... lines 59 - 66 | |
/** | |
* This fixes a validation issue with the Collection. Suppose | |
* the following situation: | |
* | |
* A) Edit a Genus | |
* B) Add 2 new scientists - don't submit & leave all fields blank | |
* C) Delete the FIRST scientist | |
* D) Submit the form | |
* | |
* The one new scientist has a validation error, because | |
* the yearsStudied field was left blank. But, this error | |
* shows at the *top* of the form, not attached to the form. | |
* The reason is that, on submit, addGenusScientist() is | |
* called, and the new scientist is added to the next available | |
* index (so, if the Genus previously had 2 scientists, the | |
* new GenusScientist is added to the "2" index). However, | |
* in the HTML before the form was submitted, the index used | |
* in the name attribute of the fields for the new scientist | |
* was *3*: 0 & 1 were used for the existing scientists and 2 was | |
* used for the first genus scientist form that you added | |
* (and then later deleted). This mis-match confuses the validator, | |
* which thinks there is an error on genusScientists[2].yearsStudied, | |
* and fails to map that to the genusScientists[3].yearsStudied | |
* field. | |
* | |
* Phew! It's a big pain :). Below, we fix it! On submit, | |
* we simply re-index the submitted data before it's bound | |
* to the form. The submitted genusScientists data, which | |
* previously had index 0, 1 and 3, will now have indexes | |
* 0, 1 and 2. And these indexes will match the indexes | |
* that they have on the Genus.genusScientists property. | |
* | |
* @param FormEvent $event | |
*/ | |
public function onPreSubmit(FormEvent $event) | |
{ | |
$data = $event->getData(); | |
$data['genusScientists'] = array_values($data['genusScientists']); | |
$event->setData($data); | |
} | |
} |
I just found out this:
This constraint cannot deal with duplicates found in a collection of items
that haven't been persisted as entities yet. You'll need to create your own
validator to handle that case.
So I guess custom validation is the way to go; Any hints/links on this one are greatly welcome :)
Hey christosp
That constraint comes shipped with Symfony since version 4.3. Give it a check: https://symfony.com/blog/ne...
Cheers!
Hi,
I am trying to apply same structure to the similar project. However I came to an issue using @UniqueEntity. The issue arises when I have more than 1 scientist working on genus, then remove the first scientist (lets say aquanaut1@example.org), and then add it again as the last one. I can see that removeGenusScientist function is called and later addGenusScientist function is called, but I still get an "This user is already studying this genus" error under aquanaut1@example.org. How to fix this bug?
Thank you in advance!
Hey Justas!
If I understand correctly, you added 2 scientists to the Genus *before* configuring the UniqueEntity validation. And then, after adding UniqueEntity, you get this weird behavior. Is that correct? If so, I've seen this as well - I think because you're *starting* with your system in an invalid state (a state that would never be allowed if UniqueEntity were used), UniqueEntity gets confused. In the real world, you should of course always have UniqueEntity from the beginning, so you won't fall into this situation. But yea, I think there *is* some weird behavior if your system starts in this invalid state. How to fix it? Ideally, you don't need to :)... because you will use UniqueEntity to never allow this. But if you have an existing database with duplications and are now adding UniqueEntity, then you can either (A) run some sort of database migration to remove the duplicates or (B) create your own custom constraint instead of using UniqueEntity - I think UniqueEntity will always act in this way.
Cheers!
Hi Ryan,
The problem is not caused by invalid state. You can remove all the scientists, save. Then add scientist1 and scientist2, save. Then when you edit the form again (you will see scientist1 and scientist2), remove the first scientist, without saving add a new one and select scientist1 again. In the form you will see scientist2 and scientist1, then try to save. It will not let you save the form saying that scientist1 is already working on the genus. Basically if you switch the order of the scientist in the collection the unique error appears.
Hey Justas!
Ah, I see! And I can repeat the issue. Here's the problem:
Suppose you start with 2 scientists: scientist1 (e.g. GenusScientist id 5) and scientist2 (GenusScientist id 6). But, you delete scientist1 from the form and re-add him (just like you described). When you submit:
1) the UniqueEntity constraint queries the database *two* times: once to see if there is already a GenusScientist with the same user+genus as scientist1 and once to see if there is already a GenusScientist with the same user+genus as scientist2. In both cases, a result is returned: GenusScientist id 5 for "scientist1" and GenusScientist id 6 for "scientist 2"
2) the UniqueEntity constraint then checks to see if the queried GenusScientist matches exactly (===) the object that it was checking again... because if there was only 1 match found... it might be *this* GenusScientist, so it shouldn't be a duplicate. For "scientist 2", it compares GenusScientist id 6 with the one from the query, GenusScientist id 6. These are ===, so it does not create a validation constraint. BUT, for "scientist 1", it compares two GenusScientist objects that are *almost* (but not quite) identical. They have all the same fields... except for "id": the one from the database of course has id 6, but the one that was just submitted has a *null* id - because this was just created by our form system. Hence, a validation error is added.
So, that's the problem :/. This issue might be related: https://github.com/symfony/.... How can we fix it? Ideally, we'd like our form to NOT create a *new* GenusScientist when one is simply removed and then re-added. The only way I can think to do this, however, would be to add some extra JS code that when the user creates a *new* scientist and assigns a user that was previously in the form, you actually dynamically update its "name" index to match the original one. In the above example, "scientist1" would originally have index 0. When you re-add it, it would have index 2. You would need to detect when the user drop-down is changed to "scientist1" and dynamically reset the index to 0. Then, when you submit, it will not look like a *new* entity anymore. Of course, that's a pretty ugly solution :). The only other alternative I can think of is to not use the UniqueEntity validator and write your own - something that looked at the "genusScientists" property directly, and checked the entire array to see if there were duplicates, before saving.
I'm not sure there's a bug in the UniqueEntity validator... but certainly because of the way it's all setup, it just doesn't have enough knowledge to get things "perfect" for our situation.
Let me know if that helps! Cheers!
And actually, a good way to do that custom constraint might just be to do a Callback constraint on the Genus entity - you could easily loop through genusScientists to see if there are any duplicates. If that works (seems like it should), that would be a super simple solution!
Hi,
I'm using Symfony 3.2.6. The problem of Collection Type is Error message on the top which may be fixed in this version. Is it right ?
Hey TamNC!
Are you talking about the UniqueEntity error ? if that the case, remember to set up the "errorPath" option to the user field, so it will appear inside the user input field.
Have a nice day!
After adding the Bug fix in my code, it shows this error instead:
"Catchable Fatal Error: Argument 1 passed to AppBundle\Form\GenusFormType::onPreSubmit() must be an instance of AppBundle\Form\FormEvent, instance of Symfony\Component\Form\FormEvent given"
any similar encounter?
Hey Louis,
You have a good tip in this error message: you just have to use the "Symfony\Component\Form\FormEvent" namespace in the GenusFormType class. ;)
Cheers!
Hi, I came accross this course while searching for a solution for my "edge case".
@Assert\Valid() at the parent object works if the validation on the embedded object is static - I mean the validation is for every embedded object the same.
I'm looking for a solution to validate the embedded objects depending on a single value of each embedded object.
E.g. the embedded object (or entity) consists of a type field, address fields (city, zip, street) and a description field. Depending on the type either the address fields are mandatory or the description field is mandatory.
I tried it via closure at the embedded form, but as you note within this course, Symfony reads only the annotation validation from the top level class.
Hey Marcus
In that case you will have to define your constraints directly in every parent FormType, so can customize them as you need, something like this:
// some FormType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('someField', EmbeddedFormType::class, [
'constraints' => [
new NotBlank(),
// more constraints specific to this FormType
]
]
}
if you need to do more logic, you may want to take a look at Form Events: https://symfony.com/doc/current/form/events.html
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"symfony/symfony": "3.4.*", // v3.4.49
"doctrine/orm": "^2.5", // 2.7.5
"doctrine/doctrine-bundle": "^1.6", // 1.12.13
"doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.7
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.23.0
"sensio/distribution-bundle": "^5.0", // v5.0.25
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
"incenteev/composer-parameter-handler": "^2.0", // v2.1.4
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
"stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.7
"symfony/phpunit-bridge": "^3.0", // v3.4.47
"nelmio/alice": "^2.1", // v2.3.6
"doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
}
}
Hi guys, not sure you are dealing with in another chapter/course but the UniqueEntity validation does not work on the "new genus" page. Is this covered somewhere and I might have missed it?
Thanks!