Compare commits

..

3 commits

Author SHA1 Message Date
d971512a31 added framework_guide to docs 2026-07-17 01:52:21 +02:00
856d380fb7 adjusted title in README; kanban and pi-tasks added to .gitignore 2026-07-17 01:47:49 +02:00
fb0321bfd5 README: replace outdated how-it-works with ZAP Framework Guide (todo #9)
Replaces the legacy 'how it works' section (which referenced non-existent
index.html, zap.php, display.inc.php, init.php) with a comprehensive
'ZAP Framework Guide' documenting the full application architecture:

- Front-controller routing (?mode=X -> ZAPController -> ZAP{Mode})
- PSR-0 autoloader in conf/init.php
- Config singleton pattern
- Session management (verbatim  keys)
- Plugin-based search engines (Search interface, Brave)
- Image processing pipeline (ImgSearch, ImgProcess, ImgIOTools, ImgTools)
- CLI bootstrap pattern (chdir + require_once, session_start skipped in CLI)
- Logging via Log class

Template rendering pipeline documented with:
- ob_start/include/extract/ob_get_clean pattern
- getZAPTemplate() helper
- Step-by-step guide for adding new templates

Interactive elements documented:
- Pattern 1: Form with client-side validation (index.tpl + js/main.js)
- Pattern 2: Polling image display (display.tpl + js/display.js, onload-driven
  two-img swap with 5s cadence)

Includes end-to-end summary for creating new features.
2026-07-17 01:21:19 +02:00
3 changed files with 485 additions and 18 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
sessions/*
ignore
kanban/
.pi-tasks

View file

@ -2,9 +2,9 @@
Revival of the old ZapMachine. A collage machine that merges random images layer by layer remaining about 50% of the original images
########################################################
#### ApFab - Zap Machine V0.9.0 ###
########################################################
## ApFab - Zap Machine V0.9.0
🄯 2009 - 2026 Fabian de Boer, Vincent Bruijne
www.apfab.com
@ -28,21 +28,7 @@ You need to acquire the following API key and put it into `conf/conf.php`:
See `conf/conf.php` for the exact constant names to use.
Adjust permission (chmod 777) for Sessions folder, run index.html, fill in the form and voila.
# how it works
- index.html contains user interface
- user input put into Session variables in init.php
- display.inc.php dynamically displays two img tags that are loaded in turn with image data from zap.php using display.js
- zap.php downloads images one by one using imgSearch Class
- zap.php processes collages one by one using imgProcess Class
- All downloaded images, collage images and logfiles(feature is far from ready yet) are put in a folder named 'Session/zap'.date()
- the machine is ready when an error is loaded into img document class. (a weak point in our construction)
Adjust permission (chmod 777) for 'sessions' folder, run index.php in the www/ directory, fill in the form and voila.
# yet to come
- Logfile also generated in imgProcess Class (0.9.2)

479
docs/framework_guide.md Normal file
View file

@ -0,0 +1,479 @@
# 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=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.
```php
// 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()`:
```php
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`:
```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:
```php
$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:
```php
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](#cli-bootstrap) below.
---
### Plugin-Based Search Engines
Search engines implement the `Search` interface (`resource/Search.interface.php`):
```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:
```php
$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:
```php
$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`:**
```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.
```php
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:
```php
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
```php
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
<?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`:
```php
<!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:
```javascript
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`):**
```javascript
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).