From 47288db8266cf063fe3c28e987cd4454b16b8132 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 31 Jul 2026 05:06:36 +0800 Subject: [PATCH] chore: initial snapshot of 2meet-data-optimizer-woocommerce-addon v0.1.0 Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY --- .gitignore | 16 + 2meet-data-optimizer-woocommerce-addon.php | 37 + CHANGELOG.md | 22 + CLAUDE.md | 74 + DEPLOY.md | 43 + DESIGN.md | 44 + PLAN.md | 43 + README.md | 65 + SECURITY.md | 36 + admin/class-tmdo-admin-wc.php | 238 ++ composer.json | 25 + composer.lock | 2106 +++++++++++++++++ includes/class-tmdo-wc-orders-interceptor.php | 170 ++ includes/class-tmdo-wc-term-count-filter.php | 236 ++ includes/class-tmdo-woocommerce-bootstrap.php | 62 + includes/class-tmdo-woocommerce.php | 672 ++++++ uninstall.php | 14 + 17 files changed, 3903 insertions(+) create mode 100644 .gitignore create mode 100644 2meet-data-optimizer-woocommerce-addon.php create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 DEPLOY.md create mode 100644 DESIGN.md create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 admin/class-tmdo-admin-wc.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 includes/class-tmdo-wc-orders-interceptor.php create mode 100644 includes/class-tmdo-wc-term-count-filter.php create mode 100644 includes/class-tmdo-woocommerce-bootstrap.php create mode 100644 includes/class-tmdo-woocommerce.php create mode 100644 uninstall.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ac74fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +vendor/ +node_modules/ +.phpunit.result.cache +.phpcs-cache +*.log +*.bak +*.swp +.DS_Store +Thumbs.db +.idea/ +.vscode/ +.full-review/ +.gstack/ +.playwright-mcp/ +.claude/ +dist/ diff --git a/2meet-data-optimizer-woocommerce-addon.php b/2meet-data-optimizer-woocommerce-addon.php new file mode 100644 index 0000000..3daa3de --- /dev/null +++ b/2meet-data-optimizer-woocommerce-addon.php @@ -0,0 +1,37 @@ + 父外掛:`../2meet-data-optimizer/CLAUDE.md` +> 父環境:`/var/www/Studio/wp-local-dev30/CLAUDE.md` +> 全域:`~/.claude/CLAUDE.md`(含 Karpathy Guidelines) + +--- + +## Reset recovery + +1. 讀本檔 +2. 讀 [PLAN.md](PLAN.md) 看 Phase 3 進度 +3. 接續下一個未完成步驟 + +--- + +## Plugin 概述 + +**2meet-data-optimizer-woocommerce-addon** 是 2meet-data-optimizer 的 Woocommerce 整合 AddOn。 + +- **目標外掛**: `woocommerce` +- **Phase**: 3 +- **預估行數**: ~1316 +- **Slug 短碼**: `woocommerce` + +簡述:WooCommerce 整合:20 表 schema 註冊 + commission tracking + product/order zone + HPOS notice + +完整拆分計畫見 `~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md`。 + +--- + +## 邊界 + +- ✅ 本 AddOn 觸碰:woocommerce 相關 schema 註冊(postmeta zone fields / custom tables / entity groups)+ 可選 admin / CLI +- ❌ 本 AddOn **不**觸碰:2meet-data-optimizer 核心、其他 AddOn、目標外掛 runtime 邏輯 +- ❌ 本 AddOn **不**繞過:Hook Bus / TMDO_API(必須走公開合約) + +--- + +## 命名前綴 + +- 類別:`TMDO_Woocommerce_*` +- 函式:`tmdo_woocommerce_*` +- 常數:`TMDO_WOOCOMMERCE_VERSION` / `TMDO_WOOCOMMERCE_PATH` / `TMDO_WOOCOMMERCE_URL` / `TMDO_WOOCOMMERCE_FILE` +- Text-domain:`tmdo-woocommerce` + +--- + +## 公開合約消費(從核心) + +```php +// 安全消費 API(必加 class_exists guard) +if ( class_exists( 'TMDO_API' ) ) { + TMDO_API::set_entity( 'user', $id, 'key', $value ); +} + +// Schema 註冊 +add_action( 'tmdo_register_entity_fields', function ( $registry_class ) { + $registry_class::register_group( '...', '...', [ ... ] ); +} ); + +add_action( 'tmdo_register_custom_tables', function ( $registry ) { + $registry->register( 'woocommerce-addon', [ ... ] ); +} ); +``` + +--- + +## Phase 進度 + +``` +Phase 0 ✅ 骨架建立 (2026-05-15) +Phase 3 ⬜ Implementation v0.1.0 +``` diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..76a5a4d --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,43 @@ +# 部署流程 + +## Prerequisites + +1. `2meet-data-optimizer` 核心 ≥ 0.1.0 已安裝並啟用 +2. `woocommerce` 已安裝並啟用 + +> 本 AddOn 透過 WordPress `Requires Plugins:` 強制相依,先決條件未滿足時無法啟用。 + +--- + +## Steps + +```bash +# 上傳 + 啟用 +wp plugin install /path/to/2meet-data-optimizer-woocommerce-addon-v0.1.0.zip --activate + +# 驗證 +wp tmdo doctor --provider=woocommerce-addon +wp tmdo custom-tables --provider=woocommerce-addon +``` + +--- + +## Rollback + +```bash +wp plugin deactivate 2meet-data-optimizer-woocommerce-addon +# 目標外掛 (woocommerce) 仍可正常運作, +# 讀寫 fall back 到 native meta(失去 zone 加速但功能不受影響) +``` + +--- + +## 整體部署順序建議 + +依 Phase 順序: +1. `2meet-data-optimizer` 核心 +2. Phase 2: `2meet-data-optimizer-hub-addon`、`2meet-data-optimizer-spoke-addon` +3. Phase 3: HivePress / WooCommerce / LatePoint AddOn +4. Phase 4: 6 個 2meet 家族 AddOn + +詳見核心外掛 [DEPLOY.md](../2meet-data-optimizer/DEPLOY.md)。 diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ecf9246 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,44 @@ +# DESIGN — 2meet Data Optimizer Woocommerce AddOn + +> 父外掛架構決策見 [`../2meet-data-optimizer/DESIGN.md`](../2meet-data-optimizer/DESIGN.md) + +--- + +## 角色定位 + +本 AddOn 是 `2meet-data-optimizer` 的整合層,負責將 `woocommerce` 的 EAV 模式(postmeta / 自訂表)橋接到核心反 EAV 引擎。 + +- **不**修改 `woocommerce` 本身 +- **不**繞過 `2meet-data-optimizer` 核心 +- 只透過公開合約(`TMDO_API`、`tmdo_register_*` hook)參與 + +--- + +## 啟動順序 + +``` +plugins_loaded:4 2meet-data-optimizer (core) 啟動 +plugins_loaded:5-6 本 AddOn bootstrap → 註冊 hook callback +plugins_loaded:6+ woocommerce 本體啟動 +init:0 核心 fire tmdo_register_* → 本 AddOn 的 callback 執行 +``` + +詳見 plan 文件第 3 章「載入順序」。 + +--- + +## 為什麼是獨立 AddOn 而非塞進核心 + +- 核心 `2meet-data-optimizer` 對外承諾「零依存任何業務外掛」 +- 整合層發版節奏應跟隨 `woocommerce` 而非核心 +- 站台不用 `woocommerce` 時不需被迫載入這段程式碼 + +--- + +## 邊界檢查清單 + +- [ ] 不直接 `SELECT FROM wp_*meta` +- [ ] 不繞過 Hook Bus +- [ ] 所有 SQL 走 `$wpdb->prepare()` +- [ ] DB 表名走 `TMDO_DB::table()` +- [ ] 對外只 expose `TMDO_Woocommerce_*` 命名空間 diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..84546c4 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,43 @@ +# PLAN — 2meet Data Optimizer Woocommerce AddOn + +> 完整四階段拆分計畫見 `~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md` (本 AddOn 屬 **Phase 3**) + +--- + +## Current state + +- **Version**: 0.1.0 (in development, scaffold complete 2026-05-15) +- **Phase**: Phase 0 → Phase 3 pending +- **Source**: 從 `wp-data-optimizer v2.16.0` 提煉 +- **預估行數**: ~1316 行 +- **目標外掛**: `woocommerce` + +--- + +## Phase 0 ✅ — 骨架建立 (2026-05-15) + +- [x] 目錄結構 +- [x] Plugin Header +- [x] Bootstrap stub +- [x] 7 件 MD +- [x] composer.json / .gitignore / uninstall.php + +--- + +## Phase 3 ⬜ — Implementation v0.1.0 + +詳細任務見上層 plan 第 5 章。簡述: + +- [ ] 從 wp-data-optimizer 對應檔案搬入 includes/ + - [ ] WPDO_ → TMDO_Woocommerce_ 重命名 + - [ ] 移除對 wp-data-optimizer 內部 class 的依存(改走 TMDO_API 公開合約) +- [ ] 完善 bootstrap:實作 init() 載入順序 +- [ ] 補完 7 件 MD 的細節 +- [ ] 跑 `scripts/package-plugin.sh 2meet-data-optimizer-woocommerce-addon` 通過 10 終檢 +- [ ] dev30 e2e + +--- + +## Lessons learned + +- (Phase 3 起記錄) diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf4b96a --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# 2meet Data Optimizer — WooCommerce AddOn + +> 為 **WooCommerce** 提供 schema 註冊、commission tracking 與 product/order 反 EAV 加速。 + +--- + +## What it does + +| 項目 | 內容 | +|---|---| +| Custom tables | WC 20 張 schema 註冊到 TMDO Custom Table Registry(`wc_admin_notes` / `wc_orders` (HPOS) / `wc_product_meta_lookup` 等)| +| Product zone | `_price` / `_sku` / `_stock_status` / `_featured` 等 11 欄位 → Hot zone (indexed) | +| Cold zone | `_product_attributes` / `_purchase_note` | +| Subscription | WC Subscriptions 偵測 + 對應欄位 | +| Order zone | `_order_total` / `_billing_email` / `_payment_method` 等(HPOS 關閉時)| +| Customer entity | `_money_spent` / `_order_count` / `_last_order` | +| Commission tracking | `wp_wpdo_wc_commissions` 表(marketplace `_hp_vendor` / `_hp_commission` 等)| +| Term count filter | `product_count_*` termmeta → wp_options transient(避免 termmeta 脹大)| +| HPOS notice | 偵測 HPOS 是否啟用,未啟用時 admin notice 推薦升級 | + +--- + +## Requirements + +- WordPress ≥ 6.0 +- PHP ≥ 8.1 +- 2meet-data-optimizer ≥ 0.1.0 +- WooCommerce ≥ 3.3(HPOS 表本身需 8.2+) +- **軟相依**:`2meet-data-optimizer-hivepress-addon` — commission tracking 需要 HP marketplace 提供 `_hp_vendor` / `_hp_listing` 等 meta,WC-only 站台 commission interceptor 會自動 skip + +--- + +## Installation + +```bash +wp plugin install /path/to/2meet-data-optimizer-woocommerce-addon-v0.1.0.zip --activate + +# 驗證 +wp tmdo custom-tables --provider=woocommerce +wp tmdo doctor --table=wpdo_wc_commissions +``` + +--- + +## HPOS(High Performance Order Storage) + +WooCommerce 8.2+ 引入 HPOS 後,order 自有 custom table,本 AddOn 偵測到 HPOS 啟用時會: + +- ✅ 仍註冊 20 張 WC 表到 Custom Table Registry(doctor 健診) +- ✅ 仍處理 `_hp_vendor` / `_hp_commission` 等 marketplace meta +- ❌ 不再為 order 建獨立 zone(HPOS 已是反 EAV,無需重複) + +未啟用 HPOS 的舊站台:本 AddOn 為 order 建 Hot zone(5 欄位)作為 legacy 加速。 + +--- + +## Phase + +Phase 3 — 與 [HivePress AddOn](../2meet-data-optimizer-hivepress-addon/) 與 [LatePoint AddOn](../2meet-data-optimizer-latepoint-addon/) 同期 ship。 + +--- + +## License + +GPL-2.0-or-later diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2429d22 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,36 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---|:-:| +| 0.1.x | ✅ | + +--- + +## Reporting a vulnerability + +請參考核心外掛 [SECURITY.md](../2meet-data-optimizer/SECURITY.md)。 + +簡述: +- Email: security@2meet.io +- 請勿在 public GitHub issue 揭露 +- 24h 內初步回應、7 天內提供修補時程 + +--- + +## 安全設計 + +本 AddOn 完全遵守 2meet-data-optimizer 核心安全設計: + +- SQL prepared statements +- Capability check (`manage_options`) +- Nonce 驗證 +- Output escaping +- 反序列化走 `TMDO_Safe_Unserialize` + +--- + +## 已知 Risk + +- (本版本暫無已知安全 risk;Phase 3 ship 時補) diff --git a/admin/class-tmdo-admin-wc.php b/admin/class-tmdo-admin-wc.php new file mode 100644 index 0000000..0e203fa --- /dev/null +++ b/admin/class-tmdo-admin-wc.php @@ -0,0 +1,238 @@ +for_provider( 'woocommerce' ) + : array(); + $vendors = self::top_vendors_by_commission( 10 ); + $totals = self::global_commission_totals(); + ?> +
+

