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
该控制器检索当前的cookie状态并将其分配给objData属性。
this->objData = $_COOKIE
prop

%cookies -> lifetimeDays

line 11
设置 cookies 的生命周期(以天为单位)。
180
method

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

line 13
使用指定的键和值设置一个cookie,并可以选择性地指定过期时间、路径、安全性和SameSite属性。
$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
通过从本地对象数据和全局 $_COOKIE 数组中取消设置来删除 cookie,并将其到期日期设置为过去。
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
使用指定的参数格式将给定文本翻译成荷兰语。
%lang->translation('nl', $text, ...$args)
function

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

line 12
此函数检索指定文本的英文翻译,并可以选择性地使用附加参数进行格式化。
%lang->translation('en', $text, ...$args)
static

lang :: asyncBatch ($from, $to, $json)

line 14
异步执行批量翻译,解码 JSON 输入,并在成功时保存翻译。
%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
此函数检索应用程序的当前语言设置,从而允许视图的本地化。
%app->lang
prop

%lang -> model

line 23
此函数检索与指定语言标识符相关联的模型。
'gpt-4o-mini'
static

lang :: fileCache

line 24
lang::$fileCache 是一个静态属性,用于存储缓存的语言文件,以便在运行时高效检索。
[]
method

%lang -> file ($lang)

line 26
获取指定语言的配置文件,格式为 'langs.$lang.ini'。
langs.$lang.'.ini'
method

%lang -> escape ($value)

line 28
对字符串中的特殊字符进行转义,以便安全地输出到HTML中,将反斜杠、双引号和换行符替换为各自的转义序列。
strtr((string)$value, [bs => bs.bs, dq => bs.dq, lf => '\n'])
method

%lang -> unescape ($value)

line 29
此函数通过用相应的字符替换转义序列来解码给定字符串。
strtr(strtr($value, [bs.bs => "\x01", bs.dq => dq, '\n' => lf]), ["\x01" => bs])
method

%lang -> lineValue ($line, $eq)

line 31
从给定字符串中提取和处理行值,去除周围的引号(如果存在)并对任何特殊字符进行反转义。
$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
从指定文件中读取所有键值对,并将其作为关联数组返回。如果文件不存在或不是有效文件,则返回空数组。
$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
在文件中搜索特定的哈希值,并在找到时返回其关联的值。
$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
根据提供的哈希值在语言文件缓存中查找值,如果文件已更改,则更新缓存。
$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
将一组键值对保存到语言文件中,确保键被排序并适当设置文件权限。
$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
从应用程序作者那里检索翻译上下文,如果可用,提供有关目的和领域的信息。
($instr = trim((string)(%app->transInstr ?? void))) !== void ? lf.'Context from the app author about purpose and domain: '.$instr : void
prop

%lang -> browser

line 121
从 'Accept-Language' HTTP 标头中提取首选语言,返回应用程序支持的语言中第一个匹配的语言代码。
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
从 cookies 中获取语言偏好,并检查它是否是应用程序可用语言中的有效选项,如果有效则返回该语言,否则返回 null。
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : null
method

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

line 123
检测给定文本的语言并返回其ISO 639-1代码,如果检测失败则默认为'en'。
$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
根据提供的文本和指定语言的前缀生成哈希值。
$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
将给定文本从指定语言翻译为应用程序当前语言,并异步处理缺失的翻译。
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
使用OpenAI API将给定文本从一种ISO 639-1语言翻译成另一种,同时保留markdown格式和大写字母。
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
使用OpenAI聊天模型将一批文本从一种语言翻译成另一种语言,返回以编号格式的翻译。
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
根据内容类型处理传入的请求有效负载,处理 JSON、URL 编码和多部分表单数据,并相应地填充 objData 属性。
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
初始化会话并将会话数据分配给 objData 属性。
ession_start()
$this->objData = $_SESSION
method

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

line 12
将指定键的会话变量设置为给定值。
$_SESSION[$key] = $this->objData[$key] = $value
method

%session -> __unset ($key)

line 13
从会话数据和内部对象数据中删除指定的键。
unset($this->objData[$key], $_SESSION[$key])
method

%session -> __isset ($key)

line 14
检查由给定键标识的会话变量是否已设置且不为 null。
isset($this->objData[$key])
method

%session -> objRegenerateId ($deleteOld = true)

line 16
为当前会话重新生成会话ID,基于$deleteOld参数可选择性地删除旧的会话数据。
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
输出当前路由的实例,用于GETSitemap方法。
output($this)
method

