DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

2 1

Enhancing a Craft CMS 3 Website with a Custom Module

Enhancing a Craft CMS 3 Website with a Custom Module

Enhanc­ing your clien­t’s Craft CMS 3 web­site with a Mod­ule lets you add cus­tom func­tion­al­i­ty with­out resort­ing to using or writ­ing a plugin

Andrew Welch / nystudio107

Craft-Cms-3-Yii2-Custom-Module

Some­times you want to enhance a client web­site with some func­tion­al­i­ty or design that’s very spe­cif­ic to that web­site. Cer­tain­ly you could do this with a cus­tom plu­g­in with scaf­fold­ing from plug​in​fac​to​ry​.io and fol­low­ing the So You Wan­na Make a Craft 3 Plu­g­in? article.

How­ev­er, for many things this just seems like too much work. Maybe you just want to enhance the look of the login screen to apply a back­ground image with the clien­t’s brand. A cus­tom plu­g­in seems like a bit much.

With Craft CMS 3, Craft intro­duces the con­cept of a Mod­ule, which fits the bill per­fect­ly for this type of scenario.

Mod­ules vs. Plugins

The pri­ma­ry dif­fer­ences between a Mod­ule and a Plu­g­in are:

  • Plu­g­ins can be disabled
  • Plu­g­ins can be uninstalled
  • Plu­g­ins have a frame­work for Set­tings in the AdminCP

Oth­er than that, they are quite sim­i­lar. Both Mod­ules and Plu­g­ins are writ­ten in PHP, and can access the full Craft CMS APIs.

Note that you can have set­tings and AdminCP sec­tions in a Mod­ule as well, but you have to ​“roll your own” via lis­ten­ing to the appro­pri­ate events, adding the appro­pri­ate routes, etc.

Even if you don’t con­sid­er your­self a ​“PHP devel­op­er”, it’s pret­ty easy to get a sim­ple Mod­ule up and run­ning that will load some cus­tom CSS or JavaScript in the Craft AdminCP that enhances the expe­ri­ence for your client.

We’ll show you exact­ly how to do that in this article.

Mod­ules Under the Hood

A nice way to think about Mod­ules is that they are Plu­g­ins that can’t be unin­stalled. They strike a nice bal­ance between being easy to imple­ment, and offer­ing the func­tion­al­i­ty of a plugin.

Module-Plugin-Balance

While it’s tempt­ing to think of Mod­ules are stripped down Plu­g­ins, the real­i­ty is that Plu­g­ins are actu­al­ly built on top of Modules!

Have a look at the code for craft\base\Plugin:


/**
 * Plugin is the base class for classes representing plugins in terms of objects.
 *
 * @property string $handle The plugin’s handle (alias of [[id]])
 * @property MigrationManager $migrator The plugin’s migration manager
 *
 * @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
 * @since 3.0
 */
