
Please consider🎗 sponsoring me 🎗 to keep writing articles like this.
Enhancing a Craft CMS 3 Website with a Custom Module
Enhancing your client’s CraftCMS3 website with a Module lets you add custom functionality without resorting to using or writing a plugin

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 frompluginfactory.io and following theSo You Wanna Make a Craft
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
Link 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
Note that youcan have settings and AdminCP sections in a Module as well, but you have to
Even if you don’t consider yourself a
We’ll show you exactly how to do that in this article.
Link 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 forcraft\base\Plugin:
The craft\base\Plugin extends Module!
/** * 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{...
The craft\base\Plugin extends Module!
What this is showing is that Craft
Note that youcan have settings and AdminCP sections in a Module as well, but you have to
This follows a theme that was discussed in theSetting up a New Craft
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

This means that we’ll likely be seeing Craft
The rest of this article discusses a custom module in detail, but you can create our own onpluginfactory.io as well:

Link 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 thesite-module GitHub repo should you want to download it.
All our site module does is load anAsset Bundle that contains
This allows you to do things like have a client brand background image on the login screen, or to tweak the look

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 thecomposer create-project -s RC craftcms/craft PATH command thatPixel
Here’s what the project tree looks like; again you can download the full source from thesite-module GitHub page:
site-module project tree
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.md13 directories, 15 files
site-module project tree
If it looks complicated, don’t worry about it. There are actually more organizational folders than files there! There are essentially
- Craft’sconfig/app.php
- The module itself inmodules/sitemodule/src/SiteModule.php
- The Asset Bundle we load inmodules/sitemodule/src/assetbundles/SiteAsset.php
We didn’t have to namespace things withsitemodule/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 themodules/ directory itself.
So let’s look at these three pieces in detail:
Link 1. Edit the config/app.php
Theconfig/ directory has a number of config files that you’re used to, likegeneral.php,db.php, etc. used for various settings in Craft
Theapp.php config file is super-powerful, in that it allows you tooverride or extend any part of the Craft
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.
config/app.php
<?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' => [ ],];
config/app.php
We’re giving Craft the class of our module, along with the handlesite to refer to it by, then we’re telling it to load it for every request viabootstrap.
Link 2. The Module Class
Next up we have our Module class itself inmodules/sitemodule/src/SiteModule.php. This is what is actually loaded and executed on each request:
modules/sitemodule/src/SiteModule.php
<?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 // =========================================================================}
modules/sitemodule/src/SiteModule.php
The__construct() method may look a little scary, but we’re just setting up a Yii
Just skip over that, and check out theinit() method.
Here we check to make sure this is an AdminCP request (which are never console / command line requests), and then listening for theEVENT_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
This is great, because we usually want to override the look or functionality of something in the AdminCP, and
Link 3. Our Asset Bundle
An Asset Bundle is just a collection of arbitrary resources such as
This is what ourmodules/sitemodule/src/assetbundles/SiteAsset.php looks like:
modules/sitemodule/src/assetbundles/SiteModuleAsset.php
<?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(); }}
modules/sitemodule/src/assetbundles/SiteModuleAsset.php
It just sets thesourcePath to ourdist/ directory, meaning that everything under thedist/ directory is what should be published on the frontend inweb/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
All you really need to understand from all of this is that everything in thedist/ directory will be published inweb/cpresources/ and the
Asset Bundle directory tree
vagrant@homestead ~/webdev/craft/site-module/modules/sitemodule/src/assetbundles/sitemodule (develop) $ tree -L 3 ..├── dist│ ├── css│ │ └── SiteModule.css│ ├── img│ └── js│ └── SiteModule.js└── SiteModuleAsset.php4 directories, 3 files
Asset Bundle directory tree
So you can modify theSite.css andSite.js to your heart’s content, and it’ll be loaded by our module in the AdminCP.
Link Making Composer Happy
To make Composer happy, we also need to make sure we have the following in our project’scomposer.json file:
Add to composer.json
"autoload": { "psr-4": { "modules\\sitemodule\\": "modules/sitemodule/src/" } },
Add to composer.json
This just ensures that Composer will know where to find our modules. You might also need to do:
Rebuilding the Composer autoload map
composer dump-autoload
Rebuilding the Composer autoload map
…from the project’s root directory if you didn’t already have the above in yourcomposer.json, to rebuild the Composer autoload map. This will happen automatically any time you do a composer install orcomposer update as well.
Link Modules in Action
Here’s a simple example of a Module in action, on my new podcast websitedevMode.fm:

Using a little
devMode.fm SiteModule.css
/** * 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;}
devMode.fm SiteModule.css
You can of course do quite a bit more than that in a Module. I recently redid thenystudio
As part of that process, I rewrote a very site-specific Plugin as a Module that loads some custom
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
Head on over topluginfactory.io and build your own custom Craft
Viva la modularity!
Further Reading
1 likes

24 mentions















