How to Develop a Custom JTL Shop Plugin (Step-by-Step)

5 min read

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.json for JTL Shop 5. Older installations may still use info.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 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.

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 xml

Bootstrap (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 php

Registering 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 php

src/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 php

Keep 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 xml

Read 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 php

Wire your config keys through the shop’s official configuration interface for your version. Avoid accessing $_ENV directly 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 sql

Add 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 sql

Store 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 minShopVersion current in plugin.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 →

Share this article

SOC 2 Type II Certified
GDPR Compliant
HIPAA Ready

All communications are encrypted with AES-256 and protected by our zero-trust security framework. We never share your data with third parties.