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 SubscribeGo 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.
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.
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.
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 <input type="text">
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!
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?
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!
// 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
}
}
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->...
`
Only this works correctly and there is no firstName at all or something connected to the UserEntity.
What I've missed?