Core
object
%cookies
/phlo/resources/cookies.phlo
method
%cookies -> controller
line 10
This controller retrieves the current state of cookies and assigns it to the objData property.
this->objData = $_COOKIEprop
%cookies -> lifetimeDays:int
line 12
Sets the lifetime of cookies in days.
180method
%cookies -> objSet ($key, $value, array $options = []):bool
line 14
Sets a cookie with the specified key and value, along with optional parameters for expiration, path, security, and SameSite attributes.
$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
Removes a cookie by unsetting it from the local object data and the global $_COOKIE array, and sets its expiration date to the past.
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
Translates the given text into Dutch using the specified arguments for formatting.
%lang->translation('nl', $text, ...$args)function
function en ($text, ...$args):string
line 12
This function retrieves a translation for the specified text in English, optionally formatting it with additional arguments.
%lang->translation('en', $text, ...$args)static
lang :: asyncBatch ($from, $to, $json):void
line 14
Executes a batch translation asynchronously, decoding JSON input and saving the translations if successful.
%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
This function retrieves the current language setting for the application, allowing for localization of views.
%app->langprop
%lang -> model:string
line 23
This function retrieves the model associated with the specified language identifier.
'gpt-4o-mini'prop
%lang -> instructions
line 24
Defines a set of instructions for the Phlo programming language.
voidstatic
lang :: fileCache:array
line 25
lang::$fileCache is a static property that stores cached language files for efficient retrieval during runtime.
[]method
%lang -> file ($lang):string
line 27
Retrieves the configuration file for the specified language, using the format 'langs.$lang.ini'.
langs.$lang.'.ini'method
%lang -> escape ($value):string
line 29
Escapes special characters in a string for safe output in HTML, replacing backslashes, double quotes, and line feeds with their respective escape sequences.
strtr((string)$value, [bs => bs.bs, dq => bs.dq, lf => '\n'])method
%lang -> unescape ($value):string
line 30
This function unescapes a given string by replacing escape sequences with their corresponding characters.
strtr(strtr($value, [bs.bs => "\x01", bs.dq => dq, '\n' => lf]), ["\x01" => bs])method
%lang -> lineValue ($line, $eq):string
line 32
Extracts and processes a line value from a given string, removing surrounding quotes if present and unescaping any special characters.
$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
Reads all key-value pairs from a specified file and returns them as an associative array. If the file does not exist or is not a valid file, it returns an empty 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
Looks up a value in the language file cache based on the provided hash, updating the cache if the file has changed.
$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
Retrieves the context from the app author regarding the purpose and domain, if available; otherwise, it returns void.
($instr = trim($this->instructions ?? void)) !== void ? lf.'Context from the app author about purpose and domain: '.$instr : voidprop
%lang -> browser:?string
line 131
Extracts the preferred language from the 'Accept-Language' HTTP header, returning the first matching language code from the application's supported languages.
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
Retrieves the language preference from cookies and checks if it is a valid option in the application's available languages, returning the language if valid or null otherwise.
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : nullmethod
%lang -> detect ($text, $fallback = 'en'):string
line 133
Detects the language of the given text and returns the ISO 639-1 code. If detection fails, it returns a specified fallback language code, defaulting to '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
Generates a hash based on the provided text and a prefix from the specified language.
$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
Translates the given text from a specified language to the application's current language, handling missing translations asynchronously.
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
Translates a given text from one ISO 639-1 language to another using AI, while preserving markdown formatting and capitalization.
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
Translates a batch of text from one language to another using AI, returning the translations in a numbered format.
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
This route generates a sitemap for SEO purposes when accessed via a GET request.
output($this)route
route GET robots.txt
line 13
Defines a route that responds to GET requests by outputting the content of the robots file in plain text format with UTF-8 charset.
output($this->robots(), type: 'text/plain; charset=utf-8')method
%seo -> robots:string
line 15
Generates a robots.txt file content based on the application's settings, specifying which user agents are allowed or disallowed from accessing certain paths, and includes a link to the sitemap.
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
Retrieves the internationalized version of a URI from the application's slugs, returning the original URI if no translation is found.
(%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
This function retrieves the locale code corresponding to the application's language setting, defaulting to 'en' if not set.
$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
This retrieves the Open Graph title for SEO purposes using the title() function.
title()prop
%seo -> ogDescr
line 56
Retrieves the description of the application, returning void if not set.
%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
This retrieves the canonical URL for SEO purposes from the request object.
%req->urlprop
%seo -> ogType:string
line 60
Sets the Open Graph type for SEO purposes, specifically to '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
This expression assigns the value of the app's title or the id to the variable $siteName for SEO purposes.
%app->title ?? idprop
%seo -> twitterCard:bool
line 64
The seo->$twitterCard property is used to configure the Twitter Card metadata for a view, enabling better integration with Twitter sharing features.
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
The seo->$noIndex expression evaluates to true if both %app->noLink and %app->noIndex are not set, otherwise it returns 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
Generates an alternate link element for SEO purposes, specifying the language and URL for the current view.
<xhtml:link rel=alternate hreflang="$lang" href="%req->base$uri"{{ slash }}>view
%seo -> link ($lang, $uri)
line 109
Generates an alternate link tag for SEO purposes, specifying the language and URL of the resource.
<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
Sets a session variable with the specified key to the given value.
$_SESSION[$key] = $this->objData[$key] = $valuemethod
%session -> __unset ($key)
line 16
Removes the specified key from the session data and the internal object data.
unset($this->objData[$key], $_SESSION[$key])method
%session -> __isset ($key):bool
line 17
Checks if a session variable identified by the given key is set and not null.
isset($this->objData[$key])method
%session -> objRegenerateId ($deleteOld = true):void
line 19
Regenerates the session ID for the current session, optionally deleting the old session data based on the $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
Accesses the directory path for tasks, specifically pointing to 'tasks/'.
data.'tasks/'static
tasks :: run:void
line 12
Executes scheduled tasks by checking their due status and locking them to prevent concurrent execution. It saves the run details and marks the task as completed after execution.
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
Saves the run data to a JSON file in the specified directory, using the provided name, do, schedule, and return values.
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
Executes a task defined by a Closure, a 'Class::method' string, or a resource-name string, returning the result of the execution.
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
Retrieves the last run timestamp of a task from a file, returning 0 if the file does not exist.
$file = static::dir().$name.'.last'
return is_file($file) ? (int)file_get_contents($file) : 0static
tasks :: markRun ($name, $ts):int|false
line 66
Writes the timestamp of the last run of a task to a file named after the task in the specified directory, using exclusive locking to prevent concurrent writes.
file_put_contents(static::dir().$name.'.last', (string)$ts, LOCK_EX)static
tasks :: lock ($name):bool
line 68
Creates a lock file for a task if it does not already exist or is older than one hour.
$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
Removes the lock file associated with a task, allowing it to be executed again.
@unlink(static::dir().$name.'.lock')object
%useragent
/phlo/resources/useragent.phlo
prop
%useragent -> source:?string
line 10
This expression retrieves the user agent string from the request object, returning null if it is not set.
%req->userAgent ?: nullprop
%useragent -> os:string
line 12
Determines the operating system from the user agent string by matching it against predefined patterns.
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
Extracts the operating system version from the user agent string if available, returning it in a cleaned format.
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
Returns the full operating system name along with its version if available; otherwise, it returns 'Unknown' or just the OS name.
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
Determines the name of the web browser based on the user agent string provided in the source. It checks for various patterns to identify popular browsers like Chrome, Firefox, and Safari, returning 'Unknown' if no match is found.
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
Extracts the version number from the user agent string if it matches specific browser patterns, returning the cleaned version or void if no match is found.
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
Returns the full user agent string, including the name and version of the user agent, formatted to exclude unnecessary parts.
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
Determines the type of device (Tablet, Phone, or Desktop) based on the user agent string stored in the source property.
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
The 'visitors::$table' refers to the database table associated with the 'visitors' resource in Phlo.
'visitors'static
visitors :: columns
line 13
Defines the columns for the visitors resource, specifying the attributes to be used in data operations.
'id,token,host,page,lang,IP,browser,os,device,active_seconds,state,width,height,referrer,created,changed'static
visitors :: history:array
line 15
Retrieves a history of visitor counts, grouping by date and counting distinct tokens and total visits.
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
This retrieves the count of distinct online visitors who have changed within the last 9 seconds.
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 9)')static
visitors :: lastHour:int
line 17
Retrieves the count of distinct visitors (tokens) who have changed within the last hour.
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 3600)')static
visitors :: isBot (?string $ua):bool
line 19
Determines if the user agent string indicates that the visitor is a bot by matching it against a predefined regex pattern.
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
Parses the referrer URL to identify the search engine used, returning a formatted string indicating the search engine or the host if no match is found.
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
Handles the PUT request for updating heartbeat information of visitors, including consent, session duration, and page interactions, while storing the data in the database.
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
Tracks user activity on the page by measuring active time and sending heartbeat data to the server, including user state and visibility status.
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
Establishes a WebSocket connection using the specified host, token, and socket parameters.
!function_exists('wsConnect') || wsConnect($wsHost, $wsToken, $wsSocket)static
websocket :: auth ($wsHost, $wsToken, $wsSocket):bool
line 11
Checks if the 'wsAuth' function exists and, if not, calls it with the provided parameters for WebSocket authentication.
!function_exists('wsAuth') || wsAuth($wsHost, $wsToken, $wsSocket)static
websocket :: receive ($wsHost, $wsToken, $wsSocket, $data):bool
line 12
Receives data from a WebSocket connection using the specified host, token, and socket, processing the incoming data as JSON.
function_exists('wsReceive') && wsReceive($wsHost, $wsToken, $wsSocket, ...json_decode($data, true))static
websocket :: close ($wsHost, $wsToken, $wsSocket):bool
line 13
Closes a WebSocket connection using the specified host, token, and 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
Initializes a WhatsApp instance with a specified URL and secret, ensuring the URL ends with a slash.
$this->url = rtrim($url, slash).slashstatic
WhatsApp :: channel ($channel):static
line 13
Creates a new instance of the WhatsApp channel using the provided URL and secret, defaulting to 'http://localhost:8081' and 'void' if not specified.
new static($channel->configData->url ?? 'http://localhost:8081', $channel->secretData->secret ?? void)method
%WhatsApp -> number ($contact):string
line 15
Extracts the phone number from a WhatsApp contact string, returning an error if the format is invalid.
($pos = strpos($contact, '@')) ? substr($contact, 0, $pos) : error('Invalid contact: '.esc($contact))method
%WhatsApp -> isGroup ($contact):bool
line 16
Checks if the specified contact is a WhatsApp group by verifying if the contact contains a '@g' suffix.
last($this->number($contact), (bool)strpos($contact, '@g'))method
%WhatsApp -> status:obj
line 18
Retrieves the current status from WhatsApp using a GET request.
$this->request('status', GET: true)method
%WhatsApp -> health:obj
line 19
Checks the health status of the WhatsApp service by sending a GET request.
$this->request('health', GET: true)method
%WhatsApp -> qr:obj
line 20
Sends a request to retrieve the QR code for WhatsApp authentication.
$this->request('qr', GET: true)method
%WhatsApp -> disconnect:obj
line 21
Disconnects the current WhatsApp session by sending a disconnect request.
$this->request('disconnect')method
%WhatsApp -> read ($chat):obj
line 23
Sends a request to read messages from a specified WhatsApp chat.
$this->request('read', chat: $chat)method
%WhatsApp -> reaction ($msg, $emoji):obj
line 24
Sends a reaction emoji to a specified message in WhatsApp.
$this->request('reaction', msg: $msg, emoji: $emoji)method
%WhatsApp -> text ($to, $text):obj
line 26
Sends a text message to the specified recipient using WhatsApp.
$this->request('text', to: $to, text: $text)method
%WhatsApp -> image ($to, file $file, $text = void):obj
line 27
Sends an image message via WhatsApp to the specified recipient, optionally including a text message.
$this->request('image', to: $to, filename: $file->name, image: $file->src, text: $text)method
%WhatsApp -> location ($to, $lat, $lon, $text):obj
line 28
Sends a location message via WhatsApp to the specified recipient with latitude, longitude, and optional text.
$this->request('location', to: $to, lat: $lat, lon: $lon, text: $text)method
%WhatsApp -> document ($to, file $file, $text = void):obj
line 29
Sends a document via WhatsApp to the specified recipient, including an optional text message.
$this->request('document', to: $to, filename: $file->name, document: $file->src, text: $text)method
%WhatsApp -> audio ($to, file $file):obj
line 31
Sends an audio message to a specified recipient using the provided audio file.
$this->request('audio', to: $to, audio: $file->src)method
%WhatsApp -> voice ($to, file $file):obj
line 32
Sends a voice message to a specified recipient using the provided audio file.
$this->request('voice', to: $to, audio: $file->src)method
%WhatsApp -> poll ($to, $name, array $options, bool $multi = false):obj
line 34
Sends a poll message to a specified WhatsApp recipient with given options, allowing for multiple selections if specified.
$this->request('poll', to: $to, name: $name, options: $options, multi: $multi)method
%WhatsApp -> startTyping ($to):obj
line 36
Starts the typing indicator for a specified recipient in WhatsApp.
$this->request('typing/start', to: $to)method
%WhatsApp -> stopTyping ($to):obj
line 37
Stops the typing indicator for a specific recipient in WhatsApp.
$this->request('typing/stop', to: $to)method
%WhatsApp -> request ($action, ...$data):obj
line 39
Sends a request to the WhatsApp API with the specified action and data, returning a response object indicating success or failure.
$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
Generates a class attribute string for HTML elements, adding 'active' to the specified class list if the condition is true.
$cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : voidfunction
age(int $time):int
/phlo/resources/age.phlo line 9
Calculates the age by subtracting the given time from the current time.
time() - $timefunction
age_human(int $age):string
/phlo/resources/age.human.phlo line 10
Calculates a human-readable time duration from the given age in seconds.
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
Waits for multiple jobs to complete, handling their output and errors, and returns the results. It manages child processes and ensures that they do not block indefinitely.
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
Creates a button element with the specified arguments passed as props.
tag('button', ...$args)function
camel(string $text):string
/phlo/resources/camel.phlo line 9
Converts a given string to camel case by capitalizing the first letter of each word and removing spaces.
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
Creates an associative array by using the values from the iterable as keys and the optional value callback to determine the corresponding values.
array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items)function
en($text, ...$args):string
/phlo/resources/lang.phlo line 12
This function retrieves a translation for the specified text in English, optionally formatting it with additional arguments.
%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
Creates an input element in the view with the specified arguments.
tag('input', ...$args)function
n8n($webhook, ?array $data = null, $test = false)
/phlo/resources/n8n.phlo line 10
Sends an HTTP POST request to an n8n webhook with optional data and a test flag.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)function
nl($text, ...$args):string
/phlo/resources/lang.phlo line 11
Translates the given text into Dutch using the specified arguments for formatting.
%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
Sends a notification with a specified title, body, type, level, and optional user to a configured URL using 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
Creates or retrieves an instance of a Phlo object based on the provided name and arguments, managing a static list of objects for reuse.
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
Initializes a Phlo application by setting up the environment, loading necessary classes and functions, and configuring various runtime parameters based on the provided arguments.
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
Executes a callback function asynchronously, passing any additional arguments, and returns the process ID if successful.
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
Dispatches a method call to an object or a static method of a class based on the provided target string, optionally passing arguments.
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
Handles exceptions by passing the Throwable object to the phlo_error_handle function for processing.
require_once engine.'error.php';
phlo_error_handle($e);function
phlo_exists(string $obj):bool
/phlo/resources/phlo.exists.phlo line 9
Checks if a specified PHP file exists in the given object path.
is_file(php.strtr($obj, [us => dot]).'.php')function
phlo_load(bool $http):void
/phlo/phlo.php line 154
Loads the necessary runtime files for the application, ensuring that the application is only loaded once and that the correct content type is set for 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
Streams output from a callback function, allowing for asynchronous processing of data. It can operate in a daemon mode or execute a command in the CLI context.
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
Executes a callback function in a synchronous manner, passing any additional arguments to it, and returns the result as a JSON object or an error message if execution fails.
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
Creates a 'select' HTML element with the provided arguments as attributes and options.
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
Converts a given string into a URL-friendly slug by removing non-alphanumeric characters, converting to lowercase, and replacing spaces with dashes.
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
Generates an HTML tag with the specified name, optional inner content, and additional attributes.
"<$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
Creates a 'textarea' HTML element with the specified arguments.
tag('textarea', ...$args)function
time_human(?int $time = null):string
/phlo/resources/time.human.phlo line 9
Converts a given timestamp into a human-readable time difference format, such as '2 days' or '3 hours'. If no timestamp is provided, it uses the current time.
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,
),
)Last updated on 23 August 2026