Core

object

%cookies

/phlo/resources/cookies.phlo
version 1.0
creator q-ai.nl
summary Cookies data object
package web
frontend false
backend true
tags cookies session browser web
method

%cookies -> controller

line 9
This controller retrieves the current state of cookies and assigns it to the objData property.
this->objData = $_COOKIE
prop

%cookies -> lifetimeDays

line 11
Sets the lifetime of cookies in days.
180
method

%cookies -> objSet ($key, $value, array $options = [])

line 13
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 true
method

%cookies -> __unset ($key)

line 21
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
version 1.1
creator q-ai.nl
summary Language and translation resource
package i18n
frontend false
backend true
requires @cookies @OpenAI @INI phlo.async
advice Use %lang in views to show current app lang (for example in links)
tags lang translation i18n locale ai
function

function nl ($text, ...$args)

line 11
Translates the given text into Dutch using the specified arguments for formatting.
%lang->translation('nl', $text, ...$args)
function

function en ($text, ...$args)

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)

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->lang
prop

%lang -> model

line 23
This function retrieves the model associated with the specified language identifier.
'gpt-4o-mini'
static

lang :: fileCache

line 24
lang::$fileCache is a static property that stores cached language files for efficient retrieval during runtime.
[]
method

%lang -> file ($lang)

line 26
Retrieves the configuration file for the specified language, using the format 'langs.$lang.ini'.
langs.$lang.'.ini'
method

%lang -> escape ($value)

line 28
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)

line 29
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)

line 31
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)

line 37
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 $items
method

%lang -> search ($file, $hash)

line 48
Searches for a specific hash in a file and returns its associated value if found.
$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 $value
method

%lang -> lookup ($hash)

line 94
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] = $value
method

%lang -> save ($lang, $pairs)

line 105
Saves a set of key-value pairs to a language file, ensuring the keys are sorted and the file permissions are set appropriately.
$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

line 119
Retrieves the translation context from the app author, providing information about the purpose and domain if available.
($instr = trim((string)(%app->transInstr ?? void))) !== void ? lf.'Context from the app author about purpose and domain: '.$instr : void
prop

%lang -> browser

line 121
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

line 122
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 : null
method

%lang -> detect ($text, $fallback = 'en')

line 123
Detects the language of the given text and returns its ISO 639-1 code, defaulting to 'en' if detection fails.
$res = %OpenAI->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) : $fallback
method

%lang -> hash ($from, $text)

line 132
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)

line 133
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) : $translation
method

%lang -> translate ($from, $to, $text)

line 152
Translates a given text from one ISO 639-1 language to another using the OpenAI API, while preserving markdown formatting and capitalization.
if ($from === $to) return $text
return %OpenAI->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,
)->answer
method

%lang -> translateBatch ($from, $to, $texts)

line 161
Translates a batch of texts from one language to another using the OpenAI chat model, 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 = %OpenAI->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 $result
object

%payload

/phlo/resources/payload.phlo
version 1.0
creator q-ai.nl
summary POST, PUT, PATCH and file-upload data object
package web
frontend false
backend true
requires @file
tags payload request upload post put patch
method

%payload -> controller

