Helix Core is now P4 and Helix Swarm is now P4 Code Review. Product user interfaces will be updated soon. Learn more >>

Documentation | 2025.5

# Example email module

The following example module demonstrates how to customize the email template P4 Code Review uses when sending notifications for comments.

P4 Code Review 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 P4 Code Review. The LICENSE.txt file is in the readme folder of your P4 Code Review installation.

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 P4 Code Review.

If you add or edit a module, P4 Code Review will not use that change until the config cache has been reloaded, this forces P4 Code Review to use the module change. You must be an admin or super user to reload the P4 Code Review 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 Email Example module are:

- Create the Module.php file
- Create the module.config.php file
- Create the Listener.php file
- Create the comment-html.phtml file
- Create the comment-text.phtml file
- Enable the EmailExample module for P4 Code Review in custom.modules.config.php

Further reading:

Extending the EmailExample module to other email templates

## File locations

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

    config/
          custom.modules.config.php
    module/
          EmailExample/
                      config/
                             module.config.php
                      src/
                         Listener/
                                  Listener.php
                      view/
                           mail/
                               commit-html.phtml
                               commit-text.phtml
                               comment-html.phtml
                               comment-text.phtml
                               review-html.phtml
                               review-text.phtml
                      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

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 EmailExample
    use Laminas\EventManager\Event

Create the Module.php file:

1. Create a directory called EmailExample in the module directory.

2. Create the file Module.php in module/EmailExample.

3. Edit Module.php to contain:

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

    namespace EmailExample;

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

4. 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
- Declare an event priority of -199 for the event listener because email delivery events are processed with a priority of -200 and the example event needs to run just before the email delivery event.

Listener::class example details:

