Module Development
The Flavor Starter Theme ships a class-based module system ("Module System 2.0"). A module is a
self-contained feature — a shortcode, an admin tool, a storefront enhancement — packaged as a single
PHP class that extends Flavor\Modules\AbstractModule. The class carries all of its own metadata
(id, version, category, name, description) as static methods, and the theme's ModuleManager asks
the class for that metadata directly. There is no metadata array and no procedural entry file.
This is the extension point the built-in modules are built on. Ahead of public launch it is documented as an advanced / first-party API: it is stable enough to build against, but treat it as "how our module system works" rather than a frozen third-party contract with long-term stability guarantees.
Anatomy of a module
Each module lives in its own directory under flavor-starter/inc/modules/{category}/{id}/:
inc/modules/eshops/stock-alerts/
├── StockAlertsModule.php # The module class (required) — filename MUST end in "Module.php"
└── assets/ # CSS/JS (optional)
├─ ─ stock-alerts.css
└── stock-alerts.js
Rules that the loader relies on:
- One class per module, extending
AbstractModule. - The class file must end in
Module.php— the theme has no PSR-4 autoloader, so the ModuleManager discovers first-party module files by scanninginc/modules/for*Module.phpand mapping each short class name to its path. A file namedstock-alerts.phpwill not be found. - Namespace follows the folder:
Flavor\Modules\Eshops\StockAlerts. - Everything else (settings, templates, assets) is up to the module.
The AbstractModule contract
Source: flavor-starter/inc/modules/AbstractModule.php.
Required — every module must implement these
| Method | Returns | Purpose |
|---|---|---|
id() | string | Unique stable slug, kebab-case (e.g. stock-alerts). Used as the option-key suffix and DB reference. |
version() | string | Strict semver X.Y.Z only (validated at registration — 1.2, v1.2.3, 1.2.3-beta are rejected). |
category() | string | general, eshops, marketing, tools, dev — or a custom slug. |
name() | string | Human-readable name. Uses __('…', 'flavor') (see i18n note below). |
description() | string | Short description. Uses __('…', 'flavor'). |
All five are abstract public static — a module that omits any of them will not compile.
Optional — override only if you need to (sensible defaults shown)
| Method | Default | Purpose |
|---|---|---|
icon() | 'admin-generic' | Dashicon slug for the admin UI. |
requiresPlugin() | false | true if the module needs the WP eCommerce Core plugin active. Enforced at registration. |
requiresPhp() | '8.2' | Minimum PHP version — checked by the compatibility gate. |
requiresWp() | '6.0' | Minimum WordPress version — checked by the compatibility gate. |
author() | 'FlavorTeam' | Author name. |
authorUri() | 'https://flavorteam.dev' | Author URL. |
external() | false | Set to true for any non-built-in module (see "Built-in vs. external"). Affects the UI badge and activation-state tracking. |
textdomain() | 'flavor' | Textdomain used in name() / description(). |
requiresParentLicense() | true | Module is skipped at boot unless the host theme/plugin license is valid. Built-in core modules override to false. |
There are also several marketplace-ready methods (homepage(), supportUri(), license(),
licenseUri(), minTier(), updateSource()) that exist so the metadata schema stays forward-compatible.
They have no behavioural effect for built-in modules today — take the defaults unless you have a
specific reason not to.
Lifecycle — instance methods you may override
Called by the ModuleManager on an active module instance (all default to no-ops):
| Method | When it runs |
|---|---|
boot() | The module is active, passed the compatibility gate, and the license is valid. Register hooks, filters and shortcodes here. |
activate() | The user enables the module. One-time setup — create tables, seed defaults, register cron. |
deactivate() | The user disables the module. Unregister cron, clear transients; preserve settings for re-enable. |
uninstall() | Full uninstall. Delete settings, drop tables, purge state. |
boot()The ModuleManager instantiates a module (new YourModule()) and then calls boot() — both run at the
same point, and only for a module that is active and licensed. The built-in modules currently register
their hooks in the constructor (see IconsModule, StockAlertsModule); registering in boot() is
equally valid and matches the documented contract. Pick one and be consistent. Do not register hooks
in a static method — you only have an instance inside the constructor / boot().
Admin page (optional)
Override getAdminMenuConfig() to add a settings screen under the Flavor Modules menu. See
Module Settings API for the exact array shape (note: the render key
is callback).
What the ModuleManager does
Source: flavor-starter/inc/modules/ModuleManager.php. It is a singleton, booted on
after_setup_theme (priority 20), and it owns the full lifecycle:
- Load active state — reads each module's
module_{id}option to know which modules are switched on. - Register built-in modules, then fire the
flavor_register_modulesaction so child themes and plugins can register their own. - Validate + gate each registration — the class must exist, extend
AbstractModule, and pass strict semver + a PHP/WP/requiresPlugin()compatibility check. Failures are recorded (surfaced in the admin UI as "incompatible") rather than fatal. - Initialise active modules — for each switched-on module that passes the parent-license gate:
require the class file,
newit (constructor runs), then callboot().
Metadata (name, version, icon, …) is read from the static methods, so the manager can list a module in the admin UI without instantiating it. A module is only instantiated when it is active.
Built-in vs. external modules
Built-in modules ship in inc/modules/ and are registered by the ModuleManager itself. Their
active-state option keys are declared in the manager's built-in list.
External modules (yours) are registered on the flavor_register_modules action from a child theme or
companion plugin. Two things matter for an external module to work end-to-end:
external()must returntrue. This is what makes the ModuleManager auto-track your module's activation state (module_{id}option, hyphens → underscores) so it can be switched on/off.- You must
require_onceyour class file yourself. The manager's auto-loader only scans the theme's owninc/modules/tree — it will not find a class outside theFlavor\Modules\namespace. Require the file before you callregister().
See Creating a Module for the full worked example.
Module lifecycle at a glance
Register (flavor_register_modules)
└─ validate: class_exists → subclass_of AbstractModule → strict semver → compatibility gate
└─ recorded in the registry (metadata read from static methods)
└─ active? (module_{id} option ON, license tier allows it)
└─ parent-license valid? (requiresParentLicense)
└─ require file → new YourModule() → boot()
└─ your hooks/shortcodes are live
Disabling a module simply flips its module_{id} option off — the class is never instantiated on the
next load, so its hooks never register.
i18n note
name() and description() use WordPress __('…', 'flavor'), matching the AbstractModule contract and
every built-in module. For runtime front-end strings inside your module logic, the theme also exposes
its own translation layer, flavor_t() / flavor_te() — see the Translation docs. Keep module metadata
on __() so it resolves the same way the manager and admin UI expect.
Next steps
- Creating a Module — the step-by-step build.
- Module Settings API — storing settings + the admin screen.
- Best Practices — conventions, security, performance, gotchas.