--- # Documentation: Async As PHP is inherently synchronous, running code asynchronously requires a background loop that observes and dispatches events while handling promise resolutions. PHP.GT/Async introduces an event loop, timers and a promise-based approach to handling deferred operations. This package provides: - `Loop`, which waits until work is due and then dispatches it - `IndividualTimer`, for one-off trigger times - `PeriodicTimer`, for repeated trigger times - `Event` and `EventDispatcher`, for lightweight publish-subscribe messaging - Loop support for tracking `Deferred` objects from `phpgt/promise` The package is standalone. We can use it in any PHP project with Composer. WebEngine also depends on this package, but the classes documented here can still be used directly outside WebEngine. ## A minimal example ```php use Gt\Async\Loop; use Gt\Async\Timer\IndividualTimer; $loop = new Loop(); $timer = new IndividualTimer(2); $timer->addCallback(function() use($loop) { echo "Two seconds have passed.", PHP_EOL; $loop->halt(); }); $loop->addTimer($timer); $loop->run(); ``` Here we schedule one callback to run two seconds in the future. Once it has run, the callback halts the loop. --- Start with [[Getting started]] to build a working loop from scratch. In this guide we will install the package, create a loop, add timers, and stop the loop cleanly. ## 1. Install the package ```bash composer require phpgt/async ``` `phpgt/promise` is installed as a dependency, so the loop is ready for the deferred work covered later in this guide. ## 2. Create a loop ```php use GT\Async\Loop; $loop = new Loop(); ``` Nothing happens yet because the loop has no timers to observe. ## 3. Add a one-off timer `IndividualTimer` schedules one or more exact trigger times. ```php use GT\Async\Timer\IndividualTimer; $timer = new IndividualTimer(1.5); $timer->addCallback(function() { echo "First callback", PHP_EOL; }); $loop->addTimer($timer); ``` Passing `1.5` to the constructor means "run once, one and a half seconds from now". ## 4. Add a repeating timer `PeriodicTimer` schedules callbacks every fixed number of seconds. ```php use Gt\Async\Timer\PeriodicTimer; $count = 0; $periodicTimer = new PeriodicTimer(0.5, true); $periodicTimer->addCallback(function() use(&$count, $loop) { $count++; echo "Tick $count", PHP_EOL; if($count === 3) { $loop->halt(); } }); $loop->addTimer($periodicTimer); ``` The second constructor argument controls the first tick: - `true` means the first tick is immediate - `false` means the first tick happens after the first period has passed ## 5. Run the loop ```php $loop->run(); ``` The loop will keep going until one of these things happens: - there are no more scheduled timers - one of the callbacks calls `$loop->halt()` - deferred completion tracking halts the loop, which we will cover later ## 6. Run only one pass If we call `run(false)`, the loop performs one pass over the timers that are currently due and then returns. ```php $loop->run(false); ``` This is useful when another part of the application controls the outer execution flow. --- Next, continue with [[Timers]] to see how the two timer types behave in more detail. Timers are the objects the loop watches. Each timer keeps a queue of trigger times, and each time a timer is due it runs every callback attached to it. ## IndividualTimer `IndividualTimer` is for one-off trigger times. We can schedule a relative delay in the constructor: ```php use GT\Async\Timer\IndividualTimer; $timer = new IndividualTimer(5); ``` That means "trigger once, five seconds from now". We can also add absolute trigger times: ```php $start = microtime(true); $timer = new IndividualTimer(); $timer->addTriggerTime($start + 1); $timer->addTriggerTime($start + 5); $timer->addTriggerTime($start + 10); ``` `addTriggerTime()` expects a Unix epoch float, not a delay value. ## PeriodicTimer `PeriodicTimer` is for repeated work at a fixed interval. ```php use Gt\Async\Timer\PeriodicTimer; $timer = new PeriodicTimer(0.25, true); ``` This timer will trigger every quarter of a second, and because the second argument is `true`, the first tick is immediate. If we want the timer to wait before the first tick, use: ```php $timer = new PeriodicTimer(0.25, false); ``` ## Adding and removing callbacks Any timer can hold more than one callback: ```php $timer->addCallback(function() { echo "A", PHP_EOL; }); $timer->addCallback(function() { echo "B", PHP_EOL; }); ``` When the timer ticks, both callbacks run in the order they were added. Callbacks can also be removed: ```php $callback = function() { echo "Only once", PHP_EOL; }; $timer->addCallback($callback); $timer->removeCallback($callback); ``` ## Running several timers together The loop can observe any number of timers at once. ```php use GT\Async\Loop; use GT\Async\Timer\IndividualTimer; use GT\Async\Timer\PeriodicTimer; $loop = new Loop(); $stopTimer = new IndividualTimer(5); $countdownTimer = new PeriodicTimer(1, true); $dotTimer = new PeriodicTimer(0.1, true); $count = 5; $countdownTimer->addCallback(function() use(&$count) { echo "Countdown: $count", PHP_EOL; $count--; }); $dotTimer->addCallback(function() { echo "."; }); $stopTimer->addCallback(function() use($loop) { echo PHP_EOL, "Stop", PHP_EOL; $loop->halt(); }); $loop->addTimer($countdownTimer); $loop->addTimer($dotTimer); $loop->addTimer($stopTimer); $loop->run(); ``` This is the main idea behind the package: small pieces of work can take turns without us having to block on one long operation. --- Next, move on to [[Deferred work]] to connect the loop to `Deferred` objects and promises. Async depends on [`phpgt/promise`](https://www.php.gt/promise/), so the loop can track `Deferred` objects while their work is being processed in small steps. This is useful when a task should not block the whole script. Instead of reading a file, stream, or queue item all at once, we can process a small piece on each tick and resolve the promise when the task is complete. ## The preferred approach: track completion only If another part of your code already decides when work should be processed, use `trackDeferred()`: ```php use GT\Async\Loop; use GT\Promise\Deferred; $loop = new Loop(); $deferred = new Deferred(); $loop->trackDeferred($deferred); $loop->haltWhenAllDeferredComplete(true); ``` This tells the loop: - this deferred task is part of the loop lifecycle - remove it from tracking when it completes - halt the loop when all tracked deferred tasks have completed `trackDeferred()` does **not** attach any process callbacks to a timer. ## Attaching deferred processing to a timer If we do want the loop to attach a deferred task's process callbacks to a timer, `addDeferredToTimer()` is available: ```php use GT\Async\Timer\PeriodicTimer; use GT\Promise\Deferred; $timer = new PeriodicTimer(0.1, true); $deferred = new Deferred(); $deferred->addProcess(function() { // Process one small step here. }); $loop->addTimer($timer); $loop->addDeferredToTimer($deferred, $timer); ``` > [!WARNING] > `addDeferredToTimer()` is deprecated. Prefer `trackDeferred()` unless you specifically need the loop to wire the process callbacks onto a timer for you. ## Halting when all deferred work is complete The loop can halt itself when the tracked deferred list becomes empty: ```php $loop->haltWhenAllDeferredComplete(true); $loop->addHaltCallback(function() { echo "All deferred work is complete.", PHP_EOL; }); ``` This is useful when the loop should stay alive only for as long as there is outstanding asynchronous work. ## A small example ```php use GT\Async\Loop; use GT\Async\Timer\PeriodicTimer; use GT\Promise\Deferred; $loop = new Loop(); $timer = new PeriodicTimer(0.1, true); $stepsRemaining = 5; $deferred = new Deferred(); $deferred->addProcess(function() use(&$stepsRemaining, $deferred) { echo "."; $stepsRemaining--; if($stepsRemaining === 0) { $deferred->resolve("Complete"); } }); $deferred->getPromise()->then(function(string $message) { echo PHP_EOL, $message, PHP_EOL; }); $loop->addTimer($timer); $loop->addDeferredToTimer($deferred, $timer); $loop->haltWhenAllDeferredComplete(true); $loop->run(); ``` Each timer tick advances the task by one step. When the deferred resolves, the loop removes it from tracking and halts because there is no more deferred work left. --- Next, continue with [[Events]] for the package's publish-subscribe classes. Alongside the loop and timers, this package includes a small in-memory event system. There are only two classes involved: - `Event`, which carries an event name and optional detail data - `EventDispatcher`, which stores subscribers by event name and publishes events to them ## Subscribing to an event ```php use GT\Async\Event\Event; use GT\Async\Event\EventDispatcher; $dispatcher = new EventDispatcher(); $dispatcher->subscribe("tick", function(Event $event) { echo "Received: ", $event->getName(), PHP_EOL; }); ``` ## Publishing an event ```php $dispatcher->publish(new Event("tick")); ``` That will invoke every callback subscribed to `"tick"`, in the same order they were added. ## Passing detail data The `Event` object can carry any detail value: ```php $dispatcher->subscribe("message", function(Event $event) { echo $event->getDetail(), PHP_EOL; }); $dispatcher->publish(new Event("message", "Hello")); ``` ## What this dispatcher does not do This is a deliberately small utility. It does not include: - wildcard subscriptions - unsubscribe methods - cross-process or network delivery If you need those features, they would need to be built on top. --- Finally, take a look at [[Examples]] for the bundled scripts in this repository. This repository includes three small example scripts in the `example/` directory. ## Delays `01-delay-1-5-10-seconds.php` uses one `IndividualTimer` with three trigger times. It shows: - adding absolute trigger times with `addTriggerTime()` - attaching one callback that runs on every trigger - running the loop until no scheduled work remains ## Countdown `02-countdown.php` combines: - an `IndividualTimer` that halts the loop after five seconds - a `PeriodicTimer` that prints a countdown once per second - another `PeriodicTimer` that prints dots more frequently This is a good example of several timers sharing one loop. ## Promise-based work `03-promise.php` wraps a slow file reader in a class that returns a promise. It shows: - adding deferred process callbacks - attaching that deferred work to a timer - resolving promises when the work is complete - halting the loop automatically when all deferred work is done The example uses `addDeferredToTimer()`, which still works, but remember that the method is deprecated in favour of `trackDeferred()` when callback attachment is not needed. --- # Documentation: Build PHP.GT Build provides a system to describe client-side build work in one place and run it from PHP projects without introducing another package manager or task runner of its own. The library does two jobs: - checks that the commands the project depends on are installed and meet the version constraints - runs the relevant commands when matching source files exist, either once or continuously in watch mode Build does not install or manage front-end tooling for us. We can keep using whatever tooling is preferred, whether that means `node_modules/.bin`, `vendor/bin`, or programs installed elsewhere on the system. ## What the configuration looks like The recommended format is `build.ini`. Each section name is a file pattern, and each section describes the command to run for that pattern. `build.ini`: ```ini [script/**/*.es6] name=Bundle JavaScript require=node_modules/.bin/esbuild >=0.17, node >=18 execute=./node_modules/.bin/esbuild ./script/app.es6 --bundle --sourcemap --outfile=./www/script.js [style/**/*.scss] name=Compile Sass require=sass >=1.6 execute=sass ./style/style.scss ./www/style.css [asset/**/*] name=Publish static assets require=vendor/bin/sync ^1.3 execute=vendor/bin/sync ./asset ./www/asset --symlink ``` When we run `vendor/bin/build`, Build checks the requirements first and then executes each task whose glob matches at least one file. ## INI is now the default Build still supports `build.json`, but `build.ini` is usually more compact for this kind of command-focused configuration. JSON support remains available for projects that already use it, or for cases where a nested object structure is preferred. There's a separate [[JSON configuration example]] page showing the equivalent layout. ## A few details worth knowing early - If both `build.ini` and `build.json` exist in the same directory, Build uses `build.ini`. - Mode files work with both formats: `build.production.ini` or `build.production.json`. - The `require` line is optional, but it is worth using so problems are detected before a build starts. - Task names are optional. If omitted, the command itself is used in log output. --- Start with [[command line usage]] to see how Build finds configuration files and how the command-line options fit together. Build is intended to be run from the command line. When the package is installed in a project, the executable is available at: ```bash vendor/bin/build ``` > [!NOTE] > In WebEngine applications, the same functionality is also exposed through: > > ```bash > gt build > ``` By default, Build looks in the current working directory and prefers these filenames in this order: 1. `build.ini` 2. `build.json` That means if both files exist, `build.ini` is the one that will be used. If we want to point somewhere else, we can use `--config` or `-c`. Examples: ```bash vendor/bin/build --config ./config/build.ini vendor/bin/build --config ./tools/build.json vendor/bin/build --config ./some/project/directory ``` If the path passed to `--config` is a directory, Build looks inside that directory for `build.ini` first, then `build.json`. ## Running once Running the command without `--watch` performs one pass: ```bash vendor/bin/build ``` For each task, Build: 1. loads the configuration 2. checks the declared requirements 3. skips tasks whose file pattern matches nothing 4. runs the command for tasks with matching files Build prints a timestamped line for each successful task and then reports the total elapsed time. ## Watching for changes To keep Build running and rebuild when source files change, use `--watch` or `-w`: ```bash vendor/bin/build --watch ``` In watch mode, Build remembers the modification time of each matched file and reruns only the tasks whose matching files changed. This is usually the command we keep running during local development. ## Build modes Mode files let us override part of the base configuration for a specific environment. ```bash vendor/bin/build --mode production vendor/bin/build -m dev ``` When the base file is `build.ini`, Build looks for files such as `build.production.ini`. When the base file is `build.json`, Build looks for files such as `build.production.json`. Only the properties that change need to be present in the mode file. The base and mode configuration are merged before the build runs. The full behaviour is covered on [[different environments]]. ## Default configuration Frameworks and shared project setups can provide a fallback configuration by using `--default` or `-d`. ```bash vendor/bin/build --default ./config/build.default.ini vendor/bin/build --default ./framework/build ``` The default path may point to either a file or a directory. If it points to a directory, the same `build.ini` then `build.json` lookup order is used. Build first looks for the project configuration. If none is found, it falls back to the default path. The override model is explained on [[default config]]. --- Next, read [[build tasks]] for the structure of individual task definitions. Each build task starts with a file pattern. When that pattern matches one or more files, Build knows which command to run. Examples: - `images/*.jpg` matches JPEG files directly inside `images/` - `script/**/*.es6` matches ES6 files anywhere under `script/` - `asset/**/*` matches every file under `asset/` For one-off builds, Build checks each pattern in turn and runs the matching tasks once. For watch mode, Build keeps checking the same patterns and reruns tasks when the modification time of a matched file changes. ## INI task format In `build.ini`, each task is a section: ```ini [script/**/*.es6] name=Bundle JavaScript require=node_modules/.bin/esbuild >=0.17, node >=18 execute=./node_modules/.bin/esbuild ./script/app.es6 --bundle --sourcemap --outfile=./www/script.js ``` The section name is the glob pattern. The supported keys are: - `name`: optional label used in success output - `require`: optional comma-separated list of command/version pairs - `execute`: required command line to run If `name` is omitted, Build logs the command instead. If `require` is omitted, the task still runs, but Build will not check the environment first. ## The `require` line The `require` line is a comma-separated list. Each item contains a command name followed by an optional version constraint. ```ini require=node >=18, sass >=1.6, vendor/bin/sync ^1.3 ``` These version constraints use Composer semver rules. If the version is omitted, it defaults to `*`, which means “the command only needs to exist”. ```ini require=esbuild, vendor/bin/sitemap ^1.0 ``` ## The `execute` line The `execute` line is split into the command and its arguments. ```ini execute=sass ./style/style.scss ./www/style.css ``` Quoted arguments are supported: ```ini execute=./node_modules/.bin/esbuild "script/my cool script.es6" --bundle --outfile=./www/script.js ``` There are a few parser rules to be aware of: - `execute` is required - the command part may not be empty - quoted arguments must be terminated correctly - the line may not contain `;`, `#`, or line breaks Those restrictions keep the configuration focused on one command invocation per task. ## Full INI example ```ini [script/**/*.es6] name=Bundle JavaScript require=node_modules/.bin/esbuild >=0.17, node >=18 execute=./node_modules/.bin/esbuild ./script/app.es6 --bundle --sourcemap --outfile=./www/script.js [style/**/*.scss] name=Compile Sass require=sass >=1.6 execute=sass ./style/style.scss ./www/style.css [page/**/*.php] name=Generate sitemap require=vendor/bin/sitemap ^1.0 execute=php vendor/bin/sitemap src/page www/sitemap.xml ``` ## JSON is still supported If a project already uses JSON, the same concepts are available through `build.json`, with `require` and `execute` written as nested objects. See [[JSON configuration example]] for a complete example. --- Next, move on to [[different environments]] to see how task definitions are overridden per mode. One project often needs slightly different build steps in different places. A local development build may generate source maps, while a production build may omit them. Build handles this through _modes_. ## How modes are named The mode name is inserted before the file extension: - `build.ini` + mode `production` becomes `build.production.ini` - `build.json` + mode `production` becomes `build.production.json` To use a mode: ```bash vendor/bin/build --mode production vendor/bin/build -m dev ``` Build loads the base configuration first, then loads the mode file, then recursively merges the two. ## INI example Base configuration: `build.ini` ```ini [script/**/*.es6] name=Bundle JavaScript require=node_modules/.bin/esbuild >=0.17 execute=./node_modules/.bin/esbuild ./script/app.es6 --bundle --sourcemap --outfile=./www/script.js [style/**/*.scss] name=Compile Sass require=sass >=1.6 execute=sass ./style/style.scss ./www/style.css ``` Production override: `build.production.ini` ```ini [script/**/*.es6] name=Bundle JavaScript for production execute=./node_modules/.bin/esbuild ./script/app.es6 --bundle --minify --outfile=./www/script.js ``` The production file only contains the task that changes. Running `vendor/bin/build --mode production` keeps the Sass task from `build.ini`, but replaces the `name` and `execute` values of the JavaScript task with those from `build.production.ini`. ## Partial overrides are allowed Mode files do not need to repeat the whole task definition. For example, if we only want to change the arguments in an execute command, we can override just that property and leave the requirements and block name alone. ## Adding tasks in a mode A mode file can also introduce brand new tasks. For example, a `build.dev.ini` file might add a sitemap generator or some diagnostic step that only runs during development. ## Missing mode files If we ask for a mode and the corresponding file does not exist, Build stops with an error rather than silently continuing with the wrong configuration. That makes deployment mistakes more obvious when they happen. --- Next, read [[default config]] to see how shared defaults and project-specific overrides fit together. Default configuration lets a framework, starter project, or shared internal tool provide a base set of build tasks without forcing every project to copy the same file. If the project does not have its own build file, Build can fall back to the path supplied by `--default`. ```bash vendor/bin/build --default ./config/build.default.ini ``` The default path may be either: - a specific file such as `./config/build.default.ini` - a directory containing `build.ini` or `build.json` ## How Build chooses between project and default config Build checks for the project configuration first. If a project configuration is found, that file is used. If no project configuration is found, Build tries the default path. This makes defaults a good fit for frameworks and skeleton applications: projects can accept the shared configuration as-is, or add their own file when they need to customise it. ## Overriding shared defaults Once a project supplies its own `build.ini` or `build.json`, that project file becomes the active configuration. This is useful when the shared default covers most projects, but one application needs a different entry point, a production-only optimisation, or an extra task for a particular directory. Mode files continue to work in the usual way on top of whichever base configuration is active. ## WebEngine WebEngine exposes Build through `gt build`, so it can provide framework-level defaults while still allowing application projects to define their own configuration when required. The Build repository is maintained separately, so the behaviour described here applies whether Build is used inside WebEngine or on its own. --- From here, continue to see a [[JSON configuration example]]. Build now recommends `build.ini`, but JSON remains fully supported. The JSON format represents the same ideas with nested objects: - the top-level keys are glob patterns - each task may contain `name`, `require`, and `execute` - `execute` contains `command` and `arguments` ## Example `build.json` ```json { "script/**/*.es6": { "name": "Bundle JavaScript", "require": { "node_modules/.bin/esbuild": ">=0.17", "node": ">=18" }, "execute": { "command": "./node_modules/.bin/esbuild", "arguments": [ "./script/app.es6", "--bundle", "--sourcemap", "--outfile=./www/script.js" ] } }, "style/**/*.scss": { "name": "Compile Sass", "require": { "sass": ">=1.6" }, "execute": { "command": "sass", "arguments": [ "./style/style.scss", "./www/style.css" ] } }, "asset/**/*": { "name": "Publish static assets", "require": { "vendor/bin/sync": "^1.3" }, "execute": { "command": "vendor/bin/sync", "arguments": [ "./asset", "./www/asset", "--symlink" ] } } } ``` ## JSON and INI side by side The same JavaScript task written in INI looks like this: ```ini [script/**/*.es6] name=Bundle JavaScript require=node_modules/.bin/esbuild >=0.17, node >=18 execute=./node_modules/.bin/esbuild ./script/app.es6 --bundle --sourcemap --outfile=./www/script.js ``` The behaviour is the same. The choice is mainly about which format fits the project better. ## Mode files in JSON Mode files work in exactly the same way as with INI. Base file: `build.json` ```json { "script/**/*.es6": { "name": "Bundle JavaScript", "execute": { "command": "./node_modules/.bin/esbuild", "arguments": ["./script/app.es6", "--bundle", "--sourcemap", "--outfile=./www/script.js"] } } } ``` Override file: `build.production.json` ```json { "script/**/*.es6": { "name": "Bundle JavaScript for production", "execute": { "arguments": ["./script/app.es6", "--bundle", "--minify", "--outfile=./www/script.js"] } } } ``` When we run `vendor/bin/build --mode production`, Build merges the two objects and keeps any properties not overridden by the mode file. --- # Documentation: Cipher Two-way encryption of messages for secure plain text transmission. When messages are passed between two systems over a public network, encryption tools are needed to protect the content in transit. Encrypting and decrypting messages correctly can be fiddly and error-prone, so this library keeps the process small and explicit through the `PlainTextMessage`, `EncryptedMessage`, `CipherText`, `Key`, and `InitVector` abstractions. Pass your secret message to the `PlainTextMessage` constructor, then call `encrypt()` with a shared `Key` to produce a `CipherText`. The encrypted payload is represented by the cipher text itself plus the IV returned by `getIv()`. Those values can then be passed to the receiver by any communication mechanism, with only the holder of the same shared key able to decrypt the original message. On the receiving side, construct an `EncryptedMessage` with the incoming cipher text and IV, then call `decrypt()` with the same `Key` to recover the original plain text. The `CipherText` class also provides a `getUri()` method for creating a pre-encoded URI. A URI containing `cipher` and `iv` query string parameters can then be passed to `EncryptedUri` and decrypted back into a `PlainTextMessage`. ## A very small example ```php use GT\Cipher\Key; use GT\Cipher\Message\EncryptedMessage; use GT\Cipher\Message\PlainTextMessage; $key = new Key(); $plainText = new PlainTextMessage("Hello, PHP.GT!"); $cipherText = $plainText->encrypt($key); $received = new EncryptedMessage((string)$cipherText, $plainText->getIv()); echo $received->decrypt($key); ``` The message starts as plain text, is encrypted with a shared key, then reconstructed and decrypted on the receiving side using the same key and IV. --- To see where each class fits before wiring anything up, continue to [[Overview]]. Cipher is intentionally small. Rather than exposing a large encryption framework, it wraps one clear workflow around libsodium's `secretbox` functions. The main idea is simple: 1. We start with a plain text message. 2. We encrypt it using a shared secret key and an initialisation-vector (IV). 3. We transmit the cipher text and IV. 4. We reconstruct the encrypted message on the receiving side. 5. We decrypt it using the same shared key. ## The main objects ### `Key` `Key` holds the shared secret bytes. If we do not pass bytes into the constructor, a secure random key is generated for us. Casting the object to a string gives us a base64 representation, which is convenient for storage or transport: ```php $key = new GT\Cipher\Key(); echo (string)$key; ``` That string form is for transport or display. The actual encryption methods use the raw bytes internally. ### `InitVector` Every message needs an IV. In this library, the `InitVector` is generated automatically when a message is constructed, unless we pass one in ourselves. The IV is not secret, but it is still required for decryption. If we send cipher text to another system, we must send the IV alongside it. > [!NOTE] > In cryptography, an Initialisation Vector is sometimes also known as a "nonce" [See wikipedia's page for more information](https://en.wikipedia.org/wiki/Cryptographic_nonce). ### `PlainTextMessage` This is the object we construct when we have clear text data and want to encrypt it. ```php use GT\Cipher\Message\PlainTextMessage; $message = new PlainTextMessage("Quarterly figures attached"); ``` Calling `encrypt()` returns a `CipherText` object: ```php $cipherText = $message->encrypt($key); ``` ### `CipherText` `CipherText` represents the encrypted bytes. Casting it to a string gives the base64 encoded cipher text that we would usually transmit to another system. It also has `getUri()`, which appends the cipher text and IV to a URI as `cipher` and `iv` query string parameters. ### `EncryptedMessage` This is the receiving-side counterpart to `PlainTextMessage`. We construct it from the incoming cipher text and IV, then call `decrypt()` with the same key. ### `EncryptedUri` If the sender chose to transmit the encrypted message in a query string, `EncryptedUri` can read the URI, extract the `cipher` and `iv` values, and decrypt them in one step. ## What Cipher does not try to do This library does not handle: - key exchange between systems - public/private key cryptography - long-term secret storage - signing or authenticity checks beyond what `secretbox` itself provides - message serialisation beyond strings and query string transport That is deliberate. Cipher keeps one common secret-key message flow small, readable, and easy to reuse. ## Choosing the right entry point - Use `PlainTextMessage` and `EncryptedMessage` when you are sending cipher text and IV separately. - Use `CipherText::getUri()` and `EncryptedUri` when you are sending encrypted data through a link or redirect. - Use `Key` directly when you need to generate, serialise, or reconstruct a shared key. --- Now that the shape of the library is clear, move on to [[Getting started]]. This guide shows the smallest useful setup for encrypting and decrypting one message. ## 1. Install the package ```bash composer require phpgt/cipher ``` The package requires the `sodium` extension. On current PHP versions this is usually available already, but it still needs to be enabled in the runtime environment. ## 2. Create a shared key Cipher uses symmetric encryption, so both sides must know the same key. ```php use GT\Cipher\Key; $sharedKey = new Key(); ``` If one side generates the key, the other side must receive the same bytes through a secure channel. ## 3. Create a plain text message ```php use GT\Cipher\Message\PlainTextMessage; $message = new PlainTextMessage("Hello from Cipher"); ``` At this point the message also has an IV attached internally. ## 4. Encrypt the message ```php $cipherText = $message->encrypt($sharedKey); ``` Now we have three important pieces of data: - the key - the IV - the cipher text The key must remain secret. The IV and cipher text can be transmitted to the receiver. For example, the sender might transmit these text-safe values: ```php $outgoingCipher = (string)$cipherText; $outgoingIv = (string)$message->getIv(); ``` ## 5. Reconstruct the encrypted message on the receiving side ```php $encryptedMessage = new EncryptedMessage($incomingCipher, $incomingIv); ``` The `EncryptedMessage` constructor takes the incoming base64 cipher string plus the incoming base64 IV. When the IV is supplied as a string, it is converted internally into an `InitVector` from base64. ## 6. Decrypt using the same key ```php $decryptedMessage = $encryptedMessage->decrypt($sharedKey); echo $decryptedMessage; ``` The output is a new `PlainTextMessage`, so it can be cast straight to a string. ## Full example ```php use GT\Cipher\Key; use GT\Cipher\Message\EncryptedMessage; use GT\Cipher\Message\PlainTextMessage; $sharedKey = new Key(); $message = new PlainTextMessage("Hello from Cipher"); $cipherText = $message->encrypt($sharedKey); $cipherString = (string)$cipherText; $ivString = (string)$message->getIv(); // Send $outgoingCipher and $outgoingIv to the receiver... // ... then on the receiver: $encryptedMessage = new EncryptedMessage( $cipherString, $ivString, ); echo $encryptedMessage->decrypt($sharedKey); ``` ## A note on sharing the key In the example above, both sides are in the same PHP script just to keep the flow visible. In a real system: - the sender and receiver are usually separate processes or separate machines - both sides must already know the same secret key - the key should not be transmitted in the same channel as the encrypted payload --- With the basic flow working, continue to [[Encrypting and decrypting messages]] for a closer look at each step. Once the basic setup is in place, the normal message flow is built around `PlainTextMessage`, `CipherText`, and `EncryptedMessage`. ## Encrypting a plain text message The sender starts by creating a `PlainTextMessage`: ```php use GT\Cipher\Message\PlainTextMessage; $message = new PlainTextMessage("Launch code: tea and biscuits"); ``` Calling `encrypt()` produces a `CipherText` object: ```php use GT\Cipher\Key; $key = new Key(); $cipherText = $message->encrypt($key); ``` At that point: - `(string)$cipherText` is the base64 encoded cipher string - `$message->getIv()` returns the `InitVector` used for encryption The `CipherText` object itself keeps the raw encrypted bytes, but it is usually the base64 form that we transmit. ## What needs to be transmitted To decrypt successfully, the receiving side needs: - the cipher text - the IV - the same shared key Only the key must remain secret. The IV is part of the normal payload. ## Decrypting an incoming message On the receiving side we construct `EncryptedMessage` using the incoming cipher text string and IV: ```php $received = new EncryptedMessage( $cipherString, $ivString, ); ``` Here the IV is shown in its transmitted base64 string form. `EncryptedMessage` will convert it internally into an `InitVector`. Then decrypt using the same key: ```php $plainText = $received->decrypt($key); echo $plainText; ``` The return value is a `PlainTextMessage`, not a raw string. That keeps the API symmetric and lets us keep using the same message abstraction if needed. ## Supplying your own IV `PlainTextMessage` creates a fresh IV by default, but we can also provide one explicitly: ```php use GT\Cipher\InitVector; use GT\Cipher\Message\PlainTextMessage; $iv = new InitVector(); $message = new PlainTextMessage("Fixed IV example", $iv); ``` In normal usage, letting the library generate the IV is the safest and simplest option. ## String representations The library uses string casting consistently: - `(string)$message` gives the plain text content - `(string)$cipherText` gives the base64 cipher text - `(string)$message->getIv()` gives the base64 IV - `(string)$key` gives the base64 key That makes it straightforward to serialise values into JSON, query strings, logs, or storage layers. We should still take care never to expose the key in logs or browser-visible output. ## Receiving from another process If the receiver is reconstructing the key from a stored base64 string, decode it first: ```php use GT\Cipher\Key; $storedBase64Key = "…"; $key = new Key(base64_decode($storedBase64Key)); ``` If the IV was also received as a base64 string, it can usually be passed straight into `EncryptedMessage`: ```php $received = new EncryptedMessage($cipherString, $storedBase64Iv); ``` If you need an `InitVector` object separately for some reason, you can still reconstruct it manually: ```php use GT\Cipher\InitVector; $iv = (new InitVector())->withBytes(base64_decode($storedBase64Iv)); ``` That gives us both options: the simple string-based constructor path for normal receiving code, and the explicit `InitVector` reconstruction when an object is needed elsewhere. --- If your transport mechanism is a URL rather than separate fields, continue to [[Encrypted URIs]]. One of the main usages of this library is the URI workflow. Instead of transmitting cipher text and IV as separate values, we can embed them directly into a URI. This is useful for: - redirects between applications - links sent between trusted systems - callback URLs that need to carry a small encrypted payload ## Creating an encrypted URI Start with a normal encrypted message flow: ```php use GT\Cipher\Key; use GT\Cipher\Message\PlainTextMessage; $key = new Key(); $message = new PlainTextMessage("Signed in as account 105"); $cipherText = $message->encrypt($key); ``` Then append the encrypted values to a base URI: ```php $uri = $cipherText->getUri("https://example.com/receiver.php"); echo $uri; ``` The returned URI will contain two query string parameters: - `cipher` - `iv` If the base URI already has a query string, those values are added alongside the existing parameters. ## Decrypting from the incoming URI On the receiving side, construct `EncryptedUri` from the request URI: ```php use GT\Cipher\EncryptedUri; $encryptedUri = new EncryptedUri((string)$uri); $plainText = $encryptedUri->decryptMessage($key); echo $plainText; ``` `decryptMessage()` returns a `PlainTextMessage`. ## Using custom query string parameter names If `cipher` and `iv` do not fit the surrounding application, we can supply custom names: ```php $encryptedUri = new EncryptedUri( $requestUri, "payload", "rand", ); ``` The constructor then expects those parameter names instead of the defaults. ## Missing parameter behaviour If either required parameter is absent or empty, Cipher throws `MissingQueryStringException`. That helps distinguish: - malformed links - tampered links - requests that were never meant for this decrypting endpoint ## URI safety notes Encrypting a message does not make a URI invisible. The full URL may still be stored in: - browser history - access logs - proxy logs - analytics tools Cipher protects the message content, but we should still think carefully before placing sensitive data in any URL. For highly sensitive flows, POST bodies or server-side session state may still be the better transport choice. --- For a closer look at the small helper objects used throughout the library, continue to [[Key and IV objects]]. `Key` and `InitVector` are deliberately tiny classes, but they are central to how the rest of the library stays readable. ## `Key` `Key` wraps the raw bytes used as the shared secret. ### Creating a new random key ```php use GT\Cipher\Key; $key = new Key(); ``` With no constructor argument, Cipher generates cryptographically secure random bytes of the correct length. ### Reconstructing an existing key ```php $storedBase64 = "…"; $key = new Key(base64_decode($storedBase64)); ``` This is the usual approach when the key comes from configuration, a secrets store, or another trusted process. ### Converting to a string ```php echo (string)$key; ``` Casting to a string returns a base64 representation of the key bytes. That is useful for storage, but the string should still be treated as secret material. ### Accessing raw bytes ```php $bytes = $key->getBytes(); ``` The raw bytes are what libsodium uses internally. ## `InitVector` `InitVector` represents the random data used during encryption and decryption. ### Creating an IV ```php use GT\Cipher\InitVector; $iv = new InitVector(); ``` In practice, we often do not need to do this ourselves because `PlainTextMessage` creates one automatically. ### Converting to a string ```php echo (string)$iv; ``` The string value is base64 encoded, which makes it suitable for transmission in query strings or other text-safe transports. ### Reconstructing from bytes ```php $iv = (new InitVector())->withBytes(base64_decode($storedBase64Iv)); ``` `withBytes()` returns a cloned object with the supplied bytes, leaving the original object untouched. ### Valid byte length If we pass an invalid length less than `1` to the constructor, Cipher throws `CipherException`. ```php new InitVector(0); // throws ``` ## Key and IV responsibilities It is useful to keep their roles separate: - the key is secret and must stay private - the IV is not secret, but it must match the encrypted message - both are required for successful decryption ## A practical pattern One common pattern is: 1. Generate the key once and store it securely. 2. Generate a fresh IV for each message. 3. Transmit the IV with the encrypted payload. 4. Reuse the same key only between the systems that are meant to trust each other. Cipher fits neatly around that model. --- Next, read [[Error handling]] to see how failures are surfaced by the API. ## Base exception All library-specific exceptions extend `GT\Cipher\CipherException`. If we only need one catch block for any Cipher failure, catching that base type is usually enough: ```php use GT\Cipher\CipherException; try { // encrypt or decrypt here } catch(CipherException $exception) { // handle all Cipher-specific failures } ``` ## Encryption failures `CipherText` throws `EncryptionFailureException` when encryption cannot be completed. This generally means libsodium rejected the inputs, such as an invalid key size. ## Message decryption failures `EncryptedMessage::decrypt()` throws `GT\Cipher\Message\DecryptionFailureException` when: - the cipher text is malformed - the key or IV does not match - libsodium rejects the inputs That allows the receiving application to treat the payload as unreadable without exposing any secret detail. ## URI decryption failures `EncryptedUri::decryptMessage()` throws `UriDecryptionFailureException` when the URI contains cipher and IV values but decryption still fails. In practice that often means: - the wrong key was used - the URI was altered - the cipher text was not valid ## Missing URI parameters The `EncryptedUri` constructor throws `MissingQueryStringException` when the required `cipher` or `iv` parameter is missing or empty. The exception message contains the missing parameter name, which is useful when responding with validation errors or debugging integration issues. ## Working with previous exceptions Where libsodium throws internally, Cipher wraps the original throwable as the `previous` exception. That means application code can log the full exception chain during development while still presenting a cleaner message to the end user. ## Typical handling approach For most applications, the following pattern is enough: ```php use GT\Cipher\CipherException; try { $plainText = $encryptedUri->decryptMessage($key); } catch(CipherException $exception) { http_response_code(400); echo "The encrypted message could not be read."; } ``` That keeps the external response calm and predictable without leaking implementation details. --- To see the library running end-to-end, continue to [[Examples]]. The `example/` directory is the quickest way to see the main workflows without building a larger application around them first. Run an example from the repository root: ```bash php example/01-encrypt-decrypt-message.php ``` ## Example index 1. [example/01-encrypt-decrypt-message.php](https://github.com/PhpGt/Cipher/blob/master/example/01-encrypt-decrypt-message.php) Basic message encryption and decryption using `PlainTextMessage` and `EncryptedMessage`. 2. [example/02-encrypt-message-in-uri.php](https://github.com/PhpGt/Cipher/blob/master/example/02-encrypt-message-in-uri.php) Embedding encrypted content into a URI and reading it back with `EncryptedUri`. 3. [example/03-share-key-between-processes.php](https://github.com/PhpGt/Cipher/blob/master/example/03-share-key-between-processes.php) Reconstructing the same shared key from a base64 string on the receiving side. ## Repository root demos There are also a few small scripts in the repository root which show the lower-level building blocks: - [sodium.php](https://github.com/PhpGt/Cipher/blob/master/sodium.php): a direct libsodium demonstration with Cipher's exception types - [sodium-lib.php](https://github.com/PhpGt/Cipher/blob/master/sodium-lib.php): message encryption and decryption using the library API - [sodium-lib-uri.php](https://github.com/PhpGt/Cipher/blob/master/sodium-lib-uri.php): URI-based encryption and decryption using the library API Those scripts are useful when reading through the repository history or comparing the raw sodium calls with the higher-level abstractions. ## What the examples are good for The examples are especially helpful when we want to: - verify the expected string forms of keys, IVs, and cipher text - see the sender and receiver flow side by side - test the package quickly in a local shell --- If you want the class and method list in one place, finish with [[API reference]]. This page is a compact reference for Cipher's public API. For walkthroughs and fuller explanations, use the earlier pages in the guide. ## Core value objects ### `GT\Cipher\Key` - `__construct(?string $bytes = null)` - `__toString(): string` - `getBytes(): string` ### `GT\Cipher\InitVector` - `__construct(int $byteLength = SODIUM_CRYPTO_BOX_NONCEBYTES)` - `getBytes(): string` - `withBytes(string $bytes): self` - `__toString(): string` ### `GT\Cipher\CipherText` - `__construct(string $data, InitVector $iv, Key $key)` - `__toString(): string` - `getBytes(): string` - `getUri(string|Psr\Http\Message\UriInterface $baseUri = ""): Psr\Http\Message\UriInterface` ### `GT\Cipher\EncryptedUri` - `__construct(string|Psr\Http\Message\UriInterface $uri, string $cipherQueryStringParameter = "cipher", string $initVectorStringParameter = "iv")` - `decryptMessage(Key $key): GT\Cipher\Message\PlainTextMessage` ## Message classes ### `GT\Cipher\Message\AbstractMessage` - `__construct(string $data, ?GT\Cipher\InitVector $iv = null)` - `__toString(): string` - `getIv(): GT\Cipher\InitVector` ### `GT\Cipher\Message\PlainTextMessage` - `encrypt(GT\Cipher\Key $key): GT\Cipher\CipherText` ### `GT\Cipher\Message\EncryptedMessage` - `decrypt(GT\Cipher\Key $key): GT\Cipher\Message\PlainTextMessage` ## Exception classes ### Base type - `GT\Cipher\CipherException` ### Encryption and URI exceptions - `GT\Cipher\EncryptionFailureException` - `GT\Cipher\MissingQueryStringException` - `GT\Cipher\UriDecryptionFailureException` ### Message exceptions - `GT\Cipher\Message\DecryptionFailureException` ## Typical data flow ### Separate fields ```php $message = new GT\Cipher\Message\PlainTextMessage("hello"); $cipherText = $message->encrypt($key); $received = new GT\Cipher\Message\EncryptedMessage( (string)$cipherText, $message->getIv(), ); $plainText = $received->decrypt($key); ``` ### Query string transport ```php $message = new GT\Cipher\Message\PlainTextMessage("hello"); $cipherText = $message->encrypt($key); $uri = $cipherText->getUri("https://example.com/"); $received = new GT\Cipher\EncryptedUri((string)$uri); $plainText = $received->decryptMessage($key); ``` ## Related package requirements - PHP `>= 8.4` - `ext-sodium` - `phpgt/http` --- # Documentation: Cli `phpgt/cli` is a command line interface builder for creating multi-command terminal applications with typed arguments, option parsing, usage generation and testable I/O streams. The primary entry point is the `Application` class, which coordinates command discovery, argument parsing and output handling. Commands extend `Gt\Cli\Command\Command` and declare their contract through required/optional named parameters and required/optional flags/options. The purpose of this library is to keep command line tools explicit and maintainable. Instead of reading raw `$argv` manually, each command receives an `ArgumentValueList`, giving predictable access to passed values and reducing parsing errors. Beyond argument parsing, this library includes built-in `help` and `version` commands, stream abstraction for unit testing, enum-backed stream selection through `StreamName`, colour-aware output through `Palette`, and interactive terminal helpers such as cursor movement and progress bars. A typical example ================= The following script is a minimal command from `example/02-greeter.php`. ```php $greeterCommand = new class extends Command { public function run(?ArgumentValueList $arguments = null):int { $name = (string)$arguments->get("name", "you"); $this->output("Hello, $name!"); return 0; } public function getName():string { return "greet"; } public function getDescription():string { return "Greet a person by name"; } public function getRequiredNamedParameterList():array { return []; } public function getOptionalNamedParameterList():array { return [ new NamedParameter("name"), ]; } public function getRequiredParameterList():array { return []; } public function getOptionalParameterList():array { return []; } }; ``` The command takes an optional positional argument (`name`) and falls back to `you` when no value is passed, producing either `Hello, Greg!` or `Hello, you!`. Command exit codes ================== `Command::run()` must return an `int` exit code. - Return `0` for success. - Return a non-zero code to end the application with that code. This gives commands a predictable way to exit early and makes CLI return-code handling explicit. *** In the next section we will walk through all bundled [[examples]]. You can also jump ahead to [[output streams]] for standard/error output, or [[progress bars]] for in-place terminal updates. The `example/` directory contains runnable scripts that progress from simple output to full multi-command applications. To run any example from the project root: ```bash php example/01-hello-world.php ``` Example index ============= 1. `01-hello-world.php` Outputs a single message using command output helpers. 2. `02-greeter.php` Accepts an optional named argument and prints a personalised greeting. 3. `03-colour.php` Demonstrates one-off coloured output and persistent output palettes. 4. `04-parameters-and-flags.php` Shows required named arguments, optional named arguments, required options and optional flags together. 5. `05-interactive.php` Demonstrates interactive input with `readLine()` when optional arguments are omitted. 6. `06-multi-command.php` Creates a multi-command CLI with `sum`, `repeat`, and built-in `help`. 7. `07-repeating-lines.php` Demonstrates suppression markers for repeated consecutive output lines. 8. `08-progress-bar.php` Shows an in-place progress bar that updates on one terminal line. The examples are intentionally small and anonymous-class based so they can be copied directly into new scripts while keeping setup noise low. *** Next, read [[parameters and arguments]] for how command contracts are defined. Commands declare their interface by implementing four parameter list methods: - `getRequiredNamedParameterList()` - `getOptionalNamedParameterList()` - `getRequiredParameterList()` - `getOptionalParameterList()` Named parameters map to positional values (`deploy production api`), while `Parameter` definitions map to options and flags (`--tag v1.2.3`, `--verbose`). Defining command contracts ========================== ```php public function getRequiredNamedParameterList():array { return [ new NamedParameter("environment"), ]; } public function getOptionalNamedParameterList():array { return [ new NamedParameter("service"), ]; } public function getRequiredParameterList():array { return [ new Parameter(true, "tag", "t", "Release tag to deploy"), ]; } public function getOptionalParameterList():array { return [ new Parameter(false, "dry-run", "d", "Preview only"), new Parameter(false, "verbose", "v", "Show extra detail"), ]; } ``` Reading values in `run()` ========================= ```php $environment = (string)$arguments->get("environment"); $service = (string)$arguments->get("service", "web"); $tag = (string)$arguments->get("tag"); $dryRun = $arguments->contains("dry-run"); $verbose = $arguments->contains("verbose"); ``` The `get($key, $default)` pattern allows simple defaults for optional values, while `contains($key)` is used for boolean flags. When required values are missing, command execution fails with explicit exceptions (`MissingRequiredParameterException`, `MissingRequiredParameterValueException`) and usage text is printed automatically. *** Next, see [[coloured output]] for consistent terminal styling. Output streams ============== Command output is written to `StreamName::OUT` by default. To write to another stream, pass a `StreamName` enum value to the output helper. ```php use GT\Cli\StreamName; $this->writeLine("Saved successfully"); $this->writeLine("Could not save file", StreamName::ERROR); ``` The available stream names are: - `StreamName::IN` - `StreamName::OUT` - `StreamName::ERROR` Using `StreamName` prevents accidental writes to unknown stream names. Passing a raw string such as `"error"` is not supported. Writing output ============== `writeLine()` appends a line ending after the message. `write()` writes the message exactly as provided. ```php $this->write("Downloading..."); $this->writeLine("done"); ``` Both helpers accept a `StreamName` argument: ```php $this->write("Warning: ", StreamName::ERROR); $this->writeLine("configuration file missing", StreamName::ERROR); ``` Colour and streams ================== The `output()` helper writes a coloured line and also accepts a stream name as its fourth argument: ```php $this->output( "Could not connect", Palette::RED, null, StreamName::ERROR ); ``` Progress bars and cursor helpers also use `StreamName` when you need to target a stream other than standard output. *** Next, read [[coloured output]] for terminal styling, or [[progress bars]] for in-place updates. Colour support is provided through `Palette` and command-level output helpers. There are two output modes: - One-off colour for a single message - Persistent palette for subsequent messages until reset Basic usage =========== ```php $this->output("Single green message", Palette::GREEN); $this->setOutputPalette(Palette::RED, Palette::BLACK); $this->output("This line uses red on black"); $this->output("So does this one"); $this->resetOutputPalette(); $this->output("Palette reset to terminal default"); ``` `output()` writes a line and applies ANSI escape codes when a foreground/background palette is present. One-off colours reset automatically after that message, while persistent palettes remain active until `resetOutputPalette()`. To send coloured output to another stream, pass a `StreamName` enum as the fourth argument: ```php $this->output( "Error: deployment failed", Palette::RED, null, StreamName::ERROR ); ``` This makes it practical to standardise message types, for example: - Success: `Palette::GREEN` - Warnings: `Palette::YELLOW` - Errors: `Palette::RED` - Verbose/debug: `Palette::CYAN` *** Next, read [[output streams]] for stream selection, or [[multiple commands]] to structure larger CLI applications. A single `Application` can host multiple commands. The first CLI argument selects the command name, and remaining arguments are parsed according to that command's declared parameters. Creating a multi-command app ============================ ```php $app = new Application( "Multi-command example", new ArgumentList(...$argv), $sumCommand, $repeatCommand ); $app->run(); ``` In `example/06-multi-command.php`: - `sum left right` validates numeric input and prints a total - `repeat text [count]` repeats user text with an optional count - `help` is automatically available for global and per-command usage Command-level errors can throw `CommandException`, which is caught by the application and displayed on the error stream. ```php if(!is_numeric($left) || !is_numeric($right)) { throw new CommandException("Both values must be numeric"); } ``` This keeps command logic focused while preserving consistent output and exit handling. For manual error output inside a command, pass `StreamName::ERROR` to `writeLine()`: ```php $this->writeLine("Could not read input file", StreamName::ERROR); ``` Commands may also return explicit exit codes from `run()`: - `0` indicates success. - Any non-zero value exits the application with that code. *** Next, refer to [[output streams]] for stream selection, or [[examples]] for complete runnable scripts. Progress bars ============= `phpgt/cli` supports dynamic, in-place progress updates for long-running tasks. Use the `ProgressBar` helper ============================ From inside a command, create a progress bar using `createProgressBar()`: ```php $progressBar = $this->createProgressBar(100, "Installing", 30); for($i = 0; $i <= 100; $i++) { $progressBar->setProgress($i); usleep(35_000); } $progressBar->finish("Installation complete"); ``` This renders a single line that updates in place, similar to package manager output. Progress bars write to `StreamName::OUT` by default. Pass a `StreamName` enum as the fourth argument to target a different stream: ```php $progressBar = $this->createProgressBar( 100, "Installing", 30, StreamName::ERROR ); ``` ProgressBar API =============== - `setProgress(int $current, ?string $label = null): void` Set an absolute progress value. - `advance(int $amount = 1, ?string $label = null): void` Increment progress by a relative amount. - `finish(?string $label = null): void` Complete the bar and end the line. Cursor management ================= Cursor movement is exposed through both `Stream` and command helper methods: - `saveCursorPosition()` - `restoreCursorPosition()` - `moveCursorUp(int $amount = 1)` - `moveCursorDown(int $amount = 1)` - `moveCursorForward(int $amount = 1)` - `moveCursorBack(int $amount = 1)` - `setCursorColumn(int $column = 1)` - `rewindCursor()` - `clearLine()` These methods make it easy to build interactive terminal UIs beyond progress bars. Each cursor helper accepts an optional `StreamName` argument when cursor control should be written somewhere other than standard output. *** See [[output streams]] for stream selection, or [[examples]] for runnable scripts including `example/08-progress-bar.php`. --- # Documentation: Config [PHP.GT/Config](https://php.gt/config) parses INI files for managing application configuration, allowing you to override files and environment variables. It keeps configuration grouped into named sections, gives you typed access to values, and provides tools for loading and writing config from both PHP code and the command line. Storing application configuration in INI files allows us to keep default values in version control, layer environment-specific overrides on top, and encapsulate sections of configuration, passing data to the parts of the application that need it. The library has four main areas: ### 1. Load configuration from files The `ConfigFactory` looks for a project's `config.ini` files, parses them, and combines them in the correct order. ```php use GT\Config\ConfigFactory; $config = ConfigFactory::createForProject(__DIR__); ``` ### 2. Read values safely The `Config` object supports dot notation and typed getters. ```php $host = $config->getString("database.host"); $port = $config->getInt("database.port"); $debug = $config->getBool("app.debug"); ``` ### 3. Work with sections Configuration can be split into logical groups, and an individual `ConfigSection` can be passed to a service that only needs one area of configuration. ```php $dbConfig = $config->getSection("database"); $password = $dbConfig?->getString("password"); ``` ### 4. Generate or write config files Deployment config can be created from CI scripts using the bundled `config-generate` command, or by writing a `Config` object back to disk in PHP. ```bash vendor/bin/config-generate staging "database.host=staging-db.internal" ``` *** To begin, let us look at some [[Example config files]]. An application's configuration is stored in INI files. Each file is split into sections, and each section contains key-value pairs. Here is a simple `config.ini`: ```ini [app] namespace=MyApp debug=true [database] driver=mysql host=localhost schema=myapp port=3306 username=app_user password=app_pass [stripe] api_key=pk_test_80dbaff1f54f61fe104ce87832ae6c4e secret_key=sk_test_de297e4be8dbc589d309ba04541d58db ``` In this example we can see three sections: + `[app]` for application-wide settings + `[database]` for connection details + `[stripe]` for payment platform API credentials The keys within a section are later accessed using dot notation such as `database.host` or `stripe.secret_key`. ## Override files Most applications need different configuration in different environments. Rather than duplicating the entire config file, this library allows smaller override files to replace only the values that change. For example, a production override file might look like this: ```ini [database] host=live-db.example.com username=live-example password=260d153ad1bc8b5b [stripe] api_key=pk_live_b3d585b7ef94ba3f2fe0645e046834e2 secret_key=sk_live_322c5de70ec80aa99c33280b0b8516fe ``` Only the keys that need to change have to be present. Everything else is inherited from the earlier config files. > [!NOTE] > It's important to not store API keys within version control. In the examples above, we're assuming the config values are placed in override files manually, or generate on-the-fly by the `config-generate` script. ## Typed values When the INI files are parsed, PHP's typed INI scanner is used. This means values such as booleans and integers are preserved rather than always being treated as plain strings. For example: ```ini [app] debug=true workers=4 cache_ttl=60 ``` These can then be read with typed getters: ```php $debug = $config->getBool("app.debug"); $workers = $config->getInt("app.workers"); $cacheTtl = $config->getInt("app.cache_ttl"); ``` ## Naming conventions The factory looks for config files in a fixed order: + `config.default.ini` + `config.ini` + `config.dev.ini` + `config.deploy.ini` + `config.production.ini` If an environment variable exists for a key, it takes precedence over the file value. Dots are replaced with underscores, so `database.password` becomes `database_password`. *** In the next section we will look at [[Automatically loading config]]. The easiest way to use this library is through `ConfigFactory`. It looks for known config files, parses each one, and combines them into a single `Config` object. ```php use GT\Config\ConfigFactory; $config = ConfigFactory::createForProject(__DIR__); ``` Under the hood, file parsing is performed by `IniParser`, but in most applications there is no need to instantiate the parser directly because the factory already handles file discovery and merge order. ## Load order Files are loaded in this order: + `config.default.ini` + `config.ini` + `config.dev.ini` + `config.deploy.ini` + `config.production.ini` Later files take priority over earlier files. Internally, each later file becomes the base configuration, and any missing values are filled from earlier files. This means that if `config.ini` contains: ```ini [database] host=localhost schema=myapp ``` and `config.dev.ini` contains: ```ini [database] host=dev-db.internal ``` the final values will be: + `database.host = dev-db.internal` + `database.schema = myapp` ## Loading project defaults first `createForProject()` accepts a second argument for a default config file outside the project root. This is useful when a framework or shared package wants to supply a baseline config. ```php $config = ConfigFactory::createForProject( __DIR__, __DIR__ . "/vendor/example/framework/config.default.ini" ); ``` Any project file can then override those defaults as usual. ## Loading a single file directly If you want to parse one INI file without the automatic project lookup, call `createFromPathName()` instead. ```php $config = ConfigFactory::createFromPathName(__DIR__ . "/config.ini"); ``` If the file does not exist, `null` is returned. ## Environment variable overrides Whenever a value is read through `Config::get()`, the environment is checked first. The key name uses underscores instead of dots, so: + `app.namespace` becomes `app_namespace` + `database.password` becomes `database_password` For example, here we can override the password without changing any file: ```php putenv("database_password=super-secret"); echo $config->get("database.password"); ``` Non-empty environment variable values take precedence over the INI files. ## Automatic production detection When `config.production.ini` is part of the loaded set, the factory automatically sets `app.production=true` unless that key is already defined somewhere else. This gives two ways to decide production mode: + define `app.production` explicitly yourself + allow the presence of `config.production.ini` to imply production mode The resulting `Config` object exposes this through `isProduction()`. *** Now that the config is loaded, the next step is [[Working with the Config object]]. The `Config` object represents the fully loaded configuration for your application. It is made up of one or more `ConfigSection` objects, each identified by name. ## Constructing a config manually Although most applications load config from INI files, the object can also be created directly in PHP. ```php use GT\Config\Config; use GT\Config\ConfigSection; $config = new Config( new ConfigSection("app", [ "namespace" => "MyApp", "debug" => "true", ]), new ConfigSection("database", [ "host" => "localhost", "port" => "3306", ]) ); ``` This is particularly useful in tests or when assembling config from another source before writing it out again. ## Reading values with dot notation The most common operation is reading a single value using section and key separated by a dot. ```php $appNamespace = $config->get("app.namespace"); $dbHost = $config->get("database.host"); ``` If the section or key does not exist, `null` is returned. ## Typed getters The `Config` class includes the typed getter methods from `PHP.GT/TypeSafeGetter`. This means we can ask for the shape of data we expect rather than casting everything ourselves. ```php $debug = $config->getBool("app.debug"); $port = $config->getInt("database.port"); $ratio = $config->getFloat("cache.ratio"); $launchAt = $config->getDateTime("release.launch_at"); ``` These methods return `null` when the key is missing. ## Accessing sections If several values belong together, it is often better to access the whole section. ```php $database = $config->getSection("database"); ``` This returns a `ConfigSection` object or `null` if the section does not exist. We will look at sections in detail in the next chapter. ## Listing section names To inspect what is available, call `getSectionNames()`. ```php foreach($config->getSectionNames() as $name) { echo $name, PHP_EOL; } ``` ## Checking production mode The `isProduction()` method returns a boolean indicating whether the configuration should be treated as production. ```php if($config->isProduction()) { // production-only behaviour } ``` This checks `app.production` first. If that key was not defined manually, the factory may have set it automatically when `config.production.ini` was loaded. ## Merging configuration objects Sometimes there is a need to combine two `Config` objects manually. The preferred way to do this is `withMerge()`. ```php $combined = $primary->withMerge($fallback); ``` The naming is important here: the current object keeps its existing values, and the passed config only fills any missing keys or sections. In other words, it behaves like adding fallback values. For example: ```php $primary = new Config( new ConfigSection("app", [ "namespace" => "Shop", ]) ); $fallback = new Config( new ConfigSection("app", [ "namespace" => "DefaultApp", "timezone" => "Europe/London", ]) ); $combined = $primary->withMerge($fallback); ``` The resulting config will contain: + `app.namespace = Shop` + `app.timezone = Europe/London` ## Deprecated mutable merge There is also a `merge()` method, but it is deprecated. It mutates the current object and should be avoided in new code. ```php $config->merge($fallback); // deprecated ``` Prefer `withMerge()` so your code stays predictable and easier to reason about. *** In the next section we will work more closely with [[Working with Config Sections]]. A `ConfigSection` represents one named group of config values such as `[database]`, `[app]` or `[stripe]`. Working with sections is useful when a class needs several related values but should not be handed the entire config object. ## Getting a section Sections are retrieved from the main config object by name: ```php $database = $config->getSection("database"); ``` If the section does not exist, `null` is returned. ## Reading values from a section Once we have a section, individual keys can be accessed without dot notation. ```php $host = $database?->get("host"); $port = $database?->getInt("port"); ``` Like `Config`, each section also supports typed getters from `PHP.GT/TypeSafeGetter`. ## Checking whether a key exists Sometimes `null` is a valid outcome from `get()`, so it is useful to check explicitly whether a key exists. ```php if($database && $database->contains("password")) { // password was defined } ``` ## Iterating over keys and values `ConfigSection` implements `IteratorAggregate`, so it can be looped over directly. ```php foreach($database as $key => $value) { echo "$key = $value", PHP_EOL; } ``` This is useful when copying a section into another object or displaying configuration for diagnostics. ## Using array-style access Sections also implement `ArrayAccess`, so values can be read using array syntax. ```php echo $database["host"]; ``` The section remains immutable, so writing or unsetting values using array syntax will throw an exception. ## Converting to an array If another API expects a plain array, call `asArray()`. ```php $databaseConfig = $database?->asArray() ?? []; ``` This returns the internal key-value data of the section. ## Creating modified copies `ConfigSection` is immutable. To add or replace a value, use `with()` to create a new section instance. ```php $newDatabase = $database->with("port", "3307"); ``` The original section is unchanged, and the returned section contains the updated value. This immutability is what allows `Config::withMerge()` to build combined configurations without mutating earlier objects. *** Once we know how to read configuration, the final piece is [[Generating config files]]. This library can also write configuration back to disk. There are two ways to do this: + from the command line with `config-generate` + from PHP using `FileWriter` ## Generating files from the CLI The package exposes a binary named `config-generate`. ```bash vendor/bin/config-generate deploy \ "database.host=staging-db.internal" \ "database.schema=shop_branch_42" \ "stripe.secret_key=secret-for-ci" ``` This creates a new INI file in the current working directory. The first argument controls the suffix, and the remaining arguments are key-value pairs written using dot notation. The allowed suffixes are: + `dev` + `deploy` + `prod` So the above example writes `config.deploy.ini`. ## CLI argument format Each config value must be written as: ```text section.key=value ``` For example: + `app.namespace=MyApp` + `database.host=localhost` + `exampleapi.key=abc123` If the arguments are invalid, the command writes an error message to standard error. Within PHP, the `Generator` class throws `GT\Config\InvalidArgumentException` when the suffix or key-value arguments are invalid. ## Writing a config file in PHP If you already have a `Config` object, use `FileWriter` to write it as an INI file. ```php use GT\Config\Config; use GT\Config\ConfigSection; use GT\Config\FileWriter; $config = new Config( new ConfigSection("app", [ "namespace" => "MyApp", "debug" => "true", ]), new ConfigSection("database", [ "host" => "localhost", "port" => "3306", ]) ); $writer = new FileWriter($config); $writer->writeIni(__DIR__ . "/config.generated.ini"); ``` Any missing directories in the pathname are created automatically. If the file cannot be written, `FileWriter` throws `GT\Config\ConfigException`. ## A note on production file naming The CLI generator's production suffix is `prod`, which writes `config.prod.ini`. The automatic project loader described earlier looks for `config.production.ini`. That means if you want a generated file to be picked up automatically by `ConfigFactory::createForProject()`, you should either: + write `config.production.ini` yourself using `FileWriter` + rename the generated `config.prod.ini` as part of your deployment process ## Typical deployment usage Here we can see how this is useful in continuous integration. A deployment pipeline can create a small override file containing secrets or environment-specific hosts without editing the committed `config.ini`. ```bash vendor/bin/config-generate deploy \ "database.host=$DB_HOST" \ "database.password=$DB_PASSWORD" \ "shopapi.key=$SHOP_API_KEY" ``` This keeps the committed config readable while still allowing dynamic values at deploy time. *** If you are using this package inside the wider PHP.GT framework, continue to [[Config within WebEngine applications]]. `PHP.GT/WebEngine` uses this library to load project configuration during the request lifecycle. The framework supplies its own default config first, then your application's config files are layered on top. In WebEngine, configuration is loaded with `ConfigFactory::createForProject()` using: + `vendor/phpgt/webengine/config.default.ini` + your project's `config.ini` + any local override files such as `config.dev.ini` or `config.production.ini` + matching environment variables This means your application usually does not need to manually create a `Config` object. Instead, WebEngine constructs it and makes it available through the service container. ## Accessing config in page logic In a page logic class, the config object is available as `$this->config`. ```php namespace App\Page; class Checkout { public function go():void { $currency = $this->config->get("app.currency"); $isProduction = $this->config->isProduction(); } } ``` Here we can see that page logic can read individual values directly, including whether the application is currently running in production mode. ## Injecting config into services WebEngine's default service loader can also inject the `Config` object into your own classes. This is useful when a service needs multiple values or an entire section. ```php use GT\Config\Config; use GT\Config\ConfigSection; class PaymentGateway { public function __construct( private ConfigSection $stripeConfig, ) {} } ``` Then in a service loader: ```php use GT\Config\Config; class ServiceLoader { public function __construct( private Config $config, ) {} public function loadPaymentGateway():PaymentGateway { $stripeConfig = $this->config->getSection("stripe"); if(!$stripeConfig) { throw new RuntimeException("Missing [stripe] config section."); } return new PaymentGateway( $stripeConfig, ); } } ``` Passing the smallest useful section helps keep services focused. A payment service rarely needs the entire application config, and this style keeps that boundary clear. ## Database integration WebEngine's default service loader already reads values from the `[database]` section to construct the database connection: + `database.driver` + `database.schema` + `database.host` + `database.port` + `database.username` + `database.password` Because of that, the most common use of this library inside WebEngine is simply maintaining accurate INI files in the project root. ## Production mode in WebEngine If your project includes `config.production.ini`, this library marks `app.production=true` automatically unless you have already defined `app.production` yourself. That means WebEngine code can call `isProduction()` without requiring a separate flag in most production deployments. *** From here, it is useful to return to the main API and continue with [[Working with the Config object]]. --- # Documentation: Cookie `phpgt/cookie` gives us an object oriented way to work with HTTP cookies without reading from and writing to the `$_COOKIE` superglobal throughout our application. Cookies are small pieces of text sent by the browser with each request. They are useful for remembering lightweight client-side state, such as a preference, a tracking token, or the identifier used by a server-side session system. Native PHP exposes incoming cookies through `$_COOKIE`, and sends outgoing cookies with the global `setcookie()` function. This library keeps that same browser behaviour, but wraps the cookie data in a small API that is easier to pass around, test, and understand where data is being changed. > [!NOTE] > WebEngine depends on this package as one of its small single-purpose components. In a WebEngine application, global variables are protected by default, so cookie access should be handled explicitly rather than by reaching into `$_COOKIE` throughout page logic. ## What this library covers - Creating a `CookieHandler` from the current request cookies. - Reading cookies using method calls or array-style syntax. - Checking whether a cookie exists. - Setting cookies by calling PHP's `setcookie()` behind the scenes. - Deleting one cookie, several cookies, or every tracked cookie. - Iterating over cookies and exporting them as a plain array. - Representing individual cookies as immutable `Cookie` objects. - Validating cookie names and values before they are accepted. ## A small example ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); if(!$cookies->contains("firstVisit")) { $cookies->set( "firstVisit", date("c"), new DateTime("+30 days"), secure: true, httponly: true, ); } echo $cookies["firstVisit"] ?? "Welcome!"; ``` The `CookieHandler` lets us read cookies with the same familiar `["name"]` syntax as `$_COOKIE`, while still giving us explicit methods for operations that affect the browser response. --- To start from the smallest working example, continue with the [[Quick start guide]]. In this guide we will build the smallest useful cookie flow: read an incoming cookie, set it if it does not exist, and delete it again when we no longer need it. ## 1. Install the package ```bash composer require phpgt/cookie ``` ## 2. Create a CookieHandler In plain PHP, construct the handler from `$_COOKIE`: ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); ``` The constructor copies the current request cookies into `Cookie` objects. After construction, the handler is the object we pass around instead of the superglobal. ## 3. Read an existing cookie We can check for a cookie before reading it: ```php if($cookies->contains("theme")) { echo "Your saved theme is " . $cookies["theme"]; } ``` Array access is intentionally similar to `$_COOKIE`. If the cookie does not exist, `$cookies["theme"]` returns `null`. ## 4. Set a cookie Use `set()` when we want the browser to store a cookie: ```php $cookies->set( "theme", "dark", new DateTime("+1 year"), secure: true, httponly: true, ); ``` This updates the handler immediately and calls PHP's `setcookie()` so the browser receives the `Set-Cookie` response header. > [!IMPORTANT] > Cookies are sent as HTTP headers. In a plain PHP script, call `set()` or `delete()` before output has started, otherwise PHP may not be able to send the header. ## 5. Delete a cookie When the cookie should no longer be used: ```php $cookies->delete("theme"); ``` The cookie is removed from the handler, and the browser is sent an expired cookie with the same name. ## 6. Use the handler in application code Once the handler exists, pass it to the part of the application that needs cookies: ```php function rememberTheme(CookieHandler $cookies, string $theme):void { $cookies->set( "theme", $theme, new DateTime("+1 year"), secure: true, httponly: true, ); } ``` That keeps the rest of the code independent from `$_COOKIE`, which makes the function easier to test and easier to reuse. --- Next, see [[Reading cookies]] for all of the ways to inspect the handler. `CookieHandler` is designed to feel familiar if we have used `$_COOKIE`, while still being explicit about the difference between reading cookies and sending new cookie headers. ## Constructing from request cookies The most common starting point is the current request: ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); ``` The array keys become cookie names, and the array values become cookie values. Each entry is stored internally as a `Cookie` object. For tests or command-line scripts, we can pass any `array`: ```php $cookies = new CookieHandler([ "theme" => "dark", "currency" => "GBP", ]); ``` ## Checking for a cookie Use `contains()` when the code needs to know whether the cookie exists: ```php if($cookies->contains("currency")) { echo "Currency preference found"; } ``` This is equivalent to using `isset()` with array access: ```php if(isset($cookies["currency"])) { echo "Currency preference found"; } ``` ## Reading a value Array access returns the cookie value as a string: ```php $currency = $cookies["currency"]; ``` If the cookie does not exist, array access returns `null`: ```php $currency = $cookies["currency"] ?? "GBP"; ``` ## Reading the Cookie object When we need the name and value together, use `get()`: ```php $cookie = $cookies->get("currency"); if($cookie) { echo $cookie->getName() . " = " . $cookie->getValue(); } ``` `get()` returns `null` when the cookie does not exist. ## Iterating over cookies `CookieHandler` is iterable. The key is the cookie name, and the value is the cookie value: ```php foreach($cookies as $name => $value) { echo "$name = $value\n"; } ``` The handler is also countable: ```php echo "There are " . count($cookies) . " cookies in this request."; ``` ## Exporting as an array Use `asArray()` when another API needs plain cookie data: ```php $cookieArray = $cookies->asArray(); ``` The returned array has the same basic shape as `$_COOKIE`: names as keys, values as strings. --- Next, see [[Setting and deleting cookies]] for the methods that send `Set-Cookie` headers. Reading cookies tells us what the browser sent with the current request. Setting and deleting cookies tells the browser what it should store for future requests. In PHP, that happens through HTTP headers. `CookieHandler` calls PHP's `setcookie()` function for us, but the same rule still applies: headers must be sent before the response body starts. ## Setting a cookie Use `set()` with a name, value, and optional expiry: ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); $cookies->set( "seenWelcome", "yes", new DateTime("+30 days"), ); ``` After this call, two things have happened: - the handler contains a `Cookie` object for `seenWelcome` - PHP has been asked to send a `Set-Cookie` header to the browser ## Expiry The third argument accepts any `DateTimeInterface`: ```php $cookies->set("promo", "spring", new DateTime("2026-05-01 00:00:00")); ``` For persistent cookies, always pass an explicit future expiry date. If the expiry argument is omitted, the current implementation sends the current timestamp to `setcookie()`. That is usually not what we want for a long-lived preference cookie, so examples in this guide pass the expiry explicitly. ## Domain, secure, and HTTP-only flags `set()` also accepts a domain, secure flag, and HTTP-only flag: ```php $cookies->set( "rememberDevice", "1", new DateTime("+90 days"), domain: "example.com", secure: true, httponly: true, ); ``` The cookie path is always sent as `/`. `secure: true` tells the browser to send the cookie only over HTTPS. `httponly: true` tells the browser not to expose the cookie to JavaScript through `document.cookie`. > [!TIP] > For authentication or other sensitive server-side state, store the real data in a session or database and use the cookie only as an identifier. Cookies are sent by the browser and should not be treated as private storage. ## Why array assignment is blocked This works for reading: ```php echo $cookies["theme"]; ``` But this does not work for writing: ```php $cookies["theme"] = "dark"; ``` Array assignment throws `CookieSetException`. Setting a cookie needs more information than a value alone, such as expiry and security flags, so the library requires us to call `set()` explicitly. ## Deleting one cookie Use `delete()` when the browser should forget a cookie: ```php $cookies->delete("theme"); ``` The cookie is removed from the handler immediately. The browser receives an expired cookie with the same name and the path `/`. Array unset uses the same behaviour: ```php unset($cookies["theme"]); ``` ## Clearing several cookies Use `clear()` with one or more names: ```php $cookies->clear("theme", "currency"); ``` Each named cookie is set to an empty value with an expiry in the past, then removed from the handler. ## Clearing every tracked cookie Call `clear()` without arguments: ```php $cookies->clear(); ``` This clears every cookie currently stored in the handler. > [!IMPORTANT] > Browsers match cookies by name, domain, and path. This library deletes cookies using the path `/`. If a cookie was created by other code with a different path or domain, that other code may need to delete it with the same attributes. --- Next, see [[Cookie objects and validation]] to understand the immutable value objects stored by the handler. `Cookie` is a small immutable value object. It stores a cookie name and value, and refuses names or values that contain characters which are not valid for HTTP cookies. ## Creating a Cookie object ```php use GT\Cookie\Cookie; $cookie = new Cookie("currency", "GBP"); ``` The value defaults to an empty string: ```php $empty = new Cookie("seenWelcome"); ``` ## Reading the name and value ```php echo $cookie->getName(); echo $cookie->getValue(); ``` The object can also be cast to a string, which returns the value: ```php echo "Selected currency: " . $cookie; ``` ## Immutability `Cookie` objects do not expose setters. To change the value, use `withValue()`: ```php $original = new Cookie("currency", "GBP"); $updated = $original->withValue("EUR"); echo $original->getValue(); // GBP echo $updated->getValue(); // EUR ``` `withValue()` returns a clone, so the original object is unchanged. ## Validation Cookie names and values are restricted by the characters that are valid in HTTP cookie headers. The library performs this validation when a `Cookie` is constructed and when `withValue()` is called. Invalid data throws `InvalidCharactersException`: ```php use GT\Cookie\Cookie; use GT\Cookie\InvalidCharactersException; try { $cookie = new Cookie("bad,name", "value"); } catch(InvalidCharactersException) { echo "The cookie name or value contains characters that cannot be used."; } ``` The exact character sets are exposed by `Validity`. ## Validity helper `Validity` can be used directly when we want to validate before constructing a `Cookie`: ```php use GT\Cookie\Validity; if(!Validity::isValidName($name)) { throw new InvalidArgumentException("Invalid cookie name"); } if(!Validity::isValidValue($value)) { throw new InvalidArgumentException("Invalid cookie value"); } ``` The helper also exposes the allowed characters as arrays: ```php $nameCharacters = Validity::getValidNameCharacters(); $valueCharacters = Validity::getValidValueCharacters(); ``` Name characters are alphanumeric characters plus the allowed cookie-name punctuation. Value characters are alphanumeric characters plus the allowed cookie-value punctuation and a space. ## Exceptions All package-specific exceptions extend `CookieException`: - `InvalidCharactersException` is thrown when a cookie name or value is invalid. - `CookieSetException` is thrown when code tries to set a cookie through array assignment. That means application code can catch the specific exception, or catch `CookieException` for any error raised by this package. --- Next, see [[WebEngine usage]] for how this package fits into a PHP.GT application. ## Why WebEngine avoids superglobals In plain PHP, it is common to read cookies directly from `$_COOKIE`. That works, but it means any code in the project can read from - and write to - global request state at any time. WebEngine protects superglobals by default so request data is accessed through explicit objects instead. This keeps page logic easier to test, because a function can receive the objects it needs rather than reaching into global state. > [!NOTE] > WebEngine also uses [PHP.GT/Input](https://www.php.gt/input) for query strings, form submissions, uploaded files, and request bodies. Cookie data is separate because browser cookies have their own request and response behaviour. ## Using CookieHandler in application code The useful pattern is to construct a `CookieHandler` near the edge of the application, then pass it to the code that needs cookies: ```php use GT\Cookie\CookieHandler; function showIntro(CookieHandler $cookies):void { if($cookies->contains("dismissedIntro")) { return; } // Render the introductory message. } ``` The same idea applies to actions that set cookies: ```php use GT\Cookie\CookieHandler; function dismissIntro(CookieHandler $cookies):void { $cookies->set( "dismissedIntro", "1", new DateTime("+1 year"), secure: true, httponly: true, ); } ``` This keeps the dependency visible in the function signature. In a WebEngine project, that is the same style used by other PHP.GT components: page logic should depend on explicit objects rather than superglobals. > [!NOTE] > The current WebEngine lifecycle also uses the incoming cookie array when initialising sessions, because PHP's session system is identified by a browser cookie when cookie-based sessions are enabled. ## Sessions and cookies Cookies are often used to identify a session, but they should not usually contain the session data itself. For example, a browser may store a session ID cookie. The server uses that ID to load the actual session data from server-side storage. That is different from storing a user's private data directly in the cookie value. When data belongs on the server, use PHP.GT/Session or another server-side store. When data is a small browser preference, `CookieHandler` can be enough on its own. ## Security defaults in application code When setting cookies from WebEngine page logic, prefer explicit security flags: ```php $cookies->set( "colourScheme", "dark", new DateTime("+6 months"), secure: true, httponly: true, ); ``` Use `httponly: false` only when JavaScript really does need to read the cookie. Use `secure: true` when the site is served over HTTPS, which should be the normal production setup. --- Next, see the [[API reference]] for the complete public API of this package. This page is a compact reference for the public API of the library. For walkthroughs and examples, use the earlier pages in the guide. ## CookieHandler `CookieHandler` represents the collection of cookies for a request and response. ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); ``` ### Constructor - `__construct(array $existingCookies = [])` The array should be shaped like `array`. ### Reading methods - `contains(string $name): bool` - `get(string $name): ?Cookie` - `asArray(): array` - `count(): int` `asArray()` returns `array`. ### Writing methods - `set(string $name, string $value, ?DateTimeInterface $expires = null, string $domain = "", bool $secure = false, bool $httponly = false): void` - `delete(string $name): void` - `clear(string ...$nameList): void` `set()` always sends the path `/` to PHP's `setcookie()` function. `delete()` expires the named cookie and removes it from the handler. `clear()` with names clears only those cookies. `clear()` without names clears every cookie currently tracked by the handler. ### ArrayAccess - `isset($cookies[$name])` checks whether the cookie exists. - `$cookies[$name]` returns the cookie value as `?string`. - `unset($cookies[$name])` deletes the cookie. - `$cookies[$name] = $value` throws `CookieSetException`. Array assignment is not supported because setting a browser cookie needs expiry and security options. ### Iterator `CookieHandler` can be used in a `foreach` loop: ```php foreach($cookies as $name => $value) { echo "$name: $value"; } ``` The key is the cookie name. The value is the cookie value. ## Cookie `Cookie` is an immutable value object. ### Constructor - `__construct(string $name, string $value = "")` ### Methods - `__toString(): string` - `getName(): string` - `getValue(): string` - `withValue(string $value): self` `withValue()` returns a cloned `Cookie` with the new value. ## Validity `Validity` exposes the character validation used by `Cookie`. - `Validity::getValidNameCharacters(): array` - `Validity::getValidValueCharacters(): array` - `Validity::isValidName(string $name): bool` - `Validity::isValidValue(string $value): bool` ## Exceptions - `CookieException` - `CookieSetException` - `InvalidCharactersException` `CookieSetException` and `InvalidCharactersException` both extend `CookieException`. ## Namespace New code should use the `GT\Cookie` namespace: ```php use GT\Cookie\CookieHandler; ``` The package also keeps a Composer autoload mapping for the legacy `Gt\Cookie` prefix so older applications can continue to load the same classes. These examples show the library on its own. In a larger application, the same operations usually live inside controllers, page logic functions, or small services. ## Remember a preference ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); if($_SERVER["REQUEST_METHOD"] === "POST") { $scheme = $_POST["scheme"] ?? "light"; $cookies->set( "colourScheme", $scheme, new DateTime("+6 months"), secure: true, httponly: true, ); } $currentScheme = $cookies["colourScheme"] ?? "light"; ``` The preference is available immediately from the handler after `set()` is called, and it will be sent back by the browser on later requests if the browser accepts the cookie. ## Dismiss a message ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); if(isset($_POST["dismiss-intro"])) { $cookies->set( "dismissedIntro", "1", new DateTime("+1 year"), secure: true, httponly: true, ); } if(!$cookies->contains("dismissedIntro")) { echo "