- Events\Listener\ListenerFactory::ALL => [

We are listening to ALL here for convenience because we are listening for mail events but this means it will trigger on every event. Usually is better to just listen for the events you are interested in. For example, if you are interested in commits and shelves you would listen on COMMIT and SHELVE events.

- Events\Listener\ListenerFactory::PRIORITY => -199,

Declares an event priority of -199 for the event listener because email delivery events are processed with a priority of -200 and the example event needs to run just before the email delivery event.

- Events\Listener\ListenerFactory::CALLBACK => 'handleEmail',

Declares the function name within the listener class that is called.

- Events\Listener\ListenerFactory::MANAGER_CONTEXT => 'queue'

Triggers your custom listener when P4 Code Review processes the P4 Code Review event queue.

Create the module.config.php file:

1. Create a directory called config in the EmailExample directory.
2. Create a file called module.config.php in config.
3. Edit module.config.php to contain:

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

    $listeners = [EmailExample\Listener\Listener::class];
    return [
        'listeners' => $listeners,
        'service_manager' =>[
            'factories' => array_fill_keys(
                $listeners,
                Events\Listener\ListenerFactory::class
            )
        ],
        Events\Listener\ListenerFactory::EVENT_LISTENER_CONFIG => [
            Events\Listener\ListenerFactory::ALL => [
                EmailExample\Listener\Listener::class => [
                    [
                        Events\Listener\ListenerFactory::PRIORITY => -199,
                        Events\Listener\ListenerFactory::CALLBACK => 'handleEmail',
                        Events\Listener\ListenerFactory::MANAGER_CONTEXT => 'queue'
                    ]
                ]
            ]
        ]
    ];

4. Now Create the Listener.php file.

## Create the Listener.php file

The Listener.php file will:

- Contains the implementation of your event listener
- Allow logging

Create the Listener.php file:

1. Create a directory called src in the EmailExample directory.
2. Create a directory called Listener in the src directory.
3. Create a file called Listener.php in Listener.
4. Edit Listener.php to contain:

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

    namespace EmailExample\Listener;

    use Events\Listener\AbstractEventListener;
    use Laminas\EventManager\Event;

    class Listener extends AbstractEventListener
    {
        /**
         * Automatically uses any custom email templates found under this
         * module's view/mail folder (e.g. Example/view/mail/commit-html.phtml).
         *
         * Valid templates include:
         *
         *  commit-html.phtml (HTML version of commit notification)
         *  commit-text.phtml (text version of commit notification)
         *  comment-html.phtml (HTML version of comment notification)
         *  comment-text.phtml (text version of comment notification)
         *  review-html.phtml (HTML version of review notification)
         *  review-text.phtml (text version of review notification)
         *
         * Note: you need to provide custom templates for both HTML and text;
         * if you do not provide both, it is possible that the search for
         * customized templates only finds the non-customized versions, making
         * it appear that this module is not working.
         */

        /**
         * Handle the Email and set the new templates.
         *
         * @param Event $event
         */
        public function handleEmail(Event $event)
        {
            $logger = $this->services->get('logger');
            $logger->info("EmailExample: handleEmail");
            $mail = $event->getParam('mail');
            if (!$mail || !isset($mail['htmlTemplate'], $mail['textTemplate'])) {
                return;
            }

            $html = __DIR__ . '/view/mail/' . basename($mail['htmlTemplate']);
            $text = __DIR__ . '/view/mail/' . basename($mail['textTemplate']);

            if (file_exists($html)) {
                $mail['htmlTemplate'] = $html;
            }
            if (file_exists($text)) {
                $mail['textTemplate'] = $text;
            }

            $event->setParam('mail', $mail);
            $logger->info("EmailExample: handleEmail end.");
        }
    }

5. Now Create the comment-html.phtml file.

## Create the comment-html.phtml file

The comment-html.phtml file is a view script that provides the content for the HTML portion of the comment notification email.

It is considered best practice to use inline CSS for styling emails.

Create the comment-html.phtml file:

1. Create a directory called view in the module/Example directory.
2. Create a directory called mail in the module/Example/view directory.
3. Create the comment-html.phtml file in module/Example/view/mail.
4. Edit comment-html.phtml to contain:

    <?php
        $user       = $activity->get('user');
        $userLink   = $user
                    ? $this->qualifiedUrl('user', array('user' => $user))
                    : null;
        $targetLink = $activity->getUrl($this->plugin('qualifiedUrl'));
    ?>
    <html>
      <body style="font-family: sans-serif; background-color: #eee; padding: 1em;">
        <div style="background-color: #fff; border: 1px solid #ccc; padding: 1em;">
          <div style="font-size: 115%;">
            <?php if ($user): ?>
              <a style="text-decoration: none;" href="<?php echo $userLink ?>">
                <?php echo $this->escapeHtml($user) ?>
              </a>
            <?php endif; ?>
            <?php echo $this->escapeHtml($activity->get('action')) ?>
            <a style="text-decoration: none;" href="<?php echo $targetLink ?>">
              <?php echo $this->escapeHtml($activity->get('target'))?>
            </a>
          </div>
          <br/>
          <?php
              // if the comment has file context, show it.
              $comment = $event->getParam('comment');
              $context = $comment
                       ? $comment->getFileContext()
                       : array('content' => null, 'line' => null);
              if (is_array($context['content']) && $context['line']) {
                 $line = $context['line'] - count($context['content']) + 1;
                 echo '<div style="font-family: monospace; white-space: nowrap;'
                    . ' padding: .5em 1em; overflow-x: auto; color: #444;'
                    . ' border: 1px solid #ddd; background-color: #f7f7f7;">';
                 foreach ((array) $context['content'] as $i => $content) {
                    echo '<div><span style="color: #999;">'
                        . str_pad($line + $i,
                                  strlen($context['line']),
                                  "0",
                                  STR_PAD_LEFT
                          )
                        . '.</span>&nbsp;'
                        . $this->preformat($content)
                               ->setLinkify(false)
                               ->setEmojify(false)
                               ->setWordWrap(900)
                        . "</div>\n";
                }
                echo '</div><br/>';
             }
          ?>
          <div style="padding-bottom: .5em;">
          <?php
              echo $this->preformat($activity->get('description'))
                      ->setBaseUrl($this->qualifiedUrl())
                      ->setEmojify(false)
                      ->setWordWrap(900)
          ?>
        </div>
      </div>
    </body>
    </html>

5. Now Create the comment-text.phtml file.

## Create the comment-text.phtml file

The comment-text.phtml is a view script that provides the content for the text-only portion of the comment notification email.

Create the comment-text.phtml file:

1. Create the file comment-text.phtml in the module/Example/view/mail directory.
2. Edit comment-text.phtml to contain:

       <?php
           echo trim($activity->get('user')
               . ' commented on '
               . $activity->get('target'));

           // if the comment has file context, show it.
           $comment = $event->getParam('comment');
           $context = $comment
                    ? $comment->getFileContext()
                    : array('content' => null);
           if (is_array($context['content'])) {
               echo "\n\n> " . $this->wordWrap(
                   implode("\n> ", $context['content']), 900
               );
           }

           echo "\n\n" . trim($this->wordWrap($activity->get('description'), 900));
           echo "\n\n" . $activity->getUrl($this->plugin('qualifiedUrl'));
       ?>

3. Now Enable the EmailExample module for P4 Code Review in custom.modules.config.php.

## Enable the EmailExample module for P4 Code Review in custom.modules.config.php

P4 Code Review 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 P4 Code Review 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 EmailExample module details:

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

Just add EmailExample 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(
                    'EmailExample'      => BASE_PATH . '/module/EmailExample/src',
                )
            )
        )
    );
    return [
        'EmailExample'
    ];

4. The P4 Code Review config cache must be reloaded so that P4 Code Review 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.

The Email Example module is now enabled for P4 Code Review. P4 Code Review will use the new custom email template whenever a comment notification is sent out.

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

## Extending the EmailExample module to other email templates

If you need to customize any other types of P4 Code Review notification email messages, locate the view scripts (both HTML and text) and copy them into module/Example/view/mail, maintaining the existing filenames, then modify the new files as desired.

If you do not copy both the HTML and text templates, it is possible for the search for customized templates to only find non-customized versions, making it appear that your module is not working.

Your search for returned results