Short description of what this feature will allow to do:
Add to the doc a example for password genaration
Example of how to use this feature
UserCrudController.php
/**
* @var UserPasswordEncoderInterface
*/
private $passwordEncoder;
/**
* @var Security
*/
private $security;
/**
* UserCrudController constructor.
* @param UserPasswordEncoderInterface $passwordEncoder
* @param Security $security
*/
public function __construct(
UserPasswordEncoderInterface $passwordEncoder,
Security $security
) {
$this->passwordEncoder = $passwordEncoder;
$this->security = $security;
// get the user id from the logged in user
if (null !== $this->security->getUser()) {
$this->password = $this->security->getUser()->getPassword();
}
}
/**
* @param string $pageName
* @return iterable
*/
public function configureFields(string $pageName): iterable
{
$password = TextField::new('password')
->setFormType(PasswordType::class)
->setFormTypeOption('empty_data', '')
->setRequired(false)
->setHelp('If the right is not given, leave the field blank.');
switch ($pageName) {
case Crud::PAGE_INDEX:
return [
$password,
];
break;
case Crud::PAGE_DETAIL:
return [
$password,
];
break;
case Crud::PAGE_NEW:
return [
$password,
];
break;
case Crud::PAGE_EDIT:
return [
$password,
];
break;
}
}
/**
*
* @param EntityManagerInterface $entityManager
* @param $entityInstance
*/
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
// set new password with encoder interface
if (method_exists($entityInstance, 'setPassword')) {
$clearPassword = trim($this->get('request_stack')->getCurrentRequest()->request->all('User')['password']);
// if user password not change save the old one
if (isset($clearPassword) === true && $clearPassword === '') {
$entityInstance->setPassword($this->password);
} else {
$encodedPassword = $this->passwordEncoder->encodePassword($this->getUser(), $clearPassword);
$entityInstance->setPassword($encodedPassword);
}
}
parent::updateEntity($entityManager, $entityInstance);
}
I tried implementing your code but i found an issue.
If you submit the form with an empty password for the currently logged in User $this->getUser()->getPassword() will return an empty string. Therefore in your database the user will no longer have a password.
Edit: to make it work i had to override the entire edit action to save the $currentPassword in a variable. If the $entityInstance->getPassword() call returns null, i reset the password to $currentPassword
In my EA its work? Please show full example off youre code
Thanks
I don't have the non working code anymore. I had more or less the same thing as yourself but if i edited the current logged in user i would lose the password.
I Fixit found the bug. I Edit my first coment
Hi,
I just needed to solve a similar problem today: the password reset management in the admin backend
Actually I also found some problems in the implementation but the way to solve it is correct.
So i decided to remake the @rogergerecke 's solution and post it here.
1) Insert in the User entity a not-mapped field (with getter/setter methods): it is used to handle the clear password.
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
//...
/**
* @var string clear password for backend
*/
private $clearpassword;
/**
* @return string
*/
public function getClearpassword(): string
{
if( $this->clearpassword == null ) return "";
return $this->clearpassword;
}
/**
* @param string $clearpassword
*/
public function setClearpassword(string $clearpassword): void
{
$this->clearpassword = $clearpassword;
}
//...
2) Inject in the User CRUD controller the passwordEncoder and override the updateEntity method to set the encoded password in the entity
MyLog is a utitlity class for debugging, I leave it to you for readability ...
namespace App\Controller\Admin;
use App\Entity\User;
use App\Utils\MyLog;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserCrudController extends AbstractCrudController
{
/**
* @var UserPasswordEncoderInterface
*/
private $passwordEncoder;
/**
* UserCrudController constructor.
* @param UserPasswordEncoderInterface $passwordEncoder
*/
public function __construct(
UserPasswordEncoderInterface $passwordEncoder
) {
$this->passwordEncoder = $passwordEncoder;
}
public static function getEntityFqcn(): string
{
return User::class;
}
public function configureFields(string $pageName): iterable
{
$password = TextField::new('clearpassword')
->setLabel("New Password")
->setFormType(PasswordType::class)
->setFormTypeOption('empty_data', '')
->setRequired(false)
->setHelp('If the right is not given, leave the field blank.')
->hideOnIndex();
return [
// ...
$password,
// ...
];
}
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
// set new password with encoder interface
if (method_exists($entityInstance, 'setPassword')) {
$clearPassword = trim($this->get('request_stack')->getCurrentRequest()->request->all()['User']['clearpassword']);
///MyLog::info("clearPass:" . $clearPassword);
// save password only if is set a new clearpass
if ( !empty($clearPassword) ) {
////MyLog::info("clearPass not empty! encoding password...");
$encodedPassword = $this->passwordEncoder->encodePassword($this->getUser(), $clearPassword);
$entityInstance->setPassword($encodedPassword);
}
}
parent::updateEntity($entityManager, $entityInstance);
}
}
@labgua and @rogergerecke your solution works great but will throw an error when there are some AJAX requests in Index, like BooleanField.
A possible solution to avoid this could be to check if it is a Xml request or not.
Update the function updateEntity to:
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
// set new password with encoder interface
if (method_exists($entityInstance, 'setPassword') && !$this->get('request_stack')->getCurrentRequest()->isXmlHttpRequest()) {
$clearPassword = trim($this->get('request_stack')->getCurrentRequest()->request->all()['User']['clearpassword']);
///MyLog::info("clearPass:" . $clearPassword);
// save password only if is set a new clearpass
if ( !empty($clearPassword) ) {
////MyLog::info("clearPass not empty! encoding password...");
$encodedPassword = $this->passwordEncoder->encodePassword($this->getUser(), $clearPassword);
$entityInstance->setPassword($encodedPassword);
}
}
parent::updateEntity($entityManager, $entityInstance);
}
Here is my solution with EasyAdmin v3 and form events.
It works even if the password field is mandatory.
<?php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserCrudController extends AbstractCrudController
{
/** @var UserPasswordEncoderInterface */
private $passwordEncoder;
public static function getEntityFqcn(): string
{
return User::class;
}
public function configureFields(string $pageName): iterable
{
return [
FormField::addPanel('Change password')->setIcon('fa fa-key'),
Field::new('plainPassword', 'New password')->onlyOnForms()
->setFormType(RepeatedType::class)
->setFormTypeOptions([
'type' => PasswordType::class,
'first_options' => ['label' => 'New password'],
'second_options' => ['label' => 'Repeat password'],
]),
];
}
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
$this->addEncodePasswordEventListener($formBuilder);
return $formBuilder;
}
public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context);
$this->addEncodePasswordEventListener($formBuilder);
return $formBuilder;
}
/**
* @required
*/
public function setEncoder(UserPasswordEncoderInterface $passwordEncoder): void
{
$this->passwordEncoder = $passwordEncoder;
}
protected function addEncodePasswordEventListener(FormBuilderInterface $formBuilder)
{
$formBuilder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
/** @var User $user */
$user = $event->getData();
if ($user->getPlainPassword()) {
$user->setPassword($this->passwordEncoder->encodePassword($user, $user->getPlainPassword()));
}
});
}
}
Hello,
I have an issue on this getter.
When I submit a form with empty passwords, I had an error "Expected argument of type "string", "null" given at property path "plainPassword".
Do you encourter the same issue ?
````
/**
* @return string
*/
public function getPlainPassword()
{
if( $this->plainPassword == null ) return "";
return $this->plainPassword;
}
Hi.
It seems to me that your error is coming from the setter and not the getter. If an argument is expected it would be at the setter level.
Can you share your setter's code please ?
Thanks for your answer.
Here is my setter, It is configured as described above 馃憤
/**
* @param string $plainPassword
*/
public function setPlainPassword(string $plainPassword): void
{
$this->plainPassword = $plainPassword;
}
Do you think I should modify something in this setter ?
No problem !
Yes, your function's argument is typed as a non nullable string.
In order to be able to send null to the setPlainPassword method you need to type your argument ?string. The question mark specifies that your argument can be null instead of a string.
Marvelous, it works !
Thanks for your help.
So, for everyone who read this thead, here is my setter
/**
* @param string $plainPassword
*/
public function setPlainPassword(?string $plainPassword): void
{
$this->plainPassword = $plainPassword;
}
The solution at https://github.com/EasyCorp/EasyAdminBundle/issues/3349#issuecomment-695214741 works nicely, however, it doesn't allow EasyAdmin to expose any other fields on the User entity.
I don't see a way to do that by changing how the configureFields() function works to return a Symfony form, rather than a list of FieldInterfaces.
For instance, the auto-upgrade from EasyAdmin 2 to 3 gives me this:
public function configureFields(string $pageName): iterable
{
$email = TextField::new('email');
$password = TextField::new('password');
$firstName = TextField::new('firstName');
$lastName = TextField::new('lastName');
$created = DateTimeField::new('created');
$updated = DateTimeField::new('updated');
if (Crud::PAGE_INDEX === $pageName) {
return [$id, $email, $firstName, $lastName, $created, $updated];
} elseif (Crud::PAGE_DETAIL === $pageName) {
return [$id, $email, $roles, $firstName, $lastName, $created, $updated];
} elseif (Crud::PAGE_NEW === $pageName) {
return [$email, $password, $firstName, $lastName, $created, $updated];
} elseif (Crud::PAGE_EDIT === $pageName) {
return [$email, $password, $firstName, $lastName, $created, $updated];
}
}
Is there a way to use the code from https://github.com/EasyCorp/EasyAdminBundle/issues/3349#issuecomment-695214741 here?
Would it not rather make more sense to create a PasswordField which extends FieldInterface?
it doesn't allow EasyAdmin to expose any other fields on the User entity
I am not sure to understand what you mean.
I gave the minimal working example.
You can of course add as many fields as you need by adding them to the returned array.
I achieved this by using EA events. Like this
<?php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserCrudController extends AbstractCrudController implements EventSubscriberInterface
{
/** @var UserPasswordEncoderInterface */
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public static function getEntityFqcn(): string
{
return User::class;
}
public function configureFields(string $pageName): iterable
{
return array_map(function ($f) use ($pageName) {
if ($f->getAsDto()->getProperty() === 'password') {
$field = TextField::new('plain_password', Crud::PAGE_NEW === $pageName ? 'Password' : 'Change password')
->setFormType(PasswordType::class);
if (Crud::PAGE_NEW === $pageName) {
$field->setRequired(true);
}
return $field;
}
return $f;
}, parent::configureFields($pageName));
}
public static function getSubscribedEvents()
{
return [
BeforeEntityPersistedEvent::class => 'encodePassword',
BeforeEntityUpdatedEvent::class => 'encodePassword',
];
}
/** @internal */
public function encodePassword($event)
{
$user = $event->getEntityInstance();
if ($user->getPlainPassword()) {
$user->setPassword($this->passwordEncoder->encodePassword($user, $user->getPlainPassword()));
}
}
}
Most helpful comment
Here is my solution with EasyAdmin v3 and form events.
It works even if the password field is mandatory.