Система управления складом с использованием CQRS и Event Sourcing. Service Layer



    В данной статье будет рассмотрен Service Layer в Magento 2 и сервисы (API интерфейсы) для управления сущностями, которые были описаны в предыдущей статье, посвященной проектированию и выделению доменных сущностей для системы управления складом (Inventory).

    Service Layer


    Так как систему управления складом мы пишем на платформе Magento 2, соответсвенно и сервисы, которые мы вводим будут описаны с учетом особенностей этой платформы.
    В Magento 2 для реализации принципа слабой связности на уровне модулей (в пределах Bounded Context-ов) был введен Service Layer, который определяет набор доступных операций для каждого модуля с точки зрения взаимодействия клиентов и других модулей системы.

    Service Layer (или Service Contracts) в Magento это набор PHP интерфейсов, которые определены для модуля и находятся в папке Api этого модуля. Сервис контракты состоят из Data Interfaces — DTO интерфейсы, представляющие данные сущностей доменной области; и Service Interfaces — интерфейсы, которые предоставляют доступ к бизнес логике, которая может быть вызвана клиентом (контроллером, web сервисом REST/SOAP, PHP кодом других модулей).

    Так как предполагается, что все внешние клиенты модуля будут работать с ним по контрактам, описанным Service Layer, то Service Layer фактически можно представить как Facade, который за собой скрывает детали реализации и сложность бизнес логики.
    Для клиентов зависимость на четко определенные API позволяет легче проводить апгрейды на следующие версии системы, так как модули подчиняются семантическому версионированию (Semantic Versioning).

    Для лучшей модулярности и отделения (decoupling) сервис контрактов от реализации иногда сервис контракты выделяют в отдельный модуль. Например, в случае Inventory мы имеем два модуля: один декларирует набор сервис интерфейсов InventoryAPI, второй — предоставляет реализацию для этих интерфейсов — Inventory. Таким образом, сторонний разработчик, который захочет подменить базовую реализацию, больше не привязан к этой реализации в коде. Все что ему нужно — интерфейсы, так как именно на интерфейсы зависят другие модули в системе.

    Интерфейсы Репозиториев — Repository


    Репозитории представляют собой интерфейсы предоставляющие набор CRUD операций для сущностей.
    Типичный интерфейс репозитория состоит из набора следующих методов:
     public function save(\Magento\Module\Api\Data\DataInterface $entityData);
     public function get($entityId);
     public function delete(\Magento\Module\Api\Data\DataInterface $entityData);
     public function deleteById($entityId);
     public function getList(SearchCriteriaInterface $searchCriteria);
    

    Набор методов может быть у́же (если для доменной сущности не характерны определенные операции. Например, удаление), но не шире, так как не рекомендовано добавлять методы с семантикой, отличающейся от предопределенного набора. Такие методы рекомендовано помещать в отдельные сервисы.

    Репозитории можно воспринимать как Фасады (Facade), которые объединяют наборы методов по управлению сущностями.

    В контексте модуля Inventory появляются репозитории для сущностей Source (сущность ответственная за представление любого физического склада где может находиться товар) и SourceItem ( сущность-связка, представляет собой количество определенного продукта (SKU) на конкретном физическом хранилище).

    
    /**
     * This is Facade for basic operations with Source
     * There is no delete method, as Source can't be deleted from the system because we want to keep Order information for all orders placed. Sources can be disabled instead.
     *
     * Used fully qualified namespaces in annotations for proper work of WebApi request parser
     *
     * @api
     */
    interface SourceRepositoryInterface
    {
        /**
         * Save Source data
         *
         * @param \Magento\InventoryApi\Api\Data\SourceInterface $source
         * @return int
         * @throws \Magento\Framework\Exception\CouldNotSaveException
         */
        public function save(SourceInterface $source);
    
        /**
         * Get Source data by given sourceId. If you want to create plugin on get method, also you need to create separate
         * plugin on getList method, because entity loading way is different for these methods
         *
         * @param int $sourceId
         * @return \Magento\InventoryApi\Api\Data\SourceInterface
         * @throws \Magento\Framework\Exception\NoSuchEntityException
         */
        public function get($sourceId);
    
        /**
         * Load Source data collection by given search criteria
         *
         * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
         * @return \Magento\InventoryApi\Api\Data\SourceSearchResultsInterface
         */
        public function getList(SearchCriteriaInterface $searchCriteria = null);
    }
    

    В случае SourceRepository у нас отсутствует метод delete, потому что такой бизнес операции над сущностями Source не существует, так как необходимо всегда сохранять всю информацию, связанную с размещенными заказами (включая откуда товар был доставлен). Соответсвенно нужно предотвратить возможную потерю таких данных в будущем (удаляя Source из которого выполнялась доставка). Вместо этого используется операция — пометить Source как неактивный (disabled).

    
    /**
     * This is Facade for basic operations with SourceItem
     *
     * The method save is absent, due to different semantic (save multiple)
     * @see SourceItemSaveInterface
     *
     * There is no get method because SourceItem identifies by compound identifier (sku and source_id),
     * thus, it's needed to use getList() method
     *
     * Used fully qualified namespaces in annotations for proper work of WebApi request parser
     *
     * @api
     */
    interface SourceItemRepositoryInterface
    {
        /**
         * Load Source Item data collection by given search criteria
         *
         * We need to have this method for direct work with Source Items, as Source Item contains
         * additional data like  qty, status (can be searchable by additional field)
         *
         * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
         * @return \Magento\InventoryApi\Api\Data\SourceItemSearchResultsInterface
         */
        public function getList(SearchCriteriaInterface $searchCriteria);
    
        /**
         * Delete Source Item data
         *
         * @param SourceItemInterface $sourceItem
         * @return void
         * @throws \Magento\Framework\Exception\NoSuchEntityException
         * @throws \Magento\Framework\Exception\CouldNotDeleteException
         */
        public function delete(SourceItemInterface $sourceItem);
    }

    Так как основные сценарии использования операции сохранения происходят с набором SourceItems, а не с одной сущностью, как это предполагает стандартный контракт save в репозитории.
    Для множественного сохранения, которое может происходить во время операций импорта или синхронизации стоков с внешними ERP или PIM системами — вводится отдельный контракт SourceItemSaveInterface, который предоставляет возможность атомарного сохранения множества SourceItem-ов в рамках одного вызова сервиса. Такой контракт позволяет обработать операцию вставки используя один запрос в базу данных, что значительно ускорит обработку. Базовая операция сохранения, принимающая одиночную сущность, не добавлена в контракт репозитория для того, чтобы не добавлять несколько точек для расширения, так как по факту в этом случае стороннему разработчику прийдется плагинизировать обе save операции (единичную и множественную). Поэтому кастомизация одной точки расширения всегда выглядет предпочтительней.

    Контракт команды множественного сохранения выглядит как Magento\InventoryApi\Api\SourceItemSaveInterface

    
    /**
     * Service method for source items save multiple
     * Performance efficient API, used for stock synchronization
     *
     * Used fully qualified namespaces in annotations for proper work of WebApi request parser
     *
     * @api
     */
    interface SourceItemSaveInterface
    {
        /**
         * Save Multiple Source item data
         *
         * @param \Magento\InventoryApi\Api\Data\SourceItemInterface[] $sourceItems
         * @return void
         * @throws \Magento\Framework\Exception\InputException
         * @throws \Magento\Framework\Exception\CouldNotSaveException
         */
        public function execute(array $sourceItems);
    }
    

    А ее реализация SourceItemSave делегирует сохранение ресурс модели SaveMultiple.

    Также в SourceItemRepository отсутствует метод get(), так как SourceItem — это сущность-связка и она определяется составным идентификатором (SKU и SourceId).

    Репозиторий для Stock (виртуальных агрегаций Source сущностей) выглядит стандартно:

    
    interface StockRepositoryInterface
    {
        /**
         * Save Stock data
         *
         * @param \Magento\InventoryApi\Api\Data\StockInterface $stock
         * @return int
         * @throws \Magento\Framework\Exception\CouldNotSaveException
         */
        public function save(StockInterface $stock);
        /**
         * Get Stock data by given stockId. If you want to create plugin on get method, also you need to create separate
         * plugin on getList method, because entity loading way is different for these methods
         *
         * @param int $stockId
         * @return \Magento\InventoryApi\Api\Data\StockInterface
         * @throws \Magento\Framework\Exception\NoSuchEntityException
         */
        public function get($stockId);
        /**
         * Find Stocks by given SearchCriteria
         *
         * @param \Magento\Framework\Api\SearchCriteriaInterface|null $searchCriteria
         * @return \Magento\InventoryApi\Api\Data\StockSearchResultsInterface
         */
        public function getList(SearchCriteriaInterface $searchCriteria = null);
        /**
         * Delete the Stock data by stockId. If stock is not found do nothing
         *
         * @param int $stockId
         * @return void
         * @throws \Magento\Framework\Exception\CouldNotDeleteException
         */
        public function deleteById($stockId);
    }
    

    Сервисы для маппинга Source и Stock


    Руководствуясь правилом "don't make your client do anything you can do for them" чтобы уменьшить количество boilerplate кода в клиенте API (в коде бизнес логики) мы не вводим Data interface SourceStockLinkInterface. Вместо этого мы вводим набор доменных сервисов-команд для связывания (assignment) Source на Stock.

    В итоге получаем три команды:

    
    interface AssignSourcesToStockInterface
    {
        /**
         * Assign list of source ids to stock
         *
         * @param int $stockId
         * @param int[] $sourceIds
         * @return void
         * @throws \Magento\Framework\Exception\InputException
         * @throws \Magento\Framework\Exception\CouldNotSaveException
         */
        public function execute(array $sourceIds, $stockId);
    }
    
    interface GetAssignedSourcesForStockInterface
    {
        /**
         * Get Sources assigned to Stock
         *
         * @param int $stockId
         * @return \Magento\InventoryApi\Api\Data\SourceInterface[]
         * @throws \Magento\Framework\Exception\InputException
         * @throws \Magento\Framework\Exception\LocalizedException
         */
        public function execute($stockId);
    }
    
    interface UnassignSourceFromStockInterface
    {
        /**
         * Unassign source from stock
         *
         * @param int $sourceId
         * @param int $stockId
         * @return void
         * @throws \Magento\Framework\Exception\InputException
         * @throws \Magento\Framework\Exception\CouldNotDeleteException
         */
        public function execute($sourceId, $stockId);
    }
    

    API vs SPI


    В рамках данного проекта было решено явно разделять API (Application Programming Interface) от SPI (Service Provider Interfaces) для того, чтобы улучшить возможности расширения и уменьшить связность компонентов.

    • Репозитории могут быть расценены как API, соответственно предполагается, что методы интерфейса репозитория вызываются в PHP коде бизнес логики.
    • Отдельные классы-команды на которые класс-реализация репозитория проксирует методы (такие как: Get, Save, GetList, Delete) могут быть расценены как SPI — интерфейсы, для которых может быть предложена своя реализация сторонним разработчиком, чтобы расширить или заменить текущее поведение системы.

    Таким образом, например, реализация репозитория Magento\Inventory\Model\StockRepository выглядит следующим образом:

    
    /**
     * @inheritdoc
     */
    class StockRepository implements StockRepositoryInterface
    {
        /**
         * @var SaveInterface
         */
        private $commandSave;
    
        /**
         * @var GetInterface
         */
        private $commandGet;
    
        /**
         * @var DeleteByIdInterface
         */
        private $commandDeleteById;
    
        /**
         * @var GetListInterface
         */
        private $commandGetList;
    
        /**
         * @param SaveInterface $commandSave
         * @param GetInterface $commandGet
         * @param DeleteByIdInterface $commandDeleteById
         * @param GetListInterface $commandGetList
         */
        public function __construct(
            SaveInterface $commandSave,
            GetInterface $commandGet,
            DeleteByIdInterface $commandDeleteById,
            GetListInterface $commandGetList
        ) {
            $this->commandSave = $commandSave;
            $this->commandGet = $commandGet;
            $this->commandDeleteById = $commandDeleteById;
            $this->commandGetList = $commandGetList;
        }
    
        /**
         * @inheritdoc
         */
        public function save(StockInterface $stock)
        {
            $this->commandSave->execute($stock);
        }
    
        /**
         * @inheritdoc
         */
        public function get($stockId)
        {
            return $this->commandGet->execute($stockId);
        }
    
        /**
         * @inheritdoc
         */
        public function deleteById($stockId)
        {
            $this->commandDeleteById->execute($stockId);
        }
    
        /**
         * @inheritdoc
         */
        public function getList(SearchCriteriaInterface $searchCriteria = null)
        {
            return $this->commandGetList->execute($searchCriteria);
        }
    }
    

    Конструктор принимает набор интерфейсов команд для каждой из предоставляемых операций. И во время вызова публичного метода из репозитория — вызов проксируется в соответствующую команду.

    Интерфейсы SPI команд выглядят следующим образом:

    
    /**
     * Save Stock data command (Service Provider Interface - SPI)
     *
     * Separate command interface to which Repository proxies initial Save call, could be considered as SPI - Interfaces
     * so that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
     * of business logic directly
     *
     * @see \Magento\InventoryApi\Api\StockRepositoryInterface
     * @api
     */
    interface SaveInterface
    {
        /**
         * Save Stock data
         *
         * @param StockInterface $stock
         * @return int
         * @throws CouldNotSaveException
         */
        public function execute(StockInterface $stock);
    }
    
    /**
     * Get Stock by stockId command (Service Provider Interface - SPI)
     *
     * Separate command interface to which Repository proxies initial Get call, could be considered as SPI - Interfaces
     * that you should extend and implement to customize current behavior, but NOT expected to be used (called) in the code
     * of business logic directly
     *
     * @see \Magento\InventoryApi\Api\StockRepositoryInterface
     * @api
     */
    interface GetInterface
    {
        /**
         * Get Stock data by given stockId
         *
         * @param int $stockId
         * @return StockInterface
         * @throws NoSuchEntityException
         */
        public function execute($stockId);
    }
    
    /**
     * Delete Stock by stockId command (Service Provider Interface - SPI)
     *
     * Separate command interface to which Repository proxies initial Delete call, could be considered as SPI - Interfaces
     * that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
     * of business logic directly
     *
     * @see \Magento\InventoryApi\Api\StockRepositoryInterface
     * @api
     */
    interface DeleteByIdInterface
    {
        /**
         * Delete the Stock data by stockId. If stock is not found do nothing
         *
         * @param int $stockId
         * @return void
         * @throws CouldNotDeleteException
         */
        public function execute($stockId);
    }
    
    /**
     * Find Stocks by SearchCriteria command (Service Provider Interface - SPI)
     *
     * Separate command interface to which Repository proxies initial GetList call, could be considered as SPI - Interfaces
     * that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
     * of business logic directly
     *
     * @see \Magento\InventoryApi\Api\StockRepositoryInterface
     * @api
     */
    interface GetListInterface
    {
        /**
         * Find Stocks by given SearchCriteria
         *
         * @param SearchCriteriaInterface|null $searchCriteria
         * @return StockSearchResultsInterface
         */
        public function execute(SearchCriteriaInterface $searchCriteria = null);
    }
    

    Эти команды представляют SPI интерфейсы модуля и находятся под неймспейсом

    Magento\Inventory\Model\Stock\Command\*

    Реализации команд выглядят следующим образом (Magento\Inventory\Model\Stock\Command\*). Например, команда сохранения Stock:

    
    /**
     * @inheritdoc
     */
    class Save implements SaveInterface
    {
        /**
         * @var StockResourceModel
         */
        private $stockResource;
    
        /**
         * @var LoggerInterface
         */
        private $logger;
    
        /**
         * @param StockResourceModel $stockResource
         * @param LoggerInterface $logger
         */
        public function __construct(
            StockResourceModel $stockResource,
            LoggerInterface $logger
        ) {
            $this->stockResource = $stockResource;
            $this->logger = $logger;
        }
    
        /**
         * @inheritdoc
         */
        public function execute(StockInterface $stock)
        {
            try {
                $this->stockResource->save($stock);
                return $stock->getStockId();
            } catch (\Exception $e) {
                $this->logger->error($e->getMessage());
                throw new CouldNotSaveException(__('Could not save Stock'), $e);
            }
        }
    }
    

    Механизм Резервирования продуктов


    Объект резервирования создается для того, чтобы иметь актуальный уровень товаров, которыми мы располагаем для продажи, между событиями создания заказа и уменьшением количества товаров на конкретных физических складах.
    Реализуя бизнес сценарий размещения заказа, подробно описанный в предыдущей части.

    Мы вводим Data Interface для резервирования

    
    /**
     * The entity responsible for reservations, created to keep inventory amount (product quantity) up-to-date.
     * It is created to have a state between order creation and inventory deduction (deduction of specific SourceItems)
     *
     * @api
     */
    interface ReservationInterface extends ExtensibleDataInterface
    {
        /**
         * Constants for keys of data array. Identical to the name of the getter in snake case
         */
        const RESERVATION_ID = 'reservation_id';
        const STOCK_ID = 'stock_id';
        const SKU = 'sku';
        const QUANTITY = 'quantity';
        const STATUS = 'status';
    
        /**#@+
         * Reservation possible statuses.
         */
        const STATUS_OPEN = 1;    
        const STATUS_CLOSED = 2;   
        /**#@-*/
    
        /**
         * Get Reservation id
         *
         * @return int|null
         */
        public function getReservationId();
    
        /**
         * Get stock id
         *
         * @return int
         */
        public function getStockId();
    
        /**
         * Get Product SKU
         *
         * @return string
         */
        public function getSku();
    
        /**
         * Get Product Qty
         *
         * @return float
         */
        public function getQuantity();
    
        /**
         * Get Reservation Status
         *
         * @return int
         */
        public function getStatus();
    }
    

    Так как мы воспринимаем резервирование как Append-Only неизменяемую сущность, то нам не нужны модификаторы (setter методы) в ReservationInterface. Соответсвенно нам нужен ReservationBuilderInterface для того, чтобы создавать объекты-резервирование.

    
    $reservationBuilder->setStockId(1);
    $reservationBuilder->setSku('sku');
    $reservationBuilder->setQty(10);
    $newReservation = $reservationBuilder->build();
    //now we could save Reservation entity 
    $reservationAppend->execute([$newReservation]);
    

    Сервисы Резервирования объектов


    Сервис добавления резервирований (бронирований) используется во время размещения заказа, обработки заказа или отмены заказа. А также создания и обработки операции возврата товара. В это время создается пачка резервирований, по одному резервированию на каждый SKU, и добавляются с помощью данного сервиса для обработки.

    
    /**
     * Command which appends reservations when order placed or canceled
     *
     * @api
     */
    interface ReservationAppend
    {
        /**
         * Append reservations when Order Placed (or Cancelled)
         *
         * @param Reservation[] $reservations
         * @return void
         * @throws \Magento\Framework\Exception\InputException
         * @throws \Magento\Framework\Exception\CouldNotSaveException
         */
        public function execute(array $reservations);
    }
    

    Следующий сервис используется для того, чтобы подсчитать точное число (Quantity) товара доступное для продажи, так как Quantity StockItem-a обновляется с задержкой (latency) вызванной природой Event Sourcing, так как во время размещения заказа система работает со StockItem сущностью (виртуальной агригацией) и не знает из каких физических складов (Source) произойдет списание. Таким образом между операцией размещения заказа и обработки — может пройти опредленное время.

    
    /**
     * Command which returns Reservation Quantity by Product SKU and Stock
     *
     * @api
     */
    interface GetReservationQuantityForProduct
    {
        /**
         * Get Reservation Quantity for given SKU in a given Stock
         *
         * @param string $sku
         * @param int $stockId
         * @return float
         */
        public function execute($sku, $stockId);
    }
    

    Каждое резервирование может иметь открытое или закрытое состояние.
    Так как резервирование — immutable объект, который не может изменяться. Вместо того, чтобы изменить состояние резервирования — мы просто создаем второй резерв, который «гасит» списание первого.
    Например,
    размещая заказ на 30 единиц товара создаем резервирование:
    ReservationID — 1, StockId — 1, SKU — SKU-1, Qty — (-30), Status — OPEN
    Обработав этот заказ — создаем другое резервирование
    ReservationID — 2, StockId — 1, SKU — SKU-1, Qty — (+30), Status — CLOSED

    Суммарно эти два резерва (-30) + (+30) = 0 не повлияют на Quantity, которое хранится в StockItem.
    Здесь важно заметить две вещи: мы не вводим связь (binding) между объектом резервирования и заказом (Order), так как резерв может быть привязан и к другим бизнес операциям. И с точки зрения склада (Inventory) нам не важен номер заказа, в рамках которого нужно отгрузить товар и уменьшить сток.
    Использование отрицательных и положительных значений для резервирований поможет нам упростить подсчет общего числа, которое мы должны отнять от Quantity сохраненным в StockItem.

    Например, с помощью такого запроса:

    select 
       SUM(r.qty) as total_reservation_qty
    from 
       Reservations as r
    where 
      stockId = {%id%} and sku  = {%sku%}
    

    Magento MSI (Multi Source Inventory)


    Данная статья является третьей статьей в цикле «Система управления складом с использованием CQRS и Event Sourcing» в рамках которого будет рассмотрен сбор требований, проектирование и разработка системы управления складом на примере Magento 2.

    Открытый проект, где ведется разработка, и куда привлекаются инженеры из сообщества, а также где можно ознакомиться с текущим состоянием проекта и документацией, доступен по ссылке.

    Более подробная документация по

    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 31

      +2
      CRUD-подобные репозитории изначально задумывались или это дань существующим API в ядре Magento 2? После предыдущих статей неожиданно было их увидеть
        0
        Хороший вопрос. Тут на самом деле ряд факторов сыграли.
        Начиная с того, что у нас мало что изменилось в концепции UI админ части по сравнению с Magento 1. Так как проектирование CQRS системы не может не затронуть UI. То, что называется Task Based UI, тут в презентации я показывал отличия от CRUD UI.
        В Magento привычно иметь UI Grid для редактирования сущностей в админке.
        Например, так выглядит грид продуктов

        В нем администратор может поменять значения для всех атрибутов сущности. Этот подход более характерен для CRUD UI, нежели для командного UI (где под изменения каждого из атрибутов пришлось бы создавать отдельную команду).
        И для многих сущностей такой грид достаточно удобен. Так как мы делаем фреймворк под разные потребности торговцев. У кого-то большой бизнес, и он хранит все товары в ERP или PIM системе и использует эту внешнюю систему как «Source of Truth», а Magento использует как витрину, которую легко кастомизировать для оформления заказов (эта категория пользователей не модифицирует данные в админке Magento, так как изменения делаются во внешней системе и в Magento они попадают при синхронизации). Для такого сценария использование Task Based UI более естественно.

        Кто-то наоборот заполняет данные используясь admin-панелью Magento и там же эти данные редактирует. Для последних CRUD подобный интерфейс достаточно удобный.
        Так как мы фреймворк, и не знает конкретных бизнес процессов продавца, который будет пользоваться Magento — мы должны покрыть все сценарии использования.
        В Таких реалиях CRUD подобные репозитории, которые предоставляют возможности менеджмента сущностей весьма удобны.

        С учетом вышеописанного Repository Pattern подходит нам как нельзя хорошо.
        A system with a complex domain model often benefits from a layer that isolates domain objects from details of the database access code. This becomes more important when there are a large number of domain classes or heavy querying. In these cases particularly, adding this layer helps minimize duplicate query logic.
        Тут стоит уточнить, что мы вводим такие Repository не для каждой сущности в системе, а только для тех сущностей для которых планируем предоставлять UI Grid для редактирования в админ-панеле.
        Т.е. в случае MSI проекта мы имеем гриды для Source, SourceItem, Stock — для этих сущностей мы предосталяем репозитории.
        Например, для объектов резервирования Reservation мы не предоставляем репозиторий.

        Основываясь на всем вышесказанном, я бы точно не воспринимал использование Repository как что-то плохое. Respository это один из тактических паттернов Domain Driven Design.
          0
          С учетом вышеописанного Repository Pattern подходит нам как нельзя хорошо.

          ИМХО репозиторий как паттерн для изоляции превосходен.
          Вот только что у вас доступ к базе данных "спрятан" не в репозитории а в командах


          class StockRepository implements StockRepositoryInterface {
          ...
          public function deleteById($stockId)
              {
                  $this->commandDeleteById->execute($stockId);
              }
            0
            В теле статьи я описал как в Magento 2 выглядят репозитории и почему.
            А доступ к базе у нас «спрятан» не в командах, которые должны быть агностичны к Data Storage механизму, а в ресурс моделях.
            Пример из статьи:
            Команда для MultipleSave опареции SourceItem — Magento\Inventory\Model\SourceItemSave.php

            Ресурс модель для этой команды — Magento\Inventory\Model\ResourceModel\SourceItem\SaveMultiple.php

            Repository у нас это Фасад, чтобы клиент мог иметь одну зависимость на репозитори для выполнения базовых операций с сущностью. Например, достать сущность по ID или поиском, изменить, сохранить (если он делает CRUD операции над сущностью, например в гриде). По большому счету Repository существует только для удобства и простоты кода клиента, так как мы могли бы просто давать набор команд.
        +1
        Промахнулся
        Это у меня наверное травма, что, при виде репозитория такого формата в Magento2, перед глазами появляется новое системное поле в бд(is_notification_sended), которое добавляется в DTO интерфейс и уже доступно к сохранению через репозиторий (или может сетаться и сохраняться эксплуатируя имплементацию дто через AbstractModel с магическими сеттерами).

        Для примера SourceInterface::setEnabled, если возникнет требование выполнения каких-либо бизнес-процессов по отключению/включению источника, то как в данном модуле это будет разруливаться? Наверное на уровне репозитория сравнение current/new field value.
        Понятно, что единые подходы на всех слоях системы способствуют скорости и приятностям в разработке, но по-хорошему тот же круд-интерфейс в сторонней системе может быть заменен на CQ и уже ему на уровне операционного слоя предется мапить все команды на круд-апишки.

        По вопросу гридов, в Magento2 насколько я видел дата провайдеры гридов не работают с репозиториями, они, пропуская слой репозитория, работают напрямую с коллекциями (сортинг, фильтры, массэкшены). Не тот ли это SmartUI, который Эванс называет антипаттерном для применения в контексте DDD?

        Thx
        Спасибо за статью и пояснения. Для меня, который все время видел только результат каких-то решений (релизы Magento), интересно понять что стоит за конкретными решениями и направление развития системы. Продолжайте, получается круто.
          0
          Для примера SourceInterface::setEnabled, если возникнет требование выполнения каких-либо бизнес-процессов по отключению/включению источника, то как в данном модуле это будет разруливаться?

          Так как на текущих мокапах UI, который были оговорен с продакт оунером проекта — у нас нет отдельного бизнес процесса — отключения и включения источника, а есть только Grid, в котором можно редактировать все поля. То пока внедрять отдельные сервисы-команды на включение и выключение источника не будем. А будем обрабатывать изменение состояния, сохраняя сущность Source с измененным состоянием через сервис SourceRepositoryInterface.

          Но стороннему разработчику ничего не мешает создать отдельные интерфейсы команд:
          interface DisableSource
          {
              /**
               * Disable given Source
               *
               * @param int $sourceId
               * @return void
               */
              public function execute($sourceId);
          }
          
          interface EnableSource
          {
              /**
               * Enable given Source
               *
               * @param int $sourceId
               * @return void
               */
              public function execute($sourceId);
          } 
          

          И в реализации этих сервисов-команд можно использовать SourceRepository как-то так:
                  /** SourceInterface $source */
                  $source = $this->sourceRepository->get($sourceId);
                  $source->setEnabled(TRUE);
                  $this->sourceRepository->save($source);
          

          Таким образом Plugin-ы которые висят на SourceRepository и запускаются при изменениях будут продолжать отрабатывать, т.е. введя такие сервисы сторонний разработчик, который хочет использовать командные сервисы — не поломает текущие расширения над CRUD API.
            0
            По вопросу гридов, в Magento2 насколько я видел дата провайдеры гридов не работают с репозиториями, они, пропуская слой репозитория, работают напрямую с коллекциями (сортинг, фильтры, массэкшены).
            Это не правило. Для DataProviders существует Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface

            В котором ничего не сказано про коллекции, и то, что вы должны их использовать.
            Вот, например, SourceDataProvider и StockDataProvider которые используются для гридов Source и Stock соответсвенно.

                public function getSearchResult()
                {
                    $searchCriteria = $this->getSearchCriteria();
                    $result = $this->stockRepository->getList($searchCriteria);
                    $searchResult = $this->searchResultFactory->create(
                        $result->getItems(),
                        $result->getTotalCount(),
                        $searchCriteria,
                        StockInterface::STOCK_ID
                    );
                    return $searchResult;
                }
            

            Поэтому нет — это нельзя назвать Smart UI, так как разделение на слои c возможностью подменять модель данных присутствует.
            0

            Извините пожалуйста, a где собственно сам Event Sourcing?

              0
              а что именно вы ожидали увидеть чего не нашли в описании Service Layer?
                0

                Еще раз извиняюсь, но все что я увидел это описание репозиториев и что-то типа command handler.
                Если Вас не затруднит, можете ткнуть пальцем, где конкретно в вашем коде "виден" Event Sourcing.

                  0
                  Можете не извиняться, но как на уровне Service Layer должен выглядеть Event Sourcing, чтобы он был заметен?
                    0

                    ну, хотя бы наличием самих Events? или если не наличием, то хотя бы dispatch

                      0
                      Ну давайте начнем с того, что в статье описан Service Layer, который представляет набор доменных сервисов (терминами DDD), соответсвенно название этих сервисов должны представлять бизнес процессы в домене на языке Ubiquitous Language и отображать как пользователи пользуются системой.
                      Поэтому на этом уровне dispatch у нас быть не может.

                      По поводу Events. В описанной системе ивентами можно считать объекты резервирования. В данном случае Reservations — это не команды (которые представляли бы собой императивные наклонения — как разместить резервацию), а сущности, которые отражают, что какое-то событие произошло в прошлом (резервирование произошло).
                      API по размещениею Reservations не мапятся напрямую на Web API (REST/SOAP), т.е. их «нельзя» вызвать из кода бизнес логики приложения напрямую. Они представляют собой SPI, который вызовется как side-effect при обработке какого-то бизнес процесса, например, размещения заказа.
                      Т.е. размещая заказ, ваш код не должен явно размещать резервации по этому заказу.

                      Ну и также эти объекты резервации «накатываются» на Rolling Snapshot для того, чтобы получить актуальное в текущий момент времени количество товаров в стоке доступное для продажи.

                      В статье сознательно не описывается метод хранения резерваций (Event Store если позволите), так как он может быть реализован по-разному. Этому может быть таблица базы данных, очередь Redis, Cassandra и т.д.
                        0
                        представляет набор доменных сервисов (терминами DDD)
                        Поэтому на этом уровне dispatch у нас быть не может.

                        Почему? А что с Domain Events? Совсем никак нельзя?


                        ивентами можно считать объекты резервирования

                        Так ивент или все таки нет?


                        /**
                         * The entity responsible for reservations, ...
                         *
                         * @api
                         */
                        interface ReservationInterface

                        Судя по декларации интерфэйса таки нет.


                        Вы случайно не знакомы с этой книжкой? https://leanpub.com/ddd-in-php

                          0
                          Давайте опишу более популярно, так как мне кажется, что вы не имели опыта работы с Magento. Поэтому делитесь взглядами и опытом приобрененным в рамках других проектов и другого технологического стека.

                          Magento — это eCommerce framework, которые может быть использован продавцами (merchant) разного размера и с разным оборотом. На базе Magento разворачивается конкретный магазин, на который будут приходить клиенты и осуществлять покупки (либо это может быть вообщем Drupal fron-end, а на Magento заказы будут приходить как Web API вызовы, headless Magento). Причем, как правило, каждый из таких магазинов отдельно кастомизируется (добавлятеся бизнес логика, меняется дефолтная и т.д.) чтобы соответствовать бизнес процессам конкртентого мерчанта.

                          Мы делаем фреймворковое решение, а не конкретную кастомизацию под конкретный бизнесс процесс, выстроенный у какого-то продавца.
                          Цитируя Грега Янга — не нужно строить Event Sourcing фреймворк (general purpose), так как вы его забросите вскоре, так как он будет ориентирован на бизнес процессы определенного бизнеса. Мы не делаем ES фреймворк.
                          И именование ES поэтому для нас не всегда уместны.

                          Мы пользуемся именами из Ubiquitous Language самого домена (Inventory).
                          Поэтому отвечая на ваш вопрос выше, объекты резерваций — в терминах Event Sourcing — это события (Events). И я выше описал почему.

                          Но называть их событиями в аннотации сущности не вижу причины, так как в определенных кастомизациях они вполне могут использоваться не как ивенты.

                          По поводу книги — нет, я эту я не читал.
                          Я читал Эванса и Implementing Domain Driven Design, Vaughn Vernon
                          из классики, ну и CQRS and Event Sourcing Грега Янга
                          а также посещал воркшопы по DDD под руководством Vaughn Vernon
                          для PHP DDD как-то по другому выглядит?
                            0

                            Должен признаться, что да, опыта работы с Magento у меня нет. Посмотрел — испугался — еле-еле отошел. Поэтому решили сделать свой eCommerce-Framework. В отличии от Magento у меня (лично) на базовый функционал ушло 3-4 месяца (сейчас точно не помню). На основе этого уже фрэймверка поднимали кастомные реализации по заказу клиентов. К сожалению не Open Source.


                            В одной реализации был конкретный заказ: Мы хотим мониторить все изменения в системе (кто, что, когда, сколько итд). Таким образом фрэймворк с одной стороны работает классически (Энтити сохраняются в конечном состоянии в базе данных). Но параллельно прокидываются ивенты (Application Events, Domain Events итд). Так что по сути двойное хранение тех же самых данных, только форма разная.


                            Цитируя Грега Янга — не нужно строить Event Sourcing фреймворк

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


                            Мы пользуемся именами из Ubiquitous Language самого домена

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


                            Но называть их событиями в аннотации сущности не вижу причины, так как в определенных кастомизациях они вполне могут использоваться не как ивенты

                            тоже странно. если уж кастомизация, то будте добры делайте сами (не Вы, а кастомайзер). а ваша имплементация как-никак базовый функционал и должна более или менее четко определять, что ваш фрэймворк делает и какие процессы и соответствующие сущности в нем уже есть.


                            PHP DDD как-то по другому выглядит

                            нет :) но там доступно и на примерах объясняют и показывают суть, ну хотя бы того самого Event Sourcing

                              0
                              Посмотрел — испугался — еле-еле отошел. Поэтому решили сделать свой eCommerce-Framework.
                              Ну в это и разница, в том, что Magento — самый популярный в мире eCommerce framework, которым пользуется более 200 000 мерчантов по всему миру. А вы написали решения под себя, заточенное под реализацию одного бизнес процесса. И вы не переживаете ни про расширяемость, ни про настраиваемость, поэтому и создаете объекты через new, и использование билдера выглядит для вас странно. Потому что количество людей, которые будут пользоваться вашей системой — ограничено и известно заранее. В этом и разница между фреймворком и проприентарной системой.
                              Но вот наименования как раз то и нужны, чтобы хотя бы разрабы знали и видели, что у них в коде.
                              Оно (наименование) есть, что легко заметить даже по этой статье, вы просто очень хотите, чтобы оно соответствовало тому, что вы прочитали в книге «Domain-Driven Design in PHP».
                              Да я понимаю и (на болезненном опыте) знаю, что бизнес-язык отличается иногда от реальности, но что мешает нам называть вещи своими именами
                              Эта фраза противоречит понятию Ubiquitous Language
                              что ваш фрэймворк делает и какие процессы и соответствующие сущности в нем уже есть.

                              За это и отвечает слой сервис контрактов, интерфейсы которого здесь представлены.
                              А то как будут они вызваться и какой будет транспорт. Будет ли использоваться классическая событийная модель, таблица базы данных, транзакционная модель с предпочтением data consistency, или BASE с Eventual Consistency — это уже обусловлено бизнес процессами конкретного мерчанта.
                              Поэтому и dispatch мы не показываем наружу. Так как диспатч — это часть реализации характерная для определенного бизнес процесса.
                                0
                                Magento — самый популярный в мире eCommerce framework

                                ну, скажем, начиналось все когда? а на безрыбье, как известно...


                                вы написали решения под себя, заточенное под реализацию одного бизнес процесса. И вы не переживаете ни про расширяемость, ни про настраиваемость

                                Извиняюсь за грубость, но замечаю уже не в первый раз, что Вы читать не умеете. Вернее читать — да, но понимание прочитанного явно хромает.
                                Мы сначала сделали фрэймворк. Делался он как раз с учетом расширяемости и "настраиваемости". И уж потом использовали его при реализации кастомных проектов. Агентура, в которой я работал, "продает" кастомные eCommerce-решения, а не сам фрэймворк. А вот для проектов попроще использовались такие фрэймворки как Magento, потому как найти разраба, умеющего поднять шоп на Magento намного проще.


                                Эта фраза противоречит понятию Ubiquitous Language

                                это почему? На сколько я знаю, этот язык как раз и должен решить проблемы понимания. Если разрабы не понимают, что от них хочет бизнес, то как они должны решить задачу правильно. Ubiquitous Languаге должен всем быть понятен. Да, понятно, что придется идти на уступки, но всем, а не односторонне.


                                Так как диспатч — это часть реализации характерная для определенного бизнес процесса.

                                Совсем не факт. Тем более если рассматривать dispatch в контексте Event Driven как механизм обеспечивающий внутрисистемный decoupling

              0
              public function getStatus();
              public function getReservationId();
              public function getStockId();
              Странное именование методов. почему getStatus? Хотя метод судя по всему возвращает int
                0
                метод getStatus возвращает одну из предопределенных констант, статуса в котором может пребывать резервация. Под капотом этот статус хранится как Int.
                Если вопрос почему мы не использовали Value-Object для хранения статуса, то мы не заменяем примитивные тимы на типы-обертки по двум причинам:
                1. performance, чтобы не создавать дополнительное большое количество объектов.
                2. это бы перегрузило и усложнило код бизнес логики (клиента), так как для создания статуса и других атрибутов (которых может быть много) код бизнес логики должен был бы использовать фабрики.
                  0
                  Зачем нужны фабрики для создания Value Object?
                0
                метод getStatus возвращает одну из предопределенных констант, статуса в котором может пребывать резервация. Под капотом этот статус хранится как Int.

                Вот как раз один из показателей, что у вас не Event Sourcing. Или по крайней мере не совсем

                  0
                  Вы вероятно пропусти описание механизма работы объектов резервирования. Само состояние в пределах объекта не изменяется. Reservation является immutable сущностью, при этом у него есть статус, который говорит о том в каких условиях (в рамках какого бизнес процесса) создавалась резервация.
                  По большому счёту это точка расширения для кастомизаторов. У нас вполне могло и не быть статуса, нам хватает набора (stockId, sku, qty) для подсчета актуальности стока.

                  Т.е. Если вы представили конечный автомат, и резервацию, которая переходит из одного состояние в другое — то это предположение ошибочно
                    0

                    Вам не кажется, что вы сами себе противоречите?
                    Если у вас резервация immutable сущность, то на каком основании у нее есть статус? Если immutable, то и статус как атрибут сущности не меняется, потому как сущность immutable.
                    А если таки статус может меняться, то это либо не immutable сущность, либо статус привязан не к той сущности.
                    В любом случае, следуя вашей реализации, резервация у вас все таки изменяема, хоть и Append Only, как вы пишите.


                    Добавление чего-то к чему-то изменяет то к чему добавили по умолчанию. Как бы вы это не крутили.


                    Да и с точки зрения бизнеса, резервация имеет определенное состояние, по крайней мере в тот момент, когда мы на нее смотрим. Не факт, что состояние актуально, но факт, что оно есть.


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


                    Если у вас меняется состояние резервации, что приводит к изменению ее статуса, то у вас резервация переходит из одного состояния в другое (было 1, стало 5).

                      0
                      Давайте по порядку. Потому как вы начали спорить с самим собой.
                      Где вы прочитали, про то, что какой-то из атрибутов сущности резервирования (в частности статус) изменяется?
                      Из описания статьи:
                      размещая заказ на 30 единиц товара создаем резервирование:
                      ReservationID — 1, StockId — 1, SKU — SKU-1, Qty — (-30), Status — CREATED
                      Обработав этот заказ — создаем другое резервирование
                      ReservationID — 2, StockId — 1, SKU — SKU-1, Qty — (+30), Status — CANCELLED
                      То есть, в тот момент, когда пользователь вашей системы хочет узнать текущее состояние резервации

                      На самом деле пользователь системы не хочет узнавать состояние резервации. Пользователь системы хочет получить актуальное количество товаров доступное для продажи. Для этого существует отдельный сервис. Логика этого сервиса следующая:
                      получить кол-во товаров в StockItem (агрегации) — отнять кол-во резерваций

                      Последнее вычисляется приблизительно так:
                      select 
                         SUM(r.qty) as total_reservation_qty
                      from 
                         Reservations as r
                      where 
                        stockId = {%id%} and sku  = {%sku%}
                      

                      Видите, нет привязки к статусу.
                      Поэтому я причисляю Reservation к SPI, т.е. напрямую ваш код не работает с ними.
                      Пользователь системы не должен хотеть работать с объектами резервирования.
                      он видит конечный автомат. Так как он не знает, что вы собираете состояние резервации. По большому счету ему на это забить, он хочет знать актуальное состояние его резервации или заказа.
                      Вы даете больше ответственности на сущность резервации чем это делаем мы.
                      Пользователю интересно — состояние заказа.
                      Сущность резервации не отвечает за статус заказа и не изменяется вместе с ним. Объект резервации создается для подсчета корректности стока, который используется для вычисления колличества продуктов в стоке.
                        0

                        Да, mea culpa, теперь понятнее. Спасибо.


                        Но не завидую вашим коллегам. Читабельностю ваш код явно не страдает

                          0
                          ну вот, вы нашли в моей статье и коде то, чего там не было, а в итоге обвинили код и его читабельность :)
                            0

                            Да не то чтобы нашел. Скорее всего наоборот.
                            Все таки если Reservation это Event, то


                            1. $reservated = new ReservateFromStock($stockId, $amount)

                              1.1 зачем какой-то builder? конструктор уже не в моде?
                              1.2 если уж вы сами говорите, что ``Reservation``` в данном случае и есть те ивенты, то к чему следующий прямой вызов?


                              $reservationAppend->execute([$newReservation]);


                            тем более Вы сами говорите, что


                            резерв может быть привязан и к другим бизнес операциям

                            dispatch(new Reservation($stockId, $amount)) и дальше по тексту.


                            1. что делает второй тип резервации — не понятно. тем более почему не меняется Quantity. но не в этом суть. тут, конечно вам видней, ваш бизнес.
                              1.1
                              $reset = new ResetReservated($reservated)
                            2. у вас STATUS_ORDER_... запилен в Reservation. это как? тем более, что Reservation не знает заказа

                            это только пара примеров. на пока что хватит.
                            но, о что вы называете Event Sourcing по вашей имплементации не более чем Event Driven. В Event Sourcing была бы сборка состояния сущности из соответствующих ивентов.
                            В вашем примере просто нет (видимого невооруженным глазом) Event Sourcing

                              0
                              Ну давайте по порядку:
                              Такой вызов нам не подходит:
                              $reservated = new ReservateFromStock($stockId, $amount)
                              
                              так как, во-первых, у нас создается отдельная резервация на каждый SKU в рамках стока, а не просто на сток.
                              зачем какой-то builder? конструктор уже не в моде?
                              Не конструкторы не в моде, а создание объектов через new не в моде, если вы хотите писать расширяемый код. Для этого существуют Creational Design patterns.
                              Зачем тогда вводить ReservationInterface если вы создаете объект резервацию через оператор new, и подменить на другую реализацию все равно не сможете.
                              в данном случае и есть те ивенты, то к чему следующий прямой вызов? $reservationAppend->execute([$newReservation]);
                              Вопрос не понятен. Этот вызов это SPI, который происходит при какой-то бизнес операции, при которой нужно создать резервации для продуктов (например, размещение заказа).
                              dispatch(new Reservation($stockId, $amount)) и дальше по тексту.

                              Насколько я понимаю, тут вы просто рекомендуете использовать именование dispatch. Или использование глобальной функции тоже часть рекомендации?
                              Класс-команда с методом execute для меня выглядет приблизительно также. Более того, такой стиль сейчас принят на проекте, поэтому для единообразия мы будем придерживаться его.
                              Использование глобальных или статических ф-ий нежелательно для кастомизации и расширения кода, поэтому их мы использовать не будем.
                              что делает второй тип резервации — не понятно. тем более почему не меняется Quantity.
                              А почему вы считаете, что количество для резервации должно меняться?
                              у вас STATUS_ORDER_… запилен в Reservation. это как?
                              Здесь прошу прощения, STATUS_ORDER_* быть не должно, копировал код для статьи из IDE, где они были.
                              На момент написания статьи идея была иметь резервации в OPEN/CLOSED состояниях.
                              Хотя сейчас видим, что состояние нам не особо нужно и больше требовалось в целях возможного дебага, поэтому решили заменить его Metadata.
                              В Event Sourcing была бы сборка состояния сущности из соответствующих ивентов. В вашем примере просто нет (видимого невооруженным глазом) Event Sourcing
                              Давайте я помогу и «вооружу» ваш взгляд.

                              У нас есть такой сервис
                              /**
                               * Service which returns Quantity of products available to be sold by Product SKU and Stock Id
                               *
                               * @api
                               */
                              interface GetProductQuantityInStock
                              {
                                  /**
                                   * Get Product Quantity for given SKU in a given Stock
                                   *
                                   * @param string $sku
                                   * @param int $stockId
                                   * @return float
                                   */
                                  public function execute($sku, $stockId);
                              }
                              

                              Задача которого посчитать актуальное количество продуктов, которые мы можем продать.
                              У нас есть StockItem, который хранит Qty как поле. Но это Qty устаревает, так как оно не обновляется моментально с каждым заказом. Оно обновляется во время реиндексации, после того когда мы выбрали склады из которых сделаем доставку, и соответственно обновили SourceItem.
                              Можно считать, что индекс StockItem — это проекция актуальная на какой-то период времени.
                              Но чтобы получить точное число продуктов в стоке та текущий момент времени — мы от этого числа отнимаем резервации, полученные условно-говоря таким запросом:
                              select 
                                 SUM(r.qty) as total_reservation_qty
                              from 
                                 Reservations as r
                              where 
                                stockId = {%id%} and sku  = {%sku%}

                              * и да резервации не обязательно могут или должны лежать в таблице.
                              Соответсвенно у нас есть Snapshot и ивенты — чем вам не Event Sourcing?
                                0
                                так как, во-первых, у нас создается отдельная резервация на каждый SKU в рамках стока, а не просто на сток.

                                извиняюсь, профакапил один параметр


                                Зачем тогда вводить ReservationInterface если вы создаете объект резервацию через оператор new, и подменить на другую реализацию все равно не сможете.

                                Да, согласен, шаблоны существуют. Но в данном случае:


                                1. Interface нужен сугубо вашему фрэймворку, чтобы ядро могло работать
                                2. Ваша реализация ReservationInterface имеет определенный Вами смысл в рамках Вашего фрэймворка.
                                3. Подмена реализации оправдывается лишь в рамках процесса. То есть в одном контексте одна конкретная, а в другом уже что-то другое. я создаю объект CustomReservation в контексте CustomReservationProcess
                                4. Ваш ReservationBuilder не выдаст ничего кроме запиленной в него реализации Reservation. Где тут заменяемость? Вам придется заменять сам Builder который будет билдить кастомную резервацию.
                                5. При вашем подходе придется мэйнтэйнить два класса чтобы получить результат, хотя Use Case, породивший надобность новой реализации, требует всего лишь одну новую CustomReservation

                                Вопрос не понятен

                                $reservationAppend->execute([$newReservation]);

                                Насколько я понимаю, тут вы просто рекомендуете использовать именование dispatch.

                                Нет, я не рекомендую само наименование. Если хотите, я рекомендую decoupling более соотвествующий Вашим же бизнес-требованиям.
                                Вы работаете напрямую с $reservationAppend. То есть, вы конкретно знаете кто и как отвечает за резервирование. На эту зависимость указывает само наименование переменной. В случае с dispatch, Вы не знаете кто и тем более как этот ивент отработает. Может там еще куча слушателей? А может быть надобность в этой операции отпала вообще.


                                А почему вы считаете, что количество для резервации должно меняться?

                                Вы не поняли вопроса.


                                точное число (Quantity) товара доступное для продажи…

                                Обработав этот заказ — создаем другое резервирование...

                                Не понятно следующее: Почему после обработки заказа резервация, как вы говорите, "гасится"? Или это такой интересный Business Case?


                                У нас есть StockItem, который хранит Qty как поле

                                Соответсвенно у нас есть Snapshot и ивенты — чем вам не Event Sourcing?

                                Теперь (частично) увидел. Спасибо. И это число вы сохраняете в StockItem, называя его Snapshot?


                                Но вы любите ссылаться на Грега Янга? Вот что он говорит насчет Snapshot.


                                https://youtu.be/8JKjvY4etTY?t=27m24s


                                Сравните с вашей реализацией. replaying events у вас сводится до примитивного агрегирующего SQL-запроса?


                                Quantity StockItem-a обновляется с задержкой (latency) вызванной природой Event Sourcing

                                Вы путаете Event Driven с Event Sourcing. Как раз при Event Sourcing задержки обновления никакой нет, потому что обновления как такового нет.


                                https://youtu.be/8JKjvY4etTY?t=9m18s


                                Приятного времени дня Вам :) Жду дальнейших публикаций.

                        0
                        Вы знаете, после общения с вами, решил немного изменить интерфейс резервации. Так как по факту статусами мы особо не пользовались. Это был больше механизм отладки для поиска потерянных резерваций. Но если он так сильно наталкивает на мысли о Finite State Machine, то лучше убрать его вообще и добавить поле Metadata.
                        github.com/magento-engcom/magento2/wiki/Reservations

                        /**
                         * The entity responsible for reservations, created to keep inventory amount (product quantity) up-to-date.
                         * It is created to have a state between order creation and inventory deduction (deduction of specific SourceItems)
                         *
                         * @api
                         */
                        interface ReservationInterface
                        {
                            /**
                             * Constants for keys of data array. Identical to the name of the getter in snake case
                             */
                            const RESERVATION_ID = 'reservation_id';
                            const STOCK_ID = 'stock_id';
                            const SKU = 'sku';
                            const QUANTITY = 'quantity';
                            const METADATA = 'metadata';
                        
                            /**
                             * Get Reservation id
                             *
                             * @return int|null
                             */
                            public function getReservationId();
                        
                            /**
                             * Get stock id
                             *
                             * @param int $stockId
                             * @return void
                             */
                            public function getStockId($stockId);
                        
                            /**
                             * Get Product SKU
                             *
                             * @return string
                             */
                            public function getSku();
                        
                            /**
                             * Get Product Qty
                             *
                             * @return float
                             */
                            public function getQuantity();
                        
                            /**
                             * Get Reservation Metadata
                             *
                             * @return string|null
                             */
                            public function getMetadata();
                        }
                        

                        пожалуй так даже лучше, так как следуя separation of concerns наличие статуса, который как-то связан с размещения заказа или обработкой — можно считать leaky abstraction

                  Only users with full accounts can post comments. Log in, please.