Текущий пользователь в обработчике события

halva

Новичок
Допустим у меня есть система, которая работает под reactphp + PSR-7.
Есть текущий пользователь (авторизованный или гость). Есть какой-то обработчик события, который обрабатывается на какой-то сущности и в нем есть логика завязанная на текущего пользователя и эту сущность.

Вопрос, как передать текущего пользователя в обработчик события?
 

Вурдалак

Продвинутый новичок
Можно ввести value object Context, который будет содержать все нужные контекстные данные, и передавать вторым аргументом:
PHP:
public function handle(OrderWasCancelled $event, Context $context) {
    // $context->getUserId()
}
Или я не совсем верно понял вопрос? Какое отношение к событиям имеет PSR-7?
 

Вурдалак

Продвинутый новичок
Если имеется в виду https://github.com/reactphp/http, то не совсем ясно зачем это нужно и в чём проблема (в плане, у тебя есть request, из него ты получаешь куки, и, соответственно, знаешь «текущего пользователя»). Я подумал речь идёт об асинхронных обработчиках событий.
 

fixxxer

К.О.
Партнер клуба
Я для этого делал обертку для соединений и реестр этих оберток.

Типа

PHP:
class ConnectionContext
{
    // everything I need
}

class Connection
{

    public function __construct(RatchetConnectionInterface $ratchetConnection, ConnectionContext $connectionContext)
    {
        $this->conn = $ratchetConnection;
        $this->setContext($connectionContext);
    }

    ....
}

class ConnectionsRegistry
{

    /**
     * @var \SplObjectStorage
     */
    private $connections;

    /**
     * @param RatchetConnectionInterface $conn
     * @param ConnectionContext $connectionContext
     */
    public function registerConnection(RatchetConnectionInterface $conn, ConnectionContext $connectionContext)
    {
        $this->connections->attach($conn, new Connection($conn, $connectionContext));
    }

    /**
     * @param RatchetConnectionInterface $conn
     */
    public function unregisterConnection(RatchetConnectionInterface $conn)
    {
        $connection = $this->find($conn);
        if ($connection) {
            $connection->close();
        }
        $this->connections->detach($conn);
    }

    /**
     * @param RatchetConnectionInterface $conn
     * @return Connection|null
     */
    public function find(RatchetConnectionInterface $conn)
    {
        if ($this->connections->contains($conn)) {
            return $this->connections->offsetGet($conn);
        } else {
            return null;
        }
    }
}

class ServerController implements MessageComponentInterface
{
    ...
    public function onOpen(RatchetConnectionInterface $ratchetConnection)
    {
        $this->connectionsRegistry->registerConnection($ratchetConnnetion, new ConnectionContext());
    }

    public function onMessage(RatchetConnectionInterface $ratchetConnection, $buffer)
    {
        if (null !== $connection = $this->connectionsRegistry->find($ratchetConnection)) {
            $this->messageRouter->handleMessage($buffer, $connection);
        } else {
            $this->logger->warning(
                'Got a message from an unregistered connection, closing: ' . json_encode((array)$ratchetConnection)
            );
            $conn->close();
        }
    }
    ...
}

...

// ну и где-то там в обработчике action-ов...
$context = $connection->getContext();
$context->associateUser($user);
$context->hasAssociatedUser();
$context->getAssociatedUser();
#etc
Но это было давно, может щас и родными средствами протащить контекст можно.
 
Последнее редактирование:

halva

Новичок
@Вурдалак, про контекст, это моя первая идея, по факту о реестр в рамках запроса. Но проблема заключается в транспортировке этого контекста в нужное место приложения.

В стандартно php приложении можно объявить реестр с доступом из любого места приложения, допустим как Yii::$app.
В reactphp же Request-Response, перекликающийся с PSR-7 по своей сути (это к вопросу о том причём тут PSR-7), нельзя писать в переменную статического класса, так как каждый клиентский запрос будет переписывать эту переменную.

Пока исполняется фронт-контроллер, выполняем экшены, с этим проблем нет. Для этих объектов Request-Response объекты как родные, да и эти объекты не трудно передавать по цепочке вызовов. Но когда появляется нужда, вызывать какие-то события, то в обработчиках по этим событиям нужно учитывать эти Request-Response объекты, передать по цепочке их уже невозможно (именно в этой теме я и пытаюсь выяснить, как их передать, как эту цепочку выстроить). Конечно это возможно, но тогда мне придется Request-объект или Context-объект текущего клиентского запроса подписывать к каждому объекту, который выполняется контексте этого клиентского запроса.

Я сам пока что не могу придумать архитектурное решение при котором эта проблема решается. Потому и пришел спросить тут, вдруг уже всё придумано и есть где посмотреть.
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Ну я вроде ответил :) У меня там, конечно, ratchet, но какая нафиг разница.
 

halva

Новичок
@fixxxer, ну это у тебя по сути контроллер и экшен, на этом уровне проблемы нет. Проблема когда ты начинаешь работать на уровень глубже, а именно с бизнес логикой или инфраструктурной логикой, которая не должна быть завязана на объект запроса. Также у тебя передается зависимость через метод, а представь что у тебя есть ивент менеджер, который может работать на разных уровнях приложения, но при этом вызывать события с контекстом текущего клиентского запроса.

