🐙

Helix Swarmのモジュール作成した際のメモ

13 min read

Helix Swarmとは

https://www.perforce.com/ja/zhipin/helix-swarm

Module overview

https://www.perforce.com/manuals/swarm/Content/Swarm/extending.modules.html
Slack通知が欲しかったので調べたら
https://www.perforce.com/manuals/swarm/Content/Swarm/extending.example_slack.html#Example_Slack_module
があったのでこれをベースに拡張(以下はだいたいこのページに書いてる内容の解説)

Swarmのベースフォルダ

デフォルトのままインストールしたら/opt/perforce/swarm/のはず

[root@localhost ~]# cd /opt/perforce/swarm
[root@localhost swarm]# ls -la
total 12
drwxr-xr-x. 12 root     root      175 Oct 22 14:23 .
drwxr-xr-x.  9 perforce perforce  155 Nov 29 10:36 ..
drwxr-xr-x.  2 root     root      125 Dec  3 18:25 config
drwx------.  6 apache   apache    218 Dec  6 11:07 data
-rw-r--r--.  1 root     root      383 Oct  5 23:36 index.html
drwxr-xr-x.  4 root     root       30 Oct 22 14:23 library
drwxr-xr-x. 35 root     root     4096 Dec 14 09:40 module
drwxr-xr-x.  5 root     root       57 Oct 22 14:23 p4-bin
drwxr-xr-x.  7 root     root      201 Oct 22 14:23 public
drwxr-xr-x.  4 root     root      111 Oct 22 14:23 readme
drwx------.  2 perforce perforce    6 Oct  5 23:36 redis
drwxr-xr-x.  2 root     root       81 Dec  2 17:25 sbin
drwxr-xr-x. 14 root     root      213 Oct 22 14:23 vendor
-rw-r--r--.  1 root     root       66 Oct  5 23:36 Version

前提

今回は引用URLのとおりSlackっていうモジュールを作成することとして説明してます

必要ファイル

config/
      custom.modules.config.php
module/
      Slack/
            config/
                   module.config.php
            src/
                Listener/
                         SlackActivityListener.php		
            Module.php

※module直下には作成するフォルダはモジュール名であるSlackとなります

custom.modules.config.php

独自で作ったモジュールを読み込むために設定するところ
(swarmで自動的に読み込まれるので複数モジュール作ってもこのファイルの設定だけでOK)
namespaces配列のキーに作成したプログラムのネームスペース名、値にソースの場所を追記する
戻り値に使用するモジュール(namespaces配列)のキーを返す

<?php
\Laminas\Loader\AutoloaderFactory::factory(
    array(
        'Laminas\Loader\StandardAutoloader' => array(
            'namespaces' => array(
                'Slack'      => BASE_PATH . '/module/Slack/src',
            )
        )
    )
);
return [
    'Slack'
];

Module.php

作ったモジュール用のコンフィグファイルの場所を返す
ネームスペースはモジュール名であるSlackにする

<?php
/**  
 * Perforce Swarm
 *
 * @copyright   2021 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
*/

namespace Slack;

class Module
{

    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }
}

module.config.php

作ったモジュール用のコンフィグ設定
このファイルでどの動作時に動作させるかの記載もする

<?php
/**
 * Perforce Swarm
 *
 * @copyright   2021 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
*/

use Events\Listener\ListenerFactory as EventListenerFactory;

// `SlackActivityListener.php`で作るクラス名
$listeners = [Slack\Listener\SlackActivityListener::class];

