Enhancing a Craft CMS 3 Website with a Custom Module
Enhancing your client’s Craft CMS 3 website with a Module lets you add custom functionality without resorting to using or writing a plugin
Andrew Welch / nystudio107
Sometimes you want to enhance a client website with some functionality or design that’s very specific to that website. Certainly you could do this with a custom plugin with scaffolding from pluginfactory.io and following the So You Wanna Make a Craft 3 Plugin? article.
However, 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 background image with the client’s brand. A custom plugin seems like a bit much.
With Craft CMS 3, Craft introduces the concept of a Module, which fits the bill perfectly for this type of scenario.
Modules vs. Plugins
The primary differences between a Module and a Plugin are:
- Plugins can be disabled
- Plugins can be uninstalled
- Plugins have a framework for Settings in the AdminCP
Other than that, they are quite similar. Both Modules and Plugins are written in PHP, and can access the full Craft CMS APIs.
Note that you can have settings and AdminCP sections in a Module as well, but you have to “roll your own” via listening to the appropriate events, adding the appropriate routes, etc.
Even if you don’t consider yourself a “PHP developer”, it’s pretty easy to get a simple Module up and running that will load some custom CSS or JavaScript in the Craft AdminCP that enhances the experience for your client.
We’ll show you exactly how to do that in this article.
Modules Under the Hood
A nice way to think about Modules is that they are Plugins that can’t be uninstalled. They strike a nice balance between being easy to implement, and offering the functionality of a plugin.
While it’s tempting to think of Modules are stripped down Plugins, the reality is that Plugins are actually 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 showing is that Craft CMS 3 Plugins are actually Yii2 Modules, but just with some enhancements added to them by Pixel & Tonic. These enhancements allow plugins to be uninstalled, have settings, AdminCP sections, etc.
Note that you can have settings and AdminCP sections in a Module as well, but you have to “roll your own” via listening to the appropriate events, adding the appropriate routes, etc.
This follows a theme that was discussed in the Setting up a New Craft CMS 3 Project article, which is that Craft CMS 3 has been entirely refactored on top of Yii2.
This is an important point, because many custom apps that would normally be built using a framework like Laravel very well may be built using Craft CMS 3. Check out the RESTful API with Craft 3 for an example of doing just that!
This means that we’ll likely be seeing Craft CMS 3 being used as a framework & foundation for web apps that want an awesome CMS backend for free. But I digress…
The rest of this article discusses a custom module in detail, but you can create our own on pluginfactory.io as well:
Setting Up a Site Module
So let’s talk about setting up an actual site module for our Craft website. All of the code listed here is available in the site-module GitHub repo should you want to download it.
All our site module does is load an Asset Bundle that contains CSS and JavaScript that we want loaded in the AdminCP.
This allows you to do things like have a client brand background image on the login screen, or to tweak the look & functionality of the AdminCP as you see fit via CSS & JavaScript.
Modules can do quite a bit more than this, in fact they can do anything a Plugin can do. But this foundation allows a frontend developer to enhance their client’s website without needing to get into the nitty gritty of how the module works.
You’ll find that if you used the composer create-project -s RC craftcms/craft PATH command that Pixel & Tonic recommends to create your new project, they’ve even provided a sample 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 download the full source from the site-module 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 complicated, don’t worry about it. There are actually more organizational folders than files there! There are essentially 3 parts to it:
- Craft’s config/app.php
- The module itself in modules/sitemodule/src/SiteModule.php
- The Asset Bundle we load in modules/sitemodule/src/assetbundles/SiteAsset.php
We didn’t have to namespace things with sitemodule/src but we want a folder to group everything contained in our module together (sitemodule) in case we have other models, and it’s a convention to put all of our source code in a src sub-directory.
You could just as easily get rid of those two directories, and put everything inside of the modules/ directory itself.
So let’s look at these three pieces in detail:
1. Edit the config/app.php
The config/ directory has a number of config files that you’re used to, like general.php, db.php, etc. used for various settings in Craft CMS 3. But it also can have an app.php config file.
The app.php config file is super-powerful, in that it allows you to override or extend any part of the Craft CMS 3 Yii2 app. Read that again, because it’s huge. With a simple config file, we can extend the Yii2 app that is Craft CMS 3, or we can replace functionality 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 Module, 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 giving Craft the class of our module, along with the handle site to refer to it by, then we’re telling it to load it for every request via bootstrap.
2. The Module Class
Next up we have our Module class itself in modules/sitemodule/src/SiteModule.php. This is what is actually loaded and executed 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 little scary, but we’re just setting up a Yii2 alias to our Module’s directory so we can use it later, then setting things up so that our module can have translations, and potentially templates 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 never console / command line requests), and then listening for the EVENT_BEFORE_RENDER_TEMPLATE event.
This event is fired just before a Twig template is about to be rendered. This lets us load our Asset Bundle along with its CSS & JavaScript last, after everything else has been loaded.
This is great, because we usually want to override the look or functionality of something in the AdminCP, and CSS Specificity means that if we’re loaded last, we get a shot at doing just that.
3. Our Asset Bundle
An Asset Bundle is just a collection of arbitrary resources such as CSS, JavaScript, images, etc. that need to be loaded and available on the frontend. You can read more about Asset Bundles 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/ directory, meaning that everything under the dist/ directory is what should be published on the frontend in web/cpresources/ in a hashed directory name.
Then it says that we depend on the AdminCP AssetBundle being loaded already, and gives a path to the CSS & JavaScript that we want injected into the AdminCP templates.
All you really need to understand from all of this is that everything in the dist/ directory will be published in web/cpresources/ and the CSS & JavaScript we specified 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 modify the Site.css and Site.js to your heart’s content, and it’ll be loaded by our module in the AdminCP.
Making Composer Happy
To make Composer happy, we also need to make sure we have the following in our project’s composer.json file:
"autoload": {
"psr-4": {
"modules\\sitemodule\\": "modules/sitemodule/src/"
}
},
This just ensures that Composer will know where to find our modules. You might also need to do:
composer dump-autoload
…from the project’s root directory if you didn’t already have the above in your composer.json, to rebuild the Composer autoload map. This will happen automatically any time you do a composer install or composer update as well.
Modules in Action
Here’s a simple example of a Module in action, on my new podcast website devMode.fm:
Using a little CSS, all it does is put our colorful background 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 Module. I recently redid the nystudio107.com website you’re reading right now to use Craft CMS 3 & Tailwind CSS.
As part of that process, I rewrote a very site-specific Plugin as a Module that loads some custom CSS & JavaScript, registers a custom Redactor II plugin, and more.
While the example presented here is relatively simplistic, you can do things like register Fields, add Twig filters, and other such things from a Module just like you can from a Plugin.
The general rule of thumb is that anything that’s very site-specific or “business logic”-ish, you might want to refactor as a Module. Then just check it into the website’s main repository, rather than having it as a separate Plugin.
Head on over to pluginfactory.io and build your own custom 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)