20 KiB
ZAP Framework Guide
This document explains the internal architecture of ZapMachine and provides step‑by‑step instructions for extending it with new handlers, templates, and interactive elements.
Architecture Overview
Front-Controller Routing (?mode=X → ZAPController → ZAP{Mode})
All HTTP requests enter through www/index.php. The front controller reads the mode query parameter and dispatches to a class whose name is formed as ZAP + the ucfirst'd mode value.
// www/index.php (simplified)
$mode = isset($_GET['mode']) ? $_GET['mode'] : 'home';
require_once '..' . DIRECTORY_SEPARATOR . 'conf' . DIRECTORY_SEPARATOR . 'init.php';
$config = Config::getInstance();
$config->register('engines', $search_engines);
$frontcontroller = new ZAPController($mode);
$output = $frontcontroller->fetch();
print($output);
ZAPController (in lib/ZAPController.class.php) constructs the class name from a prefix (ZAP) and the mode, verifies it exists via the autoloader, instantiates it, and calls getContent():
class ZAPController {
private $prefix = 'ZAP';
public function __construct($mode = 'home') {
$mode = trim(strip_tags($mode));
$this->mode = ucfirst($mode);
$clazz = $this->prefix . $this->mode;
if (!class_exists($clazz)) {
die('Wrong parameter');
}
$this->action = new $clazz();
}
public function fetch() {
return $this->action->getContent();
}
}
Registered modes and their handlers:
?mode= |
Class | Template | Purpose |
|---|---|---|---|
home (default) |
ZAPHome |
tpl/index.tpl |
Word-input form |
display |
ZAPDisplay |
tpl/display.tpl |
Collage-generation progress screen |
zap |
ZAPZap |
(raw image output) | Downloads images & processes collages, returns image blobs |
Autoloader
conf/init.php registers an spl_autoload_register callback that searches every directory on the include path for both {ClassName}.class.php and {ClassName}.interface.php:
spl_autoload_register(function ($class_name) {
$include_path_tokens = explode(PATH_SEPARATOR, get_include_path());
foreach ($include_path_tokens as $prefix) {
$path[0] = $prefix . DIRECTORY_SEPARATOR . $class_name . '.interface.php';
$path[1] = $prefix . DIRECTORY_SEPARATOR . $class_name . '.class.php';
foreach ($path as $thisPath) {
if (file_exists($thisPath)) {
require_once $thisPath;
return;
}
}
}
});
The include path is built from these directories (relative to the project root):
| Constant | Directory |
|---|---|
ZAP_RESOURCE_DIR |
resource/ — search-engine implementations |
ZAP_UTILS_DIR |
utils/ — utility classes (Session, ZAPCurl, Log) |
ZAP_SESSIONS_DIR |
sessions/ |
ZAP_LIB_DIR |
lib/ — core handlers and image processing |
ZAP_TPL_DIR |
tpl/ — templates |
ZAP_CONF_DIR |
conf/ — configuration |
This means any .class.php or .interface.php file placed in one of those directories is automatically found — no manual require_once calls needed for library classes.
Config Singleton
Configuration values that must be accessible from any class are managed through Config (conf/Config.class.php), a classic singleton:
$config = Config::getInstance();
$config->register('engines', $search_engines); // store
$engines = $config->retrieve('engines'); // retrieve — returns false if not found
The singleton is wired in www/index.php immediately after init.php is loaded.
Session Management
The Session class (utils/Session.class.php) wraps PHP's native sessions:
Session::start('ZAP'); // Begin session (sessions named 'ZAP')
Session::reset(); // Clear $_SESSION array
Session::deleteCookie(); // Remove client-side session cookie
Session::destroy(); // Reset + delete cookie + session_destroy()
Session data is stored verbatim in $_SESSION and uses these keys:
| Key | Type | Set by | Purpose |
|---|---|---|---|
words |
string[] |
ZAPDisplay |
Keyword list for image search |
folder |
string |
ZAPDisplay |
Absolute path to session folder |
collages |
int |
ZAPDisplay |
Number of collages to generate |
width |
int |
ZAPDisplay |
Collage width in pixels |
height |
int |
ZAPDisplay |
Collage height in pixels |
images |
string[] |
ZAPZap |
Queue of source image paths for current collage |
imgOriginal |
string[] |
ZAPZap |
Immutable copy of the original image list |
collageNr |
int |
ZAPZap |
Current collage sequence number |
CLI note: Session is only started automatically in web mode. See CLI Bootstrap below.
Plugin-Based Search Engines
Search engines implement the Search interface (resource/Search.interface.php):
interface Search {
public function setQuery($word);
public function setParam($key, $value);
public function getData();
}
Each engine class lives in resource/ and returns data in a universal array format:
$data['result'] = true; // Boolean: success / failure
$data['engine'] = __CLASS__; // Engine name, e.g. 'Brave'
$data['results'][0]['clickurl'] = '...'; // Image URL to download
$data['results'][0]['referer'] = '...'; // Source page URL
$data['results'][0]['thumb'] = '...'; // Thumbnail URL
Available engines are registered as an array of names in conf/conf.php and wired through the Config singleton at runtime. ImgSearch randomly selects an engine from the registered list each time it runs:
$config = Config::getInstance();
$engines = $config->retrieve('engines');
$this->engine = $engines[array_rand($engines)];
Current engine: Brave (resource/Brave.class.php) — uses the Brave Search API.
To add a new engine:
- Create
resource/YourEngine.class.phpimplementingSearch. - Add
'yourengine'to the$search_enginesarray inconf/conf.php.
Image Processing Pipeline
The image pipeline runs entirely server-side through three classes in lib/:
-
ImgSearch(lib/ImgSearch.class.php) — searches for an image URL via a registered engine, downloads the file, validates its MIME type (image/jpeg,image/gif,image/png), and saves it to the session folder. -
ImgProcess(lib/ImgProcess.class.php) — constructs collages using ImageMagick. Each collage starts withcreateBasicCanvas()(zoom-crops the first source image) then layers additional images viaaddToCanvas()(random transparency + composite). -
ImgIOTools(lib/ImgIOTools.class.php) — handles output:onScreen()streams an Imagick object as an HTTP image response;urlOnScreen()loads a file path first;saveImg()writes to disk. -
ImgTools(lib/ImgTools.class.php) — utility functions:zoomCrop(),transparantRandom(),randomRotate(),resize(),recolor(),convertGifsToPng().
CLI Bootstrap
CLI scripts (in cli_scripts/) bootstrap the application differently from web requests. Because init.php skips Session::start() when php_sapi_name() === 'cli', CLI scripts must handle sessions manually if needed.
Example — cli_scripts/test.php:
#!/usr/bin/env php
<?php
// Make sure relative paths in init.php resolve against this script's directory
chdir(__DIR__);
// Bootstrap the application (autoloader, constants, Config, but NOT session)
require_once __DIR__ . '/../conf/init.php';
// If you need sessions in CLI, call Session::start() manually:
// Session::start('ZAP');
// Now all classes, the autoloader, and Config singleton are available
$config = Config::getInstance();
$config->register('engines', $search_engines);
Key differences from the web entry point:
chdir(__DIR__)is required beforerequire_once 'init.php'becauseinit.phpusesrealpath('.')to defineZAP_APP_BASE_DIR.Session::start()is not called — theinit.phpconditionphp_sapi_name() !== 'cli'guards against it.- You must explicitly register engines with
Config::getInstance()->register('engines', …)becausewww/index.phpnormally does this.
Logging
The Log class (utils/Log.class.php) writes timestamped messages to a session folder. It is used by ImgSearch (controlled by the ZAP_LOG constant in conf/conf.php). Each log call appends to both log.txt (plain text) and log.xml (XML-friendly) in the session's output directory.
if (ZAP_LOG == true) {
$log = new Log($this->folder);
$log->addToLog('ok', 'Search: ', "downloaded to $imageName");
$log->addToLog('error', 'Search: ', 'bad search data received');
}
Template Rendering Pipeline
The template system is a minimal PHP-native pattern built around ob_start() / include / extract() / ob_get_clean(). There is no template engine — templates are plain PHP files that receive variables via the symbol table.
How Templates Work
Every ZAP{Mode} handler follows the same rendering pattern:
class ZAPHome {
private $template = 'index'; // corresponds to tpl/index.tpl
private $tpl = '';
public function __construct() {
$this->ver = ZAP_VERSION;
extract(get_object_vars($this)); // ← makes all private properties available as $variables
ob_start(); // ← start output buffer
include getZAPTemplate($this->template); // ← include the .tpl file
$this->tpl .= ob_get_clean(); // ← capture buffer content, clean buffer
}
public function getContent() {
return $this->tpl;
}
}
Step by step:
- Properties are set in the constructor (often from
$_POST,$_SESSION, or constants likeZAP_VERSION). extract(get_object_vars($this))promotes every private property to a local variable in the current symbol table. A property$this->template = 'index'becomes$template = 'index'.ob_start()begins capturing all output.include getZAPTemplate($this->template)includes the filetpl/{name}.tpl. The included file inherits the current symbol table, so it can use$ver,$folder,$words, etc. directly.ob_get_clean()retrieves the captured HTML and closes the buffer.getContent()returns the accumulated output string, whichZAPController::fetch()returns toindex.phpforprint().
The getZAPTemplate() Helper
function getZAPTemplate($tplname) {
$tplfile = $tplname . '.tpl';
$tplfilepath = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR
. ZAP_TPL_DIR . DIRECTORY_SEPARATOR . $tplfile;
if (file_exists($tplfilepath)) {
return $tplfilepath;
} else {
return ERROR_PREFIX . 'No template';
}
}
All templates live in the tpl/ directory and have the .tpl extension. The helper resolves the absolute path so the include call works from any working directory.
Adding a New Template
To add a new page/view:
-
Create the
ZAP{Mode}handler inlib/. The class must be namedZAP{Mode}(case-sensitive, PascalCase after the prefix) and must implement agetContent()method that returns a string:<?php // lib/ZAPExample.class.php class ZAPExample { private $template = 'example'; private $tpl = ''; public function __construct() { $this->pageTitle = 'My Example Page'; extract(get_object_vars($this)); ob_start(); include getZAPTemplate($this->template); $this->tpl .= ob_get_clean(); } public function getContent() { return $this->tpl; } } -
Create the template at
tpl/example.tpl:<!DOCTYPE html> <html lang="en"> <head><title>Example — <?php echo $pageTitle; ?></title></head> <body> <h1><?php echo $pageTitle; ?></h1> <p>ZAP version <?php echo $ver; ?></p> </body> </html> -
Use the route: Navigate to
index.php?mode=example. The autoloader findsZAPExampleinlib/, the front controller instantiates it, andgetZAPTemplate()loadstpl/example.tpl.
No registration step is required — the front controller's naming convention (ZAP + ucfirst(mode)) and the autoloader handle discovery automatically.
Adding Interactive Elements
This section documents the two interactive-element patterns used in the application. Each pattern pairs a server-side template with a client-side JavaScript file.
Pattern 1: Form with Client-Side Validation (index.tpl + js/main.js)
Used by ZAPHome to collect word inputs, validate them in the browser, and POST to the display mode.
Server side (ZAPHome in lib/ZAPHome.class.php):
The handler passes two template variables into tpl/index.tpl:
$ver— the ZAP version string.$status— an optional status message from the query string (?stat=...).
The template renders a <form id="form1"> that POSTs to index.php?mode=display. Each input is named word01, word02, … word{MAX_WORDS} (zeropadded to two digits). A <select name="collages"> controls the number of collages, and width/height text inputs control dimensions.
Client side (js/main.js):
The form1Submit() function performs validation before the form is submitted:
function form1Submit() {
// Minimum of two words required
var word1 = document.getElementById('word01').value;
var word2 = document.getElementById('word02').value;
var width = document.getElementById('width').value;
var height = document.getElementById('height').value;
if (word1.length <= 0) {
document.getElementById('status').innerHTML = 'Minimum of two words required';
return;
}
if (word2.length <= 0) {
document.getElementById('status').innerHTML = 'Minimum of two words required';
return;
}
if (width.length <= 0) {
document.getElementById('status').innerHTML = 'Value <b>width</b> required';
return;
}
if (height.length <= 0) {
document.getElementById('status').innerHTML = 'Value <b>height</b> required';
return;
}
if (!IsNumeric(width)) {
document.getElementById('status').innerHTML = 'Value <b>width</b> needs numeric input';
return;
}
if (!IsNumeric(height)) {
document.getElementById('status').innerHTML = 'Value <b>height</b> needs numeric input';
return;
}
// Hide form, show loading indicator, then submit
document.getElementById('status').innerHTML = ' ';
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('form1').style.visibility = 'hidden';
document.getElementById('form1').submit();
}
To reproduce this pattern in a new handler:
- Create a form in your
.tplthat POSTs toindex.php?mode=yourmode. - Create a
ZAPYourModehandler inlib/that reads$_POSTvalues in its constructor and stores them in$_SESSION. - Write a
js/your.jsfile with a validation function, and include<script src="js/your.js"></script>in the template's<head>. - Use a
<div id="status">element in the template to display validation errors.
Pattern 2: Polling Image Display (display.tpl + js/display.js)
Used by the display and zap modes to show collage progress. The server streams individual image responses, and the client alternates between two <img> tags to give the appearance of continuous updating.
Server side:
ZAPDisplay (in lib/ZAPDisplay.class.php) sets session variables from $_POST and renders tpl/display.tpl. The template declares onload="go(this,['img1','img2'],5000);" on the <body> tag, which kicks off the polling loop.
ZAPZap (in lib/ZAPZap.class.php) handles each individual poll request. On each invocation it either:
- Downloads the next source image (if
$_SESSION['words']is not empty) and streams it viaImgIOTools::onScreen(). - Starts the next collage (if
$_SESSION['words']is empty but$_SESSION['collages'] > 0). - Adds the next source image to the current collage via
ImgProcess::addToCanvas(). - Signals completion by destroying the session and returning an error string (which triggers the
onerrorhandler in JavaScript).
Client side (js/display.js):
var go = function(b, img_id, interval) {
var i = 0;
var img0 = document.getElementById(img_id[0]);
var img1 = document.getElementById(img_id[1]);
var img = [img0, img1];
var activeImage = 1; // index of visible image
var url = 'index.php?mode=zap';
if (isNaN(interval)) {
interval = 5000;
}
// Trigger the next image fetch after the current one has loaded.
img[0].onload = img[1].onload = function() {
var inactiveImage = activeImage === 0 ? 1 : 0;
img[activeImage].style.display = 'none';
img[inactiveImage].style.display = '';
activeImage = inactiveImage;
setTimeout(function() {
img[inactiveImage].src = url + '&i=' + (i++);
}, interval);
};
// When the backend signals completion it returns a small HTML page.
// Show the ready link and stop fetching new images.
img[0].onerror = img[1].onerror = function() {
document.getElementById('ready').innerHTML = '<a href="/">Ready</a>';
};
// Kick off the first request.
img[activeImage].src = url + '&i=' + (i++);
};
How the polling cadence works:
body onload="go(this,['img1','img2'],5000)"starts the loop with a 5 s interval between successive fetches.- Two
<img>tags (#img1and#img2) are used — one is visible, the other hidden. - When the visible image finishes loading (
img.onload), the pair swaps: the loaded image is hidden, and the previously hidden one is shown. After a 5 s delay (setTimeout), the newly hidden image'ssrcis set toindex.php?mode=zap&i={counter}. - Each request to
mode=zapreturns raw image data (aContent-Type: image/jpegresponse). The browser renders it into the<img>tag, triggeringonloadagain and continuing the cycle. - When
ZAPZapfinishes all collages, it destroys the session and returns an HTML error string instead of image data. The<img>tag fails to load an image, triggeringonerror, which replaces the image area with a "Ready" link.
To reproduce this pattern in a new handler:
- Create a template with two
<img>elements and<body onload="go(this,['id1','id2'],intervalMs)">. - Include
js/display.js(or copy the pattern into your own JS file). - Create a
ZAP{Mode}handler that returns raw image data (setContent-Typeheaders and useechowithreadfile()or Imagick'sgetImageBlob()). - Signal completion by returning a non-image response that will trigger
onerroron the<img>tag.
Summary: Creating a New Feature End-to-End
To add a completely new feature to ZapMachine, follow these steps:
- Add configuration in
conf/conf.php(constants, engine names) if needed. - Create the handler
lib/ZAP{Mode}.class.phpwithgetContent()returning a string. - Create the template
tpl/{mode}.tplusing theob_start/include/extract/ob_get_cleanpattern. - Choose an interactive pattern:
- For form input with validation: add a form in the template, write a JS validation function, and include the script.
- For polling/image updates: use the two-
<img>swap withonload/onerror.
- Create a JS file in
www/js/and reference it from the template's<head>. - Test the route at
index.php?mode={ModeName}. - Test from CLI using the bootstrap pattern in
cli_scripts/test.php(rememberchdir(__DIR__)+ manualSession::start()if needed).