class Plugin extends Module implements PluginInterface
{
...

What this is show­ing is that Craft CMS 3 Plu­g­ins are actu­al­ly Yii2 Mod­ules, but just with some enhance­ments added to them by Pix­el & Ton­ic. These enhance­ments allow plu­g­ins to be unin­stalled, have set­tings, AdminCP sec­tions, etc.

Note that you can have set­tings and AdminCP sec­tions in a Mod­ule as well, but you have to ​“roll your own” via lis­ten­ing to the appro­pri­ate events, adding the appro­pri­ate routes, etc.

This fol­lows a theme that was dis­cussed in the Set­ting up a New Craft CMS 3 Project arti­cle, which is that Craft CMS 3 has been entire­ly refac­tored on top of Yii2.

This is an impor­tant point, because many cus­tom apps that would nor­mal­ly be built using a frame­work like Lar­avel very well may be built using Craft CMS 3. Check out the REST­ful API with Craft 3 for an exam­ple of doing just that!

Craft-Cms-3-Content-Management-Framework

This means that we’ll like­ly be see­ing Craft CMS 3 being used as a frame­work & foun­da­tion for web apps that want an awe­some CMS back­end for free. But I digress…

The rest of this arti­cle dis­cuss­es a cus­tom mod­ule in detail, but you can cre­ate our own on plug​in​fac​to​ry​.io as well:

Pluginfactory-Io-Custom-Modules

Set­ting Up a Site Module

So let’s talk about set­ting up an actu­al site mod­ule for our Craft web­site. All of the code list­ed here is avail­able in the site-mod­ule GitHub repo should you want to down­load it.

All our site mod­ule does is load an Asset Bun­dle that con­tains CSS and JavaScript that we want loaded in the AdminCP.

This allows you to do things like have a client brand back­ground image on the login screen, or to tweak the look & func­tion­al­i­ty of the AdminCP as you see fit via CSS & JavaScript.

Craft-Asset-Bundles

Mod­ules can do quite a bit more than this, in fact they can do any­thing a Plu­g­in can do. But this foun­da­tion allows a fron­tend devel­op­er to enhance their clien­t’s web­site with­out need­ing to get into the nit­ty grit­ty of how the mod­ule works.

You’ll find that if you used the composer create-project -s RC craftcms/craft PATH com­mand that Pix­el & Ton­ic rec­om­mends to cre­ate your new project, they’ve even pro­vid­ed a sam­ple config/app.php and modules/Module.php for you already. We’ve tweaked things a bit from this, so let’s get to it!

Here’s what the project tree looks like; again you can down­load the full source from the site-mod­ule GitHub page:


vagrant@homestead ~/webdev/craft/site-module (develop) $ tree -L 8 .
.
├── CHANGELOG.md
├── composer.json
├── config
│   └── app.php
├── LICENSE.md
├── modules
│   └── sitemodule
│   ├── CHANGELOG.md
│   ├── config
│   │   └── app.php
│   ├── LICENSE.md
│   ├── README.md
│   └── src
│   ├── assetbundles
│   │   └── sitemodule
│   │   ├── dist
│   │   │   ├── css
│   │   │   │   └── SiteModule.css
│   │   │   ├── img
│   │   │   │   └── SiteModule-icon.svg
│   │   │   └── js
│   │   │   └── SiteModule.js
│   │   └── SiteModuleAsset.php
│   ├── SiteModule.php
│   └── translations
│   └── en
│   └── site-module.php
└── README.md

13 directories, 15 files

If it looks com­pli­cat­ed, don’t wor­ry about it. There are actu­al­ly more orga­ni­za­tion­al fold­ers than files there! There are essen­tial­ly 3 parts to it:

  1. Craft’s config/app.php
  2. The mod­ule itself in modules/sitemodule/src/SiteModule.php
  3. The Asset Bun­dle we load in modules/sitemodule/src/assetbundles/SiteAsset.php

We did­n’t have to name­space things with sitemodule/src but we want a fold­er to group every­thing con­tained in our mod­ule togeth­er (sitemodule) in case we have oth­er mod­els, and it’s a con­ven­tion to put all of our source code in a src sub-directory.

You could just as eas­i­ly get rid of those two direc­to­ries, and put every­thing inside of the modules/ direc­to­ry itself.

So let’s look at these three pieces in detail:

1. Edit the config/app.php

The config/ direc­to­ry has a num­ber of con­fig files that you’re used to, like general.php, db.php, etc. used for var­i­ous set­tings in Craft CMS 3. But it also can have an app.php con­fig file.

The app.php con­fig file is super-pow­er­ful, in that it allows you to over­ride or extend any part of the Craft CMS 3 Yii2 app. Read that again, because it’s huge. With a sim­ple con­fig file, we can extend the Yii2 app that is Craft CMS 3, or we can replace func­tion­al­i­ty entirely.

We’re just going to dip our toe into it, and add a bit of code to it to tell it about our new Mod­ule, and to load it for us.


<?php
/**
 * Yii Application Config
 *
 * Edit this file at your own risk!
 *
 * The array returned by this file will get merged with
 * vendor/craftcms/cms/src/config/app/main.php and [web|console].php, when
 * Craft's bootstrap script is defining the configuration for the entire
 * application.
 *
 * You can define custom modules and system components, and even override the
 * built-in system components.
 */

return [

    // All environments
    '*' => [
        'modules' => [
            'site-module' => [
                'class' => \modules\sitemodule\SiteModule::class,
            ],
        ],
        'bootstrap' => ['site-module'],
    ],

    // Live (production) environment
    'live' => [
    ],

    // Staging (pre-production) environment
    'staging' => [
    ],

    // Local (development) environment
    'local' => [
    ],
];

We’re giv­ing Craft the class of our mod­ule, along with the han­dle site to refer to it by, then we’re telling it to load it for every request via bootstrap.

2. The Mod­ule Class

Next up we have our Mod­ule class itself in modules/sitemodule/src/SiteModule.php. This is what is actu­al­ly loaded and exe­cut­ed on each request:


<?php
/**
 * Site module for Craft CMS 3.x
 *
 * An example module for Craft CMS 3 that lets you enhance your websites with a custom site module
 *
 * @link https://nystudio107.com/
 * @copyright Copyright (c) 2018 nystudio107
 */

namespace modules\sitemodule;

use modules\sitemodule\assetbundles\sitemodule\SiteModuleAsset;

use Craft;
use craft\events\RegisterTemplateRootsEvent;
use craft\events\TemplateEvent;
use craft\i18n\PhpMessageSource;
use craft\web\View;

use yii\base\Event;
use yii\base\InvalidConfigException;
use yii\base\Module;

/**
 * Class SiteModule
 *
 * @author nystudio107
 * @package SiteModule
 * @since 1.0.0
 *
 */
class SiteModule extends Module
{
    // Static Properties
    // =========================================================================