line 10
Processes incoming request payloads based on the content type, handling JSON, URL-encoded, and multipart form data, and populates the objData property accordingly.
contentType = %req->contentType
if (in_array(phlo('req')->method, ['POST', 'PUT', 'PATCH']) && str_starts_with($contentType, 'application/json')){
$data = json_read('php://input')
return $this->objData = is_object($data) ? get_object_vars($data) : (is_array($data) ? $data : [])
}
if ($_POST) $this->objImport(...$_POST)
elseif (in_array(phlo('req')->method, ['PUT', 'PATCH']) && str_starts_with($contentType, 'application/x-www-form-urlencoded')){
$body = file_get_contents('php://input')
$data = []
parse_str($body, $data)
if ($data) $this->objImport(...$data)
}
elseif (in_array(phlo('req')->method, ['PUT', 'PATCH']) && 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) $this->objImport(...loop($_FILES, fn($f) => 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

%session

/phlo/resources/session.phlo
version 1.0
creator q-ai.nl
summary Session data object
package web
frontend false
backend true
tags session web state
method

%session -> controller

line 9
Initializes the session and assigns the session data to the objData property.
ession_start()
$this->objData = $_SESSION
method

%session -> __set ($key, $value)

line 12
Sets a session variable with the specified key to the given value.
$_SESSION[$key] = $this->objData[$key] = $value
method

%session -> __unset ($key)

line 13
Removes the specified key from the session data and the internal object data.
unset($this->objData[$key], $_SESSION[$key])
method

%session -> __isset ($key)

line 14
Checks if a session variable identified by the given key is set and not null.
isset($this->objData[$key])
method

%session -> objRegenerateId ($deleteOld = true)

line 16
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 = $_SESSION
object

%sitemap

/phlo/resources/sitemap.phlo
version 1.0
creator q-ai.nl
summary Generic multilingual sitemap generator
package seo
frontend false
backend true
requires output
tags sitemap seo multilingual xml
route

route GET sitemap.xml

line 10
Outputs the current instance of the route for the GETSitemap method.
output($this)
method

%sitemap -> intl ($uri)

line 12
Retrieves the internationalized slug for a given URI from the app's slugs, or returns the URI itself if no slug is found.
(%app->slugs ?? [])[$uri] ?? $uri
view

%sitemap -> view

line 14
Generates an XML sitemap for the application by iterating over the defined pages and including their URLs.
<?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 %app->pages AS $uri>
		{{ $this->page($uri) }}
	</foreach>
</urlset>
view

%sitemap -> page ($uri)

line 22
Generates a sitemap entry for a specific view, including localized URLs for each language supported by the application.
<url>
	<loc>%req->base{( $uri ?: slash )}</loc>
	<foreach array_keys(%app->langs) 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
line 33
Generates an alternate link for a sitemap entry, specifying the language and the base URI of the request.
<xhtml:link rel=alternate hreflang="$lang" href="%req->base$uri"{{ slash }}>
view
line 34
Generates a link element for alternate language versions of a page in the sitemap, using the specified language and request URI.
<link rel=alternate hreflang="$lang" href="%req->base$uri">
object

%tasks

/phlo/resources/tasks.phlo
version 1.1
creator q-ai.nl
summary Cron runner for %app->tasks. One cron entry per app triggers this every minute.
package scheduling
frontend false
backend true
tags cron schedule tasks scheduler
static

tasks :: dir

line 9
Accesses the directory path for tasks, specifically pointing to 'tasks/'.
data.'tasks/'
static

tasks :: run

line 11
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 26
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)

line 28
Determines if a scheduled task is due to run based on its frequency settings, such as 'every', 'daily', or 'weekly'. It checks the last run time against the current time to decide if the task should be executed.
$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 today"))) return false
	return $last < strtotime('monday this week', $now)
}
return false
static

tasks :: fire ($do)

line 46
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)

line 56
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) : 0
static

tasks :: markRun ($name, $ts)

line 61
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)

line 63
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 true
static

tasks :: unlock ($name)

line 70
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
version 1.0
creator q-ai.nl
summary User agent information
package web
frontend false
backend true
tags useragent browser os device web
prop

%useragent -> source

line 9
This expression retrieves the user agent string from the request object, returning null if it is not set.
%req->userAgent ?: null
prop

%useragent -> os

line 11
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

line 27
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 void
prop

%useragent -> osFull

line 38
This method returns the operating system name along with its version, formatted to exclude minor version numbers if they are zero.
if (!$this->OS) return 'Unknown'
$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.' '.$short)
prop

%useragent -> name

line 47
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

line 64
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 void
prop

%useragent -> full

line 75
Returns the full user agent string, including the name and version of the user agent, formatted to remove 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.' '.$short)
prop

%useragent -> device

line 84
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
version 1.0
creator q-ai.nl
summary Visitor tracking via heartbeat
extends model
package analytics
frontend false
backend true
requires @payload @model token useragent
tags visitors analytics heartbeat tracking
static

visitors :: table

line 11
The 'visitors::$table' refers to the database table associated with the 'visitors' resource in Phlo.
'visitors'
static

visitors :: columns

line 12
Defines the columns for the 'visitors' resource, specifying the attributes to be included in the data structure.
'id,token,host,page,lang,IP,browser,os,device,requests,state,width,height,referrer,created,changed'
static

visitors :: history

line 14
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

line 15
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

line 16
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 18
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 23
Parses the referrer URL to identify the search engine used, returning a formatted string indicating the search engine or the host name 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\./', '', (string)(parse_url($url, PHP_URL_HOST) ?? '')))
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

