c46814ce83
A v3.0.1 把 orchestrator 的 11 個 phase 抽成可注入的 Phase 物件,B 仍是 1107 行單體、以 'phase_' . $current 字串魔法分派、completed 甚至 inline 在 tick() 裡。本 commit 對齊: 新增 13 檔 - includes/migration/interface-migration-phase.php - includes/migration/class-tmdo-migration-phase-base.php(log/get_managed_keys/ execute_bulk_pivot/values_loose_equal 等共用 helper) - includes/migration/phases/ 11 個 phase 類別 orchestrator 1107 → 640 行 - tick() 改 make_phase() 工廠 + $phase->execute($job) - 移除 final、self::ENTITY_TYPE → static::(A v3.3.0 late static binding) - 保留 B 原有的 '✓ %s (%.2fs)' 耗時 log(改為 tick 自行量測,A 版已簡化掉) - 公開介面(preflight/start/tick/get_status/cancel/resume/needs_attention/ cron_tick)經 diff 確認與 A 完全一致,呼叫端零影響 連帶 - Schema_Manager 補 table_exists 的 request-scoped cache 與 flush_table_exists_cache()(A v3.1.6 + v3.4.6),Phase 測試需要它 - back-compat 補 12 個 Phase 類別的 WPDO_ alias - 移植 MigrationPhaseTest + MigrationPhaseRemainingTest(527 行) 測試隔離差異(B 的 unit bootstrap 會載入 Member_Fields / Post_Fields, A 的不會):MigrationPhaseRemainingTest 的 setUp 需額外清空 Entity_Registry 與 wpdo_register_entity_fields listener,否則 install_schema 會真的走進 dbDelta。MigrationPhaseTest 的 interface 斷言改用 TMDO_ 正式名稱 (PHP 無法 class_alias 介面,且該契約是核心內部擴充點)。 unit 451 / integration 398 GREEN Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
167 lines
5.6 KiB
PHP
167 lines
5.6 KiB
PHP
<?php
|
|
/**
|
|
* Shared helpers for all migration phase Strategy objects.
|
|
*
|
|
* @package TMDO
|
|
* @since 3.0.1
|
|
*/
|
|
|
|
// phpcs:disable Squiz.Commenting.FunctionComment,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.PHP.YodaConditions,WordPress.Security.EscapeOutput.OutputNotEscaped -- Internal helpers; SQL composed from registry-validated identifiers; exception messages not echoed.
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Shared helpers for all migration phase Strategy objects.
|
|
*
|
|
* Concrete phases extend this class, receive the entity type at construction,
|
|
* and implement execute(). All database helpers delegate to
|
|
* TMDO_Entity_Registry / TMDO_Schema_Manager so the entity type is the only
|
|
* injection point needed for unit tests.
|
|
*/
|
|
abstract class TMDO_Migration_Phase_Base implements TMDO_Migration_Phase_Interface {
|
|
|
|
const MAX_LOG_LINES = 50;
|
|
|
|
/**
|
|
* Entity type this phase operates on (e.g. 'user', 'post').
|
|
*
|
|
* @var string
|
|
*/
|
|
protected string $entity_type;
|
|
|
|
public function __construct( string $entity_type ) {
|
|
$this->entity_type = $entity_type;
|
|
}
|
|
|
|
// ── Logging ───────────────────────────────────────────────────────────────
|
|
|
|
protected function log( array &$job, string $message ): void {
|
|
$line = '[' . gmdate( 'H:i:s' ) . '] ' . $message;
|
|
$job['log'][] = $line;
|
|
if ( count( $job['log'] ) > self::MAX_LOG_LINES ) {
|
|
$job['log'] = array_slice( $job['log'], -self::MAX_LOG_LINES );
|
|
}
|
|
$job['updated_at'] = time();
|
|
}
|
|
|
|
// ── Registry helpers ──────────────────────────────────────────────────────
|
|
|
|
protected function get_managed_keys(): array {
|
|
$keys = array();
|
|
foreach ( TMDO_Entity_Registry::get_groups_for_type( $this->entity_type ) as $group ) {
|
|
$keys = array_merge( $keys, TMDO_Entity_Registry::get_group_keys( $this->entity_type, $group ) );
|
|
}
|
|
return array_values( array_unique( $keys ) );
|
|
}
|
|
|
|
protected function group_has_json_field( string $group ): bool {
|
|
foreach ( TMDO_Entity_Registry::get_group_fields( $this->entity_type, $group ) as $field ) {
|
|
if ( 'json' === ( $field['type'] ?? '' ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── DB helpers ────────────────────────────────────────────────────────────
|
|
|
|
/** Count EAV rows for the given meta keys (or all managed keys if empty). */
|
|
protected function count_eav_residue( array $keys = array() ): int {
|
|
global $wpdb;
|
|
if ( empty( $keys ) ) {
|
|
$keys = $this->get_managed_keys();
|
|
}
|
|
if ( empty( $keys ) ) {
|
|
return 0;
|
|
}
|
|
$adapter = TMDO_Entity_Registry::get_adapter( $this->entity_type );
|
|
$meta_table = $adapter->get_native_meta_table();
|
|
$placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) );
|
|
return (int) $wpdb->get_var(
|
|
$wpdb->prepare( "SELECT COUNT(*) FROM `{$meta_table}` WHERE meta_key IN ({$placeholders})", ...$keys )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Bulk pivot for a text-only group via INSERT … SELECT … GROUP BY …
|
|
* ON DUPLICATE KEY UPDATE.
|
|
*
|
|
* @param string $group Entity group to pivot.
|
|
* @return int Number of rows affected.
|
|
* @throws \RuntimeException On DB failure.
|
|
*/
|
|
protected function execute_bulk_pivot( string $group ): int {
|
|
global $wpdb;
|
|
|
|
$fields = TMDO_Entity_Registry::get_group_fields( $this->entity_type, $group );
|
|
if ( empty( $fields ) ) {
|
|
return 0;
|
|
}
|
|
|
|
$adapter = TMDO_Entity_Registry::get_adapter( $this->entity_type );
|
|
$id_col = $adapter->get_entity_id_column();
|
|
$meta_table = $adapter->get_native_meta_table();
|
|
$table = TMDO_Schema_Manager::get_table_name( $this->entity_type, $group );
|
|
|
|
$select_cases = array();
|
|
$update_cols = array();
|
|
$col_names = array();
|
|
$keys = array();
|
|
|
|
foreach ( $fields as $f ) {
|
|
$key = $f['key'];
|
|
$col = TMDO_Schema_Manager::sanitize_column_name( $key );
|
|
$keys[] = $key;
|
|
$col_names[] = "`{$col}`";
|
|
$select_cases[] = $wpdb->prepare(
|
|
"MAX(CASE WHEN um.meta_key = %s THEN um.meta_value END) AS `{$col}`",
|
|
$key
|
|
);
|
|
$update_cols[] = "`{$col}` = COALESCE(VALUES(`{$col}`), `{$col}`)";
|
|
}
|
|
|
|
$placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) );
|
|
|
|
$sql = sprintf(
|
|
'INSERT INTO `%s` (`%s`, %s)
|
|
SELECT um.user_id, %s
|
|
FROM `%s` um
|
|
WHERE um.meta_key IN (%s)
|
|
GROUP BY um.user_id
|
|
ON DUPLICATE KEY UPDATE %s',
|
|
$table,
|
|
$id_col,
|
|
implode( ', ', $col_names ),
|
|
implode( ', ', $select_cases ),
|
|
$meta_table,
|
|
$placeholders,
|
|
implode( ', ', $update_cols )
|
|
);
|
|
|
|
$result = $wpdb->query( $wpdb->prepare( $sql, ...$keys ) );
|
|
|
|
if ( false === $result ) {
|
|
// phpcs:ignore WordPress.Security.EscapeOutput -- Exception message not echoed to browser.
|
|
throw new \RuntimeException( 'Bulk pivot failed for group ' . $group . ': ' . $wpdb->last_error );
|
|
}
|
|
return (int) $result;
|
|
}
|
|
|
|
/** Loose equality check for EAV vs flat value comparison (null/empty/scalar). */
|
|
protected function values_loose_equal( $a, $b ): bool {
|
|
if ( null === $a && null === $b ) {
|
|
return true;
|
|
}
|
|
if ( null === $a || null === $b ) {
|
|
$other = null === $a ? $b : $a;
|
|
return '' === $other || array() === $other || 0 === $other || '0' === $other;
|
|
}
|
|
if ( is_array( $a ) || is_array( $b ) ) {
|
|
return wp_json_encode( $a ) === wp_json_encode( $b );
|
|
}
|
|
return (string) $a === (string) $b;
|
|
}
|
|
}
|