Modules
Add new features
What are modules?
Modules are the primary method of adding functionality to a Monti project. A module is a self-contained, reusable piece of functionality that can be installed into any project.
Unlike a "traditional" plugin folder living inside your application, in Monti every module is an independent Composer package. This means a module:
- has its own repository and its own version history;
- can be reused across as many projects as you like, simply by requiring it;
- is installed, updated and removed using Composer, like any other dependency;
- is automatically discovered and initialized by the framework, without editing any project configuration file.
Structure of a module
A module is, first and foremost, a Composer package. The minimum recommended structure is the following:
ModuleName
├── composer.json
└── src
├── Bootstrap.php
├── MyController.php
├── MyModel.php
├── routes.php
└── views
Files description
| File | Description |
|---|---|
| composer.json | Package manifest. Declares the module's PSR-4 autoload namespace and the Monti provider(s) used to register the module (see the Bootstrap class section below). |
| src/Bootstrap.php | The class in charge of registering the module into the running application. It's the class referenced in composer.json. |
| src/MyController.php | The module's main controller. It must extend \App\System\MVC\Controller. For more information, see the MVC section. |
| src/MyModel.php | One (or more) of the module's models. It must extend \App\System\MVC\Model. For more information, see the MVC section. |
| src/routes.php | File containing the module's route declarations. For more information, see the Routing section. |
| src/views | Folder where the module's views are searched. For more information, see the MVC section. |
A module only needs the minimum set of files its purpose actually requires: a module made of nothing but a model, or nothing but a controller and a couple of routes, is perfectly valid.
Creating a module
To create a new module, create a new folder (ideally its own git repository) with a composer.json file and a src folder.
{
"name": "your-vendor/modulename",
"type": "library",
"description": "A short description of what this module does",
"autoload": {
"psr-4": {
"Your\\Namespace\\ModuleName\\": "src/"
}
},
"extra": {
"monti-framework": {
"providers": [
"Your\\Namespace\\ModuleName\\Bootstrap"
]
}
}
}
The autoload.psr-4 key declares the namespace under which all the module's classes live, and the folder they're loaded from. The extra.monti-framework.providers key tells Monti which class (or classes) it must instantiate and initialize when the module is installed in a project; this is the module's Bootstrap class, explained below.
Going back to the practical example of the hypothetical Reviews module of the MVC section, we could have a structure like this:
Reviews
├── composer.json
└── src
├── Bootstrap.php
├── Review.php
├── Reviews.php
├── routes.php
└── views
└── thank_you.php
Inside the Review.php file, we'll find the Review class in the App\Modules\Reviews namespace, matching the autoload.psr-4 declaration. You can add additional classes in additional subfolders, as long as they follow PSR-4.
Reviews
├── composer.json
└── src
├── AnotherFolder
│ └── AnotherModel.php
├── Bootstrap.php
├── Review.php
├── Reviews.php
├── routes.php
└── views
└── thank_you.php
AnotherFolder is a folder containing another model called AnotherModel.php. The class will be called AnotherModel and will have the namespace App\Modules\Reviews\AnotherFolder. The class will be available with the full name \App\Modules\Reviews\AnotherFolder\AnotherModel.
e.g. $model = new \App\Modules\Reviews\AnotherFolder\AnotherModel().
<?php
namespace App\Modules\Reviews\AnotherFolder;
class AnotherModel extends \App\System\MVC\Model {
...
}
The Bootstrap class
The Bootstrap class is the entry point of a module. It's a plain class (it doesn't need to extend anything) exposing an init() method, which Monti calls once, automatically, while the application boots.
<?php
namespace App\Modules\Reviews;
class Bootstrap {
public function init() {
(new Reviews())->load_routes();
\App\System\MVC\View\View::addModuleSourcePath(__DIR__ . "/views", "Reviews");
\App\System\Project::pushModule("Reviews", Reviews::class);
}
}
A typical Bootstrap init() method takes care of:
- loading the module's routes, using the controller's
load_routes()method (inherited from\App\System\MVC\Controller), which automatically includesroutes.php; - registering the module's views, with
\App\System\MVC\View\View::addModuleSourcePath(), so that they can be found withmodule_view("Reviews::view_name", [...]). The namespace passed as the second argument must match the module name, and is what you'll use to reference the module's views. For more details, see the MVC section; - registering the module itself with
\App\System\Project::pushModule();
None of these calls are mandatory: the Bootstrap class can do as little or as much as the module requires. A module that only ships a couple of standalone helper classes, for instance, might not even need routes or views, and could have an empty init() method or skip declaring Bootstrap.php file entirely.
init() is the only method Monti calls automatically. Everything else (routes, views, additional bootstrapping) is up to you to wire up from there.Installing a module in a project
Since a module is a regular Composer package, installing it is just a matter of requiring it in the project's composer.json, exactly like you'd do with any other dependency. If the module isn't published on Packagist, you also need to declare where Composer can find it, typically as a vcs repository:
{
"require": {
"your-vendor/modulename": "**version**"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/your-vendor/modulename.git"
}
]
}
While developing a module locally, before it has its own remote repository, you can use a path repository instead, pointing directly at the module's folder on disk:
{
"require": {
"your-vendor/modulename": "*"
},
"repositories": [
{
"type": "path",
"url": "../path/to/ModuleFolder"
}
]
}
Running composer require (or composer update / composer install) downloads the package into vendor/ as usual. Monti will scan every installed package for an extra.monti-framework.providers declaration.
cache/providers.php is generated automatically and must never be edited by hand. Whenever you install, remove or update a module, run composer dump-autoload (or any other command that triggers Composer's autoload dump) to regenerate it.Setup a module
Some modules need one-off setup work before they can be used: creating database tables, publishing a default configuration file, and so on. This is what the (optional) setup() method of your controller is for.
<?php
namespace App\Modules\Reviews;
class Reviews extends \App\System\MVC\Controller {
public function setup() {
\DB::select(
"CREATE TABLE IF NOT EXISTS `reviews` (
id int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`evaluation` tinyint(1) NOT NULL,
`comment` text DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp()
);"
);
$this->copyResources("Reviews", []);
}
}
setup() isn't called automatically: run it once, manually, whenever you install or update a module that needs it (for example from a one-off script, or from your project's src/boot.php).
copyResources()
copyResources() (inherited from \App\System\MVC\Controller) is a helper you can use inside setup() to publish part of the module's resources into the project. Unlike routes and views (which the framework reads directly from the module's package, see the Bootstrap class section above), some resources make more sense copied into the project itself: for example, a default configuration file the project owner is expected to edit.
- namespace (required): identifies the destination sub-paths (
src/views/{namespace},locales/{namespace}, ...). By convention, use the module's own name; - options (required, can be an empty array
[]): an array with the following optional keys:- source (default: the module's own folder): absolute path to the folder the resources are copied from;
- only: one or more values among
views,locales,src,config.php,additional. When set, only the indicated resources are copied, otherwise everything available is copied; - additional: a list of
source path => destination pathpairs, with the destination relative tosrc/views/{namespace};
| Resource | Copied from | Copied to |
|---|---|---|
| views | {source}/views |
src/views/{namespace} |
| locales | {source}/locales |
locales/{namespace} |
| src | {source}/src |
public/src |
| config.php | {source}/config.php |
config/{namespace, lowercase}.php |
Once copied, a module's configuration file behaves like any other project configuration file, and can be read with the regular config() helper (e.g. config("reviews.some_key")). For more information, see the Config section.
Publishing a module on Packagist
Everything above works whether your module stays private, shared only through a path or vcs repository, or ends up on Packagist, the default registry Composer looks at. Publishing there is what lets any project require it with a plain composer require your-vendor/modulename, without adding a custom repositories entry.
Once your module's repository, composer.json included, is pushed to a public git host such as GitHub, submitting it to Packagist takes a few steps: create an account, use "Submit", and paste the repository's URL. Packagist reads its composer.json and lists it under the vendor/package name declared there. From then on, every git tag that looks like a version number, for example 1.0.0, becomes an installable version, and Packagist can pick up new tags automatically if you enable its GitHub webhook, or you can trigger an update manually from the package's page.