Back to SDKs
PHP

PHP SDK Guide

A complete, runnable example for integrating Zywrap's V1 offline data (including dynamic schemas) into your PHP application.

Step 1: Download Your Data Bundle

Download the V1 ZIP file containing the highly compressed Tabular JSON data. Unzip the zywrap-data.json file from the bundle to use in the import script.

You must be logged in to download the SDK data bundle.

Step 2: Database Setup

Run this SQL. It now features the relational use_cases table containing the dynamic schema_data.

sql

-- Database Schema for Zywrap Offline SDK V1 (MySQL/MariaDB)

CREATE TABLE `ai_models` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` int(11) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `categories` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` int(11) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `languages` (
  `code` varchar(10) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` int(11) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `use_cases` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text,
  `category_code` varchar(255) DEFAULT NULL,
  `schema_data` json DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` bigint DEFAULT NULL,
  PRIMARY KEY (`code`),
  KEY `category_code` (`category_code`),
  CONSTRAINT `use_cases_ibfk_1` FOREIGN KEY (`category_code`) REFERENCES `categories` (`code`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `wrappers` (
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text,
  `use_case_code` varchar(255) DEFAULT NULL,
  `featured` tinyint(1) DEFAULT NULL,
  `base` tinyint(1) DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1,
  `ordering` bigint DEFAULT NULL,
  PRIMARY KEY (`code`),
  KEY `use_case_code` (`use_case_code`),
  CONSTRAINT `wrappers_ibfk_1` FOREIGN KEY (`use_case_code`) REFERENCES `use_cases` (`code`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `block_templates` (
  `type` varchar(50) NOT NULL,
  `code` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `status` tinyint(1) DEFAULT 1,
  PRIMARY KEY (`type`,`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `settings` (
  `setting_key` VARCHAR(255) NOT NULL,
  `setting_value` TEXT,
  PRIMARY KEY (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `usage_logs` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `trace_id` varchar(255) DEFAULT NULL,
  `wrapper_code` varchar(255) DEFAULT NULL,
  `model_code` varchar(255) DEFAULT NULL,
  `prompt_tokens` int(11) DEFAULT 0,
  `completion_tokens` int(11) DEFAULT 0,
  `total_tokens` int(11) DEFAULT 0,
  `credits_used` bigint DEFAULT 0,
  `latency_ms` int(11) DEFAULT 0,
  `status` varchar(50) DEFAULT 'success',
  `error_message` text,
  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `wrapper_idx` (`wrapper_code`),
  KEY `model_idx` (`model_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Step 3: Database Connection

Save this as db.php. It will be used by all other scripts.

php

<?php
// FILE: db.php
// Replace with your actual database credentials
$host = 'localhost';
$db   = 'zywrap_db';
$user = 'root';
$pass = 'password';
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
     $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
     throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
?>

Step 4: Import Tabular Data

This script uses a custom extractTabular() function to instantly parse and import the compressed V1 JSON.

php

<?php
// FILE: import.php
/**
 * Zywrap V1 SDK - Tabular Data Importer
 * USAGE: php import.php
 */
ini_set('max_execution_time', '300');
ini_set('memory_limit', '512M');
require_once 'db.php';

$jsonFile = 'zywrap-data.json';
if (!file_exists($jsonFile)) die("Error: Could not find 'zywrap-data.json'.\n");

$data = json_decode(file_get_contents($jsonFile), true);
if (!$data) die("Error: Could not parse JSON data.");

// Helper to expand tabular JSON data into associative arrays
function extractTabular($tabularData) {
    if (empty($tabularData['cols']) || empty($tabularData['data'])) return [];
    $cols = $tabularData['cols'];
    $result = [];
    foreach ($tabularData['data'] as $row) {
        $result[] = array_combine($cols, $row);
    }
    return $result;
}

try {
    echo "Starting full V1 data import...\n";
    
    $pdo->exec('SET FOREIGN_KEY_CHECKS = 0;');
    $pdo->exec('TRUNCATE TABLE wrappers; TRUNCATE TABLE use_cases; TRUNCATE TABLE categories;');
    $pdo->exec('TRUNCATE TABLE languages; TRUNCATE TABLE block_templates;');
    $pdo->exec('TRUNCATE TABLE ai_models; TRUNCATE TABLE settings;');
    $pdo->exec('SET FOREIGN_KEY_CHECKS = 1;');

    // START TRANSACTION (Makes imports 100x faster)
    $pdo->beginTransaction();
    echo "Clearing tables...\n";

    // 1. Categories
    if (isset($data['categories'])) {
        $stmt = $pdo->prepare("INSERT INTO categories (code, name, status, ordering) VALUES (?, ?, 1, ?)");
        foreach (extractTabular($data['categories']) as $c) {
            $stmt->execute([$c['code'], $c['name'], $c['ordering'] ?? 99999]);
        }
        echo "Categories imported successfully.\n";
    }
    
    // 2. Use Cases 
    if (isset($data['useCases'])) {
        $stmt = $pdo->prepare("INSERT INTO use_cases (code, name, description, category_code, schema_data, status, ordering) VALUES (?, ?, ?, ?, ?, 1, ?)");
        foreach (extractTabular($data['useCases']) as $uc) {
            $schemaJson = !empty($uc['schema']) ? json_encode($uc['schema']) : null;
            $stmt->execute([$uc['code'], $uc['name'], $uc['desc'], $uc['cat'], $schemaJson, $uc['ordering'] ?? 999999999]);
        }
        echo "Use Cases imported successfully.\n";
    }

    // 3. Wrappers
    if (isset($data['wrappers'])) {
        $stmt = $pdo->prepare("INSERT INTO wrappers (code, name, description, use_case_code, featured, base, status, ordering) VALUES (?, ?, ?, ?, ?, ?, 1, ?)");
        foreach (extractTabular($data['wrappers']) as $w) {
            $featured = !empty($w['featured']) ? 1 : 0;
            $base = !empty($w['base']) ? 1 : 0;
            $stmt->execute([$w['code'], $w['name'], $w['desc'], $w['usecase'], $featured, $base, $w['ordering'] ?? 999999999]);
        }
        echo "Wrappers imported successfully.\n";
    }

    // 4. Languages
    if (isset($data['languages'])) {
        $stmt = $pdo->prepare("INSERT INTO languages (code, name, status, ordering) VALUES (?, ?, 1, ?)");
        $ord = 1;
        foreach (extractTabular($data['languages']) as $l) {
            $stmt->execute([$l['code'], $l['name'], $ord++]);
        }
        echo "Languages imported successfully.\n";
    }
    
    // 5. AI Models
    if (isset($data['aiModels'])) {
        $stmt = $pdo->prepare("INSERT INTO ai_models (code, name, status, ordering) VALUES (?, ?, 1, ?)");
        foreach (extractTabular($data['aiModels']) as $m) {
            $stmt->execute([$m['code'], $m['name'], $m['ordering'] ?? 99999]);
        }
        echo "AI Models imported successfully.\n";
    }

    // 6. Block Templates 
    if (isset($data['templates'])) {
        $stmt = $pdo->prepare("INSERT INTO block_templates (type, code, name, status) VALUES (?, ?, ?, 1)");
        foreach ($data['templates'] as $type => $tabular) {
            foreach (extractTabular($tabular) as $tpl) {
                $stmt->execute([$type, $tpl['code'], $tpl['name']]);
            }
        }
        echo "Block templates imported successfully.\n";
    }

    // 7. Save Version
    if (isset($data['version'])) {
        $stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', ?) ON DUPLICATE KEY UPDATE setting_value = ?");
        $stmt->execute([$data['version'], $data['version']]);
        echo "Data version saved to settings table.\n";
    }
    
    // COMMIT TRANSACTION (Saves everything to hard drive instantly)
    $pdo->commit();

    echo "\n✅ V1 Import complete! Version: " . ($data['version'] ?? 'N/A') . "\n";

} catch (PDOException $e) {
    // If anything fails, undo all changes
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    die("Database error during import: " . $e->getMessage() . "\n");
}
?>

Step 5: The Dynamic Playground

This brand new UI automatically renders "Core Inputs" and "Additional Context" fields based on the selected AI Template's schema.

Frontend: playground.html

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Zywrap V1 Offline Playground</title>
    <style>
        :root {
            --primary: #2563eb; --primary-hover: #1d4ed8;
            --bg: #f8fafc; --card-bg: #ffffff;
            --text-main: #0f172a; --text-muted: #64748b;
            --border: #e2e8f0; --radius: 0.75rem;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text-main); line-height: 1.5; padding: 2rem; }
        .container { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1.8fr; gap: 2rem; }
        @media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
        
        .card { background: var(--card-bg); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 1.5rem; }
        .card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem; color: var(--text-main); }
        
        .form-group { margin-bottom: 1rem; }
        label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 0.35rem; color: #334155; }
        
        .input, select, textarea { 
            width: 100%; padding: 0.625rem 0.75rem; border: 1px solid var(--border); 
            border-radius: 0.5rem; font-size: 0.875rem; background: #fff; transition: border-color 0.2s; 
        }
        .input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
        textarea { resize: vertical; min-height: 80px; }
        
        .filters { display: flex; gap: 1rem; margin-top: 0.5rem; }
        .filter-label { font-size: 0.875rem; font-weight: normal; display: flex; align-items: center; gap: 0.3rem; color: var(--text-muted); cursor: pointer; }
        
        .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
        
        .btn { 
            width: 100%; background: var(--primary); color: white; border: none; padding: 0.875rem; 
            border-radius: 0.5rem; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s; 
        }
        .btn:hover { background: var(--primary-hover); }
        .btn:disabled { opacity: 0.7; cursor: not-allowed; }
        
        .schema-section { background: #f8fafc; border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; }
        .schema-title { font-size: 0.875rem; font-weight: 700; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 0.75rem; color: #475569; }
        
        #output { background: #f1f5f9; padding: 1.25rem; border-radius: 0.5rem; font-family: ui-monospace, monospace; font-size: 0.875rem; white-space: pre-wrap; min-height: 300px; border: 1px solid var(--border); overflow-x: auto; }
    </style>
</head>
<body>
    <div class="container">
        <div>
            <div class="card">
                <h2 class="card-title">Configuration</h2>
                
                <div class="form-group">
                    <label>1. Category</label>
                    <select id="category"><option value="">Loading...</option></select>
                </div>
                
                <div class="form-group">
                    <label>2. AI Template (Wrapper)</label>
                    <select id="wrapper" disabled><option value="">-- Select Category First --</option></select>
                    <div class="filters">
                        <label class="filter-label"><input type="checkbox" id="filter-base"> Base Only</label>
                        <label class="filter-label"><input type="checkbox" id="filter-featured"> Featured Only</label>
                    </div>
                </div>

                <div class="form-group">
                    <label>3. AI Model</label>
                    <select id="model"><option value="">Loading...</option></select>
                </div>

                <div class="form-group">
                    <label>4. Target Language</label>
                    <select id="language"><option value="">Loading...</option></select>
                </div>
            </div>

            <div class="card">
                <h2 class="card-title" style="font-size: 1rem;">Advanced Overrides</h2>
                <div class="grid-2 overrides">
                    <div><label>Tone</label><select id="toneCode"><option value="">-- Default --</option></select></div>
                    <div><label>Style</label><select id="styleCode"><option value="">-- Default --</option></select></div>
                    <div><label>Formatting</label><select id="formatCode"><option value="">-- Default --</option></select></div>
                    <div><label>Complexity</label><select id="complexityCode"><option value="">-- Default --</option></select></div>
                    <div><label>Length</label><select id="lengthCode"><option value="">-- Default --</option></select></div>
                    <div><label>Audience</label><select id="audienceCode"><option value="">-- Default --</option></select></div>
                    <div><label>Goal</label><select id="responseGoalCode"><option value="">-- Default --</option></select></div>
                    <div><label>Output Type</label><select id="outputCode"><option value="">-- Default --</option></select></div>
                </div>
            </div>
        </div>

        <div>
            <div class="card">
                <div id="dynamic-schema-container"></div>
                
                <div class="form-group">
                    <label id="prompt-label">Prompt / Additional Context</label>
                    <textarea id="prompt" class="input" placeholder="Type your request or additional instructions here..."></textarea>
                </div>
                
                <button id="run-button" class="btn">Generate Response</button>
            </div>

            <div class="card">
                <h2 class="card-title">AI Response</h2>
                <pre id="output">Output will appear here...</pre>
            </div>
        </div>
    </div>

    <script>
        const API_ENDPOINT = 'api.php';
        
        const elements = {
            category: document.getElementById('category'),
            wrapper: document.getElementById('wrapper'),
            model: document.getElementById('model'),
            language: document.getElementById('language'),
            schemaContainer: document.getElementById('dynamic-schema-container'),
            prompt: document.getElementById('prompt'),
            promptLabel: document.getElementById('prompt-label'),
            runBtn: document.getElementById('run-button'),
            output: document.getElementById('output'),
            filterBase: document.getElementById('filter-base'),
            filterFeatured: document.getElementById('filter-featured')
        };

        async function fetchAPI(action, params = '') {
            const res = await fetch(`${API_ENDPOINT}?action=${action}&${params}`);
            return await res.json();
        }

        function populateSelect(el, data, placeholder = '-- Select --') {
            el.innerHTML = `<option value="">${placeholder}</option>`;
            data.forEach(item => {
                const opt = document.createElement('option');
                opt.value = item.code; 
                opt.textContent = item.name;
                el.appendChild(opt);
            });
            el.disabled = false;
        }

        async function init() {
            const [categories, models, langs, templates] = await Promise.all([
                fetchAPI('get_categories'),
                fetchAPI('get_ai_models'),
                fetchAPI('get_languages'),
                fetchAPI('get_block_templates')
            ]);
            
            populateSelect(elements.category, categories);
            populateSelect(elements.model, models, 'Default Model');
            populateSelect(elements.language, langs, 'English (Default)');

            // Populate Overrides
            const overrideMap = {
                tones: 'toneCode', styles: 'styleCode', formattings: 'formatCode',
                complexities: 'complexityCode', lengths: 'lengthCode', audienceLevels: 'audienceCode',
                responseGoals: 'responseGoalCode', outputTypes: 'outputCode'
            };
            for (const [type, elId] of Object.entries(overrideMap)) {
                if (templates[type]) populateSelect(document.getElementById(elId), templates[type], '-- Default --');
            }
        }

        // Category or Filter Change -> Load Wrappers
        const loadWrappers = async () => {
            if (!elements.category.value) {
                elements.wrapper.innerHTML = '<option value="">-- Select Category First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            elements.wrapper.innerHTML = '<option value="">Loading...</option>';
            let wrappers = await fetchAPI('get_wrappers', 'category=' + elements.category.value);
            
            if (elements.filterBase.checked) wrappers = wrappers.filter(w => w.base);
            if (elements.filterFeatured.checked) wrappers = wrappers.filter(w => w.featured);
            
            populateSelect(elements.wrapper, wrappers);
        };

        elements.category.addEventListener('change', loadWrappers);
        elements.filterBase.addEventListener('change', loadWrappers);
        elements.filterFeatured.addEventListener('change', loadWrappers);

        // Wrapper Change -> Load Schema
        elements.wrapper.addEventListener('change', async () => {
            elements.schemaContainer.innerHTML = '';
            elements.promptLabel.textContent = "Prompt / Additional Context";
            if (!elements.wrapper.value) return;

            const schema = await fetchAPI('get_schema', 'wrapper=' + elements.wrapper.value);
            if (!schema || (!schema.req && !schema.opt)) return;

            let html = '';
            elements.promptLabel.textContent = "Additional Free-form Instructions";

            const buildSection = (title, data) => {
                if (!data || Object.keys(data).length === 0) return '';
                let sectionHtml = `<div class="schema-section"><h3 class="schema-title">${title}</h3><div class="grid-2">`;
                for (const [key, def] of Object.entries(data)) {
                    
                    // 1. Python minifier strips 'p' if false. If undefined, it's a default value.
                    const isPlaceholder = def.p !== undefined ? def.p : false;
                    const defaultVal = def.d !== undefined ? def.d : '';
                    
                    // 2. Set the correct HTML attributes (mirrors React's initialInputs logic)
                    const placeholderAttr = isPlaceholder ? `placeholder="${defaultVal}"` : '';
                    const valueAttr = (!isPlaceholder && defaultVal) ? `value="${defaultVal}"` : '';
                    
                    const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
                    
                    sectionHtml += `<div>
                        <label>${label}</label>
                        <input type="text" class="input schema-input" data-key="${key}" ${placeholderAttr} ${valueAttr}>
                    </div>`;
                }
                return sectionHtml + '</div></div>';
            };

            html += buildSection('Core Inputs', schema.req);
            html += buildSection('Additional Context', schema.opt);
            elements.schemaContainer.innerHTML = html;
        });

        // Execute
        elements.runBtn.addEventListener('click', async () => {
            if (!elements.wrapper.value) return alert("Please select a wrapper.");
            elements.output.textContent = 'Executing...';
            elements.runBtn.disabled = true;
            elements.runBtn.textContent = 'Generating...';
            
            let finalPrompt = elements.prompt.value.trim();
            const variables = {};
            let structuredTextParts = [];

            document.querySelectorAll('.schema-input').forEach(input => {
                const val = input.value.trim();
                if (val !== '') {
                    const key = input.dataset.key;
                    variables[key] = val;
                    structuredTextParts.push(`${key}: ${val}`);
                }
            });

            const structuredText = structuredTextParts.join('\n');
            if (finalPrompt && structuredText) {
                finalPrompt = `${finalPrompt}\n\n${structuredText}`;
            } else if (structuredText) {
                finalPrompt = structuredText;
            }

            const overrides = {};
            document.querySelectorAll('.overrides select').forEach(sel => {
                if (sel.value) overrides[sel.id] = sel.value;
            });

            try {
                const response = await fetch(API_ENDPOINT + '?action=execute', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        model: elements.model.value,
                        wrapperCode: elements.wrapper.value,
                        language: elements.language.value,
                        prompt: finalPrompt, // Pass concatenated prompt
                        variables: variables,
                        overrides: overrides
                    })
                });
                
                const text = await response.text();
                if (!response.ok) {
                    let errMsg = text;
                    try { errMsg = JSON.parse(text).error || errMsg; } catch(e) {}
                    throw new Error(errMsg);
                }
                
                const data = JSON.parse(text);
                elements.output.textContent = data.output || JSON.stringify(data, null, 2);
            } catch (error) {
                elements.output.textContent = 'Error: ' + error.message;
            } finally {
                elements.runBtn.disabled = false;
                elements.runBtn.textContent = 'Generate Response';
            }
        });

        init();
    </script>
</body>
</html>

Backend: api.php

php

<?php
// FILE: api.php
ini_set('max_execution_time', '300');
require 'db.php';
header('Content-Type: application/json');

$zywrapApiKey = "YOUR_ZYWRAP_API_KEY";

// --- V1 Backend Logic ---

function getCategories($pdo) {
    return $pdo->query("SELECT code, name FROM categories WHERE status = 1 ORDER BY ordering ASC")->fetchAll();
}

function getWrappersByCategory($pdo, $categoryCode) {
    $stmt = $pdo->prepare("
        SELECT w.code, w.name, w.featured, w.base 
        FROM wrappers w 
        JOIN use_cases uc ON w.use_case_code = uc.code 
        WHERE uc.category_code = ? AND w.status = 1 AND uc.status = 1
        ORDER BY w.ordering ASC
    ");
    $stmt->execute([$categoryCode]);
    return $stmt->fetchAll();
}

function getSchemaByWrapper($pdo, $wrapperCode) {
    $stmt = $pdo->prepare("
        SELECT uc.schema_data 
        FROM use_cases uc 
        JOIN wrappers w ON w.use_case_code = uc.code 
        WHERE w.code = ?
    ");
    $stmt->execute([$wrapperCode]);
    $result = $stmt->fetchColumn();
    return $result ? json_decode($result, true) : null;
}

function getLanguages($pdo) { 
    return $pdo->query("SELECT code, name FROM languages WHERE status = 1 ORDER BY ordering ASC")->fetchAll(); 
}

function getAiModels($pdo) { 
    return $pdo->query("SELECT code, name FROM ai_models WHERE status = 1 ORDER BY ordering ASC")->fetchAll(); 
}

function getBlockTemplates($pdo) {
    $stmt = $pdo->query("SELECT type, code, name FROM block_templates WHERE status = 1 ORDER BY type, name ASC");
    $grouped = [];
    foreach ($stmt->fetchAll() as $row) {
        $grouped[$row['type']][] = ['code' => $row['code'], 'name' => $row['name']];
    }
    return $grouped;
}

// --- Execution ---
function executeZywrapProxy($apiKey, $model, $wrapperCode, $prompt, $language, $variables, $overrides) {
    $url = 'https://api.zywrap.com/v1/proxy'; 
    
    $payloadData = [
        'model' => $model, 
        'wrapperCodes' => [$wrapperCode], 
        'prompt' => $prompt,
        'variables' => $variables,
        'source' => 'php_sdk'
    ];
    if (!empty($language)) $payloadData['language'] = $language;
    if (!empty($overrides)) $payloadData = array_merge($payloadData, $overrides);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payloadData));
    curl_setopt($ch, CURLOPT_TIMEOUT, 300); 
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $apiKey
    ]);

    $rawResponse = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 200) {
        $lines = explode("\n", $rawResponse);
        $finalJson = null;
        foreach ($lines as $line) {
            $line = trim($line);
            if (strpos($line, 'data: ') === 0) {
                $data = json_decode(substr($line, 6), true);
                if ($data && (isset($data['output']) || isset($data['error']))) $finalJson = substr($line, 6);
            }
        }
        
        $statusCode = 200;
        if ($finalJson) {
            $parsed = json_decode($finalJson, true);
            if (isset($parsed['error'])) $statusCode = 400;
        }
        
        return ['status' => $statusCode, 'response' => $finalJson ?: json_encode(['error' => 'Stream parse failed'])];
    }
    return ['status' => $httpCode, 'response' => $rawResponse];
}

// --- API Router ---
$action = $_GET['action'] ?? '';

switch ($action) {
    case 'get_categories': echo json_encode(getCategories($pdo)); break;
    case 'get_wrappers': echo json_encode(getWrappersByCategory($pdo, $_GET['category'] ?? '')); break;
    case 'get_schema': echo json_encode(getSchemaByWrapper($pdo, $_GET['wrapper'] ?? '')); break;
    case 'get_languages': echo json_encode(getLanguages($pdo)); break;
    case 'get_ai_models': echo json_encode(getAiModels($pdo)); break;
    case 'get_block_templates': echo json_encode(getBlockTemplates($pdo)); break;
    
    case 'execute':
        $input = json_decode(file_get_contents('php://input'), true);
        
        // ⏱️ Start Local Timer
        $startTime = microtime(true);
        
        $result = executeZywrapProxy(
            $zywrapApiKey, 
            $input['model'] ?? null, 
            $input['wrapperCode'] ?? '',
            $input['prompt'] ?? '', 
            $input['language'] ?? null, 
            $input['variables'] ?? [],
            $input['overrides'] ?? []
        );
        
        // ⏱️ End Local Timer
        $latencyMs = round((microtime(true) - $startTime) * 1000);

        // --- 📝 LOGGING TO LOCAL DATABASE ---
        try {
            $status = $result['status'] === 200 ? 'success' : 'error';
            $responseData = json_decode($result['response'], true);
            
            $traceId = $responseData['id'] ?? null;
            $promptTokens = $responseData['usage']['prompt_tokens'] ?? 0;
            $completionTokens = $responseData['usage']['completion_tokens'] ?? 0;
            $totalTokens = $responseData['usage']['total_tokens'] ?? 0;
            $creditsUsed = $responseData['cost']['credits_used'] ?? 0;
            
            $fallbackError = substr($result['response'], 0, 255) . (strlen($result['response']) > 255 ? '...' : '');
            $errorMessage = $status === 'error' ? ($responseData['error'] ?? $fallbackError) : null;

            $stmt = $pdo->prepare("INSERT INTO usage_logs (trace_id, wrapper_code, model_code, prompt_tokens, completion_tokens, total_tokens, credits_used, latency_ms, status, error_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
            $stmt->execute([
                $traceId, 
                $input['wrapperCode'], 
                $input['model'] ?? 'default', 
                $promptTokens, 
                $completionTokens, 
                $totalTokens, 
                $creditsUsed, 
                $latencyMs, 
                $status, 
                $errorMessage
            ]);
        } catch (Exception $e) {
            // Fail silently if logging fails so we don't break the user's API response
            error_log("Failed to write to usage_logs: " . $e->getMessage());
        }
        // ------------------------------------

        http_response_code($result['status']);
        echo $result['response'];
        break;
}
?>

Step 6: Synchronize Your Data

Set this script to run on a cron job. It points to the new /v1/sync endpoint.

php

<?php
// FILE: zywrap-sync.php
/**
 * Zywrap V1 SDK - Smart Data Synchronizer
 *
 * STRATEGY: "Download & Reconcile"
 * 1. Downloads the latest full data bundle (zip) if FULL_RESET.
 * 2. Updates existing records and inserts new ones if DELTA_UPDATE.
 *
 * USAGE: php zywrap-sync.php
 */

// Increase execution time and memory limit for large data processing
ini_set('max_execution_time', '300');
ini_set('memory_limit', '512M'); // ✅ FIX: Added limit

// --- CONFIGURATION ---
$apiKey = "YOUR_ZYWRAP_API_KEY"; 
$apiUrl = 'https://api.zywrap.com/v1/sdk/v1/sync'; // V1 Sync Endpoint
require_once 'db.php'; 

// --- HELPER: UPSERT BATCH (For Delta Updates) ---
function upsertBatch(PDO $pdo, string $tableName, array $rows, array $columns, string $pk = 'code') {
    if (empty($rows)) return;
    
    $colList = implode(", ", $columns);
    $placeholders = implode(", ", array_fill(0, count($columns), "?"));
    
    $updateClause = [];
    foreach ($columns as $col) {
        if ($col !== $pk && $col !== 'type') { 
            $updateClause[] = "$col = VALUES($col)";
        }
    }
    $updateSql = implode(", ", $updateClause);
    $sql = "INSERT INTO $tableName ($colList) VALUES ($placeholders) ON DUPLICATE KEY UPDATE $updateSql";
    
    $stmt = $pdo->prepare($sql);
    $pdo->beginTransaction();
    try {
        foreach ($rows as $row) $stmt->execute($row);
        $pdo->commit();
        echo "   [+] Upserted " . count($rows) . " records into '$tableName'.\n";
    } catch (Exception $e) {
        $pdo->rollBack();
        echo "   [!] Error upserting $tableName: " . $e->getMessage() . "\n";
    }
}

// --- HELPER: DELETE BATCH ---
function deleteBatch(PDO $pdo, string $tableName, array $ids, string $pk = 'code') {
    if (empty($ids)) return;
    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare("DELETE FROM $tableName WHERE $pk = ?");
        foreach ($ids as $id) $stmt->execute([$id]);
        $pdo->commit();
        echo "   [-] Deleted " . count($ids) . " records from '$tableName'.\n";
    } catch (Exception $e) {
        $pdo->rollBack();
        echo "   [!] Error deleting from $tableName: " . $e->getMessage() . "\n";
    }
}

// =================================================================
// MAIN LOGIC
// =================================================================

echo "--- 🚀 Starting Zywrap V1 Sync ---\n";

$stmt = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'data_version'");
$localVersion = $stmt->fetchColumn() ?: '';
echo "🔹 Local Version: " . ($localVersion ?: 'None') . "\n";

// Call API
$url = $apiUrl . '?fromVersion=' . urlencode($localVersion);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Accept: application/json',
    'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200 || !$response) die("❌ API Error ($httpCode): $response\n");
$json = json_decode($response, true);
if (!$json) die("❌ Invalid JSON response.\n");

$mode = $json['mode'] ?? 'UNKNOWN';
echo "🔹 Sync Mode: $mode\n";

// --- SCENARIO A: FULL RESET ---
if ($mode === 'FULL_RESET') {
    // 🟢 1. Save it with the official name so the user recognizes it
    $zipFile = 'zywrap-data.zip'; 
    $downloadUrl = $json['wrappers']['downloadUrl'];

    echo "⬇️  Attempting automatic download from Zywrap...\n";
    
    $fp = fopen($zipFile, 'w+');
    $chDl = curl_init($downloadUrl);
    curl_setopt($chDl, CURLOPT_TIMEOUT, 300);
    curl_setopt($chDl, CURLOPT_FILE, $fp); 
    curl_setopt($chDl, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($chDl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($chDl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $apiKey]);
    
    $success = curl_exec($chDl);
    $httpCodeDl = curl_getinfo($chDl, CURLINFO_HTTP_CODE);
    curl_close($chDl);
    fclose($fp);
    
    // 2. Check if the download succeeded
    if ($httpCodeDl === 200 && $success && file_exists($zipFile) && filesize($zipFile) > 0) {
        
        $mbSize = round(filesize($zipFile) / 1024 / 1024, 2);
        echo "✅ Data bundle downloaded successfully ({$mbSize} MB).\n";

        // 3. Attempt Auto-Unzip
        if (class_exists('ZipArchive')) {
            $zip = new ZipArchive;
            if ($zip->open($zipFile) === TRUE) {
                $zip->extractTo(__DIR__);
                $zip->close();
                echo "✅ Auto-unzip successful. Running import script...\n";
                @unlink($zipFile); // Clean up the zip file after successful extraction
                include 'import.php'; 
            } else {
                // If it fails due to folder permissions
                echo "⚠️ Failed to auto-unzip (Check directory permissions).\n";
                echo "\n👉 ACTION REQUIRED:\n";
                echo "   1. Please manually unzip 'zywrap-data.zip' in this folder.\n";
                echo "   2. Then run: php import.php\n";
            }
        } else {
            // 🟢 4. The graceful fallback if ZipArchive is missing!
            echo "⚠️ PHP 'ZipArchive' extension is missing. Auto-unzip skipped.\n";
            echo "\n👉 ACTION REQUIRED:\n";
            echo "   1. Please manually extract the 'zywrap-data.zip' file into this folder.\n";
            echo "   2. Once extracted, run this command to update your database:\n";
            echo "      php import.php\n";
        }
    } else {
        echo "❌ Automatic download failed. HTTP Status: $httpCodeDl\n";
        @unlink($zipFile); // Clean up broken/empty file
    }
}
// --- SCENARIO B: DELTA UPDATE ---
elseif ($mode === 'DELTA_UPDATE') {
    
    // 1. Categories (Prisma uses 'position' or 'displayOrder')
    $rows = [];
    foreach(($json['metadata']['categories']??[]) as $r) {
        $status = (!isset($r['status']) || $r['status']) ? 1 : 0;
        $rows[] = [$r['code'], $r['name'], $status, $r['position'] ?? $r['displayOrder'] ?? $r['ordering'] ?? null];
    }
    upsertBatch($pdo, 'categories', $rows, ['code', 'name', 'status', 'ordering']);

    // 2. Languages (Manually mapped in backend)
    $rows = [];
    foreach(($json['metadata']['languages']??[]) as $r) {
        $status = (!isset($r['status']) || $r['status']) ? 1 : 0;
        $rows[] = [$r['code'], $r['name'], $status, $r['ordering'] ?? null];
    }
    upsertBatch($pdo, 'languages', $rows, ['code', 'name', 'status', 'ordering']);

    // 3. AI Models (Prisma uses 'providerId' and 'displayOrder')
    $rows = [];
    foreach(($json['metadata']['aiModels']??[]) as $r) {
        $status = (!isset($r['status']) || $r['status']) ? 1 : 0;
        $rows[] = [
            $r['code'], 
            $r['name'], 
            $status,
            $r['displayOrder'] ?? $r['ordering'] ?? null
        ];
    }
    upsertBatch($pdo, 'ai_models', $rows, ['code', 'name', 'status', 'ordering']);

    // 4. Templates
    $rows = [];
    if (!empty($json['metadata']['templates'])) {
        foreach ($json['metadata']['templates'] as $type => $items) {
            foreach ($items as $item) {
                $status = (!isset($item['status']) || $item['status']) ? 1 : 0;
                $rows[] = [$type, $item['code'], $item['label'] ?? $item['name'] ?? null, $status];
            }
        }
    }
    upsertBatch($pdo, 'block_templates', $rows, ['type', 'code', 'name', 'status']);

    // 5. Use Cases & Schemas
    if (!empty($json['useCases']['upserts'])) {
        $rows = [];
        foreach($json['useCases']['upserts'] as $uc) {
            $schemaJson = !empty($uc['schema']) ? json_encode($uc['schema']) : null;
            $status = (!isset($uc['status']) || $uc['status']) ? 1 : 0;
            $rows[] = [
                $uc['code'], 
                $uc['name'], 
                $uc['description'] ?? null, 
                $uc['categoryCode'] ?? null, 
                $schemaJson, 
                $status,
                $uc['displayOrder'] ?? $uc['ordering'] ?? null
            ];
        }
        upsertBatch($pdo, 'use_cases', $rows, ['code', 'name', 'description', 'category_code', 'schema_data', 'status', 'ordering']);
    }

    // 6. Wrappers
    if (!empty($json['wrappers']['upserts'])) {
        $rows = [];
        foreach($json['wrappers']['upserts'] as $w) {
            $featured = !empty($w['featured'] ?? $w['isFeatured']) ? 1 : 0;
            $base = !empty($w['base'] ?? $w['isBaseWrapper']) ? 1 : 0;
            $status = (!isset($w['status']) || $w['status']) ? 1 : 0;
            
            $rows[] = [
                $w['code'], 
                $w['name'], 
                $w['description'] ?? null, 
                $w['useCaseCode'] ?? $w['categoryCode'] ?? null, 
                $featured, 
                $base, 
                $status,
                $w['displayOrder'] ?? $w['ordering'] ?? null
            ];
        }
        upsertBatch($pdo, 'wrappers', $rows, ['code', 'name', 'description', 'use_case_code', 'featured', 'base', 'status', 'ordering']);
    }

    // 7. Deletes
    if (!empty($json['wrappers']['deletes'])) deleteBatch($pdo, 'wrappers', $json['wrappers']['deletes']);
    if (!empty($json['useCases']['deletes'])) deleteBatch($pdo, 'use_cases', $json['useCases']['deletes']);

    // 8. Update Version
    if (!empty($json['newVersion'])) {
        $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', ?) ON DUPLICATE KEY UPDATE setting_value=?")->execute([$json['newVersion'], $json['newVersion']]);
    }
    
    echo "✅ Delta Sync Complete.\n";
}
?>

Programmatically Download

Use this script to fetch the latest V1 data bundle automatically.

php

<?php
// FILE: download-bundle.php
// USAGE: php download-bundle.php

$apiKey = 'YOUR_API_KEY';
$apiUrl = 'https://api.zywrap.com/v1/sdk/v1/download'; // V1 Download URL
$outputFile = 'zywrap-data.zip';

echo "Downloading latest V1 wrapper data from Zywrap...\n";

$fp = fopen($outputFile, 'w+');
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_FILE, $fp); // Let cURL write directly to the file
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Add this to prevent SSL errors
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
]);

// $success will be true/false, NOT the file contents!
$success = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Close curl AND close the file pointer to save it to disk!
curl_close($ch);
fclose($fp);

if ($httpCode !== 200 || !$success) {
    @unlink($outputFile); // ✅ FIX: Delete corrupted text file masquerading as ZIP on error
    die("Error: Failed to download file. Status code: {$httpCode}\n");
}

echo "✅ Sync complete. Data saved to {$outputFile}.\n";
echo "Run 'unzip {$outputFile}' to extract the data, then run 'php import.php'.\n";
?>
Ready to ship?

Start building with Zywrap today.

Stop wrestling with prompts. Integrate powerful, structured AI into your applications in minutes.