    /**
     * @var SiteModule
     */
    public static $instance;

    // Public Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    public function __construct($id, $parent = null, array $config = [])
    {
        Craft::setAlias('@modules/sitemodule', $this->getBasePath());
        $this->controllerNamespace = 'modules\sitemodule\controllers';

        // Translation category
        $i18n = Craft::$app->getI18n();
        /** @noinspection UnSafeIsSetOverArrayInspection */
        if (!isset($i18n->translations[$id]) && !isset($i18n->translations[$id.'*'])) {
            $i18n->translations[$id] = [
                'class' => PhpMessageSource::class,
                'sourceLanguage' => 'en-US',
                'basePath' => '@modules/sitemodule/translations',
                'forceTranslation' => true,
                'allowOverrides' => true,
            ];
        }

        // Base template directory
        Event::on(View::class, View::EVENT_REGISTER_CP_TEMPLATE_ROOTS, function (RegisterTemplateRootsEvent $e) {
            if (is_dir($baseDir = $this->getBasePath().DIRECTORY_SEPARATOR.'templates')) {
                $e->roots[$this->id] = $baseDir;
            }
        });

        // Set this as the global instance of this module class
        static::setInstance($this);

        parent::__construct($id, $parent, $config);
    }

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        self::$instance = $this;

        if (Craft::$app->getRequest()->getIsCpRequest()) {
            Event::on(
                View::class,
                View::EVENT_BEFORE_RENDER_TEMPLATE,
                function (TemplateEvent $event) {
                    try {
                        Craft::$app->getView()->registerAssetBundle(SiteModuleAsset::class);
                    } catch (InvalidConfigException $e) {
                        Craft::error(
                            'Error registering AssetBundle - '.$e->getMessage(),
                            __METHOD__
                        );
                    }
                }
            );
        }

        Craft::info(
            Craft::t(
                'site-module',
                '{name} module loaded',
                ['name' => 'Site']
            ),
            __METHOD__
        );
    }

    // Protected Methods
    // =========================================================================
}

The __construct() method may look a lit­tle scary, but we’re just set­ting up a Yii2 alias to our Mod­ule’s direc­to­ry so we can use it lat­er, then set­ting things up so that our mod­ule can have trans­la­tions, and poten­tial­ly tem­plates in the AdminCP as well.

Just skip over that, and check out the init() method.

Here we check to make sure this is an AdminCP request (which are nev­er con­sole / com­mand line requests), and then lis­ten­ing for the EVENT_BEFORE_RENDER_TEMPLATE event.

This event is fired just before a Twig tem­plate is about to be ren­dered. This lets us load our Asset Bun­dle along with its CSS & JavaScript last, after every­thing else has been loaded.

This is great, because we usu­al­ly want to over­ride the look or func­tion­al­i­ty of some­thing in the AdminCP, and CSS Speci­fici­ty means that if we’re loaded last, we get a shot at doing just that.

3. Our Asset Bundle

An Asset Bun­dle is just a col­lec­tion of arbi­trary resources such as CSS, JavaScript, images, etc. that need to be loaded and avail­able on the fron­tend. You can read more about Asset Bun­dles here.

This is what our modules/sitemodule/src/assetbundles/SiteAsset.php looks like:


