DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

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

Top comments (0)