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 SubscribeWe built a custom field type called UserSelectTextType
and we're already using it for the author
field. That's cool, except, thanks to getParent()
, it's really just a TextType
in disguise!
Internally, TextType
basically has no data transformer: it takes whatever value is on the object and tries to print it as the value
for the HTML input! For the author
field, it means that it's trying to echo that property's value: an entire User
object! Thanks to the __toString()
method in that class, this prints the first name.
Let's remove that and see what happens. Refresh! Woohoo! A big ol' error:
Object of class User could not be converted to string
More importantly, even if we put this back, yes, the form would render. But when we submitted it, we would just get a different huge error: the form would try to take the submitted string and pass that to setAuthor()
.
To fix this, our field needs a data transformer: something that's capable of taking the User
object and rendering its email
field. And on submit, transforming that email
string back into a User
object.
Here's how it works: in the Form/
directory, create a new DataTransformer/
directory, but, as usual, the location of the new class won't matter. Then add a new class: EmailToUserTransformer
.
The only rule for a data transformer is that it needs to implement a DataTransformerInterface
. I'll go to the Code -> Generate menu, or Command+N on a Mac, select "Implement Methods" and choose the two from that interface.
I love data transformers! Let's add some debug code in each method so we can see when they're called and what this value looks like. So dd('transform', $value)
and dd('reverse transform', $value)
.
... lines 1 - 7 | |
class EmailToUserTransformer implements DataTransformerInterface | |
{ | |
public function transform($value) | |
{ | |
dd('transform', $value); | |
} | |
public function reverseTransform($value) | |
{ | |
dd('reverse transform', $value); | |
} | |
} |
To make UserSelectTextType
use this, head back to that class, go to the Code -> Generate menu again, or Command + N on a Mac, and override one more method: buildForm()
.
Hey! We know this method! This is is the method that we override in our normal form type classes: it's where we add the fields! It turns out that there are a few other things that you can do with this $builder
object: one of them is $builder->addModelTransformer()
. Pass this a new EmailToUserTransformer()
.
... lines 1 - 9 | |
class UserSelectTextType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder->addModelTransformer(new EmailToUserTransformer()); | |
} | |
... lines 16 - 20 | |
} |
Let's try it! I'll hit enter on the URL in my browser to re-render the form with a GET request. And... boom! We hit the transform()
method! And the value is our User
object.
This is awesome! That's the whole point of transform()
! This method is called. when the form is rendering: it takes the raw data for a field - in our case the User
object that lives on the author
property - and our job is to transform that into a representation that can be used for the form field. In other words, the email
string.
First, if null is the value, just return an empty string. Next, let's add a sanity check: if (!$value instanceof User)
, then we, the developer, are trying to do something crazy. Throw a new LogicException()
that says:
The
UserSelectTextType
can only be used with User objects.
Finally, at the bottom, so nice, return $value
- which we now know is a User
object ->getEmail()
.
... lines 1 - 8 | |
class EmailToUserTransformer implements DataTransformerInterface | |
{ | |
public function transform($value) | |
{ | |
if (null === $value) { | |
return ''; | |
} | |
if (!$value instanceof User) { | |
throw new \LogicException('The UserSelectTextType can only be used with User objects'); | |
} | |
return $value->getEmail(); | |
} | |
... lines 23 - 27 | |
} |
Let's rock! Move over, refresh and.... hello email address!
Now, let's submit this. Boom! This time, we hit reverseTransform()
and its data is the literal string email address. Our job is to use that to query for a User
object and return it. And to do that, this class needs our UserRepository
.
Time for some dependency injection! Add a constructor with UserRepository $userRepository
. I'll hit alt+enter and select "Initialize Fields" to create that property and set it.
... lines 1 - 9 | |
class EmailToUserTransformer implements DataTransformerInterface | |
{ | |
private $userRepository; | |
public function __construct(UserRepository $userRepository) | |
{ | |
$this->userRepository = $userRepository; | |
} | |
... lines 18 - 41 | |
} |
Normally... that's all we would need to do: we could instantly use that property below. But... this object is not instantiated by Symfony's container. So, we don't get our cool autowiring magic. Nope, in this case, we are creating this object ourselves! And so, we are responsible for passing it whatever it needs.
It's no big deal, but, we do have some more work. In the field type class, add an identical __construct()
method with the same UserRepository
argument. Hit Alt+Enter again to initialize that field. The form type classes are services, so autowiring will work here.
... lines 1 - 10 | |
class UserSelectTextType extends AbstractType | |
{ | |
private $userRepository; | |
public function __construct(UserRepository $userRepository) | |
{ | |
$this->userRepository = $userRepository; | |
} | |
... lines 19 - 28 | |
} |
Thanks to that, in buildForm()
pass $this->userRepository
manually into EmailToUserTransformer
.
... lines 1 - 19 | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder->addModelTransformer(new EmailToUserTransformer($this->userRepository)); | |
} | |
... lines 24 - 30 |
Back in reverseTransform()
, let's get to work: $user = $this->userRepository
and use the findOneBy()
method to query for email
set to $value
. If there is not a user with that email, throw a new TransformationFailedException()
. This is important - and its use
statement was even pre-added when we implemented the interface. Inside, say:
No user found with email %s
and pass the value. At the bottom, return $user
.
... lines 1 - 9 | |
class EmailToUserTransformer implements DataTransformerInterface | |
{ | |
... lines 12 - 31 | |
public function reverseTransform($value) | |
{ | |
$user = $this->userRepository->findOneBy(['email' => $value]); | |
if (!$user) { | |
throw new TransformationFailedException(sprintf('No user found with email "%s"', $value)); | |
} | |
return $user; | |
} | |
} |
The TransformationFailedException
is special: when this is thrown, it's a signal that there is a validation error.
Check it out: find your browser and refresh to resubmit that form. Cool - it looks like it worked. Try a different email: spacebar3@example.com
and submit! Nice! If I click enter on the address to get a fresh load... yep! It definitely saved!
But now, try an email that does not exist, like spacebar300@example.com
. Submit and... validation error! That comes from our data transformer. This TransformationFailedException
causes a validation error. Not the type of validation errors that we get from our annotations - like @Assert\Email()
or @NotBlank()
. Nope: this is what I referred to early as "sanity" validation: validation that is built right into the form field itself.
We saw this in action back when we were using the EntityType
for the author
field: if we hacked the HTML and changed the value
attribute of an option
to a non-existent id, we got a sanity validation error message.
Next: let's see how we can customize this error and learn to do a few other fancy things to make our custom field more flexible.
Hey Matt!
Wow, awesome question! Seriously - I wondered if someone would ask this - but so soon! I’m impressed :).
These data transformers are strange objects in a sense: they’re like services, they do work and don’t hold much data, but with one practical difference: we need to be able to configure them dynamically based on the options based to the form. As you’ll see in the next chapter(s), we add a field option to control the query. The only way for the form to pass that option to the transformer is through the constructor. That’s why we instantiate it manually instead of allow the container to do it.
To say it differently: when I coded up this tutorial, I DID first inject it via a type-hint. But once I needed to pass an option, I realized that wouldn’t work. It’s an odd situation, which causes this.
Cheers!
Hello!
So, we use the constructor for passing options : each builded form has its own version of the Transformer.
But, how to "test double" the Transformer ? :(
Maybe use Prototype or service Factory ? 🤔
Hey Thomas Talbot!
Very fair question :). I would recommend 2 things:
1) For test purposes, allow the Transformer to be injected into your form class via a setTransformer() method. That's a little weird - just because we're creating this method ONLY for test purposes, but it would work. There are probably a few other ways to do this, and they're all probably fine.
2) Don't test your form class. This is actually what I would do. If you have heavy logic in your form class that you want to unit test, I would isolate that into its own class and test that instead. Also, eventually this form class WILL have a decent amount of logic - near the end of this tutorial we add some event listeners, etc. But, these *still* aren't great to unit test - they are small pieces that go into the overall picture of getting the form to function correctly. So, I would (A) isolate any logic that you *can* but ultimately (B) functionally test the form if you want to verify it's working.
Cheers!
Wouldn't UserToEmailTransformer
be a better name than EmailToUserTransformer
, based on which method is transform
and which one is reverseTransform
?
Hey there,
Yeah, I think you're right. the transform method expects a User and returns an email string. So, yes, the name of the transformer is inverted. I can tell you're paying attention to details ;)
Cheers!
Hello, I'm using the exact same approach in another context. When I'm dd()-ing the form, I'm getting the equivalent of an user object. Everything is null except the email. Is it normal?
Hey @Big Bob,
Sometimes yes, it depends on your code, you said it's another context, so if you don't have any issues with code it should be good.
Cheers!
The validation error message on the email not found doesn't match the exception message we created in reverseTransform in your video and on my end. Is this expected?
Sigh. Yet another instance of my question being answered in the next video. Next time I'm really going to wait till I watch the next video.
The last sentence in the video:
Next: let's see how we can customize this error and learn to do a few other fancy things to make our custom field more flexible.
Or they edited a video and added this later and so you did not hear.
Hey Aaron,
Glad you had got your question answered faster than our team got to it. Yeah, we try to make screencasts short and finished, but sometimes we have to split a big topic into a few screencasts.
Cheers!
hello please can you take some time, to explain to me why you use dependency injection for the userrepository ? when you can instead call it as param in the function reverseTransform(UserRepository $userrepository),
thank you !!
Hey Soufiyane,
We need to inject the UserRepository
first because the DataTransforme implements an interface and we can't just add more arguments to its methods, and second because we're not in charge of calling transform()
or reverseTransform()
.
I hope this clarifies your doubts. Cheers!
I am not getting the user object at 3:32. I am getting the value of the database table column.
Also at 4:48 I get the error that it can only be used with the User objects. I am coding along on my own project, so I don't really know what I am missing.
On Article.php I have:
`
public function setConnectedSelectType(?User $connected_select_type): self
{
$this->connected_select_type = $connected_select_type;
return $this;
}
`
Ony ArticleController.php I have:<br />$controlPlan->setConnectedSelectType($form["connected_select_type"]->getData());<br />
I don't have a UserController, but I see one in the 'finish' dir project of this course. Is that needed already?
Hey Farry,
I'd need to check your ArticleController, I believe you're not passing the User object when creating the form
Cheers!
Can you give a good example where one could render a form with a list of products and each product should have a quantity field. I tried with embedded collection, but I just can't make it work. many thanks in advance there
Hey @cybernet2u
Sorry that I'm not Ryan but I have a good example for you ;) I think best way will be to not use symfony forms for such example and do something with Ajax. For example render inputs with current quantities as value and attribute with product ID. After you can do a js listener on input change post new quantity and persist it in database. Of course don't forget about security =)
Cheers!
Hey @cybernet2u
That's a good question =) Of course I'd like to recommend you our JavaScript tutorials. Also we have a great chapter in our Upload tutorial where you can find a great example of ajax renaming object references: https://symfonycasts.com/sc...
Cheers!
Hi !
I have a strange issue with TransformationFailedException(). I'm using a data transformer to convert an Author entity (to its firstname) into an input text of an Article form. But, as the relation is ManyToMany, I have a collection of Author in my form and when an author doesn't exist the reverseTransform method calls TransformationFailedException() as you did in the course. But I get this error :
InvalidPropertyPathException
Could not parse property path "children[authors].children[[0]]". Unexpected token "]" at position 30.
Any idea?
Your help would be appreciated!
Hey Cyril S.!
Hmm. That is *super* weird! On a high level, what you're doing makes sense. And also, this error is *so* weird that it "smells" like a possible bug somewhere in Symfony (or, at the very least, Symfony is not giving us a clear error). I'm honestly not sure what to do here :/. If you're able to post a project that reproduces the issue to GitHub, I'd be happy to take a look at it - it's one of those deep problems where I would need to be able to play with the code directly :).
Cheers!
Hey Ryan.
I think I saw the problem but didn't resolve for now. When using a CollectionType in my form, each custom form field defined as entry_type is wrapped into an array. So, when flushing, doctrine says "I want an object and you give me an array with an object inside". That's the reason for theese double [[ ]].
If you want to take a look, I put a small project on GitHub : https://github.com/digitima...
You can download it, create sqlite database and run fixtures to test. I made two types of edit form : checkboxes || input text with data transformer. So you can compare data send to doctrine in each case.
Thank you very much for your help
Cyril, from France ;-)
Hey Cyril S. !
Ok, you get 100 points for a WONDERFUL reproducer project: it was clear and easy to set up! And now I know the problem!
1) In your data transformer, there was one minor mistake. This is actually not related to your problem, and my guess is that you probably made this mistake only on the test project. Anyways, in reverseTransform()
, the first line should be findOneBy
not findBy
- it was causing authors to be an array of arrays instead of an array of Author objects.
2) Now, to your real problem. It is... a bug in Symfony. And it's already been fixed, but the fix hasn't been released. Here is the issue: https://github.com/symfony/symfony/issues/37027 and the pull request that fixes it https://github.com/symfony/symfony/pull/37085 - I confirmed that the changes in that pull request DO fix the problem. So, you'll need to wait for the next release (probably a couple of weeks, but maybe sooner) or use 5.0.8, which is the latest version of Symfony that does not have the issue.
Cheers!
You're right with the first problem, it's a mistake I only made in the test project :-)
And for the real one, I will wait the next release! Thank you very must for the time spent on my case (and for your great tutorials, of course)
Hello,
as usual your explanation is really clear and very useful. I am disturbing you because I have a more complex problem to solve.
in my form I have a field "company" and a field "branch": when you choose a Company you get the list with the Branches of the company to choose from. I use jQuery to populate the branch form element. Originally I had two EntityType form elements and everything worked perfect (thanks Ryan).
However, my application has hundreds of companies to choose from, so I decided to put an autocomplete on this field. The autocompletion works perfectly and the DataTransformer on the company field is OK, but the Branch form element "forgets" it association with the Branch entity and I get a TransformationFailedException on the branch form element.
I tried to put a DataTransformer on the branch form element, but very strangely it always intercepts the "transform" action and never gets to "reverse transform".
What am I doing wrong?
Thank you for your attention, have a nice day!
Hey chieroz!
Hmm. So, yes, the data transformer should be attached directly to the "branch" field as you expected. Ignoring ALL other details, if you have a data transformer attached to a field, then when you submit, that transformer's reverseTransform
should be called every time. It sounds like your problem is here: the reverse transform is NOT being called (but we don't know why). You also mentioned that the branch form element "forgets" its association with the Branch entity. What do you mean by that? Are you also using "form events" to set all of this up?
Let me know - and pose some relevant code if you can. I think there is something minor going on - maybe with how you're registering the field (especially if you're using form events, which can be complex).
Cheers!
Hi,
I developed the EmailToUserTransformer with the transform function but when I refresh the page, I have the message: 'The UserSelectTextType can only be used with User objects'
How can I solve that please?
Hello, When the custom submit the form with iframe from youtube, how can I select only the URL to store it in my data base and use in the template to customize like I want all the parameters of iframe like that <iframe src="{{ video.videoIframe }}" frameborder="0" height="109" width="105"></iframe>
Thanks for your help.
Hey Raymond L.!
Sorry - I'm not sure I understand. If your iframe is inside a form, the data from an iframe is not submitted to your server - so that data won't be available. You could use JavaScript to parse the iframe details and put this in a form (or send those via AJAX), but I'm not sure that I'm answering your question correctly :).
Cheers!
Hi guys!
I have a problem with an author field validation error.
In some previous chapter where we set annotations for email - like @Assert\Email() or @NotBlank(message="Please enter an email!"), it did not work. I think: pff fix that later. But when it did not work now... Hmmm, it kind annoying. So I had a question what could it be?
Don't work only for author validation error, the title works fine.
( I have --- Error: This value is not valid. HTML5 one)
Problem with author solved in next chapter, and with email i forgot about novalidate. Sorry to waste your time
Hi Ryan,
my I ask you for your assistants.
I have a TextType (articleBundle) which works with jQuery autocomplete. When I select a value suggested by autocomplete, I render the "articleBundle" name in the field. Unfortunately the bundle Name is not unique, so that I can't use the data transformer on the articleBundle name property!
I have the articleBundleId which is unique and by clicking on one of the suggestions I put the Id in the jQuery object.
How do I take this id and pass it to the data transformer in order to get the right entity?
Thank you in advance and best regards!
Hey Andrea daniel C.
I think you can add a hidden field to your form so you can pass the ID, and then use it on your DataTransformer to select the right record. Give it a try and let us know how it went :)
Cheers!
Hi MolloKhan ,
first of all thank you very much for your assistance.
This is a very good idea my I ask you some questions about it.
When selecting one of the suggestions in the TextType field (articleBundle), I took the bundleId which is unique and stored it via js in the hidden field (bundleId).
On submit I took the Id from the hidden field, pass it to the data transformer and actualy I am getting the right record - WUHUU! :-)
BUT - I had to put the "mapped" => false flag to the articleBundle field otherwise I got the following error
Expected argument of type "App\Entity\ArticleBundle or null", "string" given at property path "articleBundle"
how do I take the record, which is now related to the hidden field (bundleId) and store it in the db - how do I tell Symfony4 to use the transformed object, related to the bundleId field and handle it as if it were inside the articleBundle field
Same question reverse - When editing a record, how do I take the reverse transformed object and, which is related to the hidden field (bundleId) and render it in the TextType field (articleBundle)?
Thank you in advance.
Best regards, Andrea
Hey Andrea daniel C.
Oh, yes, you have to un-mapped that field from your FormType because it's not related to any field. I would have to look at your code to get a better glance of what you are doing but what I think you can do is to manually grab the value from the form and then use it as you need to
// controllers method
...
$articleBundle = $form['fieldName']->getData();
// do more stuff here
...
Cheers!
Hi, I have followed all your great tutorials and think they are the most solid ones!
I am applying this tutorial to a DataTransformer for my own custom ChoiceType that is using a select2 component for storing article tags.
The transform method works, though i am basically using it to remove all data from the field because the select2 is being populated by ajax because of 'reasons'.
Now, with the reverseTransfrom method is a different story. Seems like the method is not even being accessed. Even the dd($value); at the beginning of the method is being missed. I googled around and saw that adding 'compound' => false to the options could help, but no luck for me. Has this something to do with the ChoiceType parent?
How are you handling that form? I believe the field name for your ChoiceType is incorrect and hence, Symfony thinks the field is empty.
P.S. Double check your post request
Cheers!
Thank you Diego! My field name is correct, it is called "tags", which is a many to many relationship.
FORM:
`
->add('tags', TagSelectType::class, [
'compound' => false, //Already tried true, false or removing it
'attr' => [
'class' => 'select2'
]
])
`
TagSelectType:
`
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer(new NameToTagTransformer($this->tagRepository));
}
public function getParent(){
return ChoiceType::class;
}
`
And finally the NameToTagTransformer:
`
public function transform($value)
{
dd($value); // <-----WORKS
}
public function reverseTransform($value)
{
dd($value); // < does NOT WORK or even dies
}
`
The DataTransformer is being called correctly when loading the form (the transfrom method) but not when going back to the controller.
When dumping the whole $request on the controller, I am getting exactly what the select2 sends:<br />array:3 [▼<br /> 0 => 8<br /> 1 => "Tag A"<br /> 2 => "Tag B"<br />]<br />
And this is why i need the datatransformer, as I am already receiving the existing tag ids (8 in the example), I want to save tag A and tag B, and then send the controller a proper array with only ids, so Symfony can set the manytomany relationship.
Ok, how are you doing the post request? Double check the field names that you are posting but if you are using the Form component for rendering the form, then you should not have this problem.
When you POST, does the other fields update?
Yes, I am using the form component for rendering the form. If I take that field out, everything saves perfectly, even with some related entities. But for this field, I get an error, because of course it is expecting an array of tag ids like ["1", "2", "3"], and is receiving ["1", "2", "New Tag A", "New Tag B"], so it does not know how to convert those strings to Tag entities. The strangest thing is that the dd() on reverseTransform is not even being fired. Does it have to do with many to many relationships? This is my tags field:
`/**
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="events")
*/
private $tags;`
Oh, and I don't know if it helps or has anything to do with it, but I am using symfony 4.2.4
Thanks a lot for your help!
PS: i have moved on to adding and removing tags full front-end with api endpoints on the tag controller, but I really want to do this the way the tutorial says.
Wait a second, before trying with a "view transformer", try changing your "TagSelectType" to be a TextType field instead of a ChoiceType
Hmm, the "dd()" it not being executed, so it's failing before calling "reverseTransform()"? It makes me think what you need is a "ViewTransformer". I'm not totally sure but give it a try: https://symfony.com/doc/current/form/data_transformers.html#about-model-and-view-transformers
You only have to change one line:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new NameToTagTransformer($this->tagRepository));
}
Changing it to a TextType field instead of TagSelectType does send the data back and forth, but then I cannot use the transformer. However, changing the ChoiceType to a TextType in the parent() function of TagSelectType does trigger the "dd()"! Now I will just deal with transforming the request data coming as a string instead of an array and I'm done!
So I guess the issue has to do with the ChoiceType after all :)
Thanks a lot!!
// 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
}
}
Hi Ryan!
Why didn't you inject EmailToUserTransformer to UserSelectTextType and call $builder->addModelTransformer($this->emailToUserTransformer)?