BlackFox — PHP фреймворк для веб-сайтов и приложений

Reuniko

Новичок
Тот факт, что в итоге вся наша логика обернется банальными UPDATE запросами сильно мешает мышлению в правильном направлении...
Эвент сорсинг немного проясняет голову, но это мало кому нужная вещь :)
А что если банальные Update запросы это мышление в правильном направлении?
a_05ab5e88.jpg

Вы бы что-нибудь поконкретнее привели, а то фразы типа "мышление в правильном направлении" можно интерпретировать очень по разному, не понятно же вообще.
 

Reuniko

Новичок
Пример Event Sourcing:

PHP:
<?php

namespace Example;

class Users extends \System\SCRUD {
    public $structure = [
        'ID'       => self::ID,
        'ACTIVE'   => [
            'TYPE'    => 'BOOL',
            'NAME'    => 'Active',
            'DEFAULT' => true,
        ],
        'LOGIN'    => [
            'TYPE'     => 'STRING',
            'NAME'     => 'Login',
            'NOT_NULL' => true,
            'INDEX'    => true,
        ],
        'PASSWORD' => [
            'TYPE'     => 'STRING',
            'NAME'     => 'Password',
            'NOT_NULL' => true,
        ],
    ];

    public function Create($fields) {
        $ID = parent::Create($fields);
        UsersHistory::I()->Create([
            'USER'    => $ID,
            'CHANGES' => $fields,
            'EVENT'   => 'Create',
        ]);
        return $ID;
    }

    public function Update($filter = [], $fields = [], $event = 'Update') {
        parent::Update($filter, $fields);
        if ($event) {
            $user_ids = $this->GetColumn($filter, 'ID');
            foreach ($user_ids as $user_id) {
                UsersHistory::I()->Create([
                    'USER'    => $user_id,
                    'CHANGES' => $fields,
                    'EVENT'   => $event,
                ]);
            }
        }
    }

    public function Ban(int $ID) {
        $is_active = $this->Get($ID, 'ACTIVE');
        if (!$is_active) {
            throw new \System\Exception("User is already banned");
        }
        $this->Update($ID, ['ACTIVE' => false], 'Ban');
    }
}
PHP:
<?php

namespace Example;

class UsersHistory extends \System\SCRUD {
    public function Init() {
        $this->structure = [
            'ID'      => self::ID,
            'USER'    => [
                'TYPE'     => 'OUTER',
                'NAME'     => 'User',
                'LINK'     => 'Users',
                'NOT_NULL' => true,
            ],
            'EVENT'   => [
                'TYPE'     => 'STRING',
                'NAME'     => 'Event',
                'NOT_NULL' => true,
            ],
            'CHANGES' => [
                'TYPE'     => 'ARRAY',
                'NAME'     => 'Changes',
                'NOT_NULL' => true,
            ],
            'MOMENT'  => [
                'TYPE'     => 'DATETIME',
                'NAME'     => 'Moment',
                'NOT_NULL' => true,
            ],
        ];
    }

    public function Create($fields) {
        $fields['MOMENT'] = time();
        return parent::Create($fields);
    }

    public function Update($filter = [], $fields = []) {
        throw new \System\ExceptionNotAllowed();
    }

    public function Delete($filter = []) {
        $user_ids = array_unique($this->GetColumn($filter, 'USER'));
        parent::Delete($filter);
        foreach ($user_ids as $user_id) {
            $this->Restore($user_id);
        }
    }

    public function Restore(int $user_id) {
        $history = $this->Select([
            'SORT'   => ['MOMENT' => 'ASC'],
            'FILTER' => ['USER' => $user_id],
        ]);
        $changes = [];
        foreach ($history as $event) {
            $changes = array_merge($changes, $event['CHANGES']);
        }
        // вызов Update с $event=null позволит обновить поля пользователя без записи в историю
        Users::I()->Update($user_id, $changes, null);
    }
}
При добавлении события в историю поле MOMENT формируется автоматически.
Редактирование событий запрещено.
При удалении событий из истории, модель восстанавливает всех пользователей, связанных с удаляемыми событиями

В дальнейшем можно большую часть кода UsersHistory вынести в абстрактного родителя History, включая большую часть массива $structure.
 

fixxxer

К.О.
Партнер клуба
Ну как persistence model это подойдёт. А бизнес логику то куда писать? Вот хотя бы на том простом примере с модерацией смены имени.

Дело не в мировоззрении, а в наступлении на те же грабли годами. И в наблюдениях за тем, как все вокруг на них же наступают, и даже умудряются получать от этого удовольствие. Ну можно сказать, что мировоззрение сформировано опытом наступания на грабли :) по использованию global вижу, что твой опыт в хождении по своим же граблям невелик, ну либо работал в основном в одиночку и знаешь, где они разложены. Многие вещи понимаешь только после работы в разнородных командах, для себя-то можно как угодно делать, пока возраст позволяет все помнить.
 
