Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Adding Extra "Unmapped" Fields

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

UserRegistrationFormType has a password field. But that means, when the user types in their password, the form component will call setPassword() and pass it that plaintext property, which will be stored on the password property.

That's both weird - because the password field should always be encrypted - and a potential security issue: if we somehow accidentally save the user at this moment, that plaintext password will go into the database.

And, yea before we save, we do encrypt that plaintext password and set that back on the password property. But, I don't like doing this: I don't like ever setting the plaintext password on a property that could be persisted: it's just risky, and, kind of strange to use this property in two ways.

Go back to UserRegistrationFormType. Change the field to plainPassword. Let's add a comment above about why we're doing this.

... lines 1 - 9
class UserRegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... line 15
// don't use password: avoid EVER setting that on a
// field that might be persisted
->add('plainPassword')
;
}
... lines 21 - 27
}

But... yea! This will break things! Go back to the form and try to register with a different user. Boom!

Neither the property plainPassword nor one of the methods getPlainPassword() blah, blah, blah, exist in class User.

And we know why this is happening! Earlier, we learned that when you add a field to your form called email, the form system, calls getEmail() to read data off of the User object. And when we submit, it calls setEmail() to set the data back on the object. Oh, and, it also calls getEmail() on submit to so it can first check to see if the data changed at all.

Anyways, the form is basically saying:

Hey! I see this plainPassword field, but there's no way for me to get or set that property!

There are two ways to fix this. First, we could create a plainPassword property on User, but make it not persist it to the database. So, don't put an @ORM\Column annotation on it. Then, we could add normal getPlainPassword() and setPlainPassword() methods... and we're good! That solution is simple. But it also means that we've added this extra property to the class just to help make the form work.

Unmapped (mapped => false) Fields

The second solution is... a bit more interesting: we can mark the field to not be "mapped". Check it out: pass null as the second argument to add() so it continues guessing the field type for now. Then, pass a new option: mapped set to false.

That changes everything. This tells the form system that we do want to have this plainPassword field on our form, but that it should not get or set its data back onto the User object. It means that we no longer need getPlainPassword() and setPlainPassword() methods!

Accessing Unmapped Fields

Woo! Except... wait, if the form doesn't set this data onto the User object... how the heck can we access that data? After all, when we call $form->getData(), it gives us the User object. Where will that plainPassword data live?

In your controller, dd($form['plainPassword']->getData()).

... lines 1 - 14
class SecurityController extends AbstractController
{
... lines 17 - 44
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 47 - 49
if ($form->isSubmitted() && $form->isValid()) {
dd($form['plainPassword']->getData());
... lines 52 - 68
}
... lines 70 - 73
}
}

Then move over, refresh and... oh! Form contains extra fields. My fault: I never fully refreshed the form after renaming password to plainPassword. So, we were still submitting the old password field. By default, if you submit extra fields to a form, you get this validation error.

Let's try that again. This time... Yes! It hits our dump and die and there is our plain password!

This uncovers a really neat thing about the form system. When you call $this->createForm(), it creates a Form object that represents the whole form. But also, each individual field is also represented as its own Form object, and it's a child of that top-level form. Yep, $form['plainPassword'] gives us a Form object that knows everything about this one field. When we call ->getData() on it, yep! That's the value for this one field.

This is a super nice solution for situations where you need to add a field to your form, but it doesn't map cleanly to a property on your entity. Copy this, remove the dd() and, down below, use that to get the plain password.

... lines 1 - 49
if ($form->isSubmitted() && $form->isValid()) {
... lines 51 - 52
$user->setPassword($passwordEncoder->encodePassword(
$user,
$form['plainPassword']->getData()
));
... lines 57 - 67
}
... lines 69 - 75

Let's try it! Move back over, refresh and... got it! We are registered!

Using the PasswordType Field

Go back to /register - there is one more thing I want to fix before we keep going: the password field is a normal, plaintext input. That's not ideal.

Find your form class. The form field guessing system has no idea what type of field plainPassword is - it's not even a property on our entity! When guessing fails, it falls back to TextType.

Change this to PasswordType::class. This won't change how the field behaves, only how it's rendered. Yep! A proper <input type="password"> field.

