chore(2meet-data-optimizer): make the hardening branch pass PHPStan and PHPCS
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 14s
Tests / PHP Lint (pull_request) Successful in 1m0s
Tests / PHPCS (pull_request) Successful in 1m15s
Tests / Unit Tests (pull_request) Successful in 57s
Tests / Integration Tests (pull_request) Successful in 1m14s
Tests / PHPStan (pull_request) Successful in 34s

- CI PHPStan (level 6, baseline for pre-existing debt) failed on 35 errors in
  code added on hardening/2.0: missing iterable value types (PHPDoc only), a
  redundant method_exists() in uninstall.php, and the new WP-CLI commands
  registered as array('Class', 'instanceMethod') — now array(new Class(), ...)
- Local runs had been masked by a PHPStan tmpDir shared with the act CI runner
  (cached container pointing at a removed phar); use a private tmpDir
- Verify: phpstan (private tmpDir) no errors, phpcs clean on changed files,
  phpunit Unit 658 OK; dev22 (running this branch) CLI post-status-sync /
  bridge-restore-native dry runs OK
This commit is contained in:
2026-09-26 07:45:05 +08:00
parent 08492dd795
commit b917e45c8c
9 changed files with 46 additions and 36 deletions
+7 -7
View File
@@ -282,8 +282,8 @@ class TMDO_CLI_Post {
* wp tmdo post-reconcile-groups --dry-run
* wp tmdo post-reconcile-groups
*
* @param array $args Positional arguments (unused).
* @param array $assoc_args Named arguments.
* @param array<int, string> $args Positional arguments (unused).
* @param array<string, string> $assoc_args Named arguments.
*/
public function post_reconcile_groups( $args, $assoc_args ): void {
$dry_run = isset( $assoc_args['dry-run'] );
@@ -327,8 +327,8 @@ class TMDO_CLI_Post {
* wp tmdo post-status-sync --dry-run
* wp tmdo post-status-sync
*
* @param array $args Positional arguments (unused).
* @param array $assoc_args Named arguments.
* @param array<int, string> $args Positional arguments (unused).
* @param array<string, string> $assoc_args Named arguments.
*/
public function post_status_sync( $args, $assoc_args ): void {
$dry_run = isset( $assoc_args['dry-run'] );
@@ -925,6 +925,6 @@ WP_CLI::add_command( 'wpdo post-benchmark', array( 'TMDO_CLI_Post', 'post_benchm
WP_CLI::add_command( 'tmdo post-benchmark', array( 'TMDO_CLI_Post', 'post_benchmark' ) );
WP_CLI::add_command( 'wpdo post-shadow-report', array( 'TMDO_CLI_Post', 'post_shadow_report' ) );
WP_CLI::add_command( 'tmdo post-shadow-report', array( 'TMDO_CLI_Post', 'post_shadow_report' ) );
WP_CLI::add_command( 'wpdo post-reconcile-groups', array( 'TMDO_CLI_Post', 'post_reconcile_groups' ) );
WP_CLI::add_command( 'tmdo post-reconcile-groups', array( 'TMDO_CLI_Post', 'post_reconcile_groups' ) );
WP_CLI::add_command( 'tmdo post-status-sync', array( 'TMDO_CLI_Post', 'post_status_sync' ) );
WP_CLI::add_command( 'wpdo post-reconcile-groups', array( new TMDO_CLI_Post(), 'post_reconcile_groups' ) );
WP_CLI::add_command( 'tmdo post-reconcile-groups', array( new TMDO_CLI_Post(), 'post_reconcile_groups' ) );
WP_CLI::add_command( 'tmdo post-status-sync', array( new TMDO_CLI_Post(), 'post_status_sync' ) );
+5 -2
View File
@@ -183,6 +183,9 @@ class TMDO_CLI_V2 {
* wp tmdo bridge-restore-native user --dry-run
* wp tmdo bridge-restore-native user --demote
* wp tmdo bridge-restore-native user --group=sso --retire
*
* @param array<int, string> $args Positional arguments.
* @param array<string, string> $assoc_args Named arguments.
*/
public function bridge_restore_native( $args, $assoc_args ): void {
$entity = (string) ( $args[0] ?? '' );
@@ -846,8 +849,8 @@ WP_CLI::add_command( 'wpdo bridge-status', array( 'TMDO_CLI_V2', 'bridge_status'
WP_CLI::add_command( 'tmdo bridge-status', array( 'TMDO_CLI_V2', 'bridge_status' ) );
WP_CLI::add_command( 'wpdo bridge-set', array( 'TMDO_CLI_V2', 'bridge_set' ) );
WP_CLI::add_command( 'tmdo bridge-set', array( 'TMDO_CLI_V2', 'bridge_set' ) );
WP_CLI::add_command( 'wpdo bridge-restore-native', array( 'TMDO_CLI_V2', 'bridge_restore_native' ) );
WP_CLI::add_command( 'tmdo bridge-restore-native', array( 'TMDO_CLI_V2', 'bridge_restore_native' ) );
WP_CLI::add_command( 'wpdo bridge-restore-native', array( new TMDO_CLI_V2(), 'bridge_restore_native' ) );
WP_CLI::add_command( 'tmdo bridge-restore-native', array( new TMDO_CLI_V2(), 'bridge_restore_native' ) );
WP_CLI::add_command( 'wpdo mode-audit', array( 'TMDO_CLI_V2', 'mode_audit' ) );
WP_CLI::add_command( 'tmdo mode-audit', array( 'TMDO_CLI_V2', 'mode_audit' ) );
WP_CLI::add_command( 'wpdo mode-set', array( 'TMDO_CLI_V2', 'mode_set' ) );
@@ -442,7 +442,7 @@ class TMDO_Site_Health {
* Not cached: the collision list is in-memory state rebuilt on every
* request, so reading it is free and a transient would only go stale.
*
* @return array Site Health result.
* @return array<string, mixed> Site Health result.
*/
public static function check_entity_group_collisions(): array {
$collisions = class_exists( 'TMDO_Entity_Registry' ) ? TMDO_Entity_Registry::get_collisions() : array();
@@ -482,7 +482,7 @@ class TMDO_Site_Health {
*
* Not cached for the same reason as check_entity_group_collisions().
*
* @return array Site Health result.
* @return array<string, mixed> Site Health result.
*/
public static function check_orphaned_entity_groups(): array {
$restored = class_exists( 'TMDO_Entity_Registry' ) ? TMDO_Entity_Registry::get_restored_groups() : array();
@@ -521,7 +521,7 @@ class TMDO_Site_Health {
*
* Not cached: two class_exists() probes.
*
* @return array Site Health result.
* @return array<string, mixed> Site Health result.
*/
public static function check_missing_integration_addons(): array {
$missing = class_exists( 'TMDO_Compatibility' ) ? TMDO_Compatibility::missing_integration_addons() : array();
+11 -5
View File
@@ -31,10 +31,10 @@ final class TMDO_Entity_Registry {
/** @var array<int, array{type:string, group:string, fields:array}> 待建表的 Schema 清單 */
private static array $pending_schemas = array();
/** @var array<string, array<string, array>> 實體類型 → meta_key → 欄位定義(僅未限定 post_type 的群組)*/
/** @var array<string, array<string, array<string, mixed>>> 實體類型 → meta_key → 欄位定義(僅未限定 post_type 的群組)*/
private static array $unscoped_index = array();
/** @var array<string, array<string, array<string, array>>> 實體類型 → post_type → meta_key → 欄位定義 */
/** @var array<string, array<string, array<string, array<string, mixed>>>> 實體類型 → post_type → meta_key → 欄位定義 */
private static array $scoped_index = array();
/** @var array<string, array<string, string[]>> 實體類型 → 群組名 → 限定的 post_type */
@@ -43,10 +43,10 @@ final class TMDO_Entity_Registry {
/** @var array<string, array<string, array<string, string[]>>> 實體類型 → 群組名 → 索引名 → 欄位 key */
private static array $indexes = array();
/** @var array<string, array> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
/** @var array<string, array<string, mixed>> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
private static array $collisions = array();
/** @var array<string, array> "entity/group" → 從 wpdo_registry_meta 還原的孤兒群組 */
/** @var array<string, array<string, mixed>> "entity/group" → 從 wpdo_registry_meta 還原的孤兒群組 */
private static array $restored = array();
/** 合法的欄位型別 */
@@ -119,7 +119,7 @@ final class TMDO_Entity_Registry {
* - unique (bool) 唯一索引
* - options (array) enum 選項
* - label (string) 顯示名稱
* @param array $args 選填:post_types (string[]) — 群組只套用於這些 post_type。
* @param array<string, mixed> $args 選填:post_types (string[]) — 群組只套用於這些 post_type。
* 多個群組共用 meta_key 時,依 post 本身的類型決定歸屬;
* 不屬於任何宣告 post_type 的 post 不受管理(走原生 meta)。
* 不列入欄位定義,所以不影響 schema hash 與衝突比對。
@@ -183,6 +183,8 @@ final class TMDO_Entity_Registry {
/**
* 驗證並正規化欄位定義(略過缺 key/type 或型別不合法者)。
* @param array<int, array<string, mixed>> $fields
* @return array<int, array<string, mixed>>
*/
private static function normalize_fields( string $entity_type, string $group_name, array $fields ): array {
$normalized = array();
@@ -229,6 +231,7 @@ final class TMDO_Entity_Registry {
*
* @param string[] $native_keys 原生 meta 仍有資料、不可還原的 key。
* @return bool 有登錄任何欄位時為 true
* @param array<int, array<string, mixed>> $fields
*/
public static function restore_group( string $entity_type, string $group_name, array $fields, array $native_keys = array() ): bool {
if ( ! isset( self::$adapters[ $entity_type ] ) || isset( self::$groups[ $entity_type ][ $group_name ] ) ) {
@@ -286,6 +289,7 @@ final class TMDO_Entity_Registry {
* 供不帶 post_type 的查詢使用;另外依是否限定 post_type 分別建索引。
*
* @param string[] $post_types 群組限定的 post_type;空陣列表示不限定。
* @param array<string, mixed> $field
*/
private static function index_field( string $entity_type, array $field, array $post_types ): void {
self::$field_index[ $entity_type ][ $field['key'] ] = $field;
@@ -312,6 +316,8 @@ final class TMDO_Entity_Registry {
* 記錄同名群組衝突(每個 entity/group 每請求一筆),並寫入 warning。
*
* 註冊每個請求都會跑,warning 以 transient 節流為每天一次,避免灌爆 wpdo_errors。
* @param array<int, array<string, mixed>> $existing
* @param array<int, array<string, mixed>> $incoming
*/
private static function record_collision( string $entity_type, string $group_name, array $existing, array $incoming ): void {
$id = $entity_type . '/' . $group_name;
+1
View File
@@ -648,6 +648,7 @@ final class TMDO_Hook_Bus {
* 欄位定義查詢;post meta 依該 post 本身的 post_type 解析(見 TMDO_Entity_Registry::get_field())。
*
* 先做不帶 post_type 的查詢:大多數 key 不受管理,可省下 get_post_type()。
* @return array<string, mixed>|null
*/
private static function field_for( string $type, int $object_id, string $meta_key ): ?array {
$field_def = TMDO_Entity_Registry::get_field( $type, $meta_key );
@@ -222,7 +222,7 @@ final class TMDO_Native_Restorer {
* @param string $table Flat table.
* @param string $id_col Entity id column.
* @param string $where WHERE clause with placeholders.
* @param array $params Placeholder values.
* @param array<int, mixed> $params Placeholder values.
* @return array<int, array<string, mixed>>
*/
private static function fetch( string $entity_type, string $table, string $id_col, string $where, array $params ): array {
@@ -240,7 +240,7 @@ final class TMDO_Native_Restorer {
* @param string $entity_type Entity type.
* @param string $group Group the rows come from.
* @param TMDO_Entity_Adapter_Interface $adapter Entity adapter.
* @param array $rows Flat rows.
* @param array<int, array<string, mixed>> $rows Flat rows.
* @return int Native values written.
*/
private static function write_rows( string $entity_type, string $group, TMDO_Entity_Adapter_Interface $adapter, array $rows ): int {
@@ -72,7 +72,7 @@ final class TMDO_Post_Group_Reconciler {
*
* @param bool $dry_run Count only; write nothing.
* @param int $batch Rows per batch.
* @return array{moves: array<int, array>, enabled: bool}
* @return array{moves: array<int, array<string, mixed>>, enabled: bool}
*/
public static function run( bool $dry_run = false, int $batch = 500 ): array {
global $wpdb;
@@ -165,9 +165,9 @@ final class TMDO_Post_Group_Reconciler {
/**
* FROM/WHERE for non-NULL legacy values of the move's post types.
*
* @param array $move One entry of plan().
* @param array<string, mixed> $move One entry of plan().
* @param string $id_col Entity id column of the flat tables.
* @return array{0:string, 1:array}
* @return array{0:string, 1:array<int, mixed>}
*/
private static function source_clause( array $move, string $id_col ): array {
global $wpdb;
@@ -184,9 +184,9 @@ final class TMDO_Post_Group_Reconciler {
/**
* Upsert one column of the target group rows and drop their cached copies.
*
* @param array $move One entry of plan().
* @param array<string, mixed> $move One entry of plan().
* @param string $id_col Entity id column of the flat tables.
* @param array $rows Source rows: list of { id, v }.
* @param array<int, array<string, mixed>> $rows Source rows: list of { id, v }.
* @return int Rows written.
*/
private static function copy_rows( array $move, string $id_col, array $rows ): int {
@@ -354,7 +354,7 @@ class TMDO_Post_Query_Router {
* The status / date copy route for a routed query, or [] when it does not apply.
*
* @param \WP_Query $query Current WP_Query.
* @param array<string, array<int, array>> $routed Routed clauses keyed by post type.
* @param array<string, array<int, array<string, mixed>>> $routed Routed clauses keyed by post type.
* @return array{post_type:string, group:string, statuses:string[], order:string}|array{}
*/
private static function copy_route( \WP_Query $query, array $routed ): array {
+1 -1
View File
@@ -77,7 +77,7 @@ $wpdo_for_each_site = static function ( callable $callback ) use ( $wpdo_site_id
};
// ── Refuse while any site holds data that exists only in wpdo_* tables ───
if ( ! $bootstrap_ok || ! method_exists( 'TMDO_Installer', 'uninstall_blockers' ) ) {
if ( ! $bootstrap_ok ) {
wp_die( esc_html( '2meet Data Optimizer:無法載入資料安全檢查,為避免資料遺失,已停止解除安裝。' ) );
}