Registration - Part 1 - Happy Path


In this video we are going to get started with perhaps the most interesting part of this whole puzzle - allowing new users to register with our API.

As we have covered so far throughout this series, this is going to involve making use of the existing FOSUserBundle registration implementation, changing as needed to meet the requirements of accepting requests, and generating responses as JSON.

Let's start by looking at the Behat feature covering the "happy path" - the journey that, if taken, should result in a user successfully registering with our system:

# /src/AppBundle/Features/register.feature

Feature: Handle user registration via the RESTful API

  In order to allow a user to sign up
  As a client software developer
  I need to be able to handle registration

  Background:
    Given there are Users with the following details:
      | id | username | email          | password |
      | 1  | peter    | peter@test.com | testpass |
    And I set header "Content-Type" with value "application/json"

  Scenario: Can register with valid data
    When I send a "POST" request to "/register" with body:
      """
      {
        "email": "chris@codereviewvideos.com",
        "username": "chris",
        "plainPassword": {
          "first": "abc123",
          "second": "abc123"
        }
      }
      """
    Then the response code should be 201
     And the response should contain "The user has been created successfully"
    When I am successfully logged in with username: "chris", and password: "abc123"
     And I send a "GET" request to "/profile/2"
     And the response should contain json:
      """
      {
        "id": "2",
        "username": "chris",
        "email": "chris@codereviewvideos.com"
      }
      """

Now, it may be that you need additional information to allow a user to successfully register. This may be something akin to a Stripe token, plan, and optional coupon code:

  {
    "email": "chris@codereviewvideos.com",
    "username": "chris",
    "plainPassword": {
      "first": "abc123",
      "second": "abc123"
    },
    "payment_information": {
      "processor": "stripe",
      "token": "123-abc-xyz",
      "plan": "plan_2",
      "coupon": "a valid coupon"
    }
  }

Whilst adding in this additional information is outside the scope of this current tutorial series, be aware that it is possible by overriding a FOSUserBundle form.

Ok, so that's the structure / shape of our data.

We know it's going to be POST'ed in as JSON, and that if everything sent in is valid, then the system should respond with a 201 created status code.

The interesting part about a successful registration is that aside from the email, username, and password, we know almost nothing about the user.

What we need is the id field that represents the newly created user entity, and optionally, any other information we feel is important to expose.

Now, as we are making use of JWT (JSON Web Tokens), we can encode any fields we want inside the JWT, and then let the client / front end code decode this token and extract the information as needed. This information is not encrypted - it is merely encoded - so don't expose anything you wouldn't want publicly available. We will cover how to do this in an upcoming video, but for now, just know it is possible.

We will return this token along with a message - the message normally created by FOSUserBundle:

"The user has been created successfully"

In our test we continue further. Once we have the 201 status code, we can assume that our user has been created. Rather than worry about extracting the token from the response, instead we will send in our username and password credentials, making use of the existing step definitions to ensure we are logged in, and our Authorization header is set.

If we now send GET request to the /profile/2 endpoint, we should see our profile information.

Note here that we can infer that we will get an id of 2, simply because our Background step only inserts one existing user into the database, and that our users database table has an auto incremented id field for new records.

Implementing The Controller

Of course, this test won't actually pass at the moment. After all, we have not yet written any code.

Let's take a look at the controller implementation:

<?php

namespace AppBundle\Controller;

use FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * @RouteResource("registration", pluralize=false)
 */
class RestRegistrationController extends FOSRestController implements ClassResourceInterface
{
    /**
     * @Annotations\Post("/register")
     */
    public function registerAction(Request $request)
    {
        /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */
        $formFactory = $this->get('fos_user.registration.form.factory');
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');
        /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user = $userManager->createUser();
        $user->setEnabled(true);

        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, $event);

        if (null !== $event->getResponse()) {
            return $event->getResponse();
        }

        $form = $formFactory->createForm([
            'csrf_protection'    => false
        ]);

        $form->setData($user);
        $form->submit($request->request->all());

        if ( ! $form->isValid()) {

            $event = new FormEvent($form, $request);

            $dispatcher->dispatch(FOSUserEvents::REGISTRATION_FAILURE, $event);

            if (null !== $response = $event->getResponse()) {
                return $response;
            }

            return $form;
        }

        $event = new FormEvent($form, $request);
        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);

        if ($event->getResponse()) {
            return $event->getResponse();
        }

        $userManager->updateUser($user);

        $response = new JsonResponse(
            [
                'msg' => $this->get('translator')->trans('registration.flash.user_created', [], 'FOSUserBundle'),
                'token' => 'abc-123' // some way of creating the token
            ],
            JsonResponse::HTTP_CREATED,
            [
                'Location' => $this->generateUrl(
                    'get_profile',
                    [ 'user' => $user->getId() ],
                    UrlGeneratorInterface::ABSOLUTE_URL
                )
            ]
        );

        $dispatcher->dispatch(
            FOSUserEvents::REGISTRATION_COMPLETED,
            new FilterUserResponseEvent($user, $request, $response)
        );

        return $response;
    }
}

This controller action has the most change from the original FOSUserBundle implementation.

For the purposes of our test, the most interesting part is around the construction of the JsonResponse.

The msg we use comes directly from FOSUserBundle. That's fine.

The token, well we will cover how that is created in a future video. For the moment, we fake it and continue.

The JsonResponse::HTTP_CREATED is a constant representing the number 201, the status code we expect.

Then, interestingly, we need to go ahead and create the Location header. As covered, we will encode and include the user's id inside their JWT. By using the location header we can simplify things for our API consumers, and send them a direct link to the newly created resource.

In this instance we will generate a link to the get_profile route. The profile endpoint will expect a valid user id to be passed in, so as the second parameter to generateUrl we pass in an array containing any routing placeholders, and their value(s). In this case, our route expects:

/profile/{user}

And so we pass in the outcome of $user->getId() as the replacement for this placeholder.

In this instance it is best to send an absolute URL, so we pass in the third parameter - UrlGeneratorInterface::ABSOLUTE_URL - to ensure we generate a full URL, instead of just a path.

Registering Our Route

If we were to run the tests now, we would still not have a pass. We would get a 404, as Symfony is not yet aware of our newly created controller.

Resolving this issue is straightforward:

# /app/config/routing_rest.yml

registration:
    type: rest
    resource: AppBundle\Controller\RestRegistrationController

But still, we do not have a passing test. We now get a 401.

This indicates we have a security related concern. We will address this problem - and more - in the very next video.

Code For This Course

Get the code for this course.

Episodes