EasyAdminBundle icon indicating copy to clipboard operation
EasyAdminBundle copied to clipboard

Doc: Add password integration example

Open rogergerecke opened this issue 4 years ago • 28 comments

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

rogergerecke avatar Jun 18 '20 18:06 rogergerecke

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

morgan-blondellet avatar Jul 08 '20 14:07 morgan-blondellet

In my EA its work? Please show full example off youre code Thanks

rogergerecke avatar Jul 08 '20 18:07 rogergerecke

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.

morgan-blondellet avatar Jul 09 '20 07:07 morgan-blondellet

I Fixit found the bug. I Edit my first coment

rogergerecke avatar Jul 22 '20 16:07 rogergerecke

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

    //...

  1. 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 avatar Aug 19 '20 10:08 labgua

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

alexandru-burca avatar Aug 31 '20 21:08 alexandru-burca

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

Seb33300 avatar Sep 19 '20 13:09 Seb33300

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

pibrom avatar Oct 19 '20 13:10 pibrom

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 ?

morgan-blondellet avatar Oct 19 '20 13:10 morgan-blondellet

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 ?

pibrom avatar Oct 19 '20 13:10 pibrom

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.

morgan-blondellet avatar Oct 19 '20 13:10 morgan-blondellet

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

pibrom avatar Oct 19 '20 13:10 pibrom

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?

BurningDog avatar Nov 23 '20 08:11 BurningDog

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.

Seb33300 avatar Nov 23 '20 09:11 Seb33300

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 instanceof User && $user->getPlainPassword()) {
            $user->setPassword($this->passwordEncoder->encodePassword($user, $user->getPlainPassword()));
        }
    }
}

zorn-v avatar Nov 26 '20 11:11 zorn-v

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

don't forget to put if (!($user instanceof User)) { return; }

Yeleup avatar Jan 12 '21 06:01 Yeleup

Yep. Or

if ($user instanceof User && $user->getPlainPassword()) {

Comment above updated

zorn-v avatar Jan 12 '21 07:01 zorn-v

Is there a special reason to put the subsriber logic in the controller? I normally do this in a seperate subscriber class

parijke avatar Jan 12 '21 07:01 parijke

Is there a special reason to put the subsriber logic in the controller?

No, it is just for example. Feel free to do it as you wish :wink: You can also subscribe to doctrine events for store encoded password from any place of your app.

zorn-v avatar Jan 12 '21 08:01 zorn-v

This really should be in the docs.

milosa avatar Jan 14 '21 13:01 milosa

@milosa make a PR then

parijke avatar Jan 14 '21 13:01 parijke

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

@Seb33300 I have tried to do that but I get this mistake "Call to a member function encodePassword() on null". How can I solve that?

CristinaEsteban97 avatar Mar 29 '21 14:03 CristinaEsteban97

@CristinaEsteban97 autowiring needs to be enabled in order to be able to use @required annotation. See https://symfony.com/doc/current/service_container/autowiring.html#autowiring-other-methods-e-g-setters-and-public-typed-properties

Seb33300 avatar Mar 30 '21 02:03 Seb33300

@ CristinaEsteban97 autowiring debe estar habilitado para poder usar la @requiredanotación. Consulte https://symfony.com/doc/current/service_container/autowiring.html#autowiring-other-methods-eg-setters-and-public-typed-properties

@Seb33300 Solved! But I don't undenstand why we need the @required anotation to the method setEncoder() and @var UserPasswordEncoderInterface to the var passwordEncoder. Can you explain me that please?

CristinaEsteban97 avatar Mar 30 '21 11:03 CristinaEsteban97

It looks like a constructor replacement to me. Why not setting it in the constructor?

parijke avatar Mar 30 '21 11:03 parijke

@CristinaEsteban97 awesome! Your example works perfectly for v4. Thank you.

s-chizhik avatar Jan 15 '22 17:01 s-chizhik

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 instanceof User && $user->getPlainPassword()) {
            $user->setPassword($this->passwordEncoder->encodePassword($user, $user->getPlainPassword()));
        }
    }
}