line 30
This route handles PUT requests to log heartbeat data from visitors, recording user consent, device information, and page details, while managing unique identifiers and referrer parsing.
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.%app->token.space.%useragent->source) : token(20, $n.space.date('Ymd').space.%app->token.space.%req->ip)
$data = arr (
	token: token(20, (string)(%app->token ?? error('No app token available'))),
	host: host,
	page: %payload->u,
	lang: strlen(%payload->l) === 2 ? %payload->l : %app->lang ?? 'en',
	IP: $consent ? %req->ip : void,
	browser: $consent ? %useragent->full.(%payload->a ? ' App' : void) : substr(md5((string)%useragent->source), 0, 8),
	os: $consent ? %useragent->osFull : void,
	device: $consent ? %useragent->device : void,
	requests: %payload->p,
	state: %payload->v,
	width: %payload->w,
	height: %payload->h,
	changed: time(),
)
$record = static::record(id: $id, columns: 'id,page,referrer')
if (($referrer = %payload->r) && !$record?->referrer && !str_contains($referrer, host)) $data['referrer'] = static::parseReferrer($referrer)
if ($record) static::change('id=?', $id, ...$data)
else static::create(...$data, id: $id, created: time())
view

script

line 56
This script manages a heartbeat mechanism that sends visitor data to the server, including consent status and app state, while also handling cookie creation and updates for visitor tracking.
let curpath = app.path
let heartbeatTimeout
const heartbeat = () => delay('heartbeat', 333, () => {
	clearTimeout(heartbeatTimeout)
	const consent = document.cookie.includes('cookieChoice=all')
	if (consent){
		const m = document.cookie.match(/phlo_visitor=([a-z0-9]{8})/)
		if (m) window.name = m[1]
		else {
			window.name ||= phlo.token(8)
			document.cookie = `phlo_visitor=${window.name};path=/;max-age=${365 * 86400};SameSite=Lax`
		}
	}
	else window.name ||= phlo.token(8)
	fetch('/heartbeat', {method: 'PUT', headers: {'Content-Type': 'application/json'}, 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: phlo.state.index, r: (r = document.referrer) ? (r === `${location.origin}/` ? null : r) : null, c: consent ? 1 : 0})})
	heartbeatTimeout = setTimeout(heartbeat, 20000)
})
document.addEventListener('visibilitychange', heartbeat)
;['focus', 'blur', 'resize'].forEach(e => addEventListener(e, heartbeat))
app.updates.push(() => curpath !== app.path && (heartbeat(), curpath = app.path))
heartbeat()
object

%websocket

/phlo/resources/websocket.phlo
version 1.0
creator q-ai.nl
summary Server-side WebSocket handler via PhloWS
advice Enable this class only when websockets are configured for the host
package realtime
frontend false
backend true
tags websocket realtime ws server
static

websocket :: connect ($host, $token, $socket)

line 10
function_exists('wsConnect') && wsConnect($host, $token, $socket)
static

websocket :: auth ($host, $token, $socket)

line 11
function_exists('wsAuth') && wsAuth($host, $token, $socket)
static

websocket :: receive ($host, $token, $socket, $data)

line 12
function_exists('wsReceive') && wsReceive($host, $token, $socket, ...json_decode($data, true))
static

websocket :: close ($host, $token, $socket)

line 13
function_exists('wsClose') && wsClose($host, $token, $socket)
object

%WhatsApp

/phlo/resources/WhatsApp.phlo
version 1.0
creator q-ai.nl
summary WhatsApp client for PhloWA using whatsapp-web.js
package messaging
frontend false
backend true
requires HTTP
tags whatsapp messaging api
method

%WhatsApp -> __construct (public string $url, public string $secret)

line 10
Initializes a WhatsApp instance with a specified URL and secret, ensuring the URL ends with a slash.
$this->url = rtrim($url, slash).slash
static

WhatsApp :: channel ($channel)

line 12
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)

line 14
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)

line 15
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

line 17
Retrieves the current status from WhatsApp using a GET request.
$this->request('status', GET: true)
method

%WhatsApp -> health

line 18
Checks the health status of the WhatsApp service by sending a GET request.
$this->request('health', GET: true)
method

%WhatsApp -> qr

line 19
Sends a request to retrieve the QR code for WhatsApp authentication.
$this->request('qr', GET: true)
method

