Compare commits
20 Commits
d6c1540444
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c5027b0fce | |||
| e33ae4e626 | |||
| d52d604d7a | |||
| 6751c69bc2 | |||
| b63ab46f54 | |||
| 2203bc471c | |||
| fa356a63b1 | |||
| fbe41e2130 | |||
| bc4fad3b86 | |||
| c712bf6e0a | |||
| 5e11c882aa | |||
| c300d56aef | |||
| e246c9539e | |||
| 77b4501682 | |||
| cd03d66151 | |||
| d8e7a7190a | |||
| ae99820bbc | |||
| 5385d72346 | |||
| f524ea3f16 | |||
| 76c01e44df |
@@ -0,0 +1,61 @@
|
|||||||
|
name: Anti-EAV Lint + Quality Gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ gitea.workflow }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
anti-eav-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Composer install (no-dev)
|
||||||
|
run: |
|
||||||
|
if [ -f composer.json ]; then
|
||||||
|
composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax lint
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
FAILED=$(find . -name '*.php' \
|
||||||
|
-not -path './vendor/*' \
|
||||||
|
-not -path './node_modules/*' \
|
||||||
|
-not -path './e2e/*' \
|
||||||
|
-not -path './tests/*' \
|
||||||
|
-exec php -l {} \; 2>&1 | grep -v 'No syntax errors' || true)
|
||||||
|
if [ -n "$FAILED" ]; then
|
||||||
|
echo "PHP syntax errors detected:"
|
||||||
|
echo "$FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Anti-EAV strict lint
|
||||||
|
run: |
|
||||||
|
if ! command -v wp >/dev/null 2>&1; then
|
||||||
|
echo "wp-cli not available; skipping anti-eav lint"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# dev30 hosts the plugin family this repo belongs to.
|
||||||
|
TMDO_HOST="/var/www/Studio/wp-local-dev30"
|
||||||
|
if [ -d "$TMDO_HOST" ]; then
|
||||||
|
wp --path="$TMDO_HOST" tmdo lint --plugin="${GITHUB_WORKSPACE}" --strict
|
||||||
|
else
|
||||||
|
echo "TMDO_HOST not found; skipping anti-eav lint"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NOTE: no packaging audit here on purpose.
|
||||||
|
# The shared package-plugin.sh resolves the plugin by SLUG to a fixed path
|
||||||
|
# under wp-content/plugins — it does NOT package the CI checkout. Running it
|
||||||
|
# from a job therefore rebuilds (and `composer install --no-dev`s) the live
|
||||||
|
# working copy, wiping the developer's vendor/bin. Packaging belongs to the
|
||||||
|
# release workflow / a manual run, not to every push.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-gate:
|
||||||
|
name: Test Gate (lint + phpcs + phpstan + unit + integration)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Host-mode runner: no service containers. Uses the host MariaDB, same as
|
||||||
|
# the Tests workflow.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: PHP Lint
|
||||||
|
run: |
|
||||||
|
find . -name "*.php" \
|
||||||
|
! -path "./vendor/*" \
|
||||||
|
! -path "./tests/*" \
|
||||||
|
-print0 | xargs -0 -n1 php -l
|
||||||
|
echo "PHP syntax OK"
|
||||||
|
|
||||||
|
- name: PHPCS
|
||||||
|
run: vendor/bin/phpcs --standard=phpcs.xml --report=checkstyle -q .
|
||||||
|
|
||||||
|
- name: PHPStan
|
||||||
|
run: vendor/bin/phpstan analyse --no-progress --error-format=github
|
||||||
|
|
||||||
|
- name: Unit Tests
|
||||||
|
run: php vendor/bin/phpunit --configuration phpunit.xml --testdox
|
||||||
|
|
||||||
|
- name: Integration Tests
|
||||||
|
env:
|
||||||
|
TMDO_TEST_DB_HOST: 127.0.0.1
|
||||||
|
TMDO_TEST_DB_USER: dbo
|
||||||
|
TMDO_TEST_DB_PASS: ${{ secrets.TMDO_TEST_DB_PASS }}
|
||||||
|
TMDO_TEST_DB_NAME: wp_wpdo_test
|
||||||
|
run: php vendor/bin/phpunit --configuration phpunit-integration.xml --testdox
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [test-gate]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build ZIP
|
||||||
|
run: bash scripts/ci-package.sh
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.RELEASE_GITEA_URL }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ gitea.ref_name }}"
|
||||||
|
ZIP=$(ls dist/*.zip | head -1)
|
||||||
|
ZIP_NAME=$(basename "$ZIP")
|
||||||
|
MD5=$(md5sum "$ZIP" | awk '{print $1}')
|
||||||
|
|
||||||
|
# Create the release
|
||||||
|
RELEASE_ID=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/releases" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"${VERSION}\",
|
||||||
|
\"name\": \"${VERSION}\",
|
||||||
|
\"body\": \"MD5: \`${MD5}\`\",
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": false
|
||||||
|
}" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||||
|
|
||||||
|
# Upload ZIP asset
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/zip" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||||||
|
--data-binary "@${ZIP}"
|
||||||
|
|
||||||
|
echo "Released ${VERSION} — asset: ${ZIP_NAME} (MD5: ${MD5})"
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit:
|
||||||
|
name: Unit Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Host-mode gitea runner (label `ubuntu-latest:host`) uses the host PHP
|
||||||
|
# toolchain (currently 8.3). `shivammathur/setup-php` cannot provision
|
||||||
|
# alternate PHP versions in host mode, so there is no 8.1/8.2/8.3 matrix.
|
||||||
|
# (Restore one if the runner ever moves to container mode.)
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: php vendor/bin/phpunit --configuration phpunit.xml --testdox
|
||||||
|
|
||||||
|
integration:
|
||||||
|
name: Integration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Host-mode runner: service containers are NOT started, so connect to the
|
||||||
|
# host MariaDB at 127.0.0.1:3306 directly. The password comes from the repo
|
||||||
|
# secret TMDO_TEST_DB_PASS (test DB `wp_wpdo_test` and user `dbo` already
|
||||||
|
# exist on the host). The bootstrap also accepts the legacy WPDO_TEST_DB_*
|
||||||
|
# names, so an existing secret of either name works.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
# Preflight: a DB outage or a rotated secret would otherwise surface as an
|
||||||
|
# opaque PHPUnit bootstrap error. Fail fast with an INFRA-vs-test
|
||||||
|
# distinction so a red integration job is diagnosable.
|
||||||
|
- name: DB preflight (host MariaDB reachable?)
|
||||||
|
env:
|
||||||
|
TMDO_TEST_DB_HOST: 127.0.0.1
|
||||||
|
TMDO_TEST_DB_USER: dbo
|
||||||
|
TMDO_TEST_DB_PASS: ${{ secrets.TMDO_TEST_DB_PASS }}
|
||||||
|
TMDO_TEST_DB_NAME: wp_wpdo_test
|
||||||
|
run: |
|
||||||
|
php -r '
|
||||||
|
mysqli_report(MYSQLI_REPORT_OFF);
|
||||||
|
$c = @mysqli_connect(getenv("TMDO_TEST_DB_HOST"), getenv("TMDO_TEST_DB_USER"), getenv("TMDO_TEST_DB_PASS"), getenv("TMDO_TEST_DB_NAME"), 3306);
|
||||||
|
if (!$c) {
|
||||||
|
fwrite(STDERR, "::error::Integration DB unreachable at " . getenv("TMDO_TEST_DB_HOST") . ":3306 db=" . getenv("TMDO_TEST_DB_NAME") . " user=" . getenv("TMDO_TEST_DB_USER") . " — host MariaDB down or TMDO_TEST_DB_PASS secret stale/rotated. This is an INFRA failure, not a test failure: " . mysqli_connect_error() . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
echo "DB preflight OK: connected to " . getenv("TMDO_TEST_DB_NAME") . " on " . getenv("TMDO_TEST_DB_HOST") . "\n";
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
env:
|
||||||
|
TMDO_TEST_DB_HOST: 127.0.0.1
|
||||||
|
TMDO_TEST_DB_USER: dbo
|
||||||
|
TMDO_TEST_DB_PASS: ${{ secrets.TMDO_TEST_DB_PASS }}
|
||||||
|
TMDO_TEST_DB_NAME: wp_wpdo_test
|
||||||
|
run: php vendor/bin/phpunit --configuration phpunit-integration.xml --testdox
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: PHP Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check PHP syntax
|
||||||
|
run: |
|
||||||
|
find . -name "*.php" \
|
||||||
|
! -path "./vendor/*" \
|
||||||
|
! -path "./tests/*" \
|
||||||
|
-print0 | xargs -0 -n1 php -l
|
||||||
|
echo "PHP syntax OK"
|
||||||
|
|
||||||
|
phpcs:
|
||||||
|
name: PHPCS
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Run PHPCS
|
||||||
|
run: vendor/bin/phpcs --standard=phpcs.xml --report=checkstyle -q .
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
name: PHPStan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# A never ran static analysis in CI; the baseline is committed, so new code
|
||||||
|
# is checked at level 6 while existing debt stays silent.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Run PHPStan (level 6, baselined)
|
||||||
|
run: vendor/bin/phpstan analyse --no-progress --error-format=github
|
||||||
@@ -14,3 +14,4 @@ Thumbs.db
|
|||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
.claude/
|
.claude/
|
||||||
dist/
|
dist/
|
||||||
|
.phpcs-cache
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PHPStan bootstrap stubs — plugin constants defined at runtime.
|
||||||
|
*
|
||||||
|
* PHPStan cannot see define()'d constants, so it reports "undefined constant"
|
||||||
|
* for every one of them. Stubbing them here keeps level 6 signal-to-noise high.
|
||||||
|
*
|
||||||
|
* TMDO_IS_SQLITE / TMDO_IS_MYSQL (and their WPDO_ aliases) are intentionally
|
||||||
|
* NOT stubbed to avoid literal-narrowing "always true/false" false positives
|
||||||
|
* on runtime-dynamic values.
|
||||||
|
*
|
||||||
|
* @package TMDO
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define( 'TMDO_VERSION', '0.0.0' );
|
||||||
|
define( 'TMDO_DB_VERSION', '0.0.0' );
|
||||||
|
define( 'TMDO_PATH', '/' );
|
||||||
|
define( 'TMDO_URL', 'https://example.test/wp-content/plugins/2meet-data-optimizer/' );
|
||||||
|
define( 'TMDO_FILE', '/2meet-data-optimizer.php' );
|
||||||
|
define( 'TMDO_TABLE_PREFIX', 'wpdo_' );
|
||||||
|
define( 'TMDO_CACHE_GROUP', 'wpdo' );
|
||||||
|
define( 'TMDO_MIN_PHP', '8.1' );
|
||||||
|
define( 'TMDO_MIN_WP', '6.0' );
|
||||||
|
|
||||||
|
// Back-compat aliases exposed by class-tmdo-back-compat.php.
|
||||||
|
define( 'WPDO_VERSION', '0.0.0' );
|
||||||
|
define( 'WPDO_DB_VERSION', '0.0.0' );
|
||||||
|
define( 'WPDO_PLUGIN_DIR', '/' );
|
||||||
|
define( 'WPDO_PLUGIN_URL', 'https://example.test/wp-content/plugins/2meet-data-optimizer/' );
|
||||||
|
define( 'WPDO_PLUGIN_FILE', '/2meet-data-optimizer.php' );
|
||||||
|
define( 'WPDO_TABLE_PREFIX', 'wpdo_' );
|
||||||
|
define( 'WPDO_CACHE_GROUP', 'wpdo' );
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: 2meet Data Optimizer
|
* Plugin Name: 2meet Data Optimizer
|
||||||
* Plugin URI: https://2meet.io/2meet-data-optimizer
|
* Plugin URI: https://2meet.io/2meet-data-optimizer
|
||||||
* Description: 通用 WordPress 反 EAV 引擎:將 postmeta / usermeta / termmeta / commentmeta 自動分流至四象限扁平表(Hot / Warm / Cold / Archive),大幅提升搜尋與篩選效能。從 wp-data-optimizer v2.16.0 提煉的純核心,整合層交給 11 個 AddOn。
|
* Description: 通用 WordPress 反 EAV 引擎:將 postmeta / usermeta / termmeta / commentmeta 自動分流至四象限扁平表(Hot / Warm / Cold / Archive),大幅提升搜尋與篩選效能。從 wp-data-optimizer v2.16.0 提煉的純核心,整合層交給 11 個 AddOn。
|
||||||
* Version: 0.1.0
|
* Version: 1.0.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Tested up to: 6.9.4
|
* Tested up to: 6.9.4
|
||||||
* Requires PHP: 8.1
|
* Requires PHP: 8.1
|
||||||
@@ -19,12 +19,14 @@
|
|||||||
* @package TMDO
|
* @package TMDO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ──────────────────────────────────────────────────────────────
|
// ── Constants ──────────────────────────────────────────────────────────────
|
||||||
define( 'TMDO_VERSION', '0.1.0' );
|
define( 'TMDO_VERSION', '1.0.0' );
|
||||||
define( 'TMDO_DB_VERSION', '2.1.0' );
|
define( 'TMDO_DB_VERSION', '2.1.0' );
|
||||||
define( 'TMDO_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'TMDO_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'TMDO_URL', plugin_dir_url( __FILE__ ) );
|
define( 'TMDO_URL', plugin_dir_url( __FILE__ ) );
|
||||||
@@ -50,12 +52,12 @@ define(
|
|||||||
define( 'TMDO_IS_MYSQL', ! TMDO_IS_SQLITE );
|
define( 'TMDO_IS_MYSQL', ! TMDO_IS_SQLITE );
|
||||||
|
|
||||||
// ── 跨外掛偵測訊號(dual-fire 對外保留 wp-data-optimizer 訊號)────────────
|
// ── 跨外掛偵測訊號(dual-fire 對外保留 wp-data-optimizer 訊號)────────────
|
||||||
const TWO_MEET_DATA_OPTIMIZER_VERSION = '0.1.0';
|
const TWO_MEET_DATA_OPTIMIZER_VERSION = '1.0.0';
|
||||||
const TWO_MEET_DATA_OPTIMIZER_FILE = __FILE__;
|
const TWO_MEET_DATA_OPTIMIZER_FILE = __FILE__;
|
||||||
const TWO_MEET_DATA_OPTIMIZER_DB_VERSION = '1.2.0';
|
const TWO_MEET_DATA_OPTIMIZER_DB_VERSION = '1.2.0';
|
||||||
// 向後相容:對外 sister plugins 仍以 WP_DATA_OPTIMIZER_VERSION 偵測。
|
// 向後相容:對外 sister plugins 仍以 WP_DATA_OPTIMIZER_VERSION 偵測。
|
||||||
if ( ! defined( 'WP_DATA_OPTIMIZER_VERSION' ) ) {
|
if ( ! defined( 'WP_DATA_OPTIMIZER_VERSION' ) ) {
|
||||||
define( 'WP_DATA_OPTIMIZER_VERSION', '0.1.0' );
|
define( 'WP_DATA_OPTIMIZER_VERSION', '1.0.0' );
|
||||||
}
|
}
|
||||||
if ( ! defined( 'WP_DATA_OPTIMIZER_FILE' ) ) {
|
if ( ! defined( 'WP_DATA_OPTIMIZER_FILE' ) ) {
|
||||||
define( 'WP_DATA_OPTIMIZER_FILE', __FILE__ );
|
define( 'WP_DATA_OPTIMIZER_FILE', __FILE__ );
|
||||||
@@ -86,7 +88,11 @@ if ( version_compare( PHP_VERSION, TMDO_MIN_PHP, '<' ) ) {
|
|||||||
add_action(
|
add_action(
|
||||||
'admin_notices',
|
'admin_notices',
|
||||||
static function () {
|
static function () {
|
||||||
if ( defined( 'WPDO_VERSION' ) && WPDO_VERSION !== TMDO_VERSION ) {
|
// Deliberately not a WPDO_VERSION comparison: this plugin defines that
|
||||||
|
// constant itself (includes/class-tmdo-back-compat.php), so the two
|
||||||
|
// would always match and the notice could never fire.
|
||||||
|
$tmdo_legacy = 'wp-data-optimizer/wp-data-optimizer.php';
|
||||||
|
if ( in_array( $tmdo_legacy, (array) get_option( 'active_plugins', array() ), true ) ) {
|
||||||
echo '<div class="notice notice-error"><p>';
|
echo '<div class="notice notice-error"><p>';
|
||||||
esc_html_e( '⚠ 偵測到舊版 wp-data-optimizer 已啟用。請停用舊外掛以避免 hook 雙觸發。', '2meet-data-optimizer' );
|
esc_html_e( '⚠ 偵測到舊版 wp-data-optimizer 已啟用。請停用舊外掛以避免 hook 雙觸發。', '2meet-data-optimizer' );
|
||||||
echo '</p></div>';
|
echo '</p></div>';
|
||||||
|
|||||||
@@ -7,6 +7,85 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.0.0] — 2026-07-31 — 取代 wp-data-optimizer v3.4.6
|
||||||
|
|
||||||
|
本版把 `wp-data-optimizer` v3.0.1–v3.4.6(92 個 commit)全數 backport 進來,
|
||||||
|
`wp-data-optimizer` 自此退休:兩個站台皆已停用它,本機目錄改名為
|
||||||
|
`wp-data-optimizer.retired`,其 git 歷史保留在 gitea。
|
||||||
|
|
||||||
|
### 🔒 Security / 正確性
|
||||||
|
|
||||||
|
- **7 處 `Logger::error()` 參數不足**(array 傳給 string `$hook`)修正 —— 4 個 stress
|
||||||
|
tester、auto-promoter、conflict-detector、hook-bus,皆為 TypeError fatal
|
||||||
|
- **`Logger::trace_id()` 補回**:`Audit_Logger::write_row()` 一直呼叫它,但提煉時漏了;
|
||||||
|
`Audit_Logger::init()` 從未註冊所以隱藏至今 —— 實機驗證才暴露
|
||||||
|
- REST 4 個公開端點加上 `show_in_rest` 欄位 allowlist(CVSS 5.3)
|
||||||
|
- 13 個破壞性 admin 動作由 GET 改 POST + nonce
|
||||||
|
- DDL 型別白名單(19 型別)、CLI 表名守衛
|
||||||
|
- 整欄清空改 `wpdo_allow_mass_column_clear` opt-in(原本 >500 列才擋)
|
||||||
|
- audit 表補 `group_name` / `action` 欄並註冊 `Audit_Logger::init()`
|
||||||
|
- `known_login_ips` 註冊(spoke-sso 實際寫入的欄位,先前落 `wp_usermeta`)
|
||||||
|
|
||||||
|
### ⚡ 並發
|
||||||
|
|
||||||
|
- warm 表 `UNIQUE KEY ui_post_meta`(缺它 `ON DUPLICATE KEY UPDATE` 不生效)
|
||||||
|
- Zone_Cold 改 `JSON_MERGE_PATCH` / `JSON_REMOVE`、Zone_Warm 原子 `increment()`、
|
||||||
|
hook-bus `write_to_flat` 改 upsert
|
||||||
|
|
||||||
|
### 🏗 架構
|
||||||
|
|
||||||
|
- 回填 `TMDO_Zone_Router` + `TMDO_Routing_Predicate`(Sync_Bridge 400→269 行)
|
||||||
|
- 回填 Migration Phase Strategy:interface + base + 11 個 phase 類別,
|
||||||
|
orchestrator 1107→640 行,公開介面不變
|
||||||
|
- 核心新增 `TMDO_Standard_Post_Interceptor`,HP AddOn 4 個 interceptor 573→344 行
|
||||||
|
|
||||||
|
### 🔌 相容性
|
||||||
|
|
||||||
|
- hook 橋改**雙向**:核心一律 `do_action('wpdo_*')`,原本單向轉發使所有
|
||||||
|
`tmdo_*` 契約收不到事件
|
||||||
|
- 57 個 AddOn 類別補 `WPDO_*` alias(核心 alias 表在 `plugins_loaded:4` 執行,
|
||||||
|
涵蓋不到 `:6` 才載入的 AddOn)
|
||||||
|
- 38 個 CLI 子指令補 `wp tmdo` 命名空間
|
||||||
|
- 選單 slug 統一回 `wp-data-optimizer`(B 內部 10+ 處連結本來就指向它)
|
||||||
|
- HP adapter 介面改為 `TMDO_ extends WPDO_`,讓 `instanceof WPDO_HivePress_Adapter` 成立
|
||||||
|
|
||||||
|
### 🖥 Admin
|
||||||
|
|
||||||
|
- Settings 七個分區各加「儲存此區塊」AJAX 存檔(`wp_ajax_wpdo_save_settings_section`
|
||||||
|
+ 7 個 `save_section_*()` + `admin/assets/wpdo-settings.js`),整頁送出仍可用
|
||||||
|
- inline `style=` 由 348 處降到 9 處(剩下的全是 `--wpdo-*` CSS custom property
|
||||||
|
載體);`wpdo-admin.css` 543 → 912 行,補 149 個 class
|
||||||
|
|
||||||
|
### 🚧 邊界
|
||||||
|
|
||||||
|
- 核心不再無守衛呼叫 AddOn 類別。`TMDO_Listing_Stats`(HivePress AddOn)先前
|
||||||
|
在 admin Dashboard、兩個公開 REST 端點、`wp tmdo cleanup --archive-expired`
|
||||||
|
被直呼,沒裝該 AddOn 的站台會 fatal;`TMDO_HPCT_Import` 在 admin 與
|
||||||
|
`wp tmdo import-hpct` 同樣缺守衛
|
||||||
|
- `TMDO_Zone_Warm::VIEW_KEY` / `VIEW_TTL` 由核心持有(值與 AddOn 常數相同的
|
||||||
|
`'wpdo_views'`,指向同一批列,無資料遷移)
|
||||||
|
- 新增 `CoreBoundaryTest`:靜態掃描 126 個核心生產檔,強制每處 AddOn 類別
|
||||||
|
呼叫都在同一函式內有 `class_exists()` 守衛
|
||||||
|
|
||||||
|
### ✅ 品質
|
||||||
|
|
||||||
|
- 128 個生產檔 `declare(strict_types=1)`
|
||||||
|
- PHPCS 0 errors / PHPStan level 6(baseline 710)
|
||||||
|
- 測試:核心 unit 587 / integration 416、hivepress-addon unit 154 /
|
||||||
|
integration 57、woocommerce-addon unit 10 / integration 18
|
||||||
|
- gitea CI:核心 6 個 job 全綠 + 11 個 AddOn workflow + branch protection
|
||||||
|
- 打包 10 項終檢全 PASS(569 KB / 187 entries,schema drift 0:34 CREATE / 34 DROP)
|
||||||
|
|
||||||
|
### ⚠️ ABI / 行為變更
|
||||||
|
|
||||||
|
- `doctor_callback` 由 3 參數改為 1 參數(表名)
|
||||||
|
- `wpdo_capture_before_value` 預設 `true` → `false`;需要 `value_before` 的
|
||||||
|
消費者(audit log)要 `add_filter( 'wpdo_capture_before_value', '__return_true' )`
|
||||||
|
- `wpdo_allow_mass_column_clear` 現為 opt-in,整欄清空預設被拒
|
||||||
|
- `SCHEMA_VERSION` 2.0.0 → 2.1.0(audit 表 ALTER)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.1.0] — 2026-05-15 (initial release, Phase 0-5 完成)
|
## [0.1.0] — 2026-05-15 (initial release, Phase 0-5 完成)
|
||||||
|
|
||||||
### Phase 5 hotfix(同日完成)
|
### Phase 5 hotfix(同日完成)
|
||||||
|
|||||||
+324
@@ -0,0 +1,324 @@
|
|||||||
|
# CONTEXT.md — 2meet Data Optimizer Domain Glossary
|
||||||
|
|
||||||
|
This file defines the canonical vocabulary for 2meet Data Optimizer.
|
||||||
|
Architecture reviews, AI assistance, and code documentation must use these terms exactly.
|
||||||
|
|
||||||
|
> **Status (2026-07-31, v1.0.0):** 核心 451 unit / 416 integration · hivepress-addon 145 unit / 57 integration · woocommerce-addon 10 unit / 18 integration · PHPCS 0 errors · PHPStan L6(baseline 710)· gitea CI 6 job 全綠。
|
||||||
|
> v1.0.0 取代 `wp-data-optimizer` v3.4.6,該外掛已退休(本機目錄改名 `.retired`,git 歷史留在 gitea)。版號自 1.0.0 重啟。
|
||||||
|
> **本外掛只有通用引擎**:4 entity(post / user / term / comment)反 EAV、zone 表、migration、wizard、admin、CLI、REST、snapshot、diagnostic、notifications。HivePress / WooCommerce / LatePoint / 2meet-* 整合層全部在 11 個獨立 AddOn,核心不得引用它們的類別。
|
||||||
|
> **共用命名空間刻意保留**:`wp_wpdo_*` 表、`wpdo_` option / cron / hook 前綴、`wpdo/v1` REST namespace 一律不改名 —— 這是資料層零遷移的前提。類別與函式前綴才是 `TMDO_` / `tmdo_`,並以 `class_alias()` 對外保留 `WPDO_*`。
|
||||||
|
> HivePress addon detection uses the **WP active-plugins list** as the authoritative signal (addons publish no per-addon class/const; they register via `add_filter('hivepress/v1/extensions', …)`). See `TMDO_HivePress_Detector::detect()` / `active_plugin_files()` in the HivePress AddOn.
|
||||||
|
> **測試 harness 隔離**:`TMDO_FSM_GUARD_DISABLED`(integration bootstrap,讓測試可強制 module state)、`TMDO_Routing_Predicate::flush_cache()`、`TMDO_Schema_Manager::flush_table_exists_cache()` 三者是跨測試污染的解法,**不是** production 缺陷的補丁。
|
||||||
|
> **gitea runner 為 wpdev 全 repo 共享**(host-mode label `ubuntu-latest:host`,無 service container,integration job 直接連本機 MariaDB)。CI 卡 pending 時先查 `GET /api/v1/user/actions/runners`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zone
|
||||||
|
|
||||||
|
A dedicated storage tier for WordPress postmeta, optimised for a specific access pattern.
|
||||||
|
|
||||||
|
| Zone | Slug | Table pattern | Purpose |
|
||||||
|
|------|------|---------------|---------|
|
||||||
|
| Hot | `hot` | `wpdo_hot_{post_type}` | Flat columns for search/filter (index-friendly) |
|
||||||
|
| Warm | `warm` | `wpdo_warm` | TTL key-value store for counts and transient flags |
|
||||||
|
| Cold | `cold` | `wpdo_cold_{post_type}` | JSON blob for display-only fields |
|
||||||
|
| Archive | `archive` | `wpdo_archive` | gzip-compressed historical data |
|
||||||
|
|
||||||
|
A post type's fields are assigned to exactly one zone via the **Schema Registry**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schema Registry
|
||||||
|
|
||||||
|
`TMDO_Schema_Registry` — singleton that maps `(post_type, meta_key) → field definition`.
|
||||||
|
|
||||||
|
A **field definition** carries `zone`, `column` (flat name), and optional `type`.
|
||||||
|
The registry is populated at `wpdo_register_fields` action by integrations and adapters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zone Router
|
||||||
|
|
||||||
|
`TMDO_Zone_Router` — static dispatch layer introduced in v3.0.1.
|
||||||
|
|
||||||
|
Single module that knows how to route `read`, `write`, and `delete_field` to the correct
|
||||||
|
zone handler (Hot / Warm / Cold / Archive) given a field definition.
|
||||||
|
Also produces canonical **module names** for Feature Flag lookups.
|
||||||
|
|
||||||
|
> **Why it exists**: before v3.0.1, routing logic was duplicated across `TMDO_Sync_Bridge`,
|
||||||
|
> `TMDO_REST_API`, and two query files. Extracting it here creates one seam for tests and
|
||||||
|
> one place to change zone routing decisions.
|
||||||
|
|
||||||
|
Interface (all static):
|
||||||
|
```
|
||||||
|
TMDO_Zone_Router::module_name(zone, post_type) → string
|
||||||
|
TMDO_Zone_Router::read(field, post_id, post_type, meta_key) → mixed
|
||||||
|
TMDO_Zone_Router::write(field, post_id, post_type, meta_key, value) → void
|
||||||
|
TMDO_Zone_Router::delete_field(field, post_id, post_type, meta_key) → void
|
||||||
|
TMDO_Zone_Router::delete_post(post_id, post_type) → void # v3.4.0: all-zone post cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routing Predicate
|
||||||
|
|
||||||
|
`TMDO_Routing_Predicate` — centralised predicate module introduced in v3.4.0.
|
||||||
|
|
||||||
|
Two recurring guard patterns previously scattered across `TMDO_Sync_Bridge`,
|
||||||
|
`TMDO_Query_Router`, and the REST API are now a single module:
|
||||||
|
|
||||||
|
```
|
||||||
|
TMDO_Routing_Predicate::entity_bridge_owns(meta_key) → bool
|
||||||
|
TMDO_Routing_Predicate::should_write_to_zone(post_type, zone) → bool
|
||||||
|
TMDO_Routing_Predicate::should_read_from_zone(post_type, zone) → bool
|
||||||
|
TMDO_Routing_Predicate::should_query_from_zone(post_type, zone) → bool
|
||||||
|
TMDO_Routing_Predicate::flush_cache() → void # test isolation helper
|
||||||
|
```
|
||||||
|
|
||||||
|
`entity_bridge_owns()` returns true when `TMDO_Mode_Manager::writes_to_flat('post')` AND
|
||||||
|
the key is registered in `TMDO_Entity_Registry` for 'post'. Result is memoised in a
|
||||||
|
request-level static cache (`$entity_bridge_cache`) keyed by `meta_key`.
|
||||||
|
|
||||||
|
The three `should_*` predicates are thin compositions of
|
||||||
|
`TMDO_Zone_Router::module_name()` + the matching `TMDO_Feature_Flags::is_*()` method.
|
||||||
|
|
||||||
|
> **Why it exists**: without this module, callers re-implemented the same two-step
|
||||||
|
> "module_name then Feature_Flags" pattern inline. The **deletion test** confirms depth:
|
||||||
|
> removing it pushes the repeated guard back into 8+ call sites in Sync_Bridge alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sync Bridge
|
||||||
|
|
||||||
|
`TMDO_Sync_Bridge` — zone-aware dual-write dispatcher hooked into the WordPress metadata API.
|
||||||
|
|
||||||
|
Intercepts `get/update/add/delete_post_metadata` and `before_delete_post`.
|
||||||
|
Calls `TMDO_Zone_Router::read/write/delete_field/delete_post` directly — no private
|
||||||
|
I/O wrappers (v3.4.1: delegate methods inlined and removed).
|
||||||
|
Uses the **Routing Predicate** for all ownership and activation checks.
|
||||||
|
|
||||||
|
The bridge skips fields owned by the **Entity Bridge** to avoid duplicate writes.
|
||||||
|
|
||||||
|
Its only private helper is `get_field_cached()` — a request-level Schema Registry
|
||||||
|
lookup cache keyed by `post_type:meta_key`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module
|
||||||
|
|
||||||
|
A named unit whose lifecycle is tracked by the **Feature Flags** 7-state machine.
|
||||||
|
Module names follow the convention: `hot_{post_type}`, `cold_{post_type}`, `warm`, `archive`.
|
||||||
|
|
||||||
|
`TMDO_Zone_Router::module_name()` is the single authoritative source for this naming.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Flags / 7-state FSM
|
||||||
|
|
||||||
|
`TMDO_Feature_Flags` — state machine governing migration lifecycle for each module.
|
||||||
|
|
||||||
|
States: `idle → dual_write → backfill → verify → cutover → cleanup → complete`
|
||||||
|
|
||||||
|
`is_write_active(module)` returns true for `dual_write` and above.
|
||||||
|
`is_read_custom(module)` returns true for `cutover` and above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Bridge
|
||||||
|
|
||||||
|
Newer anti-EAV system (v2.5+) covering user, term, comment, and post entities via a
|
||||||
|
**Hook Bus** that intercepts native `*meta()` API calls and writes to flat entity tables.
|
||||||
|
|
||||||
|
`TMDO_Mode_Manager` controls per-entity mode: `disabled → dual_write → shadow_read → aeav_only`.
|
||||||
|
`TMDO_Entity_Registry` maps `(entity_type, group) → fields`.
|
||||||
|
|
||||||
|
The Entity Bridge and the Zone system coexist; the Sync Bridge's
|
||||||
|
`is_owned_by_entity_bridge()` guard prevents duplicate writes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Phase (Strategy pattern)
|
||||||
|
|
||||||
|
Interface: `TMDO_Migration_Phase_Interface` (v3.0.1).
|
||||||
|
|
||||||
|
Each phase encapsulates a single step of the Entity Bridge migration pipeline:
|
||||||
|
|
||||||
|
| Phase class | Slug | What it does |
|
||||||
|
|-------------|------|--------------|
|
||||||
|
| `TMDO_Phase_Diagnose` | `diagnose` | Records preflight EAV row count / ratio |
|
||||||
|
| `TMDO_Phase_Backup` | `backup` | Dumps native meta table to `uploads/wpdo-backups/` |
|
||||||
|
| `TMDO_Phase_Demote` | `demote` | aeav_only → dual_write (rollback entry point) |
|
||||||
|
| `TMDO_Phase_Install_Schema` | `install_schema` | Creates flat tables via Schema Manager |
|
||||||
|
| `TMDO_Phase_Backfill_Bulk` | `backfill_bulk` | Pivots text-only groups via INSERT…SELECT |
|
||||||
|
| `TMDO_Phase_Backfill_Unserialize` | `backfill_unserialize` | Row-by-row migration for JSON fields |
|
||||||
|
| `TMDO_Phase_Promote_Shadow` | `promote_shadow` | dual_write → shadow_read |
|
||||||
|
| `TMDO_Phase_Verify_Sample` | `verify_sample` | Samples entities, compares flat vs EAV |
|
||||||
|
| `TMDO_Phase_Promote_Aeav` | `promote_aeav` | shadow_read → aeav_only |
|
||||||
|
| `TMDO_Phase_Cleanup` | `cleanup` | Deletes managed keys from EAV table |
|
||||||
|
| `TMDO_Phase_Completed` | `completed` | Terminal — marks job done, returns `'done'` |
|
||||||
|
|
||||||
|
`TMDO_Migration_Phase_Base` provides shared helpers (`log()`, `get_managed_keys()`,
|
||||||
|
`execute_bulk_pivot()`, `values_loose_equal()`).
|
||||||
|
|
||||||
|
The **Migration Orchestrator** (`TMDO_Migration_Orchestrator`) injects phase objects and
|
||||||
|
calls `execute($job)` in sequence, advancing through the pipeline.
|
||||||
|
|
||||||
|
> **Seam**: the interface is the test surface. A phase can be tested by constructing it
|
||||||
|
> with a stub entity type, calling `execute()` with a job array, and asserting on the
|
||||||
|
> job's `state`, `log`, and `metrics` — no hooks or DB needed for unit tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard Post Interceptor (Template Method pattern)
|
||||||
|
|
||||||
|
`TMDO_Standard_Post_Interceptor` — abstract base (v3.0.1) for HPCT-inherited interceptors.
|
||||||
|
|
||||||
|
Eliminates the three hook methods (`filter_update_meta`, `action_insert_post`,
|
||||||
|
`action_delete_post`) that were previously duplicated across four interceptor classes.
|
||||||
|
Subclasses declare only:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public const FIELD_MAP = ['meta_key' => 'flat_column', ...];
|
||||||
|
protected function get_post_type(): string { ... }
|
||||||
|
protected function get_table_key(): string { ... }
|
||||||
|
protected function build_insert_data(int $post_id, WP_Post $post, string $now): array { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Concrete subclasses: `TMDO_Reviews_Interceptor`, `TMDO_Messages_Interceptor`,
|
||||||
|
`TMDO_Memberships_Interceptor`, `TMDO_Requests_Interceptor`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## External Partners
|
||||||
|
|
||||||
|
`TMDO_Core::EXTERNAL_PARTNERS` (v3.0.1) — PHP class-name constant array listing all
|
||||||
|
external plugin integrations that self-register via the Hook Bus.
|
||||||
|
|
||||||
|
```php
|
||||||
|
['TMDO_Infocards', 'TMDO_Bookings', 'TMDO_Quotation',
|
||||||
|
'TMDO_Mobile_Bridge', 'TMDO_Collab', 'TMDO_Playlist']
|
||||||
|
```
|
||||||
|
|
||||||
|
Used in the late-bind priority-30 closure in `2meet-data-optimizer.php` to initialize
|
||||||
|
partner integrations only when their plugin class is present. Single source of truth —
|
||||||
|
previously the list existed only inside the closure and diverged from `TMDO_Core`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HPCT Interceptors Manifest
|
||||||
|
|
||||||
|
> **v1.0.0:本節描述的是 AddOn 的內部結構,核心已無此常數。**
|
||||||
|
> `HPCT_INTERCEPTORS` 與 `register_hpct_interceptors()` 隨整合層一起搬到
|
||||||
|
> `2meet-data-optimizer-hivepress-addon` 的 bootstrap;核心刻意不保留,否則會引用
|
||||||
|
> 8 個核心不存在的類別名。下表列的 interceptor ↔ query-handler 配對關係仍然成立。
|
||||||
|
|
||||||
|
`TMDO_Core::HPCT_INTERCEPTORS` (v3.4.0) — PHP class-constant array that is the single
|
||||||
|
authoritative list of HPCT-inherited interceptor → query-handler pairs:
|
||||||
|
|
||||||
|
```php
|
||||||
|
TMDO_Reviews_Interceptor::class → TMDO_Reviews_Query::class
|
||||||
|
TMDO_Messages_Interceptor::class → TMDO_Messages_Query::class
|
||||||
|
TMDO_Favorites_Interceptor::class → null
|
||||||
|
TMDO_Memberships_Interceptor::class → TMDO_Memberships_Query::class
|
||||||
|
TMDO_Statistics_Interceptor::class → null
|
||||||
|
TMDO_Requests_Interceptor::class → TMDO_Requests_Query::class
|
||||||
|
TMDO_Listing_Meta_Interceptor::class → TMDO_Listing_Meta_Query::class
|
||||||
|
TMDO_LatePoint_Interceptor::class → null
|
||||||
|
```
|
||||||
|
|
||||||
|
`register_hpct_interceptors()` iterates this constant; adding a new interceptor requires
|
||||||
|
only one entry here — no code change in the registration method.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HPCT (HP Custom Tables)
|
||||||
|
|
||||||
|
The plugin this replaces. Still referenced in import path (`wp tmdo import-hpct`).
|
||||||
|
Any "HPCT-inherited" interceptor means it originated in HPCT and was migrated here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adapter (HivePress)
|
||||||
|
|
||||||
|
> **v1.0.0:這 13 個 adapter 住在 `2meet-data-optimizer-hivepress-addon`,不在核心。**
|
||||||
|
> 核心只提供 `TMDO_Schema_Registry` 與 `wpdo_register_fields` 這個 seam。
|
||||||
|
|
||||||
|
Each HivePress addon (core, bookings, events, …) has a corresponding
|
||||||
|
`TMDO_Hivepress_*_Adapter` that registers its meta keys into the Schema Registry at
|
||||||
|
`wpdo_register_fields`. The adapter is the seam between HivePress and the zone system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-EAV
|
||||||
|
|
||||||
|
The overarching goal: eliminate Entity-Attribute-Value (EAV) reads from `wp_postmeta`,
|
||||||
|
`wp_usermeta`, etc. by moving data to flat tables.
|
||||||
|
|
||||||
|
Compliance is enforced by `wpdo-policy-enforcer.php` (mu-plugin) and the
|
||||||
|
`wp tmdo lint` CLI gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Group
|
||||||
|
|
||||||
|
A named set of fields registered under one entity type (user / post / term / comment),
|
||||||
|
stored as one flat table: `wp_wpdo_{entity_type}_{group_name}`.
|
||||||
|
|
||||||
|
Each group has **one row per entity** (keyed on `user_id` / `post_id` / etc.).
|
||||||
|
Fields within the group become typed columns; `sanitize_column_name()` produces the
|
||||||
|
column name (strips all non-`[a-zA-Z0-9_]` characters — hyphens are removed, not replaced).
|
||||||
|
|
||||||
|
Group registration: `TMDO_Entity_Registry::register_group(entity_type, group_name, fields[])`.
|
||||||
|
|
||||||
|
Canonical user groups (v3.1.4):
|
||||||
|
|
||||||
|
| Group | Table | Purpose |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| `membership` | `wp_wpdo_user_membership` | Tier, points, expiry |
|
||||||
|
| `activity` | `wp_wpdo_user_activity` | Login counters, last-active |
|
||||||
|
| `profile` | `wp_wpdo_user_profile` | Bio, avatar, display name |
|
||||||
|
| `sso` | `wp_wpdo_user_sso` | Hub token cache, SLO hash |
|
||||||
|
| `core_profile` | `wp_wpdo_user_core_profile` | WP first_name, last_name, nickname, description |
|
||||||
|
| `social` | `wp_wpdo_user_social` | 15 social platform URLs |
|
||||||
|
| `commerce` | `wp_wpdo_user_commerce` | WC billing/shipping + runtime stats (wc_last_active, wc_order_count_wp, last_update) |
|
||||||
|
| `hp_user` | `wp_wpdo_user_hp_user` | HP favorites, avatar, hp_verified |
|
||||||
|
| `admin_prefs` | `wp_wpdo_user_admin_prefs` | WP default admin-UI keys written by wp_insert_user + extended UI prefs (22 keys total, v3.1.4) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EAV Floor
|
||||||
|
|
||||||
|
The minimum irreducible rows that must remain in the native meta table even after full
|
||||||
|
Entity Bridge migration to `aeav_only`.
|
||||||
|
|
||||||
|
For `wp_usermeta`, the floor consists of:
|
||||||
|
|
||||||
|
| Key | Rows per user | Why irreducible |
|
||||||
|
|-----|---------------|-----------------|
|
||||||
|
| `wp_capabilities` | 1 | WP core reads directly in `WP_User` constructor (not through `get_user_meta`) |
|
||||||
|
| `session_tokens` | 0–N | WP authentication reads sessions directly; Hook Bus cannot intercept session creation safely |
|
||||||
|
| `_application_passwords` | 0–1 | WP reads directly during REST auth |
|
||||||
|
|
||||||
|
**Theoretical minimum ratio** for a standard WordPress site: `(users + sessions + app_passwords) / users ≈ 1:1.07` (varies by active sessions).
|
||||||
|
|
||||||
|
Any ratio above 1:1.07 represents reducible EAV that WPDO can absorb.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WP Admin Prefs
|
||||||
|
|
||||||
|
The set of `wp_usermeta` keys that WordPress core writes automatically:
|
||||||
|
|
||||||
|
**Written by `wp_insert_user()` for every new user (7 keys)**:
|
||||||
|
`rich_editing`, `syntax_highlighting`, `comment_shortcuts`, `admin_color`, `use_ssl`,
|
||||||
|
`show_admin_bar_front`, `dismissed_wp_pointers`
|
||||||
|
|
||||||
|
**Written on first admin-page visit or explicit user action (extended, registered v3.1.4)**:
|
||||||
|
`show_welcome_panel`, `wp_persisted_preferences`, `nav_menu_recently_edited`,
|
||||||
|
`wp_dashboard_quick_press_last_post_id`, `edit_*_per_page` variants,
|
||||||
|
`community-events-location`, `wp_user-settings`, `wp_user-settings-time`,
|
||||||
|
`managenav-menuscolumnshidden`, `metaboxhidden_nav-menus`,
|
||||||
|
`dismissed_no_secure_connection_notice`, `meta-box-order_product`
|
||||||
|
|
||||||
|
All 22 keys are registered in the `admin_prefs` Entity Group and routed to
|
||||||
|
`wp_wpdo_user_admin_prefs` by the Hook Bus when user entity mode ≥ `dual_write`.
|
||||||
@@ -207,10 +207,59 @@
|
|||||||
| 2 | DDL 與並發 B1–B4(warm UNIQUE KEY → Zone_Cold save_patch → Zone_Warm increment) | ✅ 2026-07-31 |
|
| 2 | DDL 與並發 B1–B4(warm UNIQUE KEY → Zone_Cold save_patch → Zone_Warm increment) | ✅ 2026-07-31 |
|
||||||
| 3 | 架構回填(Zone_Router + Routing_Predicate;13 個 Migration Phase 類別) | ✅ 2026-07-31 |
|
| 3 | 架構回填(Zone_Router + Routing_Predicate;13 個 Migration Phase 類別) | ✅ 2026-07-31 |
|
||||||
| 4 | 相容層 E1–E7(雙向 hook 橋、57 個 AddOn alias、38 個 CLI 子指令、選單 slug) | ✅ 2026-07-31 |
|
| 4 | 相容層 E1–E7(雙向 hook 橋、57 個 AddOn alias、38 個 CLI 子指令、選單 slug) | ✅ 2026-07-31 |
|
||||||
| 5 | AddOn 同步 F1–F5(Standard_Post_Interceptor、HP detector v3.4.5、WC) | ⬜ |
|
| 5 | AddOn 同步 F1–F5(Standard_Post_Interceptor、HP detector v3.4.5、WC) | ✅ 2026-07-31 |
|
||||||
| 6 | 功能與診斷 PR-G(crypto 健檢、doctor_callback ABI、15 個 usermeta 欄位…) | ⬜ |
|
| 6 | 功能與診斷 PR-G(crypto 健檢、doctor_callback ABI、15 個 usermeta 欄位…) | ✅ 2026-07-31(**Settings 分區 AJAX 未做**,見下) |
|
||||||
| 7 | 全域清理與 CI(strict_types、phpcs.xml、phpstan + baseline、gitea workflows) | ⬜ |
|
| 7 | 全域清理與 CI(strict_types、phpcs.xml、phpstan + baseline、gitea workflows) | 🔶 strict_types / PHPCS / PHPStan / 3 個 workflow / ci-package.sh ✅;**測試補齊(剩 36 檔,多為 HP/WC)、文件移植未做** |
|
||||||
| 退休 | 兩站台切到 B、移除 A、B 升 1.0.0 | ⬜ |
|
| 退休 | 兩站台切到 B、移除 A、B 升 1.0.0 | 🔶 dev30 已切換並驗證通過;dev 未切、A 未移除、版本未升 |
|
||||||
|
|
||||||
|
### dev30 實機驗證結果(2026-07-31)
|
||||||
|
|
||||||
|
環境:`2meet-brandcards` 1.8.3 / `2meet-hub-core` 1.16.0 / `2meet-spoke-sso` 1.15.0 同時啟用;HivePress / WooCommerce 未安裝,故只啟用核心 + hub-addon + spoke-addon。
|
||||||
|
|
||||||
|
| 項目 | A v3.0.0(切換前) | B v0.1.0(切換後) |
|
||||||
|
|---|---|---|
|
||||||
|
| `doctor` | 33 個 `[OK]` + 15 個 WC 表 MISSING 警告 | **49 個 `[OK]`、All checks passed** |
|
||||||
|
| 姊妹外掛自訂表 | 36 張 OK | 36 張 OK(含 brandcards 的 `doctor_callback` 回傳訊息) |
|
||||||
|
| 新健檢 | 無 | `Backup dir blocked (HTTP 404)`、`Crypto key derivable` 皆 OK |
|
||||||
|
|
||||||
|
逐項確認:
|
||||||
|
- **alias**:15 個核心類別 + trait + interface + `wpdo_run()` + `WPDO_VERSION` 全部可用
|
||||||
|
- **CLI**:`wp tmdo bridge-status / crypto-status / mode-audit` 與 `wp wpdo *` 兩條路徑都通(38 個子指令)
|
||||||
|
- **anti-EAV 寫入**:`WPDO_API::set_entity()` → `wp_wpdo_user_sso.known_login_ips` 寫入成功(A14 生效)
|
||||||
|
- **audit**:`wp_wpdo_audit` 的 `group_name` / `action` / `trace_id`(UUIDv4)皆有值(A11/A12 生效)
|
||||||
|
- **REST allowlist**:註冊 `show_in_rest => false` 的欄位不出現在 `get_rest_visible_hot_columns()`(A5/A6 生效)
|
||||||
|
- **doctor_callback ABI 變更**:brandcards 既有 callback 在 1 參數簽章下仍正常
|
||||||
|
- 前台 HTTP 200;`_load_textdomain_just_in_time` notice 僅出現在 `wp plugin activate` 當下(啟用鉤子早於 `init`),一般請求與前台皆無
|
||||||
|
|
||||||
|
**實機才抓到的 fatal**:`TMDO_Logger::trace_id()` 在 B 提煉時遺漏,而 `Audit_Logger::write_row()` 會呼叫它。A12 把 Audit_Logger 掛上後,任何一次受管 meta 寫入都會 fatal。PHPUnit 沒有覆蓋 audit 寫入路徑 → 這類「跨模組才會踩到」的缺口只有實機能發現。
|
||||||
|
|
||||||
|
### 尚未完成(follow-up)
|
||||||
|
|
||||||
|
1. **Settings 分區 AJAX 儲存**(A v3.0.2):`ajax_save_settings_section()` + 7 個 `save_section_*()` + `admin/assets/wpdo-settings.js`。B 目前是一次存全部的 inline `update_option` 巨塊,功能可用,屬 UX 改善。其中 `save_section_hp_transient` / `save_section_wc_term_count` 對應 AddOn 已移除的設定,移植時要拆掉或加 `class_exists` 守衛。
|
||||||
|
2. ~~CI workflow~~ ✅ 三個檔已建,但 **B 沒有 git remote**,要推上 gitea 才會實際執行;推上去後還需設 repo secret `TMDO_TEST_DB_PASS`、var `RELEASE_GITEA_URL`、secret `RELEASE_TOKEN`,並開 main branch protection。11 個 AddOn 的 CI 仍未建。
|
||||||
|
3. ~~`scripts/ci-package.sh`~~ ✅(委派共用 `package-plugin.sh`,實測 10 項終檢全 PASS)
|
||||||
|
4. ~~補測試檔~~ ✅ 核心補 `HookBusIntegrationTest`(14)+ 新寫 `AuditLoggerIntegrationTest`(4);HP AddOn 建 phpunit 基建並移植 25 檔(145 tests);WC AddOn 建基建並移植 1 檔(10 tests)。11 個 AddOn 皆有 CI workflow(HP/WC 跑 lint+unit,其餘 9 個薄註冊層只跑 lint)。
|
||||||
|
- 仍缺:`TermCommentBackfillTest`(require HP AddOn 的 term-comment-fields,可移入 HP AddOn 套件)、`ListingStatsTest`(同上)、HP/WC 的 integration 測試(`HivepressIntegrationTest` ×3、`WCTermCountFilterTest`、`ListingMetaInterceptorTest`)——這些需要 AddOn 端的 integration bootstrap。
|
||||||
|
|
||||||
|
### 全家族測試現況(2026-07-31)
|
||||||
|
|
||||||
|
| 套件 | 結果 |
|
||||||
|
|---|---|
|
||||||
|
| 核心 unit | 451 tests / 889 assertions |
|
||||||
|
| 核心 integration | 416 tests / 1165 assertions |
|
||||||
|
| hivepress-addon unit | 145 tests / 357 assertions |
|
||||||
|
| woocommerce-addon unit | 10 tests / 33 assertions |
|
||||||
|
|
||||||
|
AddOn 測試 bootstrap 直接 `require` 核心 plugin 的 `tests/bootstrap.php`(避免複製 ~700 行 WP stub),因此 **AddOn 的 CI job 必須同時 checkout 核心 plugin**,workflow 已如此設定。
|
||||||
|
5. **文件移植**:`readme.txt`、`CONTEXT.md`、`docs/`(ENTITY_ADAPTER_COOKBOOK、INTEGRATION_PATTERN_DECISION、2 篇 ADR)。
|
||||||
|
6. **實機驗證與退休切換**:兩站台目前仍跑 A(dev = v3.4.6、dev30 = v3.0.0),B 家族 12 個外掛全 inactive,尚未做過 `wp tmdo doctor` 實機驗證。
|
||||||
|
|
||||||
|
### ABI / 行為變更(升級須知)
|
||||||
|
|
||||||
|
- `doctor_callback` 由 3 參數改為 1 參數(表名)——AddOn 若註冊過該 callback 需同步。
|
||||||
|
- `wpdo_capture_before_value` 預設 `true` → `false`,每次受管寫入省一次 DB read;需要 `value_before` 的消費者(audit log)要 `add_filter( 'wpdo_capture_before_value', '__return_true' )`。
|
||||||
|
- `wpdo_allow_mass_column_clear` 現為 opt-in:`delete_all=true` 的整欄清空預設被拒。
|
||||||
|
- 選單 slug 由 `2meet-data-optimizer` 改回 `wp-data-optimizer`。
|
||||||
|
|
||||||
### Lessons learned(backport)
|
### Lessons learned(backport)
|
||||||
|
|
||||||
@@ -218,6 +267,7 @@
|
|||||||
- `composer update` 後 autoload 出現 `WPDO_Entity_Adapter_Interface` 三重宣告警告(`includes/adapters/interface-entity-adapter.php`、`back-compat/interface-wpdo-entity-adapter-alias.php`、`back-compat/trait-wpdo-anti-eav-aware-alias.php`)→ 階段 4 E2 要刪掉重複的那一份。
|
- `composer update` 後 autoload 出現 `WPDO_Entity_Adapter_Interface` 三重宣告警告(`includes/adapters/interface-entity-adapter.php`、`back-compat/interface-wpdo-entity-adapter-alias.php`、`back-compat/trait-wpdo-anti-eav-aware-alias.php`)→ 階段 4 E2 要刪掉重複的那一份。
|
||||||
- `TMDO_FSM_GUARD_DISABLED` 兩個 test bootstrap 原本都沒 define,但 `class-tmdo-feature-flags.php:128` 會檢查它 → 強制 FSM 轉換的測試會被 guard 擋掉(A 在 v3.4.6 踩過同一個坑)。
|
- `TMDO_FSM_GUARD_DISABLED` 兩個 test bootstrap 原本都沒 define,但 `class-tmdo-feature-flags.php:128` 會檢查它 → 強制 FSM 轉換的測試會被 guard 擋掉(A 在 v3.4.6 踩過同一個坑)。
|
||||||
- integration 測試需要 `TMDO_TEST_DB_PASS`(或 `WPDO_TEST_DB_PASS`)環境變數,DB 為本機 `wp_wpdo_test`。
|
- integration 測試需要 `TMDO_TEST_DB_PASS`(或 `WPDO_TEST_DB_PASS`)環境變數,DB 為本機 `wp_wpdo_test`。
|
||||||
|
- **跑完 `scripts/ci-package.sh` 或 `package-plugin.sh` 後要重跑 `composer install`**:打包流程內含 `composer install --no-dev`,會把 `vendor/bin/{phpunit,phpcs,phpstan}` 移掉,之後任何測試指令都會 "No such file or directory"。
|
||||||
- **A 不是照抄對象,有兩處自身缺陷不可照搬**:
|
- **A 不是照抄對象,有兩處自身缺陷不可照搬**:
|
||||||
1. `A/includes/engine/class-wpdo-hook-bus.php:511,522` 傳 3 個 string 給 `Logger::warning( string $event, array $context )` → A 有 `strict_types`,mass-clear 路徑必炸。B 用正確簽章。
|
1. `A/includes/engine/class-wpdo-hook-bus.php:511,522` 傳 3 個 string 給 `Logger::warning( string $event, array $context )` → A 有 `strict_types`,mass-clear 路徑必炸。B 用正確簽章。
|
||||||
2. `A/admin/class-wpdo-dashboard-widget.php:191` 的 `wpdo_postmeta_cleanup` 仍是 `wp_nonce_url` GET 連結,但其 handler 已只收 POST → 該按鈕在 A 是壞的。B 改成 form。
|
2. `A/admin/class-wpdo-dashboard-widget.php:191` 的 `wpdo_postmeta_cleanup` 仍是 `wp_nonce_url` GET 連結,但其 handler 已只收 POST → 該按鈕在 A 是壞的。B 改成 form。
|
||||||
|
|||||||
@@ -119,9 +119,19 @@ GPL-2.0-or-later
|
|||||||
|
|
||||||
## 文件
|
## 文件
|
||||||
|
|
||||||
|
- [readme.txt](readme.txt) — WordPress 外掛目錄格式說明(隨 ZIP 發佈)
|
||||||
|
- [CONTEXT.md](CONTEXT.md) — 領域詞彙表(Zone / Registry / Mode / FSM 的正式定義)
|
||||||
- [PLAN.md](PLAN.md) — 開發任務追蹤
|
- [PLAN.md](PLAN.md) — 開發任務追蹤
|
||||||
- [CHANGELOG.md](CHANGELOG.md) — 版本歷史
|
- [CHANGELOG.md](CHANGELOG.md) — 版本歷史
|
||||||
- [DEPLOY.md](DEPLOY.md) — 部署流程
|
- [DEPLOY.md](DEPLOY.md) — 部署流程
|
||||||
- [SECURITY.md](SECURITY.md) — 安全聯絡
|
- [SECURITY.md](SECURITY.md) — 安全聯絡
|
||||||
- [DESIGN.md](DESIGN.md) — 架構決策
|
- [DESIGN.md](DESIGN.md) — 架構決策
|
||||||
- [CLAUDE.md](CLAUDE.md) — AI 協作指引
|
- [CLAUDE.md](CLAUDE.md) — AI 協作指引
|
||||||
|
|
||||||
|
給夥伴外掛作者(`docs/`,不進 ZIP):
|
||||||
|
|
||||||
|
- [ENTITY_ADAPTER_COOKBOOK.md](docs/ENTITY_ADAPTER_COOKBOOK.md) — 五層整合階梯與決策樹
|
||||||
|
- [adr-001-post-entity-source-of-truth.md](docs/adr-001-post-entity-source-of-truth.md) — post entity 的權威來源契約
|
||||||
|
- [adr-002-dual-write-naming-collision.md](docs/adr-002-dual-write-naming-collision.md) — `dual_write` 在兩套 FSM 的同名衝突
|
||||||
|
- [INTEGRATION_PATTERN_DECISION.md](docs/INTEGRATION_PATTERN_DECISION.md) — 整合模式取捨紀錄(含 v1.0.0 後記)
|
||||||
|
- [anti-eav-lint.yml.template](docs/anti-eav-lint.yml.template) — 夥伴外掛 CI gate 樣板
|
||||||
|
|||||||
+401
-2
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* WP Data Optimizer — Admin Styles (Morandi Design System)
|
* 2meet Data Optimizer — Admin Styles (Morandi Design System)
|
||||||
*
|
*
|
||||||
* Morandi tokens are provided via the 'morandi-design-system' CSS dependency
|
* Morandi tokens are provided via the 'morandi-design-system' CSS dependency
|
||||||
* registered in WPDO_Admin::enqueue_assets(). No @import needed.
|
* registered in TMDO_Admin::enqueue_assets(). No @import needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── KPI Hero Strip (v2.16.0) ────────────────────────────────────────── */
|
/* ── KPI Hero Strip (v2.16.0) ────────────────────────────────────────── */
|
||||||
@@ -512,4 +512,403 @@
|
|||||||
.nav-tab .wpdo-tab-dot {
|
.nav-tab .wpdo-tab-dot {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
.wpdo-bar-fill,
|
||||||
|
.wpdo-eb-bar-fill,
|
||||||
|
.wpdo-setup-progress-fill { transition-duration: 0.01ms; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── v3.0.2 — Inline-style Elimination Utilities ────────────────────── */
|
||||||
|
|
||||||
|
/* Margin / visibility */
|
||||||
|
.wpdo-mt-0 { margin-top: 0; }
|
||||||
|
.wpdo-mt-24 { margin-top: 24px; }
|
||||||
|
.wpdo-hidden { display: none; }
|
||||||
|
|
||||||
|
/* Semantic text colours */
|
||||||
|
.wpdo-text-success { color: #155724; }
|
||||||
|
.wpdo-text-error { color: #721c24; }
|
||||||
|
.wpdo-text-muted { color: #999; }
|
||||||
|
.wpdo-text-warn { color: #856404; }
|
||||||
|
.wpdo-text-success-em { color: #46b450; font-weight: bold; }
|
||||||
|
.wpdo-text-warn-em { color: #dba617; font-weight: bold; }
|
||||||
|
.wpdo-text-neutral { color: #8c8f94; }
|
||||||
|
.wpdo-text-skip { color: #888; font-size: 0.9em; }
|
||||||
|
.wpdo-check-success { color: #155724; font-size: 12px; }
|
||||||
|
|
||||||
|
/* Mode badges — small pill (Entity Bridge status table) */
|
||||||
|
.wpdo-mode-sm {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
/* Mode badges — round pill (Entity Bridge health cards) */
|
||||||
|
.wpdo-mode-pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.wpdo-mode--disabled { background: #e0e0e0; color: #444; }
|
||||||
|
.wpdo-mode--dual-write { background: #d4edda; color: #155724; }
|
||||||
|
.wpdo-mode--shadow-read { background: #fff3cd; color: #856404; }
|
||||||
|
.wpdo-mode--aeav-only { background: #cce5ff; color: #004085; }
|
||||||
|
|
||||||
|
/* Entity Bridge grid */
|
||||||
|
.wpdo-eb-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.wpdo-eb-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,.1);
|
||||||
|
}
|
||||||
|
.wpdo-eb-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.wpdo-eb-title { margin: 0 0 4px; font-size: 16px; }
|
||||||
|
.wpdo-mode-days { margin-left: 8px; font-size: 12px; color: var(--color-text-secondary, #666); }
|
||||||
|
.wpdo-mode-warn-sm { font-size: 12px; color: #856404; background: #fff3cd; padding: 2px 8px; border-radius: 4px; }
|
||||||
|
.wpdo-eb-pipeline { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; font-size: 11px; }
|
||||||
|
.wpdo-pipeline-arrow { color: #999; }
|
||||||
|
.wpdo-stage-pill { padding: 2px 8px; border-radius: 3px; }
|
||||||
|
.wpdo-stage-pill--inactive { background: #f0f0f0; color: #666; }
|
||||||
|
|
||||||
|
.wpdo-eb-tables-row {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.wpdo-eb-tables-label { font-weight: 600; margin-right: 4px; }
|
||||||
|
.wpdo-eb-sep { color: #ccc; }
|
||||||
|
.wpdo-code-xs { font-size: 11px; }
|
||||||
|
|
||||||
|
.wpdo-eb-section { margin-bottom: 14px; }
|
||||||
|
.wpdo-eb-section-title { font-size: 12px; text-transform: uppercase; color: var(--color-text-secondary, #666); letter-spacing: .5px; font-weight: 600; }
|
||||||
|
.wpdo-eb-group { margin-top: 8px; }
|
||||||
|
.wpdo-eb-group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; }
|
||||||
|
.wpdo-eb-group-name { font-size: 13px; font-weight: 500; }
|
||||||
|
.wpdo-eb-group-meta { font-size: 12px; color: var(--color-text-secondary, #666); }
|
||||||
|
.wpdo-eb-group-bar-row { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.wpdo-eb-bar-track { flex: 1; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; }
|
||||||
|
.wpdo-eb-bar-fill { height: 100%; width: var(--wpdo-eb-pct, 0%); background: var(--wpdo-eb-color, #28a745); transition: width .4s ease; border-radius: 4px; }
|
||||||
|
.wpdo-eb-pct-label { font-size: 12px; min-width: 42px; text-align: right; color: #333; }
|
||||||
|
.wpdo-eb-key-hint { font-size: 11px; color: #888; margin-top: 2px; }
|
||||||
|
.wpdo-eb-actions { margin-top: 6px; }
|
||||||
|
|
||||||
|
.wpdo-eb-shadow-row { margin-bottom: 12px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 13px; }
|
||||||
|
.wpdo-eb-shadow-ok { color: #28a745; font-size: 12px; }
|
||||||
|
.wpdo-eb-shadow-warn { color: #dc3545; }
|
||||||
|
|
||||||
|
.wpdo-eb-promote-row { margin-bottom: 12px; font-size: 12px; color: var(--color-text-secondary, #666); }
|
||||||
|
.wpdo-eb-auto-ok { color: #155724; }
|
||||||
|
.wpdo-eb-auto-warn { color: #856404; }
|
||||||
|
.wpdo-eb-auto-badge { background: #d4edda; color: #155724; padding: 1px 5px; border-radius: 3px; margin-left: 4px; font-size: 11px; }
|
||||||
|
|
||||||
|
.wpdo-eb-info-row {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f0f4ff;
|
||||||
|
border-left: 3px solid #4f6ef7;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.wpdo-eb-button-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.wpdo-eb-btn-dim { opacity: .5; }
|
||||||
|
|
||||||
|
.wpdo-eb-footer { margin-top: 24px; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
|
||||||
|
.wpdo-eb-ol { margin-left: 20px; line-height: 1.9; font-size: 13px; }
|
||||||
|
|
||||||
|
/* Stress-test table cells */
|
||||||
|
.wpdo-td { padding: 6px; }
|
||||||
|
.wpdo-td-muted { padding: 6px; color: var(--color-text-secondary, #666); }
|
||||||
|
.wpdo-td-val { padding: 6px; font-weight: 600; }
|
||||||
|
|
||||||
|
.wpdo-stats-table { width: 100%; font-size: 13px; margin-top: 10px; border-collapse: collapse; }
|
||||||
|
|
||||||
|
/* Full-width progress bar */
|
||||||
|
.wpdo-bar-track { height: 14px; background: var(--color-bg-tertiary, #e0e0e0); border-radius: 7px; overflow: hidden; }
|
||||||
|
.wpdo-bar-fill { height: 100%; background: linear-gradient(90deg, #28a745, #20c997); width: var(--wpdo-bar-width, 0%); transition: width 0.4s; }
|
||||||
|
.wpdo-bar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 13px; }
|
||||||
|
.wpdo-bar-pct { font-weight: 600; }
|
||||||
|
.wpdo-bar-section { margin-bottom: 10px; }
|
||||||
|
|
||||||
|
/* Layouts */
|
||||||
|
.wpdo-grid-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
|
||||||
|
.wpdo-grid-4col { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; max-width: 720px; margin: 1rem 0; }
|
||||||
|
|
||||||
|
/* Notices */
|
||||||
|
.wpdo-notice-error {
|
||||||
|
margin: 14px 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.wpdo-notice-warn-inline { margin: 0; font-size: 12px; color: #856404; background: #fff3cd; padding: 6px 10px; border-radius: 4px; line-height: 1.6; }
|
||||||
|
|
||||||
|
/* Count badge */
|
||||||
|
.wpdo-count-danger { font-size: 18px; color: #dc3545; }
|
||||||
|
|
||||||
|
/* Form helpers */
|
||||||
|
.wpdo-th-main { width: 35%; }
|
||||||
|
.wpdo-label-block { display: block; margin-bottom: 6px; }
|
||||||
|
.wpdo-hr-section { margin: 18px 0; }
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.wpdo-list-disc { list-style: disc; padding-left: 1.5em; }
|
||||||
|
.wpdo-list-ol { margin-left: 20px; line-height: 1.9; font-size: 13px; }
|
||||||
|
.wpdo-list-ol-tight { margin: 6px 0 8px 22px; padding: 0; }
|
||||||
|
|
||||||
|
/* Max-width constraints */
|
||||||
|
.wpdo-max-520 { max-width: 520px; }
|
||||||
|
.wpdo-max-600 { max-width: 600px; }
|
||||||
|
.wpdo-max-720 { max-width: 720px; }
|
||||||
|
.wpdo-max-800 { max-width: 800px; }
|
||||||
|
|
||||||
|
/* Dashboard widget */
|
||||||
|
.wpdo-widget-intro { font-size: 1.1em; margin-bottom: 0.6em; }
|
||||||
|
.wpdo-widget-link-right { float: right; padding-top: 4px; }
|
||||||
|
.wpdo-widget-notice { margin: 0.6em 0; padding: 0.6em 0.8em; border-radius: 3px; }
|
||||||
|
.wpdo-widget-notice-warn { background: #fff8e5; border-left: 3px solid #dba617; }
|
||||||
|
.wpdo-widget-notice-error { background: #fef0f0; border-left: 3px solid #dc3232; }
|
||||||
|
.wpdo-widget-notice-info { background: #e5f5fa; border-left: 3px solid #00a0d2; }
|
||||||
|
.wpdo-widget-notice-neutral { background: #f6f7f7; border-left: 3px solid #2271b1; }
|
||||||
|
.wpdo-widget-notice-ok { background: #ecf7ed; border-left: 3px solid #46b450; }
|
||||||
|
|
||||||
|
/* Setup wizard */
|
||||||
|
.wpdo-setup-progress-track { background: #f0f0f1; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 2em; }
|
||||||
|
.wpdo-setup-progress-fill { background: #2271b1; height: 100%; width: var(--wpdo-progress-pct, 0%); transition: width 0.3s; }
|
||||||
|
.wpdo-setup-card-full { max-width: none; padding: 2em; }
|
||||||
|
.wpdo-setup-banner {
|
||||||
|
background: #f0f6fc;
|
||||||
|
border-left: 4px solid #2271b1;
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
.wpdo-setup-banner-btn { white-space: nowrap; }
|
||||||
|
|
||||||
|
/* HivePress / WC tabs */
|
||||||
|
.wpdo-cli-block { background: #f6f7f7; padding: 12px; border-left: 4px solid #2271b1; }
|
||||||
|
.wpdo-opacity-60 { opacity: .6; }
|
||||||
|
|
||||||
|
/* Text colour — strong danger / amber (stress-test status) */
|
||||||
|
.wpdo-text-danger { color: #dc3545; }
|
||||||
|
.wpdo-text-amber { color: #dba617; }
|
||||||
|
|
||||||
|
/* Inline code on white bg (mode-card context) */
|
||||||
|
.wpdo-code-white { background: #fff; padding: 2px 6px; border-radius: 3px; }
|
||||||
|
|
||||||
|
/* Mode info card (stress-test modal-status section) */
|
||||||
|
.wpdo-mode-card {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.wpdo-mode-card--ok { background: #e8f5e9; border-left-color: #28a745; }
|
||||||
|
.wpdo-mode-card--warn { background: #fff3cd; border-left-color: #dba617; }
|
||||||
|
|
||||||
|
/* Settings link margin */
|
||||||
|
.wpdo-ml-2 { margin-left: 8px; }
|
||||||
|
|
||||||
|
/* Matrix / details table helpers */
|
||||||
|
.wpdo-summary-toggle { cursor: pointer; color: #0073aa; font-weight: 600; }
|
||||||
|
.wpdo-table-inset { background: #fff; font-size: 12px; }
|
||||||
|
.wpdo-tr-header th, .wpdo-tr-header td { background: #f0f0f0; }
|
||||||
|
.wpdo-tr-highlight { background: #fff8e1; }
|
||||||
|
.wpdo-tr-success { background: #e8f5e9; font-weight: 600; }
|
||||||
|
|
||||||
|
/* Entity Bridge JS-hook class static styles (admin.php, updated by wpdo-entity-bridge.js) */
|
||||||
|
.wpdo-entity-bridge-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.wpdo-mode-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color var(--duration-normal, 200ms) var(--ease-default, ease),
|
||||||
|
color var(--duration-normal, 200ms) var(--ease-default, ease);
|
||||||
|
}
|
||||||
|
/* JS sets className = 'wpdo-mode-badge wpdo-mode-disabled' etc. */
|
||||||
|
.wpdo-mode-badge.wpdo-mode-disabled { background: #e0e0e0; color: #444; }
|
||||||
|
.wpdo-mode-badge.wpdo-mode-dual-write { background: #d4edda; color: #155724; }
|
||||||
|
.wpdo-mode-badge.wpdo-mode-shadow-read { background: #fff3cd; color: #856404; }
|
||||||
|
.wpdo-mode-badge.wpdo-mode-aeav-only { background: #cce5ff; color: #004085; }
|
||||||
|
|
||||||
|
.wpdo-pipeline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.wpdo-pipeline-dot { padding: 2px 8px; border-radius: 3px; background: #f0f0f0; color: #666; }
|
||||||
|
.wpdo-pipeline-inactive { background: #f0f0f0; color: #666; }
|
||||||
|
|
||||||
|
.wpdo-native-counts {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.wpdo-native-counts-label { font-weight: 600; margin-right: 4px; }
|
||||||
|
.wpdo-native-sep { color: #ccc; }
|
||||||
|
|
||||||
|
.wpdo-mig-status { font-size: 11px; padding: 1px 6px; border-radius: 3px; background: #f0f0f0; }
|
||||||
|
|
||||||
|
.wpdo-cov-pct { font-size: 12px; min-width: 42px; text-align: right; color: #333; }
|
||||||
|
.wpdo-cov-bar-fill { height: 100%; width: var(--wpdo-cov-pct, 0%); background: var(--wpdo-cov-color, #28a745); transition: width .4s ease; border-radius: 4px; }
|
||||||
|
|
||||||
|
.wpdo-recommendation {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f0f4ff;
|
||||||
|
border-left: 3px solid #4f6ef7;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings tab helpers */
|
||||||
|
.wpdo-h3-section { margin-top: 2em; }
|
||||||
|
.wpdo-h4-mb { margin-bottom: 6px; }
|
||||||
|
.wpdo-table-mb { margin-bottom: 14px; }
|
||||||
|
.wpdo-table-mb-sm { margin-bottom: 8px; }
|
||||||
|
.wpdo-pre-code { background: #f0f0f1; padding: 1em; overflow: auto; max-height: 400px; }
|
||||||
|
.wpdo-details-section {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.6em 1em;
|
||||||
|
}
|
||||||
|
.wpdo-label-inline { display: inline-block; margin-right: 1em; margin-bottom: 0.3em; }
|
||||||
|
.wpdo-description-mt { margin-top: 1em; }
|
||||||
|
.wpdo-description-sm { margin-top: 0.25em; }
|
||||||
|
.wpdo-description-xs { margin-top: 0.5em; font-size: 12px; }
|
||||||
|
.wpdo-text-ok { color: #46b450; font-weight: bold; }
|
||||||
|
.wpdo-text-err { color: #dc3232; font-weight: bold; }
|
||||||
|
.wpdo-text-muted { color: #666; font-size: 12px; }
|
||||||
|
.wpdo-text-green { color: #155724; }
|
||||||
|
.wpdo-text-olive { color: #856404; }
|
||||||
|
.wpdo-text-green-bold { color: #28a745; font-weight: 600; }
|
||||||
|
.wpdo-text-red-bold { color: #dc3545; font-weight: 600; }
|
||||||
|
.wpdo-code-sm { margin-left: 8px; font-size: 11px; }
|
||||||
|
.wpdo-badge-mode { display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 3px; font-size: 11px; }
|
||||||
|
|
||||||
|
/* Advisor confidence bar (Classifier tab) */
|
||||||
|
.wpdo-advisor-bar-track {
|
||||||
|
display: inline-block;
|
||||||
|
background: #f0f0f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 80px;
|
||||||
|
height: 18px;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.wpdo-advisor-bar-fill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
background: var(--wpdo-adv-color, #c3c4c7);
|
||||||
|
width: var(--wpdo-adv-width, 0%);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.wpdo-advisor-bar-label {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EB card group section: empty state warning */
|
||||||
|
.wpdo-eb-warn-box {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #856404;
|
||||||
|
background: #fff3cd;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional utilities */
|
||||||
|
.wpdo-text-dark-red { color: #721c24; }
|
||||||
|
.wpdo-text-red { color: #dc3545; }
|
||||||
|
.wpdo-ml-2 { margin-left: 8px; }
|
||||||
|
.wpdo-form-table-mt0 { margin-top: 0; }
|
||||||
|
.wpdo-badge-mode.wpdo-mode-disabled { background: #e0e0e0; color: #444; }
|
||||||
|
.wpdo-badge-mode.wpdo-mode-dual-write { background: #d4edda; color: #155724; }
|
||||||
|
.wpdo-badge-mode.wpdo-mode-shadow-read { background: #fff3cd; color: #856404; }
|
||||||
|
.wpdo-badge-mode.wpdo-mode-aeav-only { background: #cce5ff; color: #004085; }
|
||||||
|
.wpdo-summary-section { cursor: pointer; font-weight: 600; font-size: 1.1em; }
|
||||||
|
.wpdo-notice-warn-desc { margin-top: 8px; color: #856404; background: #fff3cd; padding: 8px 12px; border-radius: 4px; }
|
||||||
|
.wpdo-card--light { background: #f6f7f7; }
|
||||||
|
.wpdo-h3-sm { font-size: 14px; }
|
||||||
|
|
||||||
|
/* ── Settings section cards (task E) ─────────────────────────────── */
|
||||||
|
.wpdo-settings-section {
|
||||||
|
background: var(--color-surface-card, #fbf7f0);
|
||||||
|
border: 1px solid var(--color-border-soft, #e5dccd);
|
||||||
|
border-radius: var(--radius-md, 12px);
|
||||||
|
padding: var(--space-4, 16px) var(--space-6, 24px);
|
||||||
|
margin-bottom: var(--space-5, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpdo-section-save-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3, 12px);
|
||||||
|
padding-top: var(--space-3, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpdo-section-save-status {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-success, #5a8c5a);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--duration-normal, 200ms) var(--ease-default, ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpdo-section-save-status.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline form wrapper — the GET → POST hardening (v1.0.0) turned several
|
||||||
|
action links into single-button forms; they must not break the row flow. */
|
||||||
|
.wpdo-form-inline { display: inline; }
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* WP Data Optimizer — Settings per-section instant save.
|
||||||
|
*
|
||||||
|
* Each .wpdo-settings-section card has a "儲存此區塊" button that posts only
|
||||||
|
* that section's fields via AJAX, updating options without a full page reload.
|
||||||
|
*
|
||||||
|
* @since 3.0.2
|
||||||
|
*/
|
||||||
|
/* global wpdoSettings */
|
||||||
|
( function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if ( typeof wpdoSettings === 'undefined' ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ajaxUrl, nonce, i18n } = wpdoSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect form fields within a section into a FormData object.
|
||||||
|
* Unchecked checkboxes are intentionally omitted (server does isset() ? '1' : '0').
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} section
|
||||||
|
* @returns {FormData}
|
||||||
|
*/
|
||||||
|
function collectFields( section ) {
|
||||||
|
const fd = new FormData();
|
||||||
|
section.querySelectorAll( 'input, select, textarea' ).forEach( ( el ) => {
|
||||||
|
if ( ! el.name ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( el.type === 'checkbox' ) {
|
||||||
|
if ( el.checked ) {
|
||||||
|
fd.append( el.name, el.value );
|
||||||
|
}
|
||||||
|
} else if ( el.type === 'radio' ) {
|
||||||
|
if ( el.checked ) {
|
||||||
|
fd.append( el.name, el.value );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fd.append( el.name, el.value );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a transient status message, then fade it out after 2.5s.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} statusEl
|
||||||
|
* @param {string} message
|
||||||
|
* @param {boolean} isError
|
||||||
|
*/
|
||||||
|
function showStatus( statusEl, message, isError ) {
|
||||||
|
statusEl.textContent = message;
|
||||||
|
statusEl.style.color = isError ? 'var(--color-error, #b03d3d)' : 'var(--color-success, #5a8c5a)';
|
||||||
|
statusEl.classList.add( 'visible' );
|
||||||
|
clearTimeout( statusEl._wpdo_timer );
|
||||||
|
statusEl._wpdo_timer = setTimeout( () => {
|
||||||
|
statusEl.classList.remove( 'visible' );
|
||||||
|
}, 2500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener( 'click', function ( e ) {
|
||||||
|
const btn = e.target.closest( '.wpdo-section-save' );
|
||||||
|
if ( ! btn ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const section = btn.closest( '.wpdo-settings-section' );
|
||||||
|
const statusEl = btn.nextElementSibling;
|
||||||
|
const sectionKey = section ? section.dataset.section : '';
|
||||||
|
if ( ! sectionKey ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalLabel = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = i18n.saving;
|
||||||
|
|
||||||
|
const fd = collectFields( section );
|
||||||
|
fd.append( 'action', 'wpdo_save_settings_section' );
|
||||||
|
fd.append( '_ajax_nonce', nonce );
|
||||||
|
fd.append( 'section', sectionKey );
|
||||||
|
|
||||||
|
fetch( ajaxUrl, {
|
||||||
|
method : 'POST',
|
||||||
|
credentials : 'same-origin',
|
||||||
|
body : fd,
|
||||||
|
} )
|
||||||
|
.then( ( r ) => r.json() )
|
||||||
|
.then( ( data ) => {
|
||||||
|
if ( data.success ) {
|
||||||
|
showStatus( statusEl, i18n.saved, false );
|
||||||
|
} else {
|
||||||
|
showStatus( statusEl, i18n.error, true );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
showStatus( statusEl, i18n.error, true );
|
||||||
|
} )
|
||||||
|
.finally( () => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalLabel;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} )();
|
||||||
+361
-180
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: native postmeta count for dashboard widget
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Dashboard_Widget — wp-admin home dashboard widget (v2.4.0 M9).
|
* TMDO_Dashboard_Widget — wp-admin home dashboard widget (v2.4.0 M9).
|
||||||
*
|
*
|
||||||
@@ -15,6 +14,10 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: native postmeta count for dashboard widget
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -76,7 +79,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
.wpdo-widget-actions a { margin-right: 0.5em; }
|
.wpdo-widget-actions a { margin-right: 0.5em; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<p style="font-size: 1.1em; margin-bottom: 0.6em;">
|
<p class="wpdo-widget-intro">
|
||||||
<span class="wpdo-widget-light <?php echo esc_attr( $light['class'] ); ?>"></span>
|
<span class="wpdo-widget-light <?php echo esc_attr( $light['class'] ); ?>"></span>
|
||||||
<strong><?php echo esc_html( $light['label'] ); ?></strong>
|
<strong><?php echo esc_html( $light['label'] ); ?></strong>
|
||||||
<?php if ( $status['streak'] > 0 ) : ?>
|
<?php if ( $status['streak'] > 0 ) : ?>
|
||||||
@@ -92,7 +95,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php if ( '' !== $status['summary_msg'] ) : ?>
|
<?php if ( '' !== $status['summary_msg'] ) : ?>
|
||||||
<p class="description" style="margin-bottom: 0.8em;"><?php echo esc_html( $status['summary_msg'] ); ?></p>
|
<p class="description wpdo-mb-2"><?php echo esc_html( $status['summary_msg'] ); ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="wpdo-widget-stats">
|
<div class="wpdo-widget-stats">
|
||||||
@@ -110,7 +113,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
<strong><?php echo esc_html( (string) (int) $status['conflicts'] ); ?></strong>
|
<strong><?php echo esc_html( (string) (int) $status['conflicts'] ); ?></strong>
|
||||||
<?php if ( (int) $status['conflicts'] > 0 ) : ?>
|
<?php if ( (int) $status['conflicts'] > 0 ) : ?>
|
||||||
<a href="<?php echo esc_url( add_query_arg( 'tab', 'conflicts', $page_url ) ); ?>"
|
<a href="<?php echo esc_url( add_query_arg( 'tab', 'conflicts', $page_url ) ); ?>"
|
||||||
style="margin-left:0.5em;"><?php esc_html_e( '查看', '2meet-data-optimizer' ); ?></a>
|
class="wpdo-mt-1"><?php esc_html_e( '查看', '2meet-data-optimizer' ); ?></a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +140,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
if ( $actionable_count > 0 ) :
|
if ( $actionable_count > 0 ) :
|
||||||
$ms_url = add_query_arg( 'tab', 'module-suggestions', $page_url );
|
$ms_url = add_query_arg( 'tab', 'module-suggestions', $page_url );
|
||||||
?>
|
?>
|
||||||
<p style="margin: 0.6em 0; padding: 0.5em 0.7em; background: #fff8e5; border-left: 3px solid #dba617; border-radius: 3px;">
|
<p class="wpdo-widget-notice wpdo-widget-notice-warn">
|
||||||
🤖
|
🤖
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
@@ -159,7 +162,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
if ( ! empty( $attn['needs'] ) && 'running' !== ( $attn['job_state'] ?? '' ) ) :
|
if ( ! empty( $attn['needs'] ) && 'running' !== ( $attn['job_state'] ?? '' ) ) :
|
||||||
$wizard_url = add_query_arg( 'tab', 'migration-wizard', $page_url );
|
$wizard_url = add_query_arg( 'tab', 'migration-wizard', $page_url );
|
||||||
?>
|
?>
|
||||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #fef0f0; border-left: 3px solid #dc3232; border-radius: 3px;">
|
<p class="wpdo-widget-notice wpdo-widget-notice-error">
|
||||||
🔴
|
🔴
|
||||||
<?php
|
<?php
|
||||||
/* translators: 1: EAV row count, 2: group count, 3: ratio. */
|
/* translators: 1: EAV row count, 2: group count, 3: ratio. */
|
||||||
@@ -174,7 +177,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
<a href="<?php echo esc_url( $wizard_url ); ?>"><strong><?php esc_html_e( 'User 遷移精靈 →', '2meet-data-optimizer' ); ?></strong></a>
|
<a href="<?php echo esc_url( $wizard_url ); ?>"><strong><?php esc_html_e( 'User 遷移精靈 →', '2meet-data-optimizer' ); ?></strong></a>
|
||||||
</p>
|
</p>
|
||||||
<?php elseif ( 'running' === ( $attn['job_state'] ?? '' ) ) : ?>
|
<?php elseif ( 'running' === ( $attn['job_state'] ?? '' ) ) : ?>
|
||||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #e5f5fa; border-left: 3px solid #00a0d2; border-radius: 3px;">
|
<p class="wpdo-widget-notice wpdo-widget-notice-info">
|
||||||
⏳ <?php esc_html_e( 'User 遷移精靈正在執行中 —', '2meet-data-optimizer' ); ?>
|
⏳ <?php esc_html_e( 'User 遷移精靈正在執行中 —', '2meet-data-optimizer' ); ?>
|
||||||
<a href="<?php echo esc_url( add_query_arg( 'tab', 'migration-wizard', $page_url ) ); ?>"><?php esc_html_e( '查看進度', '2meet-data-optimizer' ); ?></a>
|
<a href="<?php echo esc_url( add_query_arg( 'tab', 'migration-wizard', $page_url ) ); ?>"><?php esc_html_e( '查看進度', '2meet-data-optimizer' ); ?></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -192,7 +195,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
number_format_i18n( (int) $gc['total'] )
|
number_format_i18n( (int) $gc['total'] )
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #f6f7f7; border-left: 3px solid #2271b1; border-radius: 3px;">
|
<p class="wpdo-widget-notice wpdo-widget-notice-neutral">
|
||||||
🧹
|
🧹
|
||||||
<?php
|
<?php
|
||||||
/* translators: 1: total rows, 2: transients, 3: wp_old_date, 4: edit_locks */
|
/* translators: 1: total rows, 2: transients, 3: wp_old_date, 4: edit_locks */
|
||||||
@@ -205,7 +208,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
esc_html( number_format_i18n( (int) $gc['edit_locks'] ) )
|
esc_html( number_format_i18n( (int) $gc['edit_locks'] ) )
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<form method="post" action="<?php echo esc_url( $page_url ); ?>" style="display:inline">
|
<form method="post" action="<?php echo esc_url( $page_url ); ?>" class="wpdo-form-inline">
|
||||||
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
|
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
|
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
|
||||||
<button type="submit" class="button-link"
|
<button type="submit" class="button-link"
|
||||||
@@ -243,7 +246,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
'%2$s'
|
'%2$s'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: <?php echo esc_attr( $post_bg ); ?>; border-left: 3px solid <?php echo esc_attr( $post_color ); ?>; border-radius: 3px;">
|
<p class="wpdo-widget-notice <?php echo $total_eav > 0 ? 'wpdo-widget-notice-warn' : 'wpdo-widget-notice-ok'; ?>">
|
||||||
📦
|
📦
|
||||||
<?php
|
<?php
|
||||||
if ( $total_eav > 0 ) {
|
if ( $total_eav > 0 ) {
|
||||||
@@ -273,7 +276,7 @@ class TMDO_Dashboard_Widget {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="wpdo-widget-actions">
|
<div class="wpdo-widget-actions">
|
||||||
<form method="post" action="<?php echo esc_url( $page_url ); ?>" style="display:inline">
|
<form method="post" action="<?php echo esc_url( $page_url ); ?>" class="wpdo-form-inline">
|
||||||
<input type="hidden" name="wpdo_run_health" value="1">
|
<input type="hidden" name="wpdo_run_health" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_run_health' ); ?>
|
<?php wp_nonce_field( 'wpdo_run_health' ); ?>
|
||||||
<button type="submit" class="button button-small button-primary"><?php esc_html_e( '跑健康檢查', '2meet-data-optimizer' ); ?></button>
|
<button type="submit" class="button button-small button-primary"><?php esc_html_e( '跑健康檢查', '2meet-data-optimizer' ); ?></button>
|
||||||
@@ -281,12 +284,12 @@ class TMDO_Dashboard_Widget {
|
|||||||
<a class="button button-small" href="<?php echo esc_url( $bridge_url ); ?>">
|
<a class="button button-small" href="<?php echo esc_url( $bridge_url ); ?>">
|
||||||
<?php esc_html_e( 'Entity Bridge', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( 'Entity Bridge', '2meet-data-optimizer' ); ?>
|
||||||
</a>
|
</a>
|
||||||
<form method="post" action="<?php echo esc_url( $page_url ); ?>" style="display:inline">
|
<form method="post" action="<?php echo esc_url( $page_url ); ?>" class="wpdo-form-inline">
|
||||||
<input type="hidden" name="wpdo_create_snapshot" value="1">
|
<input type="hidden" name="wpdo_create_snapshot" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_create_snapshot' ); ?>
|
<?php wp_nonce_field( 'wpdo_create_snapshot' ); ?>
|
||||||
<button type="submit" class="button button-small"><?php esc_html_e( '建立快照', '2meet-data-optimizer' ); ?></button>
|
<button type="submit" class="button button-small"><?php esc_html_e( '建立快照', '2meet-data-optimizer' ); ?></button>
|
||||||
</form>
|
</form>
|
||||||
<a href="<?php echo esc_url( $page_url ); ?>" style="float: right; padding-top: 4px;">
|
<a href="<?php echo esc_url( $page_url ); ?>" class="wpdo-widget-link-right">
|
||||||
<?php esc_html_e( '完整儀表板 →', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '完整儀表板 →', '2meet-data-optimizer' ); ?>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,7 @@ class TMDO_Help_Tabs {
|
|||||||
*/
|
*/
|
||||||
private static function content_dashboard(): string {
|
private static function content_dashboard(): string {
|
||||||
return '<p>' . esc_html__( '儀表板顯示:', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( '儀表板顯示:', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ul style="list-style: disc; padding-left: 1.5em;">'
|
. '<ul class="wpdo-list-disc">'
|
||||||
. '<li>' . esc_html__( 'System Overview — DB 引擎、HivePress、HPCT、Object Cache 是否啟用', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( 'System Overview — DB 引擎、HivePress、HPCT、Object Cache 是否啟用', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( 'Zone 行數統計 — Hot/Warm/Cold/Archive 各自累積多少資料', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( 'Zone 行數統計 — Hot/Warm/Cold/Archive 各自累積多少資料', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( 'Module 狀態 — 每個 module 在 7-state FSM 哪一格', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( 'Module 狀態 — 每個 module 在 7-state FSM 哪一格', '2meet-data-optimizer' ) . '</li>'
|
||||||
@@ -121,7 +123,7 @@ class TMDO_Help_Tabs {
|
|||||||
*/
|
*/
|
||||||
private static function content_zones(): string {
|
private static function content_zones(): string {
|
||||||
return '<p>' . esc_html__( '4 個 Zone 對應不同存取頻率與保留需求:', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( '4 個 Zone 對應不同存取頻率與保留需求:', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ul style="list-style: disc; padding-left: 1.5em;">'
|
. '<ul class="wpdo-list-disc">'
|
||||||
. '<li><strong>Hot</strong> — ' . esc_html__( '高頻索引欄位,如 listing 的 price / location。獨立 column + index,WP_Query 可 JOIN。', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>Hot</strong> — ' . esc_html__( '高頻索引欄位,如 listing 的 price / location。獨立 column + index,WP_Query 可 JOIN。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li><strong>Warm</strong> — ' . esc_html__( 'TTL 暫存(如 view count、cache stats)。固定表 wp_wpdo_warm 含 expires_at。', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>Warm</strong> — ' . esc_html__( 'TTL 暫存(如 view count、cache stats)。固定表 wp_wpdo_warm 含 expires_at。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li><strong>Cold</strong> — ' . esc_html__( '低頻 meta(settings / preferences)。讀寫透過 interceptor 攔截後保持 EAV 形式。', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>Cold</strong> — ' . esc_html__( '低頻 meta(settings / preferences)。讀寫透過 interceptor 攔截後保持 EAV 形式。', '2meet-data-optimizer' ) . '</li>'
|
||||||
@@ -137,7 +139,7 @@ class TMDO_Help_Tabs {
|
|||||||
*/
|
*/
|
||||||
private static function content_classifier(): string {
|
private static function content_classifier(): string {
|
||||||
return '<p>' . esc_html__( 'Classifier 分析 wp_postmeta 給每個 meta_key 一個 zone 建議:', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( 'Classifier 分析 wp_postmeta 給每個 meta_key 一個 zone 建議:', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ul style="list-style: disc; padding-left: 1.5em;">'
|
. '<ul class="wpdo-list-disc">'
|
||||||
. '<li><strong>Confidence</strong> — ' . esc_html__( '0.0~1.0,越高代表分類越確定。≥ 0.8 可放心採納,< 0.5 建議 manual review。', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>Confidence</strong> — ' . esc_html__( '0.0~1.0,越高代表分類越確定。≥ 0.8 可放心採納,< 0.5 建議 manual review。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li><strong>Reasons</strong> — ' . esc_html__( '說明為什麼建議這個 zone(access frequency / row count / TTL hints)。', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>Reasons</strong> — ' . esc_html__( '說明為什麼建議這個 zone(access frequency / row count / TTL hints)。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li><strong>Already-assigned</strong> — ' . esc_html__( '已透過 Schema_Registry 註冊的 meta_key 數量。', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>Already-assigned</strong> — ' . esc_html__( '已透過 Schema_Registry 註冊的 meta_key 數量。', '2meet-data-optimizer' ) . '</li>'
|
||||||
@@ -151,7 +153,7 @@ class TMDO_Help_Tabs {
|
|||||||
*/
|
*/
|
||||||
private static function content_snapshots(): string {
|
private static function content_snapshots(): string {
|
||||||
return '<p>' . esc_html__( '快照保留政策(v2.2.0):', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( '快照保留政策(v2.2.0):', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ul style="list-style: disc; padding-left: 1.5em;">'
|
. '<ul class="wpdo-list-disc">'
|
||||||
. '<li>' . esc_html__( '預設 30 天 TTL,可在 wp wpdo snapshot create 時用 --retention-days 覆蓋。', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( '預設 30 天 TTL,可在 wp wpdo snapshot create 時用 --retention-days 覆蓋。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( 'pre_uninstall / pre_v2_upgrade triggers 受 size-cap 保護(不會被自動 evict)。', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( 'pre_uninstall / pre_v2_upgrade triggers 受 size-cap 保護(不會被自動 evict)。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( '檔案存於 wp-content/uploads/wpdo-backups/,含 .htaccess deny all + 每個檔 sha256 校驗。', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( '檔案存於 wp-content/uploads/wpdo-backups/,含 .htaccess deny all + 每個檔 sha256 校驗。', '2meet-data-optimizer' ) . '</li>'
|
||||||
@@ -169,7 +171,7 @@ class TMDO_Help_Tabs {
|
|||||||
private static function content_conflicts(): string {
|
private static function content_conflicts(): string {
|
||||||
return '<p>' . esc_html__( 'Hook 衝突偵測:當多個 plugin 在同一 WordPress 的 metadata filter 上掛 callback 時,可能造成資料寫入順序不確定 / 重複處理。', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( 'Hook 衝突偵測:當多個 plugin 在同一 WordPress 的 metadata filter 上掛 callback 時,可能造成資料寫入順序不確定 / 重複處理。', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<p>' . esc_html__( '常見原因:', '2meet-data-optimizer' ) . '</p>'
|
. '<p>' . esc_html__( '常見原因:', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ul style="list-style: disc; padding-left: 1.5em;">'
|
. '<ul class="wpdo-list-disc">'
|
||||||
. '<li>' . esc_html__( 'Hook Bus 啟用(wpdo_hook_bus_enabled = 1)+ legacy interceptors 還沒卸載', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( 'Hook Bus 啟用(wpdo_hook_bus_enabled = 1)+ legacy interceptors 還沒卸載', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( 'HPCT (HP Custom Tables) plugin 還沒移除 — 與 WPDO 同時攔截', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( 'HPCT (HP Custom Tables) plugin 還沒移除 — 與 WPDO 同時攔截', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '</ul>'
|
. '</ul>'
|
||||||
@@ -183,7 +185,7 @@ class TMDO_Help_Tabs {
|
|||||||
*/
|
*/
|
||||||
private static function content_doctor(): string {
|
private static function content_doctor(): string {
|
||||||
return '<p>' . esc_html__( '7 項自動健康檢查:', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( '7 項自動健康檢查:', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ol style="padding-left: 1.5em;">'
|
. '<ol class="wpdo-list-disc">'
|
||||||
. '<li><strong>schema_drift</strong> — ' . esc_html__( '所有 v2 表是否存在', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>schema_drift</strong> — ' . esc_html__( '所有 v2 表是否存在', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li><strong>error_budget</strong> — ' . esc_html__( '7 天內 wp_wpdo_errors 行數', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>error_budget</strong> — ' . esc_html__( '7 天內 wp_wpdo_errors 行數', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li><strong>hook_conflicts</strong> — ' . esc_html__( '同上 conflicts tab', '2meet-data-optimizer' ) . '</li>'
|
. '<li><strong>hook_conflicts</strong> — ' . esc_html__( '同上 conflicts tab', '2meet-data-optimizer' ) . '</li>'
|
||||||
@@ -202,7 +204,7 @@ class TMDO_Help_Tabs {
|
|||||||
*/
|
*/
|
||||||
private static function content_logs(): string {
|
private static function content_logs(): string {
|
||||||
return '<p>' . esc_html__( '日誌讀取:', '2meet-data-optimizer' ) . '</p>'
|
return '<p>' . esc_html__( '日誌讀取:', '2meet-data-optimizer' ) . '</p>'
|
||||||
. '<ul style="list-style: disc; padding-left: 1.5em;">'
|
. '<ul class="wpdo-list-disc">'
|
||||||
. '<li>' . esc_html__( '每筆對應 wp_wpdo_errors 一行:module / zone / hook / message / timestamp。', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( '每筆對應 wp_wpdo_errors 一行:module / zone / hook / message / timestamp。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( '預設保留 90 天(wpdo_errors_gc daily cron 自動清)。', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( '預設保留 90 天(wpdo_errors_gc daily cron 自動清)。', '2meet-data-optimizer' ) . '</li>'
|
||||||
. '<li>' . esc_html__( '看 message 開頭 [WARN] 是 warning level(不影響運作但需注意)。', '2meet-data-optimizer' ) . '</li>'
|
. '<li>' . esc_html__( '看 message 開頭 [WARN] 是 warning level(不影響運作但需注意)。', '2meet-data-optimizer' ) . '</li>'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: postmeta inventory for setup wizard
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Setup_Wizard — First-run onboarding wizard (v2.3.0 M5).
|
* TMDO_Setup_Wizard — First-run onboarding wizard (v2.3.0 M5).
|
||||||
*
|
*
|
||||||
@@ -20,6 +19,10 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: postmeta inventory for setup wizard
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -125,7 +128,7 @@ class TMDO_Setup_Wizard {
|
|||||||
$step = max( 1, min( self::TOTAL_STEPS, absint( wp_unslash( $_GET['step'] ?? 1 ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
$step = max( 1, min( self::TOTAL_STEPS, absint( wp_unslash( $_GET['step'] ?? 1 ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
$completed = '1' === get_option( self::OPT_COMPLETED );
|
$completed = '1' === get_option( self::OPT_COMPLETED );
|
||||||
?>
|
?>
|
||||||
<div class="wrap" style="max-width: 800px;">
|
<div class="wrap wpdo-max-800">
|
||||||
<p>
|
<p>
|
||||||
<a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
|
<a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
|
||||||
← <?php esc_html_e( '返回 WP Data Optimizer', '2meet-data-optimizer' ); ?>
|
← <?php esc_html_e( '返回 WP Data Optimizer', '2meet-data-optimizer' ); ?>
|
||||||
@@ -134,11 +137,11 @@ class TMDO_Setup_Wizard {
|
|||||||
<h1><?php esc_html_e( 'WP Data Optimizer — 設定嚮導', '2meet-data-optimizer' ); ?></h1>
|
<h1><?php esc_html_e( 'WP Data Optimizer — 設定嚮導', '2meet-data-optimizer' ); ?></h1>
|
||||||
|
|
||||||
<?php if ( $completed ) : ?>
|
<?php if ( $completed ) : ?>
|
||||||
<div style="background:#f0f6fc; border-left:4px solid #2271b1; padding:0.75em 1em; margin-bottom:1em; display:flex; align-items:center; gap:1em;">
|
<div class="wpdo-setup-banner">
|
||||||
<span>✅ <?php esc_html_e( '嚮導已完成。你可以重新瀏覽任何步驟,或重設為全新執行。', '2meet-data-optimizer' ); ?></span>
|
<span>✅ <?php esc_html_e( '嚮導已完成。你可以重新瀏覽任何步驟,或重設為全新執行。', '2meet-data-optimizer' ); ?></span>
|
||||||
<a class="button button-secondary"
|
<a class="button button-secondary wpdo-setup-banner-btn"
|
||||||
href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpdo_wizard_reset' ), self::NONCE_NAME ) ); ?>"
|
href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpdo_wizard_reset' ), self::NONCE_NAME ) ); ?>"
|
||||||
style="white-space:nowrap;">
|
>
|
||||||
<?php esc_html_e( '重新執行精靈', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '重新執行精靈', '2meet-data-optimizer' ); ?>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,17 +154,17 @@ class TMDO_Setup_Wizard {
|
|||||||
<?php esc_html_e( '步', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '步', '2meet-data-optimizer' ); ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="background: #f0f0f1; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 2em;">
|
<div class="wpdo-setup-progress-track">
|
||||||
<div style="background: #2271b1; height: 100%; width: <?php echo (int) ( $step / self::TOTAL_STEPS * 100 ); ?>%; transition: width 0.3s;"></div>
|
<div class="wpdo-setup-progress-fill" style="--wpdo-progress-pct:<?php echo (int) ( $step / self::TOTAL_STEPS * 100 ); ?>%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="max-width: none; padding: 2em;">
|
<div class="card wpdo-setup-card-full">
|
||||||
<?php call_user_func( array( __CLASS__, 'render_step_' . $step ) ); ?>
|
<?php call_user_func( array( __CLASS__, 'render_step_' . $step ) ); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="margin-top: 2em;">
|
<p class="wpdo-mt-3">
|
||||||
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpdo_wizard_dismiss' ), self::NONCE_NAME ) ); ?>"
|
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpdo_wizard_dismiss' ), self::NONCE_NAME ) ); ?>"
|
||||||
style="color: #888; font-size: 0.9em;">
|
class="wpdo-text-skip">
|
||||||
<?php esc_html_e( '我熟悉了,跳過嚮導', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '我熟悉了,跳過嚮導', '2meet-data-optimizer' ); ?>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -182,7 +185,7 @@ class TMDO_Setup_Wizard {
|
|||||||
<p><?php esc_html_e( 'WPDO 解決的是 wp_postmeta 表「meta 爆炸」問題:當 postmeta 累積到數百萬行時,autoload 變大、JOIN 變慢、整站變慢。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( 'WPDO 解決的是 wp_postmeta 表「meta 爆炸」問題:當 postmeta 累積到數百萬行時,autoload 變大、JOIN 變慢、整站變慢。', '2meet-data-optimizer' ); ?></p>
|
||||||
|
|
||||||
<h3><?php esc_html_e( '4 個 Zone 是什麼?', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '4 個 Zone 是什麼?', '2meet-data-optimizer' ); ?></h3>
|
||||||
<table class="widefat" style="margin-bottom: 1em;">
|
<table class="widefat wpdo-mb-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Zone</th><th><?php esc_html_e( '用途', '2meet-data-optimizer' ); ?></th><th><?php esc_html_e( '舉例', '2meet-data-optimizer' ); ?></th></tr>
|
<tr><th>Zone</th><th><?php esc_html_e( '用途', '2meet-data-optimizer' ); ?></th><th><?php esc_html_e( '舉例', '2meet-data-optimizer' ); ?></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -195,13 +198,13 @@ class TMDO_Setup_Wizard {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3><?php esc_html_e( '何時需要 WPDO?', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '何時需要 WPDO?', '2meet-data-optimizer' ); ?></h3>
|
||||||
<ul style="list-style: disc; padding-left: 1.5em;">
|
<ul class="wpdo-list-disc">
|
||||||
<li><?php esc_html_e( '✅ wp_postmeta 行數 > 100k 開始考慮', '2meet-data-optimizer' ); ?></li>
|
<li><?php esc_html_e( '✅ wp_postmeta 行數 > 100k 開始考慮', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><?php esc_html_e( '✅ 行數 > 1M 強烈建議啟用', '2meet-data-optimizer' ); ?></li>
|
<li><?php esc_html_e( '✅ 行數 > 1M 強烈建議啟用', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><?php esc_html_e( '⚠️ 小站台(< 10k 行)可以裝著但別啟用 module', '2meet-data-optimizer' ); ?></li>
|
<li><?php esc_html_e( '⚠️ 小站台(< 10k 行)可以裝著但別啟用 module', '2meet-data-optimizer' ); ?></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" style="margin-top: 2em;">
|
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||||||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||||||
<input type="hidden" name="step" value="2">
|
<input type="hidden" name="step" value="2">
|
||||||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:跑健診 →', '2meet-data-optimizer' ); ?></button>
|
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:跑健診 →', '2meet-data-optimizer' ); ?></button>
|
||||||
@@ -221,18 +224,18 @@ class TMDO_Setup_Wizard {
|
|||||||
$top_keys = $wpdb->get_results( "SELECT meta_key, COUNT(*) AS c FROM `{$wpdb->postmeta}` GROUP BY meta_key ORDER BY c DESC LIMIT 10", ARRAY_A ); // phpcs:ignore WordPress.DB
|
$top_keys = $wpdb->get_results( "SELECT meta_key, COUNT(*) AS c FROM `{$wpdb->postmeta}` GROUP BY meta_key ORDER BY c DESC LIMIT 10", ARRAY_A ); // phpcs:ignore WordPress.DB
|
||||||
?>
|
?>
|
||||||
<h2><?php esc_html_e( '🔬 Baseline 健診', '2meet-data-optimizer' ); ?></h2>
|
<h2><?php esc_html_e( '🔬 Baseline 健診', '2meet-data-optimizer' ); ?></h2>
|
||||||
<table class="widefat striped" style="max-width: 600px; margin-bottom: 1em;">
|
<table class="widefat striped wpdo-max-600 wpdo-mb-2">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><th><?php esc_html_e( 'wp_postmeta 行數', '2meet-data-optimizer' ); ?></th><td><strong><?php echo esc_html( number_format_i18n( $pm_count ) ); ?></strong></td></tr>
|
<tr><th><?php esc_html_e( 'wp_postmeta 行數', '2meet-data-optimizer' ); ?></th><td><strong><?php echo esc_html( number_format_i18n( $pm_count ) ); ?></strong></td></tr>
|
||||||
<tr><th><?php esc_html_e( 'autoload 大小', '2meet-data-optimizer' ); ?></th><td><strong><?php echo esc_html( size_format( $autoload_bytes, 1 ) ); ?></strong></td></tr>
|
<tr><th><?php esc_html_e( 'autoload 大小', '2meet-data-optimizer' ); ?></th><td><strong><?php echo esc_html( size_format( $autoload_bytes, 1 ) ); ?></strong></td></tr>
|
||||||
<tr><th><?php esc_html_e( '建議啟用 WPDO?', '2meet-data-optimizer' ); ?></th>
|
<tr><th><?php esc_html_e( '建議啟用 WPDO?', '2meet-data-optimizer' ); ?></th>
|
||||||
<td>
|
<td>
|
||||||
<?php if ( $pm_count > 1_000_000 ) : ?>
|
<?php if ( $pm_count > 1_000_000 ) : ?>
|
||||||
<span style="color: #46b450; font-weight: bold;">✅ <?php esc_html_e( '強烈建議', '2meet-data-optimizer' ); ?></span>
|
<span class="wpdo-text-success-em">✅ <?php esc_html_e( '強烈建議', '2meet-data-optimizer' ); ?></span>
|
||||||
<?php elseif ( $pm_count > 100_000 ) : ?>
|
<?php elseif ( $pm_count > 100_000 ) : ?>
|
||||||
<span style="color: #dba617; font-weight: bold;">🟡 <?php esc_html_e( '可以考慮', '2meet-data-optimizer' ); ?></span>
|
<span class="wpdo-text-warn-em">🟡 <?php esc_html_e( '可以考慮', '2meet-data-optimizer' ); ?></span>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<span style="color: #8c8f94;">⏸ <?php esc_html_e( '尚不必', '2meet-data-optimizer' ); ?></span>
|
<span class="wpdo-text-neutral">⏸ <?php esc_html_e( '尚不必', '2meet-data-optimizer' ); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -249,7 +252,7 @@ class TMDO_Setup_Wizard {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" style="margin-top: 2em;">
|
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||||||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||||||
<input type="hidden" name="step" value="3">
|
<input type="hidden" name="step" value="3">
|
||||||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:看建議 →', '2meet-data-optimizer' ); ?></button>
|
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:看建議 →', '2meet-data-optimizer' ); ?></button>
|
||||||
@@ -275,7 +278,7 @@ class TMDO_Setup_Wizard {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if ( empty( $actionable ) ) : ?>
|
<?php if ( empty( $actionable ) ) : ?>
|
||||||
<div class="notice notice-info inline" style="padding: 1em;">
|
<div class="notice notice-info inline">
|
||||||
<p><?php esc_html_e( '目前環境暫無高 confidence 的 module 建議。可隨時前往「模組建議」tab 重新檢查。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( '目前環境暫無高 confidence 的 module 建議。可隨時前往「模組建議」tab 重新檢查。', '2meet-data-optimizer' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
@@ -313,13 +316,13 @@ class TMDO_Setup_Wizard {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<h3><?php esc_html_e( '黃金法則', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '黃金法則', '2meet-data-optimizer' ); ?></h3>
|
||||||
<ul style="list-style: disc; padding-left: 1.5em;">
|
<ul class="wpdo-list-disc">
|
||||||
<li><?php esc_html_e( '一次只推進 1 個 module,每階段觀察至少 24-48 小時。', '2meet-data-optimizer' ); ?></li>
|
<li><?php esc_html_e( '一次只推進 1 個 module,每階段觀察至少 24-48 小時。', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><?php esc_html_e( '進 cutover 前一定要有 snapshot(FSM Guard 自動觸發)。', '2meet-data-optimizer' ); ?></li>
|
<li><?php esc_html_e( '進 cutover 前一定要有 snapshot(FSM Guard 自動觸發)。', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><?php esc_html_e( '出事第一件事:rewind 該 module 到 idle(emergency 流程)。', '2meet-data-optimizer' ); ?></li>
|
<li><?php esc_html_e( '出事第一件事:rewind 該 module 到 idle(emergency 流程)。', '2meet-data-optimizer' ); ?></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" style="margin-top: 2em;">
|
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||||||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||||||
<input type="hidden" name="step" value="4">
|
<input type="hidden" name="step" value="4">
|
||||||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:建第一個 snapshot →', '2meet-data-optimizer' ); ?></button>
|
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:建第一個 snapshot →', '2meet-data-optimizer' ); ?></button>
|
||||||
@@ -340,10 +343,10 @@ class TMDO_Setup_Wizard {
|
|||||||
|
|
||||||
<h3><?php esc_html_e( '📦 Snapshot', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '📦 Snapshot', '2meet-data-optimizer' ); ?></h3>
|
||||||
<?php if ( $snapshot_taken ) : ?>
|
<?php if ( $snapshot_taken ) : ?>
|
||||||
<p style="color: #46b450; font-weight: bold;">✅ <?php esc_html_e( '快照已建立。可隨時於「備份快照」tab 查看與管理。', '2meet-data-optimizer' ); ?></p>
|
<p class="wpdo-text-success-em">✅ <?php esc_html_e( '快照已建立。可隨時於「備份快照」tab 查看與管理。', '2meet-data-optimizer' ); ?></p>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<p><?php esc_html_e( '我們會建立一個 baseline snapshot,命名為 wizard_baseline,保留 365 天。即使你日後沒做任何 destructive 操作,這也是一個 known-good 還原點。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( '我們會建立一個 baseline snapshot,命名為 wizard_baseline,保留 365 天。即使你日後沒做任何 destructive 操作,這也是一個 known-good 還原點。', '2meet-data-optimizer' ); ?></p>
|
||||||
<form method="post" action="<?php echo esc_url( admin_url( 'tools.php?page=wpdo-setup-wizard&step=4' ) ); ?>" style="display:inline">
|
<form method="post" action="<?php echo esc_url( admin_url( 'tools.php?page=wpdo-setup-wizard&step=4' ) ); ?>" class="wpdo-form-inline">
|
||||||
<?php wp_nonce_field( self::NONCE_NAME ); ?>
|
<?php wp_nonce_field( self::NONCE_NAME ); ?>
|
||||||
<input type="hidden" name="wpdo_take_snapshot" value="1">
|
<input type="hidden" name="wpdo_take_snapshot" value="1">
|
||||||
<p>
|
<p>
|
||||||
@@ -389,7 +392,7 @@ class TMDO_Setup_Wizard {
|
|||||||
</p>
|
</p>
|
||||||
<p class="description"><?php esc_html_e( 'Cron 會跑 7 項 Site Health 檢查;critical 警告寫入 admin notice + audit log。', '2meet-data-optimizer' ); ?></p>
|
<p class="description"><?php esc_html_e( 'Cron 會跑 7 項 Site Health 檢查;critical 警告寫入 admin notice + audit log。', '2meet-data-optimizer' ); ?></p>
|
||||||
|
|
||||||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" style="margin-top: 2em;">
|
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||||||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||||||
<input type="hidden" name="step" value="5">
|
<input type="hidden" name="step" value="5">
|
||||||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:完成 →', '2meet-data-optimizer' ); ?></button>
|
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:完成 →', '2meet-data-optimizer' ); ?></button>
|
||||||
@@ -412,14 +415,14 @@ class TMDO_Setup_Wizard {
|
|||||||
<h2><?php esc_html_e( '🎉 完成!', '2meet-data-optimizer' ); ?></h2>
|
<h2><?php esc_html_e( '🎉 完成!', '2meet-data-optimizer' ); ?></h2>
|
||||||
<p><?php esc_html_e( 'Setup wizard 已結束。下一步建議:', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( 'Setup wizard 已結束。下一步建議:', '2meet-data-optimizer' ); ?></p>
|
||||||
|
|
||||||
<ul style="list-style: disc; padding-left: 1.5em; line-height: 1.8;">
|
<ul class="wpdo-list-disc">
|
||||||
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=entity-bridge' ) ); ?>"><?php esc_html_e( '🌉 看 Entity Bridge 健康卡片', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( 'user / post / term / comment 4 entity 即時健康+模式狀態', '2meet-data-optimizer' ); ?></li>
|
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=entity-bridge' ) ); ?>"><?php esc_html_e( '🌉 看 Entity Bridge 健康卡片', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( 'user / post / term / comment 4 entity 即時健康+模式狀態', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=migration-wizard' ) ); ?>"><?php esc_html_e( '🪄 開 User / Post 遷移精靈', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '一鍵推進 disabled → dual_write → shadow_read → aeav_only', '2meet-data-optimizer' ); ?></li>
|
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=migration-wizard' ) ); ?>"><?php esc_html_e( '🪄 開 User / Post 遷移精靈', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '一鍵推進 disabled → dual_write → shadow_read → aeav_only', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=classifier' ) ); ?>"><?php esc_html_e( '🔍 跑 Classifier', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '看推薦 zone 配置', '2meet-data-optimizer' ); ?></li>
|
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=classifier' ) ); ?>"><?php esc_html_e( '🔍 跑 Classifier', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '看推薦 zone 配置', '2meet-data-optimizer' ); ?></li>
|
||||||
<li><a href="<?php echo esc_url( admin_url( 'site-health.php' ) ); ?>"><?php esc_html_e( '🏥 WP Site Health', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '7 個 WPDO 健康檢查', '2meet-data-optimizer' ); ?></li>
|
<li><a href="<?php echo esc_url( admin_url( 'site-health.php' ) ); ?>"><?php esc_html_e( '🏥 WP Site Health', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '7 個 WPDO 健康檢查', '2meet-data-optimizer' ); ?></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p style="margin-top: 2em;">
|
<p class="wpdo-mt-3">
|
||||||
<a class="button button-primary button-hero" href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
|
<a class="button button-primary button-hero" href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
|
||||||
<?php esc_html_e( '前往 WPDO 儀表板', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '前往 WPDO 儀表板', '2meet-data-optimizer' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: stress test SQL example display
|
|
||||||
/**
|
/**
|
||||||
* Comment Stress Test template (v2.13.1).
|
* Comment Stress Test template (v2.13.1).
|
||||||
*
|
*
|
||||||
@@ -16,6 +15,10 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: stress test SQL example display
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -25,6 +28,8 @@ $is_running = ( 'running' === $status_str || 'benchmarking' === $status_str );
|
|||||||
$current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'comment' ) : 'disabled';
|
$current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'comment' ) : 'disabled';
|
||||||
$mode_color = 'aeav_only' === $current_mode ? '#28a745' : ( 'shadow_read' === $current_mode ? '#dba617' : '#dc3545' );
|
$mode_color = 'aeav_only' === $current_mode ? '#28a745' : ( 'shadow_read' === $current_mode ? '#dba617' : '#dc3545' );
|
||||||
$mode_optimal = 'aeav_only' === $current_mode;
|
$mode_optimal = 'aeav_only' === $current_mode;
|
||||||
|
$mode_text_cls = 'aeav_only' === $current_mode ? 'wpdo-text-success' : ( 'shadow_read' === $current_mode ? 'wpdo-text-amber' : 'wpdo-text-danger' );
|
||||||
|
$mode_card_cls = $mode_optimal ? 'ok' : 'warn';
|
||||||
$settings_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
|
$settings_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -37,7 +42,7 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
<div class="wpdo-comment-stress-test-tab">
|
<div class="wpdo-comment-stress-test-tab">
|
||||||
<h2><?php esc_html_e( 'Comment Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
|
<h2><?php esc_html_e( 'Comment Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
|
||||||
|
|
||||||
<div style="margin:14px 0;padding:12px 14px;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545;border-radius:4px;font-size:13px;line-height:1.6;">
|
<div class="wpdo-notice-error">
|
||||||
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
|
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
|
||||||
<?php esc_html_e( '會建立大量測試 comment 並 seed 對應 hp_review 群組 keys。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '會建立大量測試 comment 並 seed 對應 hp_review 群組 keys。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,8 +52,8 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Diagnose snapshot -->
|
<!-- Diagnose snapshot -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
|
<div class="wpdo-card wpdo-mt-3">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '當前 Comment Entity 狀態', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '當前 Comment Entity 狀態', '2meet-data-optimizer' ); ?></h3>
|
||||||
<table class="widefat striped">
|
<table class="widefat striped">
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php esc_html_e( 'wp_comments', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'wp_comments', '2meet-data-optimizer' ); ?></td>
|
||||||
@@ -58,7 +63,7 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
|
||||||
<td><strong style="color:<?php echo esc_attr( $mode_color ); ?>;"><?php echo esc_html( $current_mode ); ?></strong></td>
|
<td><strong class="<?php echo esc_attr( $mode_text_cls ); ?>"><?php echo esc_html( $current_mode ); ?></strong></td>
|
||||||
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
|
||||||
<td><strong>1:<?php echo esc_html( (string) ( $total_comments > 0 ? round( $total_commentmeta / $total_comments, 2 ) : 0 ) ); ?></strong></td>
|
<td><strong>1:<?php echo esc_html( (string) ( $total_comments > 0 ? round( $total_commentmeta / $total_comments, 2 ) : 0 ) ); ?></strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -70,14 +75,14 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><?php esc_html_e( 'Stress 測試 comment 數', '2meet-data-optimizer' ); ?></td>
|
<td colspan="2"><?php esc_html_e( 'Stress 測試 comment 數', '2meet-data-optimizer' ); ?></td>
|
||||||
<td colspan="2"><strong id="wpdo-cst-count" style="color:#dc3545;font-size:18px;"><?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?></strong></td>
|
<td colspan="2"><strong id="wpdo-cst-count" class="wpdo-count-danger"><?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?></strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 驗證反 EAV 優化指南 -->
|
<!-- 驗證反 EAV 優化指南 -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:<?php echo $mode_optimal ? '#e8f5e9' : '#fff3cd'; ?>;border-left:4px solid <?php echo esc_attr( $mode_color ); ?>;border-radius:6px;margin-top:16px;font-size:13px;line-height:1.7;">
|
<div class="wpdo-card wpdo-mode-card wpdo-mode-card--<?php echo esc_attr( $mode_card_cls ); ?>">
|
||||||
<h3 style="margin-top:0;font-size:14px;">
|
<h3 class="wpdo-mt-0 wpdo-h3-sm">
|
||||||
<?php if ( $mode_optimal ) : ?>
|
<?php if ( $mode_optimal ) : ?>
|
||||||
✅ <?php esc_html_e( 'Comment Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
|
✅ <?php esc_html_e( 'Comment Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
@@ -86,26 +91,26 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
printf(
|
printf(
|
||||||
/* translators: %s: current mode */
|
/* translators: %s: current mode */
|
||||||
esc_html__( 'Comment Mode = %s — 此模式下壓力測試結果不會展示完整反 EAV 優化效果', '2meet-data-optimizer' ),
|
esc_html__( 'Comment Mode = %s — 此模式下壓力測試結果不會展示完整反 EAV 優化效果', '2meet-data-optimizer' ),
|
||||||
'<code style="background:#fff;padding:2px 6px;border-radius:3px;">' . esc_html( $current_mode ) . '</code>'
|
'<code class="wpdo-code-white">' . esc_html( $current_mode ) . '</code>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h3>
|
</h3>
|
||||||
<p style="margin:8px 0 0 0;">
|
<p class="wpdo-mt-1">
|
||||||
<strong><?php esc_html_e( '想看 wp_commentmeta 真實減量?必須兩條件同時滿足:', '2meet-data-optimizer' ); ?></strong>
|
<strong><?php esc_html_e( '想看 wp_commentmeta 真實減量?必須兩條件同時滿足:', '2meet-data-optimizer' ); ?></strong>
|
||||||
</p>
|
</p>
|
||||||
<ol style="margin:6px 0 8px 22px;padding:0;">
|
<ol class="wpdo-list-ol-tight">
|
||||||
<li>
|
<li>
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
/* translators: 1: open code, 2: close code */
|
/* translators: 1: open code, 2: close code */
|
||||||
esc_html__( 'Comment mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Comment entity)', '2meet-data-optimizer' ),
|
esc_html__( 'Comment mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Comment entity)', '2meet-data-optimizer' ),
|
||||||
'<code style="background:#fff;padding:1px 5px;border-radius:3px;">',
|
'<code class="wpdo-code-white">',
|
||||||
'</code>'
|
'</code>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php if ( ! $mode_optimal ) : ?>
|
<?php if ( ! $mode_optimal ) : ?>
|
||||||
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small" style="margin-left:8px;">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
|
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small wpdo-ml-2">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -114,15 +119,15 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
|
<div class="wpdo-grid-2col">
|
||||||
|
|
||||||
<!-- Left: Configure & Start -->
|
<!-- Left: Configure & Start -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
<div class="wpdo-card">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
|
||||||
|
|
||||||
<table class="form-table" style="margin-top:0;">
|
<table class="form-table wpdo-mt-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:35%;"><label for="wpdo-cst-post-id"><?php esc_html_e( '目標 Post', '2meet-data-optimizer' ); ?></label></th>
|
<th class="wpdo-th-main"><label for="wpdo-cst-post-id"><?php esc_html_e( '目標 Post', '2meet-data-optimizer' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<select id="wpdo-cst-post-id" class="regular-text">
|
<select id="wpdo-cst-post-id" class="regular-text">
|
||||||
<?php if ( empty( $posts ) ) : ?>
|
<?php if ( empty( $posts ) ) : ?>
|
||||||
@@ -148,11 +153,11 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
<tr>
|
<tr>
|
||||||
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
|
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<label style="display:block;margin-bottom:6px;">
|
<label class="wpdo-label-block">
|
||||||
<input type="radio" name="wpdo-cst-mode" value="fast" checked />
|
<input type="radio" name="wpdo-cst-mode" value="fast" checked />
|
||||||
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus)', '2meet-data-optimizer' ); ?>
|
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus)', '2meet-data-optimizer' ); ?>
|
||||||
</label>
|
</label>
|
||||||
<label style="display:block;">
|
<label class="wpdo-label-block">
|
||||||
<input type="radio" name="wpdo-cst-mode" value="realistic" />
|
<input type="radio" name="wpdo-cst-mode" value="realistic" />
|
||||||
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_comment + update_comment_meta(較慢,模擬生產路徑 + 觸發 Hook Bus)', '2meet-data-optimizer' ); ?>
|
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_comment + update_comment_meta(較慢,模擬生產路徑 + 觸發 Hook Bus)', '2meet-data-optimizer' ); ?>
|
||||||
</label>
|
</label>
|
||||||
@@ -178,11 +183,11 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Cleanup + Re-run benchmark -->
|
<!-- Right: Cleanup + Re-run benchmark -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
<div class="wpdo-card">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p>
|
<p>
|
||||||
<?php esc_html_e( '目前 stress test comment 數:', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '目前 stress test comment 數:', '2meet-data-optimizer' ); ?>
|
||||||
<strong id="wpdo-cst-count-mirror" style="font-size:18px;color:#dc3545;">
|
<strong id="wpdo-cst-count-mirror" class="wpdo-count-danger">
|
||||||
<?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?>
|
<?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -195,7 +200,7 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr style="margin:18px 0;" />
|
<hr class="wpdo-hr-section" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="button" id="wpdo-cst-rerun-bench" <?php disabled( $is_running || 0 === $test_comment_count ); ?>>
|
<button type="button" class="button" id="wpdo-cst-rerun-bench" <?php disabled( $is_running || 0 === $test_comment_count ); ?>>
|
||||||
@@ -206,46 +211,46 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress card (live) -->
|
<!-- Progress card (live) -->
|
||||||
<div id="wpdo-cst-progress-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo $is_running ? '' : 'display:none;'; ?>">
|
<div id="wpdo-cst-progress-card" class="wpdo-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
|
||||||
<div style="margin-bottom:10px;">
|
<div class="wpdo-bar-section">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:13px;">
|
<div class="wpdo-bar-header">
|
||||||
<span>
|
<span>
|
||||||
<strong id="wpdo-cst-pg-status"><?php echo esc_html( $status_str ); ?></strong>
|
<strong id="wpdo-cst-pg-status"><?php echo esc_html( $status_str ); ?></strong>
|
||||||
· <code id="wpdo-cst-pg-post-id">post #<?php echo esc_html( (string) ( $state['post_id'] ?? '' ) ); ?></code>
|
· <code id="wpdo-cst-pg-post-id">post #<?php echo esc_html( (string) ( $state['post_id'] ?? '' ) ); ?></code>
|
||||||
· <span id="wpdo-cst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
|
· <span id="wpdo-cst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
|
||||||
</span>
|
</span>
|
||||||
<span id="wpdo-cst-pg-pct" style="font-weight:600;"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
|
<span id="wpdo-cst-pg-pct" class="wpdo-bar-pct"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
|
<div class="wpdo-bar-track">
|
||||||
<div id="wpdo-cst-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%;transition:width .4s;"></div>
|
<div id="wpdo-cst-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
|
<table class="wpdo-stats-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;font-weight:600;"><span id="wpdo-cst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-cst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
|
<td class="wpdo-td-val"><span id="wpdo-cst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-cst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;font-weight:600;"><span id="wpdo-cst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> comments/sec</td>
|
<td class="wpdo-td-val"><span id="wpdo-cst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> comments/sec</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-cst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
|
<td class="wpdo-td"><span id="wpdo-cst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-cst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
|
<td class="wpdo-td"><span id="wpdo-cst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-cst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
|
<td class="wpdo-td"><span id="wpdo-cst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-cst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
|
<td class="wpdo-td"><span id="wpdo-cst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Benchmark report -->
|
<!-- Benchmark report -->
|
||||||
<div id="wpdo-cst-bench-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo ! empty( $state['benchmark'] ) ? '' : 'display:none;'; ?>">
|
<div id="wpdo-cst-bench-card" class="wpdo-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
|
||||||
<div id="wpdo-cst-bench-content"></div>
|
<div id="wpdo-cst-bench-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
* @since 2.8.0
|
* @since 2.8.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -174,7 +176,7 @@ $failed = 'failed' === $state;
|
|||||||
|
|
||||||
<div class="wpdo-mw-progress-bar">
|
<div class="wpdo-mw-progress-bar">
|
||||||
<div class="wpdo-mw-progress-fill" id="wpdo-mw-progress-fill"
|
<div class="wpdo-mw-progress-fill" id="wpdo-mw-progress-fill"
|
||||||
style="width:<?php echo esc_attr( (string) ( $status['overall_progress'] ?? 0 ) ); ?>%"></div>
|
style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $status['overall_progress'] ?? 0 ) ); ?>%"></div>
|
||||||
<span class="wpdo-mw-progress-pct" id="wpdo-mw-progress-pct"><?php echo esc_html( (string) ( $status['overall_progress'] ?? 0 ) ); ?>%</span>
|
<span class="wpdo-mw-progress-pct" id="wpdo-mw-progress-pct"><?php echo esc_html( (string) ( $status['overall_progress'] ?? 0 ) ); ?>%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -213,7 +215,7 @@ $wpdo_format_msg = static function ( string $code ): string {
|
|||||||
?>
|
?>
|
||||||
</p>
|
</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form method="post" style="display:inline">
|
<form method="post">
|
||||||
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
|
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
|
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
|
||||||
<button type="submit" class="button button-primary <?php echo (int) $garbage['total'] > 0 ? '' : 'disabled'; ?>"
|
<button type="submit" class="button button-primary <?php echo (int) $garbage['total'] > 0 ? '' : 'disabled'; ?>"
|
||||||
@@ -228,7 +230,7 @@ $wpdo_format_msg = static function ( string $code ): string {
|
|||||||
<h3><?php esc_html_e( '步驟 2:把 wp_postmeta 既有資料 backfill 至 flat 表(v2.9.3)', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '步驟 2:把 wp_postmeta 既有資料 backfill 至 flat 表(v2.9.3)', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p><?php esc_html_e( 'Idempotent — 重跑安全。每個 group 跑一次 bulk SQL pivot。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( 'Idempotent — 重跑安全。每個 group 跑一次 bulk SQL pivot。', '2meet-data-optimizer' ); ?></p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form method="post" style="display:inline">
|
<form method="post">
|
||||||
<input type="hidden" name="wpdo_post_backfill_all" value="1">
|
<input type="hidden" name="wpdo_post_backfill_all" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_post_backfill_all' ); ?>
|
<?php wp_nonce_field( 'wpdo_post_backfill_all' ); ?>
|
||||||
<button type="submit" class="button button-primary"
|
<button type="submit" class="button button-primary"
|
||||||
@@ -243,7 +245,7 @@ $wpdo_format_msg = static function ( string $code ): string {
|
|||||||
<h3><?php esc_html_e( '步驟 3:把 legacy wpdo_hot_hp_listing 抄到 flat 表(v2.9.5)', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '步驟 3:把 legacy wpdo_hot_hp_listing 抄到 flat 表(v2.9.5)', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p><?php esc_html_e( '非破壞 — legacy hot 表保留作為 v3.0.0 rollback safety net。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( '非破壞 — legacy hot 表保留作為 v3.0.0 rollback safety net。', '2meet-data-optimizer' ); ?></p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form method="post" style="display:inline">
|
<form method="post">
|
||||||
<input type="hidden" name="wpdo_post_cutover_legacy" value="1">
|
<input type="hidden" name="wpdo_post_cutover_legacy" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_post_cutover_legacy' ); ?>
|
<?php wp_nonce_field( 'wpdo_post_cutover_legacy' ); ?>
|
||||||
<button type="submit" class="button"
|
<button type="submit" class="button"
|
||||||
@@ -258,7 +260,7 @@ $wpdo_format_msg = static function ( string $code ): string {
|
|||||||
<h3><?php esc_html_e( '步驟 4:升級 mode 至 dual_write', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '步驟 4:升級 mode 至 dual_write', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p><?php esc_html_e( 'wp_postmeta 與 flat 表同時寫入。讀仍走 wp_postmeta(生產 safe)。建議至少觀察 24h 後再升級下一階。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( 'wp_postmeta 與 flat 表同時寫入。讀仍走 wp_postmeta(生產 safe)。建議至少觀察 24h 後再升級下一階。', '2meet-data-optimizer' ); ?></p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form method="post" style="display:inline">
|
<form method="post">
|
||||||
<input type="hidden" name="wpdo_post_promote_dual_write" value="1">
|
<input type="hidden" name="wpdo_post_promote_dual_write" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_post_promote_dual_write' ); ?>
|
<?php wp_nonce_field( 'wpdo_post_promote_dual_write' ); ?>
|
||||||
<button type="submit" class="button"
|
<button type="submit" class="button"
|
||||||
@@ -273,7 +275,7 @@ $wpdo_format_msg = static function ( string $code ): string {
|
|||||||
<h3><?php esc_html_e( '步驟 5:升級 mode 至 aeav_only(最終 cutover)', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '步驟 5:升級 mode 至 aeav_only(最終 cutover)', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p><?php esc_html_e( 'flat 表成為 source-of-truth,讀寫都走 flat。完成後可 wp wpdo post-cleanup --confirm 清掉 wp_postmeta 已遷移 keys。', '2meet-data-optimizer' ); ?></p>
|
<p><?php esc_html_e( 'flat 表成為 source-of-truth,讀寫都走 flat。完成後可 wp wpdo post-cleanup --confirm 清掉 wp_postmeta 已遷移 keys。', '2meet-data-optimizer' ); ?></p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form method="post" style="display:inline">
|
<form method="post">
|
||||||
<input type="hidden" name="wpdo_post_promote_aeav" value="1">
|
<input type="hidden" name="wpdo_post_promote_aeav" value="1">
|
||||||
<?php wp_nonce_field( 'wpdo_post_promote_aeav' ); ?>
|
<?php wp_nonce_field( 'wpdo_post_promote_aeav' ); ?>
|
||||||
<button type="submit" class="button"
|
<button type="submit" class="button"
|
||||||
@@ -284,7 +286,7 @@ $wpdo_format_msg = static function ( string $code ): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wpdo-card" style="background: #f6f7f7;">
|
<div class="wpdo-card wpdo-card--light">
|
||||||
<h3><?php esc_html_e( '回退路徑(rollback)', '2meet-data-optimizer' ); ?></h3>
|
<h3><?php esc_html_e( '回退路徑(rollback)', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p>
|
<p>
|
||||||
<?php esc_html_e( '若任一步驟出問題,可下降 mode:', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '若任一步驟出問題,可下降 mode:', '2meet-data-optimizer' ); ?>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -63,7 +65,7 @@ $post_type_options = array(
|
|||||||
<div class="wpdo-post-stress-test-tab">
|
<div class="wpdo-post-stress-test-tab">
|
||||||
<h2><?php esc_html_e( 'Post Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
|
<h2><?php esc_html_e( 'Post Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
|
||||||
|
|
||||||
<div style="margin:14px 0;padding:12px 14px;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545;border-radius:4px;font-size:13px;line-height:1.6;">
|
<div class="wpdo-notice-error">
|
||||||
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
|
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
|
||||||
<?php esc_html_e( '會建立大量測試 post 並填滿對應 wp_postmeta + flat 表。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '會建立大量測試 post 並填滿對應 wp_postmeta + flat 表。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,10 +85,12 @@ $post_type_options = array(
|
|||||||
$current_post_mode = (string) ( $diagnose['mode'] ?? 'disabled' );
|
$current_post_mode = (string) ( $diagnose['mode'] ?? 'disabled' );
|
||||||
$mode_color = 'aeav_only' === $current_post_mode ? '#28a745' : '#dc3545';
|
$mode_color = 'aeav_only' === $current_post_mode ? '#28a745' : '#dc3545';
|
||||||
$mode_optimal = 'aeav_only' === $current_post_mode;
|
$mode_optimal = 'aeav_only' === $current_post_mode;
|
||||||
|
$mode_text_cls = $mode_optimal ? 'wpdo-text-success' : 'wpdo-text-danger';
|
||||||
|
$mode_card_cls = $mode_optimal ? 'ok' : 'warn';
|
||||||
$settings_tab_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
|
$settings_tab_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
|
||||||
?>
|
?>
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
|
<div class="wpdo-card wpdo-mt-3">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '當前 Post Entity 狀態', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '當前 Post Entity 狀態', '2meet-data-optimizer' ); ?></h3>
|
||||||
<table class="widefat striped">
|
<table class="widefat striped">
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php esc_html_e( 'Posts', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Posts', '2meet-data-optimizer' ); ?></td>
|
||||||
@@ -98,18 +102,18 @@ $post_type_options = array(
|
|||||||
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
|
||||||
<td><strong>1:<?php echo esc_html( (string) $diagnose['ratio'] ); ?></strong></td>
|
<td><strong>1:<?php echo esc_html( (string) $diagnose['ratio'] ); ?></strong></td>
|
||||||
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
|
||||||
<td><strong style="color:<?php echo esc_attr( $mode_color ); ?>;"><?php echo esc_html( $current_post_mode ); ?></strong></td>
|
<td><strong class="<?php echo esc_attr( $mode_text_cls ); ?>"><?php echo esc_html( $current_post_mode ); ?></strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><?php esc_html_e( 'Stress 測試 post 數', '2meet-data-optimizer' ); ?></td>
|
<td colspan="2"><?php esc_html_e( 'Stress 測試 post 數', '2meet-data-optimizer' ); ?></td>
|
||||||
<td colspan="2"><strong id="wpdo-pst-count" style="color:#dc3545;font-size:18px;"><?php echo esc_html( number_format_i18n( $test_post_count ) ); ?></strong></td>
|
<td colspan="2"><strong id="wpdo-pst-count" class="wpdo-count-danger"><?php echo esc_html( number_format_i18n( $test_post_count ) ); ?></strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 驗證反 EAV 優化指南 -->
|
<!-- 驗證反 EAV 優化指南 -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:<?php echo $mode_optimal ? '#e8f5e9' : '#fff3cd'; ?>;border-left:4px solid <?php echo esc_attr( $mode_color ); ?>;border-radius:6px;margin-top:16px;font-size:13px;line-height:1.7;">
|
<div class="wpdo-card wpdo-mode-card wpdo-mode-card--<?php echo esc_attr( $mode_card_cls ); ?>">
|
||||||
<h3 style="margin-top:0;font-size:14px;">
|
<h3 class="wpdo-mt-0 wpdo-h3-sm">
|
||||||
<?php if ( $mode_optimal ) : ?>
|
<?php if ( $mode_optimal ) : ?>
|
||||||
✅ <?php esc_html_e( 'Post Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
|
✅ <?php esc_html_e( 'Post Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
@@ -118,27 +122,27 @@ $post_type_options = array(
|
|||||||
printf(
|
printf(
|
||||||
/* translators: %s: current mode */
|
/* translators: %s: current mode */
|
||||||
esc_html__( 'Post Mode = %s — 此模式下壓力測試結果不會展示反 EAV 優化效果', '2meet-data-optimizer' ),
|
esc_html__( 'Post Mode = %s — 此模式下壓力測試結果不會展示反 EAV 優化效果', '2meet-data-optimizer' ),
|
||||||
'<code style="background:#fff;padding:2px 6px;border-radius:3px;">' . esc_html( $current_post_mode ) . '</code>'
|
'<code class="wpdo-code-white">' . esc_html( $current_post_mode ) . '</code>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p style="margin:8px 0 0 0;">
|
<p class="wpdo-mt-1">
|
||||||
<strong><?php esc_html_e( '想看 wp_postmeta 真實減量?必須同時滿足兩個條件:', '2meet-data-optimizer' ); ?></strong>
|
<strong><?php esc_html_e( '想看 wp_postmeta 真實減量?必須同時滿足兩個條件:', '2meet-data-optimizer' ); ?></strong>
|
||||||
</p>
|
</p>
|
||||||
<ol style="margin:6px 0 8px 22px;padding:0;">
|
<ol class="wpdo-list-ol-tight">
|
||||||
<li>
|
<li>
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
/* translators: 1: settings tab anchor open, 2: settings tab anchor close */
|
/* translators: 1: settings tab anchor open, 2: settings tab anchor close */
|
||||||
esc_html__( 'Post mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Post entity)', '2meet-data-optimizer' ),
|
esc_html__( 'Post mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Post entity)', '2meet-data-optimizer' ),
|
||||||
'<code style="background:#fff;padding:1px 5px;border-radius:3px;">',
|
'<code class="wpdo-code-white">',
|
||||||
'</code>'
|
'</code>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php if ( ! $mode_optimal ) : ?>
|
<?php if ( ! $mode_optimal ) : ?>
|
||||||
<a href="<?php echo esc_url( $settings_tab_url ); ?>" class="button button-small" style="margin-left:8px;">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
|
<a href="<?php echo esc_url( $settings_tab_url ); ?>" class="button button-small wpdo-ml-2">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -146,54 +150,54 @@ $post_type_options = array(
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<details style="margin-top:8px;">
|
<details class="wpdo-mt-1">
|
||||||
<summary style="cursor:pointer;color:#0073aa;font-weight:600;"><?php esc_html_e( '📐 模式 × 寫入路徑 → 預期結果矩陣', '2meet-data-optimizer' ); ?></summary>
|
<summary class="wpdo-summary-toggle"><?php esc_html_e( '📐 模式 × 寫入路徑 → 預期結果矩陣', '2meet-data-optimizer' ); ?></summary>
|
||||||
<table class="widefat" style="margin-top:8px;background:#fff;font-size:12px;">
|
<table class="widefat wpdo-mt-1 wpdo-table-inset">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background:#f0f0f0;">
|
<tr class="wpdo-tr-header">
|
||||||
<th><?php esc_html_e( 'Post Mode', '2meet-data-optimizer' ); ?></th>
|
<th><?php esc_html_e( 'Post Mode', '2meet-data-optimizer' ); ?></th>
|
||||||
<th>⚡ Fast</th>
|
<th>⚡ Fast</th>
|
||||||
<th>🐢 Realistic</th>
|
<th>🐢 Realistic</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr<?php echo 'disabled' === $current_post_mode ? ' style="background:#fff8e1;"' : ''; ?>>
|
<tr class="<?php echo 'disabled' === $current_post_mode ? 'wpdo-tr-highlight' : ''; ?>">
|
||||||
<td><code>disabled</code></td>
|
<td><code>disabled</code></td>
|
||||||
<td>wp_postmeta 5 rows / flat 0 → <strong>1:5(無優化)</strong></td>
|
<td>wp_postmeta 5 rows / flat 0 → <strong>1:5(無優化)</strong></td>
|
||||||
<td>wp_postmeta 5 / flat 0 → 1:5(無優化)</td>
|
<td>wp_postmeta 5 / flat 0 → 1:5(無優化)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr<?php echo 'dual_write' === $current_post_mode ? ' style="background:#fff8e1;"' : ''; ?>>
|
<tr class="<?php echo 'dual_write' === $current_post_mode ? 'wpdo-tr-highlight' : ''; ?>">
|
||||||
<td><code>dual_write</code></td>
|
<td><code>dual_write</code></td>
|
||||||
<td>wp_postmeta 5 / flat 0 → 1:5</td>
|
<td>wp_postmeta 5 / flat 0 → 1:5</td>
|
||||||
<td>wp_postmeta 5 + flat 1 → 1:5(有 flat 但 wp_postmeta 不減)</td>
|
<td>wp_postmeta 5 + flat 1 → 1:5(有 flat 但 wp_postmeta 不減)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr<?php echo 'shadow_read' === $current_post_mode ? ' style="background:#fff8e1;"' : ''; ?>>
|
<tr class="<?php echo 'shadow_read' === $current_post_mode ? 'wpdo-tr-highlight' : ''; ?>">
|
||||||
<td><code>shadow_read</code></td>
|
<td><code>shadow_read</code></td>
|
||||||
<td>wp_postmeta 5 / flat 0 → 1:5</td>
|
<td>wp_postmeta 5 / flat 0 → 1:5</td>
|
||||||
<td>wp_postmeta 5 + flat 1 → 1:5(讀走 flat,寫仍雙寫)</td>
|
<td>wp_postmeta 5 + flat 1 → 1:5(讀走 flat,寫仍雙寫)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr<?php echo 'aeav_only' === $current_post_mode ? ' style="background:#e8f5e9;font-weight:600;"' : ''; ?>>
|
<tr class="<?php echo 'aeav_only' === $current_post_mode ? 'wpdo-tr-success' : ''; ?>">
|
||||||
<td><code>aeav_only</code></td>
|
<td><code>aeav_only</code></td>
|
||||||
<td>wp_postmeta 5(直 SQL 繞過 Hook Bus)/ flat 0 → 1:5 ⚠️</td>
|
<td>wp_postmeta 5(直 SQL 繞過 Hook Bus)/ flat 0 → 1:5 ⚠️</td>
|
||||||
<td>wp_postmeta <strong>0</strong> / flat 1 → <strong>0:1(完全優化)</strong> ✅</td>
|
<td>wp_postmeta <strong>0</strong> / flat 1 → <strong>0:1(完全優化)</strong> ✅</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="description" style="margin-top:6px;">
|
<p class="description wpdo-mt-1">
|
||||||
<?php esc_html_e( '範例以 nav_menu_item(5 keys/post)為基準。Fast 模式直接 $wpdb->insert,故意繞過 Hook Bus → 即使 mode=aeav_only 也會寫滿 wp_postmeta(用途:快速灌 fixture 給 Query Router benchmark)。驗證反 EAV 優化效果一律用 Realistic。', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '範例以 nav_menu_item(5 keys/post)為基準。Fast 模式直接 $wpdb->insert,故意繞過 Hook Bus → 即使 mode=aeav_only 也會寫滿 wp_postmeta(用途:快速灌 fixture 給 Query Router benchmark)。驗證反 EAV 優化效果一律用 Realistic。', '2meet-data-optimizer' ); ?>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
|
<div class="wpdo-grid-2col">
|
||||||
|
|
||||||
<!-- Left: Configure & Start -->
|
<!-- Left: Configure & Start -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
<div class="wpdo-card">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
|
||||||
|
|
||||||
<table class="form-table" style="margin-top:0;">
|
<table class="form-table wpdo-mt-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:35%;"><label for="wpdo-pst-post-type"><?php esc_html_e( 'Post Type', '2meet-data-optimizer' ); ?></label></th>
|
<th class="wpdo-th-main"><label for="wpdo-pst-post-type"><?php esc_html_e( 'Post Type', '2meet-data-optimizer' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<select id="wpdo-pst-post-type" class="regular-text">
|
<select id="wpdo-pst-post-type" class="regular-text">
|
||||||
<?php foreach ( $post_type_options as $pt => $info ) : ?>
|
<?php foreach ( $post_type_options as $pt => $info ) : ?>
|
||||||
@@ -226,11 +230,11 @@ $post_type_options = array(
|
|||||||
<tr>
|
<tr>
|
||||||
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
|
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<label style="display:block;margin-bottom:6px;">
|
<label class="wpdo-label-block">
|
||||||
<input type="radio" name="wpdo-pst-mode" value="fast" checked />
|
<input type="radio" name="wpdo-pst-mode" value="fast" checked />
|
||||||
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus)', '2meet-data-optimizer' ); ?>
|
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus)', '2meet-data-optimizer' ); ?>
|
||||||
</label>
|
</label>
|
||||||
<label style="display:block;">
|
<label class="wpdo-label-block">
|
||||||
<input type="radio" name="wpdo-pst-mode" value="realistic" />
|
<input type="radio" name="wpdo-pst-mode" value="realistic" />
|
||||||
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_post + update_post_meta(較慢,模擬生產路徑 + 觸發 Hook Bus → mode=dual_write+ 時 flat 表自動填入)', '2meet-data-optimizer' ); ?>
|
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_post + update_post_meta(較慢,模擬生產路徑 + 觸發 Hook Bus → mode=dual_write+ 時 flat 表自動填入)', '2meet-data-optimizer' ); ?>
|
||||||
</label>
|
</label>
|
||||||
@@ -256,11 +260,11 @@ $post_type_options = array(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Cleanup + Re-run benchmark -->
|
<!-- Right: Cleanup + Re-run benchmark -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
<div class="wpdo-card">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p>
|
<p>
|
||||||
<?php esc_html_e( '目前 stress test post 數:', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '目前 stress test post 數:', '2meet-data-optimizer' ); ?>
|
||||||
<strong id="wpdo-pst-count-mirror" style="font-size:18px;color:#dc3545;">
|
<strong id="wpdo-pst-count-mirror" class="wpdo-count-danger">
|
||||||
<?php echo esc_html( number_format_i18n( $test_post_count ) ); ?>
|
<?php echo esc_html( number_format_i18n( $test_post_count ) ); ?>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -273,7 +277,7 @@ $post_type_options = array(
|
|||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr style="margin:18px 0;" />
|
<hr class="wpdo-hr-section" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="button" id="wpdo-pst-rerun-bench" <?php disabled( $is_running || 0 === $test_post_count ); ?>>
|
<button type="button" class="button" id="wpdo-pst-rerun-bench" <?php disabled( $is_running || 0 === $test_post_count ); ?>>
|
||||||
@@ -287,52 +291,52 @@ $post_type_options = array(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress section (live) -->
|
<!-- Progress section (live) -->
|
||||||
<div id="wpdo-pst-progress-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo $is_running ? '' : 'display:none;'; ?>">
|
<div id="wpdo-pst-progress-card" class="wpdo-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
|
||||||
<div style="margin-bottom:10px;">
|
<div class="wpdo-bar-section">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:13px;">
|
<div class="wpdo-bar-header">
|
||||||
<span>
|
<span>
|
||||||
<strong id="wpdo-pst-pg-status"><?php echo esc_html( $wpdo_pst_stat ); ?></strong>
|
<strong id="wpdo-pst-pg-status"><?php echo esc_html( $wpdo_pst_stat ); ?></strong>
|
||||||
· <code id="wpdo-pst-pg-post-type"><?php echo esc_html( (string) ( $state['post_type'] ?? '' ) ); ?></code>
|
· <code id="wpdo-pst-pg-post-type"><?php echo esc_html( (string) ( $state['post_type'] ?? '' ) ); ?></code>
|
||||||
· <span id="wpdo-pst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
|
· <span id="wpdo-pst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
|
||||||
</span>
|
</span>
|
||||||
<span id="wpdo-pst-pg-pct" style="font-weight:600;"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
|
<span id="wpdo-pst-pg-pct" class="wpdo-bar-pct"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
|
<div class="wpdo-bar-track">
|
||||||
<div id="wpdo-pst-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%;transition:width .4s;"></div>
|
<div id="wpdo-pst-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
|
<table class="wpdo-stats-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;font-weight:600;"><span id="wpdo-pst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-pst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
|
<td class="wpdo-td-val"><span id="wpdo-pst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-pst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;font-weight:600;"><span id="wpdo-pst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> posts/sec</td>
|
<td class="wpdo-td-val"><span id="wpdo-pst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> posts/sec</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-pst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
|
<td class="wpdo-td"><span id="wpdo-pst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-pst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
|
<td class="wpdo-td"><span id="wpdo-pst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-pst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
|
<td class="wpdo-td"><span id="wpdo-pst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-pst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
|
<td class="wpdo-td"><span id="wpdo-pst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Benchmark report -->
|
<!-- Benchmark report -->
|
||||||
<div id="wpdo-pst-bench-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo ! empty( $state['benchmark'] ) ? '' : 'display:none;'; ?>">
|
<div id="wpdo-pst-bench-card" class="wpdo-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
|
||||||
<div id="wpdo-pst-bench-content"></div>
|
<div id="wpdo-pst-bench-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 7-group seed map info -->
|
<!-- 7-group seed map info -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
|
<div class="wpdo-card wpdo-mt-3">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '4. TMDO_Post_Stress_Tester seed map(7 個 post_type)', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '4. TMDO_Post_Stress_Tester seed map(7 個 post_type)', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<?php esc_html_e( '每個 stress post 自動 seed 對應 group 的 canonical meta keys(v2.9.1 entity registry 定義)。', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '每個 stress post 自動 seed 對應 group 的 canonical meta keys(v2.9.1 entity registry 定義)。', '2meet-data-optimizer' ); ?>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: stress test SQL example display
|
|
||||||
/**
|
/**
|
||||||
* Term Stress Test template (v2.13.0).
|
* Term Stress Test template (v2.13.0).
|
||||||
*
|
*
|
||||||
@@ -16,6 +15,10 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: stress test SQL example display
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -25,6 +28,8 @@ $is_running = ( 'running' === $status_str || 'benchmarking' === $status_str );
|
|||||||
$current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'term' ) : 'disabled';
|
$current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'term' ) : 'disabled';
|
||||||
$mode_color = 'aeav_only' === $current_mode ? '#28a745' : ( 'shadow_read' === $current_mode ? '#dba617' : '#dc3545' );
|
$mode_color = 'aeav_only' === $current_mode ? '#28a745' : ( 'shadow_read' === $current_mode ? '#dba617' : '#dc3545' );
|
||||||
$mode_optimal = 'aeav_only' === $current_mode;
|
$mode_optimal = 'aeav_only' === $current_mode;
|
||||||
|
$mode_text_cls = 'aeav_only' === $current_mode ? 'wpdo-text-success' : ( 'shadow_read' === $current_mode ? 'wpdo-text-amber' : 'wpdo-text-danger' );
|
||||||
|
$mode_card_cls = $mode_optimal ? 'ok' : 'warn';
|
||||||
$settings_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
|
$settings_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -37,7 +42,7 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
<div class="wpdo-term-stress-test-tab">
|
<div class="wpdo-term-stress-test-tab">
|
||||||
<h2><?php esc_html_e( 'Term Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
|
<h2><?php esc_html_e( 'Term Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
|
||||||
|
|
||||||
<div style="margin:14px 0;padding:12px 14px;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545;border-radius:4px;font-size:13px;line-height:1.6;">
|
<div class="wpdo-notice-error">
|
||||||
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
|
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
|
||||||
<?php esc_html_e( '會建立大量測試 term 並 seed 對應 hp_taxonomy 群組 keys。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '會建立大量測試 term 並 seed 對應 hp_taxonomy 群組 keys。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,8 +52,8 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Diagnose snapshot -->
|
<!-- Diagnose snapshot -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
|
<div class="wpdo-card wpdo-mt-3">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '當前 Term Entity 狀態', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '當前 Term Entity 狀態', '2meet-data-optimizer' ); ?></h3>
|
||||||
<table class="widefat striped">
|
<table class="widefat striped">
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php esc_html_e( 'wp_terms', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'wp_terms', '2meet-data-optimizer' ); ?></td>
|
||||||
@@ -58,7 +63,7 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
|
||||||
<td><strong style="color:<?php echo esc_attr( $mode_color ); ?>;"><?php echo esc_html( $current_mode ); ?></strong></td>
|
<td><strong class="<?php echo esc_attr( $mode_text_cls ); ?>"><?php echo esc_html( $current_mode ); ?></strong></td>
|
||||||
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
|
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
|
||||||
<td><strong>1:<?php echo esc_html( (string) ( $total_terms > 0 ? round( $total_termmeta / $total_terms, 2 ) : 0 ) ); ?></strong></td>
|
<td><strong>1:<?php echo esc_html( (string) ( $total_terms > 0 ? round( $total_termmeta / $total_terms, 2 ) : 0 ) ); ?></strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -70,14 +75,14 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><?php esc_html_e( 'Stress 測試 term 數', '2meet-data-optimizer' ); ?></td>
|
<td colspan="2"><?php esc_html_e( 'Stress 測試 term 數', '2meet-data-optimizer' ); ?></td>
|
||||||
<td colspan="2"><strong id="wpdo-tst-count" style="color:#dc3545;font-size:18px;"><?php echo esc_html( number_format_i18n( $test_term_count ) ); ?></strong></td>
|
<td colspan="2"><strong id="wpdo-tst-count" class="wpdo-count-danger"><?php echo esc_html( number_format_i18n( $test_term_count ) ); ?></strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 驗證反 EAV 優化指南 -->
|
<!-- 驗證反 EAV 優化指南 -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:<?php echo $mode_optimal ? '#e8f5e9' : '#fff3cd'; ?>;border-left:4px solid <?php echo esc_attr( $mode_color ); ?>;border-radius:6px;margin-top:16px;font-size:13px;line-height:1.7;">
|
<div class="wpdo-card wpdo-mode-card wpdo-mode-card--<?php echo esc_attr( $mode_card_cls ); ?>">
|
||||||
<h3 style="margin-top:0;font-size:14px;">
|
<h3 class="wpdo-mt-0 wpdo-h3-sm">
|
||||||
<?php if ( $mode_optimal ) : ?>
|
<?php if ( $mode_optimal ) : ?>
|
||||||
✅ <?php esc_html_e( 'Term Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
|
✅ <?php esc_html_e( 'Term Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
@@ -86,26 +91,26 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
printf(
|
printf(
|
||||||
/* translators: %s: current mode */
|
/* translators: %s: current mode */
|
||||||
esc_html__( 'Term Mode = %s — 此模式下壓力測試結果不會展示完整反 EAV 優化效果', '2meet-data-optimizer' ),
|
esc_html__( 'Term Mode = %s — 此模式下壓力測試結果不會展示完整反 EAV 優化效果', '2meet-data-optimizer' ),
|
||||||
'<code style="background:#fff;padding:2px 6px;border-radius:3px;">' . esc_html( $current_mode ) . '</code>'
|
'<code class="wpdo-code-white">' . esc_html( $current_mode ) . '</code>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h3>
|
</h3>
|
||||||
<p style="margin:8px 0 0 0;">
|
<p class="wpdo-mt-1">
|
||||||
<strong><?php esc_html_e( '想看 wp_termmeta 真實減量?必須兩條件同時滿足:', '2meet-data-optimizer' ); ?></strong>
|
<strong><?php esc_html_e( '想看 wp_termmeta 真實減量?必須兩條件同時滿足:', '2meet-data-optimizer' ); ?></strong>
|
||||||
</p>
|
</p>
|
||||||
<ol style="margin:6px 0 8px 22px;padding:0;">
|
<ol class="wpdo-list-ol-tight">
|
||||||
<li>
|
<li>
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
/* translators: 1: open code, 2: close code */
|
/* translators: 1: open code, 2: close code */
|
||||||
esc_html__( 'Term mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Term entity)', '2meet-data-optimizer' ),
|
esc_html__( 'Term mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Term entity)', '2meet-data-optimizer' ),
|
||||||
'<code style="background:#fff;padding:1px 5px;border-radius:3px;">',
|
'<code class="wpdo-code-white">',
|
||||||
'</code>'
|
'</code>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php if ( ! $mode_optimal ) : ?>
|
<?php if ( ! $mode_optimal ) : ?>
|
||||||
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small" style="margin-left:8px;">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
|
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small wpdo-ml-2">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -114,15 +119,15 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
|
<div class="wpdo-grid-2col">
|
||||||
|
|
||||||
<!-- Left: Configure & Start -->
|
<!-- Left: Configure & Start -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
<div class="wpdo-card">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
|
||||||
|
|
||||||
<table class="form-table" style="margin-top:0;">
|
<table class="form-table wpdo-mt-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:35%;"><label for="wpdo-tst-taxonomy"><?php esc_html_e( 'Taxonomy', '2meet-data-optimizer' ); ?></label></th>
|
<th class="wpdo-th-main"><label for="wpdo-tst-taxonomy"><?php esc_html_e( 'Taxonomy', '2meet-data-optimizer' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<select id="wpdo-tst-taxonomy" class="regular-text">
|
<select id="wpdo-tst-taxonomy" class="regular-text">
|
||||||
<?php foreach ( $taxonomies as $slug => $label ) : ?>
|
<?php foreach ( $taxonomies as $slug => $label ) : ?>
|
||||||
@@ -144,11 +149,11 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
<tr>
|
<tr>
|
||||||
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
|
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<label style="display:block;margin-bottom:6px;">
|
<label class="wpdo-label-block">
|
||||||
<input type="radio" name="wpdo-tst-mode" value="fast" checked />
|
<input type="radio" name="wpdo-tst-mode" value="fast" checked />
|
||||||
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus)', '2meet-data-optimizer' ); ?>
|
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus)', '2meet-data-optimizer' ); ?>
|
||||||
</label>
|
</label>
|
||||||
<label style="display:block;">
|
<label class="wpdo-label-block">
|
||||||
<input type="radio" name="wpdo-tst-mode" value="realistic" />
|
<input type="radio" name="wpdo-tst-mode" value="realistic" />
|
||||||
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_term + update_term_meta(較慢,模擬生產路徑 + 觸發 Hook Bus)', '2meet-data-optimizer' ); ?>
|
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_term + update_term_meta(較慢,模擬生產路徑 + 觸發 Hook Bus)', '2meet-data-optimizer' ); ?>
|
||||||
</label>
|
</label>
|
||||||
@@ -174,11 +179,11 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Cleanup + Re-run benchmark -->
|
<!-- Right: Cleanup + Re-run benchmark -->
|
||||||
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
<div class="wpdo-card">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
|
||||||
<p>
|
<p>
|
||||||
<?php esc_html_e( '目前 stress test term 數:', '2meet-data-optimizer' ); ?>
|
<?php esc_html_e( '目前 stress test term 數:', '2meet-data-optimizer' ); ?>
|
||||||
<strong id="wpdo-tst-count-mirror" style="font-size:18px;color:#dc3545;">
|
<strong id="wpdo-tst-count-mirror" class="wpdo-count-danger">
|
||||||
<?php echo esc_html( number_format_i18n( $test_term_count ) ); ?>
|
<?php echo esc_html( number_format_i18n( $test_term_count ) ); ?>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -191,7 +196,7 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr style="margin:18px 0;" />
|
<hr class="wpdo-hr-section" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="button" id="wpdo-tst-rerun-bench" <?php disabled( $is_running || 0 === $test_term_count ); ?>>
|
<button type="button" class="button" id="wpdo-tst-rerun-bench" <?php disabled( $is_running || 0 === $test_term_count ); ?>>
|
||||||
@@ -202,46 +207,46 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress card (live) -->
|
<!-- Progress card (live) -->
|
||||||
<div id="wpdo-tst-progress-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo $is_running ? '' : 'display:none;'; ?>">
|
<div id="wpdo-tst-progress-card" class="wpdo-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
|
||||||
<div style="margin-bottom:10px;">
|
<div class="wpdo-bar-section">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:13px;">
|
<div class="wpdo-bar-header">
|
||||||
<span>
|
<span>
|
||||||
<strong id="wpdo-tst-pg-status"><?php echo esc_html( $status_str ); ?></strong>
|
<strong id="wpdo-tst-pg-status"><?php echo esc_html( $status_str ); ?></strong>
|
||||||
· <code id="wpdo-tst-pg-taxonomy"><?php echo esc_html( (string) ( $state['taxonomy'] ?? '' ) ); ?></code>
|
· <code id="wpdo-tst-pg-taxonomy"><?php echo esc_html( (string) ( $state['taxonomy'] ?? '' ) ); ?></code>
|
||||||
· <span id="wpdo-tst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
|
· <span id="wpdo-tst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
|
||||||
</span>
|
</span>
|
||||||
<span id="wpdo-tst-pg-pct" style="font-weight:600;"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
|
<span id="wpdo-tst-pg-pct" class="wpdo-bar-pct"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
|
<div class="wpdo-bar-track">
|
||||||
<div id="wpdo-tst-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%;transition:width .4s;"></div>
|
<div id="wpdo-tst-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
|
<table class="wpdo-stats-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;font-weight:600;"><span id="wpdo-tst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-tst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
|
<td class="wpdo-td-val"><span id="wpdo-tst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-tst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;font-weight:600;"><span id="wpdo-tst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> terms/sec</td>
|
<td class="wpdo-td-val"><span id="wpdo-tst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> terms/sec</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-tst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
|
<td class="wpdo-td"><span id="wpdo-tst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-tst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
|
<td class="wpdo-td"><span id="wpdo-tst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-tst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
|
<td class="wpdo-td"><span id="wpdo-tst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
|
||||||
<td style="padding:6px;color:#666;"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
|
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
|
||||||
<td style="padding:6px;"><span id="wpdo-tst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
|
<td class="wpdo-td"><span id="wpdo-tst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Benchmark report -->
|
<!-- Benchmark report -->
|
||||||
<div id="wpdo-tst-bench-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo ! empty( $state['benchmark'] ) ? '' : 'display:none;'; ?>">
|
<div id="wpdo-tst-bench-card" class="wpdo-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
|
||||||
<h3 style="margin-top:0;"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
|
<h3 class="wpdo-mt-0"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
|
||||||
<div id="wpdo-tst-bench-content"></div>
|
<div id="wpdo-tst-bench-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
|
||||||
/**
|
/**
|
||||||
* TMDO_CLI_Member — Member flat-table CLI subcommands.
|
* TMDO_CLI_Member — Member flat-table CLI subcommands.
|
||||||
*
|
*
|
||||||
@@ -17,6 +16,10 @@
|
|||||||
* @since 2.5.5
|
* @since 2.5.5
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
|
||||||
/**
|
/**
|
||||||
* TMDO_CLI_Post — Post entity CLI subcommands (v2.9.0+).
|
* TMDO_CLI_Post — Post entity CLI subcommands (v2.9.0+).
|
||||||
*
|
*
|
||||||
@@ -16,6 +15,10 @@
|
|||||||
* @since 2.9.0
|
* @since 2.9.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
|
||||||
/**
|
/**
|
||||||
* TMDO_CLI_Term_Comment — Term + Comment entity CLI subcommands (v2.12.0+).
|
* TMDO_CLI_Term_Comment — Term + Comment entity CLI subcommands (v2.12.0+).
|
||||||
*
|
*
|
||||||
@@ -19,6 +18,10 @@
|
|||||||
* @since 2.12.0
|
* @since 2.12.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -646,6 +648,11 @@ class TMDO_CLI {
|
|||||||
* @subcommand import-hpct
|
* @subcommand import-hpct
|
||||||
*/
|
*/
|
||||||
public function import_hpct( $args, $assoc_args ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
public function import_hpct( $args, $assoc_args ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||||
|
// HPCT is a HivePress-family plugin; the importer ships in that AddOn.
|
||||||
|
if ( ! class_exists( 'TMDO_HPCT_Import' ) ) {
|
||||||
|
WP_CLI::error( 'import-hpct needs 2meet-data-optimizer-hivepress-addon to be active.' );
|
||||||
|
}
|
||||||
|
|
||||||
if ( TMDO_HPCT_Import::is_imported() ) {
|
if ( TMDO_HPCT_Import::is_imported() ) {
|
||||||
WP_CLI::warning( 'HPCT settings have already been imported.' );
|
WP_CLI::warning( 'HPCT settings have already been imported.' );
|
||||||
return;
|
return;
|
||||||
@@ -796,12 +803,12 @@ class TMDO_CLI {
|
|||||||
if ( ! empty( $warm_posts ) ) {
|
if ( ! empty( $warm_posts ) ) {
|
||||||
// Seed warm zone entries so reads are non-trivial.
|
// Seed warm zone entries so reads are non-trivial.
|
||||||
foreach ( $warm_posts as $pid ) {
|
foreach ( $warm_posts as $pid ) {
|
||||||
TMDO_Zone_Warm::set( (int) $pid, TMDO_Listing_Stats::VIEW_KEY, '1', DAY_IN_SECONDS );
|
TMDO_Zone_Warm::set( (int) $pid, TMDO_Zone_Warm::VIEW_KEY, '1', DAY_IN_SECONDS );
|
||||||
}
|
}
|
||||||
|
|
||||||
$start = microtime( true );
|
$start = microtime( true );
|
||||||
foreach ( $warm_posts as $pid ) {
|
foreach ( $warm_posts as $pid ) {
|
||||||
TMDO_Zone_Warm::get( (int) $pid, TMDO_Listing_Stats::VIEW_KEY );
|
TMDO_Zone_Warm::get( (int) $pid, TMDO_Zone_Warm::VIEW_KEY );
|
||||||
}
|
}
|
||||||
$warm_ms = ( microtime( true ) - $start ) * 1000;
|
$warm_ms = ( microtime( true ) - $start ) * 1000;
|
||||||
|
|
||||||
@@ -1146,6 +1153,10 @@ class TMDO_CLI {
|
|||||||
WP_CLI::log( "Deleted {$warm_deleted} expired warm entries." );
|
WP_CLI::log( "Deleted {$warm_deleted} expired warm entries." );
|
||||||
|
|
||||||
if ( $archive_expired ) {
|
if ( $archive_expired ) {
|
||||||
|
// Listing archival is HivePress-specific and ships in that AddOn.
|
||||||
|
if ( ! class_exists( 'TMDO_Listing_Stats' ) ) {
|
||||||
|
WP_CLI::warning( '--archive-expired needs 2meet-data-optimizer-hivepress-addon; skipping.' );
|
||||||
|
} else {
|
||||||
WP_CLI::log( 'Archiving expired listing fields to Zone D...' );
|
WP_CLI::log( 'Archiving expired listing fields to Zone D...' );
|
||||||
$archived = TMDO_Listing_Stats::archive_expired_listings();
|
$archived = TMDO_Listing_Stats::archive_expired_listings();
|
||||||
WP_CLI::log( "Archived {$archived} fields from expired listings." );
|
WP_CLI::log( "Archived {$archived} fields from expired listings." );
|
||||||
@@ -1153,6 +1164,7 @@ class TMDO_CLI {
|
|||||||
$stats = TMDO_Zone_Archive::stats();
|
$stats = TMDO_Zone_Archive::stats();
|
||||||
WP_CLI::log( "Zone D total: {$stats['total_rows']} rows, {$stats['compressed_rows']} compressed." );
|
WP_CLI::log( "Zone D total: {$stats['total_rows']} rows, {$stats['compressed_rows']} compressed." );
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WP_CLI::success( 'Cleanup complete.' );
|
WP_CLI::success( 'Cleanup complete.' );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,560 @@
|
|||||||
|
# Entity Adapter Cookbook
|
||||||
|
|
||||||
|
How partner plugins integrate with `2meet-data-optimizer v1.0.0+` to gain anti-EAV
|
||||||
|
benefits without modifying their existing data model.
|
||||||
|
|
||||||
|
This is the practical guide. For the canonical vocabulary see `CONTEXT.md`; for the
|
||||||
|
two standing architectural decisions see `docs/adr-001-post-entity-source-of-truth.md`
|
||||||
|
and `docs/adr-002-dual-write-naming-collision.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ TL;DR Decision Tree (read this first — v2.1.2 reordered)
|
||||||
|
|
||||||
|
The most common mistake is picking Tier 1-3 when Tier 5 was the right answer.
|
||||||
|
Ask these questions in order:
|
||||||
|
|
||||||
|
```
|
||||||
|
Q1. Does the partner plugin have its OWN custom tables / lookup tables
|
||||||
|
that already provide anti-EAV? (HPOS, wc_product_meta_lookup,
|
||||||
|
BuddyPress activity tables, EDD payments, etc.)
|
||||||
|
|
||||||
|
├── YES → 🟢 TIER 5 (integrate, don't duplicate). STOP HERE.
|
||||||
|
│ Register awareness only. Do not migrate.
|
||||||
|
│ See: TMDO_WooCommerce reference.
|
||||||
|
│
|
||||||
|
└── NO → continue to Q2.
|
||||||
|
|
||||||
|
Q2. Is this a brand-new plugin you control end-to-end?
|
||||||
|
|
||||||
|
├── YES → 🟢 TIER 4 (greenfield, anti-EAV from day 1).
|
||||||
|
│ Use `wp tmdo register-stub <slug>` for boilerplate.
|
||||||
|
│ See: 2meet-inquiries reference.
|
||||||
|
│
|
||||||
|
└── NO (existing plugin with postmeta) → continue to Q3.
|
||||||
|
|
||||||
|
Q3. Is the relevant postmeta key heavily queried (filter / sort / search)?
|
||||||
|
Benchmark: > 10k rows OR > 3-condition meta_query OR sort by meta_value.
|
||||||
|
|
||||||
|
├── YES → 🟢 TIER 1-3 (migrate to a Zone).
|
||||||
|
│ Tier 1 (5 min) for read-only optimization.
|
||||||
|
│ Tier 2 (30 min) for full dual-write.
|
||||||
|
│ Tier 3 (1-2 days) for new entity type.
|
||||||
|
│
|
||||||
|
└── NO → 🟢 LEAVE AS POSTMETA. Premature optimization.
|
||||||
|
Re-evaluate when scale crosses Q3 thresholds.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Tier 5 is FIRST**: at v2.1.2 audit time, every mature plugin we surveyed
|
||||||
|
(WC / BuddyPress potential / EDD potential / GravityForms) has its own anti-EAV.
|
||||||
|
Defaulting to Tier 1-3 risks the catastrophic "two sources of truth" failure
|
||||||
|
mode. See `docs/INTEGRATION_PATTERN_DECISION.md` for the principle.
|
||||||
|
|
||||||
|
### How to tell if a partner plugin already has anti-EAV (Tier 5 candidate)
|
||||||
|
|
||||||
|
Check these signals in order — any ONE is sufficient for Tier 5:
|
||||||
|
|
||||||
|
| Signal | Where to look | Examples |
|
||||||
|
|--------|---------------|----------|
|
||||||
|
| **Lookup tables** with name pattern `*_lookup` / `*_meta_lookup` / `*_index` | `SHOW TABLES LIKE 'wp_{prefix}_%lookup%'` | `wp_wc_product_meta_lookup`, `wp_wc_customer_lookup` |
|
||||||
|
| **HPOS-style migration toggle** (custom table replaces postmeta) | Plugin's settings → "High Performance" or "Custom Tables" feature | WooCommerce HPOS, EDD 3.0 payments |
|
||||||
|
| **Dedicated columns instead of meta** in main entity table | `DESC wp_{plugin}_entities` shows `price`, `status` etc. as columns | BuddyPress activity table, MemberPress subscriptions |
|
||||||
|
| **Plugin's own search/filter API** that bypasses `meta_query` | `wc_get_products()`, `bp_activity_get()`, `edd_get_payments()` | WC, BuddyPress, EDD all have native APIs |
|
||||||
|
| **`*_stats` / `*_aggregate` tables** for analytics queries | `wp_wc_order_stats`, `wp_*_lookup` | WC analytics, MonsterInsights |
|
||||||
|
| **db_version option** that hits `dbDelta` migration on plugin update | `wp option get {plugin}_db_version` returns a non-trivial version | Indicates the plugin has its own schema migration story |
|
||||||
|
|
||||||
|
If you see **2+ signals** → definitely Tier 5.
|
||||||
|
If you see **0 signals** but the plugin has heavy postmeta usage → Tier 1-3.
|
||||||
|
If you see **0 signals** and postmeta is light → leave it (Q3 = NO).
|
||||||
|
|
||||||
|
**Quick command-line audit**:
|
||||||
|
```bash
|
||||||
|
# List custom tables for a plugin
|
||||||
|
wp db query "SHOW TABLES LIKE 'wp_{prefix}_%'"
|
||||||
|
|
||||||
|
# Count postmeta keys the plugin owns (low = likely Tier 5; high = candidate Tier 1-3)
|
||||||
|
wp db query "SELECT COUNT(DISTINCT meta_key) FROM wp_postmeta WHERE meta_key LIKE '\\_{prefix}_%'"
|
||||||
|
|
||||||
|
# If both numbers are non-trivial → Tier 5 is correct (plugin uses both, but
|
||||||
|
# its tables are the truth and postmeta is legacy/secondary).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Three integration tiers
|
||||||
|
|
||||||
|
Pick the one that matches your plugin's data model:
|
||||||
|
|
||||||
|
### Tier 1 — `TMDO_API` facade(最少改動,5 分鐘)
|
||||||
|
|
||||||
|
If your plugin reads/writes `*_meta()` directly today, swap to the facade. This
|
||||||
|
gives you future-proofing for free — the day the field migrates to a zone or
|
||||||
|
entity adapter, your plugin needs zero changes.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```php
|
||||||
|
$token = get_post_meta( $vendor_id, 'tmeetic_ical_token', true );
|
||||||
|
update_post_meta( $vendor_id, 'tmeetic_ical_token', $new_token );
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```php
|
||||||
|
$token = class_exists( 'TMDO_API' )
|
||||||
|
? TMDO_API::get_field( $vendor_id, 'tmeetic_ical_token' )
|
||||||
|
: get_post_meta( $vendor_id, 'tmeetic_ical_token', true );
|
||||||
|
|
||||||
|
if ( class_exists( 'TMDO_API' ) ) {
|
||||||
|
TMDO_API::set_field( $vendor_id, 'tmeetic_ical_token', $new_token );
|
||||||
|
} else {
|
||||||
|
update_post_meta( $vendor_id, 'tmeetic_ical_token', $new_token );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Real example:** `2meet-courses/includes/class-2meetic-ical.php` (Wave 2 改造).
|
||||||
|
|
||||||
|
**Cross-entity:**
|
||||||
|
```php
|
||||||
|
TMDO_API::get_entity( 'user', $user_id, 'points' );
|
||||||
|
TMDO_API::get_entity( 'term', $term_id, 'usage_count' );
|
||||||
|
TMDO_API::get_entity( 'comment', $comment_id, 'helpful_count' );
|
||||||
|
TMDO_API::set_entity( 'user', $uid, 'points', 50 );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Helper:**
|
||||||
|
```php
|
||||||
|
TMDO_API::is_field_registered( 'post', 'hp_price' ); // bool
|
||||||
|
TMDO_API::trace_storage( 'post', 'hp_price', 'hp_listing' ); // 'zone_hot' | 'postmeta' | ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tier 2 — Schema Registry 註冊(中等改動,30 分鐘)
|
||||||
|
|
||||||
|
If your plugin owns specific meta_keys that benefit from Hot zone (search/filter),
|
||||||
|
Cold zone (display/JSON), or Warm zone (TTL counter) treatment.
|
||||||
|
|
||||||
|
**Implementation:** create one integration class.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// my-plugin/includes/class-tmdo-myplugin.php
|
||||||
|
final class TMDO_MyPlugin {
|
||||||
|
public static function register(): void {
|
||||||
|
// Detect partner plugin (2meet-data-optimizer) — no-op when absent.
|
||||||
|
if ( ! class_exists( 'TMDO_Schema_Registry' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
add_action( 'wpdo_register_fields', array( __CLASS__, 'register_fields' ) );
|
||||||
|
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_tables' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register_fields( TMDO_Schema_Registry $registry ): void {
|
||||||
|
$registry->register_many( 'my-plugin', array(
|
||||||
|
// Hot zone (search/filter): flat 1NF column with index.
|
||||||
|
array(
|
||||||
|
'post_type' => 'my_post_type',
|
||||||
|
'meta_key' => 'my_price',
|
||||||
|
'zone' => 'hot',
|
||||||
|
'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
|
||||||
|
'column' => 'my_price',
|
||||||
|
'indexed' => true,
|
||||||
|
),
|
||||||
|
// Cold zone (description / JSON / display).
|
||||||
|
array(
|
||||||
|
'post_type' => 'my_post_type',
|
||||||
|
'meta_key' => 'my_description',
|
||||||
|
'zone' => 'cold',
|
||||||
|
'cache_group' => 'wpdo_cold_my_post_type',
|
||||||
|
'cache_ttl' => HOUR_IN_SECONDS,
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register_tables( TMDO_Custom_Table_Registry $registry ): void {
|
||||||
|
$registry->register( 'my-plugin', array(
|
||||||
|
'table_name' => 'my_custom_table',
|
||||||
|
'primary_key' => 'id',
|
||||||
|
'post_type_link' => 'my_post_type',
|
||||||
|
'doctor_callback' => array( __CLASS__, 'doctor_my_table' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function doctor_my_table(): array {
|
||||||
|
global $wpdb;
|
||||||
|
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->prefix}my_custom_table`" );
|
||||||
|
return array( 'ok' => true, 'message' => "rows: {$count}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In your plugin's bootstrap:
|
||||||
|
add_action( 'plugins_loaded', array( 'TMDO_MyPlugin', 'register' ), 5 );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Real examples:**
|
||||||
|
- `2meet-data-optimizer/includes/integrations/class-tmdo-infocards.php` — 9 fields + 3 tables
|
||||||
|
- `2meet-data-optimizer/includes/integrations/class-tmdo-bookings.php` — 7 tables only
|
||||||
|
|
||||||
|
### Tier 3 — Custom Entity Adapter(大改動,1-2 天)
|
||||||
|
|
||||||
|
If you need cross-entity behaviour (e.g. counter that works on user / term /
|
||||||
|
comment uniformly), use the demo entity counter pattern.
|
||||||
|
|
||||||
|
**Real example:** `2meet-data-optimizer/includes/integrations/class-tmdo-demo-entity-counter.php`.
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
```php
|
||||||
|
final class My_Counter {
|
||||||
|
public const MODULE = 'entity_my_counter';
|
||||||
|
public const TABLE = 'wpdo_my_counters';
|
||||||
|
|
||||||
|
public static function install_table(): void {
|
||||||
|
global $wpdb;
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta( "CREATE TABLE {$wpdb->prefix}" . self::TABLE . " (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
entity_type varchar(20) NOT NULL,
|
||||||
|
entity_id bigint(20) unsigned NOT NULL,
|
||||||
|
counter_key varchar(100) NOT NULL,
|
||||||
|
counter_value bigint(20) NOT NULL DEFAULT 0,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY ui_entity_counter (entity_type, entity_id, counter_key),
|
||||||
|
KEY idx_lookup (entity_type, counter_key, counter_value)
|
||||||
|
) {$wpdb->get_charset_collate()};" );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function set( string $entity_type, int $entity_id, string $key, int $value ): void {
|
||||||
|
// Always write native (durability anchor).
|
||||||
|
TMDO_API::set_entity( $entity_type, $entity_id, $key, $value );
|
||||||
|
|
||||||
|
// Conditional dual-write to zone table.
|
||||||
|
if ( TMDO_Feature_Flags::is_write_active( self::MODULE ) ) {
|
||||||
|
TMDO_DB::upsert(
|
||||||
|
$GLOBALS['wpdb']->prefix . self::TABLE,
|
||||||
|
array(
|
||||||
|
'entity_type' => $entity_type,
|
||||||
|
'entity_id' => $entity_id,
|
||||||
|
'counter_key' => $key,
|
||||||
|
'counter_value' => $value,
|
||||||
|
'updated_at' => current_time( 'mysql' ),
|
||||||
|
),
|
||||||
|
array( 'counter_value', 'updated_at' ),
|
||||||
|
array( 'entity_type', 'entity_id', 'counter_key' ),
|
||||||
|
array( '%s', '%d', '%s', '%d', '%s' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get( string $entity_type, int $entity_id, string $key ): int {
|
||||||
|
if ( TMDO_Feature_Flags::is_read_custom( self::MODULE ) ) {
|
||||||
|
// Read from zone table; fallback to native if row missing.
|
||||||
|
$val = $GLOBALS['wpdb']->get_var( $GLOBALS['wpdb']->prepare(
|
||||||
|
"SELECT counter_value FROM `{$GLOBALS['wpdb']->prefix}" . self::TABLE . "`
|
||||||
|
WHERE entity_type = %s AND entity_id = %d AND counter_key = %s",
|
||||||
|
$entity_type, $entity_id, $key
|
||||||
|
) );
|
||||||
|
if ( null !== $val ) {
|
||||||
|
return (int) $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (int) TMDO_API::get_entity( $entity_type, $entity_id, $key );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function top_n( string $entity_type, string $key, int $n = 10 ): array {
|
||||||
|
// Killer query that postmeta cannot do efficiently.
|
||||||
|
return $GLOBALS['wpdb']->get_results( $GLOBALS['wpdb']->prepare(
|
||||||
|
"SELECT entity_id, counter_value FROM `{$GLOBALS['wpdb']->prefix}" . self::TABLE . "`
|
||||||
|
WHERE entity_type = %s AND counter_key = %s
|
||||||
|
ORDER BY counter_value DESC LIMIT %d",
|
||||||
|
$entity_type, $key, $n
|
||||||
|
), ARRAY_A );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FSM lifecycle reference
|
||||||
|
|
||||||
|
For Tier 3 entity adapters, drive the 7+1 state machine via CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install schema (one-time)
|
||||||
|
wp tmdo doctor # verify base tables
|
||||||
|
|
||||||
|
# 2. Initial state: idle (do not register feature flag)
|
||||||
|
wp tmdo mode-audit | grep entity_my_counter # should show 'idle'
|
||||||
|
|
||||||
|
# 3. Begin dual_write — both native + zone get writes
|
||||||
|
wp tmdo mode-set entity_my_counter dual_write
|
||||||
|
|
||||||
|
# 4. Run backfill (if you have existing data)
|
||||||
|
# (custom script or wp tmdo migrate)
|
||||||
|
|
||||||
|
# 5. Verify with shadow_read for 7 days
|
||||||
|
wp tmdo mode-set entity_my_counter verify
|
||||||
|
wp tmdo shadow-enable entity_my_counter
|
||||||
|
|
||||||
|
# Check for diffs
|
||||||
|
wp eval 'echo (int) $GLOBALS["wpdb"]->get_var("SELECT COUNT(*) FROM {$GLOBALS[\"wpdb\"]->prefix}wpdo_shadow_diffs WHERE entity_type=\"user\"");'
|
||||||
|
|
||||||
|
# 6. Cutover — reads switch to zone
|
||||||
|
wp tmdo shadow-disable entity_my_counter
|
||||||
|
wp tmdo mode-set entity_my_counter cutover
|
||||||
|
|
||||||
|
# 7. After 7 more days, cleanup native rows
|
||||||
|
wp tmdo mode-set entity_my_counter cleanup
|
||||||
|
|
||||||
|
# 8. Final state — no fallback, zone is source of truth
|
||||||
|
wp tmdo mode-set entity_my_counter complete
|
||||||
|
|
||||||
|
# Rollback at any time
|
||||||
|
wp tmdo mode-set entity_my_counter idle
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-EAV lint exemptions
|
||||||
|
|
||||||
|
Some patterns require direct SQL by design (one-off migration scans, cron token
|
||||||
|
expiry checks). Mark them with `phpcs:ignore` so `wp tmdo lint --strict` accepts them:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// phpcs:ignore WPDO.AntiEAV.PostmetaScan -- Cron sweeps postmeta to find expiring IG tokens.
|
||||||
|
$ids = $wpdb->get_col( $wpdb->prepare(
|
||||||
|
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'tmeetic_ig_token_expiry' AND CAST(meta_value AS UNSIGNED) < %d",
|
||||||
|
time() + 31 * DAY_IN_SECONDS
|
||||||
|
) );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available rules:**
|
||||||
|
- `WPDO.AntiEAV.PostmetaScan` — cross-postmeta scan in migration / cron
|
||||||
|
- `WPDO.AntiEAV.UsermetaScan` — same, usermeta
|
||||||
|
- `WPDO.AntiEAV.PostmetaFallback` — fallback path during graceful degradation
|
||||||
|
|
||||||
|
The exemption stays line-local. The lint cannot be silenced for an entire file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification checklist (before merge)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Lint passes strict
|
||||||
|
wp tmdo lint --plugin=$(pwd) --strict
|
||||||
|
|
||||||
|
# 2. Conflict scan clean
|
||||||
|
wp tmdo conflict-scan
|
||||||
|
|
||||||
|
# 3. Doctor check (your custom tables registered + healthy)
|
||||||
|
wp tmdo doctor
|
||||||
|
|
||||||
|
# 4. Tests pass (if your plugin has them)
|
||||||
|
./vendor/bin/phpunit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real-world results (dev10, 2026-04-25)
|
||||||
|
|
||||||
|
Production benchmarks measured with 50 hp_listing posts:
|
||||||
|
|
||||||
|
| Path | Latency (n=100) | Speedup |
|
||||||
|
|---|---|---|
|
||||||
|
| Native `get_post_meta()` | 205ms | 1.0x baseline |
|
||||||
|
| `TMDO_Listing_Stats::get_view_count()` (Zone B Warm + fallback) | 81ms | **2.51x** |
|
||||||
|
| `TMDO_Demo_Entity_Counter::top_n()` (Zone via 1 LEFT JOIN) | 0.19ms / call | **postmeta cannot do efficiently** |
|
||||||
|
|
||||||
|
The `top_n` example is the killer use case — sorting 1000s of users by point
|
||||||
|
count via postmeta requires a full meta_value scan + filesort. The zone table
|
||||||
|
serves it from a covering index in sub-millisecond.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tier 4: Greenfield plugin — `2meet-inquiries` (v0.5.0)
|
||||||
|
|
||||||
|
The cleanest case: a plugin written **from scratch** to be anti-EAV from day 1.
|
||||||
|
Use this as a template for all new 2meet-* plugins.
|
||||||
|
|
||||||
|
### Why this is the gold standard
|
||||||
|
|
||||||
|
| Pattern | What `2meet-inquiries` does | What `bookings`/`courses`/`infocards` had to retrofit |
|
||||||
|
|---|---|---|
|
||||||
|
| Large structured config | `wp_2mqi_forms.config_json LONGTEXT` (custom table) | Originally postmeta `_eh_inquiry_config` (anti-EAV violation) |
|
||||||
|
| Hot-zone meta on hp_vendor | `wpdo_register_fields` — `_tmqi_default_form_id` (bigint, indexed) | Some retrofitted via Schema_Registry Hot Wave 1; others still WP_Query-driven |
|
||||||
|
| Sensitive PII | `customer_email_enc BLOB` + `customer_email_hash CHAR(64)` for indexed lookup | Originally separate plugins each rolled own AES wrapper |
|
||||||
|
| Audit-trail rows | Dedicated `wp_2mqi_responses` table (1 row per submission) | Older plugins used `wp_postmeta` rows-per-field → EAV blow-up |
|
||||||
|
| Analytics events | Dedicated `wp_2mqi_analytics` (event_type, stage_index, session_id) | N/A — most plugins didn't have analytics, would've gone to postmeta if they did |
|
||||||
|
| Webhook config | `wp_options` per vendor (autoload=no, low cardinality) | Same |
|
||||||
|
|
||||||
|
### Anatomy: 4 custom tables, 0 plugin-owned postmeta keys
|
||||||
|
|
||||||
|
```
|
||||||
|
wp_2mqi_forms — form definitions (config_json + counters + slug)
|
||||||
|
wp_2mqi_responses — submitted inquiries (encrypted PII + payload_json)
|
||||||
|
wp_2mqi_drafts — in-progress submissions (token + 14-day expire)
|
||||||
|
wp_2mqi_analytics — funnel events (view, stage_*, submit)
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus 2 `hp_vendor` fields registered to Schema_Registry Hot zone:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$registry->register_many( '2meet-inquiries', array(
|
||||||
|
array(
|
||||||
|
'post_type' => 'hp_vendor',
|
||||||
|
'meta_key' => '_tmqi_default_form_id',
|
||||||
|
'zone' => 'hot',
|
||||||
|
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
|
||||||
|
'column' => '_tmqi_default_form_id',
|
||||||
|
'indexed' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'post_type' => 'hp_vendor',
|
||||||
|
'meta_key' => '_tmqi_inquiries_enabled',
|
||||||
|
'zone' => 'hot',
|
||||||
|
'data_type' => 'tinyint(1) NOT NULL DEFAULT 0',
|
||||||
|
'column' => '_tmqi_inquiries_enabled',
|
||||||
|
'indexed' => true,
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bootstrap pattern (recommended)
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class TMQI_Plugin {
|
||||||
|
use TMDO_Anti_EAV_Aware; // ← strict contract; fails to load without it
|
||||||
|
|
||||||
|
public function run(): void {
|
||||||
|
// Register tables on the canonical action.
|
||||||
|
add_action( 'wpdo_register_fields', array( __CLASS__, 'register_wpdo_fields' ) );
|
||||||
|
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) );
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register_wpdo_fields(): void { /* hot-zone fields */ }
|
||||||
|
public static function register_custom_tables( $registry = null ): void {
|
||||||
|
TMQI_WPDO_Integration::register_tables( $registry );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ground rules followed
|
||||||
|
|
||||||
|
✅ **No `update_post_meta()` calls anywhere** — even for hp_vendor metas, we call `TMDO_API::set_field()`
|
||||||
|
✅ **No direct `SELECT FROM wp_postmeta`** — `wpdo lint --strict` exits 0
|
||||||
|
✅ **All large JSON in custom tables** — `config_json` and `payload_json` columns, never postmeta
|
||||||
|
✅ **Sensitive data encrypted** — reuse `TMEETIC_Crypto::encrypt()` (don't roll your own)
|
||||||
|
✅ **Indexed search on encrypted columns** — store SHA-256 hash alongside ciphertext
|
||||||
|
✅ **Single `do_action( 'tmqi/submitted' )`** — downstream notifiers, analytics, webhook all hook here
|
||||||
|
|
||||||
|
### Verification (run on dev10 right now)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom tables registered
|
||||||
|
wp eval 'echo count(TMDO_Custom_Table_Registry::instance()->for_provider("2meet-inquiries"));'
|
||||||
|
# → 4
|
||||||
|
|
||||||
|
# Strict lint passes
|
||||||
|
wp tmdo lint --plugin=$(wp plugin path 2meet-inquiries) --strict
|
||||||
|
# → Success: Anti-EAV lint passed
|
||||||
|
|
||||||
|
# Conflict scan
|
||||||
|
wp tmdo conflict-scan
|
||||||
|
# → 0 conflicts detected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Takeaway for Wave 2/3 retrofits
|
||||||
|
|
||||||
|
When refactoring an existing plugin to be anti-EAV, the question is not "how do
|
||||||
|
we shoehorn this into postmeta less?" — it's "**what does the data look like if
|
||||||
|
we redesign it like 2meet-inquiries from day 1?**" Then plot a migration path.
|
||||||
|
|
||||||
|
For most plugins the answer is: **replace one big postmeta key with one custom
|
||||||
|
table row, and register a small number of hot-zone hp_vendor fields for search.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tier 5: Integrating with a plugin that has its OWN anti-EAV — `TMDO_WooCommerce` (v2.1.0)
|
||||||
|
|
||||||
|
The hardest case: WC core already has anti-EAV (HPOS for orders, `wp_wc_product_meta_lookup`
|
||||||
|
for products). WPDO's job is to **integrate, not duplicate**.
|
||||||
|
|
||||||
|
### Why this is different from Tiers 1-4
|
||||||
|
|
||||||
|
| Aspect | Tier 1-4 (we own the data) | Tier 5 (WC owns it) |
|
||||||
|
|--------|---------------------------|----------------------|
|
||||||
|
| Custom tables | We define + create | WC defines + creates |
|
||||||
|
| Hot-zone fields | Migrated from postmeta to our Hot zone | Already in WC's lookup tables; we just **register awareness** |
|
||||||
|
| Doctor probes | We control existence + schema | We probe but don't fix |
|
||||||
|
| Schema drift | Our migration tooling | WC's update_db_*() handles |
|
||||||
|
| Conflict | None (single owner) | **Risk: shadow lookup tables** |
|
||||||
|
|
||||||
|
### Anti-pattern: ❌ DON'T duplicate WC's lookup tables
|
||||||
|
|
||||||
|
```php
|
||||||
|
// WRONG — creates a parallel system that drifts from WC's truth
|
||||||
|
$schema->register( 'woocommerce', array(
|
||||||
|
'post_type' => 'product',
|
||||||
|
'meta_key' => '_price',
|
||||||
|
'zone' => 'hot',
|
||||||
|
// ... migrate _price into our wpdo_hot_product table
|
||||||
|
) );
|
||||||
|
// Now `_price` lives in BOTH wp_wc_product_meta_lookup AND our hot zone.
|
||||||
|
// Updates touch one but not the other. Catastrophe.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Right pattern: ✅ Register awareness, defer to WC's anti-EAV
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In TMDO_WooCommerce::register_custom_tables():
|
||||||
|
foreach ( WC_CORE_TABLES as $name => $meta ) {
|
||||||
|
$registry->register( 'woocommerce', array(
|
||||||
|
'table_name' => $name, // wp_wc_product_meta_lookup
|
||||||
|
'description' => $meta['description'],
|
||||||
|
'doctor_callback' => array( __CLASS__, 'doctor_check' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// In TMDO_WooCommerce::register_schema_fields():
|
||||||
|
// Register postmeta keys WC STILL uses (the ones not yet migrated to lookups).
|
||||||
|
// When `_price` is also in wc_product_meta_lookup, registering doesn't migrate
|
||||||
|
// to OUR hot zone — it's just a hint to TMDO_API consumers about "this is hot".
|
||||||
|
$schema->register_many( 'woocommerce', array(
|
||||||
|
array( 'post_type' => 'product', 'meta_key' => '_price', 'zone' => 'hot', ... ),
|
||||||
|
// ...
|
||||||
|
) );
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision tree for new partner integrations
|
||||||
|
|
||||||
|
```
|
||||||
|
Does the partner plugin store data in postmeta?
|
||||||
|
├── NO → already on custom tables → Tier 4 (register tables + done)
|
||||||
|
└── YES → does the partner have its own lookup/cache table?
|
||||||
|
├── NO → Tier 1-3 (we manage migration)
|
||||||
|
└── YES → Tier 5: register awareness only, never duplicate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tier 5 checklist for `TMDO_WooCommerce`
|
||||||
|
|
||||||
|
- [x] Register all 20 `wp_wc_*` tables to Custom_Table_Registry
|
||||||
|
- [x] Add `doctor_callback` that probes existence + row count (not schema diff)
|
||||||
|
- [x] Register hot-zone postmeta fields (legacy path only) — WC's lookup is the truth
|
||||||
|
- [x] Detect HPOS state via `OrderUtil::custom_orders_table_usage_is_enabled()`
|
||||||
|
- [x] When HPOS off → register order postmeta hot fields (`_order_total`, etc.)
|
||||||
|
- [x] When HPOS on → DON'T register order postmeta (would be stale)
|
||||||
|
- [x] Customer usermeta hot fields registered unconditionally (WC always uses usermeta for these)
|
||||||
|
- [x] Subscription product fields conditional on `WC_Subscriptions` OR `wc-linepay-subscription`
|
||||||
|
- [x] Own custom table (`wpdo_wc_commissions`) for vendor marketplace tracking
|
||||||
|
- [x] Admin notice recommends HPOS when legacy order count > threshold
|
||||||
|
- [x] Admin dashboard surfaces commission stats + table health
|
||||||
|
|
||||||
|
### What this DOESN'T do (and shouldn't)
|
||||||
|
|
||||||
|
- ❌ Migrate `_price` into our hot zone (WC already has wc_product_meta_lookup)
|
||||||
|
- ❌ Mirror `wc_orders` into our archive zone (WC handles its own archive)
|
||||||
|
- ❌ Intercept `update_post_meta` for product meta (interferes with WC's lookup sync)
|
||||||
|
- ❌ Create products / orders / customers (WC's domain)
|
||||||
|
|
||||||
|
### When to revisit
|
||||||
|
|
||||||
|
- WC drops a lookup table (unlikely but possible) → migrate that field path to Tier 3
|
||||||
|
- HPOS becomes default-on → audit our order postmeta registrations and remove
|
||||||
|
- New WC subextension introduces meta keys we should register → add to `register_subscription_fields()`
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Integration Pattern Decision — 2026-04-25
|
||||||
|
|
||||||
|
> **v1.0.0 後記(2026-07-31):本文結論已被架構拆分取代。**
|
||||||
|
> 當時的兩個選項是「集中在核心」vs「各外掛自帶 bridge」。v1.0.0 走的是第三條路:
|
||||||
|
> 每個夥伴外掛對應**一個獨立 AddOn 外掛**(`2meet-data-optimizer-<partner>-addon`,共 11 個),
|
||||||
|
> 核心完全不認識夥伴外掛。下文的 `class-tmdo-<partner>.php` 一律已搬進對應 AddOn。
|
||||||
|
> 本文保留是為了記錄「為什麼不選 per-plugin bridge 檔」這段推理 —— 該理由對 AddOn 邊界同樣適用。
|
||||||
|
|
||||||
|
**Trigger**: Step BB audit revealed two parallel patterns for partner plugin integration; need to commit to one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What I found
|
||||||
|
|
||||||
|
| Plugin | Centralized in 2meet-data-optimizer? | Own bridge file? |
|
||||||
|
|--------|-----------------------------------|------------------|
|
||||||
|
| 2meet-infocards | ✅ `class-tmdo-infocards.php` | ❌ |
|
||||||
|
| 2meet-bookings | ✅ `class-tmdo-bookings.php` | ❌ |
|
||||||
|
| 2meet-quotation | ✅ `class-tmdo-quotation.php` | ❌ |
|
||||||
|
| 2meet-events | ✅ `class-tmdo-events.php` | ✅ `class-tmevents-wpdo-bridge.php` (**duplicate!**) |
|
||||||
|
| 2meet-collab | ✅ `class-tmdo-collab.php` | ❌ |
|
||||||
|
| 2meet-mobile-bridge | ✅ `class-tmdo-mobile-bridge.php` | ❌ |
|
||||||
|
| 2meet-playlist | ✅ `class-tmdo-playlist.php` | ❌ |
|
||||||
|
| 2meet-courses | ❌ | ✅ `class-2meetic-courses-wpdo.php` (NEW today, P step) |
|
||||||
|
| 2meet-inquiries | ❌ | ✅ `class-tmqi-wpdo-integration.php` (NEW from scratch) |
|
||||||
|
|
||||||
|
**Inconsistency**:
|
||||||
|
- 7 plugins are integrated centrally (legacy Wave 2 demo pattern)
|
||||||
|
- 2 new plugins (today) are integrated decentrally (cookbook Tier 4 pattern)
|
||||||
|
- 2meet-events has **both** (silent dedup by registry — works but smelly)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision: **Decentralized (own bridge) is the canonical pattern**
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
1. **Cookbook Tier 4 documents it as the standard** for new plugins (already published)
|
||||||
|
2. **Each plugin owns its own data contract** — no cross-plugin coupling in 2meet-data-optimizer
|
||||||
|
3. **Easier to ship** — partner plugin can update its registration without bumping 2meet-data-optimizer
|
||||||
|
4. **Simpler mental model** — "where do tables get registered? In the plugin that owns them"
|
||||||
|
|
||||||
|
### Why we're NOT migrating today
|
||||||
|
|
||||||
|
1. **Freeze**: Per `FREEZE_2026-04-25.md`, no risky refactors during freeze
|
||||||
|
2. **Working**: All 7 centralized integrations work; registry dedup handles the events double-pattern
|
||||||
|
3. **No vendor demand**: Nobody has reported confusion or bugs from the dual pattern
|
||||||
|
4. **Risk > reward**: Moving 7 classes touches 8 plugins, requires coordinated version bumps, breaks atomic rollback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration plan (when we DO migrate)
|
||||||
|
|
||||||
|
Trigger conditions (any one):
|
||||||
|
- A new partner plugin can't ship cleanly because of the centralized pattern
|
||||||
|
- A bug in the centralized integrations affects multiple plugins simultaneously
|
||||||
|
- We hit 10+ partner plugins (currently 9) and the 2meet-data-optimizer integrations dir is too crowded
|
||||||
|
|
||||||
|
### Steps (per plugin, ~0.5 day each)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Copy /2meet-data-optimizer/includes/integrations/class-tmdo-{slug}.php
|
||||||
|
to /{plugin-slug}/includes/integrations/class-{prefix}-wpdo.php
|
||||||
|
|
||||||
|
2. Rename class:
|
||||||
|
- TMDO_Bookings → TMB_WPDO
|
||||||
|
- TMDO_Quotation → TMQUO_WPDO (etc.)
|
||||||
|
- Keep registration logic identical
|
||||||
|
|
||||||
|
3. Wire in {plugin-slug} bootstrap (after main classes load):
|
||||||
|
require_once $dir . 'includes/integrations/class-{prefix}-wpdo.php';
|
||||||
|
{prefix}_WPDO::register();
|
||||||
|
|
||||||
|
4. In 2meet-data-optimizer:
|
||||||
|
- Remove require_once line
|
||||||
|
- Remove class name from the partner array (line ~208 of main file)
|
||||||
|
- Bump WPDO version (patch)
|
||||||
|
|
||||||
|
5. Verify:
|
||||||
|
wp eval 'echo count(TMDO_Custom_Table_Registry::instance()->for_provider("{slug}"));'
|
||||||
|
→ should still match the original count
|
||||||
|
|
||||||
|
6. Bump partner plugin version (minor, since it now has new dependency)
|
||||||
|
Update Requires Plugins header to mention 2meet-data-optimizer ≥ X.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special case: 2meet-events double-pattern
|
||||||
|
|
||||||
|
Already has both. Migration = remove the centralized `class-tmdo-events.php` (the bridge in 2meet-events stays). This is the **simplest first migration** because the partner plugin's own bridge is already proven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this means for tomorrow's reader
|
||||||
|
|
||||||
|
If you're writing a NEW 2meet-* plugin: **follow Tier 4 pattern, put your integration class in your own plugin's `includes/integrations/` directory.**
|
||||||
|
|
||||||
|
If you're maintaining an EXISTING centralized integration in 2meet-data-optimizer: **leave it alone unless one of the migration triggers fires.**
|
||||||
|
|
||||||
|
If you see the dual pattern in 2meet-events and are confused: **it's intentional, registry dedups, will be cleaned up later.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-decisions (things explicitly NOT done)
|
||||||
|
|
||||||
|
- ❌ NOT moving 7 integrations today — too risky, freeze active
|
||||||
|
- ❌ NOT fixing the events double-pattern today — works fine, low priority
|
||||||
|
- ❌ NOT writing a "consolidation script" — premature optimization for a one-time migration
|
||||||
|
- ❌ NOT updating cookbook to mention the centralized pattern — would be confusing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to revisit this decision
|
||||||
|
|
||||||
|
Same as freeze conditions (`FREEZE_2026-04-25.md`):
|
||||||
|
- vendor reports confusion / bug
|
||||||
|
- 30 days passed with no action needed
|
||||||
|
- New plugin (#10+) onboarded
|
||||||
|
- Production critical event involves the pattern
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# ADR-001: Post Entity Source-of-Truth Contract
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-05-15
|
||||||
|
**Deciders:** wpdev
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Two code paths can intercept `update_post_metadata` / `add_post_metadata`:
|
||||||
|
|
||||||
|
1. **TMDO_Sync_Bridge** — the original Zone interceptor that dual-writes to
|
||||||
|
Hot (Zone A) and Cold (Zone C) flat tables.
|
||||||
|
2. **TMDO_Hook_Bus** — the Entity Bridge write path added in v2.9.x that writes
|
||||||
|
to `wp_wpdo_post_*` flat tables via Entity Registry groups.
|
||||||
|
|
||||||
|
When both are active without a clear contract, a single `update_post_meta()` call
|
||||||
|
can fan out to three distinct write paths (wp_postmeta + Zone table + entity flat
|
||||||
|
table), producing divergent row counts and confusing `TMDO_API::trace_storage()`
|
||||||
|
output.
|
||||||
|
|
||||||
|
The defensive patch `TMDO_Sync_Bridge::is_owned_by_entity_bridge()` (v2.9.2)
|
||||||
|
was added to prevent double-writes but left the authoritative contract undocumented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**When `post` entity mode is `dual_write`, `shadow_read`, or `aeav_only`,
|
||||||
|
Entity Bridge (TMDO_Hook_Bus) is the sole source of truth for keys registered
|
||||||
|
in TMDO_Entity_Registry under the `post` entity type.**
|
||||||
|
|
||||||
|
Sync_Bridge defers to Entity Bridge for those keys via `is_owned_by_entity_bridge()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// TMDO_Sync_Bridge — intercept_update() guard:
|
||||||
|
if ( self::is_owned_by_entity_bridge( $meta_key ) ) {
|
||||||
|
return $check; // pass-through — Entity Bridge owns this key
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`is_owned_by_entity_bridge()` returns `true` when both conditions hold:
|
||||||
|
- `TMDO_Mode_Manager::writes_to_flat('post')` — mode is at least dual_write
|
||||||
|
- `TMDO_Entity_Registry::get_field('post', $meta_key)` — key is registered
|
||||||
|
|
||||||
|
Keys **not** in Entity Registry continue to be owned by Sync_Bridge (Zone path).
|
||||||
|
|
||||||
|
### Invariants
|
||||||
|
|
||||||
|
| Condition | Owner |
|
||||||
|
|-----------|-------|
|
||||||
|
| post mode = `disabled` or `idle` | wp_postmeta (no interception) |
|
||||||
|
| post mode ≥ `dual_write` AND key in Entity Registry | **Entity Bridge** |
|
||||||
|
| post mode ≥ `dual_write` AND key not in Entity Registry | **Sync_Bridge** (Zone) |
|
||||||
|
| post mode = `aeav_only` | Entity Bridge for registered keys; unregistered keys fallthrough to wp_postmeta |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Good:**
|
||||||
|
- Developers adding a new post meta key can determine its owner in O(1): check
|
||||||
|
whether the key is in `wpdo_register_fields` under `post` entity. If yes →
|
||||||
|
Entity Bridge owns it; if no → Zone Sync_Bridge handles it.
|
||||||
|
- `TMDO_API::trace_storage()` output reflects this: Entity Bridge keys show the
|
||||||
|
flat entity table; Zone keys show the zone table.
|
||||||
|
|
||||||
|
**Bad / Watch out for:**
|
||||||
|
- A key registered in **both** Schema_Registry (Zone) and Entity_Registry (Entity
|
||||||
|
Bridge groups) will be captured by Entity Bridge and silently dropped by
|
||||||
|
Sync_Bridge. The duplicate registration is a misconfiguration — caught by
|
||||||
|
`TMDO_Sync_Bridge` guard and validated by `wp tmdo conflict-scan`.
|
||||||
|
- If `TMDO_Mode_Manager` or `TMDO_Entity_Registry` are unavailable (e.g. very
|
||||||
|
early bootstrap), `is_owned_by_entity_bridge()` returns `false` and all writes
|
||||||
|
fall through to the Zone path — safe degradation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `includes/interceptors/class-tmdo-sync-bridge.php` — `is_owned_by_entity_bridge()` (v2.9.2)
|
||||||
|
- `includes/engine/class-tmdo-hook-bus.php` — Entity Bridge write path
|
||||||
|
- `TMDO_API::trace_storage()` — human-readable storage path diagnostics
|
||||||
|
- `wp tmdo conflict-scan` — detects keys registered in both paths
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# ADR-002: Acknowledge 'dual_write' naming collision between Mode_Manager and Feature_Flags FSMs
|
||||||
|
|
||||||
|
**Status:** Accepted — deferred rename
|
||||||
|
**Date:** 2026-05-15
|
||||||
|
**Deciders:** wpdev
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Two distinct FSMs in the codebase both use the string `'dual_write'`:
|
||||||
|
|
||||||
|
| FSM | Class | Constant | Stored in | Semantics |
|
||||||
|
|-----|-------|----------|-----------|-----------|
|
||||||
|
| Entity Bridge | `TMDO_Mode_Manager` | `MODE_DUAL_WRITE` | `wpdo_bridge_modes` | Writes go to both UAE flat table AND `wp_*meta` |
|
||||||
|
| Zone Migration | `TMDO_Feature_Flags` | `STATUS_DUAL_WRITE` | `wpdo_features` | Writes go to both Zone A/B/C table AND `wp_postmeta` |
|
||||||
|
|
||||||
|
The two FSMs are orthogonal — a post type can be in Zone `dual_write`
|
||||||
|
(actively migrating) while the Entity Bridge is in `aeav_only` mode, or vice
|
||||||
|
versa. The collision was introduced when the Entity Bridge FSM was added in
|
||||||
|
v2.5.x alongside the pre-existing Zone Migration FSM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Defer the rename. Document the collision instead.**
|
||||||
|
|
||||||
|
Renaming either constant (e.g. Mode_Manager → `bridge_dual`) would require:
|
||||||
|
|
||||||
|
1. Updating ~85 call sites across 30+ files.
|
||||||
|
2. Writing a DB migration to translate stored option values (`'dual_write'` →
|
||||||
|
`'bridge_dual'` in `wp_options['wpdo_bridge_modes']`).
|
||||||
|
3. Updating all WP-CLI commands that accept mode strings as user input.
|
||||||
|
4. Updating all admin UI dropdowns and confirmation messages.
|
||||||
|
5. Handling sites that run the old code against a DB that has already been
|
||||||
|
migrated (or the reverse — new code on an un-migrated DB).
|
||||||
|
|
||||||
|
The risk of introducing bugs via a mechanical rename outweighs the naming
|
||||||
|
improvement at the current stage of the project.
|
||||||
|
|
||||||
|
The collision is mitigated by:
|
||||||
|
- A disambiguating docblock in `class-tmdo-mode-manager.php` (added 2026-05-15)
|
||||||
|
- This ADR, which explains the overlap to future developers
|
||||||
|
- The two FSMs operating on different option keys and being unreachable from
|
||||||
|
each other's code paths
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Future rename path**: When a DB migration is warranted (e.g. alongside
|
||||||
|
another schema change), rename `MODE_DUAL_WRITE → 'bridge_dual'` and add a
|
||||||
|
migration in `TMDO_Installer::maybe_upgrade()` that rewrites the stored string.
|
||||||
|
- **Linter**: If PHPStan or a custom rule ever flags string literal comparisons
|
||||||
|
across FSMs, this ADR is the canonical explanation for why the overlap is
|
||||||
|
intentional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `includes/engine/class-tmdo-mode-manager.php` — naming note in class docblock
|
||||||
|
- `includes/class-tmdo-feature-flags.php` — Zone FSM (7 states)
|
||||||
|
- P1-10 from full-review report (2026-05-15)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: Anti-EAV Strict Lint
|
||||||
|
|
||||||
|
# Drop this file into each partner plugin's `.github/workflows/anti-eav-lint.yml`.
|
||||||
|
# Required: `2meet-data-optimizer` v1.0.0+ available either via:
|
||||||
|
# (a) Composer dev dependency on the plugin repo, OR
|
||||||
|
# (b) Side-by-side checkout in the same workspace.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
anti-eav-lint:
|
||||||
|
name: Anti-EAV Strict Lint (wp tmdo lint --strict)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout this plugin
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: this-plugin
|
||||||
|
|
||||||
|
- name: Checkout 2meet-data-optimizer
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: wpdev/2meet-data-optimizer
|
||||||
|
path: 2meet-data-optimizer
|
||||||
|
ref: v1.0.0
|
||||||
|
|
||||||
|
- name: Setup PHP 8.3
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
tools: composer:v2, wp-cli
|
||||||
|
|
||||||
|
- name: Install 2meet-data-optimizer dependencies
|
||||||
|
working-directory: 2meet-data-optimizer
|
||||||
|
run: composer install --no-interaction --no-dev --prefer-dist
|
||||||
|
|
||||||
|
- name: Bootstrap minimal WP for wp-cli
|
||||||
|
run: |
|
||||||
|
# Install a throwaway WordPress so wp-cli has runtime context.
|
||||||
|
wp core download --path=/tmp/wp --skip-content
|
||||||
|
wp config create --path=/tmp/wp --dbname=wp_lint --dbuser=root --dbpass=root --dbhost=127.0.0.1
|
||||||
|
# Symlink 2meet-data-optimizer into the wp-content/plugins dir so its CLI loads.
|
||||||
|
mkdir -p /tmp/wp/wp-content/plugins
|
||||||
|
ln -s "$GITHUB_WORKSPACE/2meet-data-optimizer" /tmp/wp/wp-content/plugins/2meet-data-optimizer
|
||||||
|
|
||||||
|
- name: Run wp tmdo lint --strict
|
||||||
|
run: |
|
||||||
|
cd /tmp/wp
|
||||||
|
wp --skip-themes --skip-plugins=all tmdo lint \
|
||||||
|
--plugin="$GITHUB_WORKSPACE/this-plugin" \
|
||||||
|
--strict \
|
||||||
|
--max-autoload=30
|
||||||
|
# Exit non-zero on:
|
||||||
|
# - Direct SELECT FROM wp_postmeta / wp_usermeta / wp_termmeta / wp_commentmeta
|
||||||
|
# - update_post_meta() on a field already registered to the TMDO Schema Registry
|
||||||
|
# - autoload=yes options exceeding --max-autoload (default 30)
|
||||||
|
# - meta_query with ≥3 conditions but no wpdo_register_fields
|
||||||
|
# Bypass: phpcs:ignore WPDO.AntiEAV.<rule> -- <reason>
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform diagnostic: raw meta inspection for FSM state advisor
|
|
||||||
/**
|
/**
|
||||||
* TMDO_FSM_Advisor — Recommends the next FSM state per module (v2.4.0 M12).
|
* TMDO_FSM_Advisor — Recommends the next FSM state per module (v2.4.0 M12).
|
||||||
*
|
*
|
||||||
@@ -22,6 +21,10 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform diagnostic: raw meta inspection for FSM state advisor
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* @since 0.1.0
|
* @since 0.1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* @since 0.1.0
|
* @since 0.1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
* @package TMDO
|
* @package TMDO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
* @since 2.14.0
|
* @since 2.14.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Comment_Stress_Tester — Async stress fixture generator for comment entity (v2.13.1).
|
* TMDO_Comment_Stress_Tester — Async stress fixture generator for comment entity (v2.13.1).
|
||||||
*
|
*
|
||||||
@@ -21,6 +20,10 @@
|
|||||||
* @since 2.13.1
|
* @since 2.13.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
* @since 2.12.0
|
* @since 2.12.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
* @since 2.15.0 (v2 GCM, AEAD authentication)
|
* @since 2.15.0 (v2 GCM, AEAD authentication)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -14,6 +16,35 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
*/
|
*/
|
||||||
class TMDO_Logger {
|
class TMDO_Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request-scoped correlation id, lazily generated.
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
private static ?string $trace_id = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request-scoped UUIDv4 correlation id.
|
||||||
|
*
|
||||||
|
* Every audit row written during one request shares this value so a single
|
||||||
|
* update_*_meta() call can be traced across entity groups.
|
||||||
|
*
|
||||||
|
* @return string UUIDv4.
|
||||||
|
*/
|
||||||
|
public static function trace_id(): string {
|
||||||
|
if ( null === self::$trace_id ) {
|
||||||
|
try {
|
||||||
|
$bytes = random_bytes( 16 );
|
||||||
|
} catch ( \Throwable $e ) {
|
||||||
|
$bytes = pack( 'H*', md5( (string) microtime( true ) . wp_generate_password( 16, false ) ) );
|
||||||
|
}
|
||||||
|
$bytes[6] = chr( ( ord( $bytes[6] ) & 0x0f ) | 0x40 );
|
||||||
|
$bytes[8] = chr( ( ord( $bytes[8] ) & 0x3f ) | 0x80 );
|
||||||
|
self::$trace_id = vsprintf( '%s%s-%s-%s-%s-%s%s%s', str_split( bin2hex( $bytes ), 4 ) );
|
||||||
|
}
|
||||||
|
return self::$trace_id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an INFO-level event (event-based signature, used by v2.0.0 engine code).
|
* Log an INFO-level event (event-based signature, used by v2.0.0 engine code).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform shadow verifier: must read raw meta to verify zone correctness
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Post_Shadow_Verifier — Sample-and-compare flat vs wp_postmeta (v2.10.3).
|
* TMDO_Post_Shadow_Verifier — Sample-and-compare flat vs wp_postmeta (v2.10.3).
|
||||||
*
|
*
|
||||||
@@ -22,6 +21,10 @@
|
|||||||
* @since 2.10.3
|
* @since 2.10.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform shadow verifier: must read raw meta to verify zone correctness
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Post_Stress_Tester — Bulk fixture generator for post entity migration (v2.9.4).
|
* TMDO_Post_Stress_Tester — Bulk fixture generator for post entity migration (v2.9.4).
|
||||||
*
|
*
|
||||||
@@ -26,6 +25,10 @@
|
|||||||
* @since 2.9.4
|
* @since 2.9.4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 2.9.0
|
* @since 2.9.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -644,7 +646,7 @@ class TMDO_REST_API {
|
|||||||
return $err;
|
return $err;
|
||||||
}
|
}
|
||||||
|
|
||||||
$views = TMDO_Listing_Stats::get_view_count( $post_id );
|
$views = self::read_view_count( $post_id );
|
||||||
|
|
||||||
return new WP_REST_Response(
|
return new WP_REST_Response(
|
||||||
array(
|
array(
|
||||||
@@ -728,8 +730,8 @@ class TMDO_REST_API {
|
|||||||
// Mark as counted for this IP/session.
|
// Mark as counted for this IP/session.
|
||||||
set_transient( $rl_key, 1, HOUR_IN_SECONDS );
|
set_transient( $rl_key, 1, HOUR_IN_SECONDS );
|
||||||
|
|
||||||
TMDO_Listing_Stats::increment_view( $post_id );
|
self::bump_view_count( $post_id );
|
||||||
$views = TMDO_Listing_Stats::get_view_count( $post_id );
|
$views = self::read_view_count( $post_id );
|
||||||
|
|
||||||
$response = new WP_REST_Response(
|
$response = new WP_REST_Response(
|
||||||
array(
|
array(
|
||||||
@@ -1640,4 +1642,37 @@ class TMDO_REST_API {
|
|||||||
$result = TMDO_Migration_Orchestrator::resume();
|
$result = TMDO_Migration_Orchestrator::resume();
|
||||||
return new WP_REST_Response( $result, ! empty( $result['ok'] ) ? 200 : 409 );
|
return new WP_REST_Response( $result, ! empty( $result['ok'] ) ? 200 : 409 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a post's view counter.
|
||||||
|
*
|
||||||
|
* Delegates to the HivePress AddOn when it is active, because that class
|
||||||
|
* adds an hp_view_count postmeta fallback for listings migrated before the
|
||||||
|
* warm zone existed. Without the AddOn — the normal case for a plain
|
||||||
|
* install — core reads its own warm row directly. Calling the AddOn class
|
||||||
|
* unconditionally used to fatal these endpoints on any site without it.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function read_view_count( int $post_id ): int {
|
||||||
|
if ( class_exists( 'TMDO_Listing_Stats' ) ) {
|
||||||
|
return (int) TMDO_Listing_Stats::get_view_count( $post_id );
|
||||||
|
}
|
||||||
|
return (int) ( TMDO_Zone_Warm::get( $post_id, TMDO_Zone_Warm::VIEW_KEY ) ?? 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment a post's view counter.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function bump_view_count( int $post_id ): void {
|
||||||
|
if ( class_exists( 'TMDO_Listing_Stats' ) ) {
|
||||||
|
TMDO_Listing_Stats::increment_view( $post_id );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TMDO_Zone_Warm::increment( $post_id, TMDO_Zone_Warm::VIEW_KEY, 1, TMDO_Zone_Warm::VIEW_TTL );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
* @since 0.2.0
|
* @since 0.2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 2.13.3
|
* @since 2.13.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
* @since 2.12.6
|
* @since 2.12.6
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
* @since 2.12.5
|
* @since 2.12.5
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Term_Stress_Tester — Async stress fixture generator for term entity (v2.13.0).
|
* TMDO_Term_Stress_Tester — Async stress fixture generator for term entity (v2.13.0).
|
||||||
*
|
*
|
||||||
@@ -22,6 +21,10 @@
|
|||||||
* @since 2.13.0
|
* @since 2.13.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
* @since 2.12.0
|
* @since 2.12.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
|
||||||
/**
|
/**
|
||||||
* TMDO_User_Stress_Tester — User entity 壓力測試 / Benchmark 工具
|
* TMDO_User_Stress_Tester — User entity 壓力測試 / Benchmark 工具
|
||||||
*
|
*
|
||||||
@@ -16,6 +15,10 @@
|
|||||||
* @since 2.6.7
|
* @since 2.6.7
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,Generic.CodeAnalysis.UnusedFunctionParameter,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.PHP.YodaConditions,WordPress.WP.AlternativeFunctions.rand_mt_rand,WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- 測試工具:mt_rand 用於杜撰測試資料;serialize() 為 WP capabilities 標準格式
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,Generic.CodeAnalysis.UnusedFunctionParameter,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.PHP.YodaConditions,WordPress.WP.AlternativeFunctions.rand_mt_rand,WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- 測試工具:mt_rand 用於杜撰測試資料;serialize() 為 WP capabilities 標準格式
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
// phpcs:ignore WPDO.AntiEAV -- platform diagnostic: raw meta inspection for site health
|
|
||||||
/**
|
/**
|
||||||
* TMDO_Site_Health — WordPress Site Health integration (v2.2.0 M3).
|
* TMDO_Site_Health — WordPress Site Health integration (v2.2.0 M3).
|
||||||
*
|
*
|
||||||
@@ -17,6 +16,10 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// phpcs:ignore WPDO.AntiEAV -- platform diagnostic: raw meta inspection for site health
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 2.6.2
|
* @since 2.6.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
* @since 1.3.0
|
* @since 1.3.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
* @since 1.5.0
|
* @since 1.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* @since 1.1.2
|
* @since 1.1.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* @since 2.6.6
|
* @since 2.6.6
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions -- inherits engine coding standard.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions -- inherits engine coding standard.
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
* @since 2.9.1
|
* @since 2.9.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
* @since 2.12.1
|
* @since 2.12.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
* @since 2.12.4
|
* @since 2.12.4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
* @since 3.0.1
|
* @since 3.0.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_DB::table(); column validated by subclass FIELD_MAP constant.
|
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_DB::table(); column validated by subclass FIELD_MAP constant.
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user