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 SubscribeUserRegistrationFormType
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 methodsgetPlainPassword()
blah, blah, blah, exist in classUser
.
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.
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!
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!
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.
data.plainPassword
This value should not be blank.
Caused by
ConstraintViolation
LE: sorry about that, i still had plainPassword field from a previous tutorial :)
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 :)
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!
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?
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!
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?
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!
Hi, how would I transform an unmapped Datetime field into a DateTime object from an array? thank you.
Hey Benoit L.
Have you tried setting up the DataType of the unmapped field to a Symfony "DateTimeType"? https://symfony.com/doc/cur...
Cheers!
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 ?
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...
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!
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!
// 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
}
}
for me, unless I make plainPassword RepeatedType, it doesn't work.
for those who wonder how,
`->add('plainPassword', RepeatedType::class, array(