diff --git a/core/components/minishop3/migrations/20251020000000_initial_schema.php b/core/components/minishop3/migrations/20251020000000_initial_schema.php
index 8182bc2c..98c6c42e 100644
--- a/core/components/minishop3/migrations/20251020000000_initial_schema.php
+++ b/core/components/minishop3/migrations/20251020000000_initial_schema.php
@@ -50,14 +50,12 @@ public function up()
// Get MODX instance
$modxConfigPath = dirname(__FILE__, 5) . '/config.core.php';
if (!file_exists($modxConfigPath)) {
- $this->output->writeln('MODX config.core.php not found');
- return;
+ throw new \RuntimeException('MODX config.core.php not found');
}
require_once $modxConfigPath;
if (!defined('MODX_CORE_PATH')) {
- $this->output->writeln('MODX_CORE_PATH not defined');
- return;
+ throw new \RuntimeException('MODX_CORE_PATH not defined');
}
require_once MODX_CORE_PATH . 'vendor/autoload.php';
@@ -71,6 +69,7 @@ public function up()
$modx->addPackage('MiniShop3\\Model', $modelPath, null, 'MiniShop3\\');
$manager = $modx->getManager();
+ $failedTables = [];
$this->output->writeln('Creating MiniShop3 tables...');
$this->output->writeln("Model path: {$modelPath}");
@@ -82,6 +81,7 @@ public function up()
$mysqlClass = 'MiniShop3\\Model\\mysql\\' . $className;
if (!class_exists($mysqlClass)) {
$this->output->writeln(" ✗ Model class not found: {$mysqlClass}");
+ $failedTables[] = $this->resolveLogicalTableName($className) ?? $className;
continue;
}
@@ -102,18 +102,32 @@ public function up()
if ($created) {
$this->output->writeln(" ✓ Created table: {$tableName}");
} else {
+ $logicalTable = $this->resolveLogicalTableName($className) ?? trim($tableName, '`');
$this->output->writeln(" ✗ Failed to create table: {$tableName}");
+ $failedTables[] = $logicalTable;
- // Log xPDO errors
- $errors = $modx->errorHandler->errors;
- if (!empty($errors)) {
- foreach ($errors as $error) {
- $this->output->writeln(" " . print_r($error, true) . "");
- }
+ foreach ($modx->errorHandler->errors as $error) {
+ $message = is_array($error) ? ($error['message'] ?? json_encode($error)) : (string) $error;
+ $this->output->writeln(" {$message}");
}
+ continue;
+ }
+
+ $logicalTable = $this->resolveLogicalTableName($className);
+ if ($logicalTable !== null && !$this->hasTable($logicalTable)) {
+ $this->output->writeln(
+ " ✗ Failed to create table: {$tableName} (createObjectContainer succeeded but table is missing)"
+ );
+ $failedTables[] = $logicalTable;
}
}
+ if ($failedTables !== []) {
+ throw new \RuntimeException(
+ 'MiniShop3 initial schema failed for tables: ' . implode(', ', $failedTables)
+ );
+ }
+
$this->output->writeln('MiniShop3 schema creation completed!');
// Add foreign keys after all tables are created
@@ -181,6 +195,19 @@ public function down()
$this->output->writeln('MiniShop3 schema removal completed!');
}
+ /**
+ * Unprefixed Phinx table name for a model class (e.g. msGridField -> ms3_grid_fields).
+ */
+ protected function resolveLogicalTableName(string $className): ?string
+ {
+ $mysqlClass = 'MiniShop3\\Model\\mysql\\' . $className;
+ if (!class_exists($mysqlClass)) {
+ return null;
+ }
+
+ return $mysqlClass::$metaMap['table'] ?? null;
+ }
+
/**
* Convert CamelCase to snake_case
* Example: msOrderProduct -> order_products
diff --git a/core/components/minishop3/migrations/20260522120000_repair_grid_fields_if_missing.php b/core/components/minishop3/migrations/20260522120000_repair_grid_fields_if_missing.php
new file mode 100644
index 00000000..f8083df3
--- /dev/null
+++ b/core/components/minishop3/migrations/20260522120000_repair_grid_fields_if_missing.php
@@ -0,0 +1,182 @@
+ seed migration + optional follow-up patches for that grid only.
+ *
+ * @var array}>
+ */
+ private const GRID_REPAIR_PLAN = [
+ 'customers' => [
+ 'seed' => ['class' => 'SeedCustomersGridConfig', 'file' => '20251127000002_seed_customers_grid_config.php'],
+ ],
+ 'orders' => [
+ 'seed' => ['class' => 'SeedOrdersGridConfig', 'file' => '20251204140000_seed_orders_grid_config.php'],
+ 'patches' => [
+ ['class' => 'FixOrdersGridFilterable', 'file' => '20260107120000_fix_orders_grid_filterable.php'],
+ ['class' => 'UpdateOrdersGridStatusFields', 'file' => '20260119220000_update_orders_grid_status_fields.php'],
+ ],
+ ],
+ 'order_products' => [
+ 'seed' => ['class' => 'SeedOrderProductsGridConfig', 'file' => '20251215120000_seed_order_products_grid_config.php'],
+ ],
+ 'deliveries' => [
+ 'seed' => ['class' => 'SeedDeliveriesGridConfig', 'file' => '20251222120000_seed_deliveries_grid_config.php'],
+ ],
+ 'vendors' => [
+ 'seed' => ['class' => 'SeedVendorsGridConfig', 'file' => '20251223130000_seed_vendors_grid_config.php'],
+ ],
+ 'category-products' => [
+ 'seed' => ['class' => 'SeedCategoryProductsGridConfig', 'file' => '20251231140000_seed_category_products_grid_config.php'],
+ 'patches' => [
+ ['class' => 'AddDuplicatePublishToCategoryProductsActions', 'file' => '20260317120000_add_duplicate_publish_to_category_products_actions.php'],
+ ],
+ ],
+ ];
+
+ private string $prefix = '';
+
+ public function up(): void
+ {
+ $this->prefix = (string) ($this->getAdapter()->getOption('table_prefix') ?? '');
+
+ $emptyGridKeys = $this->findEmptyGridKeys();
+ if ($emptyGridKeys === []) {
+ return;
+ }
+
+ if (!$this->hasTable('ms3_grid_fields')) {
+ $this->ensureGridFieldsTable();
+ }
+
+ if (!$this->hasTable('ms3_grid_fields')) {
+ throw new \RuntimeException('ms3_grid_fields is still missing after repair attempt');
+ }
+
+ $this->loadMigrationFilesForGridKeys($emptyGridKeys);
+
+ foreach ($emptyGridKeys as $gridKey) {
+ $plan = self::GRID_REPAIR_PLAN[$gridKey];
+ $this->runChildMigration($plan['seed']['class']);
+
+ foreach ($plan['patches'] ?? [] as $patch) {
+ $this->runChildMigration($patch['class']);
+ }
+ }
+ }
+
+ public function down(): void
+ {
+ // Repair is data correction; rolling back would remove restored defaults.
+ }
+
+ /**
+ * @return list
+ */
+ private function findEmptyGridKeys(): array
+ {
+ if (!$this->hasTable('ms3_grid_fields')) {
+ return array_keys(self::GRID_REPAIR_PLAN);
+ }
+
+ $emptyGridKeys = [];
+ foreach (array_keys(self::GRID_REPAIR_PLAN) as $gridKey) {
+ if ($this->countGridRows($gridKey) === 0) {
+ $emptyGridKeys[] = $gridKey;
+ }
+ }
+
+ return $emptyGridKeys;
+ }
+
+ private function ensureGridFieldsTable(): void
+ {
+ $modx = $this->bootstrapModx();
+ $tableFqn = $modx->getTableName(\MiniShop3\Model\msGridField::class);
+ $created = $modx->getManager()->createObjectContainer(\MiniShop3\Model\msGridField::class);
+ if (!$created || !$this->hasTable('ms3_grid_fields')) {
+ throw new \RuntimeException('Failed to create table ' . trim($tableFqn, '`'));
+ }
+
+ $this->output->writeln('Created missing table ' . trim($tableFqn, '`') . '');
+ }
+
+ private function countGridRows(string $gridKey): int
+ {
+ $quotedGridKey = $this->getAdapter()->getConnection()->quote($gridKey);
+ $row = $this->fetchRow(
+ "SELECT COUNT(*) AS cnt FROM {$this->prefix}ms3_grid_fields WHERE grid_key = {$quotedGridKey}"
+ );
+
+ return (int) ($row['cnt'] ?? 0);
+ }
+
+ /**
+ * @param list $gridKeys
+ */
+ private function loadMigrationFilesForGridKeys(array $gridKeys): void
+ {
+ $migrationsDir = __DIR__ . DIRECTORY_SEPARATOR;
+ $filesToLoad = [];
+
+ foreach ($gridKeys as $gridKey) {
+ $plan = self::GRID_REPAIR_PLAN[$gridKey];
+ $filesToLoad[$plan['seed']['file']] = true;
+
+ foreach ($plan['patches'] ?? [] as $patch) {
+ $filesToLoad[$patch['file']] = true;
+ }
+ }
+
+ foreach (array_keys($filesToLoad) as $fileName) {
+ require_once $migrationsDir . $fileName;
+ }
+ }
+
+ private function runChildMigration(string $migrationClass): void
+ {
+ /** @var AbstractMigration $migration */
+ $migration = new $migrationClass();
+ $migration->setAdapter($this->getAdapter());
+ $migration->setOutput($this->output);
+ $migration->up();
+ }
+
+ private function bootstrapModx(): \MODX\Revolution\modX
+ {
+ $modxConfigPath = dirname(__FILE__, 5) . '/config.core.php';
+ if (!file_exists($modxConfigPath)) {
+ throw new \RuntimeException('MODX config.core.php not found');
+ }
+
+ require_once $modxConfigPath;
+ if (!defined('MODX_CORE_PATH')) {
+ throw new \RuntimeException('MODX_CORE_PATH not defined');
+ }
+
+ require_once MODX_CORE_PATH . 'vendor/autoload.php';
+ require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
+
+ $modx = new \MODX\Revolution\modX();
+ $modx->initialize('mgr');
+
+ $modelPath = MODX_CORE_PATH . 'components/minishop3/src/Model/';
+ $modx->addPackage('MiniShop3\\Model', $modelPath, null, 'MiniShop3\\');
+
+ return $modx;
+ }
+}
diff --git a/core/components/minishop3/schema/minishop3.mysql.schema.xml b/core/components/minishop3/schema/minishop3.mysql.schema.xml
index ec4c333d..187eb075 100644
--- a/core/components/minishop3/schema/minishop3.mysql.schema.xml
+++ b/core/components/minishop3/schema/minishop3.mysql.schema.xml
@@ -622,8 +622,8 @@
-
-
+
+
diff --git a/core/components/minishop3/src/Model/mysql/msGridField.php b/core/components/minishop3/src/Model/mysql/msGridField.php
index f46810ff..5717c99b 100644
--- a/core/components/minishop3/src/Model/mysql/msGridField.php
+++ b/core/components/minishop3/src/Model/mysql/msGridField.php
@@ -123,16 +123,15 @@ class msGridField extends \MiniShop3\Model\msGridField
'default' => 1,
],
'created_at' => [
- 'dbtype' => 'timestamp',
- 'phptype' => 'timestamp',
+ 'dbtype' => 'datetime',
+ 'phptype' => 'datetime',
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
],
'updated_at' => [
- 'dbtype' => 'timestamp',
- 'phptype' => 'timestamp',
+ 'dbtype' => 'datetime',
+ 'phptype' => 'datetime',
'null' => true,
- 'extra' => 'on update CURRENT_TIMESTAMP',
],
],
'indexes' => [