Последнее редактирование:

Adelf

Administrator
Команда форума
Постом выше описан проект с бизнес-процессом космической сложности.
Нет. это очень простой проектик. Приведу примерчик из книги моей :)

Well, this simple example can’t prove the importance of working with entities as a unit when they are real units. Let me imagine something more complex - an implementation of a Monopoly game. Everything in this game is one big unit. Players, their properties, their money, their position on the board. All of this represents the current state of the game. A player makes the move and the next state completely depends on the current state. If he steps on someone’s property, he should pay. If he has enough money, he pays. If not, he should earn the money or retreat. If the property is free, he can buy it. If he doesn’t have enough money, an auction between other players begins.
Там придется раскидывать это все по куче сущностей, которые представляют собой один агрегат. И сразу становится понятно, что все эти активрекорды, в которых нет нормального персиста релейшенов - это детские игрушки.

И да, то что ты привел это не эвент сорсинг. Ты все вручную сделал. Там надо и про оптимистические блокировки подумать и про проекции и про тесты... Кароч, забей. Наиграешься в свой фреймворк, подрастешь немного, поймешь. Опять-таки, если повезет.
 

Reuniko

Новичок
Ну как persistence model это подойдёт. А бизнес логику то куда писать? Вот хотя бы на том простом примере с модерацией смены имени.
public function Ban это пример бизнес-логики,
таким же макаром мона впилить Rename
PHP:
    public function ChangeLogin(int $ID, $new_login) {
        $current_login = $this->Get($ID, 'LOGIN');
        if ($new_login === $current_login) {
            throw new \System\Exception("Same login");
        }
        $user_exist = $this->Present(['LOGIN' => $new_login]);
        if ($user_exist) {
            throw new \System\Exception("This login already used by another user");
        }

        $this->Update($ID, ['LOGIN' => $new_login], 'ChangeLogin');
    }
по использованию global вижу, что
все еще надеюсь что вы научите меня как бы мне так хранить конфиг чтобы было лучше чем global
 
Последнее редактирование:

Reuniko

Новичок
И да, то что ты привел это не эвент сорсинг. Ты все вручную сделал. Там надо и про оптимистические блокировки подумать и про проекции и про тесты... Кароч, забей. Наиграешься в свой фреймворк, подрастешь немного, поймешь. Опять-таки, если повезет.
А, ну конечно, раз я сделал вручную, значит это не эвент сорсинг. Это даже лучше.

Метод Restore это и есть проекция.
Оптимистические блокировки мне не нужны, я ведь не на Node.js сервер пишу, а на php, который сам по себе сервер и параллелит запросы самостоятельно на своем уровне.
Накидать тестов поверх всего этого я тоже не вижу никаких проблем.
 

Reuniko

Новичок
Оборачиваешь свой блок кода между StartTransaction и Commit, в любой непонятной ситуации кидаешь Rollback и исключение и голова не болит этими вашими событиями и оптимистичными блокировками. Параллельные процессы, вместо того чтобы "думать" что бы им сделать в условиях изменившихся данных, просто стоят в очереди и ждут эти новые изменившиеся данные. Простота и гармония <3
 

Adelf

Administrator
Команда форума
https://adelf.tech/2019/architecture-of-complex-web-applications
) Захотелось что-то такое написать. Получилось немного коряво. Не покупай ) Надо будет - скину, но тебе там ничего нового не найти.

А пример с монополией... я наверно у тебя украл. Ты что-то подобное тут приводил.
 

fixxxer

К.О.
Партнер клуба
Да ты не стесняйся. Давай пиратку, покритикуем!
 

Вурдалак

Продвинутый новичок
Оффтопик: в плане API, уже использую GraphQL, это крайне удобная штука, если сделать хорошую обвязку над конфигурацией.
 

fixxxer

К.О.
Партнер клуба
А я по-старинке - типа-REST на запросы, JSON-RPC 2 на команды. Какой-нибудь ощутимый профит есть у GraphQL по сравнению с таким подходом?

Вот меня эта обвязка пугает, даже боюсь подойти к снаряду. :) На запросы кругом все обмазываются всякими врапперами над активрекордами с анемикой, чот жесть прямо.
 

Adelf

Administrator
Команда форума
  • Like
Реакции: WMix

Вурдалак

Продвинутый новичок
А я по-старинке - типа-REST на запросы, JSON-RPC 2 на команды. Какой-нибудь ощутимый профит есть у GraphQL по сравнению с таким подходом?

Вот меня эта обвязка пугает, даже боюсь подойти к снаряду. :) На запросы кругом все обмазываются всякими врапперами над активрекордами с анемикой, чот жесть прямо.
В сыром виде всякие https://github.com/webonyx/graphql-php действительно неудобно использовать. По поводу активрекордов и анемики не очень понял, я сделаю запросы в CommandBus, в QueryServices и всё окей.