%sitemap -> intl ($uri)

line 12
从应用程序的 slugs 中检索给定 URI 的国际化 slug,如果未找到 slug,则返回 URI 本身。
(%app->slugs ?? [])[$uri] ?? $uri
view

%sitemap -> view

line 14
通过遍历定义的页面并包含其 URL,为应用程序生成 XML 网站地图。
<?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
为特定视图生成站点地图条目,包括应用程序支持的每种语言的本地化 URL。
<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
为站点地图条目生成备用链接,指定请求的语言和基本URI。
<xhtml:link rel=alternate hreflang="$lang" href="%req->base$uri"{{ slash }}>
view
line 34
生成一个链接元素,用于网站地图中页面的备用语言版本,使用指定的语言和请求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
访问任务的目录路径,特别指向'tasks/'。
data.'tasks/'
static

tasks :: run

line 11
通过检查任务的到期状态并锁定它们以防止并发执行来执行计划任务。它保存运行细节,并在执行后将任务标记为完成。
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
将运行数据保存到指定目录中的JSON文件,使用提供的名称、do、schedule和return值。
json_write(static::dir().$name.'.json', arr(do: $do, schedule: $schedule, return: $return))
static

tasks :: due ($name, $task, $now)

line 28
根据任务的频率设置(如'每个'、'每日'或'每周')确定计划任务是否到期运行。它检查上次运行时间与当前时间的关系,以决定是否应执行该任务。
$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
执行由 Closure、'Class::method' 字符串或资源名称字符串定义的任务,并返回执行结果。
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
从文件中检索任务的最后运行时间戳,如果文件不存在则返回0。
$file = static::dir().$name.'.last'
return is_file($file) ? (int)file_get_contents($file) : 0
static

tasks :: markRun ($name, $ts)

line 61
将任务最后运行的时间戳写入指定目录中以任务命名的文件,使用独占锁定以防止并发写入。
file_put_contents(static::dir().$name.'.last', (string)$ts, LOCK_EX)
static

tasks :: lock ($name)

line 63
如果任务的锁文件不存在或超过一个小时,则创建一个锁文件。
$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
删除与任务相关的锁定文件,从而允许其再次执行。
@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
该表达式从请求对象中获取用户代理字符串,如果未设置则返回null。
%req->userAgent ?: null
prop

%useragent -> os

line 11
通过将用户代理字符串与预定义模式进行匹配来确定操作系统。
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
从用户代理字符串中提取操作系统版本(如果可用),并以清洁格式返回。
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
该方法返回操作系统名称及其版本,格式化以排除小版本号为零的情况。
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
根据提供的用户代理字符串确定网页浏览器的名称。它检查各种模式以识别流行的浏览器,如Chrome、Firefox和Safari,如果没有找到匹配项,则返回'未知'。
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
从用户代理字符串中提取版本号,如果匹配特定浏览器模式,则返回清理后的版本号或在未找到匹配时返回void。
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
返回完整的用户代理字符串,包括用户代理的名称和版本,格式化以去除不必要的部分。
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
根据存储在 source 属性中的用户代理字符串确定设备类型(平板电脑、手机或桌面)。
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
'visitors::$table' 指的是与 Phlo 中的 'visitors' 资源相关联的数据库表。
'visitors'
static

visitors :: columns

line 12
定义 'visitors' 资源的列,指定要包含在数据结构中的属性。
'id,token,host,page,lang,IP,browser,os,device,requests,state,width,height,referrer,created,changed'
static

visitors :: history

line 14
检索访客数量的历史记录,按日期分组并计算独特的令牌和总访问次数。
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
这获取在过去9秒内发生变化的独特在线访客的数量。
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 9)')
static

visitors :: lastHour

line 16
检索在过去一小时内发生变化的独特访客(令牌)的数量。
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 3600)')
static

visitors :: isBot (?string $ua):bool

line 18
通过将用户代理字符串与预定义的正则表达式模式进行匹配,来判断访问者是否为机器人。
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
解析引用 URL 以识别使用的搜索引擎,返回一个格式化字符串,指示搜索引擎或在未找到匹配项时返回主机名。
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
此路由处理来自访客的PUT请求,以记录心跳数据,包括用户同意、设备信息和页面细节,同时管理唯一标识符和引荐来源解析。
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
该脚本管理一个心跳机制,将访客数据发送到服务器,包括同意状态和应用状态,同时处理访客跟踪的cookie创建和更新。
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
使用指定的URL和密钥初始化WhatsApp实例,确保URL以斜杠结尾。
$this->url = rtrim($url, slash).slash
static

