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.