+ +

+ + + + + + + + + + + + + +
+ + + + ⚠️ +   + +
tables
+ +

+ 0 ) : ?> +
+
+
+
+
+
+
$
+
+
+
+
$
+
+
+
+
$
+
+
+
+ +

+ + + + + + + + + + + + + + + + + + + + + +
display_name ) : '#' . (int) $v['vendor_id']; ?>$$$
+ +

+ + +

+ + + + + + + + + + + + + + + + + +
+ +
+ + +
+ get_results( + $wpdb->prepare( + "SELECT vendor_id, COUNT(*) AS order_count, + SUM(subtotal) AS subtotal, + SUM(commission) AS commission, + SUM(vendor_payout) AS payout + FROM `{$table}` + GROUP BY vendor_id + ORDER BY commission DESC + LIMIT %d", + $limit + ), + ARRAY_A + ) ?: array(); + // phpcs:enable + } + + /** + * Global commission summary across all vendors. + * + * @return array + */ + private static function global_commission_totals(): array { + global $wpdb; + $table = TMDO_DB::table( 'wpdo_wc_commissions' ); + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- $table from TMDO_DB::table() (sanitised); aggregate query. + $row = $wpdb->get_row( + "SELECT + COUNT(*) AS order_count, + SUM(subtotal) AS total_subtotal, + SUM(commission) AS total_commission, + SUM(vendor_payout) AS total_payout + FROM `{$table}`", + ARRAY_A + ); + // phpcs:enable + return array( + 'order_count' => (int) ( $row['order_count'] ?? 0 ), + 'total_subtotal' => (float) ( $row['total_subtotal'] ?? 0 ), + 'total_commission' => (float) ( $row['total_commission'] ?? 0 ), + 'total_payout' => (float) ( $row['total_payout'] ?? 0 ), + ); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2b99483 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "2meet/2meet-data-optimizer-woocommerce-addon", + "description": "WooCommerce 整合:20 表 schema 註冊 + commission tracking + product/order zone + HPOS notice", + "type": "wordpress-plugin", + "license": "GPL-2.0-or-later", + "keywords": ["wordpress", "2meet-data-optimizer", "addon", "woocommerce"], + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5", + "wp-coding-standards/wpcs": "^3.0" + }, + "scripts": { + "phpcs": "phpcs --standard=WordPress includes/ 2meet-data-optimizer-woocommerce-addon.php" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..05915be --- /dev/null +++ b/composer.lock @@ -0,0 +1,2106 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cba6cd51d0a978057a719f3b0cf1a78d", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/includes/class-tmdo-wc-orders-interceptor.php b/includes/class-tmdo-wc-orders-interceptor.php new file mode 100644 index 0000000..4567c25 --- /dev/null +++ b/includes/class-tmdo-wc-orders-interceptor.php @@ -0,0 +1,170 @@ + + */ + private const TRACKED_STATUSES = array( 'completed', 'processing' ); + + /** + * Registers WordPress hooks for this interceptor. + * + * @return void + */ + public function register_hooks(): void { + add_action( 'woocommerce_order_status_changed', array( $this, 'action_order_status_changed' ), 10, 3 ); + } + + /** + * Sync order data when WC status changes to a tracked state. + * + * @param int $order_id Order ID. + * @param string $old_status Old status (unused). + * @param string $new_status New status. + * @return void + */ + public function action_order_status_changed( int $order_id, string $old_status, string $new_status ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + if ( ! $this->is_active() ) { + return; + } + if ( ! in_array( $new_status, self::TRACKED_STATUSES, true ) ) { + return; + } + + try { + $this->sync_commission( $order_id, $new_status ); + } catch ( \Throwable $e ) { + TMDO_Logger::error( $this->module, 'woocommerce_order_status_changed', $e->getMessage() ); + } + } + + /** + * INSERT/UPDATE wp_wpdo_wc_commissions for the given order. + * + * Skips orders without a `_hp_vendor` meta — only marketplace orders count. + * + * @param int $wc_order_id WooCommerce order ID. + * @param string $status Order status. + * @return void + */ + private function sync_commission( int $wc_order_id, string $status ): void { + if ( ! function_exists( 'wc_get_order' ) ) { + return; + } + $order = wc_get_order( $wc_order_id ); + if ( ! $order ) { + return; + } + + $vendor_id = (int) $order->get_meta( '_hp_vendor' ); + if ( $vendor_id <= 0 ) { + // Not a marketplace order — skip. + return; + } + + $listing_id = (int) $order->get_meta( '_hp_listing' ); + $subtotal = (float) ( $order->get_meta( '_hp_subtotal' ) ?: 0 ); + $commission = (float) ( $order->get_meta( '_hp_commission' ) ?: 0 ); + $vendor_payout = (float) ( $order->get_meta( '_hp_vendor_payout' ) ?: 0 ); + $commission_rate = (float) ( $order->get_meta( '_hp_commission_rate' ) ?: 0 ); + $hpos_enabled = class_exists( 'TMDO_WooCommerce' ) && TMDO_WooCommerce::is_hpos_enabled() ? 1 : 0; + + $table = TMDO_DB::table( 'wpdo_wc_commissions' ); + $now = TMDO_DB::now(); + + // v2.1.2 race-condition fix: atomic upsert via UNIQUE KEY ui_order_vendor. + // Replaces SELECT-then-INSERT/UPDATE which could race two concurrent + // status-change events (e.g. gateway IPN + admin click) and trigger + // duplicate-key DB errors. Single round-trip; on conflict only updates + // the mutable columns (status / updated_at), preserves financials. + TMDO_DB::upsert( + $table, + array( + 'wc_order_id' => $wc_order_id, + 'vendor_id' => $vendor_id, + 'listing_id' => $listing_id, + 'subtotal' => $subtotal, + 'commission' => $commission, + 'vendor_payout' => $vendor_payout, + 'commission_rate' => $commission_rate, + 'status' => $status, + 'hpos_enabled' => $hpos_enabled, + 'created_at' => $now, + 'updated_at' => $now, + ), + array( 'status', 'updated_at' ), // columns to update on conflict. + array( 'wc_order_id', 'vendor_id' ), // composite unique key. + array( '%d', '%d', '%d', '%f', '%f', '%f', '%f', '%s', '%d', '%s', '%s' ) + ); + } + + /** + * Aggregate commission summary for a vendor. + * + * @param int $vendor_id Vendor user ID. + * @param string $status Optional status filter ('processing' / 'completed' / 'all'). + * @return array{total_subtotal:float, total_commission:float, total_payout:float, order_count:int} + */ + public static function vendor_summary( int $vendor_id, string $status = 'all' ): array { + global $wpdb; + $table = TMDO_DB::table( 'wpdo_wc_commissions' ); + + $where = $wpdb->prepare( ' WHERE vendor_id = %d ', $vendor_id ); + if ( 'all' !== $status ) { + $where .= $wpdb->prepare( ' AND status = %s ', $status ); + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- {$table} via TMDO_DB::table(); {$where} is a pre-validated SQL fragment. + $row = $wpdb->get_row( + "SELECT + SUM(subtotal) AS total_subtotal, + SUM(commission) AS total_commission, + SUM(vendor_payout) AS total_payout, + COUNT(*) AS order_count + FROM `{$table}` {$where}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$table} via TMDO_DB::table(); {$where} is a pre-validated SQL fragment. + ARRAY_A + ); + + return array( + 'total_subtotal' => (float) ( $row['total_subtotal'] ?? 0 ), + 'total_commission' => (float) ( $row['total_commission'] ?? 0 ), + 'total_payout' => (float) ( $row['total_payout'] ?? 0 ), + 'order_count' => (int) ( $row['order_count'] ?? 0 ), + ); + } +} diff --git a/includes/class-tmdo-wc-term-count-filter.php b/includes/class-tmdo-wc-term-count-filter.php new file mode 100644 index 0000000..fc18070 --- /dev/null +++ b/includes/class-tmdo-wc-term-count-filter.php @@ -0,0 +1,236 @@ +` row is a transient-like cache — WC + * recalculates and writes whenever a product is added/removed from a term. + * Pattern is identical to HivePress's per-post TTL cache anti-pattern that + * v2.11.5 solved: the data is genuine cache, but storage location is wrong. + * + * Strategy + * -------- + * Mirror v2.11.5 `TMDO_Hivepress_Transient_Filter`. Intercept term metadata + * writes/reads where meta_key starts with `product_count_` and route to + * wp_options as native transient (no per-key TTL — WC manages its own + * invalidation; we just provide indistinguishable storage). + * + * Translation key: + * wp_options('_transient_wpdo_wc_termcount__') + * + * Namespacing by term_id keeps caches scoped to the right term; md5 of the + * meta_key handles arbitrary-length taxonomy slugs (`product_count_my_long_taxonomy_with_many_chars`). + * + * Default: enabled. Toggle: `wpdo_wc_term_count_filter_enabled` option. + * + * 🔒 Lessons applied (v2.11.7): added to TMDO_Hook_Bus_Bridge::COEXIST_WHITELIST + * so `wp wpdo conflict-scan` does not report a false positive. + * + * @package WP_Data_Optimizer + * @since 2.12.3 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Routes WooCommerce term count cache out of wp_termmeta into wp_options. + */ +final class TMDO_WC_Term_Count_Filter { + + /** Option toggle key. */ + public const OPT_ENABLED = 'wpdo_wc_term_count_filter_enabled'; + + /** Prefix that identifies a WooCommerce term count cache row. */ + public const PREFIX = 'product_count_'; + + /** Namespace prefix for translated wp_options entries. */ + public const TRANSLATED_NAMESPACE = 'wpdo_wc_termcount_'; + + /** + * Register the four metadata filters on `init` priority 5. + * Idempotent — safe to call multiple times. + * + * @return void + */ + public static function init(): void { + if ( ! self::is_enabled() ) { + return; + } + + // Filter priority 9 (before Hook Bus at 10), same as HivePress filter. + add_filter( 'get_term_metadata', array( self::class, 'on_read' ), 9, 4 ); + add_filter( 'add_term_metadata', array( self::class, 'on_add' ), 9, 5 ); + add_filter( 'update_term_metadata', array( self::class, 'on_update' ), 9, 5 ); + add_filter( 'delete_term_metadata', array( self::class, 'on_delete' ), 9, 5 ); + } + + /** + * Check the admin toggle. Defaults to enabled. + * + * @return bool + */ + public static function is_enabled(): bool { + return (bool) get_option( self::OPT_ENABLED, '1' ); + } + + /** + * Test whether a meta_key is a WC term count cache row. + * + * @param mixed $meta_key Candidate meta_key. + * @return bool + */ + public static function is_target_key( $meta_key ): bool { + return is_string( $meta_key ) && str_starts_with( $meta_key, self::PREFIX ); + } + + /** + * Translate (term_id, meta_key) → namespaced wp_options option_name root. + * + * @param int $term_id Term ID. + * @param string $meta_key Original `product_count_*` meta_key. + * @return string Translated option_name root (without `_transient_` prefix). + */ + public static function translate_key( int $term_id, string $meta_key ): string { + return self::TRANSLATED_NAMESPACE . $term_id . '_' . md5( $meta_key ); + } + + /** + * Filter callback: get_term_metadata. + * + * @param mixed $pre Filter accumulator (null at our priority). + * @param int $object_id Term ID. + * @param string $meta_key Meta key being read. + * @param bool $single Whether single value was requested. + * @return mixed + */ + public static function on_read( $pre, $object_id, $meta_key, $single ) { + unset( $single ); + if ( ! self::is_target_key( $meta_key ) ) { + return $pre; + } + $translated = self::translate_key( (int) $object_id, (string) $meta_key ); + $option_name = '_transient_' . $translated; + $value = get_option( $option_name, null ); + + if ( null === $value ) { + // Cache miss — fall through to wp_termmeta (back-compat). + return $pre; + } + + // WP unwraps [0] for single=true callers; return array of values. + return array( $value ); + } + + /** + * Filter callback: add_term_metadata. + * + * @param mixed $check Filter accumulator (null at our priority). + * @param int $object_id Term ID. + * @param string $meta_key Meta key being written. + * @param mixed $meta_value Value being written. + * @param bool $unique Whether the unique flag was set (unused). + * @return mixed + */ + public static function on_add( $check, $object_id, $meta_key, $meta_value, $unique ) { + unset( $unique ); + if ( ! self::is_target_key( $meta_key ) ) { + return $check; + } + self::write_translated( (int) $object_id, (string) $meta_key, $meta_value ); + return true; + } + + /** + * Filter callback: update_term_metadata. + * + * @param mixed $check Filter accumulator (null at our priority). + * @param int $object_id Term ID. + * @param string $meta_key Meta key being written. + * @param mixed $meta_value Value being written. + * @param mixed $prev_value Previous value scope (unused). + * @return mixed + */ + public static function on_update( $check, $object_id, $meta_key, $meta_value, $prev_value ) { + unset( $prev_value ); + if ( ! self::is_target_key( $meta_key ) ) { + return $check; + } + self::write_translated( (int) $object_id, (string) $meta_key, $meta_value ); + return true; + } + + /** + * Filter callback: delete_term_metadata. + * + * @param mixed $check Filter accumulator (null at our priority). + * @param int $object_id Term ID. + * @param string $meta_key Meta key being deleted. + * @param mixed $meta_value Value-scoped delete (unused). + * @param bool $delete_all Whether to delete from all objects (unused). + * @return mixed + */ + public static function on_delete( $check, $object_id, $meta_key, $meta_value, $delete_all ) { + unset( $meta_value, $delete_all ); + if ( ! self::is_target_key( $meta_key ) ) { + return $check; + } + $translated = self::translate_key( (int) $object_id, (string) $meta_key ); + $option_name = '_transient_' . $translated; + delete_option( $option_name ); + return true; + } + + /** + * Internal: persist value to wp_options as a no-expiry transient. + * + * `autoload=no` because WC reads on demand only — never via wp_load_alloptions(). + * + * @param int $term_id Term ID. + * @param string $meta_key Original `product_count_*` meta_key. + * @param mixed $meta_value Value to store. + * @return void + */ + private static function write_translated( int $term_id, string $meta_key, $meta_value ): void { + $translated = self::translate_key( $term_id, $meta_key ); + $option_name = '_transient_' . $translated; + update_option( $option_name, $meta_value, false ); + } + + /** + * One-time migration helper: count `product_count_*` rows in wp_termmeta. + * + * @return int + */ + public static function count_legacy_termmeta_rows(): int { + global $wpdb; + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + return (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$wpdb->termmeta} WHERE meta_key LIKE 'product\\_count\\_%'" + ); + // phpcs:enable + } + + /** + * One-time migration: DELETE all historical `product_count_*` rows from + * wp_termmeta. Future writes auto-route to wp_options. + * + * @return int Rows deleted. + */ + public static function purge_legacy_termmeta_rows(): int { + global $wpdb; + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + return (int) $wpdb->query( + "DELETE FROM {$wpdb->termmeta} WHERE meta_key LIKE 'product\\_count\\_%'" + ); + // phpcs:enable + } +} diff --git a/includes/class-tmdo-woocommerce-bootstrap.php b/includes/class-tmdo-woocommerce-bootstrap.php new file mode 100644 index 0000000..ef1a63a --- /dev/null +++ b/includes/class-tmdo-woocommerce-bootstrap.php @@ -0,0 +1,62 @@ +register_hooks(); + } + + // WC term count filter + if ( class_exists( 'TMDO_WC_Term_Count_Filter' ) ) { + add_action( 'init', array( 'TMDO_WC_Term_Count_Filter', 'init' ), 5 ); + } + } + + public static function missing_core_notice(): void { + echo '

