Skip to content

フック一覧

フックを使うと、コアのコードを変更せずに既存の処理へ割り込めます。現行バージョンでは 90 個のポイントが定義されており、以下にすべて掲載します。

購読の書き方

php
<?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 になります。

php
hook(P, new X());            // 500
hook(P, $this->getUser());   // 500
hook(P, ['a' => 1]);         // 500
hook(P, 'リテラル');          // 500

しかもその行が実行されたときにしか落ちません。コアにフックポイントを追加するときは、必ず変数に代入してから渡してください。

2. 購読側は定数ではなく 16 進リテラルで書く。

php
#[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_HEADER0x2全体の head。CSS はここ
ADMIN_VIEW_FOOTER0x1全体のフッタ。JS はここ
ADMIN_VIEW_BODY0x10201全体の body
ADMIN_VIEW_MENU0x3左サイドメニュー。独自の項目を追加
ADMIN_VIEW_NAV0x4上部ナビゲーション
ADMIN_VIEW_AUTH_LOGIN_FORM0x60管理画面ログインフォーム内
ADMIN_VIEW_USER_HEADER0x10002会員管理ページの head
ADMIN_VIEW_USER_FOOTER0x9会員管理ページのフッタ
ADMIN_VIEW_USER_TOOLBAR0x10会員管理ページのツールバー
ADMIN_VIEW_COMMODITY_TOOLBAR0x7商品管理のツールバー
ADMIN_VIEW_COMMODITY_FOOTER0x6商品管理のフッタ
ADMIN_VIEW_CATEGORY_TOOLBAR0x701カテゴリ管理のツールバー
ADMIN_VIEW_ORDER_TOOLBAR0x13注文管理のツールバー
ADMIN_VIEW_ORDER_FOOTER0x12注文管理のフッタ
ADMIN_VIEW_CARD_TOOLBAR0x801カード管理のツールバー
ADMIN_VIEW_CARD_FOOTER0x802カード管理のフッタ
ADMIN_VIEW_CONFIG_TOOLBAR0x14サイト設定のツールバー

メニュー項目の追加例:

php
#[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_HEADER0x128ストアフロントの head
USER_VIEW_BODY0x129ストアフロントの body
USER_VIEW_FOOTER0x130ストアフロントのフッタ
USER_GLOBAL_VIEW_HEADER0x228会員センターを含む全体の head
USER_GLOBAL_VIEW_BODY0x229全体の body
USER_GLOBAL_VIEW_FOOTER0x230全体のフッタ
USER_VIEW_INDEX_HEADER0x10001トップページの head
USER_VIEW_INDEX_BODY0x10003トップページの body
USER_VIEW_INDEX_FOOTER0x10004トップページのフッタ
USER_VIEW_MENU0x57会員センターのメニュー
USER_VIEW_HEADER_NAV0x88ストアの上部ナビ(配列型。下記参照)
USER_VIEW_AUTH_LOGIN_BUTTON0x41ログインボタンの横
USER_VIEW_AUTH_REGISTER_BUTTON0x42登録ボタンの横
USER_VIEW_SECURITY_NAV0x43セキュリティ設定のナビ
USER_VIEW_PERSONAL_FORM0x44プロフィールフォーム
USER_VIEW_QUERY_TRADE_NO0x89注文照会ページ

USER_VIEW_HEADER_NAV0x88)だけは他のビュー系と挙動が異なり、配列型です。プラグインはナビ項目を返し、描画は各テーマが行います。生の HTML を echo しないため、テーマを切り替えてもレイアウトが崩れません。

コアと管理テーブル

定数説明
KERNEL_INIT0x30コア初期化完了。最も早い介入点。リクエスト全体を横取りするならここ
HACK_ROUTE_TABLE_COLUMNS0x2005管理テーブルに列を追加する唯一の入口
HACK_ROUTE_TABLE_SEARCH0x2006管理テーブルに検索条件を追加
HACK_SUBMIT_FORM0x9038管理フォームに項目を追加
HACK_SUBMIT_TAB0x9039管理フォームにタブを追加
USER_API_AUTH_LOGIN_BEGIN0x21フロントのログイン開始前
USER_API_AUTH_REGISTER_BEGIN0x19フロントの登録開始前

データ系ポイント

引数を取り、参照渡しされます。書き換えると以降の処理に影響します。

注文と決済

