Files
2meet-data-optimizer/includes/migration/class-tmdo-migration-phase-base.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 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
2026-07-31 06:13:33 +08:00

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;
}
}