Pull to refresh
149
0
Игорь Миняйло @maghamed

Lead Architect, Magento an Adobe

Send message
у нас в om можем оставаться \Magento\Store\Model\StoreManager::getStore, или текущая категория, или значения из регистра, которые были сохранены в рамках выполнения предыдущего запроса.
В идеальном мире — если сам сервис stateless, то да. Но мы, к сожалению не в идеальном мире и у нас попрежнему есть состояния.
Было бы интересно, в частности запустить Magento web api тесты, на таком сетапе и посмотреть проходят ли они.
проблема со стейтом, к сожалению остается и с Web API, так как тот же ObjectManager и его состояние будет переиспользоваться между запросами. И если в случае одного потока это незаметно, то когда потоков будет много, и они будут в разных контекстах (сторов), то результаты могут быть неконсистентны.

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

Но поддержку PSR-7, мы точно хотим добавить. Если у вас есть время/желание — можете помочь Magento это сделать в виде контрибьюшена.
Возможно, в обозримом будущем, когда M2 полностью перейдет на декларативную схему и отпадет надобность писать патчи, это будет супер удобно. Но будет ли это и когда случится, вопрос остается открытым.


Вполне себе решенный вопрос, Magento 2.1 уже не поддерживается, а Мадженто 2.2 будет поддерживаться до декабря 2019 года, т.е. остался последний патч релиз 2.2.10 который выйдет в ближайший месяц.

magento.com/sites/default/files/magento-software-lifecycle-policy.pdf

Так что можете восприниматься, что М2 уже фактически полностью перешла на декларативную схему. Вопрос остается только с кастомизациями и экстеншенам, но с момента EOF 2.2 и они подтянутся.
Метода, который вы описали для работы с инвентори
/** @var \Magento\Catalog\Model\Product $prodEntity */
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $repoProd */
$inventory = [
    'is_in_stock' => true,
    'qty' => 1234
];
$prodEntity->setData('quantity_and_stock_status', $inventory);
$repoProd->save($prodEntity);

Является устаревшим в Magento 2.3.
C появлением нового механизма инвентори (Magento MSI) данные инвентори отвязаны от сущности продукта и сохранять их предлагается через отдельные API эндпоинты:

https://github.com/magento-engcom/msi/tree/2.3-develop/app/code/Magento/InventoryApi/Api

Здесь можно почитать подробней о новом механизме инвентори — https://devdocs.magento.com/guides/v2.3/inventory/
нет, это будет новый Entity Manager, мы добавили для старого \Magento\Framework\EntityManager\EntityManager такой комментарий в коде: github.com/magento/magento2/blob/2.2-develop/lib/internal/Magento/Framework/EntityManager/EntityManager.php#L15-L24

Основная причина отказаться от него — неэффективная работа с экстеншен атрибутами, работа с разными хранилищами данных, отсутствие эффективного механизма Query API.

>В какой версии предполагается релиз этого функционала?
Какие-то части функционала нового ORM мы уже зарелизим в 2.3.*
например декларативную схему — community.magento.com/t5/Magento-DevBlog/A-Declarative-Approach-for-Database-Schema-Upgrades/ba-p/70763
которая уже есть в 2.3-alpha

но полность функционал нового ORM должен быть доступен в версии 2.4
По поводу пункта 3.

Вы тоже правы, что extension_attributes это не о персистенсе, более того на этом уровне сущность должна быть описана persistence-agnostic.
И у нас по прежнему есть описание типа «join» в XSD Schema — github.com/magento/magento2/blob/2.2-develop/lib/internal/Magento/Framework/Api/etc/extension_attributes.xsd#L48-L55

но по факту вы не найдете его использования в модулях Magento, только в тестах.
Этот механизм, который назывался Join Directives был обьявлен как deprecated из-за причин описанных Вами и его поддержка остается только по причинам Backward Compatibility, поэтому сейчас обязанности по сохранению и извлечению атрибута возложены на программиста, зачастую это делается с помощью механизма плагинизации, например сервиса Repository::save($entity).

Более элегантное решение с точки зрения описания Entity, а также два типа декларации: persistence-agnostic высокоуровневой декларации, и низкоуровневой декларации, которая декларативно опишет загрузку данных (для ускорения производительности, так как сейчас с этим есть определенные пробелемы, так как дополнительные атрибуты хранятся в отдельных таблицах и нужно сохранять и загружать их отдельными запросами) появятся вместе с новым ORM в Magento

сорри, только сейчас заметил ваш диалог.
В целом Вы isxam все правильно написали.

Magento отказалась от наследования DTO классов от Magento\Framework\Api\AbstractExtensibleObject, и сейчас новый код, в частности новые модули Inventory, используют наследование от Magento\Framework\Model\AbstractExtensibleModel.

Основную неразбериху, думаю вызвало то, что класс AbstractExtensibleObject помечен как `@api`, в то время как AbstractExtensibleModel не имеет соответствующей пометки.

Вчера на внутреннем архитектурном митинге, где мы в частности обсуждали этот вопрос приняли решение:
  • Добавить PHP аннотацию `@api` для AbstractExtensibleModel
  • Добавить PHP аннотацию `@deprecated` для AbstractExtensibleObject и `@see` которая ссылается на AbstractExtensibleModel


