Please wait...

Guide to follow while creating your MailWizz extensions

This guide is a work in progress and it applies for upcoming MailWizz 2.0 release, we will take Amazon S3 extension as a test case.
The naming convention of the extension folder and extension init file for the extension remains the same, if your extension folder is called amazon-s3 your extension init file must be called AmazonS3Ext.php and must contain the class AmazonS3Ext which extends from ExtensionInit class:

<?php defined('MW_PATH') or exit('No direct script access allowed');

/**
 * AmazonS3Ext
 *
 */

class AmazonS3Ext extends ExtensionInit
{
}

The init class must declare following public properties and implement the run() method:

class AmazonS3Ext extends ExtensionInit
{
    /**
     * Name of the extension as shown in the backend panel
     *
     * @var string
     */
    public $name = 'Amazon S3';

    /**
     * Description of the extension as shown in backend panel
     *
     * @var string
     */
    public $description = 'Store various assets using Amazon S3.';

    /**
     * Current version of this extension
     *
     * @var string
     */
    public $version = '2.0.0';

    /**
     * Minimum app version required for this extension to run properly
     *
     * @var string
     */
    public $minAppVersion = '2.0.0';

    /**
     * The author name
     *
     * @var string
     */
    public $author = 'Your Name';

    /**
     * Author website
     *
     * @var string
     */
    public $website = 'https://www.website.com/';

    /**
     * Contact email address
     *
     * @var string
     */
    public $email = 'email@address.com';

    /**
     * In which apps this extension is allowed to run
     *
     * @var array
     */
    public $allowedApps = ['backend', 'customer'];

    /**
     * @inheritDoc
     */
    public function run()
    {
         // init code here
    }

}

When the extension is called, MailWizz will call it’s run() method. And from there you can hook into the rest of the application.
Most of the extension can have a very simple run() method, like:

    /**
     * @inheritDoc
     */
    public function run()
    {
        if ($this->isAppName('backend')) {

            // handle all backend related tasks
            $this->backendApp();

        } elseif ($this->isAppName('customer')) {

            // handle all customer related tasks
            $this->customerApp();
        }
    }

Which simply splits the tasks for each application, in the above case, backend and customer.
In both methods, backendApp() and customerApp() can register routes, controllers, and hook in other application available hooks.
Registering a new route and controller has to be done like:

        /** register the url rule to resolve the pages */
        $this->addUrlRules([
            ['settings/index', 'pattern' => 'extensions/amazon-s3/settings'],
            ['settings/', 'pattern' => 'extensions/amazon-s3/settings/*'],
        ]);

        /** add the controllers */
        $this->addControllerMap([
            'settings' => [
                'class' => 'backend.controllers.AmazonS3ExtBackendSettingsController',
            ]
        ]);

The addUrlRules() method is a shortcut to url manager addRules() method while addControllerMap() is a shortcut to Yii::app()->controllerMap, with the addition that it adds the extension prefix in the routes for you, so is is a shortcut for:

        // register the url rule to resolve the pages.
        Yii::app()->urlManager->addRules(array(
            array('amazon_s3_ext_settings/index', 'pattern'    => 'extensions/amazon-s3/settings'),
            array('amazon_s3_ext_settings/', 'pattern' => 'extensions/amazon-s3/settings/*'),
        ));

        // add the controllers
        Yii::app()->controllerMap['ext_amazon_s3_settings'] = array(
            'class'     => 'ext-amazon-s3.backend.controllers.Ext_amazon_s3_settingsController',
            'extension' => $this,
        );

Registering the routes and controllers like explained above, gives you a few benefits, like injecting the extension instance for you automatically in the controller and the ability to create extension urls from the extension/extension controller/etc using:

// in extension:
$this->createUrl('route/here');
// in extension controller:
$this->getExtension()->createUrl('route/here');

instead of

Yii::app()->createUrl('ext_amazon_s3_route/here')

which would be lengthier to write.
Another important aspect is that you don’t have to provide the full path alias for the class index in controllerMap, you just have to provide the relative path alias, so instead of:

amazon-s3-ext.backend.controllers.Ext_amazon_s3_settingsController

you just do:

backend.controllers.AmazonS3ExtBackendSettingsController

which again, is less to write.
The naming convention for controller files and classes has to follow the format:

ExtensionNameExt <-> ApplicationName <-> FullControllerName

Thus we have:

AmazonS3Ext <-> Backend <-> SettingsController

Which is stored, as the path says( backend.controllers.AmazonS3ExtBackendSettingsController ), in the extension in the backend/controllers folder:

The controller class must extend the ExtensionController, a simple example:

class AmazonS3ExtBackendSettingsController extends ExtensionController
{
    /**
     * @inheritDoc
     */
    public function getViewPath()
    {
        return $this->getExtension()->getPathOfAlias('backend.views.settings');
    }

