Helix Swarmのモジュール作成した際のメモ
Helix Swarmとは
Module overview
Slack通知が欲しかったので調べたら があったのでこれをベースに拡張(以下はだいたいこのページに書いてる内容の解説)
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