Humhubのカスタムモジュールの作り方（Widgetへのアイテム追加（->addItem））

#はじめに
　[前回](https://qiita.com/tomofu74/items/7e9e002310d8b14bb612)までは、コールバックイベントによるAdminMenuへの項目の追加の様子を、公式のドキュメントとcustom_pageモジュールの実装を参照し、概説した。今回は、自作モジュールにおいて「ログインユーザーの参加スペースに応じて、トップナビゲーション部に表示するリスト項目を追加する」場面を考えてみよう。


#プラグインモジュールの準備
 [前々回](https://qiita.com/tomofu74/items/6b42711385f4e99058f1)にしたがって、最低限必要な３つのファイルと、[前回](https://qiita.com/tomofu74/items/7e9e002310d8b14bb612)を参考にイベントに応じたコールバック関数を記述するEvents.phpファイルを用意することにしよう。今回は「topnavilist」モジュールとして開発すると想定する。ファイル構成例は、次のようになる。

    topNaviList（フォルダ）
      ├── config.php
      ├── module.json
      ├── Module.php
      └── Events.php

それぞれのファイルの作成上の注意は、前回・前々回で述べたので省略するが、注意点としてidは小文字アルファベットのみで既存のどのモジュールとも競合しないユニークな名称であることを忘れないで欲しい。ここを誤るとhumhubシステムが起動しなくなることは先述の通りだ。

　この例では、はじめに module.json を書いてみる。例えば、次のような感じだ
module.json 記述例

    {
        "id": "topnavlist",
        "name": "TopNav List",
        "description": "Qiitaでモジュール開発の解説するためのサンプルですよ。",
        "keywords": ["TopNav", "sample", "test"],
        "version": "0.0.1-test-qiita-sample",
        "humhub": {
            "minVersion": "1.6"
        },
        "license": "Apache 2.0",
        "require": {
            "php" : ">=7.3"
        },
        "homepage": "https://www.osintech.net".
        "authors" : [
            {
                 "name": "Tomoyuki 'SISHO' Fuse",
                 "email": "xxxxx@xxxxxx.com",
                 "homepage": "https://qiita.com/tomofu74",
                 "role": "Developper, Researcher",
                 "favorite": "Friendly girl"
            },
            {
                 "name": "Tama 'Homeless' Cat',
                 "email": "xxxxx@xxxxx.com",
                 "favorite": "Chaocule"
            }
       ]
    }

　[前々回](https://qiita.com/tomofu74/items/6b42711385f4e99058f1)でも注意したように、idは全て小文字で記載しておく方がよい。そのほか、"humhub"においては、動作の保証ができる最低限のバージョン番号を書いておく。大抵は、開発に使ったhumhubのバージョンを"minVersion"にしておけばよいと思うが、それ以降のhumhubバージョンアップで、以前のバージョンとは情報の取り扱いが大きく変わり、下位互換性がなくなってしまう場合などには注意が必要だ。例えば、２０２０年中のhumhubのバージョンアップは毎回仕様の変更が大きい。モジュール開発においては、公式情報（ (https://docs.humhub.org/docs/develop/modules-migrate) ）に注意を欠かせない。
　そのほか、必須でない項目について色々と記入できる。全くhumhubシステムで使われない情報を書き込んでもモジュールの動作には問題ないようだ。この際だから、会社の宣伝も入れてしまえ、という愛社精神をこっそりと表現できたりもする。

　次にModlue.phpを書いてみよう。[前々回](https://qiita.com/tomofu74/items/6b42711385f4e99058f1)では、説明を省略したが、今回は公式の情報を補足として書き加えておく。Humhub公式では、BaseModuleClassとして２つの対応を挙げている。２つの対応の違いは、モジュールの使い方のスタイルによるものだ。グローバルに設定してシステムの機能として使うもの、スペース・ユーザーが使用を選択できるもの、の２つだ。
　今回、題材としてあつかう件では、全ログインユーザーに対して選択の余地を持たせる必要はないこととし、次のようにModule.phpを作成した。

    <?php

    namespace Osintech\humhub\modules\topnavilist;

    use Yii;

    class Module extends \humhub\components\Module
    {
        public function getName()
        {
            return Yii::t('TopnavilistModule.base', 'TopNavi List');
        }
    }

　注意点のひとつは、 namespace の決定だ。Humhub　が拠り所としている PHP Yii2 フレームワーク　では、ちょっと詳しい説明が見つからない。ひとつ言えることは、ファイルシステムとは関係なく名称を設定できるようだ。よって、「humhub\modules\」フォルダの下にあるから絶対に「humhum\modules」フォルダで書き始めなければならない、ということはない。ここは開発者にわかりやすいように、このモジュールのまとまりをみせるために、モジュールの中で共通の部分を持たせておくのがよいだろう。また、DBにデータを記録するようなモジュールを扱う場合、このネームスペースが情報の一部として記録されることがあるので、運用を始めたモジュールのnamespaceは変更しづらい、と思っておくとよい。将来も見据えた、センスあるネーミングを開発者で考えるもの、なのかな・・・。今回は、会社名とHumhubのモジュールだよっ、ということがわかるような名前にしてみた（私なりのセンス云々）。
　ひとつ、getName()という関数を作った。これは、Humhubのモジュール管理機能において表示される名称の文字列を返す関数だ。Humhubがモジュールとして感知すると、自動でこの関数が呼び出される（どこにも解説が見つけられないが、経験則およびシステムコアモジュールのソースを読むとそうらしい・・・という推測。どなたか、正確な情報源をご存知な方はコメントで教えてください。）
　今回は、getName()関数の中で、　Yii::t() によって得られる文字列を返却（return）するようにコーディングしている。これは、Humhubのinternationalizationに従って、文字列を多言語に変換する仕組みを呼び出すものだ。多言語への変換は、自動で翻訳機能が働くのではなく、別途辞書ファイルを作成して対応する文字列の置き換えをさせる仕組み。公式に、少しだけ触れられている。(https://docs.humhub.org/docs/develop/i18n/)
　internationalizationについて具体的な使い方は、今後の連載で記載することとしたい。


#コールバックイベントの指定
　トップナビゲーション部に表示する項目を作り、その表示はユーザーの参加スペースに応じて切り替える、という機能を加えていく。まず、トップナビゲーション部に表示させるイベントのコールバックファンクションを作ろう。そのファンクションの中で、ユーザーの参加スペースを判定すれば良い、というわけだ。

姉様風言い回しでの、問題１）　本件に適切と思われる config.phpを作成し、配置なさい。
バルス的ではない、いきなり答え　→　config.php 記述例

    use humhub\widgets\TopMenu;
    use humhub\modules\topnavlist\Events;

    return [
        'id' => 'example',
        'class' => 'humhub\modules\topnavilist\Module',
        'namespace' => 'humhub\modules\topnavilist',
        'events' => [
            [
               'class' => TopMenu::class,
               'event' => TopMenu::EVENT_INIT, 
               'callback' => [Events::class, 'onTopMenuInit']
            ]
        ]
    ];

このようなconfig.phpでなぜ適切と思われるかについては前回を振り返って確認してみて欲しい。

　さて、eventsに、TopMenu::class（すなわち、humhub\widgets\TopMenuクラス）のEVENT_INITでのイベントでのコールバックを要求している。コールバックファンクションは、Events::class（すなわち、humhub\modules\topnavlist\Eventsクラス）の onTopMenuInit だ。

問題２）　EventsクラスにonTopMenuInitファンクションを作りなさい。
答え　→　Events.php　記述例

    <?php

    namespace humhub\modules\topnavlist;

    use Yii;

    class Events
    {
         public static function onTopMenuInit($event)
         {
             try {
               // --> ここにあとで追記する <-- //
             } catch (\Throwable $e) {
                 Yii::error($e);
             }
         }
    }

 
　まずは、 こんな形でコールバックファンクションを設置した。コールバックファンクションは返り値のない、public で staticなものを宣言するのが一般的なようだ（これも経験と既存のコアモジュールの観察の結果に基づく見解。）。引数の $event は、config.php で記載したイベント情報から自動で渡される。try ~ catch は、phpの記述に基づくものであり、解説は別に譲る。 Yiiクラスをuseして、Yii::error() に catchしたエラーメッセージ を引数として渡している。こうしておくと、発生したエラーが humhub の 動作ログにエラーとして記録される。記録されたエラーは、humhubの管理者メニューのinformation の logging タブで読むことができる。邪道かもしれないが、筆者はこの仕組みを使ってコーディングの途中で自分のほしい情報の出力を行せながら、開発している。
　ここまでの記述で、このモジュールが、humhub\widgets\TopMenu が Initイベント を発火するときに作動する、onTopMenuInit コールバックを実装した topnavlist モジュールの基礎ができた。ここまでで、topnavlistフォルダ内に目的の４つのファイルが出来上がった。このtopnavlistフォルダを、humhubシステムのユーザーモジュールフォルダ(Module Loader Path: 公式情報( https://docs.humhub.org/docs/develop/environment#module-loader-path )にコピーすると、モジュールとして認識され、モジュールの作動準備ができあがりなのだ。さっそく、各自でこのモジュールを有効にしてみてほしい。
（有効にしても実際は何も発生しない。もし、エラーが出ている場合は、何かの書き間違いがあるかもしれないので表示されるエラーに従って修正対応してみてほしい。）

#コールバックイベントの内容記述
　それでは、コールバックイベントの内容記述を検討しよう。今回の目標は「ログインユーザーの参加スペースに応じて、トップナビゲーション部に表示するリスト項目を追加する」なので、まずは”ログインユーザーの参加スペース”の情報を取得することにしよう。ログインユーザーの参加スペース情報は、 Space コアモジュールにある、 Membershipモデルの getUserSpaces()ファンクションを使って読み取ることができる。このファンクションに、ユーザーIDを引数で与えると、ユーザーIDに基づいて参加スペースの情報を返却してくれる。　
次の記述例のように、Event.phpを書き換えてみてほしい。

Event.php　（getuserSpaces()記述例）

    <?php

    namespace humhub\modules\topnavlist;

    use Yii;

    class Events
    {
         public static function onTopMenuInit($event)
         {
             try {
                 $userSpaces = Membership::getUserSpaces(Yii::$app->user->id);
                 Yii::error($userSpaces);
             } catch (\Throwable $e) {
                 Yii::error($e);
             }
         }
    }

　Yii::error()を使って、humhubシステムのログに情報を書き出してみた。 Administration -> Information -> logging タブにて、エラー情報を見ると、例えば、次のように表示される。
![スクリーンショット 2020-10-18 23.36.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699890/efa34755-14ea-94e9-2b5c-c83c4d8731f8.png)
　とにかく Spaceの情報が　マルッと　出力されていることがわかる。この情報の羅列のなかで、例えば、IDが２であるスペースに参加しているかどうかを今回の例題目標の条件にしよう。特定の条件のデータだけ取り出す方法について、筆者は恥ずかしながら foreach で逐一チェックする方法しか知らない（もし、どなたか、他に処理効率のよい方法をご存知でしたら、コメント欄でご教授ください。）。
　最後に、いよいよ”トップナビゲーション部に表示するリスト項目を追加する”部分を記述する。これは、今回の onTopMenuInitに引数として渡されている、 \$event を用いることで実現する。今回の イベントは、 INITイベント（EVENT_INIT）であった。このイベントを発火しているのは、TopMenuクラス（humhub\widgets\TopMenu） だ。この発火元を取得するには、 $event->sender と記述する。今回は、この発火元に対して、”リスト項目を追加する”ので、TopMenuクラスのaddItem()ファンクションを使おう。結果として、Event.php を次のように書き換えてほしい。

Event.php (目標達成版）

    <?php

    namespace humhub\modules\topnavlist;

    use Yii;
    use humhub\modules\ui\icon\widgets\Icon;

    class Events
    {
         public static function onTopMenuInit($event)
         {
             try {
                 $userSpaces = Membership::getUserSpaces(Yii::$app->user->id);
                 
                 foreach( $userSpaces as $space ) {

                    if ($space["id"] == 2) {

                      $event->sender->addItem([
                        'label' => Yii::t('TopnavlistModule.base', 'TOPNAVLIST'),
                        'icon' => Icon::get('anchor'),
                        'sortOrder' => 99999,
                      ]);
                      break;

                    }
                }
 
             } catch (\Throwable $e) {
                 Yii::error($e);
             }
         }
    }

　この変更のあとで、humhubシステムにいくつかのユーザーでログインしてみると、スペースID ２に参加しているユーザには、TopNav部に、アンカーのアイコンでTOPNAVLISTの見出しが表示されていることだろう。
![スクリーンショット 2020-10-19 0.36.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699890/dd1adb75-580b-0e58-b71e-ebd5fc77276c.png)

#まとめ
　今回は、「ログインユーザーの参加スペースに応じて、トップナビゲーション部に表示するリスト項目を追加する」という機能を持ったモジュールの実装を実戦的に解説した。機能の実現方法については、システムのコールバックによりWidgetのクラスの初期化に自前の情報を addItem させる方法を採用した。


　次回は、今回作成したリスト項目をクリックして、別画面に遷移する機能を追加することを解説する予定。