Core
object
%cookies
/phlo/resources/cookies.phlo
method
%cookies -> controller
line 9
该控制器检索当前的cookie状态并将其分配给objData属性。
this->objData = $_COOKIEprop
%cookies -> lifetimeDays
line 11
设置 cookies 的生命周期(以天为单位)。
180method
%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 truemethod
%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
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->langprop
%lang -> model
line 23
此函数检索与指定语言标识符相关联的模型。
'gpt-4o-mini'prop
%lang -> instructions
line 24
定义了 Phlo 编程语言的一组指令。
voidstatic
lang :: fileCache
line 25
lang::$fileCache 是一个静态属性,用于存储缓存的语言文件,以便在运行时高效检索。
[]method
%lang -> file ($lang)
line 27
获取指定语言的配置文件,格式为 'langs.$lang.ini'。
langs.$lang.'.ini'method
%lang -> escape ($value)
line 29
对字符串中的特殊字符进行转义,以便安全地输出到HTML中,将反斜杠、双引号和换行符替换为各自的转义序列。
strtr((string)$value, [bs => bs.bs, dq => bs.dq, lf => '\n'])method
%lang -> unescape ($value)
line 30
此函数通过用相应的字符替换转义序列来解码给定字符串。
strtr(strtr($value, [bs.bs => "\x01", bs.dq => dq, '\n' => lf]), ["\x01" => bs])method
%lang -> lineValue ($line, $eq)
line 32
从给定字符串中提取和处理行值,去除周围的引号(如果存在)并对任何特殊字符进行反转义。
$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 38
从指定文件中读取所有键值对,并将其作为关联数组返回。如果文件不存在或不是有效文件,则返回空数组。
$items = []
if (!is_file($file)) return $items
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] AS $line){
$eq = strpos($line, ' = ')
if ($eq === false) continue
$items[substr($line, 0, $eq)] = $this->lineValue($line, $eq)
}
return $itemsmethod
%lang -> search ($file, $hash)
line 49
在文件中搜索特定的哈希值,并在找到时返回其关联的值。
$size = (int)@filesize($file)
if (!$size) return null
$h = @fopen($file, 'rb')
if (!$h) return null
$lo = 0
$hi = $size
while ($hi - $lo > 4096){
$mid = intdiv($lo + $hi, 2)
fseek($h, $mid)
fgets($h)
$pos = ftell($h)
if ($pos >= $hi){
$hi = $mid
continue
}
$line = (string)fgets($h)
$eq = strpos($line, ' = ')
if ($eq === false){
$hi = $mid
continue
}
$cmp = strcmp(substr($line, 0, $eq), $hash)
if ($cmp < 0) $lo = ftell($h)
elseif ($cmp > 0) $hi = $pos
else {
fclose($h)
return $this->lineValue($line, $eq)
}
}
fseek($h, $lo)
$value = null
while (ftell($h) < $hi && ($line = fgets($h)) !== false){
$eq = strpos($line, ' = ')
if ($eq === false) continue
$cmp = strcmp(substr($line, 0, $eq), $hash)
if ($cmp > 0) break
if ($cmp === 0){
$value = $this->lineValue($line, $eq)
break
}
}
fclose($h)
return $valuemethod
%lang -> lookup ($hash)
line 95
根据提供的哈希值在语言文件缓存中查找值,如果文件已更改,则更新缓存。
$file = $this->file(%app->lang)
$mtime = (int)@filemtime($file)
$cache =& static::$fileCache[$file]
if (!$cache || $cache['mtime'] !== $mtime) $cache = ['mtime' => $mtime, 'items' => []]
if (array_key_exists($hash, $cache['items'])) return $cache['items'][$hash]
$value = $this->search($file, $hash)
if ($value === null && $mtime) $value = $this->readAll($file)[$hash] ?? null
return $cache['items'][$hash] = $valuemethod
%lang -> save ($lang, $pairs)
line 106
将一组键值对保存到语言文件中,确保键被排序并适当设置文件权限。
$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 120
如果可用,则从应用程序作者那里检索有关目的和领域的上下文;否则返回 void。
($instr = trim($this->instructions ?? void)) !== void ? lf.'Context from the app author about purpose and domain: '.$instr : voidprop
%lang -> browser
line 122
从 '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 123
从 cookies 中获取语言偏好,并检查它是否是应用程序可用语言中的有效选项,如果有效则返回该语言,否则返回 null。
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : nullmethod
%lang -> detect ($text, $fallback = 'en')
line 124
检测给定文本的语言并返回ISO 639-1代码。如果检测失败,则返回指定的后备语言代码,默认为'en'。
$res = %AI->chat (
model: $this->model,
system: 'Analyse which language this text is in and return only the ISO 639-1 code of the language, no other data!',
user: $text.lf.lf.'The ISO 639-1 code of the language is: ',
temperature: 0,
)->answer
return strlen($res) === 2 ? strtolower($res) : $fallbackmethod
%lang -> hash ($from, $text)
line 133
根据提供的文本和指定语言的前缀生成哈希值。
$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 134
将给定文本从指定语言翻译为应用程序当前语言,并异步处理缺失的翻译。
if ($from === %app->lang) $translation = strtr($text, ['\n' => lf])
else {
$translation = []
$missing = []
foreach (explode(lf, $text) AS $line){
if (trim($line)){
$hash = $this->hash($from, $line)
$item = $this->lookup($hash)
if ($item === null) [$missing[$hash] = $item = $line, debug(%app->lang.': '.(strlen($line) > 20 ? substr($line, 0, 18).'...' : $line))]
}
else $item = void
$translation[] = $item
}
if ($missing) phlo_async('lang::asyncBatch', $from, %app->lang, json_encode($missing))
$translation = implode(lf, $translation)
}
return $args ? sprintf($translation, ...$args) : $translationmethod
%lang -> translate ($from, $to, $text)
line 153
使用AI将给定文本从一种ISO 639-1语言翻译成另一种,同时保留markdown格式和大写字母。
if ($from === $to) return $text
return %AI->chat (
model: $this->model,
system: "You will be provided with a word, sentence or (markdown) text in ISO 639-1 language $from, and your task is to translate this string into ISO 639-1 language $to. Respect markdown, missing interpunction and specific use of capitals. Give only the translation.".$this->transContext(),
user: $text,
temperature: 0,
)->answermethod
%lang -> translateBatch ($from, $to, $texts)
line 162
使用AI将一批文本从一种语言翻译成另一种语言,以编号格式返回翻译结果。
if ($from === $to) return $texts
$hashes = array_keys($texts)
$numbered = implode(lf, array_map(fn($i, $t) => ($i + 1).'. '.$t, array_keys($values = array_values($texts)), $values))
$answer = %AI->chat (
model: $this->model,
system: "You will be provided with numbered lines in ISO 639-1 language $from. Translate each line into ISO 639-1 language $to. Return only the numbered translations in the same format. Respect markdown, missing interpunction and specific use of capitals.".$this->transContext(),
user: $numbered,
temperature: 0,
)->answer
$result = []
foreach (explode(lf, trim($answer)) AS $line){
if (preg_match('/^(\d+)\.\s*(.+)/', $line, $m))
$result[$hashes[(int)$m[1] - 1]] = $m[2]
}
return $resultobject
%payload
/phlo/resources/payload.phlo
method
%payload -> controller
line 10
contentType = %req->contentType
if (in_array(phlo('req')->method, ['POST', 'PUT', 'PATCH', 'QUERY']) && 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', 'QUERY']) && 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', 'QUERY']) && str_starts_with($contentType, 'multipart/form-data')){
$match = regex('/boundary="?([^";]+)"?/', $contentType)
if (!$match) return
$boundary = '--'.$match[1]
$arrays = []
$raw = file_get_contents('php://input')
foreach (explode($boundary, $raw) AS $part){
if (!trim($part) || $part === '--' || !str_contains($part, nl.nl)) continue
$headers = []
[$rawHeaders, $body] = explode(nl.nl, $part, 2)
foreach (explode(nl, trim($rawHeaders)) AS $header){
if (str_contains($header, colon)){
[$key, $value] = explode(colon, $header, 2)
$headers[strtolower(trim($key))] = trim($value)
}
}
if (!isset($headers['content-disposition'])) continue
if (!preg_match('/name="([^"]+)"/', $headers['content-disposition'], $match)) continue
$name = $match[1]
$body = rtrim($body, nl)
if ($body === void) $body = null
$base = $name
$keys = []
$hasEmptyIndex = false
if (preg_match('/^([^\[]+)((?:\[[^\]]*\])*)$/', $name, $m)){
$base = $m[1]
$brackets = $m[2]
if ($brackets){
preg_match_all('/\[([^\]]*)\]/', $brackets, $mm)
$keys = $mm[1]
$hasEmptyIndex = in_array(void, $keys, true)
}
}
if ($hasEmptyIndex) $arrays[] = $base
$assign = function($value) use ($base, $keys){
if ($keys){
if (!isset($this->objData[$base]) || !is_array($this->objData[$base])) $this->objData[$base] = []
$ref =& $this->objData[$base]
$count = count($keys)
foreach ($keys AS $i => $k){
$last = $i === $count - 1
if ($k === void){
if ($last) $ref[] = $value
else {
$ref[] = []
end($ref)
$idx = key($ref)
$ref =& $ref[$idx]
}
}
else {
if ($last) $ref[$k] = $value
else {
if (!isset($ref[$k]) || !is_array($ref[$k])) $ref[$k] = []
$ref =& $ref[$k]
}
}
}
}
else $this->objData[$base] = $value
};
if (preg_match('/filename="([^"]*)"/', $headers['content-disposition'], $f)){
if ($f[1] === void || $body === null){
if (!$hasEmptyIndex) $assign(null)
continue
}
$filename = $f[1]
$file = %file(tempnam(sys_get_temp_dir(), 'phlo'), $filename, $body)
$assign($file)
}
else $assign($body)
}
foreach ($this->objData AS $key => $val){
if (str_ends_with($key, '[]')){
unset($this->objData[$key])
$this->objData[substr($key, 0, -2)] = is_array($val) ? array_values(array_filter($val, fn($v) => $v !== null)) : [$val]
}
elseif (!is_array($val) && substr($key, -2) === '[]') $this->objData[$key] = [$val]
}
foreach (array_unique($arrays) AS $key) if (!isset($this->objData[$key])) $this->objData[$key] = []
}
if ($_FILES) $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
%seo
/phlo/resources/seo.phlo
route
route GET sitemap.xml
line 10
该路由在通过GET请求访问时生成用于SEO的站点地图。
output($this)route
route GET robots.txt
line 12
定义一个路由,响应GET请求,通过以UTF-8字符集的纯文本格式输出robots文件的内容。
output($this->robots(), type: 'text/plain; charset=utf-8')method
%seo -> robots
line 14
根据应用程序的设置生成robots.txt文件内容,指定哪些用户代理被允许或禁止访问某些路径,并包含指向网站地图的链接。
if (!(defined('indexable') && indexable)) return 'User-agent: *'.lf.'Disallow: /'.lf
$lines = ['User-agent: *', 'Allow: /']
foreach ((array)(%app->robotsDisallow ?? []) AS $path) $lines[] = 'Disallow: '.$path
$lines[] = 'Sitemap: '.%req->base.slash.'sitemap.xml'
return implode(lf, $lines).lfmethod
%seo -> intl ($uri)
line 22
从应用程序的 slugs 中检索 URI 的国际化版本,如果未找到翻译,则返回原始 URI。
(%app->slugs ?? [])[$uri] ?? $urimethod
%seo -> locale
line 24
此函数检索与应用程序语言设置对应的区域代码,如果未设置,则默认为'en'。
$lang = %app->lang ?? 'en'
$map = ['nl' => 'nl_NL', 'en' => 'en_US', 'de' => 'de_DE', 'fr' => 'fr_FR', 'es' => 'es_ES', 'it' => 'it_IT', 'pt' => 'pt_PT', 'pl' => 'pl_PL', 'ru' => 'ru_RU', 'el' => 'el_GR', 'tr' => 'tr_TR', 'zh' => 'zh_CN']
return $map[$lang] ?? $langprop
%seo -> ogTitle
line 30
这使用 title() 函数检索用于 SEO 目的的 Open Graph 标题。
title()prop
%seo -> ogDescr
line 31
获取应用程序的描述,如果未设置则返回 void。
%app->description ?? voidprop
%seo -> ogImage
line 32
此表达式用于检索Open Graph图像URL以用于SEO目的,如果未提供图像,则默认为'icon.webp'。
%req->base.slash.ltrim(%app->image ?? 'icon.webp', slash)prop
%seo -> canonical
line 33
这从请求对象中检索用于SEO目的的规范URL。
%req->urlprop
%seo -> ogType
line 34
为SEO目的设置Open Graph类型,具体为'website'。
'website'prop
%seo -> siteName
line 35
这个表达式将应用的标题或id的值赋给变量$siteName,以用于SEO目的。
%app->title ?? idprop
%seo -> twitterCard
line 36
seo->$twitterCard 属性用于配置视图的 Twitter Card 元数据,从而更好地与 Twitter 分享功能集成。
falseprop
%seo -> noIndex
line 37
seo->$noIndex 表达式在 %app->noLink 和 %app->noIndex 都未设置时评估为 true,否则返回 false。
%app->noLink ?? %app->noIndex ?? falseview
%seo -> view
line 39
通过遍历定义的页面并包含其 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
%seo -> page ($uri)
line 47
为视图生成一个SEO友好的XML网站地图条目,包括应用程序支持的每种语言的本地化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
%seo -> xlink ($lang, $uri)
line 58
生成一个用于SEO目的的替代链接元素,指定当前视图的语言和URL。
<xhtml:link rel=alternate hreflang="$lang" href="%req->base$uri"{{ slash }}>view
%seo -> link ($lang, $uri)
line 59
生成一个用于SEO目的的替代链接标签,指定资源的语言和URL。
<link rel=alternate hreflang="$lang" href="%req->base$uri">view
%seo -> head
line 61
为视图生成与SEO相关的元标签,包括基于提供的属性(如描述、标题和图像)的Open Graph和Twitter Card标签。
<if $this->noIndex>
<meta name=robots content=noindex,follow>
</if>
<if $this->ogDescr>
<meta name=description content="$this->ogDescr">
</if>
<meta property=og:site_name content="$this->siteName">
<meta property=og:title content="$this->ogTitle">
<meta property=og:description content="$this->ogDescr">
<meta property=og:type content="$this->ogType">
<meta property=og:url content="$this->canonical">
<meta property=og:image content="$this->ogImage">
<meta property=og:locale content="$this->locale">
<if $this->twitterCard>
<meta name=twitter:card content=summary_large_image>
<meta name=twitter:title content="$this->ogTitle">
<meta name=twitter:description content="$this->ogDescr">
<meta name=twitter:image content="$this->ogImage">
</if>
<if !$this->noIndex>
<link rel=canonical href="$this->canonical">
</if>object
%session
/phlo/resources/session.phlo
method
%session -> controller
line 9
初始化会话并将会话数据分配给 objData 属性。
ession_start()
$this->objData = $_SESSIONmethod
%session -> __set ($key, $value)
line 12
将指定键的会话变量设置为给定值。
$_SESSION[$key] = $this->objData[$key] = $valuemethod
%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 = $_SESSIONobject
%stream
/phlo/resources/stream.phlo
function
function stream ($data = null, string $type = 'application/octet-stream', ?string $name = null):void
line 10
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid stream()')
if (!$res->streaming){
$res->streaming = true
$res->type = $type
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->header('X-Accel-Buffering', 'no')
$name === null || $res->header('Content-Disposition', 'attachment; filename="'.str_replace('"', '', $name).'"')
$res->render()
}
if ($data === null) return
foreach (is_iterable($data) ? $data : [$data] as $part){
print((string)$part)
$cli || [@ob_flush(), flush()]
}view
script
line 30
app.stream = async (path, onData = null, async = false, type = null, data = null) => {
const url = `${location.origin}/${path}`
const headers = {}
const csrf = obj('meta[name="csrf"]')?.content
csrf && (headers['X-CSRF-Token'] = csrf)
async && (headers['X-Requested-With'] = 'phlo')
let body = data
if (body !== null && !(body instanceof FormData) && !(body instanceof Blob)) [body = JSON.stringify(body), headers['Content-Type'] = 'application/json']
phlo.log(`⇣ APP.STREAM ${url}`)
const res = await fetch(url, {method: body === null ? 'GET' : 'POST', credentials: 'same-origin', headers, body})
if (!res.ok) throw new Error(`stream ${url} 🔴 ${res.status}`)
const mime = (res.headers.get('content-type') ?? '').split(';')[0].trim()
const kind = type ?? (mime === 'application/x-ndjson' ? 'ndjson' : mime === 'application/json' ? 'json' : mime === 'text/event-stream' ? 'sse' : mime.startsWith('text/') ? 'text' : 'raw')
const emit = value => onData && onData(value)
if (kind === 'json'){
const value = await res.json()
emit(value)
return value
}
if (kind === 'blob'){
const value = await res.blob()
emit(value)
return value
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
const out = []
const parts = []
let text = ''
let buffer = ''
const line = raw => {
if (!raw) return
const value = JSON.parse(raw)
out.push(value)
emit(value)
}
const frame = raw => {
if (!raw.trim()) return
let event = 'message'
const data = []
raw.split('\n').forEach(l => l.startsWith('event:') ? event = l.slice(6).trim() : l.startsWith('data:') && data.push(l.slice(5).replace(/^ /, '')))
const value = {event, data: data.join('\n')}
out.push(value)
emit(value)
}
while (true){
const {done, value} = await reader.read()
if (done) break
if (kind === 'raw'){
parts.push(value)
emit(value)
continue
}
const piece = decoder.decode(value, {stream: true})
if (kind === 'text'){
text += piece
emit(piece)
continue
}
buffer += piece
if (kind === 'sse'){
const hold = buffer.endsWith('\r') ? '\r' : ''
buffer = buffer.slice(0, buffer.length - hold.length).replace(/\r\n?/g, '\n') + hold
}
const pieces = buffer.split(kind === 'sse' ? '\n\n' : '\n')
buffer = pieces.pop()
pieces.forEach(kind === 'sse' ? frame : line)
}
if (kind === 'raw') return new Blob(parts, {type: mime || 'application/octet-stream'})
const tail = decoder.decode()
if (kind === 'text'){
tail && [text += tail, emit(tail)]
return text
}
buffer += tail
buffer && (kind === 'sse' ? frame(buffer.replace(/\r\n?/g, '\n')) : line(buffer))
return out
}object
%tasks
/phlo/resources/tasks.phlo
static
tasks :: dir
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, $now))) return false
return $last < strtotime('monday this week', $now)
}
return falsestatic
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) : 0static
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 truestatic
tasks :: unlock ($name)
line 70
删除与任务相关的锁定文件,从而允许其再次执行。
@unlink(static::dir().$name.'.lock')object
%useragent
/phlo/resources/useragent.phlo
prop
%useragent -> source
line 9
该表达式从请求对象中获取用户代理字符串,如果未设置则返回null。
%req->userAgent ?: nullprop
%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 voidprop
%useragent -> osFull
line 38
返回完整的操作系统名称及其版本(如果可用);否则返回'未知'或仅操作系统名称。
if (!$this->OS) return 'Unknown'
if ($this->OS === 'Windows') return 'Windows'
$v = $this->osV
if (!$v) return $this->OS
$short = preg_replace('/^(\d+\.\d+).*/','$1',$v)
if (preg_match('/\.0$/',$short)) $short = preg_replace('/\.0$/', void, $short)
return trim($this->OS.space.$short)prop
%useragent -> name
line 48
根据提供的用户代理字符串确定网页浏览器的名称。它检查各种模式以识别流行的浏览器,如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 65
从用户代理字符串中提取版本号,如果匹配特定浏览器模式,则返回清理后的版本号或在未找到匹配时返回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 voidprop
%useragent -> full
line 76
返回完整的用户代理字符串,包括用户代理的名称和版本,格式化以排除不必要的部分。
if (!$this->name) return 'Unknown'
$v = $this->version
if (!$v) return $this->name
$short = preg_replace('/^(\d+\.\d+).*/','$1',$v)
if (preg_match('/\.0$/',$short)) $short = preg_replace('/\.0$/', void, $short)
return rtrim($this->name.space.$short)prop
%useragent -> device
line 85
根据存储在 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
static
visitors :: table
line 11
'visitors::$table' 指的是与 Phlo 中的 'visitors' 资源相关联的数据库表。
'visitors'static
visitors :: columns
line 12
定义了访客资源的列,指定了在数据操作中使用的属性。
'id,token,host,page,lang,IP,browser,os,device,active_seconds,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\./', void, (string)(parse_url($url, PHP_URL_HOST) ?? void)))
foreach ($engines AS $key => $name) if (str_contains($host, $key)) return 'search:'.$name
return $host ?: substr($url, 0, 100)route
route PUT heartbeat @n,v,l,u,w,h,a,p,r,c,s,pp,ps
line 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.%cookies->token.space.%useragent->source) : token(20, $n.space.date('Ymd').space.%cookies->token.space.%req->ip)
$delta = max(0, min(120, (int)%payload->s))
$prevDelta = max(0, min(120, (int)%payload->ps))
$lang = strlen(%payload->l) === 2 ? %payload->l : %app->lang ?? 'en'
$referrer = ($r = (string)%payload->r) && !str_contains($r, host) ? static::parseReferrer($r) : null
static::DB()->query('INSERT INTO '.static::$table.' (id, token, host, page, lang, IP, browser, os, device, active_seconds, state, width, height, referrer, created, changed) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE token = VALUES(token), host = VALUES(host), page = VALUES(page), lang = VALUES(lang), IP = VALUES(IP), browser = VALUES(browser), os = VALUES(os), device = VALUES(device), active_seconds = active_seconds + VALUES(active_seconds), state = VALUES(state), width = VALUES(width), height = VALUES(height), referrer = IF(referrer IS NULL OR referrer = '.sq.sq.', VALUES(referrer), referrer), changed = VALUES(changed)', $id, token(20, (string)(%cookies->token ?? %payload->n)), host, %payload->u, $lang, $consent ? %req->ip : void, $consent ? %useragent->full.(%payload->a ? ' App' : void) : substr(md5((string)%useragent->source), 0, 8), $consent ? %useragent->osFull : void, $consent ? %useragent->device : void, $delta + $prevDelta, %payload->v, %payload->w, %payload->h, $referrer, time(), time())
$pages = preg_replace('/visitors$/', 'visitor_pages', (string)static::$table)
if (strlen((string)%payload->p) >= 8){
$pv = token(20, $id.space.%payload->p)
static::DB()->query('INSERT INTO '.$pages.' (id, visitor, host, page, lang, active_seconds, beats, created, changed) VALUES (?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE active_seconds = active_seconds + VALUES(active_seconds), beats = beats + 1, changed = VALUES(changed)', $pv, $id, host, %payload->u, $lang, $delta, 1, time(), time())
}
if (strlen((string)%payload->pp) >= 8){
$ppv = token(20, $id.space.%payload->pp)
static::DB()->query('UPDATE '.$pages.' SET active_seconds = active_seconds + ?, beats = beats + 1, changed = ? WHERE id = ?', $prevDelta, time(), $ppv)
}view
script
line 51
通过测量活跃时间并将心跳数据发送到服务器来跟踪页面上的用户活动,包括用户状态和可见性状态。
let curpath = app.path
let pv = phlo.token(12)
let hbTimer, activeMs = 0, activeAt = null, prev = null
const visible = () => document.visibilityState === 'visible'
const accrue = () => {
if (activeAt === null) return
activeMs += performance.now() - activeAt
activeAt = visible() ? performance.now() : null
}
const flush = beacon => {
accrue()
const consent = document.cookie.includes('cookieChoice=all')
window.name ||= phlo.token(8)
const s = Math.floor(activeMs / 1000)
activeMs -= s * 1000
const body = JSON.stringify({n: window.name, v: app.state, l: obj('html').lang ?? 'en', u: app.path, w: innerWidth, h: innerHeight, a: app.mode, p: pv, r: (r = document.referrer) ? (r === `${location.origin}/` ? null : r) : null, c: consent ? 1 : 0, s, pp: prev ? prev.p : null, ps: prev ? prev.s : null})
prev = null
fetch('/heartbeat', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body, keepalive: !!beacon})
}
const heartbeat = () => delay('heartbeat', 333, () => {
clearTimeout(hbTimer)
flush()
if (visible()) hbTimer = setTimeout(heartbeat, 20000)
})
document.addEventListener('visibilitychange', () => {
if (!visible()) return flush(true)
activeAt = performance.now()
heartbeat()
})
addEventListener('resize', heartbeat)
addEventListener('pagehide', () => flush(true))
app.updates.push(() => {
if (curpath === app.path) return
if (prev) flush()
accrue()
prev = {p: pv, s: Math.floor(activeMs / 1000)}
pv = phlo.token(12)
activeMs = 0
curpath = app.path
heartbeat()
})
if (visible()) activeAt = performance.now()
heartbeat()object
%websocket
/phlo/resources/websocket.phlo
static
websocket :: connect ($wsHost, $wsToken, $wsSocket)
line 10
使用指定的主机、令牌和套接字参数建立WebSocket连接。
!function_exists('wsConnect') || wsConnect($wsHost, $wsToken, $wsSocket)static
websocket :: auth ($wsHost, $wsToken, $wsSocket)
line 11
检查 'wsAuth' 函数是否存在,如果不存在,则使用提供的参数调用它进行 WebSocket 身份验证。
!function_exists('wsAuth') || wsAuth($wsHost, $wsToken, $wsSocket)static
websocket :: receive ($wsHost, $wsToken, $wsSocket, $data)
line 12
使用指定的主机、令牌和套接字从WebSocket连接接收数据,并将传入数据处理为JSON。
function_exists('wsReceive') && wsReceive($wsHost, $wsToken, $wsSocket, ...json_decode($data, true))static
websocket :: close ($wsHost, $wsToken, $wsSocket)
line 13
使用指定的主机、令牌和套接字关闭WebSocket连接。
function_exists('wsClose') && wsClose($wsHost, $wsToken, $wsSocket)object%WhatsApp
/phlo/resources/WhatsApp.phlo
method
%WhatsApp -> __construct (public string $url, public string $secret)
line 10
使用指定的URL和密钥初始化WhatsApp实例,确保URL以斜杠结尾。
$this->url = rtrim($url, slash).slashstatic
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 8
为HTML元素生成类属性字符串,如果条件为真,则将'active'添加到指定的类列表中。
$cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : voidfunction
age(int $time)
/phlo/resources/age.phlo line 8
通过从当前时间中减去给定的时间来计算年龄。
time() - $timefunction
age_human(int $age)
/phlo/resources/age.human.phlo line 9
根据给定的年龄(以秒为单位)计算可读的时间持续时间。
time_human(time() - $age)function
apcu($key, $cb, int $duration = 3600, bool $log = true)
/phlo/resources/apcu.phlo line 9
使用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
等待多个作业完成,处理它们的输出和错误,并返回结果。它管理子进程,并确保它们不会无限期阻塞。
if (daemon) return daemon::await($jobs)
$children = []
$open = []
foreach ($jobs AS $i => $job){
[$cb, $args] = is_array($job) ? [$job[0], array_slice($job, 1)] : [$job, []]
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void)
$desc = [0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']]
$proc = proc_open($cmd, $desc, $pipes)
fclose($pipes[0])
stream_set_blocking($pipes[1], false)
stream_set_blocking($pipes[2], false)
$children[$i] = obj(proc: $proc, out: $pipes[1], err: $pipes[2], stdout: void, stderr: void)
$open['o'.$i] = $pipes[1]
$open['e'.$i] = $pipes[2]
}
// Drain every child's stdout AND stderr together: reading one stream to EOF before the
// other deadlocks a child that fills the unread pipe. Bound the whole wait so a hung
// child cannot block the caller forever.
$deadline = time() + (defined('await_timeout') ? await_timeout : 300)
while ($open){
$read = $open
$write = $except = []
if (@stream_select($read, $write, $except, 1) === false) break
foreach ($read AS $key => $stream){
$chunk = fread($stream, 65536)
if ($chunk === void || $chunk === false){
feof($stream) && $open = array_diff_key($open, [$key => 1])
continue
}
$i = substr($key, 1)
if ($key[0] === 'o') $children[$i]->stdout .= $chunk
else $children[$i]->stderr .= $chunk
}
if (time() >= $deadline) break
}
// Terminate any child still running (deadline or a stream_select error). SIGKILL cannot
// be ignored, so proc_close below will not block on a process that drops SIGTERM.
foreach ($children AS $child) (proc_get_status($child->proc)['running'] ?? false) && proc_terminate($child->proc, 9)
$results = []
foreach ($children AS $i => $child){
fclose($child->out)
fclose($child->err)
$code = proc_close($child->proc)
$err = trim($child->stderr)
if ($err !== void){
$ej = json_decode($err, true)
$results[$i] = json_last_error() === JSON_ERROR_NONE ? $ej : $err
continue
}
if ($code !== 0){
$results[$i] = obj(error: 'CLI process failed', code: $code)
continue
}
$json = json_decode($child->stdout, true)
$results[$i] = json_last_error() === JSON_ERROR_NONE ? $json : $child->stdout
}
return $resultsfunction
button(...$args):string
/phlo/resources/tags.form.phlo line 10
创建一个按钮元素,使用作为 props 传递的指定参数。
tag('button', ...$args)function
camel(string $text)
/phlo/resources/camel.phlo line 8
通过将每个单词的首字母大写并去除空格,将给定字符串转换为驼峰式。
lcfirst(str_replace(space, void, ucwords(lcfirst($text))))function
chunk(...$cmds):void
/phlo/resources/chunk.phlo line 9
此函数将数据块发送到客户端,如果尚未开始流,则初始化流并为响应设置适当的头部。
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid chunk()')
if (debug){
$res->dump && [$cmds['dump'] = $res->dump, $res->dump = []]
$res->debug && [$cmds['debug'] = $res->debug, $res->debug = []]
}
if (!$res->streaming){
$res->streaming = true
$res->type = 'application/x-ndjson'
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->render()
}
print(json_encode($cmds, 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 8
通过使用可迭代对象中的值作为键,并使用可选的值回调来确定相应的值,创建一个关联数组。
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, $QUERY = null, bool $DELETE = false, string|bool|null $agent = null, string|bool $cookies = false, int $timeout = 15, &$response = null)
/phlo/resources/HTTP.phlo line 9
$curl = curl_init($url)
if ($POST !== null || $PUT !== null || $PATCH !== null || $QUERY !== null){
if (!is_null($POST)) [$method = 'POST', $content = $POST]
elseif (!is_null($PUT)) [$method = 'PUT', $content = $PUT]
elseif (!is_null($PATCH)) [$method = 'PATCH', $content = $PATCH]
elseif (!is_null($QUERY)) [$method = 'QUERY', $content = $QUERY]
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method)
if ($JSON) [!is_string($content) && $content = json_encode($content), array_push($headers, 'Content-Type: application/json', 'Content-Length: '.strlen($content))]
curl_setopt($curl, CURLOPT_POSTFIELDS, $content)
}
elseif ($DELETE) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE')
$agent && curl_setopt($curl, CURLOPT_USERAGENT, $agent === true ? phlo('req')->userAgent : $agent)
if ($cookies !== false) [$jar = $cookies === true ? data.'cookies.txt' : $cookies, curl_setopt($curl, CURLOPT_COOKIEFILE, $jar), curl_setopt($curl, CURLOPT_COOKIEJAR, $jar)]
$resHeaders = []
curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => $timeout, CURLOPT_ENCODING => void, CURLOPT_HEADERFUNCTION => function($ch, $line) use (&$resHeaders){
$parts = explode(colon, $line, 2)
count($parts) === 2 && $resHeaders[strtolower(trim($parts[0]))] = trim($parts[1])
return strlen($line)
}])
$res = curl_exec($curl)
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE)
$response = obj(ok: $res !== false && $status >= 200 && $status < 300, status: $status, headers: $resHeaders, error: $res === false ? curl_error($curl) : null)
if ($res === false) error('HTTP error: '.curl_error($curl))
return $resfunction
input(...$args):string
/phlo/resources/tags.form.phlo line 11
在视图中创建一个具有指定参数的输入元素。
tag('input', ...$args)function
n8n($webhook, ?array $data = null, $test = false)
/phlo/resources/n8n.phlo line 9
向 n8n webhook 发送一个可选数据和测试标志的 HTTP POST 请求。
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)function
nl($text, ...$args)
/phlo/resources/lang.phlo line 11
使用指定的参数格式将给定文本翻译成荷兰语。
%lang->translation('nl', $text, ...$args)function
notify(string $title, string $body = void, string $type = 'info', string $level = 'info', ?string $user = null):void
/phlo/resources/notify.phlo line 8
通过HTTP将指定标题、正文、类型、级别和可选用户的通知发送到配置的URL。
$cfg = %creds->notify ?? null
if (!$cfg) return
$url = $cfg->url ?? void
$secret = $cfg->secret ?? void
if ($url === void || $secret === void) return
try {
HTTP($url, ['secret: '.$secret], true, [
'app' => (string)($cfg->app ?? (defined('id') ? id : 'app')),
'server' => $cfg->server ?? 'local',
'host' => %req->host ?? void,
'type' => $type,
'level' => $level,
'title' => $title,
'body' => $body,
'user' => $user,
])
}
catch (\Throwable $e){}function
phlo(?string $phloName = null, ...$args):mixed
/phlo/phlo.php line 240
根据提供的名称和参数创建或检索Phlo对象的实例,管理一个静态对象列表以供重用。
static $list = [];
if ($phloName === 'tech/reset'){
obj::$classProps = [];
return array_keys($list = array_filter($list, static fn($obj) => $obj->objPers));
}
if ($phloName === null) return array_keys($list);
$class = strtr($phloName, [slash => us]);
$handle = method_exists($class, '__handle') ? $class::__handle(...$args) : ($args ? null : $phloName);
if ($handle === true){
if (isset($list[$phloName])) return $list[$phloName]->objImport(...$args);
$handle = $phloName;
}
elseif ($handle && isset($list[$handle])) return $list[$handle];
$object = new $class(...$args);
if ($handle) $list[$handle] = $object;
if ($object->hasMethod('controller') && (!phlo('req')->cli || $phloName !== 'app')) $object->controller();
return $object;function
phlo_app(...$args):void
/phlo/phlo.php line 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['daemon'] ??= false;
$args['build'] && $args['thread'] && error('Phlo build and thread mode cannot be combined');
$args['build'] && !is_file($args['data'].'app.json') && error('Phlo build mode requires data/app.json');
$args['auth'] && !$args['build'] && error('Auth requires build mode');
foreach ($args as $key => $value) define($key, $value);
define('engine', __DIR__.slash);
if ($args['debug']) require_once __DIR__.'/debug.php';
if ($args['build']) require_once __DIR__.'/classes/changed.php';
if ($args['daemon']) require_once __DIR__.'/classes/daemon.php';
if ($args['trace']) trace::boot($args['app']);
set_error_handler(static function(int $level, string $msg, string $file = '', int $line = 0):bool {
if (!(error_reporting() & $level)) return false;
throw new ErrorException($msg, 0, $level, $file, $line);
});
set_exception_handler('phlo_exception');
spl_autoload_register(static function(string $class):void {
static $map = null, $mtime = null;
$file = php.'classmap.php';
if ($map === null || $mtime !== (is_file($file) ? filemtime($file) : null)){
$map = is_file($file) ? require $file : [];
$mtime = is_file($file) ? filemtime($file) : null;
}
if (isset($map[$class])){ require_once php.$map[$class]; return; }
});
if ($args['build']){
$engineMap = ['build' => 'build', 'reflect' => 'reflect', 'build_file' => 'file', 'build_node' => 'node', 'build_builder' => 'builder', 'build_css' => 'css', 'build_icons' => 'icons'];
spl_autoload_register(static function(string $class) use ($engineMap):void {
$name = $engineMap[strtolower($class)] ?? null;
if ($name !== null) require_once engine.'classes/'.$name.'.php';
});
}
defined('composer') && spl_autoload_register(static function(string $class):void {
static $loaded = false;
if ($loaded) return;
$loaded = true;
require_once composer.'vendor/autoload.php';
foreach (spl_autoload_functions() as $fn){
if (is_array($fn) && ($fn[0] ?? null) instanceof \Composer\Autoload\ClassLoader){
spl_autoload_unregister($fn);
spl_autoload_register($fn);
$fn[0]->loadClass($class);
return;
}
}
});
if ($args['thread'] !== false && PHP_SAPI !== 'cli'){
ignore_user_abort(true);
$handle = static function():void { phlo_thread(); };
for ($i = 1; !$args['thread'] || $i <= $args['thread']; ++$i){
$keepRunning = frankenphp_handle_request($handle);
phlo('tech/reset');
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
gc_collect_cycles();
if (!$keepRunning) break;
}
return;
}
phlo_thread();function
phlo_async(string $cb, ...$args)
/phlo/resources/phlo.async.phlo line 9
异步执行回调函数,传递任何附加参数,如果成功则返回进程ID。
if (daemon) return daemon::fire($cb, $args)
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void).' > /dev/null 2>&1 & echo $!'
exec($cmd, $r)
return isset($r[0]) && ctype_digit($r[0]) && (int)$r[0] > 0function
phlo_cli(array $args):void
/phlo/phlo.php line 190
处理命令行参数,调度目标命令,并以JSON格式打印结果。
if (!$args) return;
$target = array_shift($args);
$result = phlo_dispatch($target, $args);
if (isset($result)) print(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf);function
phlo_dispatch(string $target, array $args = []):mixed
/phlo/phlo.php line 177
根据提供的目标字符串,将方法调用分发到对象或类的静态方法,可选择性地传递参数。
if (str_contains($target, dot)){
[$object, $method] = explode(dot, $target, 2);
$handle = phlo($object);
return $args ? $handle->$method(...$args) : ($handle->hasMethod($method) ? $handle->$method() : $handle->$method);
}
if (str_contains($target, '::')){
[$class, $method] = explode('::', $target, 2);
return $class::$method(...$args);
}
return $target(...$args);function
phlo_exception(Throwable $e):void
/phlo/phlo.php line 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 8
检查指定的 PHP 文件是否存在于给定的对象路径中。
is_file(php.strtr($obj, [us => dot]).'.php')function
phlo_load(bool $http):void
/phlo/phlo.php line 153
加载应用程序所需的运行时文件,确保应用程序仅加载一次,并为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_serve():void
/phlo/phlo.php line 197
启动一个 Phlo 服务器,监听传入消息,处理这些消息并以 JSON 格式发送响应。它处理同步和异步请求,管理输出流和错误报告。
ini_set('display_errors', 'stderr');
stream_set_blocking(STDIN, true);
fwrite(STDOUT, json_encode(['t' => 'ready']).lf);
while (($line = fgets(STDIN)) !== false){
$line = trim($line);
if ($line === void) continue;
$msg = json_decode($line, true) ?: [];
$id = $msg['id'] ?? null;
$target = (string)($msg['target'] ?? void);
$args = (array)($msg['args'] ?? []);
$stream = (bool)($msg['stream'] ?? false);
$lineBuf = void;
$emit = static function(string $chunk) use (&$lineBuf, $id):string {
$lineBuf .= $chunk;
while (($pos = strpos($lineBuf, lf)) !== false){
$out = substr($lineBuf, 0, $pos);
$lineBuf = substr($lineBuf, $pos + 1);
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'line', 'data' => $out], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf);
}
return void;
};
try {
if ($target === void) error('No target');
if ($stream){
ob_start($emit, 1);
$result = phlo_dispatch($target, $args);
while (ob_get_level()) ob_end_flush();
if ($lineBuf !== void) fwrite(STDOUT, json_encode(['id' => $id, 't' => 'line', 'data' => $lineBuf], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf);
}
else $result = phlo_dispatch($target, $args);
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'done', 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf);
}
catch (Throwable $e){
while (ob_get_level()) ob_end_clean();
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'error', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf);
}
phlo('tech/reset');
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
gc_collect_cycles();
}function
phlo_stream(string $cb, ...$args)
/phlo/resources/phlo.stream.phlo line 10
从回调函数流式输出,允许异步处理数据。它可以在守护进程模式下运行或在CLI上下文中执行命令。
if (daemon){
yield from daemon::stream($cb, $args)
return
}
yield from exec_stream(cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void))function
phlo_sync(string $cb, ...$args)
/phlo/resources/phlo.sync.phlo line 9
以同步方式执行回调函数,并将任何附加参数传递给它,返回结果作为 JSON 对象或在执行失败时的错误消息。
if (daemon) return daemon::run($cb, $args)
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void)
exec($cmd.' 2>&1', $r, $code)
$out = implode(lf, $r)
if ($code !== 0) error('Could not execute "'.esc($cb).'" via CLI')
$j = json_decode($out, true)
if (json_last_error() !== JSON_ERROR_NONE) return $out
if (is_array($j) && isset($j['error'])) error($j['error'])
return $jfunction
phlo_thread():void
/phlo/phlo.php line 115
处理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') 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
setting(?string $key = null, $value = null)
/phlo/resources/settings.phlo line 10
$store = %JSON('settings', assoc: true)
if ($key === null) return $store->objData
if (func_num_args() > 1){
$store->$key = $value
$store->objChanged && $store->objWrite($store->objData)
}
return $store->$keyfunction
slug(string $text)
/phlo/resources/slug.phlo line 8
将给定字符串转换为URL友好的slug,通过删除非字母数字字符、转换为小写并用短横线替换空格。
trim(preg_replace('/[^a-z0-9]+/', dash, strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text))), dash)function
stream($data = null, string $type = 'application/octet-stream', ?string $name = null):void
/phlo/resources/stream.phlo line 10
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid stream()')
if (!$res->streaming){
$res->streaming = true
$res->type = $type
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->header('X-Accel-Buffering', 'no')
$name === null || $res->header('Content-Disposition', 'attachment; filename="'.str_replace('"', '', $name).'"')
$res->render()
}
if ($data === null) return
foreach (is_iterable($data) ? $data : [$data] as $part){
print((string)$part)
$cli || [@ob_flush(), flush()]
}function
tag(string $tagName, ?string $inner = null, ...$args)
/phlo/resources/tag.phlo line 9
生成一个具有指定名称、可选内部内容和附加属性的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 8
将给定的时间戳转换为人类可读的时间差格式,例如 '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 = daemon, ...$data)
/phlo/resources/wsCast.phlo line 9
使用提供的主机和端口向指定目标或所有目标发送WebSocket消息。
HTTP (
'http://127.0.0.1:'.$wsPort.'/message',
JSON: true,
POST: arr (
host: $wsHost,
target: $wsTarget,
data: $data,
),
)