Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Registration and Password Hashing

Registration and Password Hashing

~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Without registration, NOBODY can log in – time for new users to sign themselves up, with securely hashed passwords.

Why passwords are NEVER stored in plain text

Achtung: A plain-text password in the database means: on EVERY database leak (hack, misconfiguration, careless staff access), ALL passwords are IMMEDIATELY compromised – AND, since many users reuse passwords, potentially their accounts on OTHER services too. A hash is a ONE-WAY transformation: the hash can be computed from the password, but NOT the reverse.

Creating the registration controller

php bin/console make:registration-form

Interactively asks for the user entity, whether email verification is wanted (for this chapter: no, chapter 38 covers sending emails separately) and generates a controller, form type, AND template.

src/Form/RegistrationFormType.php
<?php

declare(strict_types=1);

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, ['label' => 'Name'])
            ->add('email', EmailType::class, ['label' => 'Email'])
            ->add('plainPassword', PasswordType::class, [
                'label' => 'Password',
                'mapped' => false,
                'constraints' => [
                    new Assert\NotBlank(message: 'Please enter a password.'),
                    new Assert\Length(
                        min: 8,
                        minMessage: 'Your password must be at least {{ limit }} characters long.',
                    ),
                ],
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults(['data_class' => User::class]);
    }
}

'mapped' => false is DECISIVE: the plainPassword field does NOT get mapped automatically to a User property (the User entity, after all, only has a HASHED password field, chapter 26) – we read the plain-text value manually and hash it ourselves, instead of letting it end up unhashed in $user->password.

The registration controller

src/Controller/RegistrationController.php
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;

class RegistrationController extends AbstractController
{
    #[Route('/register', name: 'app_register')]
    public function register(
        Request $request,
        UserPasswordHasherInterface $passwordHasher,
        EntityManagerInterface $entityManager,
    ): Response {
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $plainPassword = $form->get('plainPassword')->getData();

            $user->setPassword(
                $passwordHasher->hashPassword($user, $plainPassword)
            );

            $entityManager->persist($user);
            $entityManager->flush();

            $this->addFlash('success', 'Registration successful! You can now log in.');
            return $this->redirectToRoute('app_login');
        }

        return $this->render('registration/register.html.twig', [
            'registrationForm' => $form,
        ]);
    }
}

UserPasswordHasherInterface is a service provided by Symfony (autowiring, chapter 8) – hashPassword($user, $plainPassword) AUTOMATICALLY uses the algorithm configured in security.yaml ('auto' from chapter 26 currently picks bcrypt or Argon2id, depending on the PHP environment).

Why $user as hashPassword()'s first argument?

Some hash algorithms (bcrypt among them) factor USER-SPECIFIC data (e.g. a "salt") into the computation – two users with an IDENTICAL password end up with DIFFERENT hashes in the database because of this, which makes targeted "rainbow table" attacks considerably harder.

Rendering the registration form

templates/registration/register.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Register</h1>
    {{ form(registrationForm) }}
{% endblock %}

Achtung: unique: true on the entity's email field (chapter 26) prevents DUPLICATE registrations at the database level – additionally add a #[Assert\Unique] or UniqueEntity constraint on the form type, so the user sees a READABLE error message instead of a raw database error if the email is already taken.