Reference
PHP extension points
Libraries, variables, template-local logic and the resource data API.
Custom Shortcode Libraries
Custom shortcode libraries live as single PHP files in ext/lib/:
ext/lib/prepare-events.php
The older directory format is still supported for compatibility:
ext/lib/prepare-events/prepare-events.php
Use the directory format only when the library needs support files that belong beside the entrypoint.
The function must be named <name>_sc($params) with hyphens converted to underscores:
<?php
function prepare_events_sc($params)
{
// Read a variable set by [#data#] or [#set#]
$events = get_variable('data.event');
$result = [];
$today = date('Y-m-d');
foreach ($events as $event) {
if ($event['date'] >= $today) {
$result[] = $event;
}
}
// Write back - [#repeat data.event#] will now only see future events
set_variable('data.event', $result);
}
Called in templates exactly like any other shortcode:
[#data event filter=published:1 sort=date|string|asc#]
[#prepare-events#]
[#repeat data.event#]
PHP variable API
| Function | Description |
|---|---|
get_variable('data.event') |
Reads any template variable by dot-notation path |
set_variable('data.event', $value) |
Writes a variable at the given path |
set_variable_dot('record', $array) |
Writes an associative array as a dot-notation variable (record.field, record.uuid, etc.) |
Libraries are for business logic, not simple value lookup
Use a library only when the work involves real business logic or data preparation that cannot be expressed cleanly with existing shortcodes. Simple lookup, conditionals, date formatting, and direct field output belong in templates using [#get#], [#if#], [#fmt#], [#date#], [#data#], and similar shortcodes.
Before creating anything in ext/lib/, answer these questions in order:
- Can core already do this? Prefer existing Nimbly shortcodes, core libraries, resource configuration,
[#data#],[#repeat#],[#if#],[#fmt#], and local templates. For comma-separated UUID reference fields, load the target resource once and[#repeat ... csv#]over the UUIDs before considering PHP. Do not create a project library because you missed an existing core facility. - Is backend PHP the right place? If the work is filtering, searching, sorting, toggling UI state, expanding/collapsing content, map/list synchronization, or other browser interaction, prefer Alpine.js with
[#fmt var=... json#]. Backend PHP should prepare canonical server data, not manage client interaction state. - Can the data model or template structure solve it? If the code only works around awkward data shape, consider whether the resource field, route template, or reusable
.tplcomponent should change instead. - If a library is still needed, is it minimal? A justified library should do the smallest server-only job: normalize input, read records, compute values, and set template variables. It must not own markup, styling, layout, UI state, or copy that belongs in templates/content.
Do not create a library just to copy fields into new variables, rename values for display, or wrap a template call. That adds indirection without behavior.
Keep responsibilities narrow. A library named for one concept must not quietly take ownership of unrelated concerns such as rendering decisions, payment links, session banners, analytics, or layout state. Split those concerns into templates, existing shortcodes, configuration data, or separate purpose-built libraries.
When a library is justified, it should prepare data and set template variables. HTML belongs exclusively in .tpl files - never in PHP strings returned from a library function.
Prefer returning a small value when the logic is just classification:
// correct
function membership_status_sc($_params)
{
$expires = get_variable('user.membership_expires');
if ($expires === '2037-12-31') {
return 'lifetime';
}
if ($expires <= date('Y-m-d')) {
return 'expired';
}
return 'active';
}
[#member-status-[#membership-status#]#]
Use prepare-then-render when logic needs to compute multiple values for a template:
function prepare_membership_status_sc($_params)
{
load_library('env');
$expires = get_variable('user.membership_expires');
$link_annual = env('STRIPE_LINK_ANNUAL');
set_variable('member-expires-date', date('M j, Y', strtotime($expires)));
set_variable('member-link-annual', htmlspecialchars($link_annual));
}
[#prepare-membership-status#]
[#membership-status#]
// wrong - never do this
function membership_status_switch_sc($_params)
{
return '<section class="mb-10"><div class="bg-cnormal ...">Active Member</div></section>';
}
If a library must render a template, use run_buffered($path_to_tpl_file). The path is a filesystem path; anchor it with dirname(__FILE__). Templates for a module live in ext/modules/<name>/tpl/.
Template-local libraries (.inc)
Not every library is a reusable building block. Some PHP files exist only to serve a single template - bootstrap logic, page-specific data preparation, or one-off configuration that does not belong in the global ext/lib/ namespace.
Place these as (shortcode-name).inc files alongside the template that uses them:
core/tpl/html/
init.tpl
create-settings.inc ← loaded only when called from init.tpl
find_library() checks the calling template's directory for a matching .inc before falling back to ext/lib/ or core/lib/. Because the lookup walks the shortcode stack, it resolves to the innermost calling template's directory - so [#create-settings#] called from core/tpl/html/init.tpl finds core/tpl/html/create-settings.inc, not a global lib.
The .inc extension signals "private / internal". These files are invisible to templates in other directories.
Rules:
- Use
.incfor logic that is only meaningful in the context of one template and would not make sense reused elsewhere. - The function name still follows the
<name>_sc($params)convention. - Do not name a
.incfile after a global lib (data.inc,url-key.inc) - the local file would silently shadow the global one within that template scope. routeis a forbidden shortcode name -route.incbelongs exclusively to the routing system and cannot be repurposed as a library.
PHP data API (available in .install.inc and route.inc)
| Function | Description |
|---|---|
data_create_resource($name, $meta) |
Creates a new resource with the given .meta definition |
data_create($resource, $uuid, $data) |
Creates a new record |
data_read($resource, $uuid) |
Reads a single record by UUID |
data_exists($resource, $uuid) |
Returns true if a record exists |
load_library($name) |
Loads a shortcode library so its PHP functions are available |