'; + echo esc_html__( '2meet Data Optimizer WooCommerce AddOn 需要 2meet-data-optimizer 核心外掛,請先安裝並啟用。', 'tmdo-woocommerce' ); + echo '

'; + } +} diff --git a/includes/class-tmdo-woocommerce.php b/includes/class-tmdo-woocommerce.php new file mode 100644 index 0000000..50ffab2 --- /dev/null +++ b/includes/class-tmdo-woocommerce.php @@ -0,0 +1,672 @@ + + */ + private const WC_CORE_TABLES = array( + 'wc_admin_note_actions' => array( + 'primary_key' => 'action_id', + 'post_type_link' => null, + 'description' => 'WC admin notes — action buttons', + 'since' => 'WC 4.0', + ), + 'wc_admin_notes' => array( + 'primary_key' => 'note_id', + 'post_type_link' => null, + 'description' => 'WC admin notes', + 'since' => 'WC 4.0', + ), + 'wc_category_lookup' => array( + 'primary_key' => 'category_tree_id', + 'post_type_link' => null, + 'description' => 'Product category hierarchy lookup', + 'since' => 'WC 4.6', + ), + 'wc_customer_lookup' => array( + 'primary_key' => 'customer_id', + 'post_type_link' => null, + 'description' => 'Customer aggregate (anti-EAV by WC core)', + 'since' => 'WC 4.0', + ), + 'wc_download_log' => array( + 'primary_key' => 'download_log_id', + 'post_type_link' => null, + 'description' => 'Downloadable product access log', + 'since' => 'WC 3.3', + ), + 'wc_order_addresses' => array( + 'primary_key' => 'id', + 'post_type_link' => 'shop_order', + 'description' => 'Order billing/shipping addresses (HPOS)', + 'since' => 'WC 8.2', + ), + 'wc_order_coupon_lookup' => array( + 'primary_key' => 'order_id', + 'post_type_link' => 'shop_order', + 'description' => 'Order coupon analytics lookup', + 'since' => 'WC 4.0', + ), + 'wc_order_operational_data' => array( + 'primary_key' => 'id', + 'post_type_link' => 'shop_order', + 'description' => 'Order operational data (HPOS)', + 'since' => 'WC 8.2', + ), + 'wc_order_product_lookup' => array( + 'primary_key' => 'order_item_id', + 'post_type_link' => 'shop_order', + 'description' => 'Order product analytics lookup', + 'since' => 'WC 4.0', + ), + 'wc_order_stats' => array( + 'primary_key' => 'order_id', + 'post_type_link' => 'shop_order', + 'description' => 'Order statistics (anti-EAV by WC core)', + 'since' => 'WC 4.0', + ), + 'wc_order_tax_lookup' => array( + 'primary_key' => null, + 'post_type_link' => 'shop_order', + 'description' => 'Order tax analytics lookup', + 'since' => 'WC 4.0', + ), + 'wc_orders' => array( + 'primary_key' => 'id', + 'post_type_link' => 'shop_order', + 'description' => 'Orders main table (HPOS)', + 'since' => 'WC 8.2', + ), + 'wc_orders_meta' => array( + 'primary_key' => 'id', + 'post_type_link' => 'shop_order', + 'description' => 'Order meta (HPOS replacement for postmeta)', + 'since' => 'WC 8.2', + ), + 'wc_product_attributes_lookup' => array( + 'primary_key' => null, + 'post_type_link' => 'product', + 'description' => 'Product attributes lookup (faceted search)', + 'since' => 'WC 4.9', + ), + 'wc_product_download_directories' => array( + 'primary_key' => 'url_id', + 'post_type_link' => null, + 'description' => 'Approved download directories', + 'since' => 'WC 6.5', + ), + 'wc_product_meta_lookup' => array( + 'primary_key' => 'product_id', + 'post_type_link' => 'product', + 'description' => 'Product meta lookup (anti-EAV by WC core)', + 'since' => 'WC 3.6', + ), + 'wc_rate_limits' => array( + 'primary_key' => 'rate_limit_id', + 'post_type_link' => null, + 'description' => 'API rate limits', + 'since' => 'WC 5.6', + ), + 'wc_reserved_stock' => array( + 'primary_key' => null, + 'post_type_link' => 'product', + 'description' => 'Reserved stock (cart hold)', + 'since' => 'WC 4.5', + ), + 'wc_tax_rate_classes' => array( + 'primary_key' => 'tax_rate_class_id', + 'post_type_link' => null, + 'description' => 'Tax rate classes', + 'since' => 'WC 3.7', + ), + 'wc_webhooks' => array( + 'primary_key' => 'webhook_id', + 'post_type_link' => null, + 'description' => 'Webhooks subscriptions', + 'since' => 'WC 2.6', + ), + ); + + /** + * Wire integration. No-op when WC not detected. + * + * @return void + */ + public static function register(): void { + if ( ! self::is_active() ) { + return; + } + + add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) ); + add_action( 'wpdo_register_fields', array( __CLASS__, 'register_schema_fields' ) ); + // v2.1.6: customer hot fields (_money_spent / _order_count / _last_order) move + // to Entity_Registry so Hook Bus can route them to wp_wpdo_user_hot once + // `entity_user` mode advances past `disabled`. See PLAN-entity-user-cutover.md. + add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_user_entity_fields' ) ); + + // HPOS recommendation notice — only on WC admin pages, only when: + // (a) HPOS is OFF, (b) the threshold of legacy orders has been crossed. + if ( is_admin() ) { + add_action( 'admin_notices', array( __CLASS__, 'maybe_render_hpos_notice' ) ); + } + } + + /** + * Threshold of legacy `shop_order` posts above which we recommend HPOS. + * Below this, the HPOS migration overhead isn't justified. + */ + private const HPOS_RECOMMEND_THRESHOLD = 100; + + /** + * Option name used to permanently dismiss the notice. + */ + private const HPOS_NOTICE_DISMISSED_OPT = 'wpdo_hpos_notice_dismissed'; + + /** + * Conditionally render the HPOS recommendation admin notice. + * + * @return void + */ + public static function maybe_render_hpos_notice(): void { + // Already enabled or already dismissed. + if ( self::is_hpos_enabled() ) { + return; + } + if ( '1' === (string) get_option( self::HPOS_NOTICE_DISMISSED_OPT, '' ) ) { + return; + } + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + // Handle dismiss action (lightweight, no separate AJAX endpoint). + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only nonce check below. + if ( ! empty( $_GET['wpdo_hpos_dismiss'] ) ) { + $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : ''; + if ( wp_verify_nonce( $nonce, 'wpdo_hpos_dismiss' ) ) { + update_option( self::HPOS_NOTICE_DISMISSED_OPT, '1', false ); + return; + } + } + + $legacy_count = self::count_legacy_orders(); + if ( $legacy_count < self::HPOS_RECOMMEND_THRESHOLD ) { + return; + } + + $dismiss_url = wp_nonce_url( + add_query_arg( 'wpdo_hpos_dismiss', '1' ), + 'wpdo_hpos_dismiss' + ); + $settings_url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=features' ); + + ?> +
+

