Humhubのカスタムモジュールの作り方（公式ページのとある解釈）（２）

#はじめに
　[前回](https://qiita.com/tomofu74/items/6b42711385f4e99058f1)は、Humhubのカスタムモジュールの作り方について、モジュールの型枠といえるような部分について、公式ページについてのいち解釈を記述した。今回は、カスタムモジュールのeventsの記述についての解釈を、おじさんは語りたい。（おじさんは、「亜人ちゃんは語りたい」というアニメを見たりすると、そのオマージュとしてこんな言葉遣いをしてみたくなるのだ）。

　カスタムモジュールの利用にあたって、「このモジュールは、この操作のときにこんな風にでてきてほしい」という状態条件があることだろう。この状態条件、モジュール活性化の条件を記載することがeventsの記述であると言ったら、伝わるだろうか？コールバックのトリガーを書く、という表現があうだろうか？

今回の解釈ターゲットは、こちらの公式ページだ。
https://docs.humhub.org/docs/develop/modules-event-handler/

　前回内容と重複する部分もあるが、eventsの記述について公式ページの説明をなぞりつつ、後半、具体的にcustom_pageモジュールのメニュー表示の仕組みについて語りたい。なお、今回は、上述ターゲットの全てを網羅しない点をご容赦いただきたい。今回は、”イベントに反応してコールバックを実行すること”のもっともシンプルな解釈のみ語りたい。

#カスタムモジュールのEvents設定
　Humhubのシステムは、モジュールがある状態になると、イベントを発火する仕組みがある。これは、Yii2フレームワークに即したものであり、もちろんカスタムモジュール開発者も発火イベントを自分のモジュールに実装することができる。しかし、ここで述べたいのは、発火する側ではなく、システムの他のモジュールで発火したイベントを受けて、動作を開始する仕組みだ。

　前回のおさらいにもなるが、まずはEvents設定の記述場所を確認しよう。公式ページの記述を引用しながら語りたい。

　まずは、自作モジュールのファイルの配置だ。カスタムモジュールは、config.php, module.json, Module.phpの３ファイルを格納したフォルダを作るものであることを、前回語らせてもらった。今回、サンプルとしてあつかうカスタムモジュールは次のようなファイル配置になっているものとする。今回は、Events.phpファイルが増えていることにも留意しておいてほしい。

    example
    ├── config.php
    ├── module.json
    ├── Module.php
    └── Events.php


　Humhubのイベントハンドラーは、config.phpファイルのeventsセクションに記述する。公式ページでは次のようなサンプルで、記述事項が解説されている。

    // config.php
    use my\example\Events;
    use my\example\modules\Example;

    return [
        //...
        'events' => [
            [
                'class' => Example::class, 
                'event' => Example::EVENT_SOME_EVENT,  
                'callback' => [Events::class, 'onSomeEvent']
            ],
            //...
        ]
    ]

eventsセクションの設定項目については、[前回の解説（解釈）](https://qiita.com/tomofu74/items/6b42711385f4e99058f1#%E5%BF%85%E8%A6%81%E6%9C%80%E4%BD%8E%E9%99%90%E3%81%AE%E5%8B%95%E4%BD%9C%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB)に重複するが、改めて解釈を追加させていただく。

|キー項目名|解説|
|---|---|
|class|（イベント発火元の）モジュールクラス名を、ネームスペース付きで記述する。<br>（前回「イベントトリガーを発するクラスのクラス名をネームスペース付きで記述」と解説。）|
|event|（イベント発火元モジュールの）イベント名称。<br>（同じく「イベント名。イベント名はイベントを発するクラスが管理している。」と解説。）|
|callback|（自作するカスタムモジュールの）コールバック関数名。関数を記述したクラスを指定する。<br>（同じく「コールバックで動作させたい関数（function）の記述ファイルと、関数名をリスト記入する。）と解説。）|

サンプル記述を解釈すると、このサンプルカスタムモジュールは、次の動作を意図して設計されているといえる。

    （既存の）Exampleクラスが、EVENT_SOME_EVENTというイベントを発火したら
      　→  このカスタムモジュールの（my\example\Eventsネームスペースで定義されている）Eventsクラスの
           onSomeEventという関数を実行せよ

公式ページのEventsクラスの記載例は、次のとおり。

    // example/Events.php
    public static function onSomeEvent($event)
    {
        $exampleModel = $event->sender;
        //...
    }

公式ページでは全く解説はないが、このサンプルから重大なことが読み取れる。コールバックされる関数には、引数として\$eventを記述して、\$evnetオブジェクトを受け取っているのだ。この$eventオブジェクトのメンバー変数がわからない！どいひー

Yii2の[イベントハンドラ](https://www.yiiframework.com/doc/guide/2.0/ja/concept-events#event-handlers)のページに次の記述がある。
https://www.yiiframework.com/doc/guide/2.0/ja/concept-events#event-handlers

    イベント・ハンドラのシグネチャはこのようになります:

    function ($event) {
        // $event は yii\base\Event またはその子クラスのオブジェクト
    }
    $event パラメータを介して、イベント・ハンドラは発生したイベントに関して次の情報を得ることができます:

    ・イベント名
    ・イベント送信元: trigger() メソッドが呼ばれたオブジェクト
    ・カスタム・データ: イベント・ハンドラをアタッチするときに提供されたデータ
    // ・・・・　続く

さらに、Yii2のeventクラス解説は次のとおり。
https://www.yiiframework.com/doc/api/2.0/yii-base-event#$name-detail

初心者にはクソ不親切なリンク辿りをして理解しようと頑張らせるレベルの公式。くぅ・・・Yii2フレームワークを理解してから使ってくれい、というつもりか。きついのー。

おじさんの経験から言うと、とりあえず次の２つを押さえておけば、まずはどうにか次に進めると思う。

|情報|プロパティ名|書き方|
|---|---|---|
|イベント名|name| $event->name|
|イベント発信元|sender| $event->sender|

yii\base\Eventクラスは、yii\base\BaseObjectを継承しているので、心配性のひとは、hasProperty()メソッドでプロパティの存在を確認して使用したほうがいいかも。とりあえず、humhubのコアモジュールであれば、nameとsenderはちゃんと持ってるっぽいが。

#イベント設定の書き方実践例
いくつかのカスタムモジュール（オープンソースのもの）を参考に、解釈を確認してみる。

##（実践的解釈例　Custom_pageモジュール（Ver.（Ver.１．０．９）の config.php　における AdminMenuクラスEVENT_INIT）
custom_pageモジュールのconfig.phpを転載し、解釈を語らせていただきたい。

    <?php

    use humhub\modules\space\widgets\Menu;
    use humhub\modules\user\widgets\AccountMenu;
    use humhub\modules\admin\widgets\AdminMenu;
    use humhub\widgets\BaseMenu;
    use humhub\widgets\TopMenu;

    return [
        'id' => 'custom_pages',
        'class' => 'humhub\modules\custom_pages\Module',
        'modules' => [
            'template' => [
                'class' => 'humhub\modules\custom_pages\modules\template\Module'
            ],
        ],
        'urlManagerRules' => [
            ['class' => 'humhub\modules\custom_pages\components\PageUrlRule']
        ],
        'namespace' => 'humhub\modules\custom_pages',
        'events' => [
            ['class' => AdminMenu::class, 'event' => AdminMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onAdminMenuInit']],
            ['class' => TopMenu::class, 'event' => TopMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onTopMenuInit']],
            ['class' => AccountMenu::class, 'event' => AccountMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onAccountMenuInit']],
            ['class' => Menu::class, 'event' => Menu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onSpaceMenuInit']],

            ['class' => 'humhub\modules\space\widgets\HeaderControlsMenu', 'event' => BaseMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onSpaceAdminMenuInit']],
            ['class' => 'humhub\modules\directory\widgets\Menu', 'event' => BaseMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onDirectoryMenuInit']],

            ['class' => 'humhub\modules\dashboard\widgets\Sidebar', 'event' => BaseMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onDashboardSidebarInit']],
            ['class' => 'humhub\modules\directory\widgets\Sidebar', 'event' => BaseMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onDirectorySidebarInit']],
            ['class' => 'humhub\modules\space\widgets\Sidebar', 'event' => BaseMenu::EVENT_INIT, 'callback' => ['humhub\modules\custom_pages\Events', 'onSpaceSidebarInit']],
        ],
    ];
    ?>
    
初めのuse節で、spaceモジュールのMenuウィジェット、userモジュールのAccountMenuウィジェット、adminモジュールのAdminMenuウィジェットの参照と、BaseMenuウィジェット、TopMenuウィジェットの参照を行なっている。returnする配列として、id, classなどの記述を行うことについては前回述べた。今回のテーマに従い、evnetsセクションに注目する。

eventsセクションの冒頭を抜粋し、改行で区切って表してみた。

    'events' => [
        [
            'class' => AdminMenu::class,
            'event' => AdminMenu::EVENT_INIT,
            'callback' => [
                'humhub\modules\custom_pages\Events',
                'onAdminMenuInit'
            ]
        ],
    // ・・・　続く


まず、イベントはArrayにして複数記述することが可能であることがわかる。最初の要素は、イベント発火元クラスをAdminMenu::classと指定し、eventをEVENT_INITと指定している。・・・はい、初心者のみなさん、これってhumhubのどういうイベント状況を指しているかわかりますか？おじさんももちろん、わかりませんでした。公式のドキュメントにも「もちろん」そのままズバリの記述はありません。

とりあえず、コードを追ってみよう。

use節の解釈から、AdminMenuクラスは、「humhub\modules\admin\widgets\AdminMenu」の記述を参照することになる。このクラスには init()ファンクションが公開（public属性）されている。

さて、ここで公式ページの次の部分に着目。
https://docs.humhub.org/docs/develop/modules-event-handler#widget-events

なぜこの部分に着目するかというと、「humhub\modules\admin\widgets」にあるクラスを参照することになった現状において情報を分析するに、前回のモジュールの書き方の最後に言及した[モジュールのファイル構成](https://qiita.com/tomofu74/items/6b42711385f4e99058f1#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E6%A7%8B%E6%88%90)から読み取ると、widgetsフォルダに入っているこのクラスは、「Widgets class」のものであろうと推察されるからだ。この推察から、AdminMenuクラスは、widgetsクラスを継承しているだろう、と、ひとまずは予想する。

より厳密にコードを追うと、 class AdminMenu は、extends LeftNavigation　と宣言されており、LeftNavigationクラスは、\humhub\modules\ui\menu\widgets\Menuクラスの extends　であり、 \humhub\modules\ui\menu\widgets\Menu クラスは、 JsWidget　クラスの extends であって、JsWidget（\humhub\widgets\JsWidget）は、Widgetクラス(\humhub\components\Widget）の extends　と宣言されています。\humhub\components\Widget は、 \yii\base\Widgetの extends なので、これで、AdminMenu が　Widgetクラスを継承していることが確かめられた。

先ほどのwidget-eventsに関する公式ページに着目、widget-eventsが表にまとめられているので和訳解釈してみる。

|イベント|クラス|解説|
|---|---|---|
|Widget::EVENT_INIT|yii\base\Event|init()によって初期化されたときに発火する|
|Widget::EVENT_BEFORE_RUN|yii\base\WidgetEvent|Widgetが実行される直前に発火する|
|Widget::EVENT_AFTER_RUN|yii\base\WidgetEvent|Widgetが実行された直後に発火する|
|humhub\components\Widget::EVENT_CREATE|humhub\libs\WidgetCreateEvent|Yii::createObject()の前に発火する|

さてさて、EVENT_INIT は、init()によって初期化されたときに発火することがわかった。だが、それってhumhubの画面のどこで何をして初期化が呼ばれてるのだろうか？はい、おじさん、またこれもわかりませんでした。これ、どうやって調べたと思います？試行錯誤して実際の動作をみながら推察確認ですよ。時間がかかってしょうがない・・・。

ヒントはある。先ほど、コードを追った中で、LeftNavigationをextendsしていた。また、名前がAdminMenuだ。管理メニューっぽくて左メニューに表示されるAdminMenuを探してみた。・・・ありました。
![screenshot2020-09-05 22.06.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699890/0ce816ca-237f-e111-3329-fac904a99e60.png)

AdminMenuクラスは、このメニューを表示しているものと推察される。このメニューが初期化される時、すなわち管理者メニューが表示されようとするその時にcallbackファンクションを呼びたい、というのが、このイベントの記述の意図のようだ。

では、このイベントで発生させようとしているcallbackファンクションを見てみよう。callbackファンクションのクラスは、humhub\modules\custom_pages\Eventsを指定している。そのクラスには、次のような記述がなされている。

    <?php

    namespace humhub\modules\custom_pages;

    use humhub\modules\custom_pages\models\TemplateType;
    use Yii;
    use yii\helpers\Html;
    use humhub\modules\custom_pages\helpers\Url;

    // ・・・　（中略）　・・・ //

    /**
     * CustomPagesEvents
     *
     * @author luke
     */
    class Events
    {

     public static function onAdminMenuInit($event)
     {
        try {
            Yii::$app->moduleManager->getModule('custom_pages')->checkOldGlobalContent();

            if (!Yii::$app->user->isAdmin()) {
                return;
            }

            $event->sender->addItem([
                'label' => Yii::t('CustomPagesModule.base', 'Custom Pages'),
                'url' => Url::toPageOverview(),
                'group' => 'manage',
                'icon' => '<i class="fa fa-file-text-o"></i>',
                'isActive' => (Yii::$app->controller->module
                    && Yii::$app->controller->module->id === 'custom_pages'
                    && (Yii::$app->controller->id === 'page' || Yii::$app->controller->id === 'config')),
                'sortOrder' => 300,
            ]);
        } catch (\Throwable $e) {
            Yii::error($e);
        }
      }
    // ・・・　（以下、略）


onAdminMenuInint(\$evnet)ファンクションが記述されている。先の解説にもあったように$eventを引数として利用している。
$event->sender　で、すなわち、AdminMenuクラスを呼び、さらに ->addItem()　することで、メニューに表示させたいものがあるようだ。

試しに、humhubシステムで、実際にcustom_pageモジュールを有効化し、AdminMenuを見てみよう。
![screenshot2020-09-05 22.21.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699890/1de1f960-908a-c71a-474d-a5b3a4769f94.png)

custom_pageモジュールが有効になったあとでは、AdminMenuに「Custom Pages」メニューが増えていることがわかる。custom_pageモジュールのconfig.phpが解釈され、Eventsのファンクションが実行されてメニュー表示がなされたのだ。



以上のような調子でイベントの発生場面にあわせて、カスタムモジュールを挟み込んで行くものと理解してほしい。TopMenuの初期化されるタイミング、アカウントメニューの初期化されるタイミング、（スペースの）メニューが初期化されるタイミング、といった具合だ。自分がカスタムでモジュールを作るとき、発生させたいタイミング、表示を追加したい対象のウィジェットを探してきてAddItemすればよい、というイメージが掴めてもらえたら、私が語りたかったことは概ね伝えられたといえる。


***
#イベントの記述
では、実際にカスタムモジュールを作るにあたって、発生させたい場所を決めたら、そのイベント記述はどうするか？ それは、そのタイミングで表示を行なっている既存のモジュールを参照し、config.phpを見て真似して書くのが一番の近道だ。書き写していく際に、上述の解釈例のように、参考にしたモジュールでどういうイベントを拾っているか考える必要があるといえる。

申し訳ないが、今回は、custom_moduleのAdminMenuのEVENT_INITの解説だけで力尽きてしまった。

公式もどこの画面でどういうイベントが、どのモジュールでどういう名前で発生しているかなどという一覧表など提供してくれてはいない。モジュール開発者が自助によって満足いくように作れ、というか、そこまで解説する余力などもっていない、ということだろう。経験を積み、慣れていくしかない。・・・もの忘れのひどいおじさんにとっては苦行だが、「そういえばあのモジュールってこういう場面で表示されてるな」という勘を持っておいて、そこからなんとかはじめられるのだ、と解釈している。

***

humhubカスタムモジュール作りの基礎的な情報、公式ページ情報の、ひとつの解釈について、記述させていただいた。あくまで解釈情報なので、慣れた人には既知の情報でしかなかったとは思うが、自分自身の備忘録として記録するとともに、humhubカスタムモジュール作りにこれから踏み込もうとする人の参考になればなおよし、と考えている。

読んでいただいた方には感謝の念に堪えない。ちょっとだけhumhubを触ってみようという程度のひとにはあまりおすすめできない、さらに深く学んだからといって他に流用のきかない（潰しの効かない）ことも読んで気づかれたかもしれない。Wordpressのように利用者が多ければ、身につけた技術を活かす対応案件が多いだろうが、humhubの開発案件は（少なくとも私の周りでは）あまりきかない。humhubでのカスタムモジュールの開発は、Wordpressなどに比べれば時間と労力を要し、応用場面がすくなく開発コストが高い案件といえる。そういった点も踏まえて、今回および前回のような、humhubのカスタムモジュールの記述解釈についての情報が、humhub開発で苦労している仲間たちの役にたてば幸いである。

なお、次回以降は、引き続きカスタムモジュールの作り方の参考になるような情報を発信していきたく思っており、たとえばWidgetの記述例を掘り下げることを検討している。