How to Develop a Custom JTL Shop Plugin (Step-by-Step)
A practical, production-ready guide to building a JTL Shop plugin β from project layout and manifests to hooks, admin settings, templates, and safe deployment.

Share this article
A practical, production-ready guide to building a JTL Shop plugin β from project layout and manifests to hooks, admin settings, templates, and safe deployment.

Share this article
Who is this for? JTL Shop developers who want a clean, repeatable pattern for building and deploying production-quality plugins.
In this guide youβll build a minimal but production-grade JTL Shop plugin. Weβll cover:
All examples are in PHP (JTLβs plugin language). Where JTL Shop versions differ, Iβll call it out.
A structure that works well in teams and CI:
my-jtl-plugin/
ββ src/
β ββ Plugin.php
β ββ Services/
β ββ Hooks/
β ββ Admin/
β ββ Templates/
ββ templates/ # Smarty templates (if separated)
ββ migrations/ # SQL migrations
ββ vendor/ # if using Composer
ββ plugin.json # JTL Shop 5 manifest (preferred)
ββ info.xml # JTL Shop 4 manifest (legacy)
ββ config.xml # Admin settings (if applicable)
ββ README.md
ββ composer.json
Use
plugin.jsonfor JTL Shop 5. Older installations may still useinfo.xmlβ include it only if you must support v4.
plugin.json)Hook IDs vary by JTL Shop version. Keep IDs in constants in your code and map to the manifest to avoid magic numbers. Always check the official hook list for your target version.
info.xml) β only if you must support it<?xml version="1.0" encoding="UTF-8"?>
<jtlshopplugin>
<Name>My JTL Plugin</Name>
<Author>Your Company</Author>
<Version>1.0.0</Version>
<MinShopVersion>4.0.0</MinShopVersion>
<Hooks>
<Hook>
<HookID>0</HookID>
<File>src/Hooks/OnPageLoaded.php</File>
</Hook>
</Hooks>
</jtlshopplugin>
hljs xmlsrc/Plugin.php)Keep lifecycle code small and testable.
Create a small, testable handler class per hook and map it in the manifest.
src/Hooks/OnPageLoaded.php β read/update Smarty variables safely.
<?php
declare(strict_types=1);
namespace MyJtlPlugin\Hooks;
final class OnPageLoaded
{
public function __invoke(array $args): void
{
// $args typically contains Smarty instance and page context
$smarty = $args['smarty'] ?? null;
if ($smarty instanceof \Smarty) {
$smarty->assign('MY_PLUGIN_GREETING', 'Hello from My JTL Plugin!');
}
}
}
hljs phpsrc/Hooks/BeforeCheckout.php β example of pre-checkout validation.
<?php
declare(strict_types=1);
namespace MyJtlPlugin\Hooks;
final class BeforeCheckout
{
public function __invoke(array $args): void
{
// Inspect cart or session state. This is a conceptual example.
$cart = $args['cart'] ?? null;
if ($cart && $this->hasDisallowedItems($cart)) {
// Attach a user-friendly message via Smarty or throw a controlled exception
$smarty = $args['smarty'] ?? null;
if ($smarty instanceof \Smarty) {
$smarty->assign('MY_PLUGIN_CHECKOUT_WARNING', 'Certain items require manual review.');
}
}
}
private function hasDisallowedItems($cart): bool
{
// TODO: implement your business logic
return false;
}
}
hljs phpKeep handlers idempotent and quickly return when not applicable. That reduces performance overhead and avoids side effects on repeated hook triggers.
config.xml)Use a simple config to toggle features and store values (e.g., API keys).
<?xml version="1.0" encoding="utf-8"?>
<config>
<section key="my_jtl_plugin" label="My JTL Plugin">
<setting key="enabled" type="selectbox" label="Enable Plugin">
<option value="0">Disabled</option>
<option value="1" selected="selected">Enabled</option>
</setting>
<setting key="greeting_text" type="text" label="Greeting Text" default="Hello from My JTL Plugin!" />
</section>
</config>
hljs xmlRead settings in your hook (adapt to your shopβs config API):
// Pseudocode: replace with the config accessor available in your JTL Shop version.
// e.g., use Shop settings repository/DB rather than raw $_ENV in production.
$enabled = (int)($_ENV['MY_JTL_PLUGIN_ENABLED'] ?? 1);
$greeting = $_ENV['MY_JTL_PLUGIN_GREETING'] ?? 'Hello from My JTL Plugin!';
if ($enabled) {
// use $greeting in your Smarty assignment
}
hljs phpWire your config keys through the shopβs official configuration interface for your version. Avoid accessing
$_ENVdirectly in production; use the platformβs config loader or DI.
Add a small widget or banner into a template:
templates/snippets/my_plugin_banner.tpl
{if $MY_PLUGIN_GREETING} <div class="alert alert-info my-plugin-banner"> {$MY_PLUGIN_GREETING|escape} </div> {/if}hljs smarty
Include where appropriate (e.g., in a layout or specific page):
{include file="snippets/my_plugin_banner.tpl"}hljs smarty
If you present warnings on checkout:
templates/snippets/my_plugin_checkout_warning.tpl
{if $MY_PLUGIN_CHECKOUT_WARNING} <div class="alert alert-warning my-plugin-warning"> {$MY_PLUGIN_CHECKOUT_WARNING|escape} </div> {/if}hljs smarty
Keep migrations small and reversible when possible. Example migration to add a feature flags table.
migrations/001_create_flags.sql
CREATE TABLE IF NOT EXISTS my_plugin_flags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
hljs sqlAdd a follow-up migration when you need new columns rather than modifying existing files:
migrations/002_add_description.sql
ALTER TABLE my_plugin_flags
ADD COLUMN description VARCHAR(255) NULL AFTER name;
hljs sqlStore migrations as individual files with increasing sequence numbers. This lets you run only the required subset during updates.
MAJOR.MINOR.PATCH.minShopVersion current in plugin.json.Example CHANGELOG.md entry:
## 1.1.0 - 2025-06-10
- Added checkout warning banner (configurable)
- Introduced feature flags table (migration 001)
- Tested with JTL Shop 5.2.x
Use a sample database dump (sanitized) on staging to surface real-world edge cases early.
Example packaging script snippet (bash):
#!/usr/bin/env bash
set -euo pipefail
PLUGIN_NAME="my-jtl-plugin"
OUT="dist/${PLUGIN_NAME}.zip"
rm -f "$OUT"
mkdir -p dist
zip -r "$OUT" src/ templates/ migrations/ plugin.json info.xml config.xml README.md -x "**/.DS_Store" "**/node_modules/**" "**/vendor/**"
hljs bashβ‘οΈ One-click deploy to JTL Shop with iDeployed. Use iDeployed to spin up isolated staging shops, test your plugin with production-like data, and deploy with confidence. Start your free trial β
{
"name": "my-jtl-plugin",
"displayName": "My JTL Plugin",
"version": "1.0.0",
"author": "Your Company",
"url": "https://ideployed.com",
"minShopVersion": "5.0.0",
"description": "Adds feature X to JTL Shop.",
"hooks": [
{ "id": 0, "file": "src/Hooks/OnPageLoaded.php" },
{ "id": 99, "file": "src/Hooks/BeforeCheckout.php" }
],
"adminMenu": [
{ "name": "My Plugin", "url": "plugin.php?plugin=my-jtl-plugin", "rights": ["VIEW"] }
],
"install": "src/Plugin.php",
"uninstall": "src/Plugin.php",
"update": "src/Plugin.php"
}
hljs json<?php
declare(strict_types=1);
namespace MyJtlPlugin;
final class Plugin
{
/** Called on install */
public static function install(): void
{
// run migrations, seed defaults
self::runMigrations(__DIR__ . '/../migrations');
}
/** Called on update */
public static function update(string $fromVersion, string $toVersion): void
{
// conditional migrations by version
self::runMigrations(__DIR__ . '/../migrations');
}
/** Called on uninstall */
public static function uninstall(): void
{
// soft-clean: keep data unless user opts out
}
private static function runMigrations(string $dir): void
{
foreach (glob($dir . '/*.sql') as $file) {
$sql = file_get_contents($file);
if ($sql !== false && trim($sql) !== '') {
// Use your shop DB connection here
self::db()->exec($sql);
}
}
}
private static function db(): \PDO
{
// Obtain PDO from JTL Shopβs DB layer or your DI container
// Example only:
return new \PDO('mysql:host=localhost;dbname=shop','user','pass', [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
]);
}
}
hljs php