return [
    // お決まりごと
    'listeners' => $listeners,
    'service_manager' =>[
        'factories' => array_fill_keys(
            $listeners,
            Events\Listener\ListenerFactory::class
        )
    ],
    // ここからイベントに対応するコールバック宣言
    EventListenerFactory::EVENT_LISTENER_CONFIG => [
        EventListenerFactory::TASK_COMMIT => [
            Slack\Listener\SlackActivityListener::class => [
                [
                    Events\Listener\ListenerFactory::PRIORITY => -110,
                    Events\Listener\ListenerFactory::CALLBACK => 'handleCommit',
                    Events\Listener\ListenerFactory::MANAGER_CONTEXT => 'queue'
                ]
            ]
        ],
        EventListenerFactory::TASK_REVIEW => [
            Slack\Listener\SlackActivityListener::class => [
                [
                    Events\Listener\ListenerFactory::PRIORITY => -110,
                    Events\Listener\ListenerFactory::CALLBACK => 'handleReview',
                    Events\Listener\ListenerFactory::MANAGER_CONTEXT => 'queue'
                ]
            ]
        ]
    ],
    // 作成するモジュール用コンフィグ設定
    'slack' => [
        'token'       => 'xoxb-0000000000000-0000000000000-000000000000000000000000',
        'channel'     => 'swarm-reviews',
        'user'        => 'Swarm',
        'icon'        =>
            'https://swarm.workshop.perforce.com/view/guest/perforce_software/slack/main/images/60x60-Helix-Bee.png',
        'max_length'  => 80,
    ]
];

EventListenerFactory::EVENT_LISTENER_CONFIG配列にキーがどのイベントで反応するか、値に呼び出し関数を入れておく
この例では
EventListenerFactory::TASK_COMMITのときにhandleCommit関数を呼び出す
EventListenerFactory::TASK_REVIEWのときにhandleReview関数を呼び出す

EventListenerFactory\module\Events\src\Listener\ListenerFactory.phpにあります(ので適切なイベントに書き換えると良い)

SlackActivityListener.php

実際の処理場所(configで設定した関数が呼び出される)
ネームスペースはモジュール名であるSlack + src以下のフォルダ名(Listener)にすること

<?php
/**
 * Perforce Swarm
 *
 * @copyright   2021 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
*/

namespace Slack\Listener;

use Events\Listener\AbstractEventListener;
use P4\Spec\Change;
use P4\Spec\Exception\NotFoundException;
use Reviews\Model\Review;
use Laminas\EventManager\Event;
use Laminas\Http\Client;
use Laminas\Http\Request;

class SlackActivityListener extends AbstractEventListener
{
    public function handleReview(Event $event)
    {
        $logger = $this->services->get('logger');
        $logger->info("Slack: handleReview");
        $p4Admin = $this->services->get('p4_admin');
        try {
            $review = Review::fetch($event->getParam('id'), $p4Admin);
            // Construct your Slack Review message here
            $text = 'Review ' . $review->getId();
            $this->postSlack($text);
        } catch (\Exception $e) {
            $logger->err("Slack:" . $e->getMessage());
            return;
        }
        $logger->info("Slack: handleReview end.");
    }

    public function handleCommit(Event $event)
    {
        // connect to all tasks and write activity data
        // we do this late (low-priority) so all handlers have
        // a chance to influence the activity model.
        $logger = $this->services->get('logger');
        $logger->info("Slack: handleCommit");

        // task.change doesn't include the change object; fetch it if we need to
        $p4Admin = $this->services->get('p4_admin');
        $change  = $event->getParam('change');
        if (!$change instanceof Change) {
            try {
                $change = Change::fetchById($event->getParam('id'), $p4Admin);
                $event->setParam('change', $change);
            } catch (NotFoundException $e) {
            } catch (\InvalidArgumentException $e) {
            }
        }

        // if this isn't a submitted change; nothing to do
        if (!$change instanceof Change || !$change->isSubmitted()) {
            $logger->info("Slack: not a change...");
            return;
        }
        try {
            // Construct your Slack Commit message here
            $text = 'Review ' . $change->getId();
            $this->postSlack($text);
        } catch (\Exception $e) {
            $logger->err('Slack: ' . $e->getMessage());
        }
        $logger->info("Slack: handleCommit end.");
    }