Welcome to the application.

"; } ``` This is a good use for a cookie because the value is small and does not need to be private. ## Clear preference cookies ```php use GT\Cookie\CookieHandler; $cookies = new CookieHandler($_COOKIE); $cookies->clear( "colourScheme", "dismissedIntro", "currency", ); ``` Use this for "reset preferences" features, where a small known group of cookies should be removed together. ## Pass cookies into a function ```php use GT\Cookie\CookieHandler; function getCurrency(CookieHandler $cookies):string { return $cookies["currency"] ?? "GBP"; } function rememberCurrency(CookieHandler $cookies, string $currency):void { $cookies->set( "currency", $currency, new DateTime("+1 year"), secure: true, httponly: true, ); } ``` Passing the handler in makes the dependency clear. The function can be tested with a simple in-memory handler: ```php $cookies = new CookieHandler([ "currency" => "EUR", ]); assert(getCurrency($cookies) === "EUR"); ``` ## Use Cookie objects directly ```php use GT\Cookie\Cookie; $cookie = new Cookie("currency", "GBP"); $updated = $cookie->withValue("EUR"); echo $cookie->getValue(); // GBP echo $updated->getValue(); // EUR ``` This is useful when a function should work with one cookie value without needing the whole handler. --- For the complete method list, see the [[API reference]]. --- # Documentation: Cron `phpgt/cron` lets us schedule PHP application tasks using familiar crontab syntax, then run the due jobs from PHP itself. That means we can keep scheduled work close to the project instead of scattering it across system crontab entries, custom shell scripts, and framework-specific glue. A job can be: - a shell command - a static PHP function call - a `go()` function inside a file in the project's `cron/` directory > [!NOTE] > In WebEngine development, this library is already included. WebEngine's `gt run` command starts the cron runner alongside the local web server and build watcher, and `gt cron` can be used when we only want the scheduler. ## What this library covers - Parsing standard five-field crontab expressions. - Ignoring comments and blank lines in `crontab` files. - Running due jobs once or continuing to watch for future jobs. - Calling static PHP methods directly from the crontab. - Resolving short script names from the local `cron/` directory. - Running `go()` cron scripts with query-string input and optional dependency injection. - Extending the schedule language through custom `ExpressionFactory` implementations. ## A small example `crontab`: ```text # Run a direct shell command every hour. 0 * * * * printf 'Hourly report built\n' # Run a short script alias from cron/hello.php every minute. * * * * * hello # Run a static method every weekday evening. 0 18 * * MON-FRI App\Task\Digest::send("team@example.com") ``` Run the scheduler from the project root: ```bash vendor/bin/cron --now ``` If the jobs above are due, the runner executes them immediately. To keep the process alive and wait for future jobs, use `--watch`. ***** Start with [[Quick start guide]] to build a working project from scratch. In this guide we will build a tiny project with one cron script, one `crontab` file, and one command to run it. > [!TIP] > In WebEngine projects, `phpgt/cron` is already installed and `gt run` will start the scheduler for local development. This guide is for getting the project running outside of WebEngine, or just to understand what's going on under the bonnet. ## 1. Install the package ```bash composer require phpgt/cron ``` This gives us the `vendor/bin/cron` executable and the PHP classes behind it. ## 2. Create a `crontab` file In the project root, create a file called `crontab`: ```text * * * * * hello ``` This line means "run `hello` every minute". ## 3. Create the matching script Now create `cron/hello.php`: ```php createForProject(__DIR__); $runner->run(); ``` To ignore the schedule and run all jobs immediately: ```php $runner->runAll(); ``` To run only selected jobs immediately, pass a matcher that receives the original crontab command string: ```php $runner->runMatching( fn(string $command):bool => str_contains($command, "load-wikis") ); ``` If we need more control, we can set a callback that receives progress data after each cycle: ```php $runner->setRunCallback( function( int $jobsRan, ?DateTime $nextRun, bool $continue, array $runCommandList, ?string $nextCommand, ):void { // Log or display status here. } ); ``` ***** Move on to [[Static functions and shell commands]] to see how the command part of each crontab line is interpreted. After the schedule fields, each crontab line contains one command string. This library can interpret that string in a few different ways. The simplest two are: - run it as a shell command - treat it as a PHP callable ## Shell commands Any command that is not recognised as a PHP callable or special cron script is passed to the process runner. ```text 0 2 * * * php bin/nightly-maintenance.php */5 * * * * printf 'Heartbeat\n' ``` The command is executed through `proc_open()`. If the process exits with a non-zero status, the runner treats that as a failure. ## Static function calls If the command looks like a callable, the library runs it directly in PHP rather than starting a separate process. ```text 0 9 * * 1-5 App\Task\Digest::send 30 17 * * * App\Task\Cleanup::archive("weekly", 50) ``` This is a good fit when: - the code already lives in your PHP application - the task is easier to test as a normal method - we want Composer autoloading rather than a standalone script entry point ## Callable resolution The callable name is taken from everything before the first `(` character. That means all of these are valid shapes: ```text App\Task\Digest::send \App\Task\Digest::send App\Task\Cleanup::archive("weekly", 50) ``` If the callable does not exist, the runner raises a function execution error for that line. ## Arguments Function arguments are parsed from the text inside the brackets using CSV-style rules: ```text App\Task\Cleanup::archive("weekly", 50) ``` Be aware of two details: - values are read as strings from the crontab line - double quotes are the clearest way to include spaces or commas In practice, PHP will usually coerce those strings into scalar parameter types when the method signature expects `int`, `float`, or `bool`, but it is best to keep the arguments simple. ## When to choose which style Prefer a shell command when: - the task already exists as a standalone script - the process should be isolated from the current PHP process - the command naturally belongs to another executable Prefer a static method when: - the task is application code - we want to use Composer autoloading directly - we do not need a separate process boundary ***** If the task belongs in the project's `cron/` directory, continue with [[Cron directory scripts]] and [[Go function scripts]]. The runner can resolve short script names from a local `cron/` directory. This lets us write: ```text * * * * * hello ``` Instead of: ```text * * * * * php cron/hello.php ``` ## How alias resolution works For ordinary script aliases, the runner looks for a file directly inside the project's `cron/` directory. These two crontab lines both resolve to `cron/hello.php`: ```text * * * * * hello * * * * * hello.php ``` Using the current PHP binary automatically. ## Passing shell arguments through Any text after the script name is preserved and passed through: ```text * * * * * nightly-report "sales team" 2026-03 ``` If `cron/nightly-report.php` exists, that becomes equivalent to: ```text php cron/nightly-report.php "sales team" 2026-03 ``` ## A small example `crontab`: ```text */15 * * * * report "internal" ``` `cron/report.php`: ```php getString("mode") . PHP_EOL, FILE_APPEND ); } ``` The runner loads that function and invokes it directly. ## Why use `go()` scripts? This style is useful when we want: - a tidy task entry point in `cron/` - request-like named input instead of shell arguments - dependency injection for services - query-string input without shell quoting ## Referencing a `go()` script If `cron/cache.php` contains `function go(...)`, all of these command forms are accepted: ```text * * * * * cache?type=db&mode=daily * * * * * /cache?type=db&mode=daily * * * * * cache.php?type=db&mode=daily * * * * * cron/cache?type=db&mode=daily * * * * * /cron/cache?type=db&mode=daily ``` The part after `?` is parsed as a query string. ## Reading input Query-string values are made available through an `Input` object: ```php getString("type"); $mode = $input->getString("mode"); } ``` This keeps the task code readable when there are several named options. ## Dependency injection `go()` functions may also receive services as parameters when they can be resolved from the service container. Example: ```php write($input->getString("type")); } ``` The runner creates a container, puts the `Input` object into it, and then uses the service loaders that are available for the project. ## How project services are found When configuration is available, the runner reads: - `app.namespace` - `app.class_dir` - `app.service_loader` From there it can: - autoload project classes from the configured class directory - register WebEngine's default service loader when it is installed - register the project's own service loader class > [!NOTE] > In WebEngine projects this usually means `go()` scripts can use the same service-loading conventions as page logic, which makes cron tasks feel like part of the application rather than a separate subsystem. ## Nested files Unlike ordinary script aliases, `go()` scripts may live in subdirectories under `cron/`. For example, this may point at `cron/report/daily.php`: ```text 0 6 * * * report/daily?audience=internal ``` ## One important limitation `go()` script commands do not accept shell-style trailing arguments after the script name. The special handling is only used when the command is just the script reference, optionally followed by a query string. Use query-string parameters when the task needs named input. ## Choosing between `go()` and ordinary scripts Choose an ordinary `cron/*.php` script when: - `argv` is enough - we want a separate process - the task is already written as a standalone script Choose a `go()` script when: - we are in a WebEngine context - we want named input through `Input` - we want service injection - we want to organise tasks into nested directories ***** If the built-in cron syntax is not enough, the next page shows how to extend it with [[Custom expression factories]]. The library uses `ExpressionFactory` to turn the schedule part of each crontab line into an `Expression` object. Most projects will use the built-in `CronExpression`, but we can replace the factory when we need project-specific shorthand. A custom expression factory is handy when we want to support things such as: - extra nickname tokens - application-specific schedule words - test-only expressions - migration paths from an older internal scheduler ## Basic shape `ExpressionFactory` only needs one method: ```php use GT\Cron\Expression; use GT\Cron\ExpressionFactory; class MyExpressionFactory extends ExpressionFactory { public function create(string $expression):Expression { // Return a custom Expression here. } } ``` ## Example: `@start` schedule The repository includes an example of handling `@start` as "due immediately": ```php use DateTime; use GT\Cron\CronExpression; use GT\Cron\Expression; use GT\Cron\ExpressionFactory; $customExpressionFactory = new class extends ExpressionFactory { public function create(string $expression):Expression { if($expression === "@start") { return new class implements Expression { public function isDue(DateTime $now):bool { return true; } public function getNextRunDate(?DateTime $now = null):DateTime { return clone ($now ?? new DateTime()); } }; } return new CronExpression($expression); } }; ``` Then pass that factory into the parser: ```php use GT\Cron\CrontabParser; $parser = new CrontabParser($customExpressionFactory); ``` Now a crontab can contain: ```text @start hello 30 * * * * hourly-report ``` The above example will execute `hello` at the start of the execution, once. Then it will execute hourly-report at 30 minutes past every hour. ## Keep the contract simple An `Expression` only needs to answer two questions: - is this job due now? - when is the next run date? That small contract keeps custom schedules easy to reason about. ## Prefer clarity over cleverness If a schedule rule is specific to your project, document it near the `crontab` file or deployment docs as well. Hidden shorthand tends to confuse the next person who reads the schedule. ***** To see the built-in examples from this repository, continue with [[Examples]]. The `example/` directory is the quickest way to see the library working without building a full project first. Run an example from the repository root: ```bash php example/01-run-due-jobs.php ``` Each script embeds its own `crontab` string so the schedule and the code stay together. ## `01-run-due-jobs.php` This example shows the smallest useful flow: - create a `Queue` - parse a crontab string into it - run only the jobs due at a chosen time It uses `ScriptOutputMode::INHERIT` so the command output is printed directly. ## `02-next-job.php` This example focuses on scheduling rather than execution. It demonstrates: - running the jobs due now - asking the queue for the next run date - asking the queue for the next command due This is a good page to read after [[Running the runner]] if you want the underlying API shape. ## `03-nickname-expressions.php` This example shows that nickname schedules such as `@hourly` and `@daily` are accepted as the first token on a line. It is a small, concrete demonstration of the nickname support described in [[Crontab syntax]]. ## `04-custom-expression-factory.php` This example injects a custom `ExpressionFactory` that understands an extra `@start` token. Read this alongside [[Custom expression factories]] if you want to extend the scheduler for your own project. --- # Documentation: Csrf This library handles [CSRF protection](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) automatically for you, including generating tokens, injecting them into all forms in the page and then verifying that a valid token is present whenever a POST request is received. This repository provides the moving parts for that flow: - `TokenStore` generates, stores, verifies and spends tokens. - `HTMLDocumentProtector` injects tokens into HTML forms and exposes them through a `` tag for client-side retrieval. - Exception types tell us whether the token was missing, invalid, or already used. The package is designed to be used standalone in any PHP project. > [!NOTE] > When using PHP.GT/WebEngine, this library's functionality is automated wherever a `
` is used - so there's nothing you need to manually set up or check/validate in WebEngine applications. ## What this library does - Generates random token strings. - Stores tokens in memory or in a session-backed store. - Injects hidden `` fields into `POST` forms. - Adds or updates a `` tag in the page ``. - Verifies submitted tokens and marks them as spent after use. - Allows custom token store implementations. ## A minimal example ```php use GT\Csrf\HTMLDocumentProtector; use GT\Csrf\SessionTokenStore; use Gt\Session\SessionArrayWrapper; session_start(); $session = new SessionArrayWrapper($_SESSION); $tokenStore = new SessionTokenStore($session); if($_SERVER["REQUEST_METHOD"] === "POST") { $tokenStore->verify($_POST); } $html = << Contact
HTML; $protector = new HTMLDocumentProtector($html, $tokenStore); $protector->protect(); echo $protector->getHTMLDocument(); ``` The same `SessionTokenStore` is used in both directions: 1. Before handling a `POST`, verify the submitted token. 2. Before sending HTML to the browser, inject the next token. That is the whole flow in this package. --- Start with [[Quick start]] to build the full request and response cycle from scratch. In this guide we will set up a small standalone PHP page that uses a session-backed token store, verifies incoming `POST` requests, and injects the next token into the HTML response. > [!NOTE] > In WebEngine projects, the same building blocks are used, but request handling, document rendering and session access are usually already available elsewhere in the application. This page shows the package in isolation, using plain PHP techniques. ## 1. Install the package ```bash composer require phpgt/csrf ``` ## 2. Create a token store For real web requests we need a store that survives between one page render and the next form submission, so `SessionTokenStore` is the usual starting point. ```php use GT\Csrf\SessionTokenStore; use GT\Session\SessionArrayWrapper; session_start(); $session = new SessionArrayWrapper($_SESSION); $tokenStore = new SessionTokenStore($session); ``` ## 3. Verify incoming `POST` data Before processing the form, verify the submitted token: ```php use GT\Csrf\Exception\CsrfException; if($_SERVER["REQUEST_METHOD"] === "POST") { try { $tokenStore->verify($_POST); } catch(CsrfException $exception) { http_response_code(403); exit("Invalid CSRF token"); } } ``` If the request body is empty, `verify()` does nothing. If there is `POST` data but no CSRF token, or the token is not valid, an exception is thrown. ## 4. Build the HTML response ```php $html = <<<'HTML' Contact form
HTML; ``` ## 5. Inject tokens into the page ```php use GT\Csrf\HTMLDocumentProtector; $protector = new HTMLDocumentProtector($html, $tokenStore); $protector->protect(); echo $protector->getHTMLDocument(); ``` After `protect()` runs: - each `POST` form will contain a hidden field named `csrf-token` - the page `` will contain a `` element - the generated token will be saved into the token store ## 6. Put it together ```php use GT\Csrf\Exception\CsrfException; use GT\Csrf\HTMLDocumentProtector; use GT\Csrf\SessionTokenStore; use Gt\Session\SessionArrayWrapper; session_start(); $session = new SessionArrayWrapper($_SESSION); $tokenStore = new SessionTokenStore($session); if($_SERVER["REQUEST_METHOD"] === "POST") { try { $tokenStore->verify($_POST); } catch(CsrfException $exception) { http_response_code(403); exit("Invalid CSRF token"); } } $html = <<<'HTML' Contact form
HTML; $protector = new HTMLDocumentProtector($html, $tokenStore); $protector->protect(); echo $protector->getHTMLDocument(); ``` ## What to expect in the browser The rendered form will now include a hidden field similar to this: ```html ``` The `` will also contain: ```html ``` That meta tag is useful when client-side JavaScript needs access to the current token value. --- Next, [[Token stores]] explains how tokens are generated, retained and spent. `TokenStore` is the base abstraction in this package. It's responsible for four things: 1. generating token strings 2. saving tokens so they become valid 3. verifying that a submitted token is still valid 4. spending tokens after successful use ## The base `TokenStore` The abstract class `GT\Csrf\TokenStore` provides: - `generateNewToken()` - `setTokenLength(int $length)` - `getMaxTokens()` - `verify(array|object $postData)` It leaves these methods for concrete stores to implement: - `saveToken(string $token)` - `verifyToken(string $token)` - `consumeToken(string $token)` ### Token length By default, tokens are generated as prefixed ULIDs, so the string begins with `CSRF_` followed by a 32 character ULID. ```php $token = $tokenStore->generateNewToken(); echo $token; // CSRF_01H... ``` If needed, we can change the length before generating tokens: ```php $tokenStore->setTokenLength(64); ``` This changes the ULID portion of the token. The `CSRF_` prefix remains in place. That means the full token string length is always: `configured token length + strlen("CSRF_")` ### Token limits `TokenStore` keeps a maximum token count. By default this is `1000`. ```php $tokenStore = new ArrayTokenStore(50); echo $tokenStore->getMaxTokens(); // 50 ``` When more tokens are saved than the configured limit, the oldest stored token is discarded. ## `ArrayTokenStore` `ArrayTokenStore` stores tokens in a PHP array held in memory. ```php use GT\Csrf\ArrayTokenStore; $tokenStore = new ArrayTokenStore(); ``` This is useful for: - unit tests - short-lived scripts - custom applications where another layer persists the object for us On its own, `ArrayTokenStore` does not survive between HTTP requests, so it is usually not enough for a normal browser form workflow unless we place the object somewhere persistent ourselves. ## `SessionTokenStore` `SessionTokenStore` reads and writes the token list from a `Gt\Session\SessionContainer`. ```php use GT\Csrf\SessionTokenStore; use Gt\Session\SessionArrayWrapper; session_start(); $session = new SessionArrayWrapper($_SESSION); $tokenStore = new SessionTokenStore($session); ``` This is the usual choice for web applications because: - the token generated when rendering a page is still available on the next request - spent tokens can be tracked between requests - the same store can be shared across the whole user session Internally, the token list is saved under the session key `tokenList`. ## How stored token state works Both built-in stores keep values in the same shape: - `null` means the token exists and has not yet been used - an integer timestamp means the token has already been spent That timestamp is later used by `CsrfTokenSpentException` to report when the token was previously consumed. ## Choosing a store - Use `SessionTokenStore` for ordinary browser-based forms. - Use `ArrayTokenStore` for tests or temporary in-memory usage. - Extend `TokenStore` if your application needs a database, cache or other storage backend. --- To see how tokens are inserted into HTML documents, read [[Protecting pages]] `HTMLDocumentProtector` is responsible for taking an HTML document, generating one or more tokens, storing them, and inserting them into the right places in the markup. ## Constructing a protector The constructor accepts either a string of HTML or an existing `GT\Dom\HTMLDocument`, plus a token store: ```php use GT\Csrf\HTMLDocumentProtector; $protector = new HTMLDocumentProtector($html, $tokenStore); ``` Or with an existing document: ```php use GT\Dom\HTMLDocument; $document = new HTMLDocument($html); $protector = new HTMLDocumentProtector($document, $tokenStore); ``` In both cases, `getHTMLDocument()` returns the `HTMLDocument` object after protection has been applied. ## What `protect()` changes Calling `protect()` does three things: 1. finds `POST` forms in the document 2. prepends a hidden `` containing the token to each matching form 3. creates or updates a `` tag in the page `` ```php $protector->protect(); echo $protector->getHTMLDocument(); ``` Only forms whose method is explicitly `post` are modified. Other forms are left alone. ## Hidden input fields The inserted form field has these attributes: - `name="csrf-token"` - `value=""` - `type="hidden"` The input is inserted before the form's first child. ## The head meta tag The protector also keeps the token in a head meta tag: ```html ``` If a matching meta tag already exists, its `content` attribute is replaced. If the document has no ``, the protector creates one and appends it to the `` element before adding the meta tag. ## One token per page or one token per form The `protect()` method accepts a token-sharing mode: ```php $protector->protect(HTMLDocumentProtector::ONE_TOKEN_PER_PAGE); ``` This is the default. One token is shared by all `POST` forms on the page and by the head meta tag. If needed, we can ask for one token per form: ```php $protector->protect(HTMLDocumentProtector::ONE_TOKEN_PER_FORM); ``` In that mode: - each `POST` form gets a different token - the head meta tag contains all generated tokens joined by commas This is useful when one page can submit several different forms independently without a full page reload in between. ## Pages with no forms If the document contains no `POST` forms, `protect()` still generates and saves one token so the meta tag is available: ```php $protector->protect(); ``` That means JavaScript can still read the token from the `` even if the page itself has no form markup. ## Return value of `protect()` `protect()` returns the last token it generated. - with `ONE_TOKEN_PER_PAGE`, this is the shared page token - with `ONE_TOKEN_PER_FORM`, this is the final form token generated during the loop In most cases it is simpler to work with the document output rather than using the return value directly. --- Next to see how submitted tokens are checked and spent, continue on to [[Verifying requests]]. Once a token has been inserted into the page, the next step is to verify it when the browser submits the form. ## The `verify()` method The main entry point is `TokenStore::verify()`: ```php $tokenStore->verify($_POST); ``` `verify()` expects either: - an array of submitted values - an object with an `asArray()` method That second form exists so the package can work with object-based request input as well as plain PHP arrays. ## What `verify()` checks When `verify()` receives non-empty request data, it checks for a field named `csrf-token`. The method then: 1. throws `CsrfTokenMissingException` if the field is not present 2. calls `verifyToken()` on the submitted value 3. calls `consumeToken()` if the token is valid That final step is important: tokens are single-use. A successful verification spends the token immediately. ## Empty request bodies If the submitted data is empty, `verify()` returns without throwing an exception: ```php $tokenStore->verify([]); ``` This allows the same code path to be called safely before deciding whether the request actually contains form input. ## A common verification pattern ```php use GT\Csrf\Exception\CsrfException; if($_SERVER["REQUEST_METHOD"] === "POST") { try { $tokenStore->verify($_POST); } catch(CsrfException $exception) { http_response_code(403); exit("Invalid CSRF token"); } } ``` This works because all package-specific CSRF exceptions inherit from `CsrfException`. ## Verifying object-based input If your request input is wrapped in an object, `verify()` will accept that too as long as it provides `asArray()`: ```php $tokenStore->verify($postObject); ``` The object does not need to implement a formal interface here. The method simply checks whether `asArray()` can be called. ## Repeated submissions Because a valid token is spent immediately after verification, submitting the same form twice will fail on the second attempt with `CsrfTokenSpentException`. This matters when: - a user double-clicks the submit button - JavaScript retries a request using an old token - one page submits several background requests using the same token In those cases, render or fetch a fresh page so the client receives new tokens. ## Reading the token name The field and meta tag name are defined as: ```php GT\Csrf\HTMLDocumentProtector::TOKEN_NAME ``` The current value is `csrf-token`. --- Next, move on to [[Exceptions]] to see what each failure mode means and how to handle it. This repository uses exceptions to report CSRF verification failures. All package-specific exceptions extend `GT\Csrf\Exception\CsrfException`. ## The exception hierarchy - `CsrfException` - `CsrfTokenMissingException` - `CsrfTokenInvalidException` - `CsrfTokenSpentException` That means we can either catch specific cases or catch the base class. ## `CsrfTokenMissingException` This is thrown when submitted data exists but there is no `csrf-token` field at all. Typical causes: - the form was rendered without calling `protect()` - a client script submitted partial form data and omitted the token - a request was forged manually ## `CsrfTokenInvalidException` This is thrown when a token is present in the request, but it is not known to the current token store. Typical causes: - the token was never generated by this application instance - the token has been evicted because the store exceeded its maximum token count - the request belongs to a different session or environment ## `CsrfTokenSpentException` This is thrown when the token exists but has already been used. The exception message includes the earlier use time in ISO 8601 format, because the store records the consumption timestamp when `consumeToken()` is called. Typical causes: - the form was submitted twice - background requests reused an old token - the browser re-sent stale form data after a previous successful submission ## Catching exceptions Catch the base exception when all failures should be handled in the same way: ```php use GT\Csrf\Exception\CsrfException; try { $tokenStore->verify($_POST); } catch(CsrfException $exception) { http_response_code(403); echo $exception->getMessage(); } ``` Catch specific subclasses when you want to distinguish the response: ```php use GT\Csrf\Exception\CsrfTokenMissingException; use GT\Csrf\Exception\CsrfTokenSpentException; try { $tokenStore->verify($_POST); } catch(CsrfTokenMissingException $exception) { http_response_code(400); } catch(CsrfTokenSpentException $exception) { http_response_code(409); } ``` The package itself does not decide what HTTP response to send. It only reports the verification result. --- If tokens should be stored somewhere other than memory or the session, read [[Custom stores]]. The package includes two token stores, but the API is designed so we can implement our own storage backend when needed. ## When a custom store is useful You might want a custom `TokenStore` when tokens need to live in: - a database table - Redis or another cache service - a framework-specific session abstraction - a distributed store shared between several application instances ## What must be implemented Extend `GT\Csrf\TokenStore` and implement these methods: - `saveToken(string $token): void` - `verifyToken(string $token): void` - `consumeToken(string $token): void` The base class already provides token generation and the high-level `verify()` workflow. ## Example skeleton ```php use GT\Csrf\Exception\CsrfTokenInvalidException; use GT\Csrf\Exception\CsrfTokenSpentException; use GT\Csrf\TokenStore; class DatabaseTokenStore extends TokenStore { public function saveToken(string $token):void { // Persist the token as "not yet used". } public function verifyToken(string $token):void { $record = null; // Load from storage. if(!$record) { throw new CsrfTokenInvalidException($token); } if($record["used_at"] !== null) { throw new CsrfTokenSpentException( $token, $record["used_at"] ); } } public function consumeToken(string $token):void { // Persist the current timestamp as the use time. } } ``` ## Behaviour to preserve When writing a custom store, it helps to preserve the same behaviour as the built-in stores: - a saved token should be valid exactly once - a consumed token should produce `CsrfTokenSpentException` - a missing token should produce `CsrfTokenInvalidException` - old tokens should be removable when a maximum count is exceeded If you need a retention policy, `getMaxTokens()` exposes the configured limit from the base class. ## Using the custom store Once implemented, the custom store can be used anywhere a normal token store is expected: ```php $tokenStore = new DatabaseTokenStore(); $protector = new HTMLDocumentProtector($html, $tokenStore); ``` That includes both page protection and request verification. --- The [[API reference]] is a compact summary of classes, constants and methods. This page summarises the public API in `phpgt/csrf`. ## Namespaces The package now uses the `GT\Csrf` namespace prefix. For backwards compatibility, Composer autoloading in this repository also accepts the legacy `Gt\Csrf` prefix. ## `GT\Csrf\TokenStore` Abstract base class for token generation and verification flow. Generated tokens are returned as prefixed ULIDs beginning with `CSRF_`. The configured token length refers to the ULID portion only, so the full string length is the configured length plus the prefix length. ### Methods - `__construct(?int $maxTokens = null)` - `getMaxTokens(): int` - `setTokenLength(int $newTokenLength): void` - `generateNewToken(): string` - `verify(array|object $postData): void` - `saveToken(string $token): void` - `consumeToken(string $token): void` - `verifyToken(string $token): void` ## `GT\Csrf\ArrayTokenStore` In-memory implementation of `TokenStore`. ### Methods - inherits the constructor from `TokenStore` - `saveToken(string $token): void` - `consumeToken(string $token): void` - `verifyToken(string $token): void` ## `GT\Csrf\SessionTokenStore` Session-backed implementation of `TokenStore`. ### Constants - `SESSION_KEY = "tokenList"` ### Methods - `__construct(Gt\Session\SessionContainer $session, ?int $maxTokens = null)` - `saveToken(string $token): void` - `consumeToken(string $token): void` - `verifyToken(string $token): void` ## `GT\Csrf\HTMLDocumentProtector` Injects tokens into HTML documents. ### Constants - `ONE_TOKEN_PER_PAGE = "PAGE"` - `ONE_TOKEN_PER_FORM = "FORM"` - `TOKEN_NAME = "csrf-token"` ### Methods - `__construct(string|GT\Dom\HTMLDocument $document, TokenStore $tokenStore)` - `protect(string $tokenSharing = self::ONE_TOKEN_PER_PAGE): string` - `getHTMLDocument(): GT\Dom\HTMLDocument` ## Exceptions ### `GT\Csrf\Exception\CsrfException` Base exception for all CSRF verification failures. ### `GT\Csrf\Exception\CsrfTokenMissingException` Thrown when submitted data contains no CSRF token. ### `GT\Csrf\Exception\CsrfTokenInvalidException` Thrown when the submitted token does not exist in the store. ### `GT\Csrf\Exception\CsrfTokenSpentException` Thrown when the submitted token exists but has already been used. ## Dependencies This package depends on: - `phpgt/dom` for `GT\Dom\HTMLDocument` - `phpgt/session` for the built-in session-backed token store --- # Documentation: CssXPath Introduction ============ Both CSS selectors and XPath queries help us find elements in a document, but CSS selectors are usually easier to read and write. The challenge is that tools like `DOMXPath` expect XPath strings, not CSS selectors. `phpgt/CssXPath` bridges that gap by translating selector syntax such as `.menu li.selected > a` into the equivalent XPath query. What this project is for ======================== Use this repository when we want modern selector-style traversal on top of PHP's DOM tooling. Typical use cases: + Implementing `querySelector`-style helpers. + Using CSS selectors in existing `DOMXPath` code. + Writing tests against HTML/XML documents with terse selectors. > [!IMPORTANT] > php.gt/WebEngine heavily uses the Document Object Model (DOM), and HTMLDocument functions such as `querySelectorAll()` take CSS selectors to easily traverse the document. > > All of the functionality within this CssXPath is automatically included within WebEngine via the [PHP.GT/Dom][dom] repository. > > To use CSS selectors within WebEngine, you don't need to require this library yourself - it's included by default, so you should only ever need CssXPath if you're building your own library working with the DOM document. Quick example ============= ```php use DOMDocument; use DOMXPath; use GT\CssXPath\Translator; $html = << HTML; $document = new DOMDocument(); $document->loadHTML($html); $xpath = new DOMXPath($document); $links = $xpath->query(new Translator("nav.c-menu li.selected>a")); echo $links->item(0)?->textContent; // Blog ``` What's covered in this guide? ============================= + [[Quick start]] + [[Using with DOMDocument]] + [[Selector syntax]] + [[Attribute selectors]] + [[Pseudo selectors]] + [[HTML mode and XML mode]] + [[Prefixes and advanced usage]] Support ======= Report bugs and suggest features on the [Github issue tracker](https://github.com/PhpGt/CssXPath/issues). --- Continue to [[Quick start]]. [dom]: https://php.gt/dom Quick start =========== Install with Composer: ```bash composer require phpgt/cssxpath ``` Here we can translate a selector by constructing `Translator` and casting it to a string: ```php use Gt\CssXPath\Translator; $translator = new Translator("form label>input[name=email]"); echo (string)$translator; // .//form//label/input[@name="email"] ``` The constructor accepts three main arguments: + `cssSelector` (required): selector string to translate. + `prefix` (optional, default `.//`): XPath prefix used at the start of each selector. + `htmlMode` (optional, default `true`): whether element/attribute names are matched in HTML mode. You can also call `asXPath()` if you prefer an explicit method name: ```php $xpath = (new Translator(".c-menu li.selected"))->asXPath(); ``` --- Next, we'll learn about [[using with DOMDocument]]. Using with DOMDocument ====================== The common workflow is to load markup into `DOMDocument`, create `DOMXPath`, and pass a `Translator` instance into `query()`. ```php use DOMDocument; use DOMXPath; use Gt\CssXPath\Translator; $document = new DOMDocument(); $document->loadHTMLFile("index.html"); $xpath = new DOMXPath($document); $result = $xpath->query(new Translator("main article p")); foreach($result as $p) { echo trim($p->textContent) . PHP_EOL; } ``` Selecting relative to a node ---------------------------- `DOMXPath::query()` supports a context node. Here we can keep one translator and query different branches of the same document: ```php $allItems = $xpath->query(new Translator("li")); $itemsInNav = $xpath->query( new Translator("li"), $document->getElementsByTagName("nav")->item(0) ); ``` Grouped selectors ----------------- Comma-separated CSS selectors are translated to XPath expressions joined by `|`. ```php echo new Translator("h1, p"); // .//h1 | .//p ``` This lets us fetch mixed sets of nodes using a single query. --- Next up, we'll learn the [[selector syntax]]. Selector syntax =============== This page covers core selector patterns supported by the translator. Basic selectors =============== + Element selector: `p` + Universal selector: `*` + ID selector: `#hero` + Class selector: `.selected` + Combined selector: `nav.c-menu.main-selection` Combinators =========== + Descendant: `article p` + Child: `form>label` + Adjacent sibling: `header + div` + General sibling: `header ~ div` Examples ======== | CSS | XPath | |---|---| | `main article p` | `.//main//article//p` | | `form>label>input` | `.//form/label/input` | | `.content#content-element` | `.//*[contains(concat(' ',normalize-space(@class),' '),' content ')][@id='content-element']` | | `main header ~ div.details` | `.//main//header/following-sibling::div[contains(concat(' ',normalize-space(@class),' '),' details ')]` | Notes ===== + Whitespace around `>` / `+` / `~` is optional. + Multiple class selectors can be chained (`.a.b.c`). + Multiple selectors can be grouped with commas (`h1, p, a.button`). --- Continue learning about [[attribute selectors]]. Attribute selectors =================== Attribute selectors are fully supported, including quoted values and operator variants. Presence and exact match ======================== + `[required]` checks attribute presence. + `[name=email]` exact value match. + `[name='email']` and `[name="email"]` are both supported. Operator support ================ + `[attr*=value]` contains substring. + `[attr~=value]` contains whole word in a space-separated list. + `[attr$=value]` ends with. + `[attr|=value]` exact value or value followed by hyphen. + `[attr^=value]` starts with. Examples ======== | CSS | XPath | |---|---| | `[data-categories*=test]` | `.//*[contains(@data-categories,"test")]` | | `[data-categories~=example]` | `.//*[contains(concat(" ",@data-categories," "),concat(" ","example"," "))]` | | `[data-test-thing$=test]` | `.//*[substring(@data-test-thing,string-length(@data-test-thing) - string-length("test") + 1)="test"]` | | `[class|=en]` | `.//*[@class="en" or starts-with(@class, "en-")]` | | `[class^=class1]` | `.//*[starts-with(@class, "class1")]` | Special cases ============= + Values containing commas are handled correctly, for example: `[data-ga-client='(Test) Message, this has a comma']`. + Values containing square brackets are supported, such as: `[name='choice[]']`. --- Continue learning about [[pseudo selectors]]. Pseudo selectors ================ A focused set of pseudo selectors is currently supported. Boolean attribute pseudo selectors ================================== These map directly to attribute presence checks: + `:checked` + `:disabled` + `:selected` Example: ```css input:checked ``` Structural pseudo selectors =========================== + `:first-child` + `:nth-child(n)` + `:last-child` + `:first-of-type` + `:nth-of-type(n)` + `:last-of-type` Example: ```css form label:nth-of-type(2) input ``` Other supported pseudo selectors ================================ + `:text` maps to `[@type="text"]` (useful for form inputs). + `:contains('Example')` maps to a text contains check. Limitations =========== Only the pseudo selectors listed above are implemented. If we use an unsupported pseudo selector, it is not translated into an XPath predicate. --- Work with different document types with [[HTML mode and XML mode]]. HTML mode and XML mode ====================== `Translator` defaults to HTML mode: ```php new Translator("[data-FOO='bar']"); ``` In HTML mode (`htmlMode: true`): + Element names are treated case-insensitively. + Attribute names are lowered for matching. + Attribute values remain case-sensitive. XML mode ======== When querying XML, pass `htmlMode: false` and usually use `prefix: "//"`. ```php $translator = new Translator( "[data-FOO='bar']", prefix: "//", htmlMode: false ); ``` In XML mode: + Element names are case-sensitive. + Attribute names are case-sensitive. + Attribute values are case-sensitive. Important loading note ====================== For XML behaviour, load content as XML (`load`, `loadXML`, or `loadXMLFile`) rather than HTML loaders (`loadHTML`, `loadHTMLFile`), because HTML loaders normalise names. --- Continue to [[prefixes and advanced usage]]. Prefixes and advanced usage =========================== Custom XPath prefix =================== By default each translated selector starts with `.//`. We can customise this by passing `prefix`. ```php echo new Translator("h1, p", prefix: "descendant-or-self::"); // descendant-or-self::h1 | descendant-or-self::p ``` This is useful when integrating into existing XPath strategies. Using `asXPath()` vs casting ============================ Both of these are equivalent: ```php $xpathA = (string)new Translator(".content"); $xpathB = (new Translator(".content"))->asXPath(); ``` Use whichever reads better in your codebase. Combining predicates ==================== Complex selectors can stack class, id, attribute, and pseudo predicates together. ```css main article .content[data-categories~=test]:nth-child(2) ``` Here we can keep selectors readable while still producing a precise XPath query. Current scope ============= This repository is intentionally lightweight and dependency-free. It focuses on a practical subset of CSS selectors commonly used for DOM traversal and `querySelector`-style APIs. --- Return to [[Home]]. --- # Documentation: Curl cURL is a tool for transferring data to and from a server, and is widely available on most operating systems. It's most commonly used for making HTTP requests, such as downloading web pages, posting form data, or interacting with APIs. PHP comes with bindings to **libcurl**, a library created by Daniel Stenberg, which directly calls the underlying C implementation in a very efficient manner. This library wraps the PHP cURL functions with object oriented counterparts. The purpose is to ease the process of writing unit tests in other software that uses the library, along with normalising the way output is handled. Output handling =============== The only change introduced by this library to how the native cURL functions work is output handling. By default, cURL functions will output data they receive directly to `STDOUT`. It's common to use the `CURLOPT_RETURNTRANSFER` option to change this behaviour, but this starts to become complicated when using multiple concurrent `Curl` handles within a `CurlMulti` object. This library manages the incoming data buffer for you, normalising the method of accessing the response content into two functions: + `output():string` - returns the response body as a string. + `outputJson():JsonObject` - returns the response body as a pre-parsed JsonObject A [`JsonObject`](https://www.php.gt/json) provides type safe getter functions on the response, such as `getString()`, `getBool()`, etc. which is useful when working with well-formed JSON data. ```php $curl = new Curl("https://catfact.ninja/fact"); $curl->exec(); $json = $curl->outputJson(); echo "Here's a cat fact: {$json->getString("fact")}"; echo PHP_EOL; echo "The fact's length is {$json->getInt("length")} characters."; echo PHP_EOL; ``` Read more about the JSON library used at https://www.php.gt/json. HTTP clients and Web Standards ============================== This library was first conceived when work started on [PHP.Gt/Fetch](https://www.php.gt/fetch), a PHP implementation of the [Fetch web standard](https://fetch.spec.whatwg.org/#fetch-method). Fetch is an asynchronous HTTP client, and it made perfect sense to utilise the bulletproof CurlMulti capabilities of libcurl, but using native PHP functions was difficult to unit test. In conclusion, feel free to use this library for your project's HTTP requests, but the intention behind the project is simply to act as the internal data transfer tool within Fetch. *** If you would like to learn more about this library, continue to [[Basic Curl usage]]. The simplest possible usage of this library is to perform an HTTP GET request and display the response body: ```php use GT\Curl\Curl; $curl = new Curl("https://ipapi.co/country_name"); $curl->exec(); echo "Your country is: "; echo $curl->output(), PHP_EOL; ``` Here's an example of how to upload a file by first downloading an image from a random cat image generator, then uploading it and sending to Postman's echo server: ```php use GT\Curl\Curl; use GT\Curl\UploadFile; require(__DIR__ . "/../vendor/autoload.php"); $tmpFile = "/tmp/cat.jpg"; // Download a photo of a cat from cataas.com, save it to the $tmpFile $curl = new Curl("https://cataas.com/cat"); file_put_contents($tmpFile, $curl->output()); // Now POST a form containing the cat photo to the Postman echo test server $upload = new UploadFile($tmpFile); $curl = new Curl("https://postman-echo.com/post"); $curl->setOpt(CURLOPT_POSTFIELDS, [ "cat-photo" => $upload ]); $curl->exec(); echo $curl->output(); // Remove the temporary file before finishing unlink($tmpFile); ``` *** In the next section we will learn about [[working with JSON]]. Combining JSON with Curl is such a common task for web developers, the [PHP.GT/Json](https://www.php.gt/json) library is bundled by default, which allows you to work with JSON requests and responses in a type-safe way. Where `Curl::output()` returns a `string` representation of the HTTP response body, `Curl::outputJson()` returns a well-formed `JsonObject` that can be traversed with type safety. Here's an example that makes a request to the https://catfact.ninja/ API, then echoes the data and it receives as a `string` and an `int`: ```php use GT\Curl\Curl; $curl = new Curl("https://catfact.ninja/fact"); $curl->exec(); $json = $curl->outputJson(); echo "Here's a cat fact: " . $json->getString("fact"); echo PHP_EOL; echo "The fact's length is " . $json->getInt("length") . " characters."; echo PHP_EOL; ``` To learn more about the different ways you can work with JSON, take a look at https://www.php.gt/json. *** Next up, learn how to execute multiple [[concurrent curl requests]]. So far we've only executed individual HTTP requests. We create a `Curl` object, execute it, then read the response. If we want to execute multiple HTTP requests as part of our script, it can be inefficient to run multiple requests one after another. For example, if we have 10 HTTP requests to make that all take 1 second to execute, running them one after another will take 10 seconds to complete. However, if we ran all 10 requests concurrently, our script would only take 1 second to complete, because all HTTP requests can be executed in parallel. The way to do this is to build each individual `Curl` object as usual, but instead of calling `exec()` on them directly, we add them to a new `CurlMulti` object, and call `exec` on that instead. The `CurlMulti::exec()` function returns the number of requests still active, so while the returned number is greater than 0, there is still work to do - we should wait a while, then call `exec` again. Here's an example that makes three individual requests, executes them concurrently within a `CurlMulti` object, waits for them all to complete, then outputs all the responses: ```php use GT\Curl\Curl; use GT\Curl\CurlMulti; $curlCat = new Curl("https://catfact.ninja/fact"); $curlIp = new Curl("https://api.ipify.org/?format=json"); $curlTimeout = new Curl("https://this-domain-name-does-not-exist.example.com/nothing.json"); $multi = new CurlMulti(); $multi->add($curlCat); $multi->add($curlIp); $multi->add($curlTimeout); $stillRunning = 0; do { $multi->exec($stillRunning); usleep(100_000); echo "."; } while($stillRunning > 0); echo PHP_EOL; echo "Cat API response: " . $multi->getContent($curlCat) . PHP_EOL; echo "IP API response: " . $multi->getContent($curlIp) . PHP_EOL; echo "Timeout API response: " . $multi->getContent($curlTimeout) . PHP_EOL; ``` Streaming responses ------------------- In the above example, we are running the three requests concurrently and echoing the responses after all the responses have completed. In some situations, it may be beneficial to receive the response as soon as **any** content is received, rather than having to wait for the entire response to complete its download. For example, when large responses are expected, we can stream the incoming data somewhere to process the information in a memory efficient manner, rather than loading the entire response data into memory. To do this, we can set a header/write function on the individual Curl objects. This function will be called whenever any bytes are received from the server. To process each HTTP header as it's received, set the `CURLOPT_HEADERFUNCTION` option. To process the incoming response, whenever any bytes are received, set the `CURLOPT_WRITEFUNCTION` option. The value of this option is a function or other `callable` that takes the following signature: ```php function(CurlHandle $ch, string $buffer):int; ``` The first parameter is the native PHP `CurlHandle`. The second parameter is the most important - the incoming data. The header callback is be called once for each header and only complete header lines are passed on to the callback, ending with a newline character. The write callback is called as soon as there is data received that needs to be saved. For most transfers, this callback gets called many times and each invoke delivers another chunk of data. Here's an example that uses the same three APIs, but this time echoes the response headers and body as soon as they are received. The echoes will likely execute out of order, so if this were being used for a real project, the CurlHandle would need to be kept track of to save the responses to the correct locations. ```php use GT\Curl\Curl; use GT\Curl\CurlMulti; $urlArray = [ "https://catfact.ninja/fact", "https://api.ipify.org/?format=json", "https://this-domain-name-does-not-exist.example.com/nothing.json", ]; $multi = new CurlMulti(); foreach($urlArray as $url) { $curl = new Curl($url); $curl->setOpt(CURLOPT_HEADERFUNCTION, function ($ch, string $rawHeader):int { echo "HEADER: $rawHeader"; return strlen($rawHeader); }); $curl->setOpt(CURLOPT_WRITEFUNCTION, function ($ch, string $rawBody):int { echo "BODY: $rawBody\n"; return strlen($rawBody); }); $curl->setOpt(CURLOPT_TIMEOUT, 10); $multi->add($curl); } $stillRunning = 0; do { $multi->exec($stillRunning); usleep(10_000); echo "."; } while($stillRunning > 0); echo PHP_EOL; ``` *** The Fetch API ------------- This library was created to provide a solid foundation for [PHP.Gt/Fetch], which is a more featured library that is implemented to be compatible with web standards. Behind the scenes, Fetch uses this Curl library to manage the HTTP transport, but has a Promise-based interface that is familiar with web developers outside of the PHP community. For more information, see https://www.php.gt/fetch. [PHP.Gt/Fetch]: https://www.php.gt/fetch --- # Documentation: Daemon A software daemon is a background process that runs continuously on a system, often to perform tasks or provide services without direct user interaction. This repository provides a simple API to create and manage running processes, and allows you to bundle processes into pools to be interacted with as a single unit. Daemon lets our PHP code start operating system commands, read their output while they run, and group several commands together as one pool. This is useful when a PHP script needs to supervise another process without waiting for it to finish straight away. For example, we might start a development server, run a build step, or stream the output of several long-running commands into one CLI program. > [!NOTE] > This package can be used on its own with Composer, but is included by default in WebEngine projects, where it is normally used in higher-level commands, such as `gt start`, rather than instantiating `GT\Daemon\Process` from page logic. ## Requirements Install the package with Composer: ```bash composer require phpgt/daemon ``` The package requires PHP 8.1 or newer and the `pcntl` extension. It also uses PHP's built-in `proc_open` functions, so it needs to run in an environment where process execution is allowed. ## A small example ```php use GT\Daemon\Process; $process = new Process(PHP_BINARY, "-r", "echo 'Hello from the child process' . PHP_EOL;"); $process->exec(); while($process->isRunning()) { echo $process->getOutput(); usleep(100_000); } echo $process->getOutput(); echo "Exit code: " . $process->getExitCode() . PHP_EOL; ``` The command is passed as separate arguments. In the example above, `PHP_BINARY` is the executable, `-r` is the first argument, and the inline PHP code is the second argument. --- The [[overview]] page shows how to run one command and read its output. In this guide we will start one command, read from it while it runs, then check how it ended. ## 1. Create a process ```php use GT\Daemon\Process; $process = new Process( "/usr/bin/ping", "-c", "3", "github.com", ); ``` The constructor accepts the command as separate arguments. The first argument is the executable. The remaining arguments are passed to that executable. > [!IMPORTANT] > Do not pass a whole shell command as one string unless the executable really has that name. For example, use `new Process("echo", "hello")`, not `new Process("echo hello")` wherever possible, so PHP can escape the arguments safely. If we need shell features such as pipes, redirects or command chaining, run a shell explicitly: ```php $process = new Process("sh", "-c", "printf 'Hello' | wc -c"); ``` ## 2. Execute the process ```php $process->exec(); ``` By default, `exec()` starts the process and returns while the child process is still running. ## 3. Read output while it runs ```php do { echo $process->getOutput(); usleep(100_000); } while($process->isRunning()) ``` The output pipes are non-blocking, so `getOutput()` returns whatever is currently available. It can return an empty string when the process is still running but has not written anything new. The last `getOutput()` call after the loop collects any remaining output written just before the process finished. ## 4. Check the exit code ```php $exitCode = $process->getExitCode(); echo "Process ended with code $exitCode" . PHP_EOL; ``` `getExitCode()` returns `null` while the process is still running. Once it has ended, it returns the command's exit code. ## Reading errors Standard error is kept separate from standard output: ```php while($process->isRunning()) { echo $process->getOutput(); fwrite(STDERR, $process->getErrorOutput()); usleep(100_000); } ``` This lets us decide whether error output should be shown, logged, filtered or ignored. --- Continue with [[Process]] to see the options available on a single process. `Process` represents one running command. We create it with the command arguments, configure anything that needs to change, then call `exec()` to start it. ```php use GT\Daemon\Process; $process = new Process("php", "worker.php", "--queue", "emails"); $process->exec(); ``` ## Working directory By default, the process starts in the current working directory of the PHP script. We can change that before calling `exec()`: ```php $process = new Process("php", "worker.php"); $process->setExecCwd("/path/to/project"); $process->exec(); ``` This is useful when a command expects relative file paths. ## Environment variables The child process receives the current environment, plus any values we set: ```php $process = new Process("printenv"); $process->setEnv("APP_ENV", "dev"); $process->setEnv("QUEUE", "emails"); $process->exec(); ``` If the key already exists in the current environment, the value we set is used for the child process. ## Blocking mode Most of the time we want `exec()` to return straight away. If we want `exec()` to wait until the command finishes, call `setBlocking()` first: ```php $process = new Process("sleep", "1"); $process->setBlocking(); $process->exec(); echo $process->getExitCode(); ``` Blocking mode is useful for short commands where we still want to use the same output and exit-code API. ## Process state ```php if($process->isRunning()) { echo "Process " . $process->getPid() . " is still running"; } ``` `getPid()` returns the process ID while the process is running, or `null` once it has ended. `getExitCode()` returns `null` while the process is running. After it ends, it returns the exit code. ## Completion callbacks Completion callbacks run once when the process is observed to have finished: ```php $process->onComplete(function(Process $process):void { echo "Finished with code " . $process->getExitCode() . PHP_EOL; }); $process->exec(); while($process->isRunning()) { usleep(100_000); } ``` The callback is dispatched when methods such as `exec()`, `isRunning()` or `getExitCode()` refresh the process status. If we register a callback after the process has already completed, it is called immediately. Callbacks are not a separate event loop. Our code still needs to check the process state while it is running. ## Terminating a process ```php use GT\Daemon\Signal; $process->terminate(Signal::TERM); ``` `terminate()` sends a signal to the process and closes the input, output and error pipes. If the process has not been started, it does nothing. The default signal is `Signal::TERM`. Other signal constants are listed on [[Reference]]. The process is also terminated when the `Process` object is destroyed. ## Command failures If the executable cannot be started, `exec()` throws `CommandNotFoundException`: ```php use GT\Daemon\CommandNotFoundException; try { $process = new Process("/does/not/exist"); $process->exec(); } catch(CommandNotFoundException $exception) { echo "The command could not be started"; } ``` All package exceptions extend `DaemonException`. --- Next, continue with [[Pool]] to run several processes together. `Pool` gives names to several `Process` objects so we can start, read and close them together. ```php use GT\Daemon\Pool; use GT\Daemon\Process; $pool = new Pool(); $pool->add("letters", new Process(PHP_BINARY, "letters.php")); $pool->add("numbers", new Process(PHP_BINARY, "numbers.php")); $pool->exec(); ``` ## Reading from the pool `read()` collects standard output from every process and prefixes each line with the process name: ```php while($pool->numRunning() > 0) { echo $pool->read(); usleep(100000); } echo $pool->read(); ``` Example output: ```text [letters] A [numbers] 1 [letters] B [numbers] 2 ``` The prefix is added by the pool. The process itself still writes plain output. ## Reading errors Error output is handled separately: ```php fwrite(STDERR, $pool->readError()); ``` Error lines are prefixed with the process name and `ERROR`: ```text [letters ERROR] Could not open dictionary.txt ``` We can also read from one named process: ```php echo $pool->readOutputOf("letters"); fwrite(STDERR, $pool->readErrorOf("letters")); ``` If there is no process with that name, `readOutputOf()` and `readErrorOf()` throw `DaemonException`. ## Counting running processes ```php while($pool->numRunning() > 0) { echo $pool->read(); usleep(100000); } ``` `numRunning()` checks every process and returns the number that are still running. ## Completion callbacks A pool callback receives the `Process` that completed: ```php $pool->onComplete(function(Process $process):void { echo "One process has finished with code " . $process->getExitCode() . PHP_EOL; }); ``` The callback is connected to each process when it is added to the pool. Like process callbacks, pool callbacks run when the process status is checked. In most pool loops, that happens through `numRunning()`. ## Closing the pool ```php $exitCodes = $pool->close(); ``` `close()` sends the default termination signal to every process, waits until each one has an exit code, and returns an associative array of process name to exit code. For example: ```php [ "letters" => 0, "numbers" => 9, ] ``` Use `close()` when our supervising script is finished with the child processes and needs to tidy them up. --- Next, continue with [[Reference]] for a compact list of methods and signal constants. This page lists the public classes and methods provided by the package. ## Process ```php use GT\Daemon\Process; ``` `new Process(string ...$command)` : Stores the executable and its arguments. The process does not start until `exec()` is called. `setExecCwd(string $cwd): void` : Sets the working directory used when the process starts. `setEnv(string $key, string $value): void` : Sets one environment variable for the child process. `onComplete(callable $callback): void` : Registers a callback that receives this `Process` once it has finished. `setBlocking(bool $blocking = true): void` : Controls whether `exec()` waits for the command to finish. `exec(): void` : Starts the process. Throws `CommandNotFoundException` if the command cannot be started. `isRunning(): bool` : Returns whether the process is still running. `hasNotEnded(): bool` : Returns `true` while the process is running, or after it has ended with exit code `0`. `getCommand(): array` : Returns the command array passed to the constructor. `getOutput(int $pipe = Process::PIPE_OUT): string` : Reads all currently available output from the selected pipe. By default, it reads standard output. `getErrorOutput(): string` : Reads all currently available output from standard error. `getExitCode(): ?int` : Returns `null` while the process is running, then the exit code after it has ended. `getPid(): ?int` : Returns the process ID while the process is running, otherwise `null`. `terminate(int $signal = Signal::TERM): void` : Sends a signal to the process and closes its pipes. ## Pool ```php use GT\Daemon\Pool; ``` `add(string $name, Process $process): void` : Adds a process to the pool under the supplied name. `onComplete(callable $callback): void` : Registers a callback that receives each `Process` as it finishes. `exec(): void` : Starts every process in the pool. `numRunning(): int` : Returns the number of processes still running. `read(int $pipe = Process::PIPE_OUT): string` : Reads currently available output from all processes, prefixing each line with the process name. `readError(): string` : Reads standard error from all processes. `readOutputOf(string $name, int $pipe = Process::PIPE_OUT): string` : Reads currently available output from one named process. `readErrorOf(string $name): string` : Reads standard error from one named process. `close(): array` : Terminates every process and returns an associative array of exit codes. ## Pipe constants ```php Process::PIPE_IN Process::PIPE_OUT Process::PIPE_ERROR ``` Most code only needs `PIPE_OUT` and `PIPE_ERROR`. ## Signal constants `Signal` exposes POSIX signal constants for use with `Process::terminate()`: ```php use GT\Daemon\Signal; $process->terminate(Signal::TERM); ``` Available constants are: ```text ABRT, ALRM, BABY, BUS, CHLD, CLD, CONT, FPE, HUP, ILL, INT, IO, IOT, KILL, PIPE, POLL, PWR, QUIT, URG, USR1, USR2, SEGV, STKFLT, STOP, SYS, TSTP, TERM, TTIN, TTOU, TRAP, WINCH, XCPU, XFSZ ``` These constants map directly to PHP's `SIG*` constants from the `pcntl` extension. ## Exceptions `DaemonException` : Base exception for this package. `CommandNotFoundException` : Thrown when a process command cannot be started. --- # Documentation: DataObject `DataObject` is a small immutable container for structured data. It is designed for the common case where we want to move data around an application without exposing a mutable array or `stdClass` to every part of the codebase. The public API is centred around two classes: - `GT\DataObject\DataObject`, which stores and reads the data - `GT\DataObject\DataObjectBuilder`, which creates `DataObject` instances from existing PHP structures ## What this library does - Stores key/value data in an immutable object. - Provides typed getters such as `getString()`, `getInt()` and `getDateTime()`. - Supports nested `DataObject` instances. - Converts back to arrays, plain objects and JSON. - Builds data objects from associative arrays or standard objects. - Checks array contents against primitive or class types when required. ## Where this library fits in This package is fully standalone: ```bash composer require phpgt/dataobject ``` It is also installed as a dependency of PHP.GT/WebEngine, so you can use it directly in WebEngine applications without extra setup. The examples in this guide stay framework-independent so the API is clear in isolation. Other PHP.GT packages also build on the same idea. For example, [PHP.GT/Fetch][fetch] returns structured response objects, and [PHP.GT/Json][json] extends this package for JSON-specific behaviour. ## A tiny example ```php use GT\DataObject\DataObject; $user = (new DataObject()) ->with("id", 105) ->with("name", "Cody") ->with("active", true); echo $user->getInt("id"), PHP_EOL; echo $user->getString("name"), PHP_EOL; echo $user->getBool("active") ? "yes" : "no", PHP_EOL; ``` Each call to `with()` returns a new object, leaving the previous instance unchanged. --- Continue with [[Quick start]] to install the package and build the first `DataObject`. [fetch]: https://www.php.gt/fetch [json]: https://www.php.gt/json In this guide we will install `phpgt/dataobject`, create a `DataObject` directly, then build one from an existing PHP structure. > [!NOTE] > In WebEngine, this package is already present as a dependency, so you can use `GT\DataObject\DataObject` and `GT\DataObject\DataObjectBuilder` directly from your page logic or application classes. ## 1. Install the package ```bash composer require phpgt/dataobject ``` ## 2. Create a `DataObject` directly The simplest way to start is to create an empty object and add keys with `with()`: ```php use GT\DataObject\DataObject; $product = new DataObject() ->with("name", "Notebook") ->with("price", 7.99) ->with("inStock", true); ``` `DataObject` is immutable, so `with()` does not modify the existing instance. It returns a new one with the extra key. ## 3. Read values back out ```php echo $product->getString("name"), PHP_EOL; echo $product->getFloat("price"), PHP_EOL; echo $product->getBool("inStock") ? "yes" : "no", PHP_EOL; ``` The typed getters cast or convert where it makes sense: - `getString()` casts scalars to strings - `getInt()` casts strings, floats and booleans to integers - `getFloat()` casts strings, integers and booleans to floats - `getBool()` casts common PHP scalar values to booleans - `getDateTime()` converts strings, integers and floats to `DateTimeImmutable` ## 4. Build from an associative array If the data already exists in PHP, use `DataObjectBuilder`: ```php use GT\DataObject\DataObjectBuilder; $builder = new DataObjectBuilder(); $product = $builder->fromAssociativeArray([ "id" => 105, "name" => "Notebook", "tags" => ["stationery", "paper"], "supplier" => [ "name" => "Northwind", "country" => "UK", ], ]); ``` Associative arrays are turned into nested `DataObject` instances. Indexed arrays stay as arrays. ## 5. Build from an object The same builder can work from a plain object: ```php $source = new StdClass(); $source->id = 105; $source->name = "Notebook"; $source->supplier = new StdClass(); $source->supplier->name = "Northwind"; $product = $builder->fromObject($source); echo $product->getObject("supplier")?->getString("name"); ``` At this point we have the shape of the library: build an immutable object once, then pass it around safely. --- Next, move on to [[Building]] to see exactly how arrays, objects, nesting and builder errors behave. This page focuses on `DataObjectBuilder`, which turns existing PHP data into immutable `DataObject` instances. ## Building from associative arrays Use `fromAssociativeArray()` when the source data is an array of key/value pairs: ```php use GT\DataObject\DataObjectBuilder; $builder = new DataObjectBuilder(); $order = $builder->fromAssociativeArray([ "id" => 501, "status" => "packed", "customer" => [ "name" => "Ada", "email" => "ada@example.com", ], ]); ``` In the example above: - `id` and `status` stay as scalar values - `customer` becomes a nested `DataObject` If a value is an indexed array, it remains an array: ```php $order = $builder->fromAssociativeArray([ "items" => [ ["sku" => "A1"], ["sku" => "B2"], ], ]); ``` Each associative array inside the indexed `items` array is converted into a `DataObject`. ## Building from objects Use `fromObject()` when the source is a plain PHP object: ```php $source = new StdClass(); $source->id = 501; $source->customer = new StdClass(); $source->customer->name = "Ada"; $order = $builder->fromObject($source); ``` Nested objects are converted into nested `DataObject` instances. If an object property contains an indexed array, the array stays an array. Any object elements inside that array are converted recursively. ## Empty arrays Empty arrays are preserved in both builder methods: ```php $settings = $builder->fromAssociativeArray([ "enabled" => true, "flags" => [], ]); var_dump($settings->getArray("flags")); // [] ``` ## Builder exceptions The builder deliberately keeps the source shape consistent. `fromObject()` expects objects and indexed arrays. If it finds an associative array inside an object structure, it throws `AssociativeArrayWithinObjectException`. `fromAssociativeArray()` expects associative arrays and indexed arrays. If it finds an object directly inside an associative array structure, it throws `ObjectWithinAssociativeArrayException`. These exceptions both extend `BuilderException`, which extends `DataObjectException`. ## Using your own subclass `fromObject()` accepts an optional class name as its second argument: ```php use GT\DataObject\DataObject; class UserData extends DataObject {} $source = new StdClass(); $source->name = "Ada"; $source->profile = new StdClass(); $source->profile->city = "Leeds"; $user = $builder->fromObject($source, UserData::class); ``` The root object and any nested objects built from that source will use `UserData`. This custom-class option is only available on `fromObject()`. `fromAssociativeArray()` always returns `DataObject`. --- Next, continue with [[Reading]] to see how values are read safely once the data is inside a `DataObject`. Once the data is inside a `DataObject`, the public API is deliberately small. ## `get()` `get()` returns the raw stored value: ```php use GT\DataObject\DataObject; $user = (new DataObject()) ->with("name", "Ada") ->with("age", 27); var_dump($user->get("name")); // "Ada" var_dump($user->get("age")); // 27 ``` If the key does not exist, `get()` returns `null`. ## Typed getters `DataObject` implements the `TypeSafeGetter` contract, so it provides: - `getString()` - `getInt()` - `getFloat()` - `getBool()` - `getDateTime()` - `getInstance()` Example: ```php use GT\DataObject\DataObject; $user = new DataObject() ->with("id", "105") ->with("active", 1) ->with("joined", "2024-03-01 09:30:00"); echo $user->getInt("id"), PHP_EOL; echo $user->getBool("active") ? "yes" : "no", PHP_EOL; echo $user->getDateTime("joined")?->format("Y-m-d"), PHP_EOL; ``` The typed getters return `null` when the key is missing or explicitly contains `null`. ## Working with nested objects Use `getObject()` when you expect another `DataObject` of the same class: ```php use GT\DataObject\DataObject; $address = (new DataObject()) ->with("town", "Leeds"); $user = (new DataObject()) ->with("address", $address); echo $user->getObject("address")?->getString("town"); ``` Because `getObject()` uses `static`, it also works correctly with subclasses. If you want a different object type, use `getInstance()`: ```php $value = $user->getInstance("address", DataObject::class); ``` ## `contains()` and `typeof()` `contains()` tells you whether `isset()` succeeds for a key: ```php use GT\DataObject\DataObject; $data = new DataObject() ->with("name", "Ada") ->with("empty", null); var_dump($data->contains("name")); // true var_dump($data->contains("empty")); // false ``` That means a key set to `null` counts as not contained. See [bug #54](https://github.com/phpgt/DataObject/issues/54). If you need to distinguish between a missing key and a key explicitly set to `null`, use `typeof()`: ```php var_dump($data->typeof("name")); var_dump($data->typeof("empty")); var_dump($data->typeof("missing")); ``` `typeof()` returns: - primitive names such as `string`, `int`, `float`, `bool` and `null` - the class name for objects - `null` if the key does not exist at all ## Removing keys `without()` is the inverse of `with()`: ```php use GT\DataObject\DataObject; $user = new DataObject() ->with("name", "Ada") ->with("email", "ada@example.com"); $publicUser = $user->without("email"); ``` This is often useful when the same source data is passed to two different parts of the system but one of them should not see everything. --- Next, move on to [[Converting]] to see how arrays, JSON and fixed-type array reads work. There are two common reasons to convert a `DataObject` back out again: - a caller expects an array or plain object - one key contains an array and you want to read it with fixed element types ## `getArray()` Use `getArray()` to read an array value: ```php use GT\DataObject\DataObject; $report = new DataObject() ->with("scores", ["1", "2", "3.5"]); $scores = $report->getArray("scores"); ``` If the key is missing, `getArray()` returns `null`. If the stored value is a nested `DataObject`, `getArray()` first converts it with `asArray()`. ## Fixed-type arrays Pass a second argument to cast or validate each element: ```php use GT\DataObject\DataObject; $report = new DataObject() ->with("scores", ["1", "2", "3.5"]); $scores = $report->getArray("scores", "int"); var_dump($scores); // [1, 2, 3] ``` For built-in primitive checks, each element is cast individually. Common examples are: - `"int"` - `"float"` - `"bool"` - `"string"` Class and interface names work too: ```php use DateTimeImmutable; use DateTimeInterface; use GT\DataObject\DataObject; $dates = new DataObject() ->with("dates", [ new DateTimeImmutable("2024-01-01"), new DateTimeImmutable("2024-02-01"), ]); $typedDates = $dates->getArray("dates", DateTimeInterface::class); ``` When a class or interface name is used, values are not cast. They must already be instances of the requested type, otherwise `TypeError` is thrown. Passing a type name that does not exist also throws `TypeError`. ## `asArray()` `asArray()` returns the whole object as a PHP array: ```php use GT\DataObject\DataObject; $user = new DataObject() ->with("name", "Ada") ->with("address", (new DataObject())->with("town", "Leeds")); print_r($user->asArray()); ``` Nested `DataObject` instances are converted recursively. ## `asObject()` `asObject()` performs the same recursive conversion, but returns a plain object graph: ```php $userObject = $user->asObject(); echo $userObject->address->town; ``` ## JSON serialisation `DataObject` implements `JsonSerializable`, and `jsonSerialize()` delegates to `asArray()`: ```php echo json_encode($user), PHP_EOL; ``` That means a nested `DataObject` becomes nested JSON objects and arrays in the final output. --- Next, have a look at [[Examples]] for a few complete patterns you can adapt in your own project. This page collects a few small complete examples. ## 1. Build a DTO and serialise it to JSON ```php use GT\DataObject\DataObject; $user = (new DataObject()) ->with("id", 105) ->with("name", "Cody") ->with("roles", ["author", "editor"]); echo json_encode($user), PHP_EOL; ``` Output: ```json {"id":105,"name":"Cody","roles":["author","editor"]} ``` ## 2. Start from decoded JSON ```php use GT\DataObject\DataObjectBuilder; $json = <<<'JSON' { "name": "Cody", "address": { "town": "Leeds" }, "food": [ "biscuits", "mushrooms" ] } JSON; $builder = new DataObjectBuilder(); $user = $builder->fromObject(json_decode($json)); echo $user->getString("name"), PHP_EOL; echo $user->getObject("address")?->getString("town"), PHP_EOL; echo $user->getArray("food")[0], PHP_EOL; ``` ## 3. Remove private fields before passing data on ```php use GT\DataObject\DataObject; $user = (new DataObject()) ->with("name", "Cody") ->with("email", "cody@example.com") ->with("creditCard", "4111 1111 1111 1111"); $shippingCopy = $user->without("creditCard"); ``` `$user` still contains the original data. `$shippingCopy` is a separate immutable object without the sensitive key. ## 4. Cast each element of an array ```php use GT\DataObject\DataObject; $scores = (new DataObject()) ->with("scores", ["1", "2", "3.14159"]); $wholeNumbers = $scores->getArray("scores", "int"); print_r($wholeNumbers); ``` Output: ```text Array ( [0] => 1 [1] => 2 [2] => 3 ) ``` ## 5. Build a custom subclass from an object ```php use GT\DataObject\DataObject; use GT\DataObject\DataObjectBuilder; class UserData extends DataObject {} $source = (object)[ "name" => "Cody", "profile" => (object)[ "town" => "Leeds", ], ]; $builder = new DataObjectBuilder(); $user = $builder->fromObject($source, UserData::class); ``` Both `$user` and `$user->getObject("profile")` will be instances of `UserData`. --- # Documentation: Database This library gives us a tidy way to keep SQL close to the application, without mixing query strings throughout our PHP code. We can organise queries into collections, bind values safely, fetch predictable PHP types, and keep schema changes under version control. > [!NOTE] > If you are building with WebEngine, most of the setup is already done for you. The framework wires up the database service, reads configuration, and exposes the migration command through `gt migrate`. The WebEngine overview page is at https://www.php.gt/docs/webengine/database/. ## What this library does - Stores queries in files or PHP query classes. - Executes named queries through one `Database` object. - Supports positional and named parameter binding. - Returns rows and result sets through consistent helper types. - Supports multiple named connections. - Runs schema migrations with integrity checks. - Supports branch-local development migrations as well as canonical migrations. ## Recommended reading order 1. [[Quick start guide]] 2. [[Configuration and connections]] 3. [[Query collections]] 4. [[Parameter binding]] 5. [[Raw SQL and result sets]] 6. [[Type-safe getters]] 7. [[Database migrations]] 8. [[Examples]] ## A tiny example ```php use Gt\Database\Connection\Settings; use Gt\Database\Database; $settings = new Settings( "query", Settings::DRIVER_SQLITE, "app.sqlite" ); $db = new Database($settings); $userRow = $db->fetch("user/getById", 42); echo "User email address: " . $userRow->getString("email"); ``` In a nutshell: queries live on disk, the PHP code calls them by name, and the result comes back through a small type-safe API. --- To see the full setup from scratch, move on to the [[Quick start guide]]. In this section we will build a tiny project that can run one query against a SQLite database. > [!TIP] > In WebEngine, the same query layout is used, but you usually do not instantiate `Database` yourself. You write queries in `query/`, configure the database in `config.ini`, and use the injected database service from your page logic. See https://www.php.gt/webengine/database/. ## 1. Install the library ```bash composer require phpgt/database ``` ## 2. Create a query directory `mkdir -p query/user` Create the file `query/user/getById.sql`: ```sql select id, email from user where id = ? limit 1 ``` Calling `user/getById` later will resolve to this file. ## 3. Create a database connection ```php use Gt\Database\Connection\Settings; use Gt\Database\Database; $settings = new Settings( "query", Settings::DRIVER_SQLITE, "app.sqlite" ); $db = new Database($settings); ``` This example uses SQLite because it is simple to start with and does not need a running server. ## 4. Run the query ```php $userRow = $db->fetch("user/getById", 105); echo $userRow?->getString("email"); ``` The first argument is always the query name. The remaining arguments are the values to bind. ## 5. Build out the rest of the CRUD layer Once the first query works, it is natural to add more files: - `query/user/insert.sql` - `query/user/updateEmail.sql` - `query/user/delete.sql` Then we can call them in the same style: ```php $newId = $db->insert("user/insert", [ "email" => "dev@example.com", ]); $rowsUpdated = $db->update("user/updateEmail", [ "id" => $newId, "email" => "new@example.com", ]); $rowsDeleted = $db->delete("user/delete", $newId); ``` At this point the basics are in place: query files, one connection, and a consistent PHP API. --- Next, move on to [[Configuration and connections]] so we can see how the connection object is configured properly. The `Settings` class describes where queries live and how to connect to the database. ## The constructor at a glance The constructor arguments are: 1. base query directory 2. driver 3. schema or SQLite file path 4. host 5. port 6. username 7. password 8. connection name 9. collation 10. charset 11. initial SQL to run after connecting In practice, most projects only need the first three for SQLite, or the first seven for a server database. ## Common driver examples SQLite: ```php $settings = new Settings( "query", Settings::DRIVER_SQLITE, "app.sqlite" ); ``` MySQL: ```php $settings = new Settings( "query", Settings::DRIVER_MYSQL, "my_website", "localhost", 3306, "app_user", "app_pass", ); ``` ## Immutable configuration helpers `Settings` uses an immutable style. If we change one value, we get back a new object. ```php $reportingSettings = $settings ->withConnectionName("reporting") ->withHost("db.internal") ->withPort(3307); ``` This keeps connection changes explicit and third parties cannot modify them. ## Multiple connections If we add more than one `Settings` object to `Database`, each connection can have its own name. ```php use Gt\Database\Database; $db = new Database($mainSettings, $reportingSettings); $rows = $db->queryCollection("event", "reporting") ->fetchAll("listRecent"); ``` For raw SQL, the connection name is the third argument: ```php $db->executeSql("select 1", [], "reporting"); ``` ## PHP query namespace If we use PHP query classes instead of SQL files, the default namespace is `\App\Query`. ```php $db->setAppNameSpace("MyWebsite\\Query"); ``` > [!NOTE] > In WebEngine, most projects express these settings in `config.ini` rather than building `Settings` manually. The framework then constructs the database service for you. The WebEngine guide is at https://www.php.gt/docs/webengine/database. --- Next, move on to [[Query collections]] to see how query names map cleanly to files and classes. A query collection is simply a group of related queries, bundled within a directory of SQL or PHP files. Most of the time, this means a directory in `query/`: ```text query/ └── user/ ├── getById.sql ├── insert.sql └── updateEmail.sql ``` ## Calling SQL query files If we create `query/user/getById.sql`, we call it like this: ```php $row = $db->fetch("user/getById", 105); ``` The path maps directly to the query name, which makes it easy to find the SQL later. These separators are supported: - `user/getById` - `user.getById` - `user\\getById` Using `/` is the clearest option and is the recommended style in these docs. ## Nested collections Collections can be nested when that structure is useful. ```php $db->fetch("admin/audit/listRecent"); ``` That would resolve to `query/admin/audit/listRecent.sql`. ## Working with a `QueryCollection` object Sometimes it is helpful to pass only part of the database API into another class. This allows the concept of encapsulation to be applied to the database layer. ```php $userDb = $db->queryCollection("user"); $user = $userDb->fetch("getById", 105); $allUsers = $userDb->fetchAll("listAll"); $newId = $userDb->insert("insert", [ "email" => "dev@example.com", ]); ``` This is a tidy way to keep a repository or service focused on one area of the schema. Note that only the query name is required to be passed as the first paramter to the fetch/fetchAll queries, and that the `$userDb` reference can now be passed to areas of code related to working with the user records - the code can call all of the queries within the provided query collection, but are unable to call queries outside of the collection. As an example of when this could be used: a `ShippingService` will need to query within the `shipping` query collection, but should be prohibited from calling queries within the `payment` collection. ## PHP query collections PHP query classes are also supported, for when SQL queries add limitations. ```php namespace App\Query; use Gt\SqlBuilder\SelectBuilder; class Product { public function listAll():SelectBuilder { return new SelectBuilder() ->select("id", "name") ->from("product"); } public function listByCategory():SelectBuilder { return $this->listAll()->where("category = :category"); } } ``` Then we can call: ```php $db->fetchAll("Product/listByCategory", [ "category" => "books", ]); ``` > [!TIP] > PHP query classes are most helpful when we want to generate SQL programmatically, often with `phpgt/sqlbuilder`. The main SQL Builder docs live separately at https://www.php.gt/sqlbuilder. > [!NOTE] > In WebEngine, the same query collection layout is used under the project `query/` directory. This means the structure you learn here transfers directly into a WebEngine application. --- Next, move on to [[Parameter binding]] so we can pass values into these queries safely and readably. Parameter binding is what lets us keep SQL readable while passing values safely from PHP. ## Positional placeholders Use `?` when the query only needs a small number of values and the order is obvious. ```sql select id, name, email from user where id = ? limit 1 ``` ```php $userRow = $db->fetch("user/getById", 105); ``` This style is compact and clear when there is only one or two values to bind. ## Named placeholders Use named placeholders when the meaning of the values matters more than their order. ```sql select id, name, email from user where email = :email and isActive = :isActive ``` ```php $userRow = $db->fetch("user/getByEmail", [ "email" => "dev@example.com", "isActive" => true, ]); ``` This tends to be the better choice once a query grows beyond a couple of values. ## Automatic value handling The library helps with a few common conversions for us: - `bool` becomes a database-friendly truthy value. - `DateTimeInterface` is formatted as `Y-m-d H:i:s`. - arrays expand into indexed placeholders such as `:ids__0`, `:ids__1` (for using within `in()` conditions, for example). ## Special bindings Some placeholders are designed for safe structural SQL fragments: - `:groupBy` - `:orderBy` - `:limit` - `:offset` - `:infileName` Example: ```sql select id, email from user order by :orderBy limit :limit offset :offset ``` ```php $resultSet = $db->fetchAll("user/list", [ "orderBy" => "id desc", "limit" => 20, "offset" => 40, ]); ``` ## Dynamic bindings For more dynamic query shapes, these reserved bindings are available: - `:__dynamicValueSet` - `:__dynamicIn` - `:__dynamicOr` These are useful when we want flexible SQL without dropping into manual string concatenation. Example: https://github.com/PhpGt/Database/blob/master/example/04-dynamic-bindings.php > [!TIP] > In WebEngine, binding works in exactly the same way because the query execution still comes from this package. The WebEngine layer mainly saves you from connection setup and command wiring. --- Next, move on to [[Raw SQL and result sets]] to see what comes back from the database layer and when `executeSql()` is appropriate. Most of the time, query files are the best fit. Raw SQL is still available when we genuinely need it. ## `executeSql()` ```php $result = $db->executeSql("select count(*) as total from user"); ``` This is useful for one-off statements, administrative commands, or cases where creating a named query file would not improve clarity. If we have multiple connections, the third argument is the connection name: ```php $result = $db->executeSql( "select count(id) as total from user_page", [], "analytics" ); ``` ## Working with `ResultSet` The result object is consistent across both named queries and raw SQL: - `fetch():?Row` returns one `Row` or `null`. - `fetchAll():ResultSet` returns an iterable `ResultSet` of rows. - `affectedRows():int` returns the changed row count. - `lastInsertId():string` returns the inserted id as a string. - `asArray():array` returns plain arrays. - `ResultSet` is iterable and countable. ```php $resultSet = $db->fetchAll("user/getAllActive"); foreach($resultSet as $row) { echo $row->getString("email"), PHP_EOL; } ``` ## Multi-statement SQL files One query file may contain more than one SQL statement, separated by semicolons. That means we can keep a tightly related set of setup statements in one place when it improves readability. The statements are split and executed in order. > [!NOTE] > For schema changes over time, migrations are still the better tool. Multi-statement query files are for query execution; migrations are for controlled schema history. --- Next, move on to [[Type-safe getters]] to see the small helpers that turn common single-column queries into clean PHP values. This library includes a small set of helpers for common cases where a query returns one column and we already know the PHP type we want. ## Single-value helpers If a query returns one value, we can ask for that type directly: - `fetchBool()` - `fetchString()` - `fetchInt()` - `fetchFloat()` - `fetchDateTime()` ```php $username = $db->fetchString("user/getUsernameById", 105); $score = $db->fetchFloat("score/calculateForUser", 8008); $lastActivity = $db->fetchDateTime("user/getLatestActivity", 654); ``` This removes the usual "fetch row, then extract one column" boilerplate. ## Multiple typed values If the query still returns one column, but many rows, there are matching array helpers: - `fetchAllBool()` - `fetchAllString()` - `fetchAllInt()` - `fetchAllFloat()` - `fetchAllDateTime()` ```php $userIds = $db->fetchAllInt("user/getAllIds"); ``` ## Typed access on `Row` When we use `fetch()` or iterate over `fetchAll()`, each row also provides typed getters: - `getString()` - `getInt()` - `getFloat()` - `getBool()` - `getDateTime()` - `get()` - `asArray()` ```php $userRow = $db->fetch("user/getById", 105); if($userRow) { echo $userRow->getString("email"); } ``` The `getDateTime()` helper accepts common database date strings as well as Unix timestamps. Example: https://github.com/PhpGt/Database/blob/master/example/03-type-safe-getters.php --- Next, move on to [[Database migrations]] to see how schema changes are tracked and applied safely. Database migrations let us keep schema changes in version control and apply them in a predictable order, so development machines and production servers are ensured to be running the same schema. > [!NOTE] > In WebEngine, the same migrator is surfaced through `gt migrate`, so the file layout and flags on this page apply directly to a WebEngine project. The WebEngine overview is at https://www.php.gt/webengine/database/. ## Storing migration files The default migration directory is: ```text query/_migration/ ``` Migration files must: - end with `.sql` or `.php`. - start with a numeric prefix. - use a continuous numerical sequence with no gaps. - keep the same contents once they have been run. Examples: - `0001-create-user.sql` - `0002-add-created-at.sql` - `0003-index-user-email.sql` The numeric prefix must be at the start of the filename. A file like `a002-second.sql` is ignored rather than treated as migration `2`. ## What the migrator stores The migrator keeps its state in a database table, `_migration` by default. For each migration it records: - the migration number. - a hash of the file contents. - when it was applied. That hash is what makes integrity checks possible. If we edit a migration after it has already run, the migrator will stop and tell us the history has changed. ## Running migrations in code ```php use Gt\Database\Connection\Settings; use Gt\Database\Migration\Migrator; $settings = new Settings( "query", Settings::DRIVER_SQLITE, "app.sqlite" ); $migrator = new Migrator($settings, "query/_migration"); $migrator->createMigrationTable(); $files = $migrator->getMigrationFileList(); $migrator->checkFileListOrder($files); $current = $migrator->getMigrationCount(); $migrator->checkIntegrity($files, $current); $migrator->performMigration($files, $current); ``` That flow is safe to rerun. Only pending migrations will be applied. ## CLI usage The package ships with `bin/migrate`, and WebEngine exposes the same behaviour through `gt migrate`. ```bash php bin/migrate execute php bin/migrate execute --force php bin/migrate execute --reset php bin/migrate execute --reset 3 ``` What the flags do: - no flag: apply pending canonical migrations - `--force`: drop the schema and run again from migration `1` - `--reset`: rerun only the latest canonical migration - `--reset 3`: rerun from migration `4` onwards For SQLite, `--force` resets the database by removing the database file and rebuilding it from the migration set. ## Development migrations For feature branches, there is also a dev migration flow: ```text query/_migration/dev/ ``` These files are tracked separately in the `_migration_dev` table, and are intended for branch-local schema changes while work is still in progress. Use: ```bash migrate --dev migrate --dev-merge ``` - `--dev` runs canonical migrations first, then branch-local dev migrations. - `--dev-merge` promotes applied dev migrations into canonical files in `query/_migration/`, automatically renaming to the numerical sequence. This keeps feature branch work flexible, while still ending up with one canonical migration history in the main branch. > [!TIP] > A sensible workflow is to let each developer keep a local sequence in `query/_migration/dev/`, then have the feature author run `gt migrate --dev-merge` before the work is merged. ## Configuration keys The CLI and migrators read these settings: - `database.query_path` - `database.migration_path` - `database.migration_table` - `database.dev_migration_path` - `database.dev_migration_table` - `database.driver` - `database.schema` - `database.host` - `database.port` - `database.username` - `database.password` Example: https://github.com/PhpGt/Database/blob/master/example/05-database-migrations.php --- To see these features in runnable scripts, move on to [[Examples]]. The `example/` directory is the quickest way to see the library in action without creating a full project first. Example index: 1. [example/01-quick-start.php](https://github.com/PhpGt/Database/blob/master/example/01-quick-start.php) A minimal connection and query call. 2. [example/02-parameter-binding.php](https://github.com/PhpGt/Database/blob/master/example/02-parameter-binding.php) Positional and named binding. 3. [example/03-type-safe-getters.php](https://github.com/PhpGt/Database/blob/master/example/03-type-safe-getters.php) Single-value helpers and typed row access. 4. [example/04-dynamic-bindings.php](https://github.com/PhpGt/Database/blob/master/example/04-dynamic-bindings.php) Dynamic binding helpers. 5. [example/05-database-migrations.php](https://github.com/PhpGt/Database/blob/master/example/05-database-migrations.php) Canonical migration flow. 6. [example/06-multiple-connections.php](https://github.com/PhpGt/Database/blob/master/example/06-multiple-connections.php) Multiple named database connections. 7. [example/07-php-query-collections.php](https://github.com/PhpGt/Database/blob/master/example/07-php-query-collections.php) PHP query collections. Each example creates an isolated workspace, runs the demonstration, and cleans up afterwards. That makes them safe to run repeatedly while learning. > [!NOTE] > These examples show the Database component in isolation. In WebEngine, the same query and migration ideas are used, but the surrounding application wiring is handled for you. The WebEngine overview is at https://www.php.gt/webengine/database/. --- # Documentation: Dom The Document Object Model (DOM) is an object-oriented interface for interacting with HTML webpages and other XML documents. It's been made extremely popular by its use in JavaScript within the browser. The DOM represents a document with a logical tree. Each branch of the tree ends in a node, and each node contains objects. DOM methods allow programmatic access to the tree. With them, you can change the document's structure, style, or content. This project provides access to a DOM within your PHP scripts on the server, for use with HTML or XML Documents, allowing developers to take advantage of the well known web standards and widely understood DOM technologies to produce dynamic pages alongside, or instead of, a template engine. This documentation does **not** try to re-document the whole browser DOM API. MDN already does that very well. Here we will focus on the parts that matter when using DOM on the server: - how to construct documents and parse markup - how the live collections behave - where the implementation intentionally differs from the browser - how to fit the library into WebEngine applications - when DomTemplate is a better choice than manual DOM manipulation > [!IMPORTANT] > If you are building with WebEngine, you usually do not construct `HTMLDocument` yourself. WebEngine provides a pre-constructed HTML document for the page request. > Within WebEngine applications, DomTemplate is also shipped by default, so it is worth understanding what that library already does before reaching for manual DOM manipulation. > In many cases, binding data with DomTemplate is the cleaner option because it keeps PHP logic and HTML structure more loosely coupled. ## What this library gives us - `HTMLDocument` and `XMLDocument` wrappers around PHP's native DOM classes. - Modern selector-based traversal with `querySelector()` and `querySelectorAll()`. - Browser-style helpers such as `classList`, `dataset`, `closest()`, `before()`, `after()`, `replaceWith()`, `innerHTML`, and `outerHTML`. - Live `HTMLCollection` and live/static `NodeList` behaviour where the standard expects it. - A large amount of element-specific IDL behaviour for forms, tables, media elements, and other HTML interfaces. - Explicit exceptions when functionality only makes sense in the browser. ## A very small example ```php use GT\Dom\HTMLDocument; $html = <<

