ZapMachine/docs/framework_guide.md

20 KiB
Raw Blame History

ZAP Framework Guide

This document explains the internal architecture of ZapMachine and provides stepbystep instructions for extending it with new handlers, templates, and interactive elements.

Architecture Overview

Front-Controller Routing (?mode=XZAPControllerZAP{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:

  1. Create resource/YourEngine.class.php implementing Search.
  2. Add 'yourengine' to the $search_engines array in conf/conf.php.

Image Processing Pipeline

The image pipeline runs entirely server-side through three classes in lib/:

  1. 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.

  2. ImgProcess (lib/ImgProcess.class.php) — constructs collages using ImageMagick. Each collage starts with createBasicCanvas() (zoom-crops the first source image) then layers additional images via addToCanvas() (random transparency + composite).

  3. 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.

  4. 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 before require_once 'init.php' because init.php uses realpath('.') to define ZAP_APP_BASE_DIR.
  • Session::start() is not called — the init.php condition php_sapi_name() !== 'cli' guards against it.
  • You must explicitly register engines with Config::getInstance()->register('engines', …) because www/index.php normally 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:

  1. Properties are set in the constructor (often from $_POST, $_SESSION, or constants like ZAP_VERSION).
  2. 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'.
  3. ob_start() begins capturing all output.
  4. include getZAPTemplate($this->template) includes the file tpl/{name}.tpl. The included file inherits the current symbol table, so it can use $ver, $folder, $words, etc. directly.
  5. ob_get_clean() retrieves the captured HTML and closes the buffer.
  6. getContent() returns the accumulated output string, which ZAPController::fetch() returns to index.php for print().

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:

  1. Create the ZAP{Mode} handler in lib/. The class must be named ZAP{Mode} (case-sensitive, PascalCase after the prefix) and must implement a getContent() 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;
        }
    }
    
  2. 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>
    
  3. Use the route: Navigate to index.php?mode=example. The autoloader finds ZAPExample in lib/, the front controller instantiates it, and getZAPTemplate() loads tpl/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:

  1. Create a form in your .tpl that POSTs to index.php?mode=yourmode.
  2. Create a ZAPYourMode handler in lib/ that reads $_POST values in its constructor and stores them in $_SESSION.
  3. Write a js/your.js file with a validation function, and include <script src="js/your.js"></script> in the template's <head>.
  4. 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 via ImgIOTools::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 onerror handler 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:

  1. body onload="go(this,['img1','img2'],5000)" starts the loop with a 5s interval between successive fetches.
  2. Two <img> tags (#img1 and #img2) are used — one is visible, the other hidden.
  3. 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 5s delay (setTimeout), the newly hidden image's src is set to index.php?mode=zap&i={counter}.
  4. Each request to mode=zap returns raw image data (a Content-Type: image/jpeg response). The browser renders it into the <img> tag, triggering onload again and continuing the cycle.
  5. When ZAPZap finishes all collages, it destroys the session and returns an HTML error string instead of image data. The <img> tag fails to load an image, triggering onerror, which replaces the image area with a "Ready" link.

To reproduce this pattern in a new handler:

  1. Create a template with two <img> elements and <body onload="go(this,['id1','id2'],intervalMs)">.
  2. Include js/display.js (or copy the pattern into your own JS file).
  3. Create a ZAP{Mode} handler that returns raw image data (set Content-Type headers and use echo with readfile() or Imagick's getImageBlob()).
  4. Signal completion by returning a non-image response that will trigger onerror on the <img> tag.

Summary: Creating a New Feature End-to-End

To add a completely new feature to ZapMachine, follow these steps:

  1. Add configuration in conf/conf.php (constants, engine names) if needed.
  2. Create the handler lib/ZAP{Mode}.class.php with getContent() returning a string.
  3. Create the template tpl/{mode}.tpl using the ob_start/include/extract/ob_get_clean pattern.
  4. 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 with onload/onerror.
  5. Create a JS file in www/js/ and reference it from the template's <head>.
  6. Test the route at index.php?mode={ModeName}.
  7. Test from CLI using the bootstrap pattern in cli_scripts/test.php (remember chdir(__DIR__) + manual Session::start() if needed).