+ WP Data Optimizer: + WooCommerce → 進階 → 功能', + '關閉' + ); + ?> +

+
+ get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'shop_order'" ); + } + + /** + * Active when WC class loaded OR WC plugin file exists (loose detect for early bootstrap). + * + * @return bool + */ + public static function is_active(): bool { + return class_exists( 'WooCommerce' ) + || ( defined( 'WC_VERSION' ) && WC_VERSION ) + || file_exists( WP_PLUGIN_DIR . '/woocommerce/woocommerce.php' ); + } + + /** + * HPOS (custom orders table) detection. + * + * @return bool True when HPOS is enabled. + */ + public static function is_hpos_enabled(): bool { + // WC OrderUtil class — modern WC API. + if ( class_exists( 'Automattic\\WooCommerce\\Utilities\\OrderUtil' ) + && method_exists( 'Automattic\\WooCommerce\\Utilities\\OrderUtil', 'custom_orders_table_usage_is_enabled' ) + ) { + return (bool) Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); + } + // Fallback: option check. + return 'yes' === get_option( 'woocommerce_custom_orders_table_enabled' ); + } + + /** + * Detected WC version (or 'unknown'). + * + * @return string + */ + public static function wc_version(): string { + if ( defined( 'WC_VERSION' ) ) { + return WC_VERSION; + } + return 'unknown'; + } + + /** + * Register all 20 wp_wc_* tables. + * + * @param mixed $registry TMDO_Custom_Table_Registry (passed by action). + * @return void + */ + public static function register_custom_tables( $registry = null ): void { + if ( ! class_exists( 'TMDO_Custom_Table_Registry' ) ) { + return; + } + $registry = $registry ?: TMDO_Custom_Table_Registry::instance(); + + foreach ( self::WC_CORE_TABLES as $name => $meta ) { + $registry->register( + self::PROVIDER, + array( + 'table_name' => $name, + 'primary_key' => $meta['primary_key'] ?: 'id', + 'post_type_link' => $meta['post_type_link'], + 'doctor_callback' => array( __CLASS__, 'doctor_check' ), + 'description' => $meta['description'] . ' (' . $meta['since'] . ')', + ) + ); + } + + // Our own commission tracking table (v2.1.0). + $registry->register( + self::PROVIDER, + array( + 'table_name' => 'wpdo_wc_commissions', + 'primary_key' => 'id', + 'post_type_link' => null, + 'doctor_callback' => array( __CLASS__, 'doctor_check' ), + 'description' => 'Vendor commission tracking (replaces hpct_wc_orders, HPOS-aware) (WPDO 2.1.0)', + ) + ); + } + + /** + * Register hot-zone meta fields for products + orders + customers. + * + * Delegates to dedicated registrars to keep this method readable. + * + * @param mixed $schema TMDO_Schema_Registry (passed by action). + * @return void + */ + public static function register_schema_fields( $schema = null ): void { + if ( ! class_exists( 'TMDO_Schema_Registry' ) ) { + return; + } + $schema = $schema ?: TMDO_Schema_Registry::instance(); + + self::register_product_fields( $schema ); + + // Order fields only matter when HPOS is OFF (otherwise WC stores them in wc_orders_meta). + if ( ! self::is_hpos_enabled() ) { + self::register_order_fields( $schema ); + } + + // v2.1.6: Customer usermeta hot fields moved out of Schema_Registry — they + // are now registered via `register_user_entity_fields()` on the + // `wpdo_register_entity_fields` action so Hook Bus can intercept them. + // Schema_Registry never had a user-meta interceptor; the previous + // registration was inert. See PLAN-entity-user-cutover.md. + + // Subscription fields if WC Subscriptions OR wc-linepay-subscription is active. + if ( self::has_subscriptions() ) { + self::register_subscription_fields( $schema ); + } + } + + /** + * Product hot fields (search/filter shop pages). + * + * @param TMDO_Schema_Registry $schema Registry. + * @return void + */ + private static function register_product_fields( $schema ): void { + $schema->register_many( + self::PROVIDER, + array( + array( + 'post_type' => 'product', + 'meta_key' => '_price', + 'zone' => 'hot', + 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', + 'column' => '_price', + 'indexed' => true, + ), + array( + 'post_type' => 'product', + 'meta_key' => '_regular_price', + 'zone' => 'hot', + 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', + 'column' => '_regular_price', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_sale_price', + 'zone' => 'hot', + 'data_type' => 'decimal(15,4) NULL', + 'column' => '_sale_price', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_sku', + 'zone' => 'hot', + 'data_type' => "varchar(100) NOT NULL DEFAULT ''", + 'column' => '_sku', + 'indexed' => true, + ), + array( + 'post_type' => 'product', + 'meta_key' => '_stock', + 'zone' => 'hot', + 'data_type' => 'decimal(15,4) NULL', + 'column' => '_stock', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_stock_status', + 'zone' => 'hot', + 'data_type' => "varchar(20) NOT NULL DEFAULT 'instock'", + 'column' => '_stock_status', + 'indexed' => true, + ), + array( + 'post_type' => 'product', + 'meta_key' => '_visibility', + 'zone' => 'hot', + 'data_type' => "varchar(20) NOT NULL DEFAULT 'visible'", + 'column' => '_visibility', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_featured', + 'zone' => 'hot', + 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", + 'column' => '_featured', + 'indexed' => true, + ), + array( + 'post_type' => 'product', + 'meta_key' => '_tax_status', + 'zone' => 'hot', + 'data_type' => "varchar(20) NOT NULL DEFAULT 'taxable'", + 'column' => '_tax_status', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_virtual', + 'zone' => 'hot', + 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", + 'column' => '_virtual', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_downloadable', + 'zone' => 'hot', + 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", + 'column' => '_downloadable', + ), + ) + ); + + // Cold zone — large display fields rarely searched but always shown. + $schema->register_many( + self::PROVIDER, + array( + array( + 'post_type' => 'product', + 'meta_key' => '_product_attributes', + 'zone' => 'cold', + 'cache_group' => 'wpdo_cold_product', + 'cache_ttl' => HOUR_IN_SECONDS, + ), + array( + 'post_type' => 'product', + 'meta_key' => '_purchase_note', + 'zone' => 'cold', + 'cache_group' => 'wpdo_cold_product', + 'cache_ttl' => HOUR_IN_SECONDS, + ), + ) + ); + } + + /** + * Order hot fields (legacy postmeta path; only registered when HPOS off). + * + * @param TMDO_Schema_Registry $schema Registry. + * @return void + */ + private static function register_order_fields( $schema ): void { + $schema->register_many( + self::PROVIDER, + array( + array( + 'post_type' => 'shop_order', + 'meta_key' => '_order_total', + 'zone' => 'hot', + 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', + 'column' => '_order_total', + 'indexed' => true, + ), + array( + 'post_type' => 'shop_order', + 'meta_key' => '_order_currency', + 'zone' => 'hot', + 'data_type' => "varchar(8) NOT NULL DEFAULT ''", + 'column' => '_order_currency', + ), + array( + 'post_type' => 'shop_order', + 'meta_key' => '_billing_email', + 'zone' => 'hot', + 'data_type' => "varchar(254) NOT NULL DEFAULT ''", + 'column' => '_billing_email', + 'indexed' => true, + ), + array( + 'post_type' => 'shop_order', + 'meta_key' => '_payment_method', + 'zone' => 'hot', + 'data_type' => "varchar(60) NOT NULL DEFAULT ''", + 'column' => '_payment_method', + 'indexed' => true, + ), + array( + 'post_type' => 'shop_order', + 'meta_key' => '_customer_user', + 'zone' => 'hot', + 'data_type' => 'bigint(20) NOT NULL DEFAULT 0', + 'column' => '_customer_user', + 'indexed' => true, + ), + ) + ); + } + + /** + * Customer usermeta hot fields — registered to Entity_Registry so Hook Bus can + * route them to `wp_wpdo_user_hot` once `entity_user` mode advances past `disabled`. + * + * Replaces the v2.1.5 `register_customer_fields()` which targeted Schema_Registry + * (zone-based, no user interceptor) — that path was inert. See + * PLAN-entity-user-cutover.md for the migration playbook. + * + * Type mapping (Entity_Registry uses fixed-precision DECIMAL(18,6) / BIGINT(20), + * which is a superset of the prior `decimal(15,4)` / `bigint(20)` declarations — + * no precision loss): + * - _money_spent → decimal (DECIMAL(18,6)) + * - _order_count → integer (BIGINT(20)) + * - _last_order → integer (BIGINT(20)) + * + * @return void + */ + public static function register_user_entity_fields(): void { + if ( ! class_exists( 'TMDO_Entity_Registry' ) ) { + return; + } + TMDO_Entity_Registry::register_group( + 'user', + 'hot', + array( + array( + 'key' => '_money_spent', + 'type' => 'decimal', + 'searchable' => true, + 'label' => 'WC customer cumulative spend', + ), + array( + 'key' => '_order_count', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'WC customer order count', + ), + array( + 'key' => '_last_order', + 'type' => 'integer', + 'label' => 'WC customer last order ID', + ), + ) + ); + } + + /** + * Subscription product fields (wc-subscriptions or wc-linepay-subscription). + * + * @param TMDO_Schema_Registry $schema Registry. + * @return void + */ + private static function register_subscription_fields( $schema ): void { + $schema->register_many( + self::PROVIDER, + array( + array( + 'post_type' => 'product', + 'meta_key' => '_subscription_price', + 'zone' => 'hot', + 'data_type' => 'decimal(15,4) NULL', + 'column' => '_subscription_price', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_subscription_period', + 'zone' => 'hot', + 'data_type' => 'varchar(20) NULL', + 'column' => '_subscription_period', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_subscription_period_interval', + 'zone' => 'hot', + 'data_type' => 'int(11) NULL', + 'column' => '_subscription_period_interval', + ), + array( + 'post_type' => 'product', + 'meta_key' => '_subscription_length', + 'zone' => 'hot', + 'data_type' => 'int(11) NULL', + 'column' => '_subscription_length', + ), + ) + ); + } + + /** + * Subscription extension detection — WC native or wc-linepay-subscription. + * + * @return bool + */ + private static function has_subscriptions(): bool { + return class_exists( 'WC_Subscriptions' ) + || defined( 'WCS_INIT_TIMESTAMP' ) + || file_exists( WP_PLUGIN_DIR . '/wc-linepay-subscription/wc-linepay-subscription.php' ); + } + + /** + * WPDO doctor probe for any wc_* table — verifies existence + reports row count. + * + * @param string $table_suffix Suffix without prefix. + * @return array{ok:bool, message:string} + */ + public static function doctor_check( string $table_suffix ): array { + global $wpdb; + $full = $wpdb->prefix . $table_suffix; + $exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( 'SHOW TABLES LIKE %s', $full ) + ); + if ( ! $exists ) { + $is_hpos_table = in_array( $table_suffix, array( 'wc_orders', 'wc_orders_meta', 'wc_order_addresses', 'wc_order_operational_data' ), true ); + $msg = $is_hpos_table + ? "{$full} 不存在 — HPOS 尚未啟用或 WC 版本 < 8.2。" + : "{$full} 不存在 — 請確認 WooCommerce 已啟用。"; + return array( + 'ok' => false, + 'message' => $msg, + ); + } + // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$full}`" ); + return array( + 'ok' => true, + 'message' => "{$full}: {$count} rows", + ); + } +} diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..7e1e55a --- /dev/null +++ b/uninstall.php @@ -0,0 +1,14 @@ +