%WhatsApp -> disconnect

line 20
Disconnects the current WhatsApp session by sending a disconnect request.
$this->request('disconnect')
method

%WhatsApp -> read ($chat)

line 22
Sends a request to read messages from a specified WhatsApp chat.
$this->request('read', chat: $chat)
method

%WhatsApp -> reaction ($msg, $emoji)

line 23
Sends a reaction emoji to a specified message in WhatsApp.
$this->request('reaction', msg: $msg, emoji: $emoji)
method

%WhatsApp -> text ($to, $text)

line 25
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)

line 26
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)

line 27
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)

line 28
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)

line 30
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)

line 31
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)

line 33
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)

line 35
Starts the typing indicator for a specified recipient in WhatsApp.
$this->request('typing/start', to: $to)
method

%WhatsApp -> stopTyping ($to)

line 36
Stops the typing indicator for a specific recipient in WhatsApp.
$this->request('typing/stop', to: $to)
method

%WhatsApp -> request ($action, ...$data)

line 38
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)

/phlo/resources/active.phlo line 7
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).'"' : void
function

age(int $time)

/phlo/resources/age.phlo line 7
Calculates the age by subtracting the given time from the current time.
time() - $time
function

age_human(int $age)

/phlo/resources/age.human.phlo line 8
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 8
Caches a value using APCu with a specified key and callback, allowing for a duration and optional logging.
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)

/phlo/resources/await.phlo line 10
Executes multiple jobs asynchronously and collects their results, handling any errors that occur during execution.
	$children = []
	foreach ($jobs AS $i => $job){
		[$cb, $args] = is_array($job) ? [$job[0], array_slice($job, 1)] : [$job, []]
		$cmd = cli.space.escapeshellarg((string)$_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])
		$children[$i] = obj(proc: $proc, out: $pipes[1], err: $pipes[2])
	}
	$results = []
	foreach ($children AS $i => $child){
		$out = stream_get_contents($child->out)
		$err = stream_get_contents($child->err)
		fclose($child->out)
		fclose($child->err)
		$code = proc_close($child->proc)
		$err = trim((string)$err)
		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($out, true)
		$results[$i] = json_last_error() === JSON_ERROR_NONE ? $json : $out
	}
	return $results
function

button(...$args):string

/phlo/resources/tags.form.phlo line 10
Creates a button element with the specified arguments passed as props.
tag('button', ...$args)
function

camel(string $text)

/phlo/resources/camel.phlo line 7
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 8
This function processes a set of commands, handling debugging information and managing the response for streaming output in a specific format.
	$res = %res
	$cli = %req->cli
	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 = 'text/event-stream'
		$res->header('Cache-Control', 'no-store')
		$res->header('X-Content-Type-Options', 'nosniff')
		$res->render(206)
	}
	print(json_encode($cmds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf)
	$cli || [@ob_flush(), flush()]
function

create(iterable $items, \Closure $keyCb, ?\Closure $valueCb = null)

/phlo/resources/create.phlo line 7
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)

/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)

/phlo/resources/exec.stream.phlo line 9
Executes a command in a separate process and streams its output and error messages asynchronously, yielding them as objects. It also supports a timeout feature to terminate the process if it exceeds the specified duration.
	$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, bool $DELETE = false, ?string $agent = null)

/phlo/resources/HTTP.phlo line 8
Sends an HTTP request to the specified URL with optional headers and supports various methods including GET, POST, PUT, PATCH, and DELETE.
	$curl = curl_init($url)
	if ($POST !== null || $PUT !== null || $PATCH !== 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]
		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)
	curl_setopt_array($curl, [CURLOPT_COOKIEFILE => data.'cookies.txt', CURLOPT_COOKIEJAR => data.'cookies.txt', CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15, CURLOPT_ENCODING => void])
	$res = curl_exec($curl)
	if ($res === false) error('HTTP error: '.curl_error($curl))
	return $res
function

input(...$args):string

/phlo/resources/tags.form.phlo line 11
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 8
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

n8n_test($webhook, ?array $data = null)

/phlo/resources/n8n.test.phlo line 8
This function triggers an n8n workflow using the specified webhook and optional data array, returning the result of the n8n call.
n8n($webhook, $data, true)
function

nl($text, ...$args)