    /**
     * Common settings for Amazon S3
     */
    public function actionIndex()
    {
        /** @var AmazonS3ExtCommon $model */
        $model = new AmazonS3ExtCommon();

        if (request()->getIsPostRequest()) {
            $model->attributes = (array)request()->getPost($model->getModelName(), []);
            if ($model->save()) {
                notify()->addSuccess(t('app', 'Your form has been successfully saved!'));
            } else {
                notify()->addError(t('app', 'Your form has a few errors, please fix them and try again!'));
            }
        }

        $this->setData([
            'pageMetaTitle'   => $this->getData('pageMetaTitle') . ' | '. $this->t('Amazon S3'),
            'pageHeading'     => $this->t('Amazon S3'),
            'pageBreadcrumbs' => [
                t('app', 'Extensions') => createUrl('extensions/index'),
                $this->t('Amazon S3') => $this->getExtension()->createUrl('settings/index'),
            ]
        ]);

        $this->render('index', compact('model'));
    }
}

As you can see above, we have access to the extension instance via `$this->getExtension()`.
We use $this->getExtension()->getPathOfAlias(‘relative.path.here’) to provide a relative path of alias in our extension. Use $this->getExtension()->getPathAlias(‘relative.path.here’) in case you don’t need the alias transformed in a path and you just want the alias. We also use `$this->t(‘String here’)` to translate a string and `$this->getExtension()->createUrl()` to create a url.
As a side note, the createUrl method in the extension class is just a shortcut like:

    final public function createUrl(string $route, array $params = [], string $ampersand = '&')
    {
        return createUrl($this->getRoutePrefix() . $route, $params, $ampersand);
    }

So basically we just inject the route prefix in our route so we don’t have to type it by hand all the times.
If you need to reference the entire route, like for example when you do a comparison in a menu, you can do like:

$menuItems['settings']['items'][] = [
      'url'    => $this->createUrl('settings/index'),
      'label'  => $this->t('Amazon S3'),
      'active' => strpos($route, $this->getRoute('settings')) === 0
];

So you can use $this->getRoutePrefix() which will return something like amazon_s3_ext_ while $this->getRoute(‘settings’) returns amazon_s3_ext_settings.
As a comparison, in MailWizz 1.x the above would look like:

class Ext_amazon_s3_settingsController extends Controller
{
    // the extension instance
    public $extension;

    // move the view path
    public function getViewPath()
    {
        return Yii::getPathOfAlias('ext-amazon-s3.backend.views.settings');
    }

    /**
     * Common settings for Amazon S3
     */
    public function actionIndex()
    {
        $request = Yii::app()->request;
        $notify  = Yii::app()->notify;

        $model = new AmazonS3ExtCommon();
        $model->populate();

        if ($request->isPostRequest) {
            $model->attributes = (array)$request->getPost($model->modelName, array());
            if ($model->validate()) {
                $notify->addSuccess(Yii::t('app', 'Your form has been successfully saved!'));
                $model->save();
            } else {
                $notify->addError(Yii::t('app', 'Your form has a few errors, please fix them and try again!'));
            }
        }

        $this->setData(array(
            'pageMetaTitle'     => $this->data->pageMetaTitle . ' | '. Yii::t('ext_amazon_s3', 'Amazon S3'),
            'pageHeading'       => Yii::t('ext_amazon_s3', 'Amazon S3'),
            'pageBreadcrumbs'   => array(
                Yii::t('app', 'Extensions') => $this->createUrl('extensions/index'),
                Yii::t('ext_amazon_s3', 'Amazon S3') => $this->createUrl('ext_amazon_s3_settings/index'),
            )
        ));

        $this->render('index', compact('model'));
    }
}

While there are no huge differences, we introduced quite a few methods for easier access and less typing.
Extensions might need to store form data in the database, and if the data is simple enough, like configuration data, you can create your models and have them extend from the ExtensionModel class. This class offers a few methods for easily loading and saving form data and since this data is connected to a certain extension, this is a abstract class which requires you to implement the `getCategoryName(): string` method, which in most of the cases is as simple as:

    /**
     * @return string
     */
    public function getCategoryName(): string
    {
        return ';
    }

    /**
     * @return ExtensionInit
     */
    public function getExtension(): ExtensionInit
    {
        return extensionsManager()->getExtensionInstance('amazon-s3');
    }

We purposely left getExtension(): ExtensionInit for you to see. MailWizz will try to find the extension by it’s own, and if it can’t do it, you’ll have to overwrite this method and return proper extension instance.
The `getCategoryName()` method is used for 2 main cases, when you have multiple models, it’s good practice to give each model it’s own category so that variables with same name don’t collide between the models when they save. It’s also used for when you want to save data for different customers, so you can do things like:

    /**
     * @return string
     */
    public function getCategoryName(): string
    {
        return 'customer.id' . intval(customer()->getId());
    }

Which saved the data only for the currently logged in user.