定数引数
USER_API_ORDER_TRADE_BEGIN0x16array $map 注文時の生データ
USER_API_ORDER_TRADE_PAY_BEGIN0x171Commodity $commodity, Order $order, Pay $pay
USER_API_ORDER_TRADE_AFTER0x17Commodity $commodity, Order $order, Pay $pay
USER_API_ORDER_PAY_AFTER0x18Commodity $commodity, Order $order, Pay $pay 入金完了
ORDER_MANUAL_DELIVERY_AFTER0x2200Order $order, bool $overwrite 手動発送の書き込み後
USER_API_RECHARGE_AFTER0x18191Recharge $recharge, Pay $pay チャージ完了
SERVICE_PAY_CALLBACK_FAIL0x3010string $handle, string $reason, ?string $tradeNo, array $map

SERVICE_PAY_CALLBACK_FAIL$reason の値:handlenot_foundcredentialpluginsignstatusduplicateamount。このうち signamounthandlecredential は通常、コールバックの偽造を意味します。duplicate はゲートウェイの重複通知で正常な現象なので、アラートにしないでください。

ORDER_MANUAL_DELIVERY_AFTER の発火時点で $order->secret は新しい内容になっており、delivery_status1 です。$overwrite は既存の発送内容を上書きしたかどうかを示します。購入者に「発送しました」と通知するのに適した場所です。

アカウント

定数引数
USER_API_AUTH_REGISTER_AFTER0x20User $user
USER_API_AUTH_LOGIN_AFTER0x22User $user
USER_API_AUTH_LOGIN_FAIL0x23string $account, string $reason
ADMIN_API_AUTH_LOGIN_AFTER0x61Manage $manage
ADMIN_API_AUTH_LOGIN_FAIL0x62string $email, string $reason

フロントの失敗理由:not_foundpasswordbanned。 管理画面の失敗理由:throttledcaptchanot_foundpasswordtotpbannedshiftother(2 段階認証コードの待機は失敗に含みません)。

ログインの総当たり攻撃を検知するのに向いています。

フロントに返すデータ

これらはストアフロントへ返すデータを書き換えます。商品を隠す、表示価格を変える、項目を追加するといった用途です。

定数引数
USER_API_INDEX_CATEGORY_LIST0x49array $category
USER_API_INDEX_COMMODITY_LIST0x50array $data
USER_API_INDEX_COMMODITY_DETAIL_INFO0x51array $item
USER_API_INDEX_PAY_LIST0x53array $pay
USER_API_INDEX_QUERY_LIST0x54array $data
USER_API_INDEX_QUERY_SECRET0x55Order $order
USER_API_PURCHASE_RECORD_LIST0x56array $data

商品と在庫

定数引数
COMMODITY_CHANGE_AFTER0x8100int[] $ids, string $action, ?Commodity $before
CARD_CHANGE_AFTER0x8101int[] $commodityIds, string $reason
SERVICE_SHOP_GET_ITEM_STOCK0x8000Commodity $commodity, string $race, array $sku

COMMODITY_CHANGE_AFTER は商品の追加・編集・削除・公開状態の変更・一括設定・上流からの同期の後に発火します。必ずデータベーストランザクションのコミット後なので、受け取る内容は確実に永続化済みです。

$action の値:createupdatedeletestatusbatchsync$before は単一商品の保存経路でのみ編集前のモデルを持ち、それ以外は null です。

一括経路(statusbatch)で渡されるのはリクエストに含まれる ID の集合であり、実際には変更されていない商品も含まれ得ます。購読側は自前のスナップショットと差分を取るべきで、すべての ID が本当に変わったと仮定してはいけません。delete の時点では行がすでに存在せず、ID しか得られません。

CARD_CHANGE_AFTER はカードプールの変化、つまり自動発送商品の在庫変動の後に発火します。こちらもコミット後です。

$commodityIds商品の ID であり、カードの ID ではありません。$reason の値:importeditlockunlockselldelete

注文による在庫の減少はここを通りません。 USER_API_ORDER_PAY_AFTERORDER_MANUAL_DELIVERY_AFTER を使ってください。

チケット

定数引数
USER_API_TICKET_CREATE_AFTER0x2100Ticket $ticket, TicketMessage $message
USER_API_TICKET_REPLY_AFTER0x2101Ticket $ticket, TicketMessage $message
ADMIN_API_TICKET_REPLY_AFTER0x2102Ticket $ticket, TicketMessage $message, Manage $manage

