Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Command! make:registration-form #333

Merged
merged 6 commits into from
Dec 20, 2018

Conversation

weaverryan
Copy link
Member

@weaverryan weaverryan commented Dec 14, 2018

New command time! make:registration-form. It:

  • Generates a completely functional controller, template & form class for registration
  • Is capable of detecting or asking for the exact fields you need (e.g. email & password)
  • When using Guard, you can generate code to automatically authenticate after registration.

This is one of the last steps to getting an entire authentication system in minutes without needing FOSUserBundle (make:user, make:auth, make:registration-form).

Feedback warmly welcomed.

Cheers!

Example of generated code:

RegistrationFormType

<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email')
            ->add('plainPassword', PasswordType::class, [
                // instead of being set onto the object directly,
                // this is read and encoded in the controller
                'mapped' => false,
                'constraints' => [
                    new NotBlank([
                        'message' => 'Please enter a password',
                    ]),
                    new Length([
                        'min' => 6,
                        'minMessage' => 'Your password should be at least {{ limit }} characters',
                    ]),
                ],            ])
        ;
    }

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

RegistrationController

namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\StubAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;

class RegistrationController extends AbstractController
{
    /**
     * @Route("/register", name="app_register")
     */
    public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, StubAuthenticator $authenticator): Response
    {
        $form = $this->createForm(RegistrationFormType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /** @var User */
            $user = $form->getData();

            // encode the plain password
            $user->setPassword(
                $passwordEncoder->encodePassword(
                    $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 $guardHandler->authenticateUserAndHandleSuccess(
                $user,
                $request,
                $authenticator,
                'main' // firewall name in security.yaml
            );
        }

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

registration/register.html.twig

{% extends 'base.html.twig' %}

{% block title %}Register{% endblock %}

{% block body %}
    <h1>Register</h1>

    {{ form_start(registrationForm) }}
        {{ form_row(registrationForm.email) }}
        {{ form_row(registrationForm.plainPassword) }}

        <button class="btn">Register</button>
    {{ form_end(registrationForm) }}
{% endblock %}

@weaverryan weaverryan force-pushed the make-registration-form branch from a5141f0 to f3f4901 Compare December 15, 2018 17:42
@weaverryan
Copy link
Member Author

This should be ready to go! Added example generated output in the PR description.

final class MakeRegistrationForm extends AbstractMaker
{
private $fileManager;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the empty line breaks part of the CS guidelines?

{
$this->email = $email;

return $this;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we discourage the fluent interfaces for entities, especially as they seem unused in the rest of the code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that we don't use the fluent interface ourselves. However, adding it means that end-users can choose between using it as a fluent interface or not.

If we remove this fluent interface, we remove that flexibility entirely.

@weaverryan weaverryan merged commit f3f4901 into symfony:master Dec 20, 2018
weaverryan added a commit that referenced this pull request Dec 20, 2018
This PR was squashed before being merged into the 1.0-dev branch (closes #333).

Discussion
----------

New Command! make:registration-form

New command time! `make:registration-form`. It:

* Generates a completely functional controller, template & form class for registration
* Is capable of detecting or asking for the exact fields you need (e.g. email & password)
* When using Guard, you can generate code to automatically authenticate after registration.

This is one of the last steps to getting an entire authentication system in minutes without needing FOSUserBundle (`make:user`, `make:auth`, `make:registration-form`).

Feedback warmly welcomed.

Cheers!

### Example of generated code:

<details><summary>RegistrationFormType</summary>
<p>

```php
<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email')
            ->add('plainPassword', PasswordType::class, [
                // instead of being set onto the object directly,
                // this is read and encoded in the controller
                'mapped' => false,
                'constraints' => [
                    new NotBlank([
                        'message' => 'Please enter a password',
                    ]),
                    new Length([
                        'min' => 6,
                        'minMessage' => 'Your password should be at least {{ limit }} characters',
                    ]),
                ],            ])
        ;
    }

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

</p>
</details>

<details><summary>RegistrationController</summary>
<p>

```php
namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\StubAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;

class RegistrationController extends AbstractController
{
    /**
     * @route("/register", name="app_register")
     */
    public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, StubAuthenticator $authenticator): Response
    {
        $form = $this->createForm(RegistrationFormType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /** @var User */
            $user = $form->getData();

            // encode the plain password
            $user->setPassword(
                $passwordEncoder->encodePassword(
                    $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 $guardHandler->authenticateUserAndHandleSuccess(
                $user,
                $request,
                $authenticator,
                'main' // firewall name in security.yaml
            );
        }

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

</p>
</details>

<details><summary>registration/register.html.twig</summary>
<p>

```twig
{% extends 'base.html.twig' %}

{% block title %}Register{% endblock %}

{% block body %}
    <h1>Register</h1>

    {{ form_start(registrationForm) }}
        {{ form_row(registrationForm.email) }}
        {{ form_row(registrationForm.plainPassword) }}

        <button class="btn">Register</button>
    {{ form_end(registrationForm) }}
{% endblock %}
```

</p>
</details>

Commits
-------

f3f4901 making test 3.4-compat
8f46cc2 Asking to add UniqueEntity during make:registration-form
a018245 Adding plainPassword validation and a few other things
16d3b9b Making the make:registration-form work without authenticating
14ade82 Adding the initial version of make:registration-form 🎉
b7699a1 Refactoring form rendering into a service
@lyrixx
Copy link
Member

lyrixx commented Dec 21, 2018

Hello. I'm a bit late.

Why doing that:

        $form = $this->createForm(RegistrationFormType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /** @var User */
            $user = $form->getData();

Instead of:

        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

The last one is IMHO simpler to understand and less magical. Everything is explicit, and there are no need for this PHP Doc

@lyrixx
Copy link
Member

lyrixx commented Dec 21, 2018

Another question: why do you put the password transformation logic in the controller and not in a form listener ?

Another question: Why do you authenticate the user? This si considered as a bad practice (the email is not yet validated) This could be let as an example, but commented with an explanation

Finally, in your example, the StubAuthenticator seems useless

@weaverryan
Copy link
Member Author

@lyrixx

Why doing that: ... Instead of:

I think your idea is indeed more clear. I'll open a pull request.

Another question: why do you put the password transformation logic in the controller and not in a form listener ?

Form listeners are way too complex - I think doing a few things in the controller is easy and clear.

Another question: Why do you authenticate the user? This is considered as a bad practice (the email is not yet validated) This could be let as an example, but commented with an explanation

We ask the user whether or not they want this - so they can choose what they want :)

Finally, in your example, the StubAuthenticator seems useless

It is - but that's only because I messed up when copying (and simplifying) the code. It's all used correctly in your real app - if you choose to authenticate, you choose which authenticator, then we use that to authenticate the user StubAuthenticator is from our test suite.

@lyrixx
Copy link
Member

lyrixx commented Dec 22, 2018

Thanks for your feedback 👍

weaverryan added a commit that referenced this pull request Dec 24, 2018
…licit (weaverryan)

This PR was merged into the 1.0-dev branch.

Discussion
----------

Fixing phpcs and making the registration controller more explicit

Fixes #339 and #333 (comment)

Commits
-------

4e4fe06 Fixing phpcs and making the registration controller more explicit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants