How to Use a Custom Storage Layer in FOSUserBundle

Almost each Symfony project uses FOSUserBundle because it speeds up the development and provides useful features to manage users. It has a few built in storage layer implementations: one for Propel and a few for Doctrine (both ORM and ODM). It's great, but there are some cases when you have to use another storage. Luckily, FOSUserBundle is flexible enough and provides a way to implement a custom storage layer.  You just need to implement your own user manager to use all features of FOSUserBundle.

Use Cases

1. You already have your users stored with a 3rd party service or your own stand-alone service and talk to it via API. I had to use Google Drive API once.

2. You decided to store your users in some kind of key/value storage, like Redis.

3. You just want to use another ORM/ODM library like Eloquent ORMFlourish and so on.

A Few Simple Steps

To use a custom storage layer, you just need to do a few simple steps:

1) Create a User model

2) Set up your storage layer and the User mapping

3) Create a user manager

4) Configure security

1. Create a User Model

First of all, you need to create a user model. You have two options to choose from:

1) Implement UserInterface (the FOS\UserBundle\Model namespace)

2) Extend the User class (the FOS\UserBundle\Model namespace) and add/override required methods or properties.

I chose the 2nd option:

<?php

namespace AppBundle\Model;

use FOS\UserBundle\Model\User as BaseUser;
// other "use" statements

class User extends BaseUser implements EquatableInterface
{
    /**
     * @var string
     */
    protected $firstName;

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

    // other properties that you need

    /**
     * @param UserInterface $user
     *
     * @return bool
     */
    public function isEqualTo(UserInterface $user)
    {
        return $user->getUsername() == $this->username;
    }

    // additional getters and setters, if needed
}

Don't forget to call `parent::__construct()` if you override initialization.

OK, now we have a user that is UserInterface. Let's tell FOSUserBundle that we want to use this class:

# app/config/config.yml
fos_user:
    user_class: 'AppBundle\Model\User'

2. Operating with Users

In my case, storage is a Google Drive spreadsheet. I need a way to do basic operations with users like find by, delete, edit and so on. For these purposes I need two things:

1) Google Drive API (feel free to skip this point or replace with your own API for your storage, if needed)

I used "google/apiclient", installed it via Composer:

composer require "google/apiclient"

2) A User Repository

I don't want to operate and know about spreadsheets in the project, so, I decided that a user repository must hide all the detaiIs about spreadsheets inside and work/return only the User instances. 

Create a User Repository

As I wanted to return User instances from the repository, I needed a way to convert the spreadsheet rows into users and vise versa. In my case, it wasn't a simple column to property relation. I had to apply extra processing to decide how to map columns for each particular user. That's why I couldn't use JMSSerializer or something similar and had to create a UserMapper:

<?php

namespace AppBundle\Mapper;

// "use" statements

class UserMapper
{
    /**
     * @param array $props
     *
     * @return array
     */
    public function userToRow(array $props)
    {
        // ...
        return $row;
    }

    /**
     * @param array $row
     *
     * @return User
     */
    public function userFromRow(array $row)
    {
        // ...
        return $user;
    }
}

Now we can create a user repository:

<?php

namespace AppBundle\Model;

// "use" statements

class UserRepository implements UserRepositoryInterface
{
    /**
     * @var SpreadsheetAPI
     */
    private $api;

    /**
     * @var UserMapper
     */
    private $userMapper;

    /**
     * @param SpreadsheetAPI $api
     * @param UserMapper $userMapper
     */
    public function __construct(SpreadsheetAPI $api, UserMapper $userMapper)
    {
        $this->api = $api;
        $this->userMapper = $userMapper;
    }

    /**
     * @param string $username
     *
     * @return UserInterface|null
     */
    public function getByUsername($username)
    {
        // get a user via API
        $row = $this->api->getUserRow($username);

        // create User instance using UserMapper
        return $this->userMapper->userFromRow($row);
    }

    /**
     * @param string $username
     *
     * @return bool
     */
    public function deleteByUsername($username)
    {
        return $this->api->deleteUser($username);
    }

    /**
     * @param UserInterface $user 
     */
    public function updateUser(UserInterface $user)
    {
        // api->update 
    }

    // other methods like: getAllUsers, findBy, findOneBy and so on
}

SpreadsheetAPI, in my case, is a thin wrapper around Google API clients. It provides some handy methods to work with spreadsheets. I also added UserRepositoryInterface to be able to switch to another storage layer easier when possible. Now, we have a user repository and don't have to think or even know about spreadsheets. We operate only with the User instances. Great!

3. Create a Custom User Manager

Our user manager must implement UserManagerInterface (the FOS\UserBundle\Model namespace). We can implement it from scratch or extend UserManager (the FOS\UserBundle\Model namespace). Here I'll show the 2nd option:

<?php

namespace AppBundle\Security;

use FOS\UserBundle\Model\UserManager as BaseUserManager;
// other "use" statements

