Как использовать произвольное хранилище пользователей в FOSUserBundle

Практически все используют FOSUserBundle в своих Symfony проектах т.к. он ускоряет разработку и обладает хорошим набором функциональности для управления пользователями. Бандл предоставляет несколько готовых реализаций хранилищ данных: Propel и несколько для Doctrine (ORM и ODM). Это здорово, но иногда возникает необходимость работы с другими хранилищами данных. FOSUserBundle достаточно гибок и позволяет реализовать, и использовать произвольное хранилище. Для того, чтобы использовать все возможности FOSUserBundle Вам достаточно будет написать свой менеджер пользователей под конкретного провайдера.

Случаи использования

1. Вы храните пользователей в стороннем сервисе или же своем и обращаетесь к нему по АПИ. Например, мне как-то пришлось использовать Google Drive АПИ.

2. Вы решили хранить своих пользователей в хранилище типа ключ/значение, таком как Redis.

3. Вы просто хотите использовать другую ORM/ODM библиотеку, такую как Eloquent ORMFlourish и т.д.

Необходимые шаги

Чтобы использовать произвольное хранилище, вам достаточно выполнить несколько простых шагов:

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

Читайте также:

Docker установка и настройка

При разработке используется множество технологий. К примеру данный блог использует php(Symfony 2), mongodb, elastic, nginx это основные но также используется nodejs к примеру для минификации css, js. При разработке приходится настраивать все технологии как показано в статье. Но что делать если проектов несколько или они используют разные технологии, например другой проект использует MySQL, или еще могут использовать разные версии php или других библиотек. Для разработки и поддержки проектов на разных технологиях можно использовать Виртуальную машину и поставить на нее к примеру centos, но это не совсем удобно, надо все равно настраивать подобное окружение как на сервере и на других машинах разработчиков. Мы будет использовать контейнеры, такие как Docker. Настроем блог разработчиков чтобы использовать Docker для разработки.

Интеграция Paysera в Symfony

Сегодня сложно представить себе серьезный проект, где не понадобилась бы интеграция с платежными системами. Не смотря на то что существует множество популярных систем и аггрегаторов, таких как PayPal, RBKMoney, Paymentwall, Robokassa и т.д., я хочу рассказать о Paysera. Это еще одна, довольно новая платежная система. Они позиционируют себя как выгодных с точки зрения комиссий за их услуги. Paysera позволяет вашим пользователям расплачиваться карточками, SMS и т.д. Интеграция довольно простая, однако имеет некоторые неочевидные моменты, которые я и хочу осветить.

Настройка php, MySQL, nodejs, nginx и mongodb в OS X El Capitan

Недавно вышла OS X El Capitan, давайте обновим систему. Систему будем устанавливать с usb-flash. Из рабочего окружения мы поставим  php, nginx, mariadb, mongodb, elasticsearch, nodejs используя brew и настроем проект на symfony2.

Создание сайта “Обратный отсчет” на Symfony2

Мы иногда видим сайты с обратным отсчетом, проект стартует через … Его сделать достаточно просто, и не займет много времени. Мы воспользуемся проектом fdevs/coming-soon, который основан на Symfony2. Также будем сохранять введеный пользователями адреса электронной почтой в базу данных MongoDB. У нас есть настроенное рабочее окружение Osx, о настройке можно прочитать в статье Yosemite настройка рабочего окружения. Но главное версия php не меньше 5.4. В проекте можно также использовать реляционную базу данных типа MySQL. По умолчанию в проекте вообще не используется база данных, а введеный email отправляется на почту.

Автоматический deploy Symfony 2 используя Capifony

При разработке, и тем более поддержке действующего проекта на Symfony 2 одной из задач, которые приходится постоянно делать, это выгрузка изменений на сервер: рабочей или тестовой. Но так как программисты народ достаточно ленивый, чтобы автоматизировать повторяющиеся монотонные задачи, есть достаточное количество инструментов для облегчения данного процесса. Для Symfony2 мы используем Сapifony. Он основан на Capistrano но оптимизирован для Symfony.