Data Transformer

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

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

Creating the Data Transformer

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
}

The transform() Method

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!

The reverseTransform() Method

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.

Leave a comment!

  • 2020-06-10 Digit Image

    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)

  • 2020-06-10 weaverryan

    Hey Digit Image !

    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/... and the pull request that fixes it https://github.com/symfony/... - 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!

  • 2020-06-09 Digit Image

    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 ;-)

  • 2020-06-09 weaverryan

    Hey Digit Image!

    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!

  • 2020-06-09 Digit Image

    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!

  • 2020-03-09 weaverryan

    Hey Carlo Mario Chierotti!

    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!

  • 2020-03-06 Carlo Mario Chierotti

    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!

  • 2020-01-24 Vladimir Sadicov

    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!

  • 2020-01-23 Diego Aguiar

    Ohh, I think the problem relies on that you are using a DataViewTransformer. What you need is a DataModelTransformer. You can read the technical details here: https://symfony.com/doc/cur...

    I hope it helps. Cheers!

  • 2020-01-22 Big Bob

    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?

  • 2020-01-22 bob

    I've created a data transformer. I'm using what you suggested 'addViewTransformer()'. It works for the transform but not for the reverse transform. I'm getting nothing as a value. Essentially, I have a dropdown with a list of user, when I find the user I want, like in the tutorial it's using the mail to find the user in the repo. Oddly enough, if I write manually in a texttype the mail, it will find give me a good answer. If I select using the choicetype, it gives me the famous 'Hmm, user not found!';

  • 2020-01-22 Diego Aguiar

    Hey @bob

    I think the solution here would be the same, create a DataTransformer so you can manipulate your data in any type/format and then transform it back to satisfy the system needs

    Cheers!

  • 2020-01-21 bob

    Hi, I was wondering if there was a fix to the whole "ChoiceType - TextType" as I too am using a choice type as my js plugin doesn't support the text type

  • 2020-01-02 Victor Bocharsky

    Hey Coder,

    I just double-checked this in Git - it looks like it was from the scratch :p

    Cheers!

  • 2020-01-01 Coder

    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.

  • 2019-11-18 Victor Bocharsky

    Hey Stephansav,

    Ah, an easy fix! Glad you got it working!

    Cheers!

  • 2019-11-16 stephansav

    I solved it! I just forgot the use statement for User in EmailToUserTransformer class.

  • 2019-11-15 stephansav

    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?

  • 2019-11-04 weaverryan

    Hey Raymond!

    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!

  • 2019-10-29 Raymond

    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.

  • 2019-10-29 Raymond

    Hello and thanks for all.

  • 2019-08-28 Diego Aguiar

    Haha, no problem Igor, you are welcome :)

  • 2019-08-28 Igor

    Problem with author solved in next chapter, and with email i forgot about novalidate. Sorry to waste your time

  • 2019-08-28 Igor

    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)

  • 2019-07-24 Diego Aguiar

    Hey Andrea Daniel Calciano

    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!

  • 2019-07-23 Andrea Daniel Calciano

    Hi Diego Aguiar ,

    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

  • 2019-07-22 Diego Aguiar

    Hey Andrea Daniel Calciano

    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!

  • 2019-07-22 Andrea Daniel Calciano

    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!

  • 2019-03-28 Diego Aguiar

    Oh yes, that's what I meant "ChoiceType to TextType". The thing here is that the ChoiceType will try to get the entity objects list before applying the reverse transformation, and since your keys are wrong, then you get an error :)

    Cheers!

  • 2019-03-27 Manny

    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!!

  • 2019-03-27 Manny

    Thank you! I will give it a try ;)

  • 2019-03-27 Diego Aguiar

    Wait a second, before trying with a "view transformer", try changing your "TagSelectType" to be a TextType field instead of a ChoiceType

  • 2019-03-27 Diego Aguiar

    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/cur...

    You only have to change one line:


    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder->addViewTransformer(new NameToTagTransformer($this->tagRepository));
    }
  • 2019-03-25 Manny

    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.

  • 2019-03-25 Diego Aguiar

    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?

  • 2019-03-25 Manny

    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:

    array:3 [▼
    0 => 8
    1 => "Tag A"
    2 => "Tag B"
    ]

    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.

  • 2019-03-22 Diego Aguiar

    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!

  • 2019-03-22 Manny

    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?

  • 2019-02-28 Diego Aguiar

    Hey Samuel Halera

    Double check the code where you generate such URL. That error means that you forgot to pass the ID parameter

    Cheers!

  • 2019-02-28 Samuel Halera

    Hi Ryan,
    Really nice tutorials!!

    I'm having a symfony a error using the Data Transformer. When I update the article's author field, after I submit the form with btn "Update", I get this message error :

    " Some mandatory parameters are missing ("id") to generate a URL for route "admin_article_edit". "

    I don't understand why...
    Thank you for your helps

  • 2019-01-28 Victor Bocharsky

    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!

  • 2019-01-25 Aaron Kincer

    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.

  • 2019-01-24 Aaron Kincer

    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?

  • 2018-12-31 weaverryan

    Hey cybernet2u!

    Ahh! Two things:

    1) Right before that error, do a dump($value);die; so you can see what the value actually is.

    2) But... I think I already know what it will be ;). Some sort of ArrayCollection or PersistetCollection Doctrine object.

    In your situation, you have a ManyToMany. So, ran will not contain one Rank object. And, on the other side, inside Rank, your "members" property will not contain just a single Members object - it will be a Collection of objects (basically an array).

    So, your data transformer needs to be a bit smarter:

    A) In transform, you will be passed this "collection" of objects - you need to combine this into one string. To follow the example in this tutorial, because I'm using a text box, I would probably create a comma-separated string of all of the email addresses.

    B) In reverseTransform, you will be passed the comma-separated list of email addresses (or members in your situation - but I'm not sure your exact setup). You would then split that into separate email addresses, and query for all of the User object (Member objects for you). Ultimately, you would return an array of Member objects.

    Hopefully that will get you started!

    Cheers!

  • 2018-12-29 cybernet2u

    Hi ... again :D
    I'm trying to edit a App:Member's App:Rank in a Form with data_class Members but i'm getting the LogicException error

    if (!$value instanceof Members) {
    throw new \LogicException('The RankSelect can only be used with Rank objects');
    }

    any ideas :?

    Relation is

    /**
    * @ORM\ManyToMany(targetEntity="App\Entity\Rank", inversedBy="members")
    */
    private $rank;

  • 2018-12-03 weaverryan

    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!

  • 2018-12-02 Thomas Talbot

    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 ? 🤔

  • 2018-12-01 weaverryan

    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!

  • 2018-11-30 Matt

    Hi Ryan!

    Why didn't you inject EmailToUserTransformer to UserSelectTextType and call $builder->addModelTransformer($this->emailToUserTransformer)?