Hello, you!

HTML; $document = new HTMLDocument($html); $document->querySelector(".name")->innerText = "Cody"; echo $document; ``` Output: ```html

Hello, Cody!

``` The code feels very similar to browser DOM code, but it runs entirely on the server. --- To see where this library fits in, continue to [[Overview]]. This package sits in a deliberately narrow space: it gives us a standards-shaped DOM in PHP, but it does not try to become a framework, router, or templating language. That is useful when we want to work with HTML or XML documents directly and keep the code close to the web standards many of us already know from the browser. ## What to expect from the API Most of the public API mirrors familiar DOM concepts: - documents such as `HTMLDocument` and `XMLDocument` - generic nodes such as `Element`, `Text`, `Comment`, and `DocumentFragment` - traversal helpers such as `querySelector`, `children`, `closest`, and `parentElement` - browser-like collection types such as `HTMLCollection`, `NodeList`, `DOMTokenList`, and `DOMStringMap` For details of what those APIs do, MDN remains the best reference: - [Document on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document) - [Element on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element) - [HTMLElement on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) - [Node on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Node) ## What this documentation covers instead The more useful questions for this repository are usually integration questions rather than API catalogue questions: - When should we construct a document manually? - What gets added automatically to an `HTMLDocument`? - Which collections are live? - What happens when we call browser APIs that only make sense client-side? - How should this fit into a WebEngine application? - When should we stop manipulating nodes directly and use DomTemplate instead? Those are the questions covered in this guide. ## DOM versus DomTemplate It is perfectly valid to use this library directly: ```php $price = $document->querySelector(".price"); $price->innerText = "£14.99"; ``` But if the application is mainly binding data into HTML, repeating lists, expanding components, or composing page layouts, DOM manipulation can become too tightly coupled to the exact shape of the markup. That is the gap filled by [DomTemplate](https://www.php.gt/domtemplate): the HTML says what can change, and PHP supplies the data. In WebEngine, DomTemplate is provided by default for precisely that reason. ## WebEngine context This library is also a foundational part of WebEngine. - WebEngine provides a pre-constructed `HTMLDocument` for the current page. - The framework handles the page lifecycle around that document. - DomTemplate is included by default, so many common page updates can happen declaratively without low-level DOM code. If you are writing page logic in WebEngine, you will usually work on an existing document rather than instantiate one from scratch, and it would make sense to use DomTemplate's bind attributes rather than manipulating the DOM directly yourself. ## PHP 8.4's native HTMLDocument Since PHP 8.4, a native `HTMLDocument` class has shipped which enhances the functionality of the old `DOMDocument` class, but still misses a lot of specification in terms of properties and functions. Discussions are being made about whether this native class can be used within this repository, to increase efficiency of code execution and standardise further, but the native class is declared `final` so none of PHP.GT/Dom enhancements can be applied. That means that for now, the extension of the original `DOMDocument` will continue into the forseable future, but it's worth knowing about PHP's native HTMLDocument class in case you don't need any of this library's helpers. A small subset of what PHP.GT/Dom has that PHP's native HTMLDocument doesn't: [`document.contentType`](https://developer.mozilla.org/en-US/docs/Web/API/Document/contentType), [`embeds`](https://developer.mozilla.org/en-US/docs/Web/API/Document/embeds), [`forms`](https://developer.mozilla.org/en-US/docs/Web/API/Document/forms), [`images`](https://developer.mozilla.org/en-US/docs/Web/API/Document/images), [`links`](https://developer.mozilla.org/en-US/docs/Web/API/Document/links), [`scripts`](https://developer.mozilla.org/en-US/docs/Web/API/Document/scripts), [`getElementsByName`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByName), [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText), [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset), [`hidden`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/hidden), [`title`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/title), [`tabIndex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex), `RadioNodeList`(https://developer.mozilla.org/en-US/docs/Web/API/RadioNodeList), and all properties are static (changing the document does not update a referenced NodeList's contents, whereas PHP.GT/Dom's document does). --- Next, we will put together the smallest useful setup in [[Getting started]]. This section shows the smallest useful setup outside of WebEngine. > [!TIP] > In WebEngine, you usually do not need this setup code. The framework gives you a pre-constructed `HTMLDocument`, and DomTemplate is also available by default if the task is really data binding rather than low-level DOM work. ## 1. Install the package ```bash composer require phpgt/dom ``` The package requires PHP's `dom`, `libxml`, and `mbstring` extensions. ## 2. Construct an `HTMLDocument` ```php use GT\Dom\HTMLDocument; $html = <<<'HTML'

Hello, you!

HTML; $document = new HTMLDocument($html); ``` At this point we have a document tree we can query and modify using familiar DOM calls. ## 3. Query the document ```php $name = $document->querySelector(".name"); $name->innerText = "Cody"; ``` These methods follow the same basic behaviour documented on MDN: - [`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/querySelector) - [`querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/querySelectorAll) - [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText) ## 4. Serialise the document ```php echo $document; ``` Casting the document to a string serialises it back to markup. For `HTMLDocument`, the output always includes a doctype and the root document structure: ```html

Hello, Cody!

``` ## 5. Create nodes instead of concatenating HTML when practical `innerHTML` and `outerHTML` are supported, but when we are constructing a few nodes programmatically it is often clearer to use DOM methods directly: ```php $list = $document->createElement("ul"); foreach(["Tea", "Milk", "Biscuits"] as $item) { $li = $document->createElement("li"); $li->innerText = $item; $list->appendChild($li); } $document->body->appendChild($list); ``` MDN references: - [`createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) - [`appendChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) - [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/append) ## 6. Keep the right level of abstraction If you find yourself doing lots of this: ```php $document->querySelector(".customer-name")->innerText = $customer->name; $document->querySelector(".customer-email")->innerText = $customer->email; $document->querySelector(".customer-tier")->innerText = $customer->tier; ``` it is worth pausing and asking whether the real problem is template binding rather than DOM manipulation. In WebEngine applications especially, DomTemplate is usually the better fit. --- Now that we have a document on hand, move on to [[Parsing documents]]. There are three main entry points when working with markup in this library: - `HTMLDocument` for HTML documents - `XMLDocument` for XML documents - `DOMParser` when the incoming content type decides which one to use ## `HTMLDocument` `HTMLDocument` is the most common starting point. ```php use GT\Dom\HTMLDocument; $document = new HTMLDocument("

Hello

"); ``` The constructor does a few useful things for us: - it loads the HTML using PHP's DOM extension - it ensures there is a root `` element - it ensures there is a `` - it ensures there is a `` - it preserves non-element child nodes where possible That means even a small fragment ends up as a proper document tree when serialised. ## `XMLDocument` For XML, use `XMLDocument` instead: ```php use GT\Dom\XMLDocument; $document = new XMLDocument(''); ``` `XMLDocument` is useful when we are working with XML semantics rather than HTML semantics. In particular, XML-specific features such as CDATA sections belong here, not in `HTMLDocument`. MDN references: - [XML DOM overview](https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction) - [`createCDATASection()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection) ## `DOMParser` If we receive markup together with a MIME type, `DOMParser` can choose the right document class: ```php use GT\Dom\DOMParser; $parser = new DOMParser(); $document = $parser->parseFromString($markup, "text/html"); ``` Supported content types are effectively HTML and XML families. Unsupported MIME types throw a `MimeTypeNotSupportedException`. MDN reference: - [`DOMParser.parseFromString()`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString) ## Character encoding `HTMLDocument` defaults to `UTF-8`, and the constructor includes a workaround so UTF-8 HTML is parsed and serialised correctly by PHP's DOM layer. In practice, that means content such as emoji and non-ASCII text can be loaded and emitted without requiring extra manual handling in normal cases. ## Document writing and stream behaviour The `Document` classes also implement a stream-like interface compatible with PSR-7 style expectations. ```php $document = new HTMLDocument("

Example

"); $document->open(); $document->write("More text"); echo $document->getContents(); ``` This is mainly useful when a document needs to behave like a stream object in surrounding infrastructure. A few things are worth knowing: - writing is only supported for `HTMLDocument` - calling `open()` is required before stream writing - XML documents are not writable in this way - `write()` appends to the document body rather than acting like a browser runtime parser If what you need is normal DOM manipulation, the standard node APIs are usually clearer than the stream API. ## HTML versus XML: practical advice - Use `HTMLDocument` for webpages, fragments, email markup, generated HTML, and most server-rendered output. - Use `XMLDocument` for feeds, XML configuration, or XML-based interchange formats. - Use `DOMParser` when the caller already gives you the content type and you want a standards-shaped entry point. > [!NOTE] > In WebEngine, the framework has already loaded the page into an `HTMLDocument`, so this whole document-construction step is usually skipped. --- Next we will look at the parts of the API that are used to navigate around a document: [[Document traversal]]. ## CSS selectors in PHP The library supports selector-based traversal through: - [`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/querySelector) - [`querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/querySelectorAll) - [`closest()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest) - [`matches()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches) ```php $card = $document->querySelector(".product-card"); $buttons = $document->querySelectorAll("form[action] button"); $section = $card?->closest("section"); ``` Internally, selectors are translated to XPath. In normal use that is an implementation detail, but it matters in one practical way: invalid selectors raise `XPathQueryException`. ## Live versus static collections This library follows the DOM's distinction between live and static collections. ### `HTMLCollection` is live Methods and properties such as these return a live `HTMLCollection`: - [`children`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children) - [`getElementsByTagName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByTagName) - [`getElementsByClassName()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByClassName) - document-wide properties such as `forms`, `images`, `links`, and `scripts` ```php $forms = $document->forms; $document->body->appendChild($document->createElement("form")); echo $forms->length; // now includes the new form ``` ### `querySelectorAll()` returns a static `NodeList` This matches browser behaviour: ```php $items = $document->querySelectorAll("li"); ``` If the document changes afterwards, that particular result set does not update. ### `getElementsByName()` returns a live `NodeList` This is one of the places where the return type is `NodeList`, but the behaviour is live. ## Collection helpers The collection objects are intentionally array-like and iterable in PHP: ```php foreach($document->images as $image) { $image->loading = "lazy"; } $first = $document->links[0] ?? null; ``` Useful MDN references: - [HTMLCollection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection) - [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) ## Child and parent helpers The server-side API includes the browser-style convenience methods: - [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/append) - [`prepend()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/prepend) - [`before()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) - [`after()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/after) - [`replaceWith()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith) - [`remove()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove) ```php $notice = $document->querySelector(".notice"); $notice?->before($document->createElement("hr")); $notice?->after("Updated just now"); ``` These are often more pleasant than the older `insertBefore()`-style calls when we are doing small structural edits. > [!TIP] > In WebEngine applications, it is worth checking whether DomTemplate can express the page update before writing repeated manual selectors. --- With traversal covered, the next page looks at the HTML-specific conveniences and the places where server-side DOM naturally differs from browser DOM: [[Working with HTML-specific helpers]]. Beyond the generic DOM interfaces, this library implements a large amount of browser-style HTML behaviour on `HTMLElement`. This page is not a catalogue of every element property. Instead, it highlights the pieces that are especially useful in server-side code. ## `classList` [`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) behaves as a `DOMTokenList`, so we can add, remove, replace, and toggle class names in a clean way: ```php $button = $document->querySelector("button"); $button->classList->add("is-ready"); $button->classList->remove("is-loading"); ``` This is usually clearer than manual string concatenation on the `class` attribute. ## `dataset` [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) is supported through `DOMStringMap`: ```php $card = $document->querySelector(".card"); $card->dataset->productId = "42"; ``` That becomes `data-product-id="42"` in the markup. CamelCase property names are converted to kebab-case data attributes, which makes it pleasant to work with from PHP. To help with static analysis, a non-standard `get` and `set` method is added to `DOMStringMap`, allowing for the following: ```php $card = $document->querySelector(".card"); $card->dataset->set("productId", "42"); ``` ## `innerHTML`, `outerHTML`, and `innerText` These properties are often the quickest way to make targeted changes: - [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) - [`outerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML) - [`innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText) ```php $panel = $document->querySelector("#panel"); $panel->innerHTML = "

Updated content

"; ``` For server-side use, the important distinction is this: - use `innerText` when the value is text and should be escaped safely - use `innerHTML` when the value is markup we intend to parse This matters just as much in PHP as it does in the browser. `innerText` also respects `[hidden]` ancestors when reading text, which makes it closer to the browser's rendered-text behaviour than plain `textContent`. ## Forms, tables, and document conveniences This repository includes a substantial amount of HTML-specific IDL behaviour, including support around: - forms and form controls - select boxes and `selectedOptions` - table sections, rows, and cells - document shortcuts such as `forms`, `images`, `links`, and `scripts` MDN is the right reference for the precise browser semantics: - [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement) - [HTMLSelectElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement) - [HTMLTableElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableElement) The practical point for this library is that we can usually write the same kind of code we would expect in browser JavaScript: ```php $table = $document->querySelector("table"); $row = $table->insertRow(); $row->insertCell()->innerText = "Ada"; $row->insertCell()->innerText = "Admin"; ``` ## Browser-only properties Some parts of the DOM specification simply do not make sense on the server. Anything that depends on user interaction, layout, media playback state, browsing context, or browser UI cannot be implemented in PHP. In those cases, this library throws `ClientSideOnlyFunctionalityException`. Examples include behaviour around: - file inputs and selected files - media playback state - iframe browsing contexts - style/layout information that depends on a rendering engine That is intentional. It is better to fail explicitly than to pretend the browser state exists on the server when it does not. ## Practical advice - Use the browser-shaped helpers when they make code clearer. - Prefer `innerText` or node construction over `innerHTML` for plain text, to avoid [cross-site-scripting](https://owasp.org/www-community/attacks/xss/) attacks. - Treat `ClientSideOnlyFunctionalityException` as a sign that the job belongs in browser JavaScript rather than server PHP. --- Next, we will place this library in its most common real-world context: [[WebEngine and DomTemplate]]. For many PHP.GT applications, this is the page that matters most. `phpgt/dom` is a standalone library and can be used anywhere, but in practice a large amount of usage happens inside WebEngine applications. ## What WebEngine already does for us WebEngine provides a pre-constructed `HTMLDocument` for the current request. That means: - the page HTML has already been loaded - the document lifecycle is already in motion - your page logic can work on the document immediately So, in WebEngine, we usually do **not** do this: ```php $document = new HTMLDocument(file_get_contents("page.html")); ``` because the framework has already done it. ## Manipulating pages using DomTemplate WebEngine also ships with [DomTemplate](https://www.php.gt/domtemplate) by default. That is important because many problems that look like DOM problems are actually template-binding problems: - putting scalar values into the page - binding object properties - repeating lists - composing pages from partials and components - cleaning up optional elements When we solve those jobs with direct DOM manipulation, the PHP logic can become tightly coupled to the exact shape of the HTML. For example: ```php $document->querySelector(".customer-name")->innerText = $customer->name; $document->querySelector(".customer-email")->innerText = $customer->email; $document->querySelector(".customer-status")->innerText = $customer->status; ``` works, but it means changes to the view structure often force changes in the PHP too. DomTemplate is designed to reduce that coupling. Read more at https://www.php.gt/domtemplate ## A useful division of responsibility In WebEngine, a practical split is: - use DomTemplate for binding application data into the page - use DOM directly when we need structural document operations that are genuinely DOM-shaped Examples where direct DOM is still a good fit: - inserting or moving nodes based on parsing or transformation logic - sanitising or post-processing imported markup - generating fragments from existing nodes - working with XML documents or document fragments outside the normal page template flow Examples where DomTemplate is usually the better fit: - filling headings, paragraphs, and form values from data - rendering rows, cards, or menu items from lists - composing reusable page sections and components ## If you are not using WebEngine Outside WebEngine, `phpgt/dom` still stands perfectly well on its own. The main difference is that you become responsible for: - loading the source HTML - constructing the document - wiring your own request lifecycle around it That is completely fine for libraries, scripts, generators, crawlers, email renderers, or bespoke applications. --- The final page covers the behaviour that is closest to "know this before it catches you out": [[Specification notes and quirks]]. This repository aims to stay very close to the DOM specification, but server-side PHP is not a browser, and PHP's own DOM extension has some constraints of its own. These are the main points worth keeping in mind. ## 1. Some browser functionality is intentionally unavailable Anything that depends on browser runtime state, rendering, media playback, user input devices, or browsing context cannot be implemented faithfully on the server. When code reaches one of those areas, the library throws `ClientSideOnlyFunctionalityException`. That is expected behaviour, not an incomplete edge case. ## 2. Element typing is specification-shaped, but still PHP-friendly The library exposes HTML element types through `ElementType`, rather than through a huge inheritance tree of concrete PHP element subclasses that you would check with `instanceof`. In practice that means we often write: ```php if($element->elementType === GT\Dom\ElementType::HTMLInputElement) { // ... } ``` instead of depending on a separate PHP class per HTML tag. ## 3. `tagName` casing needs care One of the long-standing unavoidable differences documented in the project is element name casing. In normal HTML usage, the library creates HTML elements in lowercase and works consistently with that form. The broader point is that tag-name casing should not be treated as a portability boundary in application logic. Prefer selectors, element type checks, and DOM structure over casing assumptions. ## 4. `HTMLDocument` normalises the document shape If the incoming HTML does not contain full document structure, the constructor ensures the document still has: - `` - `` - `` That is helpful for server-side work, but it also means serialised output may be more complete than the input string you started with. ## 5. Some collections are live This catches people out more in PHP than in JavaScript because we are often less used to keeping long-lived collection objects around. - `HTMLCollection` is live - `querySelectorAll()` returns a static `NodeList` - `getElementsByName()` returns a live `NodeList` If a collection appears to change "by itself", that is usually just the expected DOM behaviour, and completely consistent with using the document in a browser with JavaScript. ## 6. Invalid selectors fail explicitly Selector methods are translated internally to XPath. If the selector string is malformed, the library throws `XPathQueryException`. That is usually preferable to silent failure because it makes bad selectors obvious during development. ## 7. Writing to documents is not a general replacement for DOM manipulation `Document::open()` and `write()` exist, but in this implementation they are mainly stream-oriented helpers for HTML documents. For most application code, direct DOM calls such as `append`, `replaceWith`, `innerText`, `innerHTML`, and `createElement` are clearer and less surprising. ## 8. MDN remains the reference for DOM semantics This guide deliberately does not duplicate the whole DOM API surface. When you need exact behaviour for a property or method, use MDN: - [Document](https://developer.mozilla.org/en-US/docs/Web/API/Document) - [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) - [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) - [HTML DOM API index](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API) --- # Documentation: DomTemplate Built on top of [PHP.GT/Dom][dom], this project provides dynamic data binding to DOM Documents, document templating, and reusable HTML components. Directly manipulating the DOM in your PHP code can lead to tightly coupling the logic and view. Binding data using custom elements and data attributes leads to highly readable, maintainable view files that are loosely coupled to the application logic. HTML can be made dynamic with `data-bind` and `data-list` attributes, placeholders such as `{{name}}`, reusable ``, and partial page templates. Why not just use DOM to manipulate the page? -------------------------------------------- The layers of your application should ideally have a strong separation of concerns. The layers are often described as the Model, View, and Controller - referred to as [MVC architecture][wiki-mvc]. In MVC, the _Model_ represents the application's data, the _View_ is the HTML user interface, and the _Controller_ contains the PHP application logic that binds data from the Model to the View. A strong [separation of concerns][wiki-separation-of-concerns] means that HTML view and PHP logic are not tightly coupled. Changes to the HTML structure should not require changes to the PHP data-binding logic. When PHP code directly manipulates the DOM, the HTML and PHP become closely tied together. As a result, even small changes to the HTML can break existing PHP code and force updates whenever the page structure is edited. Data binding helps avoid this problem. By binding data through the `data-bind:*` attributes introduced by DomTemplate, we can create HTML view files that are more readable, maintainable, and loosely coupled to the PHP controller logic. This means the layout and structure of the HTML can change signifciantly without requiring changes to the PHP code. An example of what using this project looks like in your HTML: ```html

Your order

The order total is £0.00

Items in your order:

  • Item Name £0.00
``` In the above example, the HTML can be maintained completely separately to the PHP, promoting a strong separation of concerns. The entire HTML structure can change, without having to communicate this to PHP, as long as the `data-bind:*` and `data-list` attributes are applied to the appropriate elements. This is what's referred to as _loosely coupled_ code - the page logic is loosely coupled to the page view, leading to highly maintainable systems. > [!NOTE] > If you are building with WebEngine, this library is provided ready to use without setup. The framework constructs the binder objects, expands partials and components automatically, and cleans the document after your page logic runs. ## What this library does - Binds scalar values and key/value data to HTML elements. - Repeats sections of markup from iterable data. - Binds nested objects and iterable objects to corresponding HTML structure. - Builds tables from several compatible data structures. - Expands custom HTML components from separate files. - Extends page templates using partial HTML documents. ## Recommended reading order 1. [[Overview]] 2. [[Getting started]] 3. [[Binding basics]] 4. [[Bind properties and modifiers]] 5. [[Binding lists]] 6. [[Binding objects]] 7. [[Binding tables]] 8. [[Conditional and optional elements]] 9. [[HTML components]] 10. [[Partials]] 11. [[Debugging]] 12. [[API reference]] ## A simple example ```html

Hello, you!

``` ```php use GT\Dom\HTMLDocument; use GT\DomTemplate\BindableCache; use GT\DomTemplate\DocumentBinder; function example(DocumentBinder $binder):void { $binder->bindKeyValue("name", "Cody"); $binder->cleanupDocument(); echo $document; } ``` Output HTML: ```html

Hello, Cody!

``` In a nutshell: the HTML says what can change, the PHP supplies the data, and DomTemplate joins the two together. --- To see the whole shape of the library before we start wiring it up, move on to [[Overview]]. [dom]: https://www.php.gt/dom [domtemplate]: https://www.php.gt/domtemplate [wiki-mvc]: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller [wiki-separation-of-concerns]: https://en.wikipedia.org/wiki/Separation_of_concerns DomTemplate falls into three ideas: 1) bind data into HTML, 2) repeat bits of HTML from lists of data, 3) compose whole pages from reusable pieces. In this guide we will build those ideas up one at a time, starting with the smallest useful examples and then gradually moving into the more advanced behaviour. ## 1. Bind data to HTML elements ```html

Welcome back, friend!

``` ```php $binder->bindData([ "firstName" => "Ada", "email" => "ada@example.com", ]); ``` Here we can see the basic pattern of the library: the HTML already contains sensible defaults, and the binder only replaces the pieces we have marked. ## 2. Repeat markup from iterable data ```html
  • Item
``` ```php $binder->bindList([ "Tea", "Milk", "Biscuits", ]); ``` If we do this, the original `
  • ` is treated as a template and cloned once for each list item. ## 3. Inject partial values with placeholders ```html View {{name ?? this user}} ``` ```php $binder->bindData([ "id" => 210, "name" => "Cody", ]); ``` This is handy when we only want to replace part of an attribute or bit of text rather than the whole property. ## 4. Expand components ```html ``` If there is a matching component file, DomTemplate will replace the contents of the custom element with that component's HTML. ## 5. Extend partial page templates ```html

    Orders

    ``` A partial template allows us to provide reusable outer page layout in a flexible way that can be extended in other pages. > [!NOTE] > In WebEngine applications, the use of _header.html and _footer.html files might be simple enough for your page template needs - consider using this before reaching for partial page templates - it might be a simpler solution that works for you. --- Now that we know what the library is for, move on to [[getting started]] so we can wire up a working binder. In this section we will build the smallest useful DomTemplate setup from scratch. The main thing to know is that `DocumentBinder` is the public entry point, but it relies on a few smaller helper objects behind the scenes. Once those are wired together, we can bind values, lists, tables, components, and partials in a consistent way. > [!TIP] > In WebEngine, you usually do not instantiate these objects yourself. The framework builds and injects them for you. ## 1. Install the library ```bash composer require phpgt/domtemplate ``` ## 2. Create an `HTMLDocument` ```php use Gt\Dom\HTMLDocument; $html = <<<'HTML'

    Hello, you!

    HTML; $document = new HTMLDocument($html); ``` DomTemplate works with `HTMLDocument` from `phpgt/dom`, so this is always our starting point. ## 3. Construct the binder's dependency tree Each piece of functionality within DomTemplate is controlled by a separate class. Here we'll build up the dependency tree, so all classes are aware of each other in a way that respects object oriented encapsulation: ```php use GT\DomTemplate\BindableCache; use GT\DomTemplate\DocumentBinder; use GT\DomTemplate\ElementBinder; use GT\DomTemplate\HTMLAttributeBinder; use GT\DomTemplate\HTMLAttributeCollection; use GT\DomTemplate\ListBinder; use GT\DomTemplate\ListElementCollection; use GT\DomTemplate\PlaceholderBinder; use GT\DomTemplate\TableBinder; $elementBinder = new ElementBinder(); $placeholderBinder = new PlaceholderBinder(); $tableBinder = new TableBinder(); $listBinder = new ListBinder(); $listElementCollection = new ListElementCollection($document); $bindableCache = new BindableCache(); $htmlAttributeBinder = new HTMLAttributeBinder(); $htmlAttributeCollection = new HTMLAttributeCollection(); $elementBinder->setDependencies( $htmlAttributeBinder, $htmlAttributeCollection, $placeholderBinder, ); $htmlAttributeBinder->setDependencies($listBinder, $tableBinder); $listBinder->setDependencies( $elementBinder, $listElementCollection, $bindableCache, $tableBinder, ); $tableBinder->setDependencies( $listBinder, $listElementCollection, $elementBinder, $htmlAttributeBinder, $htmlAttributeCollection, $placeholderBinder, ); $binder = new DocumentBinder($document); $binder->setDependencies( $elementBinder, $placeholderBinder, $tableBinder, $listBinder, $listElementCollection, $bindableCache, ); ``` That looks like quite a lot at first glance, but we only have to set it up once. After that, all normal binding goes through `$binder`. ## 4. Bind some data ```php $binder->bindKeyValue("name", "Cody"); ``` At this point the document contains the updated text, but it still contains the original DomTemplate attributes too. ## 5. Clean the document ```php $binder->cleanupDocument(); echo $document; ``` `cleanupDocument()` removes helper attributes such as `data-bind`, `data-list`, `data-template`, and `data-element` from the output. It also removes unbound optional elements marked with `data-element`. Output HTML: ```html

    Hello, Cody!

    ``` ## 6. Know which binder to use Most of the time, `DocumentBinder` is the right tool. - Use `DocumentBinder` when working on the whole page. - Use `ComponentBinder` when working inside a single expanded component. `ComponentBinder` has the same binding methods, but it automatically limits the bind scope to one component element. > [!NOTE] > In WebEngine, you can just use `Binder` instead of `ComponentBinder` or `DocumentBinder` and you'll be given the correct instance depending on the context in which you're using it. > Within a page's PHP you'll get the `DocumentBinder` and within a component's PHP you'll get the `ComponentBinder`. ## 7. Optional page composition helpers If we are also using component files or partial templates, there are two extra helpers: - `ComponentExpander` expands custom HTML elements from a component directory. - `PartialExpander` applies a page template from a partial directory. Those are covered properly later in [[HTML components]] and [[Partials]]. --- Our binder is now wired up. Next we can learn the core public API in [[Binding basics]]. This section covers the main public API of DomTemplate. Here we will look at the three most common operations: - bind one value everywhere it's needed - bind a single named key/value pair - bind a whole map or object in one operation Along the way we'll also look at bind contexts, selector strings, and placeholders. ## The `data-bind:*` pattern At the HTML level, the basic idea is always the same: ```html Guest View profile ``` The bit after `data-bind:` is the **bind property** (e.g. the property of the HTML Element that we'll be setting), and the attribute's value is known as the **bind key** (e.g. the key of the data structure we'll use for the value). When we bind the key `name`, the first element changes. When we bind the key `profileUrl`, the second element changes - only elements with a matching bind key will be affected, allowing you to keep default values stored within the HTML page itself. ### `bindValue` `bindValue($value, $context = null)` binds one value to every matching `data-bind:*` attribute that has no key. HTML: ```html

    Hello

    Goodbye

    ``` PHP: ```php $binder->bindValue("Welcome"); ``` Output HTML: ```html

    Welcome

    Welcome

    ``` This is especially useful for simple repeated text, scalar list items, or placeholders without named keys. ### `bindKeyValue` `bindKeyValue(string $key, $value, $context = null)` binds one named key/value pair. HTML: ```html

    Hello, you!

    Signed in as guest.

    ``` PHP: ```php $binder->bindKeyValue("name", "Cody"); ``` Output HTML: ```html

    Hello, Cody!

    Signed in as Cody.

    ``` ### `bindData` `bindData($data, $context = null)` binds a whole key/value data source in one call. HTML: ```html

    Guest

    Email: guest@example.com

    Status: Offline

    ``` PHP: ```php $binder->bindData([ "username" => "Ada", "email" => "ada@example.com", "status" => "Reviewing pull requests", ]); ``` If we do this, DomTemplate iterates over each key and applies it anywhere that key is used within the current scope. ## Bind contexts All the main binding methods accept an optional context. ### Pass an element ```php $profileSection = $document->querySelector("#profile"); $binder->bindData($profileData, $profileSection); ``` This keeps the binding local to one part of the page. ### Pass a selector string ```php $binder->bindData($profileData, "#profile"); ``` This is often a little more convenient when we already know the selector we want. > [!NOTE] > If no element matches the selector string, DomTemplate throws `ContextElementNotFoundException`. ## Placeholders with `{{curly braces}}` Sometimes we do not want to replace the whole text node or the whole attribute value. We only want to inject part of it. HTML: ```html View {{name ?? this user}} ``` PHP: ```php $binder->bindData([ "id" => 42, "name" => "Cody", ]); ``` Output HTML: ```html View Cody ``` If `name` is `null` or an empty string, the default text `this user` is used instead. ## `bindValue` vs placeholders As a rule of thumb: - use `data-bind:text`, `data-bind:href`, and friends when we want to replace a whole property - use `{{placeholder}}` when we only want to replace part of a string That keeps the HTML easy to read and makes the default state obvious to anyone scanning the template. ## When to clean the document After binding is finished, call: ```php $binder->cleanupDocument(); ``` That strips DomTemplate's helper attributes from the output and removes any unbound `data-element` nodes. --- We now have the core mental model in place. Next, move on to [[bind properties and modifiers]] so we can see the different ways an element can be mutated. Most `data-bind:*` attributes set an element attribute or property, but some bind properties and modifier characters add extra behaviour. ## Common bind properties Here are the bind properties we will use most often: - `text` sets `textContent` - `html` sets `innerHTML` - `value` sets form values - `class` adds or toggles class names - `list` binds a nested iterable into a contained list template - `table` binds table data into a `` - `remove` removes an element conditionally - any other property name is treated as a normal attribute name ## `text` and `html` HTML: ```html

    Guest

    Plain text bio
    ``` If we bind `name`, the element's text content changes. If we bind `bioHtml`, the element's inner HTML changes. > [!IMPORTANT] > `html` inserts HTML, so it should only be used with trusted content to avoid [cross-site scripting (XSS)][xss]. If we're only using plain text, stick to `text`. ## `class` Without a modifier, `data-bind:class` adds one or more class names. HTML: ```html
    ``` If we bind `extraClass` to `featured`, the element ends up with `class="panel featured"`. If the bound value contains multiple class names separated by spaces, each one is added individually. HTML: ```html
    ``` PHP: ```php $binder->bindKeyValue("stateClasses", "featured compact"); ``` Output HTML: ```html ``` Arrays of strings are supported here too, so you can pass an array such as `["featured", "compacts"]` and DomTemplate will add the class names in the array. ### The `:` token modifier The colon modifier toggles a token within a token list, most commonly a class name. HTML: ```html
  • ``` If `isSelected` is truthy, `selected` is added. If it is falsey, `selected` is removed. We can also toggle several classes together: ```html
  • ``` If `isSelected` is truthy, both `selected` and `featured` are added. If it is falsey, they are both removed. If we omit the explicit token name: ```html
  • ``` the bound value itself becomes the token. This also works with several class names: ```php $binder->bindKeyValue("status", "featured compact"); ``` In that case, both `featured` and `compact` are toggled together. #### The inverse token modifier If we want to toggle a token when a value is falsey rather than truthy, we can add `!` alongside `:`. HTML: ```html
  • ``` Here we can read it as: "add `hidden` when `isVisible` is not truthy". The order of the modifier characters does not matter, so `!:` works in the same way as `:!`: ```html
  • ``` ### The `?` boolean modifier The question mark toggles an attribute based on truthiness. HTML: ```html ``` If `isArchived` is truthy, the button gets `disabled`. Otherwise, that attribute is removed. #### The `?!` inverse boolean modifier HTML: ```html ``` Here we can read it as: "disable the button when `isEditable` is not truthy". The order does not matter here either, so `!?` works in the same way as `?!`: ```html ``` ## Boolean equality checks with `=` We can also make the boolean modifier compare against a specific string value. HTML: ```html ``` If `size` is `"m"`, the middle radio becomes checked. This is especially handy for radios, tab state, and selected options. ### The `@` attribute reference modifier The `@` modifier lets us reuse an existing attribute value instead of repeating ourselves. HTML: ```html ``` This behaves as though we had written: ```html ``` There is also a shorthand: ```html ``` which means the same thing as `@name`. ## Combining modifiers Modifiers can be combined in one expression. HTML: ```html ``` Here we are saying: - look up the bind key from `name` - compare it with the current element's `value` - add `checked` when they match We can also bundle several modifier expressions for the same property by separating them with semicolons: ```html
    ``` ## `data-rebind` By default, once a bind attribute has been used, DomTemplate removes it to prevent any further bind functions from affecting the element again. If we want the property to remain bindable, add `data-rebind`. HTML: ```html ``` This is useful when we bind the same area more than once during one request. ## `data-bind:remove` `remove` is a special bind property that removes the element itself rather than mutating one of its attributes. HTML: ```html

    It's night-time. It's daytime.

    ``` If `isDay` is true, the first span disappears. If `isDay` is false, the second span disappears. ## `data-bind:list` This property binds a nested iterable into a child list template. HTML: ```html
    • Order
    ``` When the key `orderList` is bound, the contained list template is used. We will cover that properly in the [[binding lists]] section. ## `data-bind:table` This property tells DomTemplate where table-shaped data should go. HTML: ```html
    ``` We will go through the supported table data shapes in the [[binding tables]] section. --- Now that the element-level behaviour is clear, move on to [[binding lists]] for the part of the library that does the heavy lifting with repeated markup. [xss]: https://owasp.org/www-community/attacks/xss/ Binding lists to the document allows us to take any iterative data such as an array of objects from the database, and clone an element in the document for each item in the list. Each cloned element will also have any `data-bind:*` attributes bound at the point of cloning. Any element can be marked as a list element with `data-list`, then DomTemplate can clone it once for each item in an iterable data source. ## A simple scalar list HTML: ```html
    • Item
    ``` PHP: ```php $binder->bindList([ "Tea", "Milk", "Biscuits", ]); ``` Output HTML: ```html
    • Tea
    • Milk
    • Biscuits
    ``` Because the list items are simple strings, we do not need any named bind keys here. ## A list of associative arrays or objects HTML: ```html ``` PHP: ```php $binder->bindList([ ["id" => 67, "name" => "Pot plant", "price" => 12.99], ["id" => 20, "name" => "Umbrella", "price" => 9.50], ]); ]); ``` Here we can see the two list systems working together: the `
  • ` repeats, and each clone then receives its own key/value data. An array of associative arrays is used in this example, but wherever possible it is preferred to have a class representation of the data structure - the benefits will become clear as you read on. ## Associative scalar lists and the reserved key `{{}}` When the iterable keys matter, DomTemplate exposes the current list key through the reserved bind key `{{}}`. HTML: ```html

    CODE

    Name

    ``` PHP: ```php $binder->bindList([ "GBP" => "Pound sterling", "EUR" => "Euro", ]); ``` Output HTML: ```html

    GBP

    Pound sterling

    EUR

    Euro

    ``` ## Named list templates If a page contains more than one list template, it is often clearer to name them. HTML: ```html
    • Language
    • Game
    ``` PHP: ```php $binder->bindList(["PHP", "TypeScript", "SCSS"], templateName: "languages"); $binder->bindList(["Portal", "Celeste", "Terraria"], templateName: "games"); ``` This is a little easier to reason about than relying on context alone. ## Using a context instead of a template name An alternative approach is to pass the document context of where the list should be bound: ```php $binder->bindList(["PHP", "TypeScript"], "#language-panel"); $binder->bindList(["Portal", "Celeste"], "#game-panel"); ``` In this example, we would have un-named `data-list` elements within the individual elements with ID `language-panel` and `game-panel`. If more than 1 unnamed list element exists in the same context, a `ListElementNotFoundInContextException` will be thrown. ## Nested lists DomTemplate can recurse through nested iterable data. HTML: ```html
    • Artist name

      • Album title

        1. Track
    ``` PHP: ```php $binder->bindList([ "A Band From Your Childhood" => [ "This Album is Good" => [ "The Best Song You‘ve Ever Heard", "Another Cracking Tune", "Top Notch Music Here", "The Best Is Left ‘Til Last", ], "Adequate Collection" => [ "Meh", "‘sok", "Sounds Like Every Other Song", ], ], "Bongo and The Bronks" => [ "Salad" => [ "Tomatoes", "Song About Cucumber", "Onions Make Me Cry (but I love them)", ], "Meat" => [ "Steak", "Is Chicken Really a Meat?", "Don‘t Look in the Sausage Factory", "Stop Horsing Around", ], "SnaxX" => [ "Crispy Potatoes With Salt", "Pretzel Song", "Pork Scratchings Are Skin", "The Peanut Is Not Actually A Nut", ], ], ]); ``` If we do this, the outer list binds artists, the next list binds albums, and the innermost list binds tracks. ## `data-bind:list` for nested object properties For richer data, `data-bind:list` lets us point at a nested list property. HTML: ```html

    Customer

    • Order ID for customer you@example.com
    ``` If the bound customer object contains an `orderList` property, DomTemplate binds that sub-list into the nested template automatically. Within the bind operations, nested properties can be addressed with dot notation - in the example above `customer.email` will set the text of the HTML element to the email property of the customer object from the current bound object. When a custom element is used as the list container, the list name can be inferred from the tag name: ```html
    • Order ID
    ``` `order-list` maps to the key `orderList` on the bound object. // TODO: Link to unit test to show this in action. ## `bindListCallback` `bindListCallback` works like `bindList`, but gives us a hook for each iteration. ```php $binder->bindListCallback( $productList, function(\GT\Dom\Element $template, array $row, int|string $key):array { if(($row["stock"] ?? 0) === 0) { $template->classList->add("out-of-stock"); } $row["price"] = number_format((float)$row["price"], 2); return $row; } ); ``` This is useful when we want to tweak the cloned element, adjust the data shape, or skip pre-processing elsewhere. ## `