Практически все используют FOSUserBundle в своих Symfony проектах т.к. он ускоряет разработку и обладает хорошим набором функциональности для управления пользователями. Бандл предоставляет несколько готовых реализаций хранилищ данных: Propel и несколько для Doctrine (ORM и ODM). Это здорово, но иногда возникает необходимость работы с другими хранилищами данных. FOSUserBundle достаточно гибок и позволяет реализовать, и использовать произвольное хранилище. Для того, чтобы использовать все возможности FOSUserBundle Вам достаточно будет написать свой менеджер пользователей под конкретного провайдера.
Случаи использования
1. Вы храните пользователей в стороннем сервисе или же своем и обращаетесь к нему по АПИ. Например, мне как-то пришлось использовать Google Drive АПИ.
2. Вы решили хранить своих пользователей в хранилище типа ключ/значение, таком как Redis.
3. Вы просто хотите использовать другую ORM/ODM библиотеку, такую как Eloquent ORM, Flourish и т.д.
Необходимые шаги
Чтобы использовать произвольное хранилище, вам достаточно выполнить несколько простых шагов:
1) Создать свою модель пользователя
2) Настроить хранилище и маппинг
3) Создать менеджера пользователей
4) Настроить Security в Symfony
1. Модель пользователя
Прежде всего вы должны создать пользователя. FOSUserBundle предлагает несколько вариантов:
1) Реализовать UserInterface (FOS\UserBundle\Model)
2) Расширить User класс (FOS\UserBundle\Model) и добавить, переопределить его методы и свойства.
Я выбрал второй вариант:
<?php
namespace AppBundle\Model;
use FOS\UserBundle\Model\User as BaseUser;
// другие "use"
class User extends BaseUser implements EquatableInterface
{
/**
* @var string
*/
protected $firstName;
/**
* @var string
*/
protected $lastName;
// все необходимые св-ва
/**
* @param UserInterface $user
*
* @return bool
*/
public function isEqualTo(UserInterface $user)
{
return $user->getUsername() == $this->username;
}
// дополнительные геттеры и сеттеры при необходимости
}
Не забудьте вызвать `parent::__construct()` если вы переопределяете конструктор.
Отлично, теперь у нас есть пользователь, который реализует UserInterface. Давайте укажем FOSUserBundle, что мы хотим использовать именно его:
# app/config/config.yml
fos_user:
user_class: 'AppBundle\Model\User'
2. Операции над пользователями
В моем случае хранилищем являлись файлы-таблицы в Google Drive. Для нормальной работы с пользователями нужны такие методы как findBy, delete, edit и т.д.. Для этих целей мне необходимо две вещи:
1) Клиент для Google Drive АПИ (пропустите этот шаг или замените клиент на тот, который работает с вашим АПИ при необходимости)
Я использовал "google/apiclient", установил с помощью Composer:
composer require "google/apiclient"
2) Репозиторий пользователей - UserRepository
Я не хотел работать с гугл таблицами по всему проекту, поэтому решил что репозиторий пользователей спрячет внутри себя все детали о Google таблицах и будет принимать/возвращать только User объекты.
Создаем UserRepository
Т.к. репозиторий взаимодействует с Google Drive клиентом и должен возвращать только User объекты, необходимо было найти способ конвертации строк в Google таблице в User объект и наоборот. В моем конкретном случае маппинг не был простым соотношением колонка - св-во пользователя, необходимо было дополнительно обрабатывать данные колонок, чтобы определить какие свойства и с какими значениями необходимо выставить для конкретного пользователя. По этой причине я не использовал JMSSerializer или подобную библиотеку, а создал простой маппер:
<?php
namespace AppBundle\Mapper;
// необходимые "use"
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;
}
}
Теперь мы можем создать репозиторий для пользователя:
<?php
namespace AppBundle\Model;
// необходимые "use"
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)
{
// получить строку пользователя через Google API
$row = $this->api->getUserRow($username);
// создать User объект используя 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
}
// дополнительные методы, такие как: getAllUsers, findBy, findOneBy и т.д.
}
В моем случае SpreadsheetAPI - это тонкая обертка над Google клиентом. Она предоставляет набор удобных методов для работы с Google таблицами. Я также добавил UserRepositoryInterface чтобы в дальнейшем было проще переключиться на другое хранилище данных. Теперь у нас есть репозиторий и в остальной части проекта мы в принципе не думаем и не знаем о том, что где-то используются Google таблицы. Мы просто оперируем объектами User.
3. Создаем менеджера пользователей
Наш менеджер пользователей должен реализовывать UserManagerInterface (FOS\UserBundle\Model). Мы можем написать его с нуля или же отнаследоваться от UserManager (FOS\UserBundle\Model). Далее я покажу второй вариант:
<?php
namespace AppBundle\Security;
use FOS\UserBundle\Model\UserManager as BaseUserManager;
// другие "use"
class UserManager extends BaseUserManager
{
/**
* @var UserRepositoryInterface
*/
private $userRepository;
/**
* @var string
*/
private $userClass;
/**
* @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());
}
}
}
/**
* @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);
}
}
}
Настраиваем его как сервис app.user_manager:
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%
Тут в качестве последнего аргумента я использую параметр fos_user.model.user.class т.к. наш менеджер должен реализовать метод getClass, который является абстрактным в базовом менеджере от FOSUserBundle.
Далее необходимо настроить FOSUserBundle, указать что мы хотим использовать наш менеджер пользователей:
# app/config/config.yml
fos_user:
db_driver: 'custom'
service:
user_manager: 'app.user_manager'
4. Провайдер пользователей и настройка Security
Нам необходимо указать Symfony что наши пользователи хранятся в Google таблицах. Как обычно, у нас есть несколько вариантов как мы можем это сделать: используя провайдер пользователей предоставляемый FOSUserBundle или реализовать UserProviderInterface (Symfony\Component\Security\Core\User). В обоих случаях нам необходимо будет обновить файл security.yml:
# app/config/security.yml
providers:
fos_user:
id: fos_user.user_provider.username
Провайдер от FOSUserBundle внутри использует менеджер пользователей, который указан в настройках бандла. В нашем случае это как раз наш собственный менеджер пользователей, который работает с Google таблицами.
Выводы
FOSUserBundle предоставляет простой способ по использованию произвольных хранилищ данных, достаточно лишь реализовать своего менеджера пользователей, который будет взаимодействовать с конкретным хранилищем пользователей, будь то АПИ или какая-либо база данных. При этом все остальные функции FOSUserBundle вроде регистрации и ее подтверждения по email, восстановления и смены пароля будут работать как прежде.