フック一覧
フックを使うと、コアのコードを変更せずに既存の処理へ割り込めます。現行バージョンでは 90 個のポイントが定義されており、以下にすべて掲載します。
購読の書き方
<?php
namespace App\Plugin\Demo\Hook;
use App\Controller\Base\View\UserPlugin;
use Kernel\Annotation\Hook;
class Main extends UserPlugin
{
#[Hook(point: 0x130)] // USER_VIEW_FOOTER
public function footer(): void
{
echo '<script>console.log("hi")</script>';
}
}3 つの鉄則
1. 引数は変数でなければならない。
hook() の可変長引数は参照渡しで受け取られるため、呼び出し側でリテラルを渡すと即座に致命的な 500 になります。
hook(P, new X()); // 500
hook(P, $this->getUser()); // 500
hook(P, ['a' => 1]); // 500
hook(P, 'リテラル'); // 500しかもその行が実行されたときにしか落ちません。コアにフックポイントを追加するときは、必ず変数に代入してから渡してください。
2. 購読側は定数ではなく 16 進リテラルで書く。
#[Hook(point: 0x2300)] // 推奨
#[Hook(point: \App\Consts\Hook::XXX)] // 古いコアでプラグインが固まります属性の引数はプラグイン有効化時に評価されます。その定数を持たない古いコアでは Error になり、「START は実行済み、STATUS は未書き込み」という中途半端な状態で止まります。
3. bool を返すとチェーン全体が打ち切られる。
フックのメソッドが bool を返すと、以降の購読者は実行されず、呼び出し元がその値をそのまま受け取ります。これを意図的に使っているのは SERVICE_SMTP_SEND_BEFORE だけです(true はプラグインが送信を引き取ったことを意味します)。それ以外のポイントでは void を返してください。
ビュー系ポイント
引数を取りません。購読側は HTML/CSS/JavaScript を echo するだけです。
管理画面
| 定数 | 値 | 位置 |
|---|---|---|
ADMIN_VIEW_HEADER | 0x2 | 全体の head。CSS はここ |
ADMIN_VIEW_FOOTER | 0x1 | 全体のフッタ。JS はここ |
ADMIN_VIEW_BODY | 0x10201 | 全体の body |
ADMIN_VIEW_MENU | 0x3 | 左サイドメニュー。独自の項目を追加 |
ADMIN_VIEW_NAV | 0x4 | 上部ナビゲーション |
ADMIN_VIEW_AUTH_LOGIN_FORM | 0x60 | 管理画面ログインフォーム内 |
ADMIN_VIEW_USER_HEADER | 0x10002 | 会員管理ページの head |
ADMIN_VIEW_USER_FOOTER | 0x9 | 会員管理ページのフッタ |
ADMIN_VIEW_USER_TOOLBAR | 0x10 | 会員管理ページのツールバー |
ADMIN_VIEW_COMMODITY_TOOLBAR | 0x7 | 商品管理のツールバー |
ADMIN_VIEW_COMMODITY_FOOTER | 0x6 | 商品管理のフッタ |
ADMIN_VIEW_CATEGORY_TOOLBAR | 0x701 | カテゴリ管理のツールバー |
ADMIN_VIEW_ORDER_TOOLBAR | 0x13 | 注文管理のツールバー |
ADMIN_VIEW_ORDER_FOOTER | 0x12 | 注文管理のフッタ |
ADMIN_VIEW_CARD_TOOLBAR | 0x801 | カード管理のツールバー |
ADMIN_VIEW_CARD_FOOTER | 0x802 | カード管理のフッタ |
ADMIN_VIEW_CONFIG_TOOLBAR | 0x14 | サイト設定のツールバー |
メニュー項目の追加例:
#[Hook(point: 0x3)]
public function menu(): void
{
echo '<div class="menu-item"><a class="menu-link" href="/plugin/Demo/api/index">'
. '<span class="menu-title">マイプラグイン</span></a></div>';
}ストアフロント
| 定数 | 値 | 位置 |
|---|---|---|
USER_VIEW_HEADER | 0x128 | ストアフロントの head |
USER_VIEW_BODY | 0x129 | ストアフロントの body |
USER_VIEW_FOOTER | 0x130 | ストアフロントのフッタ |
USER_GLOBAL_VIEW_HEADER | 0x228 | 会員センターを含む全体の head |
USER_GLOBAL_VIEW_BODY | 0x229 | 全体の body |
USER_GLOBAL_VIEW_FOOTER | 0x230 | 全体のフッタ |
USER_VIEW_INDEX_HEADER | 0x10001 | トップページの head |
USER_VIEW_INDEX_BODY | 0x10003 | トップページの body |
USER_VIEW_INDEX_FOOTER | 0x10004 | トップページのフッタ |
USER_VIEW_MENU | 0x57 | 会員センターのメニュー |
USER_VIEW_HEADER_NAV | 0x88 | ストアの上部ナビ(配列型。下記参照) |
USER_VIEW_AUTH_LOGIN_BUTTON | 0x41 | ログインボタンの横 |
USER_VIEW_AUTH_REGISTER_BUTTON | 0x42 | 登録ボタンの横 |
USER_VIEW_SECURITY_NAV | 0x43 | セキュリティ設定のナビ |
USER_VIEW_PERSONAL_FORM | 0x44 | プロフィールフォーム |
USER_VIEW_QUERY_TRADE_NO | 0x89 | 注文照会ページ |
USER_VIEW_HEADER_NAV(0x88)だけは他のビュー系と挙動が異なり、配列型です。プラグインはナビ項目を返し、描画は各テーマが行います。生の HTML を echo しないため、テーマを切り替えてもレイアウトが崩れません。
コアと管理テーブル
| 定数 | 値 | 説明 |
|---|---|---|
KERNEL_INIT | 0x30 | コア初期化完了。最も早い介入点。リクエスト全体を横取りするならここ |
HACK_ROUTE_TABLE_COLUMNS | 0x2005 | 管理テーブルに列を追加する唯一の入口 |
HACK_ROUTE_TABLE_SEARCH | 0x2006 | 管理テーブルに検索条件を追加 |
HACK_SUBMIT_FORM | 0x9038 | 管理フォームに項目を追加 |
HACK_SUBMIT_TAB | 0x9039 | 管理フォームにタブを追加 |
USER_API_AUTH_LOGIN_BEGIN | 0x21 | フロントのログイン開始前 |
USER_API_AUTH_REGISTER_BEGIN | 0x19 | フロントの登録開始前 |
データ系ポイント
引数を取り、参照渡しされます。書き換えると以降の処理に影響します。
注文と決済
| 定数 | 値 | 引数 |
|---|---|---|
USER_API_ORDER_TRADE_BEGIN | 0x16 | array $map 注文時の生データ |
USER_API_ORDER_TRADE_PAY_BEGIN | 0x171 | Commodity $commodity, Order $order, Pay $pay |
USER_API_ORDER_TRADE_AFTER | 0x17 | Commodity $commodity, Order $order, Pay $pay |
USER_API_ORDER_PAY_AFTER | 0x18 | Commodity $commodity, Order $order, Pay $pay 入金完了 |
ORDER_MANUAL_DELIVERY_AFTER | 0x2200 | Order $order, bool $overwrite 手動発送の書き込み後 |
USER_API_RECHARGE_AFTER | 0x18191 | Recharge $recharge, Pay $pay チャージ完了 |
SERVICE_PAY_CALLBACK_FAIL | 0x3010 | string $handle, string $reason, ?string $tradeNo, array $map |
SERVICE_PAY_CALLBACK_FAIL の $reason の値:handle、not_found、credential、plugin、sign、status、duplicate、amount。このうち sign/amount/handle/credential は通常、コールバックの偽造を意味します。duplicate はゲートウェイの重複通知で正常な現象なので、アラートにしないでください。
ORDER_MANUAL_DELIVERY_AFTER の発火時点で $order->secret は新しい内容になっており、delivery_status は 1 です。$overwrite は既存の発送内容を上書きしたかどうかを示します。購入者に「発送しました」と通知するのに適した場所です。
アカウント
| 定数 | 値 | 引数 |
|---|---|---|
USER_API_AUTH_REGISTER_AFTER | 0x20 | User $user |
USER_API_AUTH_LOGIN_AFTER | 0x22 | User $user |
USER_API_AUTH_LOGIN_FAIL | 0x23 | string $account, string $reason |
ADMIN_API_AUTH_LOGIN_AFTER | 0x61 | Manage $manage |
ADMIN_API_AUTH_LOGIN_FAIL | 0x62 | string $email, string $reason |
フロントの失敗理由:not_found、password、banned。 管理画面の失敗理由:throttled、captcha、not_found、password、totp、banned、shift、other(2 段階認証コードの待機は失敗に含みません)。
ログインの総当たり攻撃を検知するのに向いています。
フロントに返すデータ
これらはストアフロントへ返すデータを書き換えます。商品を隠す、表示価格を変える、項目を追加するといった用途です。
| 定数 | 値 | 引数 |
|---|---|---|
USER_API_INDEX_CATEGORY_LIST | 0x49 | array $category |
USER_API_INDEX_COMMODITY_LIST | 0x50 | array $data |
USER_API_INDEX_COMMODITY_DETAIL_INFO | 0x51 | array $item |
USER_API_INDEX_PAY_LIST | 0x53 | array $pay |
USER_API_INDEX_QUERY_LIST | 0x54 | array $data |
USER_API_INDEX_QUERY_SECRET | 0x55 | Order $order |
USER_API_PURCHASE_RECORD_LIST | 0x56 | array $data |
商品と在庫
| 定数 | 値 | 引数 |
|---|---|---|
COMMODITY_CHANGE_AFTER | 0x8100 | int[] $ids, string $action, ?Commodity $before |
CARD_CHANGE_AFTER | 0x8101 | int[] $commodityIds, string $reason |
SERVICE_SHOP_GET_ITEM_STOCK | 0x8000 | Commodity $commodity, string $race, array $sku |
COMMODITY_CHANGE_AFTER は商品の追加・編集・削除・公開状態の変更・一括設定・上流からの同期の後に発火します。必ずデータベーストランザクションのコミット後なので、受け取る内容は確実に永続化済みです。
$action の値:create、update、delete、status、batch、sync。 $before は単一商品の保存経路でのみ編集前のモデルを持ち、それ以外は null です。
一括経路(
status/batch)で渡されるのはリクエストに含まれる ID の集合であり、実際には変更されていない商品も含まれ得ます。購読側は自前のスナップショットと差分を取るべきで、すべての ID が本当に変わったと仮定してはいけません。deleteの時点では行がすでに存在せず、ID しか得られません。
CARD_CHANGE_AFTER はカードプールの変化、つまり自動発送商品の在庫変動の後に発火します。こちらもコミット後です。
$commodityIds は商品の ID であり、カードの ID ではありません。$reason の値:import、edit、lock、unlock、sell、delete。
注文による在庫の減少はここを通りません。
USER_API_ORDER_PAY_AFTERとORDER_MANUAL_DELIVERY_AFTERを使ってください。
チケット
| 定数 | 値 | 引数 |
|---|---|---|
USER_API_TICKET_CREATE_AFTER | 0x2100 | Ticket $ticket, TicketMessage $message |
USER_API_TICKET_REPLY_AFTER | 0x2101 | Ticket $ticket, TicketMessage $message |
ADMIN_API_TICKET_REPLY_AFTER | 0x2102 | Ticket $ticket, TicketMessage $message, Manage $manage |
3 つともトランザクションのコミット後に発火し、フック内で例外を投げても API の結果には影響しません。
メール
| 定数 | 値 | 引数 |
|---|---|---|
SERVICE_SMTP_SEND_BEFORE | 0x3000 | array $config, string $email, string $title, string $content |
SERVICE_SMTP_SEND_SUCCESS | 0x3001 | 同上 |
SERVICE_SMTP_SEND_ERROR | 0x3002 | 同上 |
SERVICE_SMTP_SEND_BEFORE は戻り値でチェーンを打ち切る唯一のポイントです。true を返すとプラグインが送信を引き取ったことになり、コアは SMTP を使いません。メールを Telegram など別の経路に振り替えるときに使います。
コアとルーティング
| 定数 | 値 | 引数 |
|---|---|---|
CONTROLLER_CALL_BEFORE | 0x31 | object $controller, string $action |
CONTROLLER_CALL_AFTER | 0 | object $controller, string $action, mixed $result |
HTTP_ROUTE_RESPONSE | 0x47 | string $routePath, mixed $result |
HTTP_NOT_FOUND | 0x48 | string $routePath ルート未一致 |
RENDER_VIEW | 0x33 | string $result 描画結果。書き換え可能 |
WAF_INTERCEPT | 0x289 | string $message WAF が遮断したとき |
CSP_SOURCE_ALLOW | 0x8102 | array $sources CSP の許可リスト |
LANG_MISS | 0x9100 | array $sourceList, array $langList 未翻訳の文言 |
ADMIN_API_PLUGIN_SAVE_CONFIG | 0x15 | int $id, array $map プラグイン設定の保存時 |
HTTP_NOT_FOUND はスキャン検知に向いています。短時間に大量の 404 が出るのは、ほぼ誰かが探りを入れているときです。
リスク判定・目視確認のポイント
0x2300 から 0x2305 はリスク管理用のグループです。他のフックと 2 点で異なるので、購読前に必ず読んでください。
1. すべて RiskContext $risk を参照渡しで受け取り、書き換えて void を返す
絶対に return true/false しないでください。 ディスパッチャは bool でチェーン全体を打ち切るため、最初に bool を返した購読者が、後続のリスク系プラグインをすべて黙らせてしまいます。
public const PASS = 0; // 許可
public const LIMIT = 1; // 静かに制限(購読側が実装。コアは特別なことをしません)
public const REVIEW = 2; // 目視確認のため保留
public const DENY = 3; // 拒否
$risk->escalate(RiskContext::DENY, 'プラグイン名', lang('理由')); // 上げるだけで、下げません
$risk->hardAllow('プラグイン名', '理由'); // 強制的に許可し、決定を固定
$risk->ref = 'AR-XXXX'; // 追跡用の識別子。そのまま返されますフックから戻った後、コアは $risk->action を読みます。DENY は JSONException を投げ、REVIEW は各場面ごとの保留処理に進み、LIMIT は購読側に委ねられます。
2. 例外だけでは足りない理由
拒否は例外で表現できますが、保留はできません。保留には「アカウントは作るが、セッションは発行しない」ということをコアに伝える必要があり、例外ではそれを表現できないからです。
各ポイント
| 定数 | 値 | 引数 | 位置 |
|---|---|---|---|
USER_API_AUTH_REGISTER_VALIDATED | 0x2300 | $risk, $user | 登録:検証後、INSERT 前 |
USER_API_AUTH_PASSWORD_BEGIN | 0x2301 | $risk, $account | パスワード再設定:コード検証の前 |
USER_API_RECHARGE_TRADE_BEGIN | 0x2302 | $risk, $user, $map | チャージ:金額とチャネルの検証後 |
USER_API_CASH_SUBMIT_BEGIN | 0x2303 | $risk, $user, $map | 出金:紐付け確認後、INSERT 前 |
USER_API_TICKET_CREATE_BEGIN | 0x2304 | $risk, $user, $map | チケット作成:サービス層に入る前 |
USER_API_ORDER_DELIVERY_BEGIN | 0x2305 | $risk, $order, $commodity | 発送の直前 |
いくつかの設計上の補足:
登録(0x2300) が 0x19 より優れている点は 3 つあります。ユーザー名・メール・電話が重複排除済みの最終値であること。$user が参照渡しなのでフィールドを直接書き換えられること。そして囲みの try ブロックの外にあることです。try の中では、どんな例外も「登録に失敗しました」に書き換えられてしまい、こちらが伝えたい理由がユーザーに届きません。REVIEW の場合、コアは $user->status を 0 にし、loginSuccess() を飛ばします。飛ばさないと、ユーザーは「登録成功」の直後、次のクリックでログアウトさせられてしまいます。
パスワード再設定(0x2301) は意図的にコード検証の前に置いてあります。拒否するのに、ユーザーが手にしている認証コードを浪費すべきではありませんし、攻撃者に運営者の SMS 費用を使わせるべきでもありません。
チャージ(0x2302) はコントローラではなくサービス層にあります。コントローラ側では $map を組み立てておらず、金額はサービス層で初めて解析されるためです。$map は読み取り専用のコンテキストで、書き換えても意味がありません(下流は $_POST を直接読みます)。
出金(0x2303) に新しい状態は不要です。cash.status = 0 がもともと「運営者の対応待ち」を意味します。自動着金するのは type == 2(消費可能な残高への換金)だけなので、保留時はその近道を塞ぐだけで足ります。
発送の直前(0x2305) は、入金後にカードを差し止められる唯一の場所であり、ここ 1 か所で全決済経路(0 円注文、残高決済、各ゲートウェイのコールバック)を網羅できます。
お金はすでに受け取っているので、この時点で「拒否」を論じるのは適切ではありません。判断すべきは商品を出すかどうかだけです。したがって購読側は REVIEW のみを使います。delivery_status を 0 のままにし、secret を案内文に差し替える — これは手動発送商品が入金から発送までの間に取る状態と同じです。副作用(カードの取り出し、在庫の減算、報酬と紹介料の明細、発送メール)はどれも実行されていないため、承認後に同じ処理をもう一度冪等に実行するのがちょうど正しい動作になります。
廃止されたポイント
以下の 8 つの定数は Hook.php に残っていますが、コアからはもう一切呼ばれていません。購読してもエラーにはなりませんが、発火することもありません。
| 定数 | 値 | 代わりに使うもの |
|---|---|---|
ADMIN_VIEW_USER_TABLE | 0x8 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_COMMODITY_TABLE | 0x5 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_CATEGORY_TABLE | 0x702 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_ORDER_TABLE | 0x11 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_CATEGORY_POST | 0x703 | HACK_SUBMIT_FORM |
ADMIN_VIEW_COMMODITY_POST | 0x45 | HACK_SUBMIT_FORM |
USER_VIEW_COMMODITY_POST | 0x46 | — |
USER_API_INDEX_TRADE_CALC_AMOUNT | 0x52 | — |
ネット上の古い解説記事では、いまだに
ADMIN_VIEW_USER_TABLEに JSON の列定義を echo して管理テーブルに列を足す方法が紹介されています。現行バージョンではまったく機能しません。
管理テーブルに列を追加する正しい方法
現在の唯一の入口は HACK_ROUTE_TABLE_COLUMNS(0x2005)で、JSON を echo するのではなく Column エンティティを使います。
注意点が 2 つあります。
- 列の描画コード内では
escapeHtmlがグローバルに使えません。エスケープは自分で行ってください Order.amountは文字列です。計算に使う前にキャストしてください
