Как загрузить?
- PHP 8.2+ — современный синтаксис, типизация, атрибуты
- Namespaces — все классы организованы в пространстве имён MiniShop3\
- PSR-4 автозагрузка — через Composer
- Миграции Phinx — версионирование структуры БД
Улучшенная архитектура
- REST API — полноценный API для headless-интеграций
- Service Container — зависимости через DI-контейнер MODX
- Vue 3 + PrimeVue — современный интерфейс админки через VueTools
- Современный фронтенд — без jQuery, нативный JavaScript
Совместимость
MiniShop3 сохраняет обратную совместимость с miniShop2 на уровне:
- Имена сниппетов (msProducts, msCart, msOrder и др.)
- Структура чанков и плейсхолдеров
- Параметры сниппетов
Системные требования
- MODX Revolution | 3.0.0+
- PHP | 8.1+
- MySQL | 5.7+ / MariaDB 10.3+
Зависимости MODX
- pdoTools 3.x — для работы сниппетов и шаблонизатора Fenom
- VueTools — Vue 3 и PrimeVue для административного интерфейса
- Scheduler (опционально) — для фоновых задач (импорт, уведомления, очистка)
MiniShop3 использует Vue 3 для современного интерфейса админки. Пакет VueTools должен быть установлен до или вместе с MiniShop3. При отсутствии пакета будет показано сообщение с инструкцией по установке.
Composer библиотеки
MiniShop3 использует следующие PHP библиотеки (включены в пакет):
nikic | ^1.3 | Маршрутизация REST API
rakit/validation | ^1.4 | Валидация данных форм и API
intervention/image | ^3.0 | Обработка изображений (ресайз, водяные знаки)
robmorgan/phinx | ^0.16 | Миграции базы данных
ramsey/uuid | ^4.7 | Генерация UUID для токенов
[2026-06-22] 🚀 Версия 1.12.0-beta1
Тип релиза: MINOR (beta) — три новые фичи в админке + обширная серия фиксов install reliability, processor флоу и Manager API.
✨ Добавлено
Тип extra field «повторитель» — ms3-repeater (#299, #301): JSON-массив объектов со схемой колонок, drag-and-drop сортировкой строк и автоматическим проставлением rank при сохранении (MIGX-паттерн).
- Backend. Миграция
add_repeater_config_to_extra_fieldsдобавляет колонкуrepeater_config TEXTвms3_extra_fields. Новый сервисRepeaterFieldService(ms3_repeater_field) с pipelinedecode → normalize → validate: парсит schema, валидируетminRows/maxRows/required/numberfieldcells, авто-простановкаrankот 0. Интеграция вExtraFieldsService::applyRepeaterConstraints()форситdbtype=json, phptype=json, null=trueдля repeater extra field.ProductDataService::prepareObject()исключает repeater из option-sync.OrdersControllerотдаёт 422 при invalid payload для repeater-полейmsOrder/msOrderAddress. - Vue Manager. Компонент
RepeaterField(рендер таблицы + dnd черезvuedraggable),RepeaterSchemaEditorвExtraFieldsManager(настройка колонок),OrderExtraFieldsSectionдля msOrder/msOrderAddress extra fields, утилитаrepeaterField.js(parse/normalize/encode).DynamicFieldрендерит `` + hidden input для bridge'а в legacy POST формы товара. - Public output. На странице товара
{$_modx->resource.repeater}нативно возвращает массив (xPDOphptype='json'авто-декод). В листингах черезmsProducts— raw JSON string, требует\| json_decode : true(MIGX-паттерн).
Грид товаров категории — option-колонки (#140, #154): новый тип колонки option показывает значения опций товара (msProductOption) в гриде с сортировкой и фильтром.
- Read-only display:
LEFT JOIN msProductOption+GROUP_CONCAT(DISTINCT ...)для multi-value опций. КонтроллерCategoryProductsListService(ms3_category_products_list) делегирует SQL-сборку сервису; DTOOptionColumnSpecвалидируетoption.key([a-z0-9_]+) иfieldName(whitelist + denylist builtin полей вродеprice,article— иначе FETCHASSOC overwrite ломал бы реальные значения). Filter — `filter{fieldName}LIKE по значению опции; контракт фронт-фильтрации стыкуется сdirect_filter_keys` (см. #317). - В
GridFieldsConfig.vueпоявился новый тип «Опция товара» в Add/Edit диалогах + поле «Ключ опции»; чекбокс «Редактируемое поле» автоматически скрывается для option-колонок (option-колонки read-only by design — редактирование опций товара делается в карточке товара).
Грид товаров категории — inline-edit с Select / Combo (#155, #157): для редактируемых колонок добавлены два новых типа редактора: select (статический список из editor_options JSON) и combo (API-источник через editor_reference).
- Реестр allowed reference-ключей в
GridEditorReferenceRegistry(на старте —vendors), валидация combo при сохранении конфига колонки. Override-URL черезeditor_combo_endpointпринимается только если путь в allowlist (/api/mgr/references/). EndpointGET /api/mgr/grid-config/category-productsтеперь отдаётeditor_references(список доступных reference-ключей) для UI настройки. ЭндпойнтGET /api/mgr/references/vendorsдополняет ответ единым массивомoptions: [{value, label}]; полеvendorsсохранено для обратной совместимости. - Vue composable
useCategoryProductsInlineEditинкапсулирует state inline-edit, загрузку combo, type coercion (фиксbool ↔ intmismatch'а в PrimeVue Select), global ESC handler и click-outside dismiss. УтилитыgridEditorOptions.js: allowlist URL, маппинг ответа API → options.
🐛 Исправлено
Сохранение Data и extra fields в процессорах товаров (#297, #298): при вызове MiniShop3\Processors\Product\Create|Update (импорт CSV, сторонние интеграции, alias Resource\Create::getInstance() для class_key=msProduct) значения колонок msProductData молча терялись.
- Корневые причины:
Resource\Create|Updateвызывает$object->fromArray($properties)безignoreInvalid— колонкиmsProductDataне попадали на объект; наCreatecompositeDataтребовалidресурса, которого ещё не было вbeforeSave(). - Решение — trait
ProductDataPayloadTrait: захват блокаDataвbeforeSet()(с логом при невалидном JSON), whitelist черезgetDataFieldsNames()+loadMap()для extra fields, применение черезapplyProductDataPayload()вbeforeSave()(Update) иpersistProductDataPayload()вafterSave()(Create). Alias-классыmsProductCreateProcessor/msProductUpdateProcessorдля резолвераResource\Create|Update::getInstance().
Per-field null payload semantics в Manager API (#289, #310): контракт null в payload теперь определяется явно для каждого поля.
- Раньше во всех API-ручках Manager (
OrdersController::updateProduct/create,ExtraFieldsService::updateField,CustomerAddressManager::update) проверка черезisset($data[$field])молча пропускалаnull→ пользователь не мог очистить поле, отправляяnullиз PrimeVueInputNumber. Прямой замены наarray_key_existsоказалось мало — открывала risk обнуленияcount/price/weightпри случайномnullв partial payload. - Per-field семантика: для числовых полей (
count,price,weight) и required-полей (label,xtype,active) —nullskip; для JSON/options/text nullable (options,select_options,description, адресные поля) —null= очистить. Документировано в коде. - Advisory для апгрейдеров. Сторонним API-клиентам, которые слали
nullдля текстовых nullable-полей с намерением «очистить» — теперь работает как ожидается; если кто-то слалnullрассчитывая на silent skip — теперь поле очистится. Поведение для числовых полей не меняется.
Установка не создавала grid_fields правильно (#270, #294): на части установок таблица ms3_grid_fields отсутствовала или была частично заполнена.
initial_schemaтеперь fail-fast: при ошибкеcreateObjectContainer()или если таблица не создалась послеsuccess=true, миграция бросаетRuntimeExceptionс перечнем проблемных таблиц вместо тихого продолжения. Repair-миграция20260522120000_repair_grid_fields_if_missing.phpself-heal'ит установки, где таблица отсутствует или отдельныеgrid_keyпусты (в т.ч. после no-op seed из #276). ALTER-миграция20260528120000_alter_grid_fields_datetime_columns.phpпереводит существующие установки с TIMESTAMP на DATETIME дляcreated_at/updated_at(новые установки получают DATETIME сразу).
Установка падала на установках с не-utf8 MODX-кодировкой (#307, #308): Phinx брал из $modx->getOption('charset') веб-кодировку вроде UTF-8, MySQL не знал такого имени → сидированные русские темы email-уведомлений писались битыми.
- В
phinx.phpдобавлены: парсинг charset изdatabase_dsn, нормализаторms3PhinxNormalizeMysqlCharset()(UTF-8→utf8mb4,utf8mb3→utf8mb4для новых установок, и т.п.), резолвер collation. Каскад приоритетов: DSN charset >database_charset> MODXcharset(с$preferUtf8mb4=trueдля fallback). Статический тестtests/PhinxCharsetConfigTest.phpбез MODX bootstrap.
Phinx-таблица миграций не получала table_prefix (#313, #318): Phinx писал ms3_migrations без префикса, в то время как остальные таблицы компонента создавались с {table_prefix}ms3_*.
- Конфиг
phinx.phpтеперь подставляет$dbConfig['table_prefix']вdefault_migration_table. Для существующих установок с непустым префиксом и legacy таблицей без префикса добавлен safeRENAME TABLEперед стартом Phinx — иначе Phinx посчитал бы установку «свежей» и попытался прогнать все миграции с нуля. Логирование в MODX-лог. Тестtests/PhinxMigrationTablePrefixTest.phpпокрывает четыре сценария (rename / both exist noop / fresh install / empty prefix).
Customer-уведомления использовали устаревший email клиента (#218, #320): при оформлении нескольких заказов в одной сессии с разными контактами уведомления слались по email из msCustomer, а не из формы заказа.
OrderStatusService::getCustomerRecipient()теперь резолвит контакты в порядкеmsOrderAddress→msCustomer→modUserProfile. Дополнительный ключrecipient['address']доступен плагинам/шаблонам; резолвленныеemail/phoneзеркалятся вrecipient['customer']для backward compat.EmailChannel::getRecipientEmail()симметриченSmsChannel::getRecipientPhone()(fallback на address.email). Логика создания/обновленияmsCustomerне менялась.
Customers grid — autofill ломал строку поиска (#286, #319): браузер после сохранения клиента автозаполнял логин MODX-пользователя в строку поиска грида, список пропадал.
- Слоёная защита от browser autofill:
` обёртка формы редактирования, префиксные ID полей (ms3-customer-*),autocomplete="new-password"для пароля, явныйtype="button"на кнопках формы. Defensive restore:searchQueryсохраняется до save и восстанавливается черезnextTick` после закрытия модалки.
class_key не подставлялся при создании msProduct/msCategory через резолвер (#305, #306, #315): при вызове Resource\Create::getInstance() без явного class_key (импорт CSV, сторонние интеграции) ресурс создавался как modDocument.
Product\Create::initialize()иCategory\Create::initialize()теперь подставляют$this->classKey(=msProduct::class/msCategory::class), если в payloadclass_keyотсутствует / пустой / равенmodDocument::class. Явно переданныйclass_key(например,msVariationот кастомного клиента) — сохраняется.
Импорт CSV — multi-value option columns (#309, #312): ячейка CSV вида "мука, вода, соль" для multi-value опций (comboMultiple, comboColors, comboOptions) сохранялась одной записью в ms3_product_options.
- Парсер
Utils::parseImportedOptionValue()в pipelineJSON decode → comma-split → trim → filter empty. Для single-value типов запятая остаётся литералом (текстовые опции не ломаются). ПолеmsOption.typeчитается одним запросом для всех ключей (N+1 fix), импорт делегируетOptionService::saveProductOptions(..., removeOther: false)— partial update, не затирает не указанные в CSV опции. HelpermsOptionType::isMultiValueType()централизует whitelist.
Orders grid — переключатель черновиков + фильтры (#302, #303): в Vue-гриде заказов добавлен чекбокс «Показывать черновики» с persistence через localStorage. Стартовое значение читается из системной настройки ms3_order_show_drafts.
- На бэкенде
OrdersController::applyDraftVisibilityFilter()— отдельный метод, переиспользуется вgetList()иgetOrdersStats(). Параметрshow_draftsв запросе перебивает системную настройку черезFILTER_VALIDATE_BOOLEAN. Подсказка над блоком статистики «Количество и сумма по оформленным заказам; черновики не учитываются» (счётчик намеренно не реагирует на drafts toggle — статистика по «оформленным»). - Tooltip над статистикой объясняет почему счётчик и грид могут показывать разные числа.
♻️ Рефакторинг
Единый контракт direct_filter_keys для гридов (#314, #317): список фильтров, отправляемых без префикса filter_, теперь живёт на backend и отдаётся фронту вместе с конфигом грида. Убрано дублирование между Vue-гридами и PHP-контроллерами.
OrdersController::DIRECT_FILTER_KEYS,CustomersController::DIRECT_FILTER_KEYS+ статическийgetDirectFilterKeys().GridConfigController::DIRECT_FILTER_KEY_PROVIDERSмаппитgrid_keyна класс провайдера и добавляет ключи в ответ/api/mgr/grid-config/{key}.- Vue composable
useGridFilterParams(для OrdersGrid и CustomersGrid):addFilterParam(params, key, value)решает префикс на основе массива от backend. При пустом списке (старый/кэшированный backend) — safe fallback наfilter_-префикс. - Дополнительные правки
OrdersController: общийapplyDirectFilters()дляgetList()иgetOrdersStats()(раньше — дублирующийся inline-блок ~24 строк); попутный bug fix — address JOIN в stats query при наличииfilter_customer/email/phone(до этого фильтрация по адресу в stats тихо валилась).
XML-схема синхронизирована с msOptionGroup (#10, #322): core/components/minishop3/schema/minishop3.mysql.schema.xml отставал от src/Model/mysql/* после введения msOptionGroup в 1.11.0.
msOption.modcategory_id→option_group_id(nullable, default NULL); индекс и aggregate переименованы;Group → msOptionGroupвместоCategory → modCategory.- Добавлен
` (таблицаms3_option_groups, 5 полей, BTREE наsort_order, aggregateOptions**local owner** — не composite, чтобы$group->remove()` не каскадно удалял опции). - Runtime source of truth —
src/Model/mysql/*, XML остаётся как справочный документ.
📋 Документация
docs/components/minishop3/development/events/notifications.md— новая подтаблицаrecipientдля customer-получателя (резолвaddress → customer → profile, зеркалирование вrecipient['customer'], ссылка на issue #218). Закрывает гэп после PR #320.docs/components/minishop3/development/routing.md— новый раздел «Конфигурация гридов (/grid-config)» с CRUD-эндпойнтами, известнымиgrid_key, структурой ответа (columns/direct_filter_keys/editor_references) и контрактом фронт-фильтрацииuseGridFilterParams. Раньше в доке этой группы роутов не было вообще.
📦 Зависимости
Без изменений.




Последние обсуждения в сообществе MODX.pro