3 つともトランザクションのコミット後に発火し、フック内で例外を投げても API の結果には影響しません。

メール

定数引数
SERVICE_SMTP_SEND_BEFORE0x3000array $config, string $email, string $title, string $content
SERVICE_SMTP_SEND_SUCCESS0x3001同上
SERVICE_SMTP_SEND_ERROR0x3002同上

SERVICE_SMTP_SEND_BEFORE は戻り値でチェーンを打ち切る唯一のポイントです。true を返すとプラグインが送信を引き取ったことになり、コアは SMTP を使いません。メールを Telegram など別の経路に振り替えるときに使います。

コアとルーティング

定数引数
CONTROLLER_CALL_BEFORE0x31object $controller, string $action
CONTROLLER_CALL_AFTER0object $controller, string $action, mixed $result
HTTP_ROUTE_RESPONSE0x47string $routePath, mixed $result
HTTP_NOT_FOUND0x48string $routePath ルート未一致
RENDER_VIEW0x33string $result 描画結果。書き換え可能
WAF_INTERCEPT0x289string $message WAF が遮断したとき
CSP_SOURCE_ALLOW0x8102array $sources CSP の許可リスト
LANG_MISS0x9100array $sourceList, array $langList 未翻訳の文言
ADMIN_API_PLUGIN_SAVE_CONFIG0x15int $id, array $map プラグイン設定の保存時

HTTP_NOT_FOUND はスキャン検知に向いています。短時間に大量の 404 が出るのは、ほぼ誰かが探りを入れているときです。


リスク判定・目視確認のポイント

0x2300 から 0x2305 はリスク管理用のグループです。他のフックと 2 点で異なるので、購読前に必ず読んでください

1. すべて RiskContext $risk を参照渡しで受け取り、書き換えて void を返す

絶対に return true/false しないでください。 ディスパッチャは bool でチェーン全体を打ち切るため、最初に bool を返した購読者が、後続のリスク系プラグインをすべて黙らせてしまいます。

php
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 を読みます。DENYJSONException を投げ、REVIEW は各場面ごとの保留処理に進み、LIMIT は購読側に委ねられます。

2. 例外だけでは足りない理由

拒否は例外で表現できますが、保留はできません。保留には「アカウントは作るが、セッションは発行しない」ということをコアに伝える必要があり、例外ではそれを表現できないからです。

各ポイント

定数引数位置
USER_API_AUTH_REGISTER_VALIDATED0x2300$risk, $user登録:検証後、INSERT 前
USER_API_AUTH_PASSWORD_BEGIN0x2301$risk, $accountパスワード再設定:コード検証の前
USER_API_RECHARGE_TRADE_BEGIN0x2302$risk, $user, $mapチャージ:金額とチャネルの検証後
USER_API_CASH_SUBMIT_BEGIN0x2303$risk, $user, $map出金:紐付け確認後、INSERT 前
USER_API_TICKET_CREATE_BEGIN0x2304$risk, $user, $mapチケット作成:サービス層に入る前
USER_API_ORDER_DELIVERY_BEGIN0x2305$risk, $order, $commodity発送の直前

いくつかの設計上の補足:

登録(0x23000x19 より優れている点は 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_TABLE0x8HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_COMMODITY_TABLE0x5HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_CATEGORY_TABLE0x702HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_ORDER_TABLE0x11HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_CATEGORY_POST0x703HACK_SUBMIT_FORM
ADMIN_VIEW_COMMODITY_POST0x45HACK_SUBMIT_FORM
USER_VIEW_COMMODITY_POST0x46
USER_API_INDEX_TRADE_CALC_AMOUNT0x52

ネット上の古い解説記事では、いまだに ADMIN_VIEW_USER_TABLE に JSON の列定義を echo して管理テーブルに列を足す方法が紹介されています。現行バージョンではまったく機能しません。

管理テーブルに列を追加する正しい方法

現在の唯一の入口は HACK_ROUTE_TABLE_COLUMNS0x2005)で、JSON を echo するのではなく Column エンティティを使います。

注意点が 2 つあります。

  • 列の描画コード内では escapeHtml がグローバルに使えません。エスケープは自分で行ってください
  • Order.amount は文字列です。計算に使う前にキャストしてください

MIT ライセンスで公開