How to Develop a Custom JTL Shop Plugin (Step-by-Step)
Who is this for? JTL Shop developers who want a clean, repeatable pattern for building and deploying production-quality plugins.
Overview
In this guide you’ll build a minimal but production-grade JTL Shop plugin. We’ll cover:
- Project layout that scales
- Manifests for JTL Shop 4/5
- Registering hooks (with safe patterns)
- Admin settings and validation
- Template integration (Smarty)
- Database migrations
- Versioning and deployment tips
All examples are in PHP (JTL’s plugin language). Where JTL Shop versions differ, I’ll call it out.
Prerequisites
- PHP 7.4+ (check your JTL Shop version requirements)
- Access to a JTL Shop dev/stage environment
- Composer (optional but recommended for autoloading/utilities)
- Familiarity with Smarty templates
Folder Structure
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.
Manifest (JTL Shop 5: plugin.json)
{
"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 jsonHook 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.
Legacy (JTL Shop 4: 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 xmlBootstrap (src/Plugin.php)
Keep lifecycle code small and testable.
<?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 phpRegistering Hooks (Examples)
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.
Admin Settings (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.
Template Integration (Smarty)
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
Database Migrations
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.
Versioning & Compatibility
- Use SemVer for your plugin:
MAJOR.MINOR.PATCH. - Declare and keep
minShopVersioncurrent inplugin.json. - Maintain a CHANGELOG.md with user-facing entries.
- If you support multiple JTL Shop major versions, isolate compatibility shims behind interfaces.
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
Testing Checklist
- ✅ Hooks fire only once per request (idempotent)
- ✅ Admin settings save/validate correctly
- ✅ No Smarty variable leaks or name collisions
- ✅ Works on staging with realistic data
- ✅ Rollback path (uninstall won’t drop user data unless confirmed)
- ✅ Performance acceptable under production-like load
Use a sample database dump (sanitized) on staging to surface real-world edge cases early.
Deployment Tips
- Package only what you need: source, manifests, templates, migrations
- Keep secrets out of the repo; load via environment or shop config
- Use a staging shop that mirrors production
- Automate rollout
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 →