<?php declare(strict_types=1);
namespace App\Form\Type;
use App\Entity\BackendUser;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class EncodedPasswordType extends AbstractType
{
private PasswordHasherFactoryInterface $passwordHasherFactory;
public function __construct(PasswordHasherFactoryInterface $passwordHasherFactory)
{
$this->passwordHasherFactory = $passwordHasherFactory;
}
/**
* @param mixed[] $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$eventDispatcher = $options['event_dispatcher'];
if (!$eventDispatcher instanceof EventDispatcherInterface) {
return;
}
$eventDispatcher->addListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
$form = $event->getForm();
$passwordField = $form->get('password');
if ($passwordField->isDisabled() ||
!$passwordField->isSubmitted() ||
!$passwordField->isValid()
) {
return;
}
$plainPassword = $passwordField->getData();
if (!is_string($plainPassword) || '' === $plainPassword) {
return;
}
/** @var User|BackendUser $user */
$user = $form->getData();
if (is_callable([$user, 'setPlainPassword'])) {
$user->setPlainPassword($plainPassword);
}
$passwordEncoder = $this->passwordHasherFactory->getPasswordHasher($user);
$user->setPassword($passwordEncoder->hash($plainPassword));
}, -200); // after validation
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'mapped' => false, // so encoder will have a chance to set on parent user object and not be overridden
'password_validation_groups' => [],
'create' => false,
'constraints' => static function (Options $options): array {
return [];
},
]);
// $resolver->setDefault('password_validation_groups', ['Default', 'Registration']);
$resolver->setAllowedTypes('password_validation_groups', ['array']);
$resolver->setRequired('event_dispatcher');
$resolver->setAllowedTypes('event_dispatcher', [EventDispatcherInterface::class, 'null']);
$resolver->setAllowedTypes('create', 'bool');
$resolver->setNormalizer('constraints', static function (Options $options, $constraints): array {
if (is_object($constraints)) {
$constraints = [$constraints];
}
$groups = $options['password_validation_groups'];
if (!is_array($groups)) {
throw new UnexpectedTypeException($groups, 'array');
}
if (count($groups) > 0) {
$constraints = array_merge([
new NotBlank([
'message' => 'password.empty',
'groups' => $groups,
]),
new Length([
'min' => 6,
'minMessage' => 'password.length',
'groups' => $groups,
]),
new Regex(
[
'pattern' => '/(?=.*\p{Lu})(?=.*[0-9])/',
'message' => 'password.pattern',
'groups' => $groups,
]
),
], $constraints);
} else {
if (!is_bool($options['create'])) {
throw new UnexpectedTypeException($options['create'], 'bool');
}
$isCreate = $options['create'];
$constraints = array_merge($constraints, [new Callback([
'callback' => static function ($value, ExecutionContextInterface $context) use ($isCreate): void {
if (!$isCreate || $value) {
return;
}
$context->buildViolation('password.empty')
->addViolation();
},
])]);
}
return $constraints;
});
}
public function getParent(): ?string
{
return PasswordType::class;
}
}