Походу я пишу одно и тоже второй раз. Допустим есть такой экшен:
Код:
public function savePage($request) 
{
    $pages = $this->getEntityManager()->getRepository('pages');
    $page = $this->getEntityManager()->getEntity('pages');
    $page = $this->hydrateFromRequest($request);
    $pages->save($page);
}
Где то на сохранении сущности, выполняется обработчик события, допустим такого содержимого
Код:
'onSave' => function ($entity, $context) {
    if (!$entity->getUser()) {
        $entity->setUser($context->getCurrentUser());
    }
},
Вопрос, каким способом можно получить $context в onSave. Я пока что вижу, только такое решение

Код:
public function savePage($request) 
{
    $pages = $this->getEntityManager()->getRepository('pages');
    $page = $this->getEntityManager()->getEntity('pages');
    $page->getEventManager()->setContext($request->getAttribute('context'));
    $page = $this->hydrateFromRequest($request);
    $pages->save($page);
}
где, для каждой сущности $page, да и любой сущности будет свой ивент менеджер.

Код:
public function getEventManager()
{
    if (!$this->eventManager) {
        $this->eventManage = new \EventManager;
    }
  
    return $this->eventManager;
}

Но как по мне это фуууу.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
@halva тебя интересует как пробросить объект контекста в какой-то конкретной библиотеке, но почему-то ты заводишь задачу, говоря про reactphp и PSR-7.
 

halva

Новичок
@halva тебя интересует как пробросить объект контекста в какой-то конкретной библиотеке, но почему-то ты заводишь задачу, говоря про reactphp и PSR-7.
Я говорю о них, что бы не описывать бэкграунд из архитектурных решений и ограничений вытекающих из этих решений. Меня интересует как пробросить контекст, а не только как пробросить контекст в конкретной библиотеке. Если в какой-то библиотеке такое пробрасывание контекста уже есть, я не против ссылок на код или описание механизмов такого пробрасывания.
 

Вурдалак

Продвинутый новичок
Если очень упрощённо, то мы делаем
PHP:
// BEGIN TRANSACTION
$user = $this->userRepository->find(...); // SELECT ... FOR UPDATE
$user->doSomething();
$events = $user->getEvents();
$this->userRepository->save($user);
// COMMIT
$this->eventDispatcher->dispatch($events, $context);
Ну и в dispatcher просто можно заюзать любой message bus, хоть Symfony EventDispatcher, упаковав объект события + context в одно мета-событие. Потом при вызове listener'а прокинуть объекты в аргументы. Но я подозреваю, что тебе это не особо интересно, потому что ты ищешь какой-то секретный универсальный способ пробрасывания контекста, именно по этой причине ты настаиваешь на том, что тебя интересует просто «как пробросить контекст», а в какой библиотеке — не важно. Если Doctrine не поддерживает такого концепта, то красиво сделать вряд ли выйдет.
 

fixxxer

К.О.
Партнер клуба
@halva, все твои проблемы сводятся к глобальным переменным (или статике/синглтонам, что одно и то же). Не используй их, и все будет хорошо.

Что происходит в обычном случае, без всяких реактов, при обработке запроса? Создается IoC-контейнер и дальше понеслась. Тут ровно то же самое, только раз у тебя пачка реквестов в рамках одного процесса, то и контейнеров будет столько же.
 

halva

Новичок
@fixxxer, ну да, видимо все сводится к тому что на каждый реквест придется делать свой IoC-контейнер.

Советовался сегодня с джавистом. Он сказал, что такую проблему решает созданием реестра реквестов, где ключ это id потока. Такой бы вариант подходил если бы reactphp был многопоточный, но он однопоточный и асинхронный.
 

fixxxer

К.О.
Партнер клуба
Реестр реквестов (а точнее соединений) - это ровно то, что я продемонстрировал примером кода выше. Ключом вполне может быть и объект (потому там и SplObjectStorage). Многопоточность тут вообще не имеет значения. Без многопоточности даже проще - не нужно думать о thread safety.

IoCC достаточно клонировать существующий. Ну или сделать цепочку PerRequestContainer->AppContainer. Глобальные инстансы ищутся в общей пачке, зависящее от конкретного реквеста - в локальной. Учитывая, что "синглтоны" не должны зависеть от реквеста, в локальной пачке, по идее, будет только реквест (и, возможно, соединение, если оно где-то может быть нужно). Создавать PerRequestContainer при получении запроса, типа $perRequestContainer = ContainerFactory::assembleForRequest($request, $appContainer). Дальше резолвишь в этом perRequestContainer что у тебя там первым (фронт-контроллер? роутер? пер-реквест-бутстрапер)? ну и по цепочке понеслась.
 
Последнее редактирование:

halva

Новичок
@fixxxer, Понятно. Делегирование общего контейнера в контейнер запроса выглядит понятнее чем клонирование, да и таким разделением можно описывать карты зависимостей отдельно для контейнера запроса и общего контейнера. Из минусов, конечно же лишние вызовы.
 

fixxxer

К.О.
Партнер клуба
Ну, я на ходу придумываю, сам-то такого не делал, нужды не было. Но особых проблем не вижу в этой идее.
 
Сверху