Example Slack module

The following example module demonstrates how to integrate Swarm with Slack so that Swarm posts a message in a Slack channel each time a change is committed, a review is created, or a review is updated.

Note

Swarm supports the Laminas component versions in the LICENSE.txt file, features and functions in the Laminas documentation that were introduced in later versions of Laminas will not work with Swarm. The LICENSE.txt file is in the readme folder of your Swarm installation.

Tip

You must test your custom modules on a test system before transferring them to your production system. This avoids any negative impact on the operation of your production system. If you have more than one custom module, the modules should all be tested at the same time on the same test system as this ensures that the modules operate correctly with each other and with Helix Swarm.

Tip

If you add or edit a module, Swarm will not use that change until the config cache has been reloaded, this forces Swarm to use the module change. You must be an admin or super user to reload the Swarm config cache. Navigate to the User id dropdown menu, select System Information, click the Cache Info tab, and click the Reload Configuration button.

Basic steps needed to create the Slack module are:

File locations

For reference, the Slack module uses the following filenames and locations:

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

Create the Module.php file

Module.php will:

  • Declare the module namespace, this must match the directory name of the module.
  • Provide the getConfig() function
  • Tip

    Optional: You can also implement the onBootstrap (Event $event) function if you want to do some early setup when the module is loaded:

    public function onBootstrap(Event $event)
    {
    }

    Event is a Laminas class and is added after namespace:

    namespace Slack
    use Laminas\EventManager\Event
    

Create the Module.php file:

  1. Create a directory called Slack in the module directory.
  2. Create the file Module.php in module/Slack.

  3. Edit Module.php to contain:
  4. <?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';
        }
    }
  5. Now Create the module.config.php file.

Create the module.config.php file

The module.config.php file will:

  • Configure your module including event listeners.
  • Provide the Slack channel and token details
  • Provide an icon for Swarm that is displayed in Slack
  • Declare an event priority of -110 for the COMMIT and REVIEW event listeners because the Activity event is processed with a priority of -200 and this is a sensible time to post the message to Slack.
Tip

Listener::class example details for the COMMIT and Review classes:

  • Events\Listener\ListenerFactory::COMMIT => [
  • We are listening to COMMIT here because we are interested in commits.

  • Events\Listener\ListenerFactory::Review => [
  • We are listening to Review here because we are interested in when a review is created or updated.

  • Events\Listener\ListenerFactory::PRIORITY => -110,
  • Declares an event priority of -110 for the event listener because the Activity event is processed with a priority of -200 and this is a sensible time to post the message to Slack.

  • Events\Listener\ListenerFactory::CALLBACK => 'handleCommit',
  • Declares the function name within the listener class that is called.

  • Events\Listener\ListenerFactory::CALLBACK => 'handleReview',
  • Declares the function name within the listener class that is called.

  • Events\Listener\ListenerFactory::MANAGER_CONTEXT => 'queue'
  • Triggers your custom listener when Swarm processes the Swarm event queue.

Create the module.config.php file:

  1. Create a directory called config in the Slack directory.
  2. Create a file called module.config.php in config.
  3. Edit module.config.php to contain:
  4. <?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;
    
    $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,
        ]
    ];		

  5. Now Create the SlackActivityListener.php file.

Create the SlackActivityListener.php file

The SlackActivityListener.php file will:

  • Contains the implementation of your event listener
  • Contains the Slack messages for a commit and a review
  • Tip

    The configuration of your Slack URLs and messages will depend on your Slack system and your requirements. This is a basic example but you can edit it so that you post your messages to a different channel for each project branch. You can even add to the Project settings page using CSS and JavaScript so that users can configure the Slack channel URLs and messages for each project.

  • Allow logging

Create the SlackActivityListener.php file:

  1. Create a directory called src in the Slack directory.
  2. Create a directory called Listener in the src directory.
  3. Create a file called SlackActivityListener.php in Listener.
  4. Edit SlackActivityListener.php to contain:
  5. <?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;
        }
    }

  6. Now Enable the Slack module for Swarm in custom.modules.config.php.

Enable the Slack module for Swarm in custom.modules.config.php

Swarm uses the custom.modules.config.php file to auto-load classes and to check which custom modules it should run. This gives you control over which modules Swarm loads and prevents modules from being loaded by mistake.

Create the custom.modules.config.php file:

  1. Create the config directory at the same level as the module directory if it does not exist.
  2. Create the custom.modules.config.php file in the config directory if it does not exist.
  3. Edit the custom.modules.config.php file so that it contains the auto-loader and the Slack module details:
  4. Tip

    If you already have one or more custom modules enabled for Swarm, the auto-loader and the existing module details will already be in the file.

    Just add Slack to the namespaces and return arrays of the custom.modules.config.php file.

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

  5. The Swarm config cache must be reloaded so that Swarm can see your new module. As an admin or super user, navigate to the User id dropdown menu, select System Information, click the Cache Info tab, and click the Reload Configuration button.
  6. The Slack module is now enabled for Swarm. Swarm will post a message on a Slack channel each time a change is committed, a review is created, or a review is updated.

  7. Check that the module works correctly before moving it to your production server.