Практически все используют 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, восстановления и смены пароля будут работать как прежде.