Ну и судя по статье вы сделали классический God object из своего объекта Wish. В нем зашили вообще всю логику домена, нарушив при этом Single Responsibility принцип множество раз.
Само количество публичных методов у этого объекта (а их там 20) толжно было дать вам почувствовать «smell» в вашем коде.
у вас даже начало статьи принципиально не DDD-шное, и показывает, что вы мыслите CRUD подходом.
Потому, что статью вы начинаете с того, как создаете проект и базу данных для него.
Для Domain Drive Design — это абсолютно не важно. Детали реализации, которые должны быть вынесены за рамки статьи. Так как один из главных принципов DDD, который отражается в проектировании это persistence ignorance.

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

За это и отвечает слой сервис контрактов, интерфейсы которого здесь представлены.
А то как будут они вызваться и какой будет транспорт. Будет ли использоваться классическая событийная модель, таблица базы данных, транзакционная модель с предпочтением data consistency, или BASE с Eventual Consistency — это уже обусловлено бизнес процессами конкретного мерчанта.
Поэтому и dispatch мы не показываем наружу. Так как диспатч — это часть реализации характерная для определенного бизнес процесса.
Ну давайте по порядку:
Такой вызов нам не подходит:
$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?
Вы знаете, после общения с вами, решил немного изменить интерфейс резервации. Так как по факту статусами мы особо не пользовались. Это был больше механизм отладки для поиска потерянных резерваций. Но если он так сильно наталкивает на мысли о 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
ну вот, вы нашли в моей статье и коде то, чего там не было, а в итоге обвинили код и его читабельность :)
Давайте опишу более популярно, так как мне кажется, что вы не имели опыта работы с 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 как-то по другому выглядит?
Давайте по порядку. Потому как вы начали спорить с самим собой.
Где вы прочитали, про то, что какой-то из атрибутов сущности резервирования (в частности статус) изменяется?
Из описания статьи:
размещая заказ на 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, т.е. напрямую ваш код не работает с ними.
Пользователь системы не должен хотеть работать с объектами резервирования.
он видит конечный автомат. Так как он не знает, что вы собираете состояние резервации. По большому счету ему на это забить, он хочет знать актуальное состояние его резервации или заказа.
Вы даете больше ответственности на сущность резервации чем это делаем мы.
Пользователю интересно — состояние заказа.
Сущность резервации не отвечает за статус заказа и не изменяется вместе с ним. Объект резервации создается для подсчета корректности стока, который используется для вычисления колличества продуктов в стоке.
Вы вероятно пропусти описание механизма работы объектов резервирования. Само состояние в пределах объекта не изменяется. Reservation является immutable сущностью, при этом у него есть статус, который говорит о том в каких условиях (в рамках какого бизнес процесса) создавалась резервация.
По большому счёту это точка расширения для кастомизаторов. У нас вполне могло и не быть статуса, нам хватает набора (stockId, sku, qty) для подсчета актуальности стока.

Т.е. Если вы представили конечный автомат, и резервацию, которая переходит из одного состояние в другое — то это предположение ошибочно
В теле статьи я описал как в Magento 2 выглядят репозитории и почему.
А доступ к базе у нас «спрятан» не в командах, которые должны быть агностичны к Data Storage механизму, а в ресурс моделях.
Пример из статьи:
Команда для MultipleSave опареции SourceItem — Magento\Inventory\Model\SourceItemSave.php

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

Repository у нас это Фасад, чтобы клиент мог иметь одну зависимость на репозитори для выполнения базовых операций с сущностью. Например, достать сущность по ID или поиском, изменить, сохранить (если он делает CRUD операции над сущностью, например в гриде). По большому счету Repository существует только для удобства и простоты кода клиента, так как мы могли бы просто давать набор команд.
метод getStatus возвращает одну из предопределенных констант, статуса в котором может пребывать резервация. Под капотом этот статус хранится как Int.
Если вопрос почему мы не использовали Value-Object для хранения статуса, то мы не заменяем примитивные тимы на типы-обертки по двум причинам:
1. performance, чтобы не создавать дополнительное большое количество объектов.
2. это бы перегрузило и усложнило код бизнес логики (клиента), так как для создания статуса и других атрибутов (которых может быть много) код бизнес логики должен был бы использовать фабрики.
Ну давайте начнем с того, что в статье описан Service Layer, который представляет набор доменных сервисов (терминами DDD), соответсвенно название этих сервисов должны представлять бизнес процессы в домене на языке Ubiquitous Language и отображать как пользователи пользуются системой.
Поэтому на этом уровне dispatch у нас быть не может.

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

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

В статье сознательно не описывается метод хранения резерваций (Event Store если позволите), так как он может быть реализован по-разному. Этому может быть таблица базы данных, очередь Redis, Cassandra и т.д.
По вопросу гридов, в 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 возможностью подменять модель данных присутствует.

Information

Rating
Does not participate
Location
Киев, Киевская обл., Украина
Date of birth
Registered
Activity