class UserManager extends BaseUserManager
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;

    /**
     * @var string
     */
    private $userClass;

    /**
     * Constructor.
     *
     * @param EncoderFactoryInterface $encoderFactory
     * @param CanonicalizerInterface  $usernameCanonicalizer
     * @param CanonicalizerInterface  $emailCanonicalizer
     */
    public function __construct(
        EncoderFactoryInterface $encoderFactory,
        CanonicalizerInterface $usernameCanonicalizer,
        CanonicalizerInterface $emailCanonicalizer,
        UserRepositoryInterface $userRepository,
        $userClass
    ) {
        parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer);
        $this->userRepository = $userRepository;
        $this->userClass = $userClass;
    }

    /**
     * {@inheritdoc}
     */
    public function deleteUser(UserInterface $user)
    {
        return $this->userRepository->deleteByUsername($user->getUsernameCanonical());
    }

    /**
     * {@inheritdoc}
     */
    public function findUserByUsername($username)
    {
        return $this->userRepository->getByUsername($username);
    }

    /**
     * {@inheritdoc}
     */
    public function findUserByEmail($email)
    {
        return $this->userRepository->getByUsername($email);
    }

    /**
     * {@inheritdoc}
     */
    public function getClass()
    {
        return $this->userClass;
    }

    /**
     * {@inheritdoc}
     */
    public function findUserBy(array $criteria)
    {
        return $this->userRepository->findOneBy($criteria);
    }

    /**
     * {@inheritdoc}
     */
    public function findUsers()
    {
        return $this->userRepository->getAllUsers();
    }

    /**
     * {@inheritdoc}
     */
    public function reloadUser(UserInterface $user)
    {
        if ($freshUser = $this->userRepository->getByUsername($user->getUsername())) {
            $reflectionUser = new \ReflectionObject($user);
            $freshReflectionUser = new \ReflectionObject($freshUser);
            foreach ($freshReflectionUser->getProperties() as $refProp) {
                $reflectionUser->getProperty($refProp->getName())->setValue($refProp->getValue());
            }
        }
    }

    /**
     * Updates a user.
     *
     * @param UserInterface $user
     * @param bool $andFlush
     *
     * @return void
     */
    public function updateUser(UserInterface $user, $andFlush = true)
    {
        $this->updateCanonicalFields($user);
        $this->updatePassword($user);

        if ($andFlush) {
            $this->userRepository->updateUser($user);
        }
    }
}

And configure it as the app.user_manager service:

app.user_manager:
    class: AppBundle\Security\UserManager
    arguments:
      - @security.encoder_factory
      - @fos_user.util.username_canonicalizer
      - @fos_user.util.email_canonicalizer
      - @app.repository.user
      - %fos_user.model.user.class%

Here I inject the fos_user.model.user.class parameter because I have to implement the getClass method which is abstract in the base UserManager.

To use a sutom storage layer, we need to configure FOSUserBundle properly:

# app/config/config.yml
fos_user:
    db_driver: 'custom'
    service:
        user_manager: 'app.user_manager'

4. Security and a User Provider

We need to tell Symfony that we're able to provide users from Google spreadheets. Again, we have a few options here: use a user provider that FOSUserBundle has or implement UserProviderInterface (the Symfony\Component\Security\Core\User namespace). In both cases we will need to update security.yml:

# app/config/security.yml
providers:
        fos_user:
            id: fos_user.user_provider.username

It uses the user manager that is provided by the bundle, in our case it's our custom user manager that works with Google spreadsheets.

Conclusions

FOSUserBundle provides an easy way to use custom storage layers. You just need to implement your custom user manager that will talk to your storage layer, be it API or any database. You won't have to change anything else in FOSUserBundle and will be able to use all its features like registration and email confirmation, password recovery and so on as you usually do it.

See also:

Paysera Symfony2 Integration

It's a common need to integrate a payment gateway into a project. Even though there are many well known systems and aggregators like PayPal, RBKMoney, Paymentwall, Robokassa and so on, I want to talk about Paysera. It's another quite new payment system. They claim to have low commission fees for a convenient and wide range of services. Paysera allows your users to pay via cards and SMS. Integration is quite easy, but has some unclear moments during the process that I want to point out.

YAML as a data format for application's configuration

It seems obvious that almost every application has its configuration. We can use a lot of data formats for configuration files, for example YAML or XML, JSON or plain PHP (replace with any language you use) files. Personally I like and prefer to use YAML to other formats and here I just want to describe a little one of Symfony components called YAML. Also It's quite interesting for me which format you use, what pros and cons you see, so don't hesitate to leave a comment with your thoughts.

Travis - Beginning

 - it's a service for automated code testing. It's integrated with GitHub, supports many languages and libraries. This time, we're interested in PHP and the PHPUnit tests. It's very convenient to have such an instrument while developing an open source library, because other developers don't need to run tests locally, everything will be available on Travis. I'm going to show how we can add a library to travis-ci.org, and for this purpose I'm going to use 4devs/blog.