    private function postSlack($msg)
    {
        $logger = $this->services->get('logger');
        $config = $this->services->get('config');

        $icon = $config['slack']['icon'];
        $user = $config['slack']['user'];
        $token = $config['slack']['token'];
        $channel = $config['slack']['channel'];

        $url = 'https://slack.com/api/chat.postMessage'

        $logger->info("Slack: POST to $url");
        $logger->info("Slack: user=$user");
        $logger->info("Slack: icon=$icon");

        try {

            $headers = [
                "Content-type: application/json",
                "Authorization: Bearer " . $token
            ];

            $body = [
                "channel" => $channel
                "text" => $msg,
                "username" => $user,
                "icon_url" => $icon,
            ];

            $json = json_encode($body);

            $logger->info("Slack: sending request:");
            $logger->info($json);
 
            $request = new Request();
            $request->setMethod('POST');
            $request->setUri($url);
            $request->getHeaders()->addHeaders($headers);
            $request->setContent($json);

            $client = new Client();
            $client->setEncType(Client::ENC_FORMDATA);

            // set the http client options; including any special overrides for our host
            $options = $config + ['http_client_options' => []];
            $options = (array) $options['http_client_options'];
            if (isset($options['hosts'][$client->getUri()->getHost()])) {
                $options = (array) $options['hosts'][$client->getUri()->getHost()] + $options;
            }
            unset($options['hosts']);
            $client->setOptions($options);

            // POST request
            $response = $client->dispatch($request);

            $logger->info("Slack: response from server:");
            $logger->info($response->getBody());

            if (!$response->isSuccess()) {
                $logger->err(
                    'Slack failed to POST resource: ' . $url . ' (' .
                    $response->getStatusCode() . " - " . $response->getReasonPhrase() . ').',
                    [
                        'request'   => $client->getLastRawRequest(),
                        'response'  => $client->getLastRawResponse()
                    ]
                );
                return false;
            }
 
            return true;

        } catch (\Exception $e) {
            $logger->err($e);
        }
        return true;
    }
}

イベント呼び出し後のデータ取得に関して(ここがメイン)

module.config.phpで記載したconfigデータ取得

        $config = $this->services->get('config');
	
        // コンフィグから必要情報取得する
        $icon = $config['slack']['icon'];
        $user = $config['slack']['user'];
        $token = $config['slack']['token'];
        $channel = $config['slack']['channel'];

自身が作ったモジュールにおいては

EventListenerFactory::TASK_REVIEW…レビュー状態が変わったとき
EventListenerFactory::TASK_COMMENT…コメントが記載されたとき
のときに反応させております

EventListenerFactory::TASK_REVIEW

            // レビュー情報取得
            $review = Review::fetch($event->getParam('id'), $p4Admin);

でレビューデータが取得できます

EventListenerFactory::TASK_COMMENT

            // コメント情報取得
            $comment = Comment::fetch($event->getParam('id'), $p4Admin);
            // トピックを取得
            $topic = $comment->get('topic');

            // トピックからレビューを取る(moduleのソース見たらこの取得方法だった)
            if (strpos($topic, 'reviews/') === 0) {
                // レビューID取得
                $reviewID = explode('/', $topic);
                // IDからレビュー情報取得
                $review   = Review::fetch(end($reviewID), $p4Admin);
            }

でコメントデータおよびレビューデータが取得できます

Reviewデータ

module\Reviews\src\Model\Review.phpにあります。
function getで検索すれば取得できる情報だいたい分かるかと

レビュワーデータ取得

        // レビュワー情報
        $reviewers = $review->getReviewers();
        if(false === empty($reviewers)) {
            $result = User::fetchAll([User::FETCH_BY_NAME => $reviewers], $p4Admin);
        }

Userデータ

module\Users\src\Model\User.phpおよびlibrary\P4\Spec\User.phpにあります。
function getで検索すれば取得できる情報だいたい分かるかと

Commentデータ

module\Comments\src\Model\Comment.phpにあります。
function getで検索すれば取得できる情報だいたい分かるかと

開発時の注意

プログラム変更時

モジュール(phpファイル)更新後はphp-fpmを再起動する必要があります
systemctl restart php-fpm

config変更時

設定キャッシュファイルを消す必要があります
rm -rf /opt/perforce/swarm/data/cache//module-c*

Discussion

ログインするとコメントできます