diff --git a/Controller/Admin/ConfigController.php b/Controller/Admin/ConfigController.php index d6a45ab..8c06e37 100644 --- a/Controller/Admin/ConfigController.php +++ b/Controller/Admin/ConfigController.php @@ -126,6 +126,7 @@ public function index(Request $request, Connection $em) ]); // $csvDir 内のファイルをすべて読み込む + // PostgreSQLはUPSERT方式を使うため、TRUNCATE不要 $files = scandir($csvDir); foreach ($files as $file) { // csvファイルのみ処理 @@ -343,6 +344,12 @@ private function saveToC($em, $tmpDir, $csvName, $tableName = null, $allow_zero $value[$column] = !empty($data[$column]) ? $data[$column] : null; } elseif ($column == 'point') { $value[$column] = empty($data[$column]) ? 0 : (int) $data[$column]; + } elseif ($column == 'two_factor_auth_enabled' && ($tableName == 'dtb_member' || $tableName == 'dtb_customer')) { + // 4.0系には存在しないカラム。デフォルト値として0(無効)を設定 + $value[$column] = isset($data[$column]) ? $data[$column] : 0; + } elseif ($column == 'two_factor_auth_key' && ($tableName == 'dtb_member' || $tableName == 'dtb_customer')) { + // 4.0系には存在しないカラム。NULLを設定 + $value[$column] = isset($data[$column]) ? $data[$column] : null; } elseif ($allow_zero) { $value[$column] = isset($data[$column]) ? $data[$column] : null; } else { @@ -399,6 +406,9 @@ private function saveToC($em, $tmpDir, $csvName, $tableName = null, $allow_zero $value[$column] = !empty($data[$column]) ? $data[$column] : null; } elseif ($column == 'creator_id') { $value[$column] = !empty($data[$column]) ? $data[$column] : 1; + } elseif ($column == 'two_factor_auth_enabled' && $tableName == 'dtb_member') { + // 4.0系には存在しないカラム。デフォルト値として0(無効)を設定 + $value[$column] = isset($data[$column]) ? $data[$column] : 0; } elseif ($column == 'plg_mailmagazine_flg') { $value[$column] = (!empty($data['mailmaga_flg']) && $data['mailmaga_flg'] != 3) ? 1 : 0; } elseif ($column == 'id' && $tableName == 'dtb_member') { @@ -1050,6 +1060,15 @@ protected function upsertAuthorityAndMember($em, $dir) $insertValues[$col] = $data[$col] ?? 'member'; continue; } + // 4.0系のカラム名マッピング + if ($col === 'work' && !array_key_exists($col, $data) && array_key_exists('work_id', $data)) { + $insertValues[$col] = $data['work_id']; + continue; + } + if ($col === 'authority' && !array_key_exists($col, $data) && array_key_exists('authority_id', $data)) { + $insertValues[$col] = $data['authority_id']; + continue; + } if (array_key_exists($col, $data)) { $insertValues[$col] = $data[$col]; } else { @@ -1064,6 +1083,19 @@ protected function upsertAuthorityAndMember($em, $dir) $insertValues[$dcol] = $now; } } + // login_dateなどのNULL許可のタイムスタンプカラムは、空文字列をnullに変換 + foreach (['login_date', 'first_buy_date', 'last_buy_date', 'payment_date'] as $dcol) { + if (isset($insertValues[$dcol]) && (empty($insertValues[$dcol]) || strpos($insertValues[$dcol], '0000') === 0)) { + $insertValues[$dcol] = null; + } + } + // 4.0系には存在しないカラムのデフォルト値を設定 + if (array_key_exists('two_factor_auth_enabled', $insertValues) && $insertValues['two_factor_auth_enabled'] === null) { + $insertValues['two_factor_auth_enabled'] = 0; + } + if (array_key_exists('two_factor_auth_key', $insertValues) && $insertValues['two_factor_auth_key'] === null) { + $insertValues['two_factor_auth_key'] = null; // NULL許可(この行は冗長だが明示的に残す) + } $colsSql = implode(',', array_map(fn($c) => '"' . $c . '"', array_keys($insertValues))); $placeholders = implode(',', array_fill(0, count($insertValues), '?')); $updateSql = implode(', ', array_map(fn($c) => '"' . $c . '" = EXCLUDED."' . $c . '"', $updateCols)); @@ -2153,7 +2185,22 @@ private function fix4x($em, $tmpDir, $csvName) } $platform = $this->dataMigrationService->begin($em); - $this->dataMigrationService->resetTable($em, $tableName); + + // PostgreSQLではUPSERTを使うため、resetTableは不要 + // MySQLは従来通りresetTable()を使用 + if ($platform !== 'postgresql') { + $this->dataMigrationService->resetTable($em, $tableName); + } + + // PostgreSQLの場合、UPSERT用のプライマリキーを取得 + $primaryKeys = []; + if ($platform === 'postgresql') { + $schemaManager = $em->getSchemaManager(); + $table = $schemaManager->introspectTable($tableName); + if ($table->hasPrimaryKey()) { + $primaryKeys = $table->getPrimaryKey()->getColumns(); + } + } $builder = new BulkInsertQuery($em, $tableName); $builder->setColumns($listTableColumns); @@ -2176,30 +2223,80 @@ private function fix4x($em, $tmpDir, $csvName) foreach ($columns as $column) { $columnName = $column->getName(); - if ($column->getNotNull()) { + + // 特定カラムの処理 + if ($columnName == 'two_factor_auth_enabled' && ($tableName == 'dtb_member' || $tableName == 'dtb_customer')) { + // 4.0系には存在しないカラム。デフォルト値として0(無効)を設定 + $value[$columnName] = isset($data[$columnName]) && $data[$columnName] !== '' ? $data[$columnName] : 0; + } elseif ($columnName == 'two_factor_auth_key' && ($tableName == 'dtb_member' || $tableName == 'dtb_customer')) { + // 4.0系には存在しないカラム。NULLを設定 + $value[$columnName] = isset($data[$columnName]) && $data[$columnName] !== '' ? $data[$columnName] : null; + } elseif ($columnName == 'work' && $tableName == 'dtb_member') { + // 4.0系ではwork_idというカラム名 + $value[$columnName] = isset($data['work_id']) && $data['work_id'] !== '' ? $data['work_id'] : null; + } elseif ($columnName == 'authority' && $tableName == 'dtb_member') { + // 4.0系ではauthority_idというカラム名 + $value[$columnName] = isset($data['authority_id']) && $data['authority_id'] !== '' ? $data['authority_id'] : null; + } elseif ($columnName == 'create_date' || $columnName == 'update_date') { + // create_date/update_dateは、空または'0000-00-00 00:00:00'の場合は現在時刻を設定 + $value[$columnName] = (isset($data[$columnName]) && $data[$columnName] !== '' && $data[$columnName] != '0000-00-00 00:00:00') ? $data[$columnName] : date('Y-m-d H:i:s'); + } elseif ($columnName == 'login_date' || $columnName == 'first_buy_date' || $columnName == 'last_buy_date' || $columnName == 'payment_date') { + // タイムスタンプ型カラムで、NULL許可の場合は、空または'0000-00-00 00:00:00'の場合はnullを設定 + $value[$columnName] = (isset($data[$columnName]) && $data[$columnName] !== '' && $data[$columnName] != '0000-00-00 00:00:00') ? $data[$columnName] : null; + } elseif ($columnName == 'sex_id' || $columnName == 'job_id' || $columnName == 'country_id' || $columnName == 'pref_id') { + // 外部キー制約があるカラムは、空の場合nullを設定(0を設定すると外部キー違反になる) + $value[$columnName] = isset($data[$columnName]) && $data[$columnName] !== '' ? $data[$columnName] : null; + } elseif ($columnName == 'discriminator_type') { + // discriminator_typeは、テーブル名から生成 + $search = ['dtb_', 'mtb_', '_']; + $value[$columnName] = str_replace($search, '', $tableName); + } elseif ($column->getNotNull()) { $value[$columnName] = isset($data[$columnName]) && $data[$columnName] !== '' ? $data[$columnName] : 0; } else { $value[$columnName] = isset($data[$columnName]) && $data[$columnName] !== '' ? $data[$columnName] : null; } } - $builder->setValues($value); - - if (($i % $batchSize) === 0) { + if ($platform === 'postgresql' && !empty($primaryKeys)) { + // PostgreSQLはUPSERTで行ごとに処理 try { - $builder->execute(); - $this->addSuccess($tableName, 'admin'); + $cols = array_map(fn($c) => '"' . $c . '"', array_keys($value)); + $placeholders = array_fill(0, count($value), '?'); + $updateCols = array_filter(array_keys($value), fn($c) => !in_array($c, $primaryKeys)); + $updateSet = array_map(fn($c) => '"' . $c . '" = EXCLUDED."' . $c . '"', $updateCols); + $conflictCols = array_map(fn($c) => '"' . $c . '"', $primaryKeys); + + $sql = 'INSERT INTO "' . $tableName . '" (' . implode(', ', $cols) . ') ' . + 'VALUES (' . implode(', ', $placeholders) . ') ' . + 'ON CONFLICT (' . implode(', ', $conflictCols) . ') ' . + 'DO UPDATE SET ' . implode(', ', $updateSet); + + $em->executeStatement($sql, array_values($value)); } catch (\Exception $e) { $this->addDanger($e->getMessage(), 'admin'); $em->rollback(); return; } + } else { + // MySQLはバッチINSERT + $builder->setValues($value); + + if (($i % $batchSize) === 0) { + try { + $builder->execute(); + $this->addSuccess($tableName, 'admin'); + } catch (\Exception $e) { + $this->addDanger($e->getMessage(), 'admin'); + $em->rollback(); + return; + } + } } $i++; } - if (count($builder->getValues()) > 0) { + if ($platform !== 'postgresql' && count($builder->getValues()) > 0) { try { $builder->execute(); $this->addSuccess($tableName, 'admin'); diff --git a/Service/DataMigrationService.php b/Service/DataMigrationService.php index 317f2c1..8493e03 100644 --- a/Service/DataMigrationService.php +++ b/Service/DataMigrationService.php @@ -142,6 +142,10 @@ public function resetTable(Connection $em, $tableName) if ($platform == 'mysql') { $em->exec('DELETE FROM ' . $tableName); + } elseif ($platform == 'postgresql') { + // PostgreSQLでは fix4x() はUPSERTを使うため、このメソッドは呼ばれない + // saveToC() などから呼ばれる場合はDELETEを実行 + $em->exec('DELETE FROM "' . $tableName . '"'); } else { $em->exec('DELETE FROM ' . $tableName); } @@ -271,7 +275,13 @@ public function begin($em, $context = NULL) if ($platform == 'mysql') { $em->exec('SET FOREIGN_KEY_CHECKS = 0;'); $em->exec("SET SESSION sql_mode = 'NO_AUTO_VALUE_ON_ZERO'"); // STRICT_TRANS_TABLESを無効にする。 - } else { + } elseif ($platform == 'postgresql') { + // PostgreSQLでは外部キー制約チェックをトランザクション終了時まで遅延 + // fix4x()ではUPSERTを使うため不要だが、他の処理(saveToC等)のために残す + $em->exec('SET CONSTRAINTS ALL DEFERRED;'); + } + + if ($platform != 'mysql') { try { switch ($context) { case "Customer": diff --git a/Tests/Fixtures/member_test.tar.gz b/Tests/Fixtures/member_test.tar.gz new file mode 100644 index 0000000..b2bbbd3 Binary files /dev/null and b/Tests/Fixtures/member_test.tar.gz differ diff --git a/Tests/Fixtures/member_test/dtb_member.csv b/Tests/Fixtures/member_test/dtb_member.csv deleted file mode 100644 index 6fe0012..0000000 --- a/Tests/Fixtures/member_test/dtb_member.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,department,login_id,password,authority_id,work_id,creator_id,create_date,update_date,discriminator_type -99,テスト管理者,開発部,testadmin,$2y$10$dummyhash000000000000000000000000000000000000000000,0,1,1,2024-01-01 00:00:00,2024-01-01 00:00:00,member -100,テスト店舗オーナー,営業部,testowner,$2y$10$dummyhash000000000000000000000000000000000000000000,1,1,1,2024-01-01 00:00:00,2024-01-01 00:00:00,member diff --git a/Tests/Fixtures/member_test/mtb_authority.csv b/Tests/Fixtures/member_test/mtb_authority.csv deleted file mode 100644 index da0ec86..0000000 --- a/Tests/Fixtures/member_test/mtb_authority.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,sort_no -0,システム管理者,0 -1,店舗オーナー,1 diff --git a/Tests/Web/Admin/ConfigControllerTest.php b/Tests/Web/Admin/ConfigControllerTest.php index d9ac35c..71004a7 100644 --- a/Tests/Web/Admin/ConfigControllerTest.php +++ b/Tests/Web/Admin/ConfigControllerTest.php @@ -28,20 +28,21 @@ public function tearDown(): void public function versionProvider() { return [ - //['2_11_5', 1, 0, 3], - //['2_12_6', 1, 3, 2], - ['2_13_5', 1, 3, 2], - //['3_0_9', 1, 2, 6], // PostgreSQL対応により3.x系も復活 - //['3_0_18', 1, 2, 4], // PostgreSQL対応により3.x系も復活 - //['4_0_6', 1, 12, 20], - //['4_1_2', 1, 12, 20], + ['2_11_5', 1, 0, 3, 0], + ['2_12_6', 1, 3, 2, 0], + ['2_13_5', 1, 3, 2, 0], + ['3_0_9', 1, 2, 6, 0], + ['3_0_18', 1, 2, 4, 0], + ['4_0_6', 1, 12, 20, 0], + ['4_1_2', 1, 12, 20, 0], + ['member_test', 0, 0, 0, 2], // Member import test ]; } /** * @dataProvider versionProvider */ - public function testバックアップファイルをアップロードできるかテスト($v, $c, $p, $o) + public function testバックアップファイルをアップロードできるかテスト($v, $c, $p, $o, $m = 0) { $container = self::getContainer(); $project_dir = $container->getParameter('kernel.project_dir'); @@ -87,6 +88,11 @@ public function testバックアップファイルをアップロードできる $orders = $this->entityManager->getRepository(Order::class)->findAll(); self::assertEquals($o, count($orders)); + if ($m > 0) { + $members = $this->entityManager->getRepository(\Eccube\Entity\Member::class)->findAll(); + self::assertEquals($m, count($members), 'メンバーが正しくインポートされること'); + } + // ECCUBE_AUTH_MAGICの値を取得してアサート //$eccubeConfig = $container->get('Eccube\Common\EccubeConfig'); //$authMagic = $eccubeConfig->get('eccube_auth_magic'); @@ -100,128 +106,4 @@ public function testバックアップファイルをアップロードできる throw $e; } } - - public function testUpsertAuthorityAndMember() - { - $container = self::getContainer(); - $project_dir = $container->getParameter('kernel.project_dir'); - $fixtureDir = $project_dir . '/app/Plugin/DataMigration43/Tests/Fixtures/member_test/'; - - // Controllerのインスタンスを取得 - $controller = $container->get('Plugin\DataMigration43\Controller\Admin\ConfigController'); - - // ReflectionClassを使ってprotectedメソッドにアクセス - $reflection = new \ReflectionClass($controller); - $method = $reflection->getMethod('upsertAuthorityAndMember'); - $method->setAccessible(true); - - // EntityManagerの接続を取得 - $em = $this->entityManager->getConnection(); - - try { - // テスト実行前に既存のメンバーを削除(idが99, 100の場合) - $em->executeStatement('DELETE FROM dtb_member WHERE id IN (99, 100)'); - $em->executeStatement('DELETE FROM mtb_authority WHERE id IN (0, 1)'); - - // メソッドを実行 - $method->invoke($controller, $em, $fixtureDir); - - // 権限マスタが正しくインポートされたか確認 - $authorities = $em->fetchAllAssociative('SELECT * FROM mtb_authority ORDER BY id'); - self::assertCount(2, $authorities, '権限マスタが2件インポートされること'); - self::assertEquals(0, $authorities[0]['id']); - self::assertEquals('システム管理者', $authorities[0]['name']); - self::assertEquals(1, $authorities[1]['id']); - self::assertEquals('店舗オーナー', $authorities[1]['name']); - - // メンバーが正しくインポートされたか確認 - $members = $em->fetchAllAssociative('SELECT * FROM dtb_member WHERE id IN (99, 100) ORDER BY id'); - self::assertCount(2, $members, 'メンバーが2件インポートされること'); - self::assertEquals(99, $members[0]['id']); - self::assertEquals('テスト管理者', $members[0]['name']); - self::assertEquals('testadmin', $members[0]['login_id']); - self::assertEquals(0, $members[0]['authority_id']); - self::assertEquals(1, $members[0]['work_id'], 'work_idが1(稼働中)であること'); - - self::assertEquals(100, $members[1]['id']); - self::assertEquals('テスト店舗オーナー', $members[1]['name']); - self::assertEquals('testowner', $members[1]['login_id']); - self::assertEquals(1, $members[1]['authority_id']); - self::assertEquals(1, $members[1]['work_id'], 'work_idが1(稼働中)であること'); - - } catch (\Exception $e) { - // エラーが発生した場合は、トランザクションをリセットしてから例外を再スローする - if ($this->entityManager->getConnection()->isTransactionActive()) { - $this->entityManager->getConnection()->rollBack(); - $this->entityManager->getConnection()->beginTransaction(); - } - throw $e; - } - } - - public function testUpsertAuthorityAndMemberでログイン可能() - { - $container = self::getContainer(); - $project_dir = $container->getParameter('kernel.project_dir'); - $fixtureDir = $project_dir . '/app/Plugin/DataMigration43/Tests/Fixtures/member_test/'; - - // Controllerのインスタンスを取得 - $controller = $container->get('Plugin\DataMigration43\Controller\Admin\ConfigController'); - - // ReflectionClassを使ってprotectedメソッドにアクセス - $reflection = new \ReflectionClass($controller); - $method = $reflection->getMethod('upsertAuthorityAndMember'); - $method->setAccessible(true); - - // EntityManagerの接続を取得 - $em = $this->entityManager->getConnection(); - - try { - // テスト実行前に既存のメンバーを削除(idが99, 100の場合) - $em->executeStatement('DELETE FROM dtb_member WHERE id IN (99, 100)'); - - // 正しいパスワードハッシュでメンバーデータを更新 - $encoder = $container->get('security.user_password_encoder.generic'); - $memberRepository = $this->entityManager->getRepository(\Eccube\Entity\Member::class); - - // 既存の管理者を取得してパスワードハッシュを参考にする - $existingMember = $memberRepository->find(1); - if ($existingMember) { - // 実際にログイン可能なパスワードハッシュを生成 - $testPassword = 'testpassword123'; - - // フィクスチャファイルを一時的に更新(本番では別の方法が望ましい) - $hashedPassword = password_hash($testPassword, PASSWORD_BCRYPT); - - $csvContent = "id,name,department,login_id,password,authority_id,work_id,creator_id,create_date,update_date,discriminator_type\n"; - $csvContent .= "99,テスト管理者,開発部,testadmin,$hashedPassword,0,1,1,2024-01-01 00:00:00,2024-01-01 00:00:00,member\n"; - - file_put_contents($fixtureDir . 'dtb_member.csv', $csvContent); - } - - // メソッドを実行 - $method->invoke($controller, $em, $fixtureDir); - - // ログアウト - $this->logoutTo(); - - // インポートしたメンバーでログインを試みる - $this->client->request('POST', $this->generateUrl('admin_login'), [ - 'login_id' => 'testadmin', - 'password' => 'testpassword123', - ]); - - // ログイン成功を確認(管理画面にリダイレクトされること) - self::assertTrue($this->client->getResponse()->isRedirect($this->generateUrl('admin_homepage')), - 'インポートしたメンバーでログインできること'); - - } catch (\Exception $e) { - // エラーが発生した場合は、トランザクションをリセットしてから例外を再スローする - if ($this->entityManager->getConnection()->isTransactionActive()) { - $this->entityManager->getConnection()->rollBack(); - $this->entityManager->getConnection()->beginTransaction(); - } - throw $e; - } - } }