Symfony a RESTFul app: REST Levels 0, 1, 2 ( FOSUserBundle )

In the previous post we set up a basic symfony project aiming to develop a RESTFul solution.

Real projects need to work with databases and persistent data so this post will add this new interaction with the basic entities of all the users.

As always all the code on this series can be found on this github repository under the symfony2_restful_example branch.

Customer:

Lets define our Customer model following the clean architecture on a directory called Customer. Why Customer and not User, since the terminology of user can be confuse some times, and because “user” is a reserved word on common databases; it will be better to use a more clear term, you can change it to something else if you feel it will be better, or feet your business bests.

<?php

namespace Customer;

use FOS\UserBundle\Model\User as BaseUser;

/**
 * Customer
 */
class Customer extends BaseUser
{

    /**
     * @var string
     */
    protected $id;

    /**
     * Customer constructor.
     * @param $id
     */
    public function __construct()
    {
        $this->id = $this->id ? $this->id : uniqid();
        parent::__construct();
    }

    /**
     * Get id
     *
     * @return string
     */
    public function getId()
    {
        return $this->id;
    }
}

and add it to the autoload

$loader->addPsr4("Customer\\", __DIR__.'/../../Customer', true);

Now lets integrate that model with doctrine creating an entity class on the Entity directory under the bundle

use Customer\Customer as CustomerBase;

/**
 * Customer ORM Entity
 */
class Customer extends CustomerBase
{
}

and the mapping on the Resources/config/doctrine under the bundle

AppBundle\Entity\Customer:
    type: entity
    table: null
    repositoryClass: AppBundle\Entity\CustomerRepository
    id:
        id:
            type: string
            length: 255
    lifecycleCallbacks: {  }

This is a simple customer model with only the id. You may notice that the id its an string, and not the usual integer auto-incremental; this its because we will be using UUID, this is a good practice i learn from Mathias Verraes, for that notice that the constructor will set that id.

FOSUserBundle:

This great bundle comes to be so popular that even have its own page on the symfony documentation. It expose configurations and enable features that allow us to build the common interaction of users, with our systems; on symfony in a quick and easy way. So lets follow the steps described on the page.

First lets enable the bundle on the AppKernel, then lets configure the bundle with the basic configuration:

fos_user:
    db_driver: orm
    firewall_name: main
    user_class: AppBundle\Entity\Customer

Finally we need to all the fields that suppose to be on a customer/user table, FOSUserBundle propose to use to use and extends their common base class, its a complete structure that define a common users interaction and implements the Symfony user Interfaces. But this class has one back draw, it requires and use Collection and ArrayCollection from Doctrine, this its not a good implementation of the DI principle, since we will be now depending our whole architecture from Doctrine’s. The class should look like this:

<?php

namespace Customer;

use FOS\UserBundle\Model\User as BaseUser;

/**
 * Customer
 */
class Customer extends BaseUser
{

    /**
     * @var string
     */
    protected $id;

    /**
     * Get id
     *
     * @return string
     */
    public function getId()
    {
        return $this->id;
    }
}

After this we can execute the doctrine command to create our schema with:

doctrine:schema:create

Even if we are not going to use any authentication method yet we need to configure some security sections so FOSUserBundle can create our customers/users, our security.yml end like this:

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            anonymous: ~
            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html

Now lets create our first costumer with the FOSUserBundle command

fos:user:create

This command will prompt us for the user username, email and password, use the data you like.

Ok now we have the bundle correctly configured and our first customer on db, lets start building our REST API. To start lets create a basic end point to get all the registered customers.

Create a CustomersController and inside it create a getAction that will query the db to find all the registered customers

<?php

namespace AppBundle\Controller;

use FOS\RestBundle\Controller\Annotations as FOSRestBundleAnnotations;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;

/**
 * @FOSRestBundleAnnotations\View()
 */
class CustomersController extends FOSRestController implements ClassResourceInterface
{
    public function cgetAction()
    {
        $em = $this->getDoctrine()->getEntityManager();
        $repository = $em->getRepository("AppBundle:Customer");
        $customers = $repository->findAll();
        return $customers;
    }
}

Sure we can filter that query to get only the enabled ones but this suppose to be a simple example, your business requirement will lead you to what ever you really needs.

After query the end point at “/customers” you should see something like this:

[
 {
 "id":"55edc9c58b131",
 "username":"fabien",
 "username_canonical":"fabien",
 "email":"fabien@symfony.com",
 "email_canonical":"fabien@symfony.com",
 "enabled":true,
 "salt":"tr24is47dj4wgkcoc0kockgg0c80wgk",
 "password":"$2y$13$tr24is47dj4wgkcoc0kocea5.mMJOVBgQAqND.no.ad7bSEWzWTxi",
 "locked":false,
 "expired":false,
 "roles":[],
 "credentials_expired":false
 }
]

Add a new action to retrieve a customer by id its as simple as using the symfony param converter

public function getAction(Customer $customer)
{
    return $customer;
}

Lets try a hard one and create a new customer through the API. What we need its a form that handles the customer basic data and a new end point that can process that form.

<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CustomerType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username', 'text')
            ->add('email', 'email')
            ->add('password', 'password');
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Customer',
            'csrf_protection' => false
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return '';
    }
}
public function postAction(Request $request)
{
    $customer = new Customer();
    $customerForm = $this->createForm(new CustomerType(), $customer);

    $customerForm->handleRequest($request);

    if ($customerForm->isValid()) {
        $em = $this->getDoctrine()->getEntityManager();
        $em->persist($customer);
        $em->flush();
        return $customer;
    }

    return $customerForm->getErrors();
}

A simple form and controller action, if you try post to the end point /customers with:

{
 "username": "yasmany",
 "email": "yasmanycm@gmail.com",
 "password": "ok"
}

You should get a response like

{
  id: "55edde808316d"
  username: "yasmany"
  username_canonical: "yasmany"
  email: "yasmanycm@gmail.com"
  email_canonical: "yasmanycm@gmail.com"
  enabled: false
  salt: "q8k22c6q2c0cs008s04wgsggwsgco0w"
  password: "ok"
  locked: false
  expired: false
  roles: [0]
  credentials_expired: false
}

Great we can now add new customers to our database. You may notice that the code does not perform any error validation, again this suppose to be a simple example and your business rules will guide you on that process.

Conclusion:

Our REST API its growing, so far we manage to create some basics end points and have it working easily enough with the help of this awesome bundles. Lets keep in mind the big picture:

  • Have a REST API that follow the 4 levels of a RESTFul app
  • To have a clean architecture where we can separate the framework from the business

So far our end points define resources and we use the HTTP verbs to interact with our resources, we may say, till now; “mission accomplish”, so lets keep moving.

Series:

  1. Motivation
  2. REST Levels 0, 1, 2 ( FOSRestBundle, JMSSerializerBundle )
  3. REST Levels 0, 1, 2 ( FOSUserBundle )
  4. REST Levels 0, 1, 2 ( NelmioApiDocBundle )
  5. REST Levels 3 ( BazingaHateoasBundle )
  6. Security ( FOSOAuthServerBundle )
  7. Security ( Securing the token path )
  8. Security ( Securing the token path – FIXED )
Advertisements

7 Comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s