76c01e44df
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入 並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
169 lines
5.6 KiB
PHP
169 lines
5.6 KiB
PHP
<?php
|
|
/**
|
|
* Shared helpers for all migration phase Strategy objects.
|
|
*
|
|
* @package TMDO
|
|
* @since 3.0.1
|
|
*/
|
|
|
|
declare(strict_types=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;
|
|
}
|
|
}
|