Registration Form

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Head back over to /register. We built this in our security tutorial. It does work... but we kind of cheated. Back in your editor, open src/Controller/SecurityController.php and find the register() method. Yep, it's pretty obvious: we did not use the form component. Instead, we manually read and handled the POST data. The template - templates/security/register.html.twig - is just a hardcoded HTML form.

Ok, first: even if you use and love the Form component, you do not need to use it in every single situation. If you have a simple form and want to skip it, sure! You can totally do that. But... our registration form is missing one key thing that all forms should have: CSRF protection. When you use the Form component. you get CSRF protection for free! And, usually, that's enough of a reason for me to use it. But, you can add CSRF protection without the form system: check out our login from for an example.

make:form

Let's refactor our code to use the form system. Remember step 1? Create a form class... like we did with ArticleFormType. That's pretty easy. But to be even lazier, we can generate it! Find your terminal and run:

php bin/console make:form

Call the class, UserRegistrationFormType. This will ask if you want this form to be bound to a class. That's usually what we want, but it's optional. Bind our form to the User class.

Nice! It created one new file. Find that and open it up!

... lines 1 - 9
class UserRegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('roles')
->add('firstName')
->add('password')
->add('twitterUsername')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

Customizing & Using UserRegistrationFormType

Cool. It set the data_class to User and even looked at the properties on that class and pre-filled the fields! Let's see: we don't want roles or twitterUsername for registration. And, firstName is something that I won't include either - the current form has just these two fields: email and password.

... lines 1 - 11
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('password')
;
}
... lines 19 - 27

Ok: step 2: go the controller and create the form! And, yes! I get to remove a "TODO" in my code - that never happens! Use the normal $form = this->createForm() and pass this UserRegistrationFormType::class. But don't pass a second argument: we want the form to create a new User object.

Then, add $form->handleRequest($request) and, for the if, use $form->isSubmitted() && $form->isValid().

... lines 1 - 14
class SecurityController extends AbstractController
{
... lines 17 - 44
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
$form = $this->createForm(UserRegistrationFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
... lines 51 - 72
}
}

Beautiful, boring, normal, code. And now that we're using the form system, instead of creating the User object like chumps, say $user = $form->getData(). I'll add some inline documentation so that PhpStorm knows what this variable is. Oh, and we don't need to set the email directly anymore: the form will do that! And I'll remove my firstName hack: we'll fix that in a minute.

... lines 1 - 44
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 47 - 49
if ($form->isSubmitted() && $form->isValid()) {
/** @var User $user */
$user = $form->getData();
... lines 53 - 67
}
... lines 69 - 72
}
... lines 74 - 75

About the password: we do need to encode the password. But now, the plain text password will be stored on $user->getPassword(). Hmm. That is a little weird: the form system is setting the plaintext password on the password field. And then, a moment later, we're encoding that and setting it back on that same property! We're going to change this in a few minutes - but, it should work.

... lines 1 - 49
if ($form->isSubmitted() && $form->isValid()) {
... lines 51 - 52
$user->setPassword($passwordEncoder->encodePassword(
$user,
$user->getPassword()
));
... lines 57 - 67
}
... lines 69 - 75

Down below when we render the template, pass a new registrationForm variable set to $form->createView().

