Extensions

Capabilities

Latch extensions are capable of providing any or all of the following behaviors:

  1. Loading stylesheets. Latch autoloads css/style.css for each active extension when the file exists.
  2. Overriding core scripts and templates. Most files are loaded through Latch\File\ext_part(), which checks active extensions before core files. Files in app contain CMS initialization logic and are not extension override targets.
  3. Event handlers. Extensions can run PHP code for any Latch event and can define custom event names for their own code.
  4. Custom admin forms that write data to the latch_extensions table.
  5. Custom shortcodes from extensions/my_extension/lib/shortcodes.
  6. Custom post templates, page templates, form templates, datatype handlers, cachers, and other files resolved through ext_part().

Metadata

Every extension must have an extension.json file in the root of the extension directory, for example extensions/my_extension/extension.json.

Here is an example with the common supported keys:

{
    "name": "Backups",
    "description": "Schedule backups of your Latch files and database.",
    "author": "Sparknight",
    "url": "https://sparknight.io",
    "version": "0.0.1",
    "icon": "latch-icon ph-hard-drive",
    "show_in_sidebar": true
}

A few points to keep in mind:

  • name is the friendly value shown in the dashboard. The extension directory is the unique identifier used by the filesystem.
  • icon is optional visual metadata and does not control whether the extension appears in the Admin Dashboard sidebar.
  • Set show_in_sidebar to true to add an active extension to the Extensions sidebar submenu. The link is rendered without an icon.
  • version should follow Semantic Versioning.

Database Persistence

The latch_extensions table is the registry and persistence boundary for installed extensions. Each extension directory has one row, identified by its unique name; Latch stores its friendly name, enabled state, load priority, and extension-owned JSON in the metadata column.

An extension settings form with database-action set to extension writes its submitted values into that metadata object. Extension code can use the same object for internal state by reserving keys that begin with an underscore, such as _queue; ordinary form saves preserve those internal keys.

Do not create or alter application tables as part of an extension installation routine. If the metadata object cannot reasonably support an extension's data model or workload, document the limitation and coordinate the database design with the siteowner before introducing schema changes.

Importing

Administrators can import an extension from the Extensions page of the Admin Dashboard. The ZIP may contain the extension files directly or wrap them in one top-level directory. In either layout, extension.json must be at the extension root.

Latch validates archive paths, symbolic links, file counts, extracted size, required name and description metadata, Semantic Versioning, URLs, and directory/name collisions before moving the extension into place. An imported extension remains disabled until an administrator explicitly enables and saves it.

Extensions can execute server-side code and should only be imported from trusted sources. Validation confirms that a package is structurally safe to extract; it does not establish that the extension's code is trustworthy.

Priority

Extensions are loaded in the order they appear in the active extension list.

For file overrides, the first active extension with a matching file takes precedence. Other matches further down the extension list are ignored.

For events, multiple extensions can hook into the same event. The earlier extension in the active extension list runs first.

Managed Apache Configuration

Extensions that need root .htaccess directives should use Latch\Util\upsert_htaccess_block() and Latch\Util\remove_htaccess_block() with a stable, extension-specific block name. The helpers preserve line endings, serialize read-modify-write operations with a file lock, replace existing named blocks idempotently, and leave unrelated directives untouched.

Listen for extensions_update when configuration should be removed on disable. The event runs through the previous registry before extension handlers are unloaded.

Events

Event files live at:

extensions/my_extension/lib/events/event_name.php

The event class must use this namespace and class pattern:

<?php

namespace Latch\Ext\MyExtension\Events;

class EventName
{
    public static function event($args)
    {
        // Your processing here.
    }
}

For example, to add content to the bottom of a Latch form, create extensions/my_extension/lib/events/form_after.php:

<?php

namespace Latch\Ext\MyExtension\Events;

class FormAfter
{
    public static function event($args)
    {
        return <<<HTML
            <button type="button">My Custom Button</button>
        HTML;
    }
}

The $args variable is a named array provided by $_SESSION["latch"]->events->trigger().

Initialization Event

The init event is triggered immediately after extension event files are loaded. It is a good place to include extension libraries, run installation routines, or initialize namespaced services.

Example init.php:

<?php

namespace Latch\Ext\MyExtension\Events;

class Init
{
    public static function event($args)
    {
        include_once __DIR__ . "/../my-extension.php";
    }
}

If your extension exposes reusable code, put it in a namespaced file outside the event handler, such as extensions/my_extension/lib/my-extension.php:

<?php

namespace Latch\Ext\MyExtension;

function get_log()
{
    return \Latch\Util\new_log("ext.my_extension");
}

Cron Event

A cron job is a task that runs automatically at a specific time or interval. Latch relies on a single cron job that fires every minute. It calls lib/hooks/cron.php, which triggers the cron event.

An extension can add scheduled behavior by creating:

extensions/my_extension/lib/events/cron.php

The handler should decide for itself whether work is due. For example, the Backups extension checks configured backup jobs and only runs a job when its schedule has elapsed.