Hi! Excuse me, where can I put the rest of the field configuration? (I'm new to EA, sorry)

"Attempted to call an undefined method named "getAsDto" of class "Generator"."

lDeleted avatar Feb 05 '22 09:02 lDeleted

Reading all the previous comments (thanks a lot guys for your examples) I've got it working en EasyAdmin 4 + Symfony 5.4 (php 8.1.1). Works for me on edit user and new user actions. In the New User action password field is required. In the Edit User action it is not. You can pass a blank password and the current one won't be changed.

    <?php
    #Controller/Admin/UserCrudController.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\Controller\AbstractCrudController;
    use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
    use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
    use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
    use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
    use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
    use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
    use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
    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\PasswordHasher\Hasher\UserPasswordHasherInterface;
    
    class UserCrudController extends AbstractCrudController {
    
    	private UserPasswordHasherInterface $passwordEncoder;
    
    	public function __construct( UserPasswordHasherInterface $passwordEncoder ) {
    		$this->passwordEncoder = $passwordEncoder;
    	}
    
    	public static function getEntityFqcn(): string {
    		return User::class;
    	}
    
    	public function configureFields( string $pageName ): iterable {
    		yield FormField::addPanel( 'User data' )->setIcon( 'fa fa-user' );
    		yield EmailField::new( 'email' )->onlyWhenUpdating()->setDisabled();
    		yield EmailField::new( 'email' )->onlyWhenCreating();
    		yield TextField::new( 'email' )->onlyOnIndex();
    		$roles = [ 'ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_USER' ];
    		yield ChoiceField::new( 'roles' )
    		                 ->setChoices( array_combine( $roles, $roles ) )
    		                 ->allowMultipleChoices()
    		                 ->renderAsBadges();
    		yield FormField::addPanel( 'Change password' )->setIcon( 'fa fa-key' );
    		yield Field::new( 'password', 'New password' )->onlyWhenCreating()->setRequired( true )
    		           ->setFormType( RepeatedType::class )
    		           ->setFormTypeOptions( [
    			           'type'            => PasswordType::class,
    			           'first_options'   => [ 'label' => 'New password' ],
    			           'second_options'  => [ 'label' => 'Repeat password' ],
    			           'error_bubbling'  => true,
    			           'invalid_message' => 'The password fields do not match.',
    		           ] );
    		yield Field::new( 'password', 'New password' )->onlyWhenUpdating()->setRequired( false )
    		           ->setFormType( RepeatedType::class )
    		           ->setFormTypeOptions( [
    			           'type'            => PasswordType::class,
    			           'first_options'   => [ 'label' => 'New password' ],
    			           'second_options'  => [ 'label' => 'Repeat password' ],
    			           'error_bubbling'  => true,
    			           'invalid_message' => 'The password fields do not match.',
    		           ] );
    	}
        
    	public function createEditFormBuilder( EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context ): FormBuilderInterface {
    		$plainPassword = $entityDto->getInstance()?->getPassword();
    		$formBuilder   = parent::createEditFormBuilder( $entityDto, $formOptions, $context );
    		$this->addEncodePasswordEventListener( $formBuilder, $plainPassword );
    
    		return $formBuilder;
    	}
    
    	public function createNewFormBuilder( EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context ): FormBuilderInterface {
    		$formBuilder = parent::createNewFormBuilder( $entityDto, $formOptions, $context );
    		$this->addEncodePasswordEventListener( $formBuilder );
    
    		return $formBuilder;
    	}
    
    	protected function addEncodePasswordEventListener( FormBuilderInterface $formBuilder, $plainPassword = null ): void {
    		$formBuilder->addEventListener( FormEvents::SUBMIT, function ( FormEvent $event ) use ( $plainPassword ) {
    			/** @var User $user */
    			$user = $event->getData();
    			if ( $user->getPassword() !== $plainPassword ) {
    				$user->setPassword( $this->passwordEncoder->hashPassword( $user, $user->getPassword() ) );
    			}
    		} );
    	}
    }

In the User class I've set password as nullable:

    <?php
    #Entity/User.php
    
    namespace App\Entity;
    
    use App\Repository\UserRepository;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
    use Symfony\Component\Security\Core\User\UserInterface;
    
    #[ORM\Entity( repositoryClass: UserRepository::class )]
    class User implements UserInterface, PasswordAuthenticatedUserInterface {
    
    	#[ORM\Column( type: 'string', nullable: true )]
    	private ?string $password = null;
    
    	public function getPassword(): ?string {
    		return $this->password;
    	}

	public function setPassword( ?string $password ): self {
		if (!is_null($password)) {
			$this->password = $password;
		}

		return $this;
	}

And to avoid deprecation messages I've also set this:

    #config/packages/framework.yaml
    
    framework:
        form:
            legacy_error_messages: false

luismisanchez avatar Jun 14 '22 16:06 luismisanchez

Hello! I'm trying @luismisanchez solution on Symfony 6.1.5, with EA 4.3 and I'm getting an error. I don't really know what is happening.

The error is as follow:

App\Controller\Admin\UserCrudController::addEncodePasswordEventListener(): Argument #1 ($formBuilder) must be of type Symfony\Component\Form\Test\FormBuilderInterface, Symfony\Component\Form\FormBuilder given, called in /opt/homebrew/var/www/cesida/src/Controller/Admin/UserCrudController.php on line 77

The implementation of the UserCrudController and the User entity is the same as @luismisanchez solution.

Im new in PHP and Symfony and I don't understand why if FormBuilder implements the FormBuilderInterface interface, I'm getting this error.

Any help would be much appreciated :D

alejandrogr avatar Oct 05 '22 17:10 alejandrogr

You probably have the wrong import in your UserCrudController? image

Should be use Symfony\Component\Form\FormBuilderInterface;

parijke avatar Oct 06 '22 07:10 parijke