... lines 1 - 44
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 47 - 69
return $this->render('security/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
... lines 74 - 75

Awesome! Let's find that template and get to work. Remove the TODO - we're killing it - then comment out all the old markup: I want to keep it for reference. Render with {{ form_start(registrationForm) }}, form_end(registrationForm) and, in the middle, render all of the fields with form_widget(registrationForm). Oh, and we need a submit button. Steal that from the old code and move it here.

... lines 1 - 10
{% block body %}
... lines 12 - 13
<div class="col-sm-12">
{{ form_start(registrationForm) }}
{{ form_widget(registrationForm) }}
<button class="btn btn-lg btn-primary btn-block" type="submit">
Register
</button>
{{ form_end(registrationForm) }}
... lines 22 - 40
</div>
... lines 42 - 43
{% endblock %}

Perfect! Let's go check this thing out! Refresh! Oh... wow... it looks terrible! Our old form code was using Bootstrap... but it was pretty customized. We will need to talk about how we can get back our good look.

Making firstName Optional

But, other than that... it seems to render fine! Before we test it, open your User entity class. We originally made the firstName field not nullable. That's the default value for nullable. So if you don't see nullable=true, it means that the field is required in the database.

Now, I do want to allow users to register without their firstName. No problem: set nullable=true.

... lines 1 - 13
class User implements UserInterface
{
... lines 16 - 33
/**
* @ORM\Column(type="string", length=255, nullable=true)
... line 36
*/
private $firstName;
... lines 39 - 245
}

Then, find your terminal and run:

php bin/console make:migration

Let's go check out that new file. Yep! No surprises: it just makes the column not required.

... lines 1 - 10
final class Version20181018165320 extends AbstractMigration
{
public function up(Schema $schema) : void
{
... lines 15 - 17
$this->addSql('ALTER TABLE user CHANGE first_name first_name VARCHAR(255) DEFAULT NULL');
}
... lines 20 - 27
}

Move back over and run this with:

php bin/console doctrine:migrations:migrate

Excellent! Let's try to register! Register as geordi@theenterprise.org, password, of course, engage. Hit enter and... nice! We are even logged in as Geordi!

Next: we have a problem! We're temporarily storing the plaintext password on the password field... which is a big no no! If something goes wrong, we might accidentally save the user's plaintext password to the database.

To fix that, we, for the first time, will add a field to our form that does not exist on our entity. An awesome feature called mapped will let us do that.

Leave a comment!

  • 2020-05-04 Diego Aguiar

    Hey codercoder123123

    Seems like you forgot to initialize your router property on its constructor

    Cheers!

  • 2020-05-01 codercoder123123

    Hello! Why this $url = $this->router->generate('user_activate', array('token' => $user->getToken()), self::ABSOLUTE_URL); generate me an error "Call to a member function generate() on null". Thank you!

  • 2020-05-01 codercoder123123

    Can I send you the code here? I did it, but it isn't here I don't know why.

  • 2020-05-01 codercoder123123

    Hello Diego!

  • 2020-05-01 codercoder123123

    Thank you Diego! Sorry for this late reply! Okay, this is what i did
    First i transformed my User class like this:
    firstName;
    }

    public function setFirstName(?string $name): self
    {
    $this->firstName = $name;

    return $this;
    }

    public function getLastName(): ?string
    {
    return $this->lastName;
    }

    public function setLastName(?string $name): self
    {
    $this->lastName = $name;

    return $this;
    }

    public function getEmail(): ?string
    {
    return $this->email;
    }

    public function setEmail(string $email): self
    {
    $this->email = $email;

    return $this;
    }

    /**
    * @return bool
    */
    public function getIsActive()
    {
    return $this->isActive;
    }

    /**
    * @param bool $isActive
    */
    public function setIsActive($isActive): void
    {
    $this->isActive = $isActive;
    }

    /**
    * @return mixed
    */
    public function getToken()
    {
    return $this->token;
    }

    /**
    * @param mixed $token
    */
    public function setToken($token): void
    {
    $this->token = $token;
    }


    /**
    * A visual identifier that represents this user.
    *
    * @see UserInterface
    */
    public function getUsername(): string
    {
    return (string) $this->email;
    }

    /**
    * @see UserInterface
    */
    public function getRoles(): array
    {
    $roles = $this->roles;
    // guarantee every user at least has ROLE_USER
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
    $this->roles = $roles;

    return $this;
    }

    public function addRole ($role){
    array_push($this->roles,$role);
    }

    /**
    * @see UserInterface
    */
    public function getPassword(): string
    {
    return (string) $this->password;
    }

    public function setPassword(string $password): self
    {
    $this->password = $password;

    return $this;
    }

    /**
    * @see UserInterface
    */
    public function getSalt()
    {
    // not needed when using the "auto" algorithm in security.yaml
    }

    /**
    * @see UserInterface
    */
    public function eraseCredentials()
    {
    // If you store any temporary, sensitive data on the user, clear it here
    // $this->plainPassword = null;
    }

    public function getGender(): ?string
    {
    return $this->gender;
    }

    public function setGender(string $gender): self
    {
    $this->gender = $gender;

    return $this;
    }

    public function getBirthday(): ?\DateTimeInterface
    {
    return $this->birthday;
    }

    public function setBirthday(\DateTimeInterface $birthday): self
    {
    $this->birthday = $birthday;

    return $this;
    }

    public function getAddress(): ?string
    {
    return $this->address;
    }

    public function setAddress(?string $address): self
    {
    $this->address = $address;

    return $this;
    }

    public function getTelephone(): ?string
    {
    return $this->telephone;
    }

    public function setTelephone(?string $telephone): self
    {
    $this->telephone = $telephone;

    return $this;
    }

    public function getClient(): ?Client
    {
    return $this->client;
    }

    public function setClient(Client $client): self
    {
    $this->client = $client;

    // set the owning side of the relation if necessary
    if ($client->getUser() !== $this) {
    $client->setUser($this);
    }

    return $this;
    }

    public function getMedicalStaff(): ?MedicalStaff
    {
    return $this->medicalStaff;
    }

    public function setMedicalStaff(MedicalStaff $medicalStaff): self
    {
    $this->medicalStaff = $medicalStaff;

    // set the owning side of the relation if necessary
    if ($medicalStaff->getUser() !== $this) {
    $medicalStaff->setUser($this);
    }

    return $this;
    }

    }

    This is my Mail class:
    mailer = $mailer;
    $this->router = $router;
    $this->twig = $twig;
    $this->logger = $logger;
    $this->noreply = $noreply;
    }

    /**
    * @param User $user
    * @throws \Exception
    * @throws \Throwable
    */
    public function sendActivationEmailMessage(User $user)
    {
    $url = $this->router->generate('user_activate', ['token' => $user->getToken()], UrlGeneratorInterface::ABSOLUTE_URL);

    $context = [
    'user' => $user,
    'activationUrl' => $url
    ];

    $this->sendMessage('registration/register-done.html.twig', $context, $this->noreply, $user->getEmail());
    }

    /**
    * @param User $user
    * @throws \Exception
    * @throws \Throwable
    */
    public function sendResetPasswordEmailMessage(User $user)
    {
    $url = $this->router->generate('user_reset_password', ['token' => $user->getToken()], UrlGeneratorInterface::ABSOLUTE_URL);

    $context = [
    'user' => $user,
    'resetPasswordUrl' => $url,
    ];

    $this->sendMessage('user/email/request-password.html.twig', $context, $this->noreply, $user->getEmail());
    }

    /**
    * @param $templateName string
    * @param $context array
    * @param $fromEmail string
    * @param $toEmail string
    * @throws \Exception
    * @throws \Throwable
    * @return bool
    */
    protected function sendMessage($templateName, $context, $fromEmail, $toEmail)
    {
    $context = $this->twig->mergeGlobals($context);
    $template = $this->twig->load($templateName);
    $subject = $template->renderBlock('subject', $context);
    $textBody = $template->renderBlock('body_text', $context);
    $htmlBody = $template->renderBlock('body_html', $context);

    $message = (new \Swift_Message())
    ->setSubject($subject)
    ->setFrom($fromEmail)
    ->setTo($toEmail);

    if (!empty($htmlBody)) {
    $message->setBody($htmlBody, 'text/html')->addPart($textBody, 'text/plain');
    } else {
    $message->setBody($textBody);
    }
    $result = $this->mailer->send($message);

    $log_context = ['to' => $toEmail, 'message' => $textBody, 'template' => $templateName];
    if ($result) {
    $this->logger->info('SMTP email sent', $log_context);
    } else {
    $this->logger->error('SMTP email error', $log_context);
    }

    return $result;
    }
    }

    This is TokeGenerator class:
    logger = $logger;
    // determine whether to use OpenSSL
    if (defined('PHP_WINDOWS_VERSION_BUILD') && version_compare(PHP_VERSION, '5.3.4', '<')) {
    $this->useOpenSsl = false;
    } elseif (!function_exists('openssl_random_pseudo_bytes')) {
    if (null !== $this->logger) {
    $this->logger->notice('It is recommended that you enable the "openssl" extension for random number generation.');
    }
    $this->useOpenSsl = false;
    } else {
    $this->useOpenSsl = true;
    }
    }

    /**
    * @return string
    */
    public function generateToken()
    {
    return rtrim(strtr(base64_encode($this->getRandomNumber()), '+/', '-_'), '=');
    }

    /**
    * @return string
    */
    private function getRandomNumber()
    {
    $nbBytes = 32;
    // try OpenSSL
    if ($this->useOpenSsl) {
    $bytes = openssl_random_pseudo_bytes($nbBytes, $strong);
    if (false !== $bytes && true === $strong) {
    return $bytes;
    }
    if (null !== $this->logger) {
    $this->logger->info('OpenSSL did not produce a secure random number.');
    }
    }
    return hash('sha256', uniqid(mt_rand(), true), true);
    }
    }

    And this is my UserController:
    createForm(RegistrationType::class);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
    $user = $form->getData();
    // encode the plain password
    $user->setPassword($encoder->encodePassword($user, $user->getPassword()));
    $token = $tokenGenerator->generateToken();
    $user->setToken($token);
    $user->setIsActive(false);

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

    if (self::DOUBLE_OPT_IN) {
    $mailer->sendActivationEmailMessage($user);
    $this->addFlash('success', 'user.activation-link');
    return $this->redirect($this->generateUrl('/homepage'));
    }

    return $this->render('registration/register.html.twig.', [
    'registrationForm' => $form->createView(),
    ]);
    }
    }
    /**
    * @Route("/activate/{token}", name="activate")
    */
    public function activate(Request $request, User $user, GuardAuthenticatorHandler $authenticatorHandler, LoginFormAuthenticator $loginFormAuthenticator)
    {
    $user->setIsActive(true);
    $user->setToken(null);

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

    $this->addFlash('success', 'user.welcome');

    // automatic login
    return $authenticatorHandler->authenticateUserAndHandleSuccess(
    $user,
    $request,
    $loginFormAuthenticator,
    'main'
    );
    }

    /**
    * @Route("/",name="app_profile_main")
    */
    public function helloPerson(){
    return new Response("<h1> Hello ".$this->getUser()->getEmail()."</h1>");
    }

    /**
    * @Route("/makeReservation/{id}",name="app_profile_reservation")
    */
    public function makeReservation(Service $service, Request $request, EntityManagerInterface $manager)
    {
    $reservation = new Reservation();
    $reservation->setClient($this->getUser()->getClient());
    $reservation->setService($service);
    $form = $this->createForm(ReservationFormType::class);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
    $reservation->setMedicalStaff($form->get('medicalStaff')->getData());
    $reservation->setDay($form->get('day')->getData());
    $reservation->setStatus($form->get('status')->getData());
    $manager->persist($reservation);
    $manager->flush();
    echo '<div style="background-color:white; color:black;">U shtua rezervimi</div>';
    }

    return $this->render('user/reservation.html.twig', [
    'reservationForm' => $form->createView(),
    ]);
    }
    }

    When I fill my registration form returns me this return new Response("<h1> Hello ".$this->getUser()->getEmail()."</h1>"); and register the user without sending any email adress. Also i have configure switfmailer in gmail a i have test it in terminal and send email perfectly.

  • 2020-04-30 Diego Aguiar

    Hey codercoder123123

    Can you tell us what problem did you get so I can help you out?

    Cheers!

  • 2020-04-30 codercoder123123

    Hello! Sorry can we communicate i have a version for this but doesnt work yet. Thank you for everything!

  • 2020-04-30 weaverryan

    Hey codercoder123123!

    It's funny you ask about that - we're working on a bundle to help with this right now - https://github.com/SymfonyC... - unfortunately it's not ready yet!

    The easiest way to accomplish this feature is this:

    A) Add a isEmailConfirmed property to your User entity and default it to false
    B) Add another field called emailVerificationToken to your User entity and set it to some random string - you can use this code - https://github.com/FriendsO... - you could do that in the constructor of your User entity, or set it in the controller right after a successful registration.
    C) Create a route + controller - something like /email/verify/{token}. In that method, you would look at the currently-authenticated user and, if their emailVerificationToken matches the {token} in the URL, you will call $user->setIsEmailConfirmed(true) and save.
    D) Finally, after registration, use SwiftMailer (in your case) to send an email that includes a link to the route you generated in (C).

    It's not overly complicated - it just takes a bit of work. Let me know if you have any questions. The bundle we're working on will not require a emailVerificationToken property on your user entity, but if you're building this by hand, I'd recommend leveraging a property like that... as it makes life simpler.

    Cheers!

  • 2020-04-30 codercoder123123

    Hello guys! I advanced with registration in Symfony 4 and i make the registration form but i want to send a confirmation email wth a link. What can i do? I am using SwiftMailer?

  • 2019-01-31 Diego Aguiar

    Hey Amin

    This problem is related to a release of Doctrine migration package, we're investigating the issue but at least you can read a couple of solutions that Ryan provides here: https://symfonycasts.com/sc...

    Cheers!

  • 2019-01-31 Amin

    yes, I think autogenerated namespace is wrong, in a (start) folder from Symfony Security tutorials AbstractMigration.php live in two places: ..\vendor\doctrine\migrations\lib\Doctrine\DBAL\Migrations\AbstractMigration.php ..\vendor\doctrine\migrations\lib\Doctrine\Migrations\AbstractMigration.php

    but the start folder from Form tutorials is only in
    ..\vendor\doctrine\migrations\lib\Doctrine\Migrations\AbstractMigration.php I try to reinstall and update bundles and

    this did not fix the problem, DBAL folder is not exist in the new tutorial code Form Tutorials: ..\vendor\doctrine\migrations\lib\Doctrine\ folder

  • 2019-01-30 Diego Aguiar

    Ohh, so the autogenerated namespace was wrong, or what was the problem?
    Did you fix it already?

  • 2019-01-30 Amin

    i'm sorry I am new with symfony but I’m sure that
    Doctrine migrations bundle is installed and everything was good until 14 - Registration Form.
    The whole message is:

    php ./bin/console make:migration
    Fatal error: Class 'Doctrine\DBAL\Migrations\AbstractMigration' not found in D:\wamp64\www\the_spacebar\src\Migrations\Version20180413174059.php on line 12
    15:22:39 CRITICAL [php] Fatal Error: Class 'Doctrine\DBAL\Migrations\AbstractMigration' not found ["exception" => Symfony\Component\Debug\Exception\FatalErrorException { …}]
    In Version20180413174059.php line 12:

    Attempted to load class "AbstractMigration" from namespace "Doctrine\DBAL\Migrations".
    Did you forget a "use" statement for "Doctrine\Migrations\AbstractMigration"?

    when I search for (AbstractMigration.php) I funded in:
    doctrine\migrations\lib\Doctrine\Migrations

    thanks

  • 2019-01-29 Diego Aguiar

    Hey Amin

    I believe you haven't installed the Doctrine migrations bundle. Once you install it you should not have this error anymore (unless I'm missing something :p)


    composer require symfony/maker-bundle --dev
  • 2019-01-29 Amin

    hello
    wen i try to create migration (php bin/console make:migration) for updating user firstname property I got an error:
    Attempted to load class "AbstractMigration" from namespace "Doctrine\DBAL\Migrations".
    Did you forget a "use" statement for "Doctrine\Migrations\AbstractMigration"?

    can you help me with this?