/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 = '', 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 = (string)($cfg->url ?? void)
	$secret = (string)($cfg->secret ?? void)
	if ($url === void || $secret === void) return
	try {
		HTTP($url, ['secret: '.$secret], true, [
			'app' => (string)($cfg->app ?? (defined('id') ? id : 'app')),
			'server' => (string)($cfg->server ?? 'local'),
			'host' => (string)(%req->host ?? ''),
			'type' => $type,
			'level' => $level,
			'title' => $title,
			'body' => $body,
			'user' => $user,
		])
	}
	catch (\Throwable $e){}
function

phlo(?string $phloName = null, ...$args):mixed

/phlo/phlo.php line 190
Creates or retrieves an instance of a Phlo object based on the provided name and arguments, managing a static list of objects for efficient access.
static $list = [];
if ($phloName === 'tech/reset') 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 38
Initializes the Phlo application with specified arguments, setting up necessary configurations, autoloading classes, and handling errors and exceptions.
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['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['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)

/phlo/resources/phlo.async.phlo line 8
Executes a callback function asynchronously in the background, passing any additional arguments to it, and returns the process ID of the spawned process.
last($cmd = cli.space.escapeshellarg((string)$_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void).' > /dev/null 2>&1 & echo $!', exec($cmd, $r), isset($r[0]) && ctype_digit($r[0]) && (int)$r[0] > 0)
function

phlo_cli(array $args):void

/phlo/phlo.php line 174
Executes a method or function specified in the `$args` array and outputs the result as a JSON string.
if (!$args) return;
$target = array_shift($args);
if (str_contains($target, dot)){
	[$object, $method] = explode(dot, $target, 2);
	$handle = phlo($object);
	$result = $args ? $handle->$method(...$args) : ($handle->hasMethod($method) ? $handle->$method() : $handle->$method);
}
elseif (str_contains($target, '::')){
	[$class, $method] = explode('::', $target, 2);
	$result = $class::$method(...$args);
}
else $result = $target(...$args);
if (isset($result)) print(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf);
function

phlo_exception(Throwable $e):void

/phlo/phlo.php line 33
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)

/phlo/resources/phlo.exists.phlo line 7
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 151
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_stream(string $cb, ...$args)

/phlo/resources/phlo.stream.phlo line 10
Executes a callback in a streaming manner, passing additional arguments to it while yielding the output.
yield from exec_stream(cli.space.escapeshellarg((string)$_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 8
Executes a specified callback function in the CLI context with provided arguments and returns the result as a JSON object, handling errors appropriately.
	$cmd = cli.space.escapeshellarg((string)$_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)
	$j = json_decode($out, true)
	if ($code !== 0) error('Could not execute "'.esc($cb).'" via CLI')
	if (json_last_error() !== JSON_ERROR_NONE) return $out
	if (is_array($j) && isset($j['error'])) error((string)$j['error'])
	return $j
function

phlo_thread():void

/phlo/phlo.php line 113
Handles the main execution flow of a Phlo application, managing requests for both CLI and web environments, including authentication and dashboard rendering.
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;
	}
	$isDashboard = build && debug && control && str_starts_with($req->path.slash, control.slash);
	if (auth && !$isDashboard){
		phlo_auth('site', 'Phlo App - '.host);
		if (phlo('res')->done) return;
	}
	if ($isDashboard){
		require_once engine.'control.php';
		phlo_dashboard::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' || $e->getMessage() === 'PhloStop') return;
	phlo_exception($e);
}
catch (Throwable $e){
	phlo_exception($e);
}
function

select(...$args):string

/phlo/resources/tags.form.phlo line 12
Creates a 'select' HTML element with the provided arguments as attributes and options.
tag('select', ...$args)
function

slug(string $text)

/phlo/resources/slug.phlo line 7
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

tag(string $tagName, ?string $inner = null, ...$args)

/phlo/resources/tag.phlo line 8
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 13
Creates a 'textarea' HTML element with the specified arguments.
tag('textarea', ...$args)
function

time_human(?int $time = null)

/phlo/resources/time.human.phlo line 7
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 = websocket, ...$data)

/phlo/resources/wsCast.phlo line 8
Sends a message to a specified WebSocket target, allowing for data to be transmitted over the WebSocket connection.
HTTP (
	'http://127.0.0.1:'.$wsPort.'/message',
	JSON: true,
	POST: arr (
		host: $wsHost,
		target: $wsTarget,
		data: $data,
	),
)

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.