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 ORM, Flourish and so on.
A Few Simple Steps
To use a custom storage layer, you just need to do a few simple steps:
2) Set up your storage layer and the User mapping
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.