Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Field Type

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

Go back to /admin/article/new and click to create a new article. Oh, duh! We're not logged in as an admin anymore. Log out, then log back in with admin2@thespacebar.com password engage. Cool. Try /admin/article/new again.

Now, open ArticleFormType so we can take a closer look at the field types. Right now, we're using TextType, this is TextareaType, this is a DateTimeType and the author drop-down is an EntityType. We learned earlier that the purpose of each field type is really two things. First: it controls how the field is rendered, like <input type="text">, <textarea>, <input type="datetime-local"> or a <select> drop down. The second purpose of a field type is more important: it determines how the field's data is transformed.

For example, the publishedAt field has a nice date widget that was added by my browser. But, really, this is just an input text field. What I mean is: the data from this field is submitted as a raw text string. But ultimately, on my Article entity, the setPublishedAt method requires a DateTime object! That's the job of the DateTimeType: to convert that specially-formatted date string into a DateTime object.

And just as important, it also transforms the other direction. Go to the list page and click to edit an existing, published article. Inspect the published at field. Yep! When the form loaded, the DateTimeType took the DateTime object from the Article and transformed it back into the string format that's used for the value attribute.

Custom Field for Author

Why are we talking about this? Because I want to completely replace this author dropdown, to avoid a future problem. Imagine if we had 10,000 users. Hmm, in that case, it wouldn't be very easy to find the person we want - that would be a big drop-down! Plus, querying for 10,000 users and rendering them would be pretty slow!

So, new plan: I want to convert this into a text field where I can type the author's email. That's... easy! We could use EmailType for that! But, there's a catch: when we submit, we need to create a data transformer that's able to take that email address string and query for the User object. Because, ultimately, when the form calls setAuthor(), the value needs to be a User object.

Creating the Custom Form Type

To do all of this, we're going to create our first, custom form field type. Oh, and it's really cool: it looks almost identical to the normal form classes that we've already been building.

Create a new class: let's call it UserSelectTextType. Make it extend that same AbstractType that we've been extending in our other form classes. Then, go to the Code + Generate menu, or Command + N on a Mac, and select override methods. But this time, instead of overriding buildForm(), override getParent(). Inside, return TextType::class. Well, actually, EmailType::class might be better: it will make it render as an <input type="email">, but either will work fine.

... lines 1 - 7
class UserSelectTextType extends AbstractType
{
public function getParent()
{
return TextType::class;
}
}

Internally, the form fields have an inheritance system. For not-too-interesting technical reasons, the form classes don't use real class inheritance - we don't literally extend the TextType class. But, it works in a similar way.

By saying that TextType is our parent, we're saying that, unless we say otherwise, we want this field to look and behave like a normal TextType.

And... yea! We're basically set up. We're not doing anything special yet, but this should work! Go back over to ArticleFormType. Remove all of this EntityType stuff and say UserSelectTextType::class.

Let's try it! Move over, refresh and... it actually works! It's a text field filled with the firstName of the current author.

But... it only works thanks to some luck. When this field is rendered, the author field is a User object. The <input type="text"> field needs a string that it can use for its value attribute. By chance, our User class has a __toString() method. And so, we get the first name!

But check this out: when we submit! Big, hairy, giant error:

Expected argument of type User or null, string given

When that first name string is submitted, the TextType has no data transformer. And so, the form system ultimately calls setAuthor() and tries to pass it the string first name!

We'll fix this next with a data transformer.

Leave a comment!

5
Login or Register to join the conversation
Default user avatar
Default user avatar MASQUERADE promotion | posted 4 years ago | edited

Hey,
How "Let's try it! Move over, refresh and... it actually works! It's a text field filled with the firstName of the current author." if in case of deleting the whole EntityType stuff we've have this:
`
$builder->...

        ->add('author', UserSelectTextType::class, [
            'invalid_message' => 'Invalid data type has been pushed.'
        ]);

`
Only this works correctly and there is no firstName at all or something connected to the UserEntity.
What I've missed?

1 Reply

Hey MASQUERADE promotion!

Great question. Here is what happens behind the scenes at this exact moment:

1) Symfony starts rendering the "author" field
2) It sees that the Article.author field is populated by a User object
3) It then looks at the UserSelectTextType and sees that its parent is TextType::class. That means it should render as an input type="text" field
4) It renders the &lt;input type="text"&gt; field. For the value attribute, it literally just tries to print the author field - so it literally tries to print the User object that is on that field. Thanks to the __toString() method on the User object, instead of an error, the firstName is printed.

So, you're right - the form does not have ANY configuration yet to connect it to the User entity. BUT, the Article.author property IS a User object, and so the form tries to render using that value.

I hope this helps!

Cheers!

Reply
Default user avatar
Default user avatar MASQUERADE promotion | weaverryan | posted 4 years ago

ok. thanks. Just didn't have that written, while had the __toString() and then wrote you.

Reply
Default user avatar
Default user avatar Wazir Khan | posted 4 years ago

I am facing an unknown issue here, I create the custom type same as in the tutorial, User class already have the __toString method, I changed the ArticleFormType's author field as per instructions, my form rendering correctly, but with no value (author firstname is not there), I checked that value in the custom transformer, null was passed there. If I remove the custom type, and leave it to symfony to guess the type, then it renders correctly as dropdown with all user-names, but even if I changed it to TextType, it doesn't show anything (just empty field).

What could be the reason?

Reply

Hey Wazir Khan!

Sorry for my super slow reply! Hmm... what does your __toString() method look like? And what does your ArticleFormType look like? I'm looking at a few things... and they all point to this being some, small subtle typo somewhere in your code. It could be in __toString()... though I'm pretty sure that when you let Symfony "guess" the type, it would also use that (but it's actually showing the usernames? That's curious). Anyways, if you're still having the issue, post some code here and we'll get to the bottom of it.

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