Core
object
%cookies
/phlo/resources/cookies.phlo
method
%cookies -> controller
line 10
Deze controller haalt de huidige staat van cookies op en wijst deze toe aan de objData-eigenschap.
this->objData = $_COOKIEprop
%cookies -> lifetimeDays:int
line 12
Stelt de levensduur van cookies in dagen in.
180method
%cookies -> objSet ($key, $value, array $options = []):bool
line 14
Stelt een cookie in met de opgegeven sleutel en waarde, samen met optionele parameters voor vervaldatum, pad, beveiliging en SameSite-attributen.
$this->objData[$key] = $value
$_COOKIE[$key] = $value
$defaults = ['expires' => time() + $this->lifetimeDays * 86400, 'path' => slash, 'secure' => %req->secure, 'httponly' => true, 'samesite' => 'Lax']
setcookie($key, $value, array_merge($defaults, $options))
return truemethod
%cookies -> __unset ($key):void
line 22
Verwijdert een cookie door deze uit de lokale objectgegevens en de globale $_COOKIE-array te verwijderen, en stelt de vervaldatum in op het verleden.
unset($this->objData[$key], $_COOKIE[$key])
$options = ['expires' => time() - 86400, 'path' => slash, 'secure' => %req->secure, 'httponly' => true, 'samesite' => 'Lax']
setcookie($key, void, $options)object
%lang
/phlo/resources/lang.phlo
function
function nl ($text, ...$args):string
line 11
Vertaal de gegeven tekst naar het Nederlands met de opgegeven argumenten voor opmaak.
%lang->translation('nl', $text, ...$args)function
function en ($text, ...$args):string
line 12
Deze functie haalt een vertaling op voor de opgegeven tekst in het Engels, en kan optioneel worden opgemaakt met extra argumenten.
%lang->translation('en', $text, ...$args)static
lang :: asyncBatch ($from, $to, $json):void
line 14
Voert een batchvertaling asynchroon uit, decodeert JSON-invoer en slaat de vertalingen op als deze succesvol zijn.
%app->lang = $to
$texts = json_decode($json, true)
$translations = $this->translateBatch($from, $to, $texts)
if ($translations) $this->save($to, $translations)view
%lang -> view
line 21
Deze functie haalt de huidige taalinstelling van de applicatie op, waardoor lokalisatie van views mogelijk is.
%app->langprop
%lang -> model:string
line 23
Deze functie haalt het model op dat is gekoppeld aan de opgegeven taalidentificator.
'gpt-4o-mini'prop
%lang -> instructions
line 24
Definieert een set instructies voor de Phlo programmeertaal.
voidstatic
lang :: fileCache:array
line 25
lang::$fileCache is een statische eigenschap die gecachete taaldocumenten opslaat voor efficiënte toegang tijdens runtime.
[]method
%lang -> file ($lang):string
line 27
Haal het configuratiebestand op voor de opgegeven taal, met het formaat 'langs.$lang.ini'.
langs.$lang.'.ini'method
%lang -> escape ($value):string
line 29
Escaped speciale tekens in een string voor veilige uitvoer in HTML, vervangend backslashes, dubbele aanhalingstekens en regeleinden met hun respectieve escape-sequenties.
strtr((string)$value, [bs => bs.bs, dq => bs.dq, lf => '\n'])method
%lang -> unescape ($value):string
line 30
Deze functie ontsnapt een gegeven string door escape-sequenties te vervangen door hun overeenkomstige karakters.
strtr(strtr($value, [bs.bs => "\x01", bs.dq => dq, '\n' => lf]), ["\x01" => bs])method
%lang -> lineValue ($line, $eq):string
line 32
Extraheert en verwerkt een lijnwaarde uit een gegeven string, verwijdert omringende aanhalingstekens indien aanwezig en ontsnapt speciale tekens.
$value = rtrim(substr($line, $eq + 3), cr.lf)
if (strlen($value) > 1 && $value[0] === dq && substr($value, -1) === dq) $value = substr($value, 1, -1)
return $this->unescape($value)method
%lang -> readAll ($file):array
line 38
Leest alle sleutel-waarde paren uit een opgegeven bestand en retourneert deze als een associatieve array. Als het bestand niet bestaat of geen geldig bestand is, retourneert het een lege array.
$items = []
if (!is_file($file)) return $items
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] AS $line){
$eq = strpos($line, ' = ')
if ($eq === false) continue
$items[substr($line, 0, $eq)] = $this->lineValue($line, $eq)
}
return $itemsmethod
%lang -> search ($file, $hash):?string
line 54
Finds one line with a binary search over the raw bytes of the translation file.
$size = (int)@filesize($file)
if (!$size) return null
$h = @fopen($file, 'rb')
if (!$h) return null
$lo = 0
$hi = $size
while ($hi - $lo > 4096){
$mid = intdiv($lo + $hi, 2)
fseek($h, $mid)
fgets($h)
$pos = ftell($h)
if ($pos >= $hi){
$hi = $mid
continue
}
$line = (string)fgets($h)
$eq = strpos($line, ' = ')
if ($eq === false){
$hi = $mid
continue
}
$cmp = strcmp(substr($line, 0, $eq), $hash)
if ($cmp < 0) $lo = ftell($h)
elseif ($cmp > 0) $hi = $pos
else {
fclose($h)
return $this->lineValue($line, $eq)
}
}
fseek($h, $lo)
$value = null
while (ftell($h) < $hi && ($line = fgets($h)) !== false){
$eq = strpos($line, ' = ')
if ($eq === false) continue
$cmp = strcmp(substr($line, 0, $eq), $hash)
if ($cmp > 0) break
if ($cmp === 0){
$value = $this->lineValue($line, $eq)
break
}
}
fclose($h)
return $valuemethod
%lang -> lookup ($hash):?string
line 100
Zoekt een waarde in de taalbestanden cache op basis van de opgegeven hash en werkt de cache bij als het bestand is gewijzigd.
$file = $this->file(%app->lang)
$mtime = (int)@filemtime($file)
$cache =& static::$fileCache[$file]
if (!$cache || $cache['mtime'] !== $mtime) $cache = ['mtime' => $mtime, 'items' => []]
if (array_key_exists($hash, $cache['items'])) return $cache['items'][$hash]
$value = $this->search($file, $hash)
if ($value === null && $mtime) $value = $this->readAll($file)[$hash] ?? null
return $cache['items'][$hash] = $valuemethod
%lang -> save ($lang, $pairs):void
line 115
Writes the whole file under a temporary name and renames it into place.
$file = $this->file($lang)
$items = $this->readAll($file)
foreach ($pairs AS $hash => $value) $items[$hash] = $value
ksort($items, SORT_STRING)
$out = void
foreach ($items AS $hash => $value) $out .= $hash.' = '.dq.$this->escape($value).dq.lf
$tmp = $file.'.'.getmypid().'.tmp'
file_put_contents($tmp, $out, LOCK_EX)
@chmod($tmp, 0664)
rename($tmp, $file)
unset(static::$fileCache[$file])method
%lang -> transContext:string
line 129
Haal de context op van de app-auteur met betrekking tot het doel en het domein, indien beschikbaar; anders retourneert het void.
($instr = trim($this->instructions ?? void)) !== void ? lf.'Context from the app author about purpose and domain: '.$instr : voidprop
%lang -> browser:?string
line 131
Haal de voorkeurstaal uit de 'Accept-Language' HTTP-header, en retourneer de eerste overeenkomende taalcodes uit de ondersteunde talen van de applicatie.
last($langs = array_filter(explode(comma, %req->acceptLanguage), fn($lang) => isset(%app->langs[substr($lang, 0, 2)])), $langs ? substr(current($langs), 0, 2) : null)method
%lang -> cookie:?string
line 132
Haal de taalvoorkeur uit cookies en controleer of het een geldige optie is in de beschikbare talen van de applicatie, waarbij de taal wordt geretourneerd als deze geldig is of null anders.
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : nullmethod
%lang -> detect ($text, $fallback = 'en'):string
line 133
Detecteert de taal van de gegeven tekst en retourneert de ISO 639-1 code. Als de detectie mislukt, retourneert het een opgegeven fallback-taalcode, standaard 'en'.
$res = %AI->chat (
model: $this->model,
system: 'Analyse which language this text is in and return only the ISO 639-1 code of the language, no other data!',
user: $text.lf.lf.'The ISO 639-1 code of the language is: ',
temperature: 0,
)->answer
return strlen($res) === 2 ? strtolower($res) : $fallbackmethod
%lang -> hash ($from, $text):string
line 142
Genereert een hash op basis van de opgegeven tekst en een voorvoegsel van de opgegeven taal.
$from.($short = substr(implode(regex_all('/[A-Za-z0-9]+/', ucwords($text))[0]), 0, 8)).substr(md5($text), 0, 10 - strlen($short))method
%lang -> translation ($from, $text, ...$args):string
line 143
Vertaling van de gegeven tekst van een opgegeven taal naar de huidige taal van de applicatie, waarbij ontbrekende vertalingen asynchroon worden afgehandeld.
if ($from === %app->lang) $translation = strtr($text, ['\n' => lf])
else {
$translation = []
$missing = []
foreach (explode(lf, $text) AS $line){
if (trim($line)){
$hash = $this->hash($from, $line)
$item = $this->lookup($hash)
if ($item === null) [$missing[$hash] = $item = $line, debug(%app->lang.': '.(strlen($line) > 20 ? substr($line, 0, 18).'...' : $line))]
}
else $item = void
$translation[] = $item
}
if ($missing) phlo_async('lang::asyncBatch', $from, %app->lang, json_encode($missing))
$translation = implode(lf, $translation)
}
return $args ? sprintf($translation, ...$args) : $translationmethod
%lang -> translate ($from, $to, $text):string
line 162
Vertaal een gegeven tekst van de ene ISO 639-1 taal naar de andere met behulp van AI, terwijl markdown-opmaak en hoofdletters behouden blijven.
if ($from === $to) return $text
return %AI->chat (
model: $this->model,
system: "You will be provided with a word, sentence or (markdown) text in ISO 639-1 language $from, and your task is to translate this string into ISO 639-1 language $to. Respect markdown, missing interpunction and specific use of capitals. Give only the translation.".$this->transContext(),
user: $text,
temperature: 0,
)->answermethod
%lang -> translateBatch ($from, $to, $texts):array
line 171
Vertaal een batch tekst van de ene naar de andere taal met behulp van AI, en geef de vertalingen in genummerd formaat terug.
if ($from === $to) return $texts
$hashes = array_keys($texts)
$numbered = implode(lf, array_map(fn($i, $t) => ($i + 1).'. '.$t, array_keys($values = array_values($texts)), $values))
$answer = %AI->chat (
model: $this->model,
system: "You will be provided with numbered lines in ISO 639-1 language $from. Translate each line into ISO 639-1 language $to. Return only the numbered translations in the same format. Respect markdown, missing interpunction and specific use of capitals.".$this->transContext(),
user: $numbered,
temperature: 0,
)->answer
$result = []
foreach (explode(lf, trim($answer)) AS $line){
if (preg_match('/^(\d+)\.\s*(.+)/', $line, $m))
$result[$hashes[(int)$m[1] - 1]] = $m[2]
}
return $resultobject
%lastmod
/phlo/resources/lastmod.phlo
static
lastmod :: file ($dir = null):string
line 13
Where the map lives, beside the generated PHP so it travels with a release.
($dir ?: php).'lastmod.json'prop
%lastmod -> sources:array
line 17
Sources the convention cannot find, as a uri to file path map.
[]static
lastmod :: resolve ($uri):?string
line 20
The file most likely to render this page.
$slug = trim((string)$uri, slash)
$names = $slug === void ? ['home.phlo', 'page.home.phlo'] : [$slug.'.phlo', 'page.'.$slug.'.phlo', strtr($slug, [slash => dot]).'.phlo']
foreach ($names AS $name) if (is_file($file = app.$name)) return $file
return nullstatic
lastmod :: day ($file):?string
line 27
$file && is_file($file) ? date('Y-m-d', filemtime($file)) : nullstatic
lastmod :: uriOf ($page):string
line 29
is_string($page) ? $page : (string)(is_array($page) ? ($page['uri'] ?? void) : ($page->uri ?? void))static
lastmod :: stamp ($dir = null):array
line 31
$map = []
foreach ((array)(%app->pages ?? []) AS $page) ($d = static::day(static::resolve(static::uriOf($page)))) && $map[static::uriOf($page)] = $d
foreach ((array)%lastmod->sources AS $uri => $file) ($d = static::day($file)) && $map[$uri] = $d
ksort($map)
file_put_contents($target = static::file($dir), json_encode($map, jsonPretty))
return ['file' => $target, 'pages' => count($map)]prop
%lastmod -> map:array
line 40
is_file($file = static::file()) ? (array)json_decode((string)file_get_contents($file), true) : []method
%lastmod -> for ($uri):?string
line 42
$this->map[$uri] ?? nullobject
%manifest
/phlo/resources/manifest.phlo
route
route GET manifest.json
line 10
if (!%manifest->body) return false
manifest::output(%manifest->body)prop
%manifest -> body
line 15
nullprop
%manifest -> maxAge:int
line 16
60static
manifest :: encode ($body):string
line 18
json_encode($body, jsonPretty)static
manifest :: output ($body, $maxAge = null):void
line 20
%res->header('Cache-Control', 'public, max-age='.($maxAge ?? %manifest->maxAge))
output(static::encode($body), type: 'application/manifest+json')view
%manifest -> view
line 25
manifest::encode(%manifest->body)view
%manifest -> head
line 27
<link rel=manifest href=/manifest.json>object
%manual
/phlo/resources/manual.phlo
static
manual :: instruction
line 11
'Summarise what this application does and what has changed recently. Write in the same language as the description, at most eight sentences, running text without lists or headings. Do not open with a sentence about what you are going to do.'prop
%manual -> translate:bool
line 13
function_exists('en')prop
%manual -> labels:array
line 14
arr(
title: 'Manual',
lead: 'This page comes from the source itself: the description, what the code exposes and the latest changes.',
state: 'State',
unknown: 'unknown',
summary: 'In short',
about: 'What this app is',
files: 'Files',
routes: 'Routes',
changes: 'Recent changes',
why: 'Why',
own: 'Codebase',
shared: 'Shared with the sibling app',
noKey: 'No AI key is configured, so there is no summary. The rest of this page comes straight from the source and works without one.',
noAnswer: 'The model returned nothing.',
noSummary:'The summary could not be fetched: ',
noInfo: 'This app does not carry a data/app.md yet.',
noNodes: 'This file carries no nodes; it consists of markup or script.',
)method
%manual -> label ($key):string
line 33
$this->translate ? en($this->labels[$key]) : $this->labels[$key]static
manual :: appInfo:string
line 35
(string)(reflect::appInfo() ?: void)static
manual :: commits (string $path, int $limit = 12):array
line 37
if (!$path || !is_dir($path)) return []
$field = perc.'x1f'
$record = perc.'x1e'
$format = perc.'h'.$field.perc.'ad'.$field.perc.'s'.$field.perc.'b'.$record
$command = 'git -C '.escapeshellarg($path).' log -'.(int)$limit.' --no-merges --date=iso --format='.$format.' 2>/dev/null'
$raw = (string)@shell_exec($command)
$out = []
foreach (array_filter(explode("\x1e", $raw), 'trim') AS $entry){
$parts = explode("\x1f", trim($entry))
if (count($parts) < 3) continue
$when = strtotime($parts[1]) ?: 0
$out[] = obj(
hash: $parts[0],
when: $when,
date: $when ? date('d-m-Y H:i', $when) : $parts[1],
subject: $parts[2],
body: trim((string)($parts[3] ?? void)),
)
}
return $outstatic
manual :: repos:array
line 60
$path = app
$dirs = [%manual->label('own') => $path]
$config = json_decode((string)@file_get_contents($path.'data/app.json'), true)
foreach ((array)($config['paths']['resources'] ?? []) AS $dir){
$dir = rtrim((string)$dir, slash).slash
$label = is_dir($dir) ? static::dirLabel($dir) : void
if ($label === void || isset($dirs[$label])) continue
$dirs[$label] = $dir
}
$roots = []
$out = []
foreach ($dirs AS $label => $dir){
$root = trim((string)@shell_exec('git -C '.escapeshellarg($dir).' rev-parse --show-toplevel 2>/dev/null'))
if ($root === void || isset($roots[$root])) continue
$roots[$root] = true
$out[$label] = $dir
}
return $outstatic
manual :: commitList (int $limit = 20):array
line 81
$all = []
foreach (static::repos() AS $label => $dir){
foreach (static::commits($dir) AS $commit){
$commit->origin = $label
$all[] = $commit
}
}
usort($all, fn($a, $b) => $b->when <=> $a->when)
return array_slice($all, 0, $limit)static
manual :: head:string
line 93
$commits = static::commits(app, 1)
return (string)($commits[0]->hash ?? void)static
manual :: routes:array
line 98
(array)reflect::compactRoutes()static
manual :: routeGroups:array
line 100
$groups = []
foreach (static::routes() AS $route){
$file = basename((string)($route['file'] ?? 'unknown'))
$groups[$file][] = $route
}
ksort($groups)
return $groupsstatic
manual :: nodesByFile:array
line 110
if (isset(%req->docsNodes)) return (array)%req->docsNodes
$found = []
foreach (['route', 'view', 'static', 'method', 'prop'] AS $type){
foreach ((array)reflect::find($type, null, true, 'all') AS $row){
$row = (array)$row
$file = basename((string)($row['file'] ?? void), '.phlo')
if ($file === void) continue
$found[$file][] = obj(
type: $type,
name: (string)($row['name'] ?? void),
args: (string)($row['args'] ?? void),
ret: (string)($row['type'] ?? void),
summary: (string)($row['summary'] ?? void),
)
}
}
%req->docsNodes = $found
return $foundstatic
manual :: fileRow (string $file):obj
line 131
$meta = (new build_file($file))->meta
$key = basename($file, '.phlo')
return obj(
name: basename($file),
summary: trim((string)($meta['summary'] ?? void)),
advice: trim((string)($meta['advice'] ?? void)),
lines: count(file($file) ?: []),
nodes: static::nodesByFile()[$key] ?? [],
)static
manual :: sources:array
line 143
$path = app
$own = %manual->label('own')
$config = json_decode((string)@file_get_contents($path.'data/app.json'), true)
if (!is_array($config)) return []
$dirs = [$path => $own]
foreach ((array)($config['paths']['resources'] ?? []) AS $dir){
$dir = rtrim((string)$dir, slash).slash
if (!is_dir($dir)) continue
$label = static::dirLabel($dir)
if ($label !== void) $dirs[$dir] = $label
}
$groups = []
foreach ($dirs AS $dir => $label) $groups[$label] = []
foreach (glob($path.'*.phlo') ?: [] AS $file) $groups[$own][] = static::fileRow($file)
foreach ((array)($config['resources'] ?? []) AS $name){
foreach ($dirs AS $dir => $label){
if ($label === $own) continue
$file = $dir.$name.'.phlo'
if (!is_file($file)) continue
$groups[$label][] = static::fileRow($file)
break
}
}
return array_filter($groups)static
manual :: dirLabel (string $dir):string
line 172
$root = trim((string)@shell_exec('git -C '.escapeshellarg($dir).' rev-parse --show-toplevel 2>/dev/null'))
$marker = $root === void ? null : json_decode((string)@file_get_contents($root.slash.'layer.json'), true)
if (is_array($marker) && ($marker['docs'] ?? true) !== false){
$label = trim((string)($marker['label'] ?? void))
if ($label !== void) return $label
preg_match('~^#\s+(.+)~m', (string)@file_get_contents($root.slash.'README.md'), $match)
if (($label = trim((string)($match[1] ?? void))) !== void) return $label
}
if (str_starts_with(rtrim($dir, slash), rtrim(dirname(rtrim(app, slash)), slash))) return %manual->label('shared')
return voidstatic
manual :: cacheFile:string
line 185
data.'manual.json'static
manual :: cached:?obj
line 187
$file = static::cacheFile()
if (!is_file($file)) return null
$row = json_decode((string)file_get_contents($file))
return $row instanceof \stdClass ? obj(...(array)$row) : nullstatic
manual :: configured:bool
line 194
if (!class_exists('AI') || !class_exists('creds')) return false
return (string)(%creds->OpenAI ?? void) !== void || (string)(%creds->Claude ?? void) !== voidstatic
manual :: appName:string
line 199
defined('id') ? (string)id : (string)%app->titlestatic
manual :: model:string
line 201
$set = class_exists('setting') ? (string)(setting::value('docs.model') ?: void) : void
return $set ?: 'gpt-5.4-mini'static
manual :: summary:obj
line 206
$info = static::appInfo()
$head = $info === void ? void : md5($info)
$row = static::cached()
if ($row && (string)$row->hash === $head && $head !== void) return $row
if (!static::configured()) return obj(hash: $head, text: void, missing: true)
$lines = ['This is the description of the app:']
$lines[] = mb_substr(static::appInfo(), 0, 4000)
$lines[] = 'And these are the latest changes:'
foreach (static::commits(app) AS $commit) $lines[] = $commit->date.space.$commit->subject
try {
$answer = phlo('AI')->chat(model: static::model(), system: static::$instruction, user: implode(lf, $lines))
$text = trim((string)($answer->answer ?? void))
}
catch (\Throwable $e){
return obj(hash: $head, text: void, error: mb_substr($e->getMessage(), 0, 200))
}
if ($text === void) return obj(hash: $head, text: void, error: %manual->label('noAnswer'))
@file_put_contents(static::cacheFile(), json_encode(['hash' => $head, 'text' => $text, 'written' => time()]))
return obj(hash: $head, text: $text)static
manual :: pageFile:string
line 229
data.'manual.html'static
manual :: store (string $body, string $page):bool
line 231
$mark = '<!-- manual '.md5($body).' -->'
$file = static::pageFile()
if (is_file($file) && str_contains((string)@file_get_contents($file), $mark)) return false
return (bool)@file_put_contents($file, preg_replace('~<meta name="csrf"[^>]*>\s*~', void, $page).lf.$mark.lf)static
manual :: mdBlocks (string $md):array
line 238
$lines = explode(lf, str_replace(cr, void, $md))
$count = count($lines)
$blocks = []
$para = []
$i = 0
while ($i < $count){
$line = $lines[$i]
if (trim($line) === void){
static::mdPara($para, $blocks)
$i++
continue
}
if (preg_match('/^ {0,3}(`{3,}|~{3,})/', $line, $match)){
static::mdPara($para, $blocks)
$fence = '/^ {0,3}'.preg_quote($match[1][0], slash).'{3,}\s*$/'
$body = []
$i++
while ($i < $count && !preg_match($fence, $lines[$i])){
$body[] = $lines[$i]
$i++
}
$i++
$blocks[] = obj(type: 'code', text: implode(lf, $body))
continue
}
if (preg_match('/^ {0,3}(#{1,6})\s+(.*?)\s*#*$/', $line, $match)){
static::mdPara($para, $blocks)
$blocks[] = obj(type: 'heading', depth: min(4, max(2, strlen($match[1]))), text: trim($match[2]))
$i++
continue
}
if (preg_match('/^ {0,3}([-*_])(?:\s*\1){2,}\s*$/', $line)){
static::mdPara($para, $blocks)
$blocks[] = obj(type: 'hr')
$i++
continue
}
if (preg_match('/^ {0,3}>/', $line)){
static::mdPara($para, $blocks)
$body = []
while ($i < $count && preg_match('/^ {0,3}>\s?(.*)$/', $lines[$i], $match)){
$body[] = $match[1]
$i++
}
$blocks[] = obj(type: 'quote', text: trim(implode(lf, $body)))
continue
}
if (str_contains($line, pipe) && preg_match('/^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/', (string)($lines[$i + 1] ?? void))){
static::mdPara($para, $blocks)
$head = static::mdCells($line)
$rows = []
$i += 2
while ($i < $count && str_contains($lines[$i], pipe)){
$rows[] = static::mdCells($lines[$i])
$i++
}
$blocks[] = obj(type: 'table', head: $head, rows: $rows)
continue
}
if (($marker = static::mdMarker($line)) !== null){
static::mdPara($para, $blocks)
$raw = []
while ($i < $count){
if (trim($lines[$i]) === void){
if (static::mdBreaks(static::mdMarker((string)($lines[$i + 1] ?? void)), $marker)) break
$raw[] = void
$i++
continue
}
$here = static::mdMarker($lines[$i])
if ($here === null && !preg_match('/^ {2,}\S/', $lines[$i])) break
if ($here !== null && static::mdBreaks($here, $marker)) break
$raw[] = $lines[$i]
$i++
}
$blocks[] = obj(type: 'list', ordered: $marker->ordered, items: static::mdItems($raw, $marker->indent))
continue
}
$para[] = $line
$i++
}
static::mdPara($para, $blocks)
return $blocksstatic
manual :: mdMarker (string $line):?obj
line 324
if (!preg_match('/^( *)([*+-]|\d{1,9}\.)\s+/', $line, $match)) return null
return obj(indent: strlen($match[1]), ordered: ctype_digit($match[2][0]))static
manual :: mdBreaks (?obj $here, obj $marker):bool
line 329
$here === null || ($here->indent <= $marker->indent && $here->ordered !== $marker->ordered)static
manual :: mdPara (array &$para, array &$blocks):void
line 331
$text = trim(implode(lf, $para))
$para = []
if ($text !== void) $blocks[] = obj(type: 'para', text: $text)static
manual :: mdCells (string $line):array
line 337
array_map('trim', explode(pipe, trim(trim($line), pipe)))static
manual :: mdItems (array $lines, int $indent):array
line 339
$groups = []
$group = null
foreach ($lines AS $line){
if (preg_match('/^( *)([*+-]|\d{1,9}\.)\s+(.*)$/', $line, $match) && strlen($match[1]) <= $indent){
if ($group !== null) $groups[] = $group
$group = [$match[3]]
continue
}
if ($group === null) continue
$group[] = preg_replace('/^ {1,'.($indent + 2).'}/', void, $line)
}
if ($group !== null) $groups[] = $group
$items = []
foreach ($groups AS $group){
$raw = trim(implode(lf, $group))
$checked = null
if (preg_match('/^\[([ xX])\]\s*(.*)$/s', $raw, $match)){
$checked = strtolower($match[1]) === 'x'
$raw = $match[2]
}
$sub = static::mdBlocks($raw)
$text = void
if ($sub && $sub[0]->type === 'para') $text = array_shift($sub)->text
$items[] = obj(text: $text, checked: $checked, blocks: $sub)
}
return $itemsstatic
manual :: mdInline (string $text):string
line 368
$out = void
foreach (preg_split('/(`+[^`]*`+)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE) AS $index => $part){
if ($index % 2) $out .= '<code>'.esc(trim($part, bt)).'</code>'
else $out .= static::mdFormat($part)
}
return $outstatic
manual :: mdFormat (string $text):string
line 377
$out = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', esc($text))
$out = preg_replace('/(?<![*\w])\*([^*\n]+)\*(?![*\w])/', '<em>$1</em>', $out)
return preg_replace('/\[([^\]]+)\]\(\s*((?:https?:|mailto:|[\/#])\S*?)\s*\)/', '<a href="$2">$1</a>', $out)route
route both GET manual
line 383
%app->title = static::appName()
%app->css = []
%app->js = []
%app->defer = []
$body = %manual->page()
$page = view($body, title: %manual->label('title'), css: [], js: [], inline: true, ns: 'manual')
static::store($body, $page)
return $pageview
%manual -> page
line 394
<main.docs>
<header.docs__head>
<h1>{{ manual::appName() }}</h1>
<p.docs__sub>{{ %manual->label('lead') }} {{ %manual->label('state') }} {{ static::head() ?: %manual->label('unknown') }}.</p>
</header>
<div#docs-summary>{{ %manual->summaryBlock }}</div>
{{ %manual->infoBlock }}
{{ %manual->filesBlock }}
{{ %manual->routesBlock }}
{{ %manual->commitsBlock }}
</main>view
%manual -> summaryBlock
line 407
$row = static::summary()
if ($row->missing) return indentView(lf.'<section class="docs__box docs__box--quiet"><p>'.esc(%manual->label('noKey')).'</p></section>'.lf)
if ((string)($row->error ?? void) !== void) return indentView(lf.'<section class="docs__box docs__box--quiet"><p>'.esc(%manual->label('noSummary')).esc((string)$row->error).'</p></section>'.lf)
return indentView(lf.$this->summaryText($row).lf)view
%manual -> summaryText ($row)
line 414
<section.docs__box>
<h2>{{ %manual->label('summary') }}</h2>
<p.docs__lead>{{ $row->text }}</p>
</section>view
%manual -> infoBlock
line 420
$info = static::appInfo()
if ($info === void) return indentView(lf.'<section class="docs__box"><p>'.esc(%manual->label('noInfo')).'</p></section>'.lf)
return indentView(lf.$this->infoText(static::mdBlocks(preg_replace('~^#[^\n]*\n~', void, $info))).lf)view
%manual -> infoText (array $blocks)
line 426
<details.docs__box open>
<summary.docs__summary>{{ %manual->label('about') }}</summary>
<div.docs__md>
<foreach $blocks AS $block>
{{ %manual->mdBlock($block) }}
</foreach>
</div>
</details>view
%manual -> mdBlock ($block)
line 436
<if $block->type === 'heading' && $block->depth === 2>
<h2>{{ %manual->mdInline($block->text) }}</h2>
<elseif $block->type === 'heading' && $block->depth === 3>
<h3>{{ %manual->mdInline($block->text) }}</h3>
<elseif $block->type === 'heading'>
<h4>{{ %manual->mdInline($block->text) }}</h4>
<elseif $block->type === 'code'>
<pre.docs__pre>{[ $block->text ]}</pre>
<elseif $block->type === 'quote'>
<blockquote>{{ %manual->mdInline($block->text) }}</blockquote>
<elseif $block->type === 'hr'>
<hr>
<elseif $block->type === 'list'>
{{ %manual->mdList($block) }}
<elseif $block->type === 'table'>
{{ %manual->mdTable($block) }}
<else>
<p>{{ %manual->mdInline($block->text) }}</p>
</if>view
%manual -> mdList ($block)
line 457
$items = $block->items
return $block->ordered ? $this->mdOrdered($items) : $this->mdBullets($items)view
%manual -> mdBullets (array $items)
line 462
<ul>
<foreach $items AS $item>
{{ %manual->mdItem($item) }}
</foreach>
</ul>view
%manual -> mdOrdered (array $items)
line 469
<ol>
<foreach $items AS $item>
{{ %manual->mdItem($item) }}
</foreach>
</ol>view
%manual -> mdItem ($item)
line 476
<if $item->checked === true>
<li.docs__task.docs__task--done>{{ %manual->mdBody($item) }}</li>
<elseif $item->checked === false>
<li.docs__task>{{ %manual->mdBody($item) }}</li>
<else>
<li>{{ %manual->mdBody($item) }}</li>
</if>view
%manual -> mdBody ($item)
line 485
$out = static::mdInline((string)$item->text)
foreach ($item->blocks AS $block) $out .= lf.$this->mdBlock($block)
return $outview
%manual -> mdTable ($block)
line 491
<table.docs__table>
<thead>
<tr>
<foreach $block->head AS $cell>
<th>{{ %manual->mdInline($cell) }}</th>
</foreach>
</tr>
</thead>
<tbody>
<foreach $block->rows AS $row>
<tr>
<foreach $row AS $cell>
<td>{{ %manual->mdInline($cell) }}</td>
</foreach>
</tr>
</foreach>
</tbody>
</table>view
%manual -> filesBlock
line 511
$groups = static::sources()
if (!$groups) return void
return indentView(lf.$this->filesTable($groups).lf)view
%manual -> filesTable (array $groups)
line 517
<details.docs__box>
<summary.docs__summary>{{ %manual->label('files') }} ({{ array_sum(array_map('count', $groups)) }})</summary>
<foreach $groups AS $label => $files>
<h3>{{ $label }} <span.docs__num>{{ count($files) }}</span></h3>
<foreach $files AS $file>
{{ %manual->fileNodes($file) }}
</foreach>
</foreach>
</details>view
%manual -> fileNodes ($file)
line 528
<details.docs__file>
<summary.docs__summary>
<span.docs__mono>{{ $file->name }}</span>
<span.docs__filesum>{{ $file->summary ?: void }}</span>
<span.docs__num>{{ count($file->nodes) }}</span>
</summary>
<if $file->advice !== void>
<p.docs__meta>{{ $file->advice }}</p>
</if>
<if !$file->nodes>
<p.docs__meta>{{ %manual->label('noNodes') }}</p>
</if>
<foreach $file->nodes AS $node>
<div.docs__node>
<span.docs__kind>{{ $node->type }}</span>
<span.docs__mono>{{ $node->name }}{( $node->args !== void ? '('.$node->args.')' : void )}{( $node->ret !== void ? ': '.$node->ret : void )}</span>
<if $node->summary !== void>
<p.docs__meta>{{ mb_substr($node->summary, 0, 240) }}</p>
</if>
</div>
</foreach>
</details>view
%manual -> routesBlock
line 552
$groups = static::routeGroups()
if (!$groups) return void
return indentView(lf.$this->routesTable($groups).lf)view
%manual -> routesTable (array $groups)
line 558
<details.docs__box>
<summary.docs__summary>{{ %manual->label('routes') }} ({{ array_sum(array_map('count', $groups)) }})</summary>
<foreach $groups AS $file => $routes>
<h3>{{ $file }} <span.docs__num>{{ count($routes) }}</span></h3>
<foreach $routes AS $route>
{{ %manual->routeRow($route) }}
</foreach>
</foreach>
</details>view
%manual -> routeRow (array $route)
line 569
$whole = (string)($route['route'] ?? void)
$space = strpos($whole, space)
$method = $space === false ? void : substr($whole, 0, $space)
$path = $space === false ? $whole : substr($whole, $space + 1)
return indentView(lf.$this->routeLine($method, $path, (string)($route['summary'] ?? void)).lf)view
%manual -> routeLine (string $method, string $path, string $summary)
line 577
<div.docs__route>
<span.docs__origin>{{ $method ?: 'GET' }}</span>
<span.docs__path>/{{ str_replace(space, slash, trim($path)) }}</span>
<if $summary !== void>
<p.docs__meta>{{ $summary }}</p>
</if>
</div>view
%manual -> commitsBlock
line 586
$commits = static::commitList()
if (!$commits) return void
return indentView(lf.$this->commitsList($commits).lf)view
%manual -> commitsList (array $commits)
line 592
<details.docs__box>
<summary.docs__summary>{{ %manual->label('changes') }} ({{ count($commits) }})</summary>
<foreach $commits AS $commit>
<article.docs__commit>
<h4>{{ $commit->subject }}</h4>
<p.docs__meta>
<span.docs__origin>{{ $commit->origin }}</span>
{{ $commit->date }} · <span.docs__mono>{{ $commit->hash }}</span>
</p>
<if $commit->body !== void>
<details.docs__more>
<summary.docs__summary>{{ %manual->label('why') }}</summary>
<pre.docs__pre>{{ $commit->body }}</pre>
</details>
</if>
</article>
</foreach>
</details>view
style
line 612
.docs {
max-width: 54rem
margin: 0 auto
padding: 2rem 1.2rem 4rem
font: 16px/1.6 system-ui, sans-serif
color: #1d2126
}
.docs__head {
margin-bottom: 1.4rem
}
.docs h1 {
margin: 0 0 .3rem
font-size: 1.5rem
}
.docs__sub {
margin: 0 0 .9rem
color: #68727d
font-size: .92rem
}
.docs__box {
margin: 0 0 1rem
padding: 1rem 1.1rem
border: 1px solid #e2e6ea
border-radius: 10px
background: #fff
}
.docs__box--quiet {
background: #f6f8f9
color: #68727d
font-size: .92rem
}
.docs__box h2 {
margin: 0 0 .5rem
font-size: 1.05rem
}
.docs__summary {
cursor: pointer
font-weight: 600
font-size: .98rem
display: flex
align-items: baseline
gap: .5rem
list-style: none
\::-webkit-details-marker: display: none
}
.docs__summary::before {
content: '\25b8'
color: #9aa4ae
font-weight: 400
transition: transform .12s ease
}
.docs__file[open] > .docs__summary::before {
transform: rotate(90deg)
}
.docs__lead {
margin: 0 0 .8rem
}
.docs__md h2 {
margin: 1.1rem 0 .4rem
font-size: 1rem
}
.docs__md p, .docs__md ul {
margin: 0 0 .7rem
}
.docs__task {
list-style: none
margin-left: -1.1rem
}
.docs__task::before {
content: '\25cb\a0'
color: #68727d
}
.docs__task--done::before {
content: '\25cf\a0'
color: #1d2126
}
.docs__table {
width: 100%
border-collapse: collapse
font-size: .9rem
margin: .4rem 0 1rem
}
.docs__table th, .docs__table td {
padding: .28rem .5rem .28rem 0
border-bottom: 1px solid #eef1f3
vertical-align: top
}
.docs__table th {
text-align: left
color: #68727d
}
.docs__mono {
font-family: ui-monospace, monospace
font-size: .86rem
white-space: nowrap
}
.docs__num {
text-align: right
color: #68727d
}
.docs h3, .docs__md h4 {
margin: 1rem 0 .2rem
font-size: .9rem
color: #68727d
}
.docs__file {
margin: .15rem 0
padding: .3rem .5rem
border: 1px solid #eef1f3
border-radius: 8px
}
.docs__filesum {
color: #68727d
font-size: .86rem
font-weight: 400
margin-left: auto
text-align: right
}
.docs__node {
padding: .3rem 0 .3rem .8rem
border-left: 2px solid #eef1f3
margin: .3rem 0
}
.docs__kind {
display: inline-block
min-width: 4rem
color: #68727d
font-size: .78rem
text-transform: uppercase
}
.docs__origin {
display: inline-block
margin-right: .4rem
padding: .05rem .4rem
border-radius: 4px
background: #eef1f3
color: #4a5560
font-size: .74rem
text-transform: uppercase
letter-spacing: .02em
}
.docs__route {
padding: .3rem 0
border-bottom: 1px solid #eef1f3
}
.docs__path {
font-family: ui-monospace, monospace
font-size: .86rem
word-break: break-word
}
.docs__commit {
padding: .6rem 0
border-bottom: 1px solid #eef1f3
}
.docs__commit h4 {
margin: 0 0 .15rem
font-size: .95rem
}
.docs__meta {
margin: 0
color: #68727d
font-size: .84rem
}
.docs__pre {
margin: .4rem 0 0
padding: .6rem .7rem
border-radius: 8px
background: #f6f8f9
font-family: ui-monospace, monospace
font-size: .84rem
white-space: pre-wrap
}
.docs__more {
margin-top: .3rem
}
.docs__btn {
padding: .35rem .8rem
border: 1px solid #d6dade
border-radius: 6px
background: #fff
font-size: .88rem
cursor: pointer
}object
%payload
/phlo/resources/payload.phlo
method
%payload -> controller
line 11
contentType = %req->contentType
if (in_array(phlo('req')->method, ['POST', 'PUT', 'PATCH', 'QUERY']) && str_starts_with($contentType, 'application/json')){
$data = json_decode((string)file_get_contents('php://input'))
return $this->objData = is_object($data) ? get_object_vars($data) : (is_array($data) ? $data : [])
}
if ($_POST) loop($_POST, fn($value, $key) => $this->$key = $value)
elseif (in_array(phlo('req')->method, ['PUT', 'PATCH', 'QUERY']) && str_starts_with($contentType, 'application/x-www-form-urlencoded')){
$body = file_get_contents('php://input')
$data = []
parse_str($body, $data)
if ($data) loop($data, fn($value, $key) => $this->$key = $value)
}
elseif (in_array(phlo('req')->method, ['PUT', 'PATCH', 'QUERY']) && str_starts_with($contentType, 'multipart/form-data')){
$match = regex('/boundary="?([^";]+)"?/', $contentType)
if (!$match) return
$boundary = '--'.$match[1]
$arrays = []
$raw = file_get_contents('php://input')
foreach (explode($boundary, $raw) AS $part){
if (!trim($part) || $part === '--' || !str_contains($part, nl.nl)) continue
$headers = []
[$rawHeaders, $body] = explode(nl.nl, $part, 2)
foreach (explode(nl, trim($rawHeaders)) AS $header){
if (str_contains($header, colon)){
[$key, $value] = explode(colon, $header, 2)
$headers[strtolower(trim($key))] = trim($value)
}
}
if (!isset($headers['content-disposition'])) continue
if (!preg_match('/name="([^"]+)"/', $headers['content-disposition'], $match)) continue
$name = $match[1]
$body = rtrim($body, nl)
if ($body === void) $body = null
$base = $name
$keys = []
$hasEmptyIndex = false
if (preg_match('/^([^\[]+)((?:\[[^\]]*\])*)$/', $name, $m)){
$base = $m[1]
$brackets = $m[2]
if ($brackets){
preg_match_all('/\[([^\]]*)\]/', $brackets, $mm)
$keys = $mm[1]
$hasEmptyIndex = in_array(void, $keys, true)
}
}
if ($hasEmptyIndex) $arrays[] = $base
$assign = function($value) use ($base, $keys){
if ($keys){
if (!isset($this->objData[$base]) || !is_array($this->objData[$base])) $this->objData[$base] = []
$ref =& $this->objData[$base]
$count = count($keys)
foreach ($keys AS $i => $k){
$last = $i === $count - 1
if ($k === void){
if ($last) $ref[] = $value
else {
$ref[] = []
end($ref)
$idx = key($ref)
$ref =& $ref[$idx]
}
}
else {
if ($last) $ref[$k] = $value
else {
if (!isset($ref[$k]) || !is_array($ref[$k])) $ref[$k] = []
$ref =& $ref[$k]
}
}
}
}
else $this->objData[$base] = $value
};
if (preg_match('/filename="([^"]*)"/', $headers['content-disposition'], $f)){
if ($f[1] === void || $body === null){
if (!$hasEmptyIndex) $assign(null)
continue
}
$filename = $f[1]
$file = %file(tempnam(sys_get_temp_dir(), 'phlo'), $filename, $body)
$assign($file)
}
else $assign($body)
}
foreach ($this->objData AS $key => $val){
if (str_ends_with($key, '[]')){
unset($this->objData[$key])
$this->objData[substr($key, 0, -2)] = is_array($val) ? array_values(array_filter($val, fn($v) => $v !== null)) : [$val]
}
elseif (!is_array($val) && substr($key, -2) === '[]') $this->objData[$key] = [$val]
}
foreach (array_unique($arrays) AS $key) if (!isset($this->objData[$key])) $this->objData[$key] = []
}
if ($_FILES) loop($_FILES, fn($f, $key) => $this->$key = is_array($f['name']) ? loop(array_keys($f['name']), fn($i) => $f['error'][$i] ? null : %file($f['tmp_name'][$i], $f['name'][$i], mime: $f['type'][$i], size: $f['size'][$i])) : ($f['error'] ? null : %file($f['tmp_name'], $f['name'], mime: $f['type'], size: $f['size'])))object
%seo
/phlo/resources/seo.phlo
route
route GET sitemap.xml
line 11
Deze route genereert een sitemap voor SEO-doeleinden wanneer deze via een GET-verzoek wordt benaderd.
output($this)route
route GET robots.txt
line 13
Definieert een route die reageert op GET-verzoeken door de inhoud van het robots-bestand in platte tekstformaat met UTF-8-charset uit te geven.
output($this->robots(), type: 'text/plain; charset=utf-8')method
%seo -> robots:string
line 15
Genereert de inhoud van een robots.txt-bestand op basis van de instellingen van de applicatie, waarbij wordt gespecificeerd welke gebruikersagenten zijn toegestaan of geweerd bij het toegang krijgen tot bepaalde paden, en voegt een link naar de sitemap toe.
if (!(defined('indexable') && indexable)) return 'User-agent: *'.lf.'Disallow: /'.lf
$lines = ['User-agent: *', 'Allow: /']
foreach ((array)(%app->robotsDisallow ?? []) AS $path) $lines[] = 'Disallow: '.$path
$lines[] = 'Sitemap: '.%req->base.slash.'sitemap.xml'
return implode(lf, $lines).lfmethod
%seo -> intl ($uri):string
line 23
Haal de geïndividualiseerde versie van een URI op uit de slugs van de applicatie, en retourneer de originele URI als er geen vertaling wordt gevonden.
(%app->slugs ?? [])[$uri] ?? $urimethod
%seo -> uri ($page):string
line 28
A sitemap entry is a uri string, or an object carrying that uri plus more.
is_string($page) ? $page : (string)($this->field($page, 'uri') ?? void)method
%seo -> field ($page, $key):mixed
line 30
if (is_string($page)) return null
return is_array($page) ? ($page[$key] ?? null) : ($page->$key ?? null)method
%seo -> lastmod ($page):?string
line 40
The date for one entry, or nothing at all when it does not parse.
$mod = $this->field($page, 'lastmod')
($mod === null || $mod === void) && class_exists('lastmod') && $mod = %lastmod->for($this->uri($page))
if ($mod === null || $mod === void) return null
if (is_string($mod) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $mod)) return $mod
$ts = is_int($mod) ? $mod : strtotime((string)$mod)
return $ts ? date('c', $ts) : nullmethod
%seo -> locale:string
line 49
Deze functie haalt de locale-code op die overeenkomt met de taalinstelling van de applicatie, met 'en' als standaard als deze niet is ingesteld.
$lang = %app->lang ?? 'en'
$map = ['nl' => 'nl_NL', 'en' => 'en_US', 'de' => 'de_DE', 'fr' => 'fr_FR', 'es' => 'es_ES', 'it' => 'it_IT', 'pt' => 'pt_PT', 'pl' => 'pl_PL', 'ru' => 'ru_RU', 'el' => 'el_GR', 'tr' => 'tr_TR', 'zh' => 'zh_CN']
return $map[$lang] ?? $langprop
%seo -> ogTitle:string
line 55
Dit haalt de Open Graph-titel op voor SEO-doeleinden met behulp van de title() functie.
title()prop
%seo -> ogDescr
line 56
Haal de beschrijving van de applicatie op, retourneert void als deze niet is ingesteld.
%app->description ?? voidprop
%seo -> ogImageFile:string
line 57
(string)(%app->image ?? (is_file(www.'icon.webp') ? 'icon.webp' : void))prop
%seo -> ogImage:string
line 58
$this->ogImageFile === void ? void : %req->base.slash.ltrim($this->ogImageFile, slash)prop
%seo -> canonical:string
line 59
Dit haalt de canonical URL op voor SEO-doeleinden uit het aanvraagobject.
%req->urlprop
%seo -> ogType:string
line 60
Stelt het Open Graph-type in voor SEO-doeleinden, specifiek op 'website'.
'website'prop
%seo -> sitemapPages:array
line 61
(array)(%app->pages ?? [''])prop
%seo -> sitemapLangs:array
line 62
array_keys((array)(%app->langs ?? []))prop
%seo -> siteName:string
line 63
Deze expressie wijst de waarde van de titel van de app of de id toe aan de variabele $siteName voor SEO-doeleinden.
%app->title ?? idprop
%seo -> twitterCard:bool
line 64
De seo->$twitterCard eigenschap wordt gebruikt om de Twitter Card metadata voor een view te configureren, waardoor een betere integratie met Twitter deelfunctionaliteiten mogelijk is.
falseprop
%seo -> structuredData
line 69
Schema.org data for this page, as '@type' plus what only your app knows.
%app->structuredData ?? nullmethod
%seo -> schemaData:?array
line 71
if (!$this->structuredData) return null
$base = [
'@context' => 'https://schema.org',
'url' => $this->canonical,
'inLanguage' => %app->lang ?? 'en',
'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => $this->canonical],
'publisher' => ['@type' => 'Organization', 'name' => $this->siteName, 'url' => %req->base],
]
$this->ogDescr && $base['description'] = $this->ogDescr
$this->ogImage && $base['image'] = $this->ogImage
return array_replace($base, (array)$this->structuredData)prop
%seo -> noIndex:bool
line 84
De seo->$noIndex expressie evalueert naar true als zowel %app->noLink als %app->noIndex niet zijn ingesteld, anders retourneert het false.
%app->noLink ?? %app->noIndex ?? falseview
%seo -> view
line 86
<?xml version=1.0 encoding="UTF-8"?>
<urlset xmlns=http://www.sitemaps.org/schemas/sitemap/0.9 xmlns:xhtml=http://www.w3.org/1999/xhtml xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation=http://www.sitemaps.org/schemas/sitemap/0.9+http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd>
<foreach $this->sitemapPages AS $page>
{{ $this->page($page) }}
</foreach>
</urlset>view
%seo -> page ($page)
line 94
<url>
<loc>%req->base{( ($uri = $this->uri($page)) ?: slash )}</loc>
<if $stamp = $this->lastmod($page)>
<lastmod>$stamp</lastmod>
</if>
<foreach $this->sitemapLangs AS $lang>
<if $lang === %app->lang>
{{ $this->xlink('x-default', $uri ?: slash) }}
</if>
{{ $this->xlink($lang, $lang === %app->lang ? ($uri ?: slash) : "/$lang".($this->intl($uri) ?: void)) }}
</foreach>
</url>view
%seo -> xlink ($lang, $uri)
line 108
Genereert een alternatieve linkelement voor SEO-doeleinden, waarbij de taal en URL voor de huidige weergave worden gespecificeerd.
<xhtml:link rel=alternate hreflang="$lang" href="%req->base$uri"{{ slash }}>view
%seo -> link ($lang, $uri)
line 109
Genereert een alternatieve link-tag voor SEO-doeleinden, die de taal en de URL van de bron specificeert.
<link rel=alternate hreflang="$lang" href="%req->base$uri">view
%seo -> head
line 111
<if $this->noIndex>
<meta name=robots content=noindex,follow>
</if>
<if $this->ogDescr>
<meta name=description content="$this->ogDescr">
</if>
<meta property=og:site_name content="$this->siteName">
<meta property=og:title content="{[ $this->ogTitle ]}">
<meta property=og:description content="$this->ogDescr">
<meta property=og:type content="$this->ogType">
<meta property=og:url content="{[ $this->canonical ]}">
<if $this->ogImage>
<meta property=og:image content="$this->ogImage">
</if>
<meta property=og:locale content="$this->locale">
<if $this->twitterCard>
<meta name=twitter:card content=summary_large_image>
<meta name=twitter:title content="{[ $this->ogTitle ]}">
<meta name=twitter:description content="$this->ogDescr">
<if $this->ogImage>
<meta name=twitter:image content="$this->ogImage">
</if>
</if>
<if $data = $this->schemaData>
<script type="application/ld+json">{{ json_encode($data, JSON_UNESCAPED_UNICODE) }}</script>
</if>
<if !$this->noIndex>
<link rel=canonical href="{[ $this->canonical ]}">
</if>object
%session
/phlo/resources/session.phlo
static
session :: options:array
line 10
[]method
%session -> controller
line 12
ession_start(static::options())
$this->objData = $_SESSIONmethod
%session -> __set ($key, $value)
line 15
Stelt een sessievariabele in met de opgegeven sleutel op de gegeven waarde.
$_SESSION[$key] = $this->objData[$key] = $valuemethod
%session -> __unset ($key)
line 16
Verwijdert de opgegeven sleutel uit de sessiegegevens en de interne objectgegevens.
unset($this->objData[$key], $_SESSION[$key])method
%session -> __isset ($key):bool
line 17
Controleert of een sessievariabele die door de gegeven sleutel is geïdentificeerd, is ingesteld en niet null is.
isset($this->objData[$key])method
%session -> objRegenerateId ($deleteOld = true):void
line 19
Regenerates de sessie-ID voor de huidige sessie, waarbij optioneel de oude sessiegegevens worden verwijderd op basis van de $deleteOld-parameter.
session_regenerate_id($deleteOld)
$this->objData = $_SESSIONobject
%stream
/phlo/resources/stream.phlo
function
function stream ($data = null, string $type = 'application/octet-stream', ?string $name = null):void
line 11
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid stream()')
if (!$res->streaming){
$res->streaming = true
$res->type = $type
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->header('X-Accel-Buffering', 'no')
$name === null || $res->header('Content-Disposition', 'attachment; filename="'.str_replace('"', '', $name).'"')
$res->render()
}
if ($data === null) return
foreach (is_iterable($data) ? $data : [$data] as $part){
print((string)$part)
$cli || [@ob_flush(), flush()]
}view
script
line 31
const streamFilename = disposition => {
const extended = disposition.match(/(?:^|;)\s*filename\*\s*=\s*UTF-8'[^']*'([^;]+)/i)
const plain = disposition.match(/(?:^|;)\s*filename\s*=\s*(?:"((?:\\.|[^"])*)"|([^;]+))/i)
const value = extended?.[1] ?? plain?.[1] ?? plain?.[2]
if (!value) return null
const name = value.trim().replace(/\\(.)/g, '$1')
try { return decodeURIComponent(name) }
catch (_) { return name }
}
app.stream = async (path, onData = null, async = false, type = null, data = null) => {
const url = `${location.origin}/${path}`
const headers = {}
const csrf = obj('meta[name="csrf"]')?.content
csrf && (headers['X-CSRF-Token'] = csrf)
async && (headers['X-Requested-With'] = 'phlo')
let body = data
if (body !== null && !(body instanceof FormData) && !(body instanceof Blob)) [body = JSON.stringify(body), headers['Content-Type'] = 'application/json']
phlo.log(`⇣ APP.STREAM ${url}`)
const res = await fetch(url, {method: body === null ? 'GET' : 'POST', credentials: 'same-origin', headers, body})
if (!res.ok) throw new Error(`stream ${url} 🔴 ${res.status}`)
const mime = (res.headers.get('content-type') ?? '').split(';')[0].trim()
const kind = type ?? (mime === 'application/x-ndjson' ? 'ndjson' : mime === 'application/json' ? 'json' : mime === 'text/event-stream' ? 'sse' : mime.startsWith('text/') ? 'text' : 'raw')
const emit = value => onData && onData(value)
if (kind === 'json'){
const value = await res.json()
emit(value)
return value
}
if (kind === 'blob'){
const blob = await res.blob()
const name = streamFilename(res.headers.get('content-disposition') ?? '')
const value = name ? new File([blob], name) : blob
emit(value)
return value
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
const out = []
const parts = []
let text = ''
let buffer = ''
const line = raw => {
if (!raw) return
const value = JSON.parse(raw)
out.push(value)
emit(value)
}
const frame = raw => {
if (!raw.trim()) return
let event = 'message'
const data = []
raw.split('\n').forEach(l => l.startsWith('event:') ? event = l.slice(6).trim() : l.startsWith('data:') && data.push(l.slice(5).replace(/^ /, '')))
const value = {event, data: data.join('\n')}
out.push(value)
emit(value)
}
while (true){
const {done, value} = await reader.read()
if (done) break
if (kind === 'raw'){
parts.push(value)
emit(value)
continue
}
const piece = decoder.decode(value, {stream: true})
if (kind === 'text'){
text += piece
emit(piece)
continue
}
buffer += piece
if (kind === 'sse'){
const hold = buffer.endsWith('\r') ? '\r' : ''
buffer = buffer.slice(0, buffer.length - hold.length).replace(/\r\n?/g, '\n') + hold
}
const pieces = buffer.split(kind === 'sse' ? '\n\n' : '\n')
buffer = pieces.pop()
pieces.forEach(kind === 'sse' ? frame : line)
}
if (kind === 'raw') return new Blob(parts, {type: mime || 'application/octet-stream'})
const tail = decoder.decode()
if (kind === 'text'){
tail && [text += tail, emit(tail)]
return text
}
buffer += tail
buffer && (kind === 'sse' ? frame(buffer.replace(/\r\n?/g, '\n')) : line(buffer))
return out
}object
%tasks
/phlo/resources/tasks.phlo
static
tasks :: dir:string
line 10
Toegang tot het pad van de directory voor taken, specifiek verwijzend naar 'tasks/'.
data.'tasks/'static
tasks :: run:void
line 12
Voert geplande taken uit door hun vervaldatum te controleren en ze te vergrendelen om gelijktijdige uitvoering te voorkomen. Het slaat de uitvoeringsdetails op en markeert de taak als voltooid na uitvoering.
is_dir(static::dir()) || mkdir(static::dir(), 0755, true)
$now = time()
foreach (%app->tasks ?? [] AS $name => $task){
$task = (object)$task
if (!static::due($name, $task, $now)) continue
if (!static::lock($name)) continue
$schedule = array_intersect_key((array)$task, array_flip(['every', 'daily', 'weekly']))
$do = is_string($task->do) ? $task->do : null
static::saveRun($name, $do, $schedule, static::fire($task->do))
static::markRun($name, $now)
static::unlock($name)
}static
tasks :: saveRun ($name, $do, $schedule, $return)
line 27
Slaat de uitvoergegevens op in een JSON-bestand in de opgegeven map, met de opgegeven naam, do, schema en return-waarden.
json_write(static::dir().$name.'.json', arr(do: $do, schedule: $schedule, return: $return))static
tasks :: due ($name, $task, $now):bool
line 33
daily and weekly match the exact minute the task names.
$last = static::lastRun($name)
if (isset($task->every)){
$every = preg_match('/^\d/', $task->every) ? $task->every : '1 '.$task->every
$seconds = strtotime("+$every", 0) ?: 0
return $seconds > 0 && ($now - $last) >= $seconds
}
if (isset($task->daily)){
if (date('H:i', $now) !== $task->daily) return false
return $last < strtotime('today 00:00', $now)
}
if (isset($task->weekly)){
if (date('D H:i', $now) !== date('D H:i', strtotime($task->weekly, $now))) return false
return $last < strtotime('monday this week', $now)
}
return falsestatic
tasks :: fire ($do)
line 51
Voert een taak uit die is gedefinieerd door een Closure, een 'Class::method' string of een resource-naam string, en retourneert het resultaat van de uitvoering.
if ($do instanceof \Closure) return $do()
if (is_string($do) && str_contains($do, '::')){
[$class, $method] = explode('::', $do, 2)
return $class::$method()
}
if (is_string($do)) return phlo($do)
error('Task do must be Closure, "Class::method" string, or resource-name string')static
tasks :: lastRun ($name):int
line 61
Haal de laatste uitvoeringstijdstempel van een taak op uit een bestand, en retourneer 0 als het bestand niet bestaat.
$file = static::dir().$name.'.last'
return is_file($file) ? (int)file_get_contents($file) : 0static
tasks :: markRun ($name, $ts):int|false
line 66
Schrijft de tijdstempel van de laatste uitvoering van een taak naar een bestand met de naam van de taak in de opgegeven directory, met gebruik van exclusieve vergrendeling om gelijktijdige schrijfbewerkingen te voorkomen.
file_put_contents(static::dir().$name.'.last', (string)$ts, LOCK_EX)static
tasks :: lock ($name):bool
line 68
Maakt een lockbestand voor een taak aan als het nog niet bestaat of ouder is dan een uur.
$file = static::dir().$name.'.lock'
if (is_file($file) && (time() - filemtime($file)) < 3600) return false
touch($file)
return truestatic
tasks :: unlock ($name):bool
line 75
Verwijdert het vergrendelingsbestand dat aan een taak is gekoppeld, zodat deze opnieuw kan worden uitgevoerd.
@unlink(static::dir().$name.'.lock')object
%useragent
/phlo/resources/useragent.phlo
prop
%useragent -> source:?string
line 10
Deze expressie haalt de user agent-string op uit het aanvraagobject en retourneert null als deze niet is ingesteld.
%req->userAgent ?: nullprop
%useragent -> os:string
line 12
Bepaalt het besturingssysteem op basis van de user agent-string door deze te vergelijken met vooraf gedefinieerde patronen.
if (!$this->source) return 'Unknown'
$list = [
'Android' => '/Android/i',
'iPadOS' => '/iPad.*OS/i',
'iOS' => '/iPhone|iPod/i',
'Windows' => '/Windows NT/i',
'macOS' => '/Mac OS X/i',
'ChromeOS' => '/CrOS/i',
'Linux' => '/Linux/i',
]
foreach ($list AS $n => $r) if (preg_match($r, $this->source)) return $n
if (preg_match('/iPad/i',$this->source) && preg_match('/Mac OS X/i',$this->source)) return 'iPadOS'
return 'Unknown'prop
%useragent -> osV:string
line 28
Haalt de versie van het besturingssysteem uit de user agent-string als deze beschikbaar is, en retourneert deze in een schone indeling.
if (!$this->source) return void
if (preg_match('/(?:Android|OS X|OS|Windows NT)\s*([0-9._]+)/i', $this->source, $m)){
$v = strtr($m[1], [us => dot])
$v = preg_replace('/[^0-9.].*/', void, $v)
$v = preg_replace('/(?:\.0)+$/', void, $v)
return $v
}
return voidprop
%useragent -> osFull:string
line 39
Geeft de volledige naam van het besturingssysteem terug, samen met de versie als deze beschikbaar is; anders retourneert het 'Onbekend' of alleen de naam van het besturingssysteem.
if (!$this->OS) return 'Unknown'
if ($this->OS === 'Windows') return 'Windows'
$v = $this->osV
if (!$v) return $this->OS
$short = preg_replace('/^(\d+\.\d+).*/','$1',$v)
if (preg_match('/\.0$/',$short)) $short = preg_replace('/\.0$/', void, $short)
return trim($this->OS.space.$short)prop
%useragent -> name:string
line 49
Bepaalt de naam van de webbrowser op basis van de gebruikersagentstring die in de source is opgegeven. Het controleert verschillende patronen om populaire browsers zoals Chrome, Firefox en Safari te identificeren en retourneert 'Onbekend' als er geen overeenkomst wordt gevonden.
if (!$this->source) return 'Unknown'
if (preg_match('/\bwv\b/',$this->source) || (preg_match('/Version\/\d+\.\d+/',$this->source) && strpos($this->source,'Chrome/')!==false && strpos($this->source,'Safari/')!==false && strpos($this->source,' Mobile ')!==false)) return 'Android WebView'
if (preg_match('/CriOS\/([0-9.]+)/',$this->source)) return 'Chrome'
if (preg_match('/FxiOS\/([0-9.]+)/',$this->source)) return 'Firefox'
$list = [
'Edge' => '/Edg\/([0-9.]+)/',
'Opera' => '/OPR\/([0-9.]+)/',
'Samsung Internet' => '/SamsungBrowser\/([0-9.]+)/i',
'Chrome' => '/Chrome\/([0-9.]+)/',
'Firefox' => '/Firefox\/([0-9.]+)/',
'Safari' => '/Version\/([0-9.]+).*Safari/i',
]
foreach ($list AS $n => $r) if (preg_match($r, $this->source)) return $n
return 'Unknown'prop
%useragent -> version:string
line 66
Haalt het versienummer uit de user agent-string als deze overeenkomt met specifieke browserpatronen, en retourneert het schoongemaakte versienummer of void als er geen overeenkomst is.
if (!$this->source) return void
if (preg_match('/(?:Edg|OPR|Chrome|Firefox|Version|CriOS|FxiOS|SamsungBrowser)\/([0-9.]+)/', $this->source, $m)){
$v = $m[1]
$v = preg_replace('/[^0-9.].*/', void, $v)
$v = preg_replace('/(?:\.0)+$/', void, $v)
return $v
}
return voidprop
%useragent -> full:string
line 77
Geeft de volledige user agent-string terug, inclusief de naam en versie van de user agent, geformatteerd om onnodige delen uit te sluiten.
if (!$this->name) return 'Unknown'
$v = $this->version
if (!$v) return $this->name
$short = preg_replace('/^(\d+\.\d+).*/','$1',$v)
if (preg_match('/\.0$/',$short)) $short = preg_replace('/\.0$/', void, $short)
return rtrim($this->name.space.$short)prop
%useragent -> device:string
line 86
Bepaalt het type apparaat (Tablet, Telefoon of Desktop) op basis van de user agent-string die is opgeslagen in de source-eigenschap.
if (!$this->source) return 'Unknown'
if (preg_match('/iPad|Tablet|Tab|SM-T|Nexus 7|Nexus 10/i', $this->source)) return 'Tablet'
if (preg_match('/Mobile|iPhone|Android.*Mobile|SM-G|Pixel [0-9]/i', $this->source)) return 'Phone'
return 'Desktop'object
%visitors
/phlo/resources/visitors.phlo
static
visitors :: table:string
line 12
De 'visitors::$table' verwijst naar de database tabel die is gekoppeld aan de 'visitors' resource in Phlo.
'visitors'static
visitors :: columns
line 13
Definieert de kolommen voor de visitors resource, waarbij de te gebruiken attributen voor gegevensbewerkingen worden gespecificeerd.
'id,token,host,page,lang,IP,browser,os,device,active_seconds,state,width,height,referrer,created,changed'static
visitors :: history:array
line 15
Haal een geschiedenis op van bezoekersaantallen, gegroepeerd op datum en tel unieke tokens en totale bezoeken.
static::records(columns: 'FROM_UNIXTIME(changed, "%Y-%m-%d") AS date,COUNT(DISTINCT token) AS visitors,COUNT(id) AS visits', group: 'date', order: 'date DESC')static
visitors :: online:int
line 16
Dit haalt het aantal unieke online bezoekers op die in de afgelopen 9 seconden zijn veranderd.
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 9)')static
visitors :: lastHour:int
line 17
Haal het aantal unieke bezoekers (tokens) op die in het afgelopen uur zijn veranderd.
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 3600)')static
visitors :: isBot (?string $ua):bool
line 19
Bepaalt of de user agent-string aangeeft dat de bezoeker een bot is door deze te vergelijken met een vooraf gedefinieerd regex-patroon.
if (!$ua) return false
return (bool)preg_match('/bot|crawl|spider|slurp|baiduspider|facebookexternalhit|twitterbot|linkedinbot|curl|wget|python-requests|go-http-client|java\//i', $ua)static
visitors :: parseReferrer (string $url):string
line 24
Analyseert de referrer-URL om de gebruikte zoekmachine te identificeren en retourneert een geformatteerde string die de zoekmachine aangeeft of de host als er geen overeenkomst wordt gevonden.
static $engines = ['google' => 'Google', 'bing' => 'Bing', 'duckduckgo' => 'DuckDuckGo', 'yahoo' => 'Yahoo', 'baidu' => 'Baidu', 'yandex' => 'Yandex', 'ecosia' => 'Ecosia', 'startpage' => 'Startpage', 'brave' => 'Brave', 'kagi' => 'Kagi']
$host = strtolower(preg_replace('/^www\./', void, (string)(parse_url($url, PHP_URL_HOST) ?? void)))
foreach ($engines AS $key => $name) if (str_contains($host, $key)) return 'search:'.$name
return $host ?: substr($url, 0, 100)route
route PUT heartbeat @n,v,l,u,w,h,a,p,r,c,s,pp,ps
line 31
Behandelt het PUT-verzoek voor het bijwerken van hartslaginformatie van bezoekers, inclusief toestemming, sessieduur en pagina-interacties, terwijl de gegevens in de database worden opgeslagen.
if (static::isBot(%useragent->source)) return
$consent = (bool)%payload->c
$n = strlen(%payload->n) === 8 ? %payload->n : date('Ymd')
$id = $consent ? token(20, $n.space.%cookies->token.space.%useragent->source) : token(20, $n.space.date('Ymd').space.%cookies->token.space.%req->ip)
$delta = max(0, min(120, (int)%payload->s))
$prevDelta = max(0, min(120, (int)%payload->ps))
$lang = strlen(%payload->l) === 2 ? %payload->l : %app->lang ?? 'en'
$referrer = ($r = (string)%payload->r) && !str_contains($r, host) ? static::parseReferrer($r) : null
static::DB()->query('INSERT INTO '.static::$table.' (id, token, host, page, lang, IP, browser, os, device, active_seconds, state, width, height, referrer, created, changed) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE token = VALUES(token), host = VALUES(host), page = VALUES(page), lang = VALUES(lang), IP = VALUES(IP), browser = VALUES(browser), os = VALUES(os), device = VALUES(device), active_seconds = active_seconds + VALUES(active_seconds), state = VALUES(state), width = VALUES(width), height = VALUES(height), referrer = IF(referrer IS NULL OR referrer = '.sq.sq.', VALUES(referrer), referrer), changed = VALUES(changed)', $id, token(20, (string)(%cookies->token ?? %payload->n)), host, %payload->u, $lang, $consent ? %req->ip : void, $consent ? %useragent->full.(%payload->a ? ' App' : void) : substr(md5((string)%useragent->source), 0, 8), $consent ? %useragent->osFull : void, $consent ? %useragent->device : void, $delta + $prevDelta, %payload->v, %payload->w, %payload->h, $referrer, time(), time())
$pages = preg_replace('/visitors$/', 'visitor_pages', (string)static::$table)
if (strlen((string)%payload->p) >= 8){
$pv = token(20, $id.space.%payload->p)
static::DB()->query('INSERT INTO '.$pages.' (id, visitor, host, page, lang, active_seconds, beats, created, changed) VALUES (?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE active_seconds = active_seconds + VALUES(active_seconds), beats = beats + 1, changed = VALUES(changed)', $pv, $id, host, %payload->u, $lang, $delta, 1, time(), time())
}
if (strlen((string)%payload->pp) >= 8){
$ppv = token(20, $id.space.%payload->pp)
static::DB()->query('UPDATE '.$pages.' SET active_seconds = active_seconds + ?, beats = beats + 1, changed = ? WHERE id = ?', $prevDelta, time(), $ppv)
}view
script
line 52
Volgt gebruikersactiviteit op de pagina door actieve tijd te meten en heartbeat-gegevens naar de server te verzenden, inclusief gebruikersstatus en zichtbaarheidstoestand.
let curpath = app.path
let pv = phlo.token(12)
let hbTimer, activeMs = 0, activeAt = null, prev = null
const visible = () => document.visibilityState === 'visible'
const accrue = () => {
if (activeAt === null) return
activeMs += performance.now() - activeAt
activeAt = visible() ? performance.now() : null
}
const flush = beacon => {
accrue()
const consent = document.cookie.includes('cookieChoice=all')
window.name ||= phlo.token(8)
const s = Math.floor(activeMs / 1000)
activeMs -= s * 1000
const body = JSON.stringify({n: window.name, v: app.state, l: obj('html').lang ?? 'en', u: app.path, w: innerWidth, h: innerHeight, a: app.mode, p: pv, r: (r = document.referrer) ? (r === `${location.origin}/` ? null : r) : null, c: consent ? 1 : 0, s, pp: prev ? prev.p : null, ps: prev ? prev.s : null})
prev = null
fetch('/heartbeat', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body, keepalive: !!beacon})
}
const heartbeat = () => delay('heartbeat', 333, () => {
clearTimeout(hbTimer)
flush()
if (visible()) hbTimer = setTimeout(heartbeat, 20000)
})
document.addEventListener('visibilitychange', () => {
if (!visible()) return flush(true)
activeAt = performance.now()
heartbeat()
})
addEventListener('resize', heartbeat)
addEventListener('pagehide', () => flush(true))
app.updates.push(() => {
if (curpath === app.path) return
if (prev) flush()
accrue()
prev = {p: pv, s: Math.floor(activeMs / 1000)}
pv = phlo.token(12)
activeMs = 0
curpath = app.path
heartbeat()
})
if (visible()) activeAt = performance.now()
heartbeat()object
%websocket
/phlo/resources/websocket.phlo
static
websocket :: connect ($wsHost, $wsToken, $wsSocket):bool
line 10
Stelt een WebSocket-verbinding tot stand met behulp van de opgegeven host, token en socketparameters.
!function_exists('wsConnect') || wsConnect($wsHost, $wsToken, $wsSocket)static
websocket :: auth ($wsHost, $wsToken, $wsSocket):bool
line 11
Controleert of de 'wsAuth'-functie bestaat en roept deze, indien niet, aan met de opgegeven parameters voor WebSocket-authenticatie.
!function_exists('wsAuth') || wsAuth($wsHost, $wsToken, $wsSocket)static
websocket :: receive ($wsHost, $wsToken, $wsSocket, $data):bool
line 12
Ontvangt gegevens van een WebSocket-verbinding met behulp van de opgegeven host, token en socket, en verwerkt de binnenkomende gegevens als JSON.
function_exists('wsReceive') && wsReceive($wsHost, $wsToken, $wsSocket, ...json_decode($data, true))static
websocket :: close ($wsHost, $wsToken, $wsSocket):bool
line 13
Sluit een WebSocket-verbinding met behulp van de opgegeven host, token en socket.
function_exists('wsClose') && wsClose($wsHost, $wsToken, $wsSocket)object%WhatsApp
/phlo/resources/WhatsApp.phlo
method
%WhatsApp -> __construct (public string $url, public string $secret)
line 11
Initialiseert een WhatsApp-instantie met een opgegeven URL en geheim, waarbij wordt gegarandeerd dat de URL eindigt met een schuine streep.
$this->url = rtrim($url, slash).slashstatic
WhatsApp :: channel ($channel):static
line 13
Maakt een nieuwe instantie van het WhatsApp-kanaal met de opgegeven URL en geheim, standaard 'http://localhost:8081' en 'void' als niet gespecificeerd.
new static($channel->configData->url ?? 'http://localhost:8081', $channel->secretData->secret ?? void)method
%WhatsApp -> number ($contact):string
line 15
Extraheert het telefoonnummer uit een WhatsApp-contactstring en retourneert een fout als het formaat ongeldig is.
($pos = strpos($contact, '@')) ? substr($contact, 0, $pos) : error('Invalid contact: '.esc($contact))method
%WhatsApp -> isGroup ($contact):bool
line 16
Controleert of de opgegeven contactpersoon een WhatsApp-groep is door te verifiëren of de contactpersoon een '@g' achtervoegsel bevat.
last($this->number($contact), (bool)strpos($contact, '@g'))method
%WhatsApp -> status:obj
line 18
Haal de huidige status van WhatsApp op met een GET-verzoek.
$this->request('status', GET: true)method
%WhatsApp -> health:obj
line 19
Controleert de gezondheidsstatus van de WhatsApp-service door een GET-verzoek te verzenden.
$this->request('health', GET: true)method
%WhatsApp -> qr:obj
line 20
Stuurt een verzoek om de QR-code voor WhatsApp-authenticatie op te halen.
$this->request('qr', GET: true)method
%WhatsApp -> disconnect:obj
line 21
Verbreekt de huidige WhatsApp-sessie door een verbreekverzoek te sturen.
$this->request('disconnect')method
%WhatsApp -> read ($chat):obj
line 23
Stuurt een verzoek om berichten te lezen uit een opgegeven WhatsApp-chat.
$this->request('read', chat: $chat)method
%WhatsApp -> reaction ($msg, $emoji):obj
line 24
Stuurt een reactie-emoticon naar een specifiek bericht in WhatsApp.
$this->request('reaction', msg: $msg, emoji: $emoji)method
%WhatsApp -> text ($to, $text):obj
line 26
Verzendt een tekstbericht naar de opgegeven ontvanger via WhatsApp.
$this->request('text', to: $to, text: $text)method
%WhatsApp -> image ($to, file $file, $text = void):obj
line 27
Verzendt een afbeeldingsbericht via WhatsApp naar de opgegeven ontvanger, met de optie om een tekstbericht toe te voegen.
$this->request('image', to: $to, filename: $file->name, image: $file->src, text: $text)method
%WhatsApp -> location ($to, $lat, $lon, $text):obj
line 28
Verzendt een locatiebericht via WhatsApp naar de opgegeven ontvanger met breedtegraad, lengtegraad en optionele tekst.
$this->request('location', to: $to, lat: $lat, lon: $lon, text: $text)method
%WhatsApp -> document ($to, file $file, $text = void):obj
line 29
Verzendt een document via WhatsApp naar de opgegeven ontvanger, inclusief een optioneel tekstbericht.
$this->request('document', to: $to, filename: $file->name, document: $file->src, text: $text)method
%WhatsApp -> audio ($to, file $file):obj
line 31
Verzendt een audiobericht naar een opgegeven ontvanger met het opgegeven audiobestand.
$this->request('audio', to: $to, audio: $file->src)method
%WhatsApp -> voice ($to, file $file):obj
line 32
Verzendt een spraakbericht naar een opgegeven ontvanger met behulp van het opgegeven audiobestand.
$this->request('voice', to: $to, audio: $file->src)method
%WhatsApp -> poll ($to, $name, array $options, bool $multi = false):obj
line 34
Verzendt een peilingbericht naar een opgegeven WhatsApp-ontvanger met de gegeven opties, met de mogelijkheid voor meerdere selecties indien opgegeven.
$this->request('poll', to: $to, name: $name, options: $options, multi: $multi)method
%WhatsApp -> startTyping ($to):obj
line 36
Start de typindicator voor een opgegeven ontvanger in WhatsApp.
$this->request('typing/start', to: $to)method
%WhatsApp -> stopTyping ($to):obj
line 37
Stop de typindicator voor een specifieke ontvanger in WhatsApp.
$this->request('typing/stop', to: $to)method
%WhatsApp -> request ($action, ...$data):obj
line 39
Verzendt een verzoek naar de WhatsApp API met de opgegeven actie en gegevens, en retourneert een responsobject dat succes of falen aangeeft.
$get = $data['GET'] ?? false
unset($data['GET'])
$raw = trim((string)HTTP($this->url.$action, ['secret: '.$this->secret], true, $get ? null : $data))
if (strtolower($raw) === 'ok') return obj(ok: true)
$res = json_decode($raw)
if (!$res && $raw) return obj(ok: false, error: $raw)
return $res ?: obj(ok: false, error: 'Empty WhatsApp response')Functions
function
active(bool $cond, string $classList = void):string
/phlo/resources/active.phlo line 9
Genereert een class-attribuutstring voor HTML-elementen, waarbij 'active' aan de opgegeven classlijst wordt toegevoegd als de voorwaarde waar is.
$cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : voidfunction
age(int $time):int
/phlo/resources/age.phlo line 9
Berechnet de leeftijd door de gegeven tijd van de huidige tijd af te trekken.
time() - $timefunction
age_human(int $age):string
/phlo/resources/age.human.phlo line 10
Berechnet een leesbare tijdsduur op basis van de gegeven leeftijd in seconden.
time_human(time() - $age)function
apcu($key, $cb, int $duration = 3600, bool $log = true)
/phlo/resources/apcu.phlo line 14
apcu_entry holds a lock on the key for the length of the callback.
first($value = apcu_entry($key, $cb, $duration), $log && debug('C: '.(strlen($key) > 58 ? substr($key, 0, 55).'...' : $key).(is_array($value) ? ' ('.count($value).')' : (is_numeric($value) ? ":$value" : (is_string($value) ? ':string:'.strlen($value) : colon.gettype($value))))))function
await(...$jobs):array
/phlo/resources/await.phlo line 11
Wacht op meerdere taken om te voltooien, beheert hun uitvoer en fouten, en retourneert de resultaten. Het beheert kindprocessen en zorgt ervoor dat ze niet eindeloos blokkeren.
if (daemon) return daemon::await($jobs)
$children = []
$open = []
foreach ($jobs AS $i => $job){
[$cb, $args] = is_array($job) ? [$job[0], array_slice($job, 1)] : [$job, []]
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void)
$desc = [0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']]
$proc = proc_open($cmd, $desc, $pipes)
fclose($pipes[0])
stream_set_blocking($pipes[1], false)
stream_set_blocking($pipes[2], false)
$children[$i] = obj(proc: $proc, out: $pipes[1], err: $pipes[2], stdout: void, stderr: void)
$open['o'.$i] = $pipes[1]
$open['e'.$i] = $pipes[2]
}
// Drain every child's stdout AND stderr together: reading one stream to EOF before the
// other deadlocks a child that fills the unread pipe. Bound the whole wait so a hung
// child cannot block the caller forever.
$deadline = time() + (defined('await_timeout') ? await_timeout : 300)
while ($open){
$read = $open
$write = $except = []
if (@stream_select($read, $write, $except, 1) === false) break
foreach ($read AS $key => $stream){
$chunk = fread($stream, 65536)
if ($chunk === void || $chunk === false){
feof($stream) && $open = array_diff_key($open, [$key => 1])
continue
}
$i = substr($key, 1)
if ($key[0] === 'o') $children[$i]->stdout .= $chunk
else $children[$i]->stderr .= $chunk
}
if (time() >= $deadline) break
}
// Terminate any child still running (deadline or a stream_select error). SIGKILL cannot
// be ignored, so proc_close below will not block on a process that drops SIGTERM.
foreach ($children AS $child) (proc_get_status($child->proc)['running'] ?? false) && proc_terminate($child->proc, 9)
$results = []
foreach ($children AS $i => $child){
fclose($child->out)
fclose($child->err)
$code = proc_close($child->proc)
$err = trim($child->stderr)
if ($err !== void){
$ej = json_decode($err, true)
$results[$i] = json_last_error() === JSON_ERROR_NONE ? $ej : $err
continue
}
if ($code !== 0){
$results[$i] = obj(error: 'CLI process failed', code: $code)
continue
}
$json = json_decode($child->stdout, true)
$results[$i] = json_last_error() === JSON_ERROR_NONE ? $json : $child->stdout
}
return $resultsfunction
button(...$args):string
/phlo/resources/tags.form.phlo line 11
Maakt een knop element met de opgegeven argumenten die als props worden doorgegeven.
tag('button', ...$args)function
camel(string $text):string
/phlo/resources/camel.phlo line 9
Converteert een gegeven string naar camel case door de eerste letter van elk woord te kapitaliseren en spaties te verwijderen.
lcfirst(str_replace(space, void, ucwords(lcfirst($text))))function
chunk(...$cmds):void
/phlo/resources/chunk.phlo line 10
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid chunk()')
if (debug){
$res->dump && [$cmds['dump'] = $res->dump, $res->dump = []]
$res->debug && [$cmds['debug'] = $res->debug, $res->debug = []]
}
if (!$res->streaming){
$res->streaming = true
$res->type = 'application/x-ndjson'
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->render()
}
print(json_encode($cmds, jsonFlat).lf)
$cli || [@ob_flush(), flush()]function
create(iterable $items, \Closure $keyCb, ?\Closure $valueCb = null):array
/phlo/resources/create.phlo line 9
Maakt een associatieve array door de waarden uit de iterable als sleutels te gebruiken en de optionele waarde callback te gebruiken om de bijbehorende waarden te bepalen.
array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items)function
en($text, ...$args):string
/phlo/resources/lang.phlo line 12
Deze functie haalt een vertaling op voor de opgegeven tekst in het Engels, en kan optioneel worden opgemaakt met extra argumenten.
%lang->translation('en', $text, ...$args)function
exec_stream(string $cmd, ?int $timeoutSec = 0):Generator
/phlo/resources/exec.stream.phlo line 13
Reads stdout and stderr in one select loop rather than one after the other.
$desc = [0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']]
$proc = proc_open($cmd, $desc, $pipes)
if (!is_resource($proc)) return
stream_set_blocking($pipes[1], false)
stream_set_blocking($pipes[2], false)
$bufOut = void
$bufErr = void
while (true){
$status = proc_get_status($proc)
$running = $status['running']
$read = []
$w = null
$e = null
if (!feof($pipes[1])) $read[] = $pipes[1]
if (!feof($pipes[2])) $read[] = $pipes[2]
if ($read) @stream_select($read, $w, $e, 0, 200000)
foreach ($read AS $r){
$chunk = fread($r, 8192)
if ($chunk === void || $chunk === false) continue
if ($r === $pipes[1]){
$bufOut .= $chunk
while (($pos = strpos($bufOut, lf)) !== false){
$line = substr($bufOut, 0, $pos)
$bufOut = substr($bufOut, $pos + 1)
yield obj(data: $line)
}
}
else {
$bufErr .= $chunk
while (($pos = strpos($bufErr, lf)) !== false){
$line = substr($bufErr, 0, $pos)
$bufErr = substr($bufErr, $pos + 1)
yield obj(data: $line, error: true)
}
}
}
if (!$running) break
if ($timeoutSec > 0 && ($status['running_time'] ?? 0) > $timeoutSec){
proc_terminate($proc)
yield obj(data: 'process timeout', error: true)
break
}
}
if ($bufOut !== void) yield obj(data: $bufOut)
if ($bufErr !== void) yield obj(data: $bufErr, error: true)
foreach ($pipes AS $p) @fclose($p)
proc_close($proc)function
HTTP(string $url, array $headers = [], bool $JSON = false, $POST = null, $PUT = null, $PATCH = null, $QUERY = null, bool $DELETE = false, string|bool|null $agent = null, string|bool $cookies = false, int $timeout = 15, &$response = null)
/phlo/resources/HTTP.phlo line 15
Follows redirects on its own and raises only when the transport itself fails.
$curl = curl_init($url)
if ($POST !== null || $PUT !== null || $PATCH !== null || $QUERY !== null){
if (!is_null($POST)) [$method = 'POST', $content = $POST]
elseif (!is_null($PUT)) [$method = 'PUT', $content = $PUT]
elseif (!is_null($PATCH)) [$method = 'PATCH', $content = $PATCH]
elseif (!is_null($QUERY)) [$method = 'QUERY', $content = $QUERY]
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method)
if ($JSON) [!is_string($content) && $content = json_encode($content), array_push($headers, 'Content-Type: application/json', 'Content-Length: '.strlen($content))]
curl_setopt($curl, CURLOPT_POSTFIELDS, $content)
}
elseif ($DELETE) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE')
$agent && curl_setopt($curl, CURLOPT_USERAGENT, $agent === true ? phlo('req')->userAgent : $agent)
if ($cookies !== false) [$jar = $cookies === true ? data.'cookies.txt' : $cookies, curl_setopt($curl, CURLOPT_COOKIEFILE, $jar), curl_setopt($curl, CURLOPT_COOKIEJAR, $jar)]
$resHeaders = []
curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => $timeout, CURLOPT_ENCODING => void, CURLOPT_HEADERFUNCTION => function($ch, $line) use (&$resHeaders){
$parts = explode(colon, $line, 2)
count($parts) === 2 && $resHeaders[strtolower(trim($parts[0]))] = trim($parts[1])
return strlen($line)
}])
$res = curl_exec($curl)
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE)
$response = obj(ok: $res !== false && $status >= 200 && $status < 300, status: $status, headers: $resHeaders, error: $res === false ? curl_error($curl) : null)
if ($res === false) error('HTTP error: '.curl_error($curl))
return $resfunction
input(...$args):string
/phlo/resources/tags.form.phlo line 12
Maakt een invoerelement in de view met de opgegeven argumenten.
tag('input', ...$args)function
n8n($webhook, ?array $data = null, $test = false)
/phlo/resources/n8n.phlo line 10
Stuurt een HTTP POST-verzoek naar een n8n webhook met optionele gegevens en een testvlag.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)function
nl($text, ...$args):string
/phlo/resources/lang.phlo line 11
Vertaal de gegeven tekst naar het Nederlands met de opgegeven argumenten voor opmaak.
%lang->translation('nl', $text, ...$args)function
notify(string $title, string $body = void, string $type = 'info', string $level = 'info', ?string $user = null):void
/phlo/resources/notify.phlo line 9
Verzendt een melding met een opgegeven titel, inhoud, type, niveau en optionele gebruiker naar een geconfigureerde URL via HTTP.
$cfg = %creds->notify ?? null
if (!$cfg) return
$url = $cfg->url ?? void
$secret = $cfg->secret ?? void
if ($url === void || $secret === void) return
try {
HTTP($url, ['secret: '.$secret], true, [
'app' => (string)($cfg->app ?? (defined('id') ? id : 'app')),
'server' => $cfg->server ?? 'local',
'host' => %req->host ?? void,
'type' => $type,
'level' => $level,
'title' => $title,
'body' => $body,
'user' => $user,
])
}
catch (\Throwable $e){}function
phlo(?string $phloName = null, ...$args):mixed
/phlo/phlo.php line 241
Maakt of haalt een instantie van een Phlo-object op op basis van de opgegeven naam en argumenten, en beheert een statische lijst van objecten voor hergebruik.
static $list = [];
if ($phloName === 'tech/reset'){
obj::$classProps = [];
return array_keys($list = array_filter($list, static fn($obj) => $obj->objPers));
}
if ($phloName === null) return array_keys($list);
$class = strtr($phloName, [slash => us]);
$handle = method_exists($class, '__handle') ? $class::__handle(...$args) : ($args ? null : $phloName);
if ($handle === true){
if (isset($list[$phloName])) return $list[$phloName]->objImport(...$args);
$handle = $phloName;
}
elseif ($handle && isset($list[$handle])) return $list[$handle];
$object = new $class(...$args);
if ($handle) $list[$handle] = $object;
if ($object->hasMethod('controller') && (!phlo('req')->cli || $phloName !== 'app')) $object->controller();
return $object;function
phlo_app(...$args):void
/phlo/phlo.php line 39
Initialiseert een Phlo-applicatie door de omgeving in te stellen, noodzakelijke klassen en functies te laden, en verschillende runtime-parameters te configureren op basis van de opgegeven argumenten.
if ($args['trace'] ??= false) require_once __DIR__.'/classes/trace.php';
require_once __DIR__.'/functions'.($args['trace'] ? '.trace.php' : '.php');
require_once __DIR__.'/classes/obj.php';
require_once __DIR__.'/classes/req.php';
require_once __DIR__.'/classes/res.php';
$args['app'] ?? error('No "app" path defined');
$args['debug'] ??= false;
$args['build'] ??= false;
$args['host'] ??= null;
$args['control'] ??= ($args['build'] && $args['debug']) ? 'phlo' : false;
$args['auth'] ??= false;
$args['data'] ??= $args['app'].'data/';
$args['php'] ??= $args['app'].'php/';
$args['www'] ??= $args['app'].'www/';
$args['cli'] ??= ZEND_THREAD_SAFE ? 'php-zts' : 'php';
$args['thread'] ??= false;
$args['daemon'] ??= false;
$args['build'] && $args['thread'] && error('Phlo build and thread mode cannot be combined');
$args['build'] && !is_file($args['data'].'app.json') && error('Phlo build mode requires data/app.json');
$args['auth'] && !$args['build'] && error('Auth requires build mode');
foreach ($args as $key => $value) define($key, $value);
define('engine', __DIR__.slash);
if ($args['debug']) require_once __DIR__.'/debug.php';
if ($args['build']) require_once __DIR__.'/classes/changed.php';
if ($args['daemon']) require_once __DIR__.'/classes/daemon.php';
if ($args['trace']) trace::boot($args['app']);
set_error_handler(static function(int $level, string $msg, string $file = '', int $line = 0):bool {
if (!(error_reporting() & $level)) return false;
throw new ErrorException($msg, 0, $level, $file, $line);
});
set_exception_handler('phlo_exception');
spl_autoload_register(static function(string $class):void {
static $map = null, $mtime = null;
$file = php.'classmap.php';
if ($map === null || $mtime !== (is_file($file) ? filemtime($file) : null)){
$map = is_file($file) ? require $file : [];
$mtime = is_file($file) ? filemtime($file) : null;
}
if (isset($map[$class])){ require_once php.$map[$class]; return; }
});
if ($args['build']){
$engineMap = ['build' => 'build', 'reflect' => 'reflect', 'build_file' => 'file', 'build_node' => 'node', 'build_builder' => 'builder', 'build_css' => 'css', 'build_icons' => 'icons'];
spl_autoload_register(static function(string $class) use ($engineMap):void {
$name = $engineMap[strtolower($class)] ?? null;
if ($name !== null) require_once engine.'classes/'.$name.'.php';
});
}
defined('composer') && spl_autoload_register(static function(string $class):void {
static $loaded = false;
if ($loaded) return;
$loaded = true;
require_once composer.'vendor/autoload.php';
foreach (spl_autoload_functions() as $fn){
if (is_array($fn) && ($fn[0] ?? null) instanceof \Composer\Autoload\ClassLoader){
spl_autoload_unregister($fn);
spl_autoload_register($fn);
$fn[0]->loadClass($class);
return;
}
}
});
if ($args['thread'] !== false && PHP_SAPI !== 'cli'){
ignore_user_abort(true);
$handle = static function():void { phlo_thread(); };
for ($i = 1; !$args['thread'] || $i <= $args['thread']; ++$i){
$keepRunning = frankenphp_handle_request($handle);
phlo('tech/reset');
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
gc_collect_cycles();
if (!$keepRunning) break;
}
return;
}
phlo_thread();function
phlo_async(string $cb, ...$args):bool
/phlo/resources/phlo.async.phlo line 10
Voert een callbackfunctie asynchroon uit, waarbij eventuele extra argumenten worden doorgegeven, en retourneert de proces-ID als het succesvol is.
if (daemon) return daemon::fire($cb, $args)
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void).' > /dev/null 2>&1 & echo $!'
exec($cmd, $r)
return isset($r[0]) && ctype_digit($r[0]) && (int)$r[0] > 0function
phlo_cli(array $args):void
/phlo/phlo.php line 191
if (!$args) return;
$target = array_shift($args);
$result = phlo_dispatch($target, $args);
if (isset($result)) print(json_encode($result, jsonFlat).lf);function
phlo_dispatch(string $target, array $args = []):mixed
/phlo/phlo.php line 178
Verzendt een methode-aanroep naar een object of een statische methode van een klasse op basis van de opgegeven doelsstring, met optioneel doorgegeven argumenten.
if (str_contains($target, dot)){
[$object, $method] = explode(dot, $target, 2);
$handle = phlo($object);
return $args ? $handle->$method(...$args) : ($handle->hasMethod($method) ? $handle->$method() : $handle->$method);
}
if (str_contains($target, '::')){
[$class, $method] = explode('::', $target, 2);
return $class::$method(...$args);
}
return $target(...$args);function
phlo_exception(Throwable $e):void
/phlo/phlo.php line 34
Behandelt uitzonderingen door het Throwable-object door te geven aan de phlo_error_handle functie voor verwerking.
require_once engine.'error.php';
phlo_error_handle($e);function
phlo_exists(string $obj):bool
/phlo/resources/phlo.exists.phlo line 9
Controleert of een specifiek PHP-bestand bestaat op het opgegeven objectpad.
is_file(php.strtr($obj, [us => dot]).'.php')function
phlo_load(bool $http):void
/phlo/phlo.php line 154
Laadt de noodzakelijke runtime-bestanden voor de applicatie, waarbij ervoor wordt gezorgd dat de applicatie slechts eenmaal wordt geladen en dat het juiste contenttype wordt ingesteld voor HTTP-responses.
static $loaded = false, $loadedApp = null;
if ($loaded && $loadedApp === app){
if ($http && !phlo('res')->type) phlo('res')->type = 'text/html; charset=UTF-8';
return;
}
if (build && (!is_file(php.'functions.php') || !is_file(php.'app.php') || build_base::changed())){
debug('Builder started');
$changed = build::run();
$changed && debug('Built '.implode(', ', array_map('basename', $changed)).' ('.count($changed).')');
}
if (!is_file(php.'functions.php') || !is_file(php.'app.php')) error('Compiled runtime not available');
if (!$loaded){
require_once php.'functions.php';
$loaded = true;
}
if ($loadedApp !== app){
require_once php.'app.php';
$loadedApp = app;
}
if ($http && !phlo('res')->type) phlo('res')->type = 'text/html; charset=UTF-8';function
phlo_serve():void
/phlo/phlo.php line 198
ini_set('display_errors', 'stderr');
stream_set_blocking(STDIN, true);
fwrite(STDOUT, json_encode(['t' => 'ready']).lf);
while (($line = fgets(STDIN)) !== false){
$line = trim($line);
if ($line === void) continue;
$msg = json_decode($line, true) ?: [];
$id = $msg['id'] ?? null;
$target = (string)($msg['target'] ?? void);
$args = (array)($msg['args'] ?? []);
$stream = (bool)($msg['stream'] ?? false);
$lineBuf = void;
$emit = static function(string $chunk) use (&$lineBuf, $id):string {
$lineBuf .= $chunk;
while (($pos = strpos($lineBuf, lf)) !== false){
$out = substr($lineBuf, 0, $pos);
$lineBuf = substr($lineBuf, $pos + 1);
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'line', 'data' => $out], jsonFlat).lf);
}
return void;
};
try {
if ($target === void) error('No target');
if ($stream){
ob_start($emit, 1);
$result = phlo_dispatch($target, $args);
while (ob_get_level()) ob_end_flush();
if ($lineBuf !== void) fwrite(STDOUT, json_encode(['id' => $id, 't' => 'line', 'data' => $lineBuf], jsonFlat).lf);
}
else $result = phlo_dispatch($target, $args);
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'done', 'result' => $result], jsonFlat).lf);
}
catch (Throwable $e){
while (ob_get_level()) ob_end_clean();
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'error', 'message' => $e->getMessage()], jsonFlat).lf);
}
phlo('tech/reset');
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
gc_collect_cycles();
}function
phlo_stream(string $cb, ...$args):Generator
/phlo/resources/phlo.stream.phlo line 11
Stroomt uitvoer van een callbackfunctie, waardoor asynchrone verwerking van gegevens mogelijk is. Het kan in een daemon-modus werken of een opdracht in de CLI-context uitvoeren.
if (daemon){
yield from daemon::stream($cb, $args)
return
}
yield from exec_stream(cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void))function
phlo_sync(string $cb, ...$args)
/phlo/resources/phlo.sync.phlo line 10
Voert een callbackfunctie synchronisch uit, waarbij eventuele extra argumenten worden doorgegeven, en retourneert het resultaat als een JSON-object of een foutmelding als de uitvoering mislukt.
if (daemon) return daemon::run($cb, $args)
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void)
exec($cmd.' 2>&1', $r, $code)
$out = implode(lf, $r)
if ($code !== 0) error('Could not execute "'.esc($cb).'" via CLI')
$j = json_decode($out, true)
if (json_last_error() !== JSON_ERROR_NONE) return $out
if (is_array($j) && isset($j['error'])) error($j['error'])
return $jfunction
phlo_thread():void
/phlo/phlo.php line 116
try {
$req = phlo('req');
if ($req->cli){
$target = $req->args[0] ?? void;
if (str_starts_with($target, 'build::') || str_starts_with($target, 'reflect::')){
phlo_cli($req->args);
return;
}
phlo_load(false);
phlo('app');
phlo_cli($req->args);
return;
}
$isControl = build && debug && control && str_starts_with($req->path.slash, control.slash);
if (auth && !$isControl){
phlo_auth('site', 'Phlo App - '.host);
if (phlo('res')->done) return;
}
if ($isControl){
require_once engine.'control.php';
phlo_control::handle(substr($req->path, strlen(control) + 1));
phlo('res')->render();
return;
}
phlo_load(true);
phlo('app');
phlo('res')->render();
}
catch (RuntimeException $e){
if ($e->getMessage() === 'PhloDump') return;
phlo_exception($e);
}
catch (Throwable $e){
phlo_exception($e);
}function
select(...$args):string
/phlo/resources/tags.form.phlo line 13
Maakt een 'select' HTML-element met de opgegeven argumenten als attributen en opties.
tag('select', ...$args)function
setting(?string $key = null, $value = null):mixed
/phlo/resources/settings.phlo line 11
$store = %JSON('settings', assoc: true)
if ($key === null) return $store->objData
if (func_num_args() > 1){
$store->$key = $value
$store->objChanged && $store->objWrite($store->objData)
}
return $store->$keyfunction
slug(string $text):string
/phlo/resources/slug.phlo line 9
Converteert een gegeven string naar een URL-vriendelijke slug door niet-alfanumerieke tekens te verwijderen, naar kleine letters om te zetten en spaties door streepjes te vervangen.
trim(preg_replace('/[^a-z0-9]+/', dash, strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text))), dash)function
stream($data = null, string $type = 'application/octet-stream', ?string $name = null):void
/phlo/resources/stream.phlo line 11
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid stream()')
if (!$res->streaming){
$res->streaming = true
$res->type = $type
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->header('X-Accel-Buffering', 'no')
$name === null || $res->header('Content-Disposition', 'attachment; filename="'.str_replace('"', '', $name).'"')
$res->render()
}
if ($data === null) return
foreach (is_iterable($data) ? $data : [$data] as $part){
print((string)$part)
$cli || [@ob_flush(), flush()]
}function
tag(string $tagName, ?string $inner = null, ...$args):string
/phlo/resources/tag.phlo line 10
Genereert een HTML-tag met de opgegeven naam, optionele inhoud en extra attributen.
"<$tagName".loop(array_filter($args, fn($value) => !is_null($value)), fn($value, $key) => space.strtr($key, [us => dash]).($value === true ? void : '="'.esc($value).'"'), void).'>'.(is_null($inner) ? void : "$inner</$tagName>")function
textarea(...$args):string
/phlo/resources/tags.form.phlo line 14
Maakt een 'textarea' HTML-element met de opgegeven argumenten.
tag('textarea', ...$args)function
time_human(?int $time = null):string
/phlo/resources/time.human.phlo line 9
Converteert een gegeven tijdstempel naar een leesbaar tijdsverschil formaat, zoals '2 dagen' of '3 uur'. Als er geen tijdstempel wordt opgegeven, wordt de huidige tijd gebruikt.
static $labels
$labels ??= last($labels = arr(seconds: 60, minutes: 60, hours: 24, days: 7, weeks: 4, months: 13, years: 1), defined('tsLabels') && $labels = array_combine(tsLabels, $labels), $labels)
$age = time() - $time
foreach ($labels AS $range => $multiplier){
if ($age / $multiplier < 1.6583) break
$age /= $multiplier
}
return round($age)." $range"function
wsCast($wsTarget = 'all', $wsHost = host, $wsPort = daemon, $wsExcept = void, ...$data)
/phlo/resources/wsCast.phlo line 10
HTTP (
'http://127.0.0.1:'.$wsPort.'/message',
JSON: true,
POST: arr (
host: $wsHost,
target: $wsTarget,
except: $wsExcept,
data: $data,
),
)Laatst bijgewerkt op 23-08-2026