Writing a plugin
On this page
A plugin is a folder plugins/<code>/ (code: lower-case letters, digits, dashes) with two required files:
plugins/<code>/plugin.json who and what (read without running code)
plugins/<code>/plugin.php returns an object extending AbstractPlugin
plugins/<code>/pages/*.php admin pages (optional)
plugins/<code>/views/*.php HTML pieces the plugin draws (optional)
plugins/<code>/assets/* CSS / JS / pictures, served as /plugins/<code>/assets/...
plugin.json
{
"code": "pokes", "name": "Pokes", "version": "1.0.0",
"description": "Poke a friend to say hi.", "icon": "zap", "category": "Social",
"author": "MD OMAR FARUK", "author_url": "https://www.faruk.bd",
"requires": "1.0.0", "php": "8.1", "extensions": [],
"tier": "free", "addon": "pokes"
}
tier is free or premium. A premium plugin boots only when the licence from license.faruk.bd carries the addon code (addon, default = the plugin code). icon is a name from includes/icons.php.
plugin.php
<?php
defined('MMB_VERSION') or exit;
class PokesPlugin extends AbstractPlugin
{
public function getCode(): string { return 'pokes'; }
public function install(): void // runs on every activation: make it safe to run twice
{
$this->sql("CREATE TABLE IF NOT EXISTS mmb_pokes_log (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
$this->defaults(['daily_limit' => 20]); // config defaults (stored as JSON in mmb_plugins.config)
}
public function boot(): void // every request while active: register everything here, print nothing
{
$this->route('GET', '~^/pokes$~', 'pagePokes', 'member'); // front page -> $this->pagePokes()
add_filter('api_action', [$this, 'api'], 10); // POST /api/pokes/send
add_action('profile_buttons', [$this, 'profileButton']); // print a button on profiles
add_filter('notification_text', [$this, 'notifText'], 10); // words for our notification type
add_filter('cron_jobs', fn ($jobs) => $jobs + ['pokes-clean' => ['label' => 'Clean old pokes', 'every' => 86400, 'run' => fn () => '...']]);
}
public function adminMenu(): array // rows under "Plugin pages" in the admin rail: /admincp/plugin/pokes[/<page>]
{
return [['label' => 'Pokes', 'page' => '', 'icon' => 'zap']]; // served by pages/index.php
}
}
return new PokesPlugin();
Config: $this->get('key', $default), $this->on('key'), $this->set('key', $v), $this->setMany([...]).
Paths: $this->dir(), $this->assetUrl('x.css'), $this->pageUrl('settings'), $this->view('name', [...vars]) (views/name.php; a theme can override it at themes/<t>/plugins/<code>/name.php).
What a plugin can hook into
Routes $this->route($methods, $regex, $method, $access) / add_route(...). Regex groups become method arguments. Access: public, member, moderator, admin. POST routes are CSRF-checked by the router. Core routes win on a clash.
JSON add_filter('api_action', fn($answer, $action, $method, $rest) => ...): when $action is yours ('pokes/send'), return an array (it is sent as JSON with ok:true), call json_fail('why'), or return true after printing your own output. Everything else: return $answer. Reads that may use GET: add_filter('api_reads', fn($list) => $list + ['pokes/list']). Inputs: api_str(), api_int(), api_arr(), api_require_login().
HTML slots (actions; print HTML): theme_head_early, theme_head, theme_foot, body_top, body_bottom, home_top, feed_top($user,$mode), feed_between($index,$post), sidebar_left, sidebar_right_top, sidebar_right, side_menu_after, composer_buttons, post_head_extra, post_head_actions, post_body_before, post_body_after, post_actions, profile_buttons($profile,$me), profile_menu, profile_intro, profile_side, profile_feed_top, profile_about_rows, friend_buttons, login_top, login_extra, register_fields, register_extra, settings_*_fields, settings_security_cards, videos_title_actions, welcome_hero, admin_head, admin_foot.
Menus (filters on arrays): site_menu, bottom_menu, create_menu, account_menu, footer_links, top_nav, top_right (buttons with a data-count badge key), settings_tabs + settings_tab_html($html,$tab,$user,$vars) + settings_tab_save, profile_tabs + profile_tab_html, feed_tabs, search_tabs + search_sections + search_suggest, admin_sections, admin_settings_tabs + admin_settings_tab_html + admin_settings_tab_save, admin_dashboard_cards, admin_notices.
Posts and feeds: post_row (change or refuse with ['_error' => ...]), post_update_row, post_kinds, post_can_view($null,$post,$viewer), post_can_edit, post_can_delete, post_menu, post_context, post_backgrounds, feelings_list, feed_sources($sqlParts,$viewer,$mode) (add "p.page_id IN (...)" to pull your posts into the home feed), feed_where, feed_posts, feed_api($null,$get,$before,$limit) (answer /api/post/feed for your own feeds), video_player_html($html,$media,$post), audio_player_html, post_embed_html, text_render, media_url, avatar_url, media_row, upload_allowed, upload_max_mb, allowed_extensions.
Events (actions): mmb_ready, plugins_booted, user_registered, user_logged_in, user_logged_out, user_verified_email, user_deleting, user_deleted, user_blocked, post_created, post_updated, post_deleting, post_deleted, post_published, post_viewed, comment_created, comment_deleted, reaction_changed, friend_requested, friend_accepted, friend_removed, followed, notification_created($id,$userId,$type,$actorId), media_stored, media_deleted, avatar_changed, cover_changed, password_changed, report_created, verification_requested, contact_sent, plugin_activated, plugin_deactivated, theme_activated, license_activated, route_matched, posts_prepared.
Other filters: notification_text($null,$row,$whoHtml), notification_pref_key, notify_rows, privacy_rows, privacy_save, notify_prefs_save, profile_details_save, reaction_types, reaction_target_check, report_reasons, report_target_types, report_target_info, search_*, sitemap_urls, manifest, service_worker_js, js_config (values for window.MMB.cfg), icons, mail_templates, mail_html, cron_jobs, poll_counts($counts,$uid) (badge numbers the browser polls every 30 s), poll_answer, lang_strings, license_addons, two_factor_check, two_factor_hint, auth_login_check, register_check, user_register_row, user_export, contact_url($url,$friend), routes.
Helpers to use: db(), get_setting()/set_setting(), current_user()/user_id()/is_admin(), user_by_id(), display_name(), avatar_url(), user_url(), post_get(), posts_by_ids(), posts_prepare(), part_html('post', ['post' => $p]), render('page', $vars, 'main'|'auth') (pages come from templates/ or themes/<t>/pages/; a plugin passes its own HTML through render('static', ['title'=>..., 'body'=>$html]) or draws a full page with part('topbar') etc.), page_title(), page_meta(), add_css(), add_js(), add_head(), notify(), mail_template_send(), media_store(), icon(), e(), __(), clean_text(), time_ago(), cache_remember(), rate_limited(), admin_head()/admin_foot()/admin_field()/admin_pager()/admin_status_pill()/admin_log() (in admin pages).
Rules: tables start with mmb_<code>_; never call a function that lives in another plugin (put shared things in core or check plugin_active() and degrade); escape everything with e(); every text in __(); CSS classes from assets/css/app.css (cards, buttons, forms) so themes style you; keep boot() cheap (no queries unless needed); JavaScript goes in assets/ and is added with add_js($this->assetUrl('x.js')) only on the pages that need it; the whole plugin must work on a phone.
Front-end JS (window.MMB): MMB.api(action, data) (POST JSON with CSRF), MMB.get(action, params), MMB.upload(file, as, onProgress), MMB.toast(msg, 'ok'|'bad'), MMB.modal({title, html, foot, size}), MMB.confirm(text), MMB.icon(name), MMB.esc(), MMB.on('poll', fn) (every poll answer; add your counts through the poll_counts filter and show them with data-count="<key>" badges), MMB.on('html', fn(node)) (new HTML was inserted), MMB.composer.open({...}), MMB.feed(box), MMB.mentions(textarea), MMB.autosize(textarea), MMB.emit/MMB.on for your own events, data-act="<name>" clicks reach MMB.on('act:<name>', fn(el, event)).