Некоторые плюсы:
* клиент забирает всю страницу одним запросом (react-php позволяет ускорить заполнение данных) и только то, что ему реально нужно;
* сразу решены проблемы с документацией (есть и так хипстерские решения: https://apis.guru/graphql-voyager/), расширения для Chrome, etc.;,
* появляется типизация над скалярами (можно задекларировать тип scalar Email и когда клиент передаёт строку, это валидируется и инжектится сразу твой VO-объект Email;
* сразу есть семантическое разделение на queries/mutations, что очень доставляет;
* очень быстро получается добавлять новые объекты/методы в API, но это уже больше от обвязки зависит;
* появляется контроль за использованием всех полей клиентом: можно вести статистику какие клиенты используют то или иное поле и потом выпиливать его, если им почти никто не пользуется;
* разные приятные мелочи типа аннотаций, сам GraphQL language приятен.
 

fixxxer

К.О.
Партнер клуба
> клиент забирает всю страницу одним запросом
При абсолютном доминировании HTTP/2 принципиальной разницы вроде как и нет. Ну и в JSON-RPC батчи тоже есть.
> только то, что ему реально нужно
> появляется контроль за использованием всех полей клиентом
Вот это как бы основной поинт "за". Но когда архитектура фронта Redux-подобная (у меня ngrx с преферансом и куртизанками), все равно кладешь все подряд в store, а там уже компоненты отселектят, что им надо. Частных случаев достаточно немного, чтобы не париться и просто сделать аргументы запроса/метода или разные запросы/методы.
> сразу решены проблемы с документацией
> появляется типизация над скалярами
О, тут у меня свой велосипед: немного расширенная JSON-Schema, которая компилится в PHP-шные DTO и в Typescript-интерфейсы. Примерно то же самое выходит.

я сделаю запросы в CommandBus, в QueryServices и всё окей
Ну там народ любит достать из базы только нужное. Мне это кажется сомнительной вещью - и с точки зрения вымывания кэшей, и с точки зрения излишней сложности. Хотя в конкретных случаях, наверное, полезно (типа, когда можно выбросить подзапрос). Если с этим не заморачиваться, то вроде все просто. Но опять же, см. пункт про ngrx.

В общем, мне что-то кажется, что я шило на мыло поменяю.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Вот это как бы основной поинт "за". Но когда архитектура фронта Redux-подобная (у меня ngrx с преферансом и куртизанками), все равно кладешь все подряд в store, а там уже компоненты отселектят, что им надо. Частных случаев достаточно немного, чтобы не париться и просто сделать аргументы запроса/метода или разные запросы/методы.
Я не понял, честно говоря. Имеешь в виду, что современные фронт-фреймворки все равно запрашивают много лишних полей? Я просто совсем не разбираюсь в клиентском стаффе. Была, кстати, идея считать сложность запросов (каждое запрошенное поле — штраф) и выводить статистику куда-то по клиентам (web/iOS/Android). Команда с самыми «тяжелыми» запросами будет лишаться премии )

Ну там народ любит достать из базы только нужное. Мне это кажется сомнительной вещью - и с точки зрения вымывания кэшей, и с точки зрения излишней сложности.
Ну это же не проблема GraphQL, мало ли что там любят.

О, тут у меня свой велосипед
GraphQL сейчас очень популярный формат, под все клиенты есть куча либ, тулз, клиенту проще как-то втягиваться, чем изучать очередную кастомную вариацию REST/JSON-RPC, плюс уже не возникает споров на тему разделения mutations/queries, людям дали авторитетный источник — ну всё, значит так надо.

Ну и в JSON-RPC батчи тоже есть.
В GraphQL ты можешь (точнее должен) выбрать какие-то поля у mutation при выполнении. Ты можешь выполнить команду и запросить нужные тебе read models в ответ.

Ну и один автокомплит в расширениях типа ChromeiQL чего стоит.

Рано или поздно, но на GraphQL перейдут все.
 

fixxxer

К.О.
Партнер клуба
Была, кстати, идея считать сложность запросов (каждое запрошенное поле — штраф) и выводить статистику куда-то по клиентам (web/iOS/Android). Команда с самыми «тяжелыми» запросами будет лишаться премии )
Ага, сэкономили 50 байт на запросе, а потом втащили какой-нибудь jquery ui весом в пару мегабайт. :) Ну не знаю, экономия на спичках это все.

клиенту проще как-то втягиваться
Да, для публичного API - согласен, тут без вопросов вообще.

Про Redux-подобные фронтенды завтра расскажу, много букв. Это все удивительно похоже на CQRS. Только на клиенте.
 
Сверху