
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.
-- 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
// 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
// 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
<!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
// 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
// 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
// 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";
?>

