Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Field: configureOptions() & Allowing Empty Input

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Thanks 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.

configureOptions(): Defining Field Options / Default

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!

Fixing Empty Value Case in the Data Transformer

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.

Leave a comment!

15
Login or Register to join the conversation

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?

1 Reply

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!

1 Reply

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.

Reply

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!

1 Reply

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?

Reply

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!

Reply
Oscar Z. Avatar

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?

Reply

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!

1 Reply
Farshad Avatar
Farshad Avatar Farshad | posted 1 year ago

What's the difference between if (null === $value) and if (!$value) ?

Reply

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!

1 Reply
Simon L. Avatar
Simon L. Avatar Simon L. | posted 1 year ago

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?

Reply

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!

2 Reply
Simon L. Avatar

Thanks for your reply Victor :)

Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 2 years ago

Very good and clear videos about those custom types.

Reply

Hey Coder,

Thank you for your feedback! We are happy to hear it's clear and easy to understand :)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}