... lines 1 - 12
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 16 - 18
->add('plainPassword', PasswordType::class, [
... line 20
]);
... line 22
}
... lines 24 - 32

Next: time to add validation! Which, hmm, is going to be a bit interesting. First, we need to validate that the user is unique in the database. And second, for the first time, we need to add validation to a form field where there is no corresponding property on our class.

Leave a comment!

16
Login or Register to join the conversation
Remus M. Avatar
Remus M. Avatar Remus M. | posted 3 years ago

for me, unless I make plainPassword RepeatedType, it doesn't work.

for those who wonder how,
->add('plainPassword', RepeatedType::class, array(
'type' => PasswordType::class,
'first_options' => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password'),
))

1 Reply

Hey Remus M.

What's not working for you, the mapped => false option?

Reply
Remus M. Avatar

data.plainPassword
This value should not be blank.

Caused by

ConstraintViolation

LE: sorry about that, i still had plainPassword field from a previous tutorial :)

Reply
Tobias I. Avatar
Tobias I. Avatar Tobias I. | posted 3 years ago

For those who may be as confused as I was about this, you can use RepeatedType to render two password fields. Symfony will check for you that both values match. Read more about it on the docs https://symfony.com/doc/cur...
Hope this helps :)

1 Reply

Hey Tobias,

Thank you for sharing it with others! Yeah, if you want to double sure users didn't misprint his password - you can use RepeatedType along with PasswordType.

Cheers!

Reply
Kevin Avatar

Hi,

I was taking a look at the 5.4 docs after watching this video and noticed `PasswordAuthenticatedUserInterface` being implemented to User on one of the examples. Would this be the equivalent of doing the plainPassword technique shown in this vid on newer versions of Symfony?

Reply

Hey Kevin

Yes, the main idea here is to avoid adding a property on the User object that will hold the value of plainPassword. So, in this video Ryan teaches how you can add fields to a form that are not mapped to an entity

Cheers!

Reply
Abdul mannan Avatar
Abdul mannan Avatar Abdul mannan | posted 2 years ago

why don't we just handle the encoding within the setPassword method in the entity ? Doesn't it make more sense to have it one place rather having encoding code duplicated in multiple places?

Reply

Hey Abdul,

The problem is that PasswordEncoder - that's a *service*, and you don't have access to services from your entities because entities are just simple data objects. That's why we have to pass the entity through some code that will generate the correct password using that service and set it on the entity.

I hope this is clearer for you now.

Cheers!

Reply
Benoit L. Avatar
Benoit L. Avatar Benoit L. | posted 3 years ago

Hi, how would I transform an unmapped Datetime field into a DateTime object from an array? thank you.

Reply

Hey Benoit L.

Have you tried setting up the DataType of the unmapped field to a Symfony "DateTimeType"? https://symfony.com/doc/cur...

Cheers!

Reply
Martin C. Avatar
Martin C. Avatar Martin C. | posted 3 years ago

It's great the unmmaped field but I have a problem on my side. The password on the user entity is required. So when I want to register a user I don't pass the isValid method because the password is not set (only the plaintext password), and we're supposed to set the password after validation of the form.

So should I make the password nullable in the database ?

Reply
ionik Avatar

Hi,

If you use symfony 4.3
Use validation groups to do validation because of this new feature : https://symfony.com/blog/ne...
https://symfony.com/doc/cur...

1 Reply

Hey ionik!

you're right - this issue is related. We've temporarily disabled this new automatic validation feature on new Symfony 4.3 projects - you can still enable it in validator.yaml, but it's no longer enabled by default: https://github.com/symfony/...

If you started a Symfony 4.3 project during the first few weeks of June, you will have this setting enabled. If it's causing issues, you can disable the "auto_mapping" key in validator.yaml. We're working to add a few more features to make it a bit more flexible before auto-enabling it in the future :).

Cheers!

Reply
ionik Avatar

Thanks for your reply of explanation ;)

Reply

Hey Martin,

You need to add validation constraints to the plainPassword instead of hashed password, because you work with plainPassword in the form. And then you will not need to make the password field nullable in the DB, you still can keep it required, but then it's your job to properly set the password field when user sent you a plainPassword and the form is valid.

Cheers!

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