<?php
/**
 * Site module for Craft CMS 3.x
 *
 * An example module for Craft CMS 3 that lets you enhance your websites with a custom site module
 *
 * @link https://nystudio107.com/
 * @copyright Copyright (c) 2018 nystudio107
 */

namespace modules\sitemodule\assetbundles\SiteModule;

use Craft;
use craft\web\AssetBundle;
use craft\web\assets\cp\CpAsset;

/**
 * @author nystudio107
 * @package SiteModule
 * @since 1.0.0
 */
class SiteModuleAsset extends AssetBundle
{
    // Public Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    public function init()
    {
        $this->sourcePath = "@modules/sitemodule/assetbundles/sitemodule/dist";

        $this->depends = [
            CpAsset::class,
        ];

        $this->js = [
            'js/SiteModule.js',
        ];

        $this->css = [
            'css/SiteModule.css',
        ];

        parent::init();
    }
}

It just sets the sourcePath to our dist/ direc­to­ry, mean­ing that every­thing under the dist/ direc­to­ry is what should be pub­lished on the fron­tend in web/cpresources/ in a hashed direc­to­ry name.

Then it says that we depend on the AdminCP Asset­Bun­dle being loaded already, and gives a path to the CSS & JavaScript that we want inject­ed into the AdminCP templates.

All you real­ly need to under­stand from all of this is that every­thing in the dist/ direc­to­ry will be pub­lished in web/cpresources/ and the CSS & JavaScript we spec­i­fied will be loaded:


vagrant@homestead ~/webdev/craft/site-module/modules/sitemodule/src/assetbundles/sitemodule (develop) $ tree -L 3 .
.
├── dist
│   ├── css
│   │   └── SiteModule.css
│   ├── img
│   └── js
│   └── SiteModule.js
└── SiteModuleAsset.php

4 directories, 3 files

So you can mod­i­fy the Site.css and Site.js to your heart’s con­tent, and it’ll be loaded by our mod­ule in the AdminCP.

Mak­ing Com­pos­er Happy

To make Com­pos­er hap­py, we also need to make sure we have the fol­low­ing in our pro­jec­t’s composer.json file:


"autoload": {
    "psr-4": {
      "modules\\sitemodule\\": "modules/sitemodule/src/"
    }
  },

This just ensures that Com­pos­er will know where to find our mod­ules. You might also need to do:


composer dump-autoload

…from the pro­jec­t’s root direc­to­ry if you did­n’t already have the above in your composer.json, to rebuild the Com­pos­er autoload map. This will hap­pen auto­mat­i­cal­ly any time you do a composer install or composer update as well.

Mod­ules in Action

Here’s a sim­ple exam­ple of a Mod­ule in action, on my new pod­cast web­site dev​Mode​.fm:

Devmode-Fm-Craft-Module

Using a lit­tle CSS, all it does is put our col­or­ful back­ground image on the login page:


/**
 * SiteModule CSS
 *
 * @author nystudio107
 * @copyright Copyright (c) 2017 nystudio107
 * @link https://nystudio107.com
 * @package SiteModule
 * @since 1.0.0
 */

body.login {
    background-size: 600px;
    background-repeat: repeat;
    background-image: url('/img/site/devmode-fm-light-bg-opaque.svg');
}

body.login label, body.login #forgot-password {
    background-color: #FFF;
}

You can of course do quite a bit more than that in a Mod­ule. I recent­ly redid the nys​tu​dio107​.com web­site you’re read­ing right now to use Craft CMS 3 & Tail­wind CSS.

As part of that process, I rewrote a very site-spe­cif­ic Plu­g­in as a Mod­ule that loads some cus­tom CSS & JavaScript, reg­is­ters a cus­tom Redac­tor II plu­g­in, and more.

While the exam­ple pre­sent­ed here is rel­a­tive­ly sim­plis­tic, you can do things like reg­is­ter Fields, add Twig fil­ters, and oth­er such things from a Mod­ule just like you can from a Plugin.

The gen­er­al rule of thumb is that any­thing that’s very site-spe­cif­ic or ​“busi­ness logic”-ish, you might want to refac­tor as a Mod­ule. Then just check it into the web­site’s main repos­i­to­ry, rather than hav­ing it as a sep­a­rate Plugin.

Head on over to plug​in​fac​to​ry​.io and build your own cus­tom Craft CMS 3 module!

Viva la modularity!

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay