From zenbu-powers
PHP WordPress 整合測試程式碼品質規範。涵蓋 SOLID、Test Class 組織、 IntegrationTestCase 基類規範、Meta 清理、WordPress 特有安全實踐、3 層架構、程式碼品質。 由 /aibdd.auto.php.it.refactor 載入作為重構依據。非 user-invocable。
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
本 skill 定義 PHP WordPress 整合測試與生產程式碼的品質標準,作為 `/zenbu-powers:aibdd.auto.php.it.refactor` 的重構依據。
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Share bugs, ideas, or general feedback.
本 skill 定義 PHP WordPress 整合測試與生產程式碼的品質標準,作為 /zenbu-powers:aibdd.auto.php.it.refactor 的重構依據。
一個 class 只做一件事。將 Service、Repository、NotificationService 分離。
// ❌ 錯誤:一個 class 混合多種職責
class LessonService
{
public function updateProgress(int $userId, int $lessonId, int $progress): void
{
// 直接操作 DB
global $wpdb;
$wpdb->update('lesson_progress', ...);
// 直接寄 email
wp_mail($email, '進度更新', '...');
// 計算業務規則
if ($progress >= 100) { ... }
}
}
// ✅ 正確:職責分離
class LessonService
{
public function __construct(
private LessonProgressRepository $repo,
private NotificationService $notifier
) {}
public function updateProgress(int $userId, int $lessonId, int $progress): void
{
$this->validateProgress($progress);
$this->repo->save($userId, $lessonId, $progress);
if ($progress >= self::COMPLETION_THRESHOLD) {
$this->notifier->notifyCompletion($userId, $lessonId);
}
}
}
透過 interface 與 strategy pattern 擴展,不修改既有程式碼。
interface PaymentGatewayInterface
{
public function charge(int $amount, string $currency): PaymentResult;
}
class StripeGateway implements PaymentGatewayInterface { /* ... */ }
class PaypalGateway implements PaymentGatewayInterface { /* ... */ }
class OrderService
{
public function __construct(private PaymentGatewayInterface $gateway) {}
}
子類別必須能完全替換父類別,不破壞契約。
小而專一的 interface,優於肥大的 interface。
// ✅ 正確
interface ReadableRepository { public function find(int $id); }
interface WritableRepository { public function save($entity): void; }
class LessonRepository implements ReadableRepository, WritableRepository { /* ... */ }
高層模組不依賴低層模組,皆依賴抽象。Service 以 constructor 注入 Repository。
class LessonService
{
public function __construct(
private LessonProgressRepositoryInterface $progressRepo,
private LessonRepositoryInterface $lessonRepo,
) {}
}
tests/integration/
├── IntegrationTestCase.php # 基類
├── Lesson/
│ ├── UpdateVideoProgressTest.php
│ └── CompleteLessonTest.php
├── Order/
│ ├── CreateOrderTest.php
│ └── RefundOrderTest.php
├── Product/
│ └── ListProductsTest.php
└── Helpers/
└── StatusMapper.php
| 元件 | 風格 | 範例 |
|---|---|---|
| Test Class | PascalCase + Test 後綴 | UpdateVideoProgressTest |
| Test Method | test_ 前綴 + snake_case/中文 | test_成功增加影片進度 |
| Subdomain 目錄 | PascalCase | Lesson/, Order/ |
| Helper Class | PascalCase | StatusMapper |
.feature 檔 → 一個 Test ClassScenario / Example → 一個 test_* method// Given / // When / // Then 註解所有整合測試皆繼承此基類。禁止各 Test class 自行實作 set_up / tear_down 基礎邏輯。
<?php
declare(strict_types=1);
namespace Tests\Integration;
abstract class IntegrationTestCase extends \Yoast\WPTestUtils\WPIntegration\TestCase
{
protected ?\Throwable $lastError = null;
protected mixed $queryResult = null;
protected array $ids = [];
protected object $repos;
protected object $services;
abstract protected function configure_dependencies(): void;
public function set_up(): void
{
parent::set_up();
$this->lastError = null;
$this->queryResult = null;
$this->ids = [];
$this->repos = new \stdClass();
$this->services = new \stdClass();
$this->configure_dependencies();
}
protected function assert_operation_succeeded(): void
{
$this->assertNull(
$this->lastError,
'預期操作成功,但發生錯誤: ' . ($this->lastError?->getMessage() ?? '')
);
}
protected function assert_operation_failed(): void
{
$this->assertNotNull($this->lastError, '預期操作失敗,但成功執行');
}
protected function assert_operation_failed_with_type(string $type): void
{
$this->assert_operation_failed();
$this->assertInstanceOf($type, $this->lastError);
}
protected function assert_operation_failed_with_message(string $msg): void
{
$this->assert_operation_failed();
$this->assertStringContainsString($msg, $this->lastError->getMessage());
}
}
$this->ids 儲存 Gherkin 中的人/物識別(如 $this->ids['Alice'] = 123)$this->repos 與 $this->services 為依賴容器configure_dependencies() 僅做 DI wiring,不做資料準備TRUNCATE重構階段(Phase A)必須刪除:
// TODO: ...// [Handler: aggregate-given] 等 Handler 標註// 參考 /zenbu-powers:aibdd.auto.php.it.handlers.xxx 連結提示$this->markTestIncomplete(...) 佔位呼叫必須保留:
// Given / // When / // Then 業務語意註解@test, @testdox, @dataProvider, @group)所有 $wpdb 查詢必須使用 prepare()。
global $wpdb;
// ❌ 錯誤:直接字串插值
$row = $wpdb->get_row("SELECT * FROM {$table} WHERE id = $id");
// ✅ 正確:使用 prepare + placeholder
$row = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id)
);
// ✅ 多參數
$wpdb->prepare(
"SELECT * FROM {$table} WHERE user_id = %d AND status = %s",
$userId,
$status
);
Placeholder 速查:%d 整數、%f 浮點、%s 字串、%i 識別符(欄位名)。
| 情境 | 函數 |
|---|---|
| 一般文字輸入 | sanitize_text_field() |
| 整數 ID | absint() |
sanitize_email() | |
| URL | esc_url_raw() |
| HTML 內容(受限標籤) | wp_kses_post() |
| 檔名 | sanitize_file_name() |
| 情境 | 函數 |
|---|---|
| HTML 內文 | esc_html() |
| HTML 屬性 | esc_attr() |
| URL 輸出 | esc_url() |
| JavaScript 內嵌 | esc_js() |
| 允許部分 HTML | wp_kses() |
// 表單端
wp_nonce_field('myplugin_update_lesson', '_myplugin_nonce');
// 處理端
if (!wp_verify_nonce($_POST['_myplugin_nonce'] ?? '', 'myplugin_update_lesson')) {
wp_die('Security check failed');
}
// Admin page
check_admin_referer('myplugin_update_lesson');
任何寫入/刪除/敏感動作前,必須檢查:
if (!current_user_can('edit_lesson', $lessonId)) {
throw new PermissionDeniedException('無權編輯此課程');
}
| 元件 | 慣例 | 範例 |
|---|---|---|
| Plugin 全域函數 | {prefix}_{action} | myplugin_update_lesson() |
| Class(傳統) | {Prefix}_{Name} | MyPlugin_LessonService |
| Class(PSR-4 推薦) | Namespace | App\Services\LessonService |
| Hook 名稱 | {prefix}_{event} | myplugin_lesson_updated |
| Option key | {prefix}_{name} | myplugin_settings |
| DB table 前綴 | {$wpdb->prefix}{plugin}_{name} | wp_myplugin_lessons |
src/
├── Models/ # Plain PHP objects(不依賴 WordPress)
│ └── LessonProgress.php
├── Repositories/ # WordPress DB 抽象層
│ └── LessonProgressRepository.php
├── Services/ # 業務邏輯層
│ └── LessonService.php
└── Exceptions/ # 自訂例外
├── InvalidStateException.php
└── NotFoundException.php
| 層 | 職責 | 允許使用 | 禁止 |
|---|---|---|---|
| Models | 資料容器 + 純行為 | PHP 標準庫 | WordPress 全域函數、$wpdb |
| Repositories | DB / WP API 封裝 | $wpdb、WP function | 業務規則、拋業務例外 |
| Services | 業務規則、協調 Repo | Repositories、Models | $wpdb 直接呼叫 |
| Exceptions | 業務錯誤訊息 | - | 包含邏輯 |
// ❌ Service 直接操作 $wpdb
class LessonService
{
public function update(int $id): void
{
global $wpdb;
$wpdb->update(...); // 應透過 Repository
}
}
// ❌ Model 依賴 WordPress
class LessonProgress
{
public function save(): void
{
update_post_meta(...); // Model 不該知道 WP
}
}
// ❌ Repository 含業務邏輯
class LessonProgressRepository
{
public function save($progress): void
{
if ($progress->value < 0) {
throw new InvalidStateException(...); // 應在 Service
}
}
}
// ❌ 深層巢狀
public function updateProgress(int $userId, int $lessonId, int $progress): void
{
if ($this->userExists($userId)) {
if ($this->lessonExists($lessonId)) {
if ($progress >= 0 && $progress <= 100) {
// 實際邏輯
}
}
}
}
// ✅ Early Return
public function updateProgress(int $userId, int $lessonId, int $progress): void
{
if (!$this->userExists($userId)) {
throw new NotFoundException('用戶不存在');
}
if (!$this->lessonExists($lessonId)) {
throw new NotFoundException('課程不存在');
}
if ($progress < 0 || $progress > 100) {
throw new InvalidStateException('進度必須在 0-100 之間');
}
// 實際邏輯
}
class LessonService
{
private const COMPLETION_THRESHOLD = 100;
private const STATUS_MAPPING = [
'not_started' => '未開始',
'in_progress' => '進行中',
'completed' => '已完成',
];
}
// ✅ 明確 nullable 型別
public function find(int $id): ?LessonProgress
{
// ...
}
// ✅ 呼叫端 null coalescing throw (PHP 8+)
$progress = $this->repo->find($id)
?? throw new NotFoundException("進度 {$id} 不存在");
三次以上重複才抽取共用方法。勿過早抽象。
| ❌ 模糊 | ✅ 清晰 |
|---|---|
process($data) | updateVideoProgress(int $userId, int $lessonId, int $progress) |
handle() | refundOrder() |
$d | $lessonId |
$flag | $isCompleted |
以 is, has, can, should 開頭:
public function isCompleted(): bool
public function hasPermission(int $userId): bool
public function canEdit(User $user): bool
public function shouldNotify(): bool
重構完成前逐項確認:
// TODO: 殘留// [Handler: xxx] 標註declare(strict_types=1);?Type 明確標註prepare()