If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet's add a registration form to our site. There's a funny thing about registration forms: they have basically nothing to do with security! Think about it: the point of a registration form is just to insert new users into the database. So creating a registration form is really not any different than creating a form to insert any data into your database.
And to make things even simpler, we're going to cheat... by generating code. Find your terminal and run:
symfony console make:registration-form
Ooh! This gives us an error! It says:
Missing packages: run
composer require form validator
In this Symfony 5 series, we haven't talked about the Form component. And that's in part because it hasn't changed much since our Symfony 4 tutorial. We're not going to go into too much detail about it right now, but we do need it to run this command. So let's install both packages:
composer require form validator
Awesome. When that finishes, run:
symfony console make:registration-form
again. Cool! So the first question asks:
Do we want to add a
@UniqueEntity
validation annotation to ourUser
class to make sure duplicate accounts aren't created.
You almost definitely want to say "Yes" so that the user gets a validation error if they enter an email that's already taken.
Next:
Do you want to send an email to verify the user's email address after registration?
We're going to add this later, but I want to do it manually. So say "No".
Do you want to automatically authenticate the user after registration?
That sounds awesome, but say "No", because we're also going to do that manually. I know, I'm making us work! The last question is:
What route should the user be redirected to after registration?
Let's just use our homepage route. So that's number 16 for me. And... done!
This command just gave us a RegistrationController
, a form type, and a template that renders that form. Let's... go check that stuff out!
Start with the controller: src/Controller/RegistrationController.php
:
... lines 1 - 12 | |
class RegistrationController extends AbstractController | |
{ | |
/** | |
* @Route("/register", name="app_register") | |
*/ | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher): Response | |
{ | |
$user = new User(); | |
$form = $this->createForm(RegistrationFormType::class, $user); | |
$form->handleRequest($request); | |
if ($form->isSubmitted() && $form->isValid()) { | |
// encode the plain password | |
$user->setPassword( | |
$userPasswordHasher->hashPassword( | |
$user, | |
$form->get('plainPassword')->getData() | |
) | |
); | |
$entityManager = $this->getDoctrine()->getManager(); | |
$entityManager->persist($user); | |
$entityManager->flush(); | |
// do anything else you need here, like send an email | |
return $this->redirectToRoute('app_homepage'); | |
} | |
return $this->render('registration/register.html.twig', [ | |
'registrationForm' => $form->createView(), | |
]); | |
} | |
} |
Again, we're not going to talk much about the Form component. But, on a high level, this controller creates a User
object and then, on submit, it hashes the plain password that was submitted and then saves the User
. This is exactly the same thing that we're doing in our fixtures to create users: there's nothing special about this at all.
So... let's see what this looks like! Head over to /register
to see... the world's ugliest form! We... can do better. The template for this page is registration/register.html.twig
. Open that up:
{% extends 'base.html.twig' %} | |
{% block title %}Register{% endblock %} | |
{% block body %} | |
<h1>Register</h1> | |
{{ form_start(registrationForm) }} | |
{{ form_row(registrationForm.email) }} | |
{{ form_row(registrationForm.plainPassword, { | |
label: 'Password' | |
}) }} | |
{{ form_row(registrationForm.agreeTerms) }} | |
<button type="submit" class="btn">Register</button> | |
{{ form_end(registrationForm) }} | |
{% endblock %} |
and... I'm just going to add a couple of divs to give this more structure. Awesome... then indent all of this form stuff to be inside of those... and then we just need 3 closing divs on the bottom:
... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="bg-light mt-4 p-4"> | |
<h1>Register</h1> | |
{{ form_start(registrationForm) }} | |
{{ form_row(registrationForm.email) }} | |
{{ form_row(registrationForm.plainPassword, { | |
label: 'Password' | |
}) }} | |
{{ form_row(registrationForm.agreeTerms) }} | |
<button type="submit" class="btn">Register</button> | |
{{ form_end(registrationForm) }} | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Cool. That doesn't really fix the form... but at least our ugly form sort of appear in the center of the page. Oh, but let me fix my typo on the mt-4
. And... yea, that looks better.
To fix the form itself, we can tell Symfony to output the form with markup that's Bootstrap 5-friendly. This is... kind of a topic for the form tutorial, but it's easy. Go to config/packages/twig.yaml
. Here, add an option called form_themes
with one new item: boostrap_5_layout.html.twig
:
twig: | |
default_path: '%kernel.project_dir%/templates' | |
form_themes: | |
- bootstrap_5_layout.html.twig | |
... lines 6 - 9 |
Try it now and... woh! That made a huge difference! Oh, but let me add one more class to that registration button... so that it's not invisible: btn-primary
:
... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="bg-light mt-4 p-4"> | |
... lines 9 - 10 | |
{{ form_start(registrationForm) }} | |
... lines 12 - 17 | |
<button type="submit" class="btn btn-primary">Register</button> | |
{{ form_end(registrationForm) }} | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Cool.
And while we're making things look and work nicely, we can finally make the "Sign up" button.. actually go somewhere. In base.html.twig
, search for "Sign up" - here it is - set the href
to path()
and target the new route, which... if we look... is called app_register
:
... lines 1 - 12 | |
class RegistrationController extends AbstractController | |
{ | |
/** | |
* @Route("/register", name="app_register") | |
*/ | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher): Response | |
{ | |
... lines 20 - 43 | |
} | |
} |
So path('app_register')
:
... line 1 | |
<html> | |
... lines 3 - 14 | |
<body | |
... lines 16 - 21 | |
<nav | |
class="navbar navbar-expand-lg navbar-light bg-light px-1" | |
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }} | |
> | |
<div class="container-fluid"> | |
... lines 27 - 35 | |
<div class="collapse navbar-collapse" id="navbar-collapsable"> | |
... lines 37 - 47 | |
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %} | |
... lines 49 - 73 | |
{% else %} | |
... line 75 | |
<a href="{{ path('app_register') }}" class="btn btn-dark">Sign up</a> | |
{% endif %} | |
</div> | |
</div> | |
</nav> | |
... lines 81 - 85 | |
</body> | |
</html> |
Beautiful!
This would now work if we tried it. But, before we do, I want to add one other feature to this. After successfully submitting the registration form, I want to automatically authenticate the user. Is that possible? Of course! Let's do it next.
Hey Bartlomeij,
This chapter may help you understand how to get the error message on a failed login attempt. https://symfonycasts.com/screencast/symfony-security/errors-logout
Cheers!
Thank you for your answer. I finished the course, and the video didn't give an answer. I needed to disable turbo in registration form to see form validation errors with data-turbo="false"
Ohh, you're using Turbo. Sometimes Turbo does unexpected things. I believe the error is because you're not returning the exact HTTP error code that Turbo requires. Try setting the response status code to "422" or, if you're using the Symfony Form component, you can use this controller shortcut method $this->renderForm()
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
Hello guys and gals!
user registration happy path works fine, but I have another problem.
When a user tries to register with an email that is already registered in the database, enters too short password or makes any other mistake - the account is obviously not created, but no error is shown. There is no message what went wrong.
I figured out the problem occurs only when Turbo is enabled. Any idea how to fix it?
Best!