WhatsApp :: channel ($channel)

line 12
使用提供的URL和密钥创建WhatsApp频道的新实例,如果未指定,则默认为'http://localhost:8081'和'void'。
new static($channel->configData->url ?? 'http://localhost:8081', $channel->secretData->secret ?? void)
method

%WhatsApp -> number ($contact)

line 14
从WhatsApp联系人字符串中提取电话号码,如果格式无效则返回错误。
($pos = strpos($contact, '@')) ? substr($contact, 0, $pos) : error('Invalid contact: '.esc($contact))
method

%WhatsApp -> isGroup ($contact)

line 15
通过验证联系人是否包含 '@g' 后缀来检查指定的联系人是否为 WhatsApp 群组。
last($this->number($contact), (bool)strpos($contact, '@g'))
method

%WhatsApp -> status

line 17
使用GET请求从WhatsApp获取当前状态。
$this->request('status', GET: true)
method

%WhatsApp -> health

line 18
通过发送 GET 请求检查 WhatsApp 服务的健康状态。
$this->request('health', GET: true)
method

%WhatsApp -> qr

line 19
发送请求以检索WhatsApp身份验证的二维码。
$this->request('qr', GET: true)
method

%WhatsApp -> disconnect

line 20
通过发送断开请求来断开当前的WhatsApp会话。
$this->request('disconnect')
method

%WhatsApp -> read ($chat)

line 22
发送请求以读取指定WhatsApp聊天中的消息。
$this->request('read', chat: $chat)
method

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

line 23
向WhatsApp中的指定消息发送反应表情符号。
$this->request('reaction', msg: $msg, emoji: $emoji)
method

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

line 25
通过WhatsApp向指定的收件人发送文本消息。
$this->request('text', to: $to, text: $text)
method

%WhatsApp -> image ($to, file $file, $text = void)

line 26
通过WhatsApp向指定的收件人发送图像消息,选项包括附加文本消息。
$this->request('image', to: $to, filename: $file->name, image: $file->src, text: $text)
method

%WhatsApp -> location ($to, $lat, $lon, $text)

line 27
通过WhatsApp向指定的收件人发送带有纬度、经度和可选文本的位置消息。
$this->request('location', to: $to, lat: $lat, lon: $lon, text: $text)
method

%WhatsApp -> document ($to, file $file, $text = void)

line 28
通过WhatsApp将文档发送给指定的收件人,包括可选的文本消息。
$this->request('document', to: $to, filename: $file->name, document: $file->src, text: $text)
method

%WhatsApp -> audio ($to, file $file)

line 30
使用提供的音频文件向指定的接收者发送音频消息。
$this->request('audio', to: $to, audio: $file->src)
method

%WhatsApp -> voice ($to, file $file)

line 31
使用提供的音频文件向指定的收件人发送语音消息。
$this->request('voice', to: $to, audio: $file->src)
method

%WhatsApp -> poll ($to, $name, array $options, bool $multi = false)

line 33
向指定的WhatsApp接收者发送带有给定选项的投票消息,如果指定,则允许多选。
$this->request('poll', to: $to, name: $name, options: $options, multi: $multi)
method

%WhatsApp -> startTyping ($to)

line 35
为WhatsApp中的指定收件人启动输入指示器。
$this->request('typing/start', to: $to)
method

%WhatsApp -> stopTyping ($to)

line 36
停止WhatsApp中特定收件人的输入指示器。
$this->request('typing/stop', to: $to)
method

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

line 38
向WhatsApp API发送指定操作和数据的请求,返回一个指示成功或失败的响应对象。
$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
为HTML元素生成类属性字符串,如果条件为真,则将'active'添加到指定的类列表中。
$cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : void
function

age(int $time)

/phlo/resources/age.phlo line 7
通过从当前时间中减去给定的时间来计算年龄。
time() - $time
function

age_human(int $age)

/phlo/resources/age.human.phlo line 8
根据给定的年龄(以秒为单位)计算可读的时间持续时间。
time_human(time() - $age)
function

apcu($key, $cb, int $duration = 3600, bool $log = true)

/phlo/resources/apcu.phlo line 8
使用APCu缓存一个值,指定键和回调,允许设置持续时间和可选日志记录。
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
异步执行多个作业并收集其结果,处理执行过程中发生的任何错误。
	$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
