简体   繁体   中英

How can I update mapped fields only when a condition is satisfied?

In my Symfony2 application I created a custom form to edit objects of a User class. Users have a password property which contains a hash of the user's password. For obvious reasons, I do not want to echo this property's value into a field. However, I want a password field on my form so that when editing a user it is possible to change the user's password.

This password field should behave as follows:

  • If the user has a password set, then the field should contain ******** .
  • If the user has no password set, then the field should be empty.
  • (It turns out the previous two points are impossible to achieve with my current architecture, so instead I am going for:) When the page is loaded, the field should be empty, regardless of whether the user has a password set.
  • If the field is posted with content, then the user's password should be set to the hashed value of the field.
  • If the field is posted empty, the user's password should not be changed and, more importantly, not cleared.

I thought of implementing this with a custom data transformer. However, the data transformer does not provide me with a way to skip updating the user's password property when the password field is posted empty.

Where do I need to extend the framework to add custom logic deciding which fields should be updated?

UPDATE

This is the legacy code I am trying to replace:

/* SomeController.php */

$pass = $request->get('password');
if (strlen($pass) >= 5 && strlen($pass) <= 16) {
    $factory = $this->get('security.encoder_factory');
    $encoder = $factory->getEncoder($user);
    $password = $encoder->encodePassword($pass, $user->getSalt());

    $user->setPassword($password);
}

I can live with removing the string length checks. All I want to check is whether something has been entered.

As you can see, I can not simply move this code to a data transformer as it needs to access the $user which is the user we are currently editing. I don't think it is a good idea to create a service providing this value.

Just insert a control directly into your entity method and use data transformer (as you have insight)

So your entity will be

class User
{
  //some properties and methods here

  public function setPassword($pwd = null) {
    if (null !== $pwd) {
      $this->password = //do actions here like hash or whatever
    }
    //don't update the password
  }
}

If you want to take advantage only of DataTransformers, you could still do what you need that way

use Symfony\Component\DependencyInjection\ContainerInterface;

class PasswordTransformer implements DataTransformerInterface
{
  private $ci;

  public function __construct(ContainerInterface $ci) {
    $this->ci = $ci;
  }
  //...
  public function reverseTransform($form_password) {
    if (!$form_password) {
      //return password already stored
      return $this->ci->get('security.context')
                      ->getToken()
                      ->getUser()
                      ->getPassword();
    }
  }
}

Of course you need to inject service_container service into you data transformer (or better, you should inject it into your form type's selector and pass to DataTransformer constructor as follows:

services:
your.bundle.password_selector_type:
    class: Your\Bundle\Form\Type\PasswordSelectorType
    arguments: ["@security.context"]
    tags:
        - { name: form.type, alias: password_selector_type }

For the form part, you should take a look a this widget.

http://symfony.com/doc/current/reference/forms/types/repeated.html

It provides an easy way to ask and treat confirmation on a field (it also hide values with stars when you set type to password).

$builder->add('password', 'repeated', array(
'type' => 'password',
'invalid_message' => 'The password fields must match.',
'options' => array('attr' => array('class' => 'password-field')),
'required' => true,
'first_options'  => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password')));

It will check first and second options to be equal. If that's the case then your form will be considered as valid. Otherwise, invalid_message will be displayed and first field will set to the content typed by user while confirmation field (second option) will be emptied.

You can add some logic afterwards like hashing the password to finally persist your entity. (Extracting it in a form handler would be a good practice).

Here is what I came up with for now but I am not happy with the solution as it involves custom form processing in the controller. However, so far this is the only way I have found to make it work.

My form class adds an unmapped field for the user's password:

class UserType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add('username')
            ->add('displayName')
            ->add('password', 'password', ['mapped' => false]);
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults(array('data_class' => 'MyCompany\MyBundle\Entity\User'));
    }

    public function getName() {
        return 'user';
    }
}

This field is then processed manually in my controller class:

class UserAdminController extends Controller {
    public function editUserAction($userId, Request $request) {
        $user = $this->getDoctrine()->getRepository('MyCompanyMyBundle:User')->findOneById($userId);

        $form = $this->createForm('user', $user);

        $form->handleRequest($request);

        if ($form->isValid()) {
            $newPassword = $form['password']->getData();
            if ($newPassword !== "") {
                $factory = $this->get('security.encoder_factory');
                $encoder = $factory->getEncoder($user);
                $password = $encoder->encodePassword($newPassword, $user->getSalt());

                $user->setPassword($password);
            }
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();
        }

        return $this->render(
            "MyCompanyMyBundle:Admin/Management/User:Edit.html.php",
            [
                "form"  => $form->createView()
            ]
        );
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM