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 SubscribeThanks to our data transformer - specifically the fact that it throws a TransformationFailedException
when a bad email is entered - our UserSelectTextType
has some built-in sanity validation!
But, the message we passed to the exception is not what's shown to the user. That's just internal. To control the message, well, we already know the answer! Add an invalid_message
option when we create the field.
Or... instead of configuring that option when we're adding the specific field, we can give this option a default value for our custom field type. Open UserSelectTextType
, go back to the Code -> Generate menu, or Command + N on a Mac, and this time, override configureOptions()
. Inside, add $resolver->setDefaults()
and give the invalid_message
option a different default: "User not found".
... lines 1 - 11 | |
class UserSelectTextType extends AbstractType | |
{ | |
... lines 14 - 30 | |
public function configureOptions(OptionsResolver $resolver) | |
{ | |
$resolver->setDefaults([ | |
'invalid_message' => 'Hmm, user not found!', | |
]); | |
} | |
} |
Try that out! Go back, refresh and... very nice!
And hey! We've seen this configureOptions()
method before inside our normal form classes! When you're building an entire form, configureOptions()
is used to set some options on your... whole form. There aren't very many common things to configure at that level.
But when you're creating a custom field type: configureOptions()
is used to set the options for that specific field. We've just changed the default value for the invalid_message
option. The cool thing is that this can still be overridden if we want: we could add an invalid_message
option to the author field and it would win!
I want to talk more about field options because they can unlock some serious possibilities. But first, there is a teenie, tiny bug with our data transformer. Clear out the author
text box and try to submit. Duh - disable HTML5 validation by adding the novalidate
attribute. Hit update!
Oh! Our sanity validation still fails: User not found. That's not quite what we want. Instead of failing, our data transformer should probably just return null
.
Go back to EmailToUserTransformer
. In reverseTransform()
, if $value
is empty, just return
. So, if the field is submitted empty, null
should be passed to setAuthor()
.
But, hmm... the problem now is that, while it's technically ok to call setAuthor()
with a null
argument, we want that field to be required!
Re-submit the form! Oof - an integrity constraint violation: it's trying to save to the database with null
set as the author_id
column. We purposely made this required in the database and this is a great example of... messing up! We forget to add an important piece of business validation: to make the author
required. No worries: open the Article
class, find the $author
field and, above it, add @Assert\NotNull()
. Give it a message: Please set an author
.
... lines 1 - 17 | |
class Article | |
{ | |
... lines 20 - 71 | |
/** | |
... lines 73 - 74 | |
* @Assert\NotNull(message="Please set an author") | |
*/ | |
private $author; | |
... lines 78 - 269 | |
} |
Try that again. Excellent! This is the behavior - and error - we expect.
Next: how could we make our custom field type behave differently if it was used in different forms? Like, what if in one form, we want the user to be able to enter any user's email address but in another form we only want to allow the user to enter the email address of an admin user. Let's learn more about the power of form field options.
Hey Coder,
Thank you for your feedback! We are happy to hear it's clear and easy to understand :)
Cheers!
Hi! When we make a new form, we create some *FormType class which derives from the AbstractType class. And when we want to customise some field we also need to extend AbstractType. This looks like a mess (for now). A whole form vs just a single field. What's the logic behind this?
Hey Yuri,
You could write that code in the same (AtriclyType) form class, but separate that logic allows you to reuse that UserSelectTextType in other forms, and just makes code clearer: now it's 2 small classes instead of one big that holds all the mixed logic inside. But yeah, I see your point. It's simple when you have simple tasks, but complicates and require more work when your tasks become more complex. But this approach is flexible and allows you to build really complex forms that might be nested to each other and where some parts could be re-used in other forms.
Cheers!
Hi Victor, thank you for your response. Separation is great, no problem with that. But I was confused that the form and the form element are represented by the identical class. Intuitively the whole form and just the single element are very different "entities". Well, maybe I should use it in practice for some time to get the idea.
Hey Yuri,
Yeah, I see your point, but that's the one of many things that makes Symfony Forms so powerful. With this implementation your form behaves as a nested tree, and thanks to this you can make more complex and nested forms, as you can easily put one form into another form, inside another form and so on :) Yeah, definitely, the more practice - the better understanding. Also, go with Symfony Docs about the Form component will helps to understand it better and cover blind spots.
Cheers!
Hi, thanks for the video. I have a question tho. :)
Is there a general rule on where the form validation should be? It is shown that when a user is not found you can implement that with an exception in the data transformer, while if the input is empty the validation is done on the entity proper via `@Assert\NotNull()`. I would assume it is technically possible to also add an exception to the data transformer to check for the input not being null, but would that be a bad practice?
Hey RobinBastiaan!
That's a very smart question :). The short answer is that, yes, you could really put all of your validation rules into a data transformer. I'm not saying you should, but you definitely *could* do this. You wouldn't be able to customize the error the users sees based on different wrong values (e.g. "this field is required" vs "this field needs more characters"), but it would work. One other difference is that, if you fail in a data transformer, the submitted value is never set back onto your object.
Anyways, the general rule for where each should live comes down to what I call "sanity validation" vs "business rules" validation. Entering the word "apple" for a NumberType::class field is an "insane" value: there is no sane way for us to take "apple" and find a "number" that we could set back onto your object. And so, we fail. The same is true for a ChoiceType::class, if the user (it would require some HTML hacking) submitted a value that is *not* in the choice list. That is simply an "insane" value.
The idea is: if a value is at least "sane", then allow it to be set onto your object. And THEN use the normal validation. The normal validation has all those nice, built-in validators anyways, and you can customize different errors per failure type.
Let me know if this helps!
Cheers!
Why are we allowing to enter a null author value in the entity when it is set as not nullable in the database? Wouldn't null be an "insane" value for author in that case if it will eventually throw an exception when saved to the database?
Hey Oscar Z.!
Hmm, I see your point! I guess where the "is this insane" line is drawn is in PHP itself. What I mean is: in PHP, for example, it is "insane" to set an "int" property to a string. But, it's not considered "insane" to set null to a property that allows null (in PHP), even though it would explode in the database if you actually saved it that way. So, I hear what you're saying... this is more of an explanation of the thinking behind this :).
Of course, this whole philosophy assumes that you are "ok" with your entity objects being in an "invalid" state temporarily: (A) the form sets some data into your object... including data you may consider invalid and then (B) that data is validated. There is a minority (but decent-sized group of people) who don't like this, and instead create dedicated DTO objects that they bind to the forms (instead of entities). They allow these DTOs to be "invalid" and have validation applied to them. Then, they do some extra work (after validation is successful) to "transfer" the data from the DTO onto the entity. The advantage is that your entity can always be in a "valid" state. I don't personally use this approach, as it requires a good deal more work, but it is totally valid.
Cheers!
Hey Farry7,
With "null === $value" you have strict (Identical) comparison, when check both values and types. With "!$value" the value of the variable is. converted to boolean first, and then you revert boolean value to the opposite. See more info and examples in PHP docs: https://www.php.net/manual/...
Cheers!
Hi there !
In the tutorials, you usually use the function sprintf in the Exception error message, instead of just passing a string (which can be concatenate with a variable).
Is there any reason for that?
Hey Stileex,
It's just a matter of taste ;) Well, sprintf() came from another languages where usually it's the standard way of putting some dynamic vars into a string. Yes, PHP has concatenation so you can totally use it instead. Or, you can even put vars directly in the string with double quotes - it should work as well. But depends on the length of your variables, and if you need to call some getters on the objects, etc. - it might be more readable to use sprintf(). So, it depends, but it's totally up to you. There might be some performance improvements using one or another option... but those should be too minor to think about.
I hope this helps!
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.1
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.6
"symfony/console": "^4.0", // v4.1.6
"symfony/flex": "^1.0", // v1.17.6
"symfony/form": "^4.0", // v4.1.6
"symfony/framework-bundle": "^4.0", // v4.1.6
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.6
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.6
"symfony/validator": "^4.0", // v4.1.6
"symfony/web-server-bundle": "^4.0", // v4.1.6
"symfony/yaml": "^4.0", // v4.1.6
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
"symfony/dotenv": "^4.0", // v4.1.6
"symfony/maker-bundle": "^1.0", // v1.8.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.6
}
}
Very good and clear videos about those custom types.