创建一个按钮元素,使用作为 props 传递的指定参数。
tag('button', ...$args)
function

camel(string $text)

/phlo/resources/camel.phlo line 7
通过将每个单词的首字母大写并去除空格,将给定字符串转换为驼峰式。
lcfirst(str_replace(space, void, ucwords(lcfirst($text))))
function

chunk(...$cmds):void

/phlo/resources/chunk.phlo line 8
此函数处理一组命令,处理调试信息并管理以特定格式的流输出响应。
	$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
通过使用可迭代对象中的值作为键,并使用可选的值回调来确定相应的值,创建一个关联数组。
array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items)
function

en($text, ...$args)

/phlo/resources/lang.phlo line 12
此函数检索指定文本的英文翻译,并可以选择性地使用附加参数进行格式化。
%lang->translation('en', $text, ...$args)
function

exec_stream(string $cmd, ?int $timeoutSec = 0)

/phlo/resources/exec.stream.phlo line 9
在单独的进程中执行命令,并异步流式传输其输出和错误消息,以对象形式返回。它还支持超时功能,以在超过指定持续时间时终止进程。
	$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
向指定的URL发送HTTP请求,支持可选的头部,并支持包括GET、POST、PUT、PATCH和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
在视图中创建一个具有指定参数的输入元素。
tag('input', ...$args)
function

n8n($webhook, ?array $data = null, $test = false)

/phlo/resources/n8n.phlo line 8
向 n8n webhook 发送一个可选数据和测试标志的 HTTP POST 请求。
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

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

/phlo/resources/n8n.test.phlo line 8
此函数使用指定的 webhook 和可选的数据数组触发 n8n 工作流,返回 n8n 调用的结果。
n8n($webhook, $data, true)
function

nl($text, ...$args)

/phlo/resources/lang.phlo line 11
使用指定的参数格式将给定文本翻译成荷兰语。
%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
通过HTTP将指定标题、正文、类型、级别和可选用户的通知发送到配置的URL。
	$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
根据提供的名称和参数创建或检索Phlo对象的实例,管理一个静态对象列表以实现高效访问。
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
使用指定的参数初始化Phlo应用程序,设置必要的配置,自动加载类,并处理错误和异常。
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
在后台异步执行回调函数,并将任何附加参数传递给它,返回生成的进程的进程ID。
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
执行在`$args`数组中指定的方法或函数,并将结果作为JSON字符串输出。
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
通过将Throwable对象传递给phlo_error_handle函数来处理异常。
require_once engine.'error.php';
phlo_error_handle($e);
function

phlo_exists(string $obj)

/phlo/resources/phlo.exists.phlo line 7
检查指定的 PHP 文件是否存在于给定的对象路径中。
is_file(php.strtr($obj, [us => dot]).'.php')
function

phlo_load(bool $http):void

/phlo/phlo.php line 151
加载应用程序所需的运行时文件,确保应用程序仅加载一次,并为HTTP响应设置正确的内容类型。
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
以流式方式执行回调,并在返回输出时传递额外参数。
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
在CLI上下文中执行指定的回调函数,并使用提供的参数返回结果作为JSON对象,适当地处理错误。
	$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
处理Phlo应用程序的主要执行流程,管理CLI和Web环境的请求,包括身份验证和仪表板渲染。
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
创建一个 'select' HTML 元素,使用提供的参数作为属性和选项。
tag('select', ...$args)
function

slug(string $text)

/phlo/resources/slug.phlo line 7
将给定字符串转换为URL友好的slug,通过删除非字母数字字符、转换为小写并用短横线替换空格。
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
生成一个具有指定名称、可选内部内容和附加属性的HTML标签。
"<$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
创建一个带有指定参数的 'textarea' HTML 元素。
tag('textarea', ...$args)
function

time_human(?int $time = null)

/phlo/resources/time.human.phlo line 7
将给定的时间戳转换为人类可读的时间差格式,例如 '2天' 或 '3小时'。如果未提供时间戳,则使用当前时间。
	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
向指定的WebSocket目标发送消息,允许通过WebSocket连接传输数据。
HTTP (
	'http://127.0.0.1:'.$wsPort.'/message',
	JSON: true,
	POST: arr (
		host: $wsHost,
		target: $wsTarget,
		data: $data,
	),
)

我们使用必要的cookie来使该网站正常工作。在您的许可下,我们还使用分析工具来改善网站。