Profile - Part 1 - Happy Path


In this video we are going to get started adding in the User's Profile functionality into our Symfony 3 RESTful API.

If you are familiar with the FOSUserBundle profile options then you will already know the features we are about to add in.

If not, then simply this will allow a User to read their current profile (GET), and make changes (PUT / PATCH).

There is no use case for a User being able to delete their own profile data. If they were to leave the 'site' for whatever reason then removal of their profile would be handled by an administrator, or their account would be marked as inactive / dormant in some way.

Likewise, the User cannot create (POST) a new Profile. This happens during the registration flow, which will be covered in a future video.

Behat Feature Spec

As with our Login flow, we are going to start by writing a Behat feature containing the "happy path" - the path that, should everything go as planned, the User will get the desired outcome.

In this case, we want a logged in user to be able to access their own profile, receiving a JSON response containing their id, username, and email address information.

# /src/AppBundle/Features/profile.feature

Feature: Manage User profile data via the RESTful API

  In order to allow a user to keep their profile information up to date
  As a client software developer
  I need to be able to let users read and update their profile

  Background:
    Given there are Users with the following details:
      | id | username | email          | password |
      | 1  | peter    | peter@test.com | testpass |
      | 2  | john     | john@test.org  | johnpass |
    And I am successfully logged in with username: "peter", and password: "testpass"
    And I set header "Content-Type" with value "application/json"

  Scenario: Can view own profile
    When I send a "GET" request to "/profile/1"
    Then the response code should be 200
     And the response should contain json:
      """
      {
        "id": "1",
        "username": "peter",
        "email": "peter@test.com"
      }
      """

Thankfully - and as you will find throughout this series - we don't need to write any more Behat step definitions. We have them all already.

Note - our Behat Background steps ensure our database has the expected User accounts, and that our User will be successfully logged in before any tests begin.

At this stage our test should run, and of course, it should fail - as we haven't implemented any part of the Profile journey.

Towards The Profile Happy Path

The first thing I will do is add in the routing setup to hook up an as-yet-uncreated RestProfileController file.

# /app/config/routing_rest.yml

profile:
    type: rest
    resource: AppBundle\Controller\RestProfileController

With this in place I can go ahead and create the RestProfileController and add in my implementation.

A side note here - the naming of my controller is totally made up. There's no need to call it this, call it whatever you like.

<?php

// /src/AppBundle/Controller/RestProfileController.php

namespace AppBundle\Controller;

use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\RestBundle\Controller\Annotations\RouteResource;

/**
 * @RouteResource("profile", pluralize=false)
 */
class RestProfileController extends FOSRestController implements ClassResourceInterface
{
    /**
     * @Get("/profile/{user}")
     */
    public function getAction()
    {
    }
}

A few things to note here.

I am declaring the RouteResource manually to stop any potential naming mishaps. It's force of habit for me to do this now, just so that I can control the route naming myself.

Next, I am implementing ClassResourceInterface which should ensure that we can name our controller action as getAction, instead of getProfileAction, and that this should figure out the route path accordingly. Again though, force of habit for me now is to explicitly define the route path myself, just to stop any potential mishaps.

I leave in ClassResourceInterface as sometimes I do use the short hand action names without defining a path, and this gives me that option.

Lastly, in our path we define the {user} placeholder, and you may usually do this as {userId}. To clarify here, I am expecting a user ID, but I want to use a ParamConverter to transform the given ID into a User entity so I can immediately use that entity in my action.

Let's see that in action:

<?php

// /src/AppBundle/Controller/RestProfileController.php

namespace AppBundle\Controller;

use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @RouteResource("profile", pluralize=false)
 */
class RestProfileController extends FOSRestController implements ClassResourceInterface
{
    /**
     * @Get("/profile/{user}")
     *
     * @ParamConverter("user", class="AppBundle:User")
     *
     * Note: Could be refactored to make use of the User Resolver in Symfony 3.2 onwards
     * more at : http://symfony.com/blog/new-in-symfony-3-2-user-value-resolver-for-controllers
     */
    public function getAction(UserInterface $user)
    {
        if ($user !== $this->getUser()) {
            throw new AccessDeniedHttpException();
        }

        return $user;
    }
}

By using the ParamConverter we can skip the step of having to grab the entity manager, go to the repository, grab the user, etc. Instead, we just inject the $user and crack on.

We do need to check that the requested User is who they say they are.

We can not stop a User from sending in a request for a User's profile that they don't have access to:

  • GET http://my.api/profile/1
  • GET http://my.api/profile/99

But we can stop them from actually getting the response if the User they are requesting does not match the User represented by their token. Any naughty behaviour will be met with the AccessDeniedHttpException.

And then we can simply return the $user entity itself, and FOSRESTBundle will ensure it is serialized into JSON for us.

Now, this introduces a problem in that unless we explicitly say otherwise, all our User entity fields will be exposed. This is highly unlikely to be the outcome we desire, but Behat won't actually care.

Note that in our Behat scenario we have this:

# /src/AppBundle/Features/profile.feature

  Scenario: Can view own profile
    When I send a "GET" request to "/profile/1"
    Then the response code should be 200
     And the response should contain json:
      """
      {
        "id": "1",
        "username": "peter",
        "email": "peter@test.com"
      }
      """

And by now our scenario should be passing. After all, we are logged in as user "peter" and we are requesting Peter's profile. Everything checks out, so our User entity is returned. In full.

Because the $user entity contains id, username, and email, our test passes.

However, it also contains a bunch of other things, like the encoded password and the salt used :/

All our test checks for is the inclusion of those three fields. Are they there? Yes, awesome, this test passes.

Ok, so the counter measures to this are to add in extra checks in our Behat feature that say we are definitely not exposing those fields. We should also add in additional annotations to our entity to ensure only whitelisted fields are exposed in serialization. We will do this towards the end of this series.

Code For This Course

Get the code for this course.

Episodes