Creating a Yii Module for Craft 3

In some cases, you may want to make new functionality available to your Craft website that does not need to be installable, like a plugin. Some examples of this include:

  • Something needs to be able to hit a controller
  • Something needs to be executed on every request
  • Multiple plugins that depend on some of the same code

In Craft 3, it is possible to create a Yii Module to achieve this. The ability to include Yii Modules in our Craft projects adds an exciting new dimension to Craft development.

In this article, we'll take a look at building a Yii Module that can be shared between multiple Craft plugins and adds a controller action and a new template to a Craft site. This article will assume you are comfortable with (or have a general understanding of) Craft 3 plugin development, Yii Modules, and composer.

For this example we'll need:

  • A Craft 3 website
  • A Yii Module
  • A custom plugin

Our example will take place across the following files:

- composer.json

- vendor/vendorname/mycraftplugin/composer.json
- vendor/vendorname/mycraftplugin/src/Plugin.php
- vendor/vendorname/mycraftplugin/src/templates/index.html

- vendor/vendorname/myyiimodule/src/Module.php
- vendor/vendorname/myyiimodule/src/ModuleHelper.php
- vendor/vendorname/myyiimodule/src/controllers/FancyController.php
- vendor/vendorname/myyiimodule/src/templates/index.html

Add a Yii Module in a Craft project #

If your Module exists in a separate repo and does not need to be used by other plugins, you can reference it in your project's composer.json file.

composer.json

{
  ...
  "require": {
      "vendorname/myyiimodule": "^1.0.0"
  },
  ...
}

You can make your Module available to the Craft::$app instance in the config/app.php configuration file:

config/app.php

<?php

use barrelstrength\sproutcore\Module as MyYiiModule;

return [
    'modules' => [
        'my-yii-module' => MyYiiModule::class
    ],
    'bootstrap' => ['my-yii-module'],
];

Your Module will now run it's Module::init() on every page load and you can add any other functionality you need from there. You can also access your Module via the Craft::$app instance, if you wish:

$myYiiModule = Craft::$app->getModule('my-yii-module');

This will work if your Yii Module exists in a separate repo and you are importing it as a composer dependency. If you have a project-specific module that doesn't exist as a composer dependency, you will have to take some additional steps to autoload the Module's classes in your project. We won't go into those details here.

Add a Yii Module in a Craft plugin #

While some Modules may be project-specific and have no plugin dependencies, if you wish to create a Module as a complement to a plugin, you'll have to take a few more steps.

In this next example, we will use a Module to complement our plugins. In this scenario, the only thing you'll need to reference in your project's composer.json file is your plugin:

composer.json

{
    ...
    "require": {
      "vendorname/mycraftplugin": "^1.0.0"
  },
  ...
}

In your plugin's composer.json file, you will require your Module:

vendor/vendorname/mycraftplugin/composer.json

{
    ...
    "require": {
      "vendorname/myyiimodule": "^1.0.0"
  },
  ...
}

You can require your Module from one or more of your plugins. Just as all libraries required by composer, your Module will only be included once in your Craft project's vendor folder.

Note: we will extend this example at the end of this article for Modules that are being developed locally and not publicly available.

Register a Yii Module on the Craft::$app instance #

In your plugin's primary class file you'll register your Yii Module and add it to the Craft::$app instance:

vendor/vendorname/mycraftplugin/src/Plugin.php

<?php

// Make sure the ModuleHelper is loaded
use vendorname\myyiimodule\ModuleHelper;

class MyCraftPlugin extends \craft\base\Plugin
{
  public function init()
  {
    parent::init();

    // Register the Yii Module on the Craft::$app instance
    SproutCoreHelper::registerMyYiiModule();

    // ...
  }
}

The Yii ModuleHelper class checks to see if your Module is already registered, adds it to the Craft::$app instance, and loads it:

vendor/vendorname/myyiimodule/src/ModuleHelper.php

<?php

namespace vendorname\myyiimodule;

use Craft;

abstract class ModuleHelper
{
  public static function registerMyYiiModule()
  {
    // Check to see if the Module is registered
    if (!Craft::$app->hasModule('my-yii-module')) {

      // Add it to the Craft::$app instance
      Craft::$app->setModule('my-yii-module', Module::class);

      // Have Craft load this module right away 
      // This ensures it can be used for templates
      Craft::$app->getModule('my-yii-module');
    }
  }
}

