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.
// 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.9.10
"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
}
}