Skip to main content

Creating a Module

This guide builds a working module for the Flavor Starter Theme using Module System 2.0. A module is a single PHP class that extends Flavor\Modules\AbstractModule and is registered on the flavor_register_modules action.

Before you start

Read the Module System Overview first. The key idea: metadata lives on the class as static methods — there is no metadata array and no procedural module.php entry file.

Step 1 — Create the module directory

Put your module in a child theme (recommended) or a companion plugin:

flavor-starter-child/
└── modules/
└── sale-banner/
├── SaleBannerModule.php # filename MUST end in "Module.php"
└── assets/
└── sale-banner.css

Step 2 — Write the module class

Extend AbstractModule and implement the five required static methods. Register your hooks in the constructor (or in boot() — both run when the module is active). This example renders a dismissible banner above the site header and stores its message + colour as settings.

<?php
namespace MyVendor\FlavorModules\SaleBanner;

use Flavor\Modules\AbstractModule;

defined('ABSPATH') || exit;

class SaleBannerModule extends AbstractModule
{
// ── Required metadata ─────────────────────────────────────────
public static function id(): string { return 'sale-banner'; }
public static function version(): string { return '1.0.0'; } // strict semver X.Y.Z
public static function category(): string { return 'general'; }
public static function icon(): string { return 'megaphone'; } // dashicon slug

public static function name(): string
{
return __('Sale Banner', 'flavor');
}

public static function description(): string
{
return __('Shows a promotional banner above the header', 'flavor');
}

// ── This is a third-party module ──────────────────────────────
public static function external(): bool { return true; }

/** Single option that holds this module's settings (array). */
private string $settingsKey = 'module_sale_banner_settings';

// ── Register hooks (runs only when the module is active) ───────
public function __construct()
{
add_action('flavor_header_before', [$this, 'render']);
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
}

private function getSettings(): array
{
$saved = flavor_get_option($this->settingsKey, []);
return array_merge([
'message' => __('Summer sale — 20% off everything!', 'flavor'),
'bg_color' => '#ef4444',
], is_array($saved) ? $saved : []);
}

public function render(): void
{
$s = $this->getSettings();
printf(
'<div class="sale-banner" style="--sale-bg:%s">%s</div>',
esc_attr($s['bg_color']),
esc_html($s['message'])
);
}

public function enqueueAssets(): void
{
wp_enqueue_style(
'sale-banner',
get_stylesheet_directory_uri() . '/modules/sale-banner/assets/sale-banner.css',
[],
self::version()
);
}
}

Notes on this example:

  • external() returns true. Without it the ModuleManager will not track your on/off state, so the module can never be switched on. Every third-party module needs this.
  • Hooks are registered in the constructor. The manager only instantiates active, licensed modules, so the constructor is a safe place to wire hooks. boot() works identically if you prefer it.
  • flavor_header_before is a real theme action (others: flavor_header_start, flavor_header_end, flavor_header_after). See the Theme Hooks reference.
  • Settings are stored as one array option and read with flavor_get_option($key, []). See the Settings API.

Step 3 — Register the module

In your child theme's functions.php (or your plugin's bootstrap), hook flavor_register_modules and call register() with the class name — not an array. Because your class lives outside the theme's Flavor\Modules\ namespace, you must require_once its file yourself first:

add_action('flavor_register_modules', function ($manager) {
require_once __DIR__ . '/modules/sale-banner/SaleBannerModule.php';
$manager->register(\MyVendor\FlavorModules\SaleBanner\SaleBannerModule::class);
});

register() validates the class before adding it: it must exist, extend AbstractModule, use a strict X.Y.Z version, and pass the PHP / WordPress / requiresPlugin() compatibility gate. If any check fails the module is skipped (and, outside a product-update window, a _doing_it_wrong() notice is raised) — it will not fatal your site.

Step 4 — Enable and test

  1. Go to Appearance → Flavor Options → Modules.
  2. Your module appears in the list — switch it on.
  3. Reload the storefront and confirm the feature works.
  4. Open Flavor Modules (top-level admin menu) to see it in the active-modules Overview, and check the unified Debug viewer for any warnings (see Best Practices).

If your module does not appear or won't stay enabled, check the three most common causes:

SymptomCause
Module never appearsClass file not named *Module.php, or require_once missing before register().
Appears but can't be enabledexternal() doesn't return true.
Registered but never runsHost license invalid and requiresParentLicense() is true (the default).
Silently rejectedversion() isn't strict semver X.Y.Z, or a requires*() gate failed.

Required vs. optional methods (quick reference)

Required (abstract — must implement): id(), version(), category(), name(), description().

Commonly overridden: icon(), requiresPlugin(), external(), requiresParentLicense(), requiresPhp(), requiresWp(), plus the lifecycle methods boot(), activate(), deactivate(), uninstall() and the admin hook getAdminMenuConfig().

Full list + defaults: Module System Overview.

Learn from the built-in modules

The shipped modules are the canonical reference for real-world structure:

  • inc/modules/general/icons/IconsModule.php — a minimal General module: metadata, a constructor that registers a shortcode, and an admin page via getAdminMenuConfig().
  • inc/modules/eshops/stock-alerts/StockAlertsModule.php — a plugin-dependent module (requiresPlugin()true): a custom table created on init, AJAX handlers, front-end assets, an admin settings screen, and a settings array.

Copy their shape; adjust the metadata and hooks to your feature.