In this example, we've chosen to register our Module with the handle my-yii-module, however, you can choose any name you want. Be sure you choose a name that is unique as you don't want to choose something that will conflict with another plugin name.

Initialize a Yii Module #

As Craft plugins have a primary Plugin.php file, your Yii Module has a primary Module.php file. Here is a simple version of what that file may look like:

vendor/vendorname/myyiimodule/src/Module.php

<?php

namespace vendorname\myyiimodule;

class Module extends \yii\base\Module
{
  public function init()
  {
    parent::init();

    $this->params['foo'] = 'bar';

    // ...
  }
}

Access a Yii Module in a Craft Plugin #

Once your Module is loaded, you can access your Module using the getModule method. Let's take another look at the plugin's primary class file and access the Module after it is loaded:

vendor/vendorname/mycraftplugin/src/Plugin.php

<?php

use vendorname\myyiimodule\ModuleHelper;

class MyCraftPlugin extends \craft\base\Plugin
{
  public function init()
  {
    parent::init();

    // Register our Yii Module on our Craft::$app instance
    SproutCoreHelper::registerMyYiiModule();

    // Do something with our Yii Module
    $myYiiModule = Craft::$app->getModule('my-yii-module');
  }
}

Adding a Controller to a Yii Module #

Modules in Yii, just like Plugins in Craft, support adding custom controllers to a controllers folder. The same naming conventions apply, as Craft just gets those conventions from Yii to begin with:

vendor/vendorname/myyiimodule/src/controllers/FancyController.php

<?php
namespace vendorname\myyiimodule\controllers;

class FancyController extends BaseController
{
  public function actionDoSomething()
  {
    // All that jazz...
  }
}

You can now add a form to a template file in your Craft plugin that submits to your Yii Module's controller:

vendor/vendorname/mycraftplugin/src/templates/index.html

{% extends "_layouts/cp" %}

{% block content %}

<form method="post" accept-charset="utf-8">
  {{ getCsrfInput() }}
  <input type="hidden" name="action" value="my-yii-module/fancy/do-something">
  <input type="text" name="whatever" value="" />
  <button>Submit</button>
</form>

{% endblock %}

Notice that the URL that the action uses (admin/actions/my-yii-module/fancy/do-something) does not include a reference to any installed plugins — just the Yii module. The my-yii-module segment of the URL triggers your Module's controller because you registered it on the Craft::$app instance above.

Register new Craft template paths in a Yii Module #

To allow your Yii Module to display templates within Craft, you'll need to take an additional step. By default, Yii Modules do not support templates in the same way that Craft does. In our Module class file, you'll need to register your Module's template path so Craft knows it can find templates there too:

vendor/vendorname/myyiimodule/src/Module.php

<?php

namespace vendorname\myyiimodule;

// Identify any classes you need to use
use Craft;
use yii\base\Event;
use craft\web\View;
use craft\events\RegisterTemplateRootsEvent;

class Module extends \yii\base\Module
{
  public function init()
  {
    parent::init();

    // Register a new templates folder within a Module 
    Event::on(View::class, View::EVENT_REGISTER_CP_TEMPLATE_ROOTS, function(RegisterTemplateRootsEvent $e) {
      $e->roots['my-yii-module'] = $this->getBasePath().DIRECTORY_SEPARATOR.'templates';
    });
  }
}

Once you've registered your Module's template folder, you can now access templates in your Module in the same way that you would access them in your plugin's template folder. The following template would be visible at the URL:

http:://website.com/admin/my-yii-module/index

vendor/vendorname/myyiimodule/src/templates/index.html

{% extends "_layouts/cp" %}

{% block content %}

  My Yii Module Template!

{% endblock %}

Including a private Yii Module in a Craft Project #

This article has assumed you could add your Yii Module to your project via composer. If your Module is public, the method described above will work fine. If you have not yet made your Module public, or you don't wish to, you'll have to take an additional step and tell composer where it can find your Module using the path type in composer's repositories array.

Here is an abbreviated composer file that illustrates how that might work:

{
  ...

  "require": {
    "vendorname/mycraftplugin": "^1.0.0"
  },
  "repositories": [
    {
      "type": "path",
      "url": "../../relative/path/to/myyiimodule"
    },
    {
      "type": "composer",
      "url": "https://asset-packagist.org"
    }
  ]
}

Don't miss any Craft tips, tricks, and community updates