Core
object
%cookies
/phlo/resources/cookies.phlo
method
%cookies -> controller
line 10
该控制器检索当前的cookie状态并将其分配给objData属性。
this->objData = $_COOKIEprop
%cookies -> lifetimeDays:int
line 12
设置 cookies 的生命周期(以天为单位)。
180method
%cookies -> objSet ($key, $value, array $options = []):bool
line 14
使用指定的键和值设置一个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):void
line 22
通过从本地对象数据和全局 $_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):string
line 11
使用指定的参数格式将给定文本翻译成荷兰语。
%lang->translation('nl', $text, ...$args)function
function en ($text, ...$args):string
line 12
此函数检索指定文本的英文翻译,并可以选择性地使用附加参数进行格式化。
%lang->translation('en', $text, ...$args)static
lang :: asyncBatch ($from, $to, $json):void
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:string
line 23
此函数检索与指定语言标识符相关联的模型。
'gpt-4o-mini'prop
%lang -> instructions
line 24
定义了 Phlo 编程语言的一组指令。
voidstatic
lang :: fileCache:array
line 25
lang::$fileCache 是一个静态属性,用于存储缓存的语言文件,以便在运行时高效检索。
[]method
%lang -> file ($lang):string
line 27
获取指定语言的配置文件,格式为 'langs.$lang.ini'。
langs.$lang.'.ini'method
%lang -> escape ($value):string
line 29
对字符串中的特殊字符进行转义,以便安全地输出到HTML中,将反斜杠、双引号和换行符替换为各自的转义序列。
strtr((string)$value, [bs => bs.bs, dq => bs.dq, lf => '\n'])method
%lang -> unescape ($value):string
line 30
此函数通过用相应的字符替换转义序列来解码给定字符串。
strtr(strtr($value, [bs.bs => "\x01", bs.dq => dq, '\n' => lf]), ["\x01" => bs])method
%lang -> lineValue ($line, $eq):string
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):array
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):?string
line 54
Finds one line with a binary search over the raw bytes of the translation file.
$size = (int)@filesize($file)
if (!$size) return null
$h = @fopen($file, 'rb')
if (!$h) return null
$lo = 0
$hi = $size
while ($hi - $lo > 4096){
$mid = intdiv($lo + $hi, 2)
fseek($h, $mid)
fgets($h)
$pos = ftell($h)
if ($pos >= $hi){
$hi = $mid
continue
}
$line = (string)fgets($h)
$eq = strpos($line, ' = ')
if ($eq === false){
$hi = $mid
continue
}
$cmp = strcmp(substr($line, 0, $eq), $hash)
if ($cmp < 0) $lo = ftell($h)
elseif ($cmp > 0) $hi = $pos
else {
fclose($h)
return $this->lineValue($line, $eq)
}
}
fseek($h, $lo)
$value = null
while (ftell($h) < $hi && ($line = fgets($h)) !== false){
$eq = strpos($line, ' = ')
if ($eq === false) continue
$cmp = strcmp(substr($line, 0, $eq), $hash)
if ($cmp > 0) break
if ($cmp === 0){
$value = $this->lineValue($line, $eq)
break
}
}
fclose($h)
return $valuemethod
%lang -> lookup ($hash):?string
line 100
根据提供的哈希值在语言文件缓存中查找值,如果文件已更改,则更新缓存。
$file = $this->file(%app->lang)
$mtime = (int)@filemtime($file)
$cache =& static::$fileCache[$file]
if (!$cache || $cache['mtime'] !== $mtime) $cache = ['mtime' => $mtime, 'items' => []]
if (array_key_exists($hash, $cache['items'])) return $cache['items'][$hash]
$value = $this->search($file, $hash)
if ($value === null && $mtime) $value = $this->readAll($file)[$hash] ?? null
return $cache['items'][$hash] = $valuemethod
%lang -> save ($lang, $pairs):void
line 115
Writes the whole file under a temporary name and renames it into place.
$file = $this->file($lang)
$items = $this->readAll($file)
foreach ($pairs AS $hash => $value) $items[$hash] = $value
ksort($items, SORT_STRING)
$out = void
foreach ($items AS $hash => $value) $out .= $hash.' = '.dq.$this->escape($value).dq.lf
$tmp = $file.'.'.getmypid().'.tmp'
file_put_contents($tmp, $out, LOCK_EX)
@chmod($tmp, 0664)
rename($tmp, $file)
unset(static::$fileCache[$file])method
%lang -> transContext:string
line 129
如果可用,则从应用程序作者那里检索有关目的和领域的上下文;否则返回 void。
($instr = trim($this->instructions ?? void)) !== void ? lf.'Context from the app author about purpose and domain: '.$instr : voidprop
%lang -> browser:?string
line 131
从 '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:?string
line 132
从 cookies 中获取语言偏好,并检查它是否是应用程序可用语言中的有效选项,如果有效则返回该语言,否则返回 null。
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : nullmethod
%lang -> detect ($text, $fallback = 'en'):string
line 133
检测给定文本的语言并返回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):string
line 142
根据提供的文本和指定语言的前缀生成哈希值。
$from.($short = substr(implode(regex_all('/[A-Za-z0-9]+/', ucwords($text))[0]), 0, 8)).substr(md5($text), 0, 10 - strlen($short))method
%lang -> translation ($from, $text, ...$args):string
line 143
将给定文本从指定语言翻译为应用程序当前语言,并异步处理缺失的翻译。
if ($from === %app->lang) $translation = strtr($text, ['\n' => lf])
else {
$translation = []
$missing = []
foreach (explode(lf, $text) AS $line){
if (trim($line)){
$hash = $this->hash($from, $line)
$item = $this->lookup($hash)
if ($item === null) [$missing[$hash] = $item = $line, debug(%app->lang.': '.(strlen($line) > 20 ? substr($line, 0, 18).'...' : $line))]
}
else $item = void
$translation[] = $item
}
if ($missing) phlo_async('lang::asyncBatch', $from, %app->lang, json_encode($missing))
$translation = implode(lf, $translation)
}
return $args ? sprintf($translation, ...$args) : $translationmethod
%lang -> translate ($from, $to, $text):string
line 162
使用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):array
line 171
使用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
%lastmod
/phlo/resources/lastmod.phlo
static
lastmod :: file ($dir = null):string
line 13
Where the map lives, beside the generated PHP so it travels with a release.
($dir ?: php).'lastmod.json'prop
%lastmod -> sources:array
line 17
Sources the convention cannot find, as a uri to file path map.
[]static
lastmod :: resolve ($uri):?string
line 20
The file most likely to render this page.
$slug = trim((string)$uri, slash)
$names = $slug === void ? ['home.phlo', 'page.home.phlo'] : [$slug.'.phlo', 'page.'.$slug.'.phlo', strtr($slug, [slash => dot]).'.phlo']
foreach ($names AS $name) if (is_file($file = app.$name)) return $file
return nullstatic
lastmod :: day ($file):?string
line 27
$file && is_file($file) ? date('Y-m-d', filemtime($file)) : nullstatic
lastmod :: uriOf ($page):string
line 29
is_string($page) ? $page : (string)(is_array($page) ? ($page['uri'] ?? void) : ($page->uri ?? void))static
lastmod :: stamp ($dir = null):array
line 31
$map = []
foreach ((array)(%app->pages ?? []) AS $page) ($d = static::day(static::resolve(static::uriOf($page)))) && $map[static::uriOf($page)] = $d
foreach ((array)%lastmod->sources AS $uri => $file) ($d = static::day($file)) && $map[$uri] = $d
ksort($map)
file_put_contents($target = static::file($dir), json_encode($map, jsonPretty))
return ['file' => $target, 'pages' => count($map)]prop
%lastmod -> map:array
line 40
is_file($file = static::file()) ? (array)json_decode((string)file_get_contents($file), true) : []method
%lastmod -> for ($uri):?string
line 42
$this->map[$uri] ?? nullobject
%manifest
/phlo/resources/manifest.phlo
route
route GET manifest.json
line 10
if (!%manifest->body) return false
manifest::output(%manifest->body)prop
%manifest -> body
line 15
nullprop
%manifest -> maxAge:int
line 16
60static
manifest :: encode ($body):string
line 18
json_encode($body, jsonPretty)static
manifest :: output ($body, $maxAge = null):void
line 20
%res->header('Cache-Control', 'public, max-age='.($maxAge ?? %manifest->maxAge))
output(static::encode($body), type: 'application/manifest+json')view
%manifest -> view
line 25
manifest::encode(%manifest->body)view
%manifest -> head
line 27
<link rel=manifest href=/manifest.json>object
%manual
/phlo/resources/manual.phlo
static
manual :: instruction
line 11
'Summarise what this application does and what has changed recently. Write in the same language as the description, at most eight sentences, running text without lists or headings. Do not open with a sentence about what you are going to do.'prop
%manual -> translate:bool
line 13
function_exists('en')prop
%manual -> labels:array
line 14
arr(
title: 'Manual',
lead: 'This page comes from the source itself: the description, what the code exposes and the latest changes.',
state: 'State',
unknown: 'unknown',
summary: 'In short',
about: 'What this app is',
files: 'Files',
routes: 'Routes',
changes: 'Recent changes',
why: 'Why',
own: 'Codebase',
shared: 'Shared with the sibling app',
noKey: 'No AI key is configured, so there is no summary. The rest of this page comes straight from the source and works without one.',
noAnswer: 'The model returned nothing.',
noSummary:'The summary could not be fetched: ',
noInfo: 'This app does not carry a data/app.md yet.',
noNodes: 'This file carries no nodes; it consists of markup or script.',
)method
%manual -> label ($key):string
line 33
$this->translate ? en($this->labels[$key]) : $this->labels[$key]static
manual :: appInfo:string
line 35
(string)(reflect::appInfo() ?: void)static
manual :: commits (string $path, int $limit = 12):array
line 37
if (!$path || !is_dir($path)) return []
$field = perc.'x1f'
$record = perc.'x1e'
$format = perc.'h'.$field.perc.'ad'.$field.perc.'s'.$field.perc.'b'.$record
$command = 'git -C '.escapeshellarg($path).' log -'.(int)$limit.' --no-merges --date=iso --format='.$format.' 2>/dev/null'
$raw = (string)@shell_exec($command)
$out = []
foreach (array_filter(explode("\x1e", $raw), 'trim') AS $entry){
$parts = explode("\x1f", trim($entry))
if (count($parts) < 3) continue
$when = strtotime($parts[1]) ?: 0
$out[] = obj(
hash: $parts[0],
when: $when,
date: $when ? date('d-m-Y H:i', $when) : $parts[1],
subject: $parts[2],
body: trim((string)($parts[3] ?? void)),
)
}
return $outstatic
manual :: repos:array
line 60
$path = app
$dirs = [%manual->label('own') => $path]
$config = json_decode((string)@file_get_contents($path.'data/app.json'), true)
foreach ((array)($config['paths']['resources'] ?? []) AS $dir){
$dir = rtrim((string)$dir, slash).slash
$label = is_dir($dir) ? static::dirLabel($dir) : void
if ($label === void || isset($dirs[$label])) continue
$dirs[$label] = $dir
}
$roots = []
$out = []
foreach ($dirs AS $label => $dir){
$root = trim((string)@shell_exec('git -C '.escapeshellarg($dir).' rev-parse --show-toplevel 2>/dev/null'))
if ($root === void || isset($roots[$root])) continue
$roots[$root] = true
$out[$label] = $dir
}
return $outstatic
manual :: commitList (int $limit = 20):array
line 81
$all = []
foreach (static::repos() AS $label => $dir){
foreach (static::commits($dir) AS $commit){
$commit->origin = $label
$all[] = $commit
}
}
usort($all, fn($a, $b) => $b->when <=> $a->when)
return array_slice($all, 0, $limit)static
manual :: head:string
line 93
$commits = static::commits(app, 1)
return (string)($commits[0]->hash ?? void)static
manual :: routes:array
line 98
(array)reflect::compactRoutes()static
manual :: routeGroups:array
line 100
$groups = []
foreach (static::routes() AS $route){
$file = basename((string)($route['file'] ?? 'unknown'))
$groups[$file][] = $route
}
ksort($groups)
return $groupsstatic
manual :: nodesByFile:array
line 110
if (isset(%req->docsNodes)) return (array)%req->docsNodes
$found = []
foreach (['route', 'view', 'static', 'method', 'prop'] AS $type){
foreach ((array)reflect::find($type, null, true, 'all') AS $row){
$row = (array)$row
$file = basename((string)($row['file'] ?? void), '.phlo')
if ($file === void) continue
$found[$file][] = obj(
type: $type,
name: (string)($row['name'] ?? void),
args: (string)($row['args'] ?? void),
ret: (string)($row['type'] ?? void),
summary: (string)($row['summary'] ?? void),
)
}
}
%req->docsNodes = $found
return $foundstatic
manual :: fileRow (string $file):obj
line 131
$meta = (new build_file($file))->meta
$key = basename($file, '.phlo')
return obj(
name: basename($file),
summary: trim((string)($meta['summary'] ?? void)),
advice: trim((string)($meta['advice'] ?? void)),
lines: count(file($file) ?: []),
nodes: static::nodesByFile()[$key] ?? [],
)static
manual :: sources:array
line 143
$path = app
$own = %manual->label('own')
$config = json_decode((string)@file_get_contents($path.'data/app.json'), true)
if (!is_array($config)) return []
$dirs = [$path => $own]
foreach ((array)($config['paths']['resources'] ?? []) AS $dir){
$dir = rtrim((string)$dir, slash).slash
if (!is_dir($dir)) continue
$label = static::dirLabel($dir)
if ($label !== void) $dirs[$dir] = $label
}
$groups = []
foreach ($dirs AS $dir => $label) $groups[$label] = []
foreach (glob($path.'*.phlo') ?: [] AS $file) $groups[$own][] = static::fileRow($file)
foreach ((array)($config['resources'] ?? []) AS $name){
foreach ($dirs AS $dir => $label){
if ($label === $own) continue
$file = $dir.$name.'.phlo'
if (!is_file($file)) continue
$groups[$label][] = static::fileRow($file)
break
}
}
return array_filter($groups)static
manual :: dirLabel (string $dir):string
line 172
$root = trim((string)@shell_exec('git -C '.escapeshellarg($dir).' rev-parse --show-toplevel 2>/dev/null'))
$marker = $root === void ? null : json_decode((string)@file_get_contents($root.slash.'layer.json'), true)
if (is_array($marker) && ($marker['docs'] ?? true) !== false){
$label = trim((string)($marker['label'] ?? void))
if ($label !== void) return $label
preg_match('~^#\s+(.+)~m', (string)@file_get_contents($root.slash.'README.md'), $match)
if (($label = trim((string)($match[1] ?? void))) !== void) return $label
}
if (str_starts_with(rtrim($dir, slash), rtrim(dirname(rtrim(app, slash)), slash))) return %manual->label('shared')
return voidstatic
manual :: cacheFile:string
line 185
data.'manual.json'static
manual :: cached:?obj
line 187
$file = static::cacheFile()
if (!is_file($file)) return null
$row = json_decode((string)file_get_contents($file))
return $row instanceof \stdClass ? obj(...(array)$row) : nullstatic
manual :: configured:bool
line 194
if (!class_exists('AI') || !class_exists('creds')) return false
return (string)(%creds->OpenAI ?? void) !== void || (string)(%creds->Claude ?? void) !== voidstatic
manual :: appName:string
line 199
defined('id') ? (string)id : (string)%app->titlestatic
manual :: model:string
line 201
$set = class_exists('setting') ? (string)(setting::value('docs.model') ?: void) : void
return $set ?: 'gpt-5.4-mini'static
manual :: summary:obj
line 206
$info = static::appInfo()
$head = $info === void ? void : md5($info)
$row = static::cached()
if ($row && (string)$row->hash === $head && $head !== void) return $row
if (!static::configured()) return obj(hash: $head, text: void, missing: true)
$lines = ['This is the description of the app:']
$lines[] = mb_substr(static::appInfo(), 0, 4000)
$lines[] = 'And these are the latest changes:'
foreach (static::commits(app) AS $commit) $lines[] = $commit->date.space.$commit->subject
try {
$answer = phlo('AI')->chat(model: static::model(), system: static::$instruction, user: implode(lf, $lines))
$text = trim((string)($answer->answer ?? void))
}
catch (\Throwable $e){
return obj(hash: $head, text: void, error: mb_substr($e->getMessage(), 0, 200))
}
if ($text === void) return obj(hash: $head, text: void, error: %manual->label('noAnswer'))
@file_put_contents(static::cacheFile(), json_encode(['hash' => $head, 'text' => $text, 'written' => time()]))
return obj(hash: $head, text: $text)static
manual :: pageFile:string
line 229
data.'manual.html'static
manual :: store (string $body, string $page):bool
line 231
$mark = '<!-- manual '.md5($body).' -->'
$file = static::pageFile()
if (is_file($file) && str_contains((string)@file_get_contents($file), $mark)) return false
return (bool)@file_put_contents($file, preg_replace('~<meta name="csrf"[^>]*>\s*~', void, $page).lf.$mark.lf)static
manual :: mdBlocks (string $md):array
line 238
$lines = explode(lf, str_replace(cr, void, $md))
$count = count($lines)
$blocks = []
$para = []
$i = 0
while ($i < $count){
$line = $lines[$i]
if (trim($line) === void){
static::mdPara($para, $blocks)
$i++
continue
}
if (preg_match('/^ {0,3}(`{3,}|~{3,})/', $line, $match)){
static::mdPara($para, $blocks)
$fence = '/^ {0,3}'.preg_quote($match[1][0], slash).'{3,}\s*$/'
$body = []
$i++
while ($i < $count && !preg_match($fence, $lines[$i])){
$body[] = $lines[$i]
$i++
}
$i++
$blocks[] = obj(type: 'code', text: implode(lf, $body))
continue
}
if (preg_match('/^ {0,3}(#{1,6})\s+(.*?)\s*#*$/', $line, $match)){
static::mdPara($para, $blocks)
$blocks[] = obj(type: 'heading', depth: min(4, max(2, strlen($match[1]))), text: trim($match[2]))
$i++
continue
}
if (preg_match('/^ {0,3}([-*_])(?:\s*\1){2,}\s*$/', $line)){
static::mdPara($para, $blocks)
$blocks[] = obj(type: 'hr')
$i++
continue
}
if (preg_match('/^ {0,3}>/', $line)){
static::mdPara($para, $blocks)
$body = []
while ($i < $count && preg_match('/^ {0,3}>\s?(.*)$/', $lines[$i], $match)){
$body[] = $match[1]
$i++
}
$blocks[] = obj(type: 'quote', text: trim(implode(lf, $body)))
continue
}
if (str_contains($line, pipe) && preg_match('/^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/', (string)($lines[$i + 1] ?? void))){
static::mdPara($para, $blocks)
$head = static::mdCells($line)
$rows = []
$i += 2
while ($i < $count && str_contains($lines[$i], pipe)){
$rows[] = static::mdCells($lines[$i])
$i++
}
$blocks[] = obj(type: 'table', head: $head, rows: $rows)
continue
}
if (($marker = static::mdMarker($line)) !== null){
static::mdPara($para, $blocks)
$raw = []
while ($i < $count){
if (trim($lines[$i]) === void){
if (static::mdBreaks(static::mdMarker((string)($lines[$i + 1] ?? void)), $marker)) break
$raw[] = void
$i++
continue
}
$here = static::mdMarker($lines[$i])
if ($here === null && !preg_match('/^ {2,}\S/', $lines[$i])) break
if ($here !== null && static::mdBreaks($here, $marker)) break
$raw[] = $lines[$i]
$i++
}
$blocks[] = obj(type: 'list', ordered: $marker->ordered, items: static::mdItems($raw, $marker->indent))
continue
}
$para[] = $line
$i++
}
static::mdPara($para, $blocks)
return $blocksstatic
manual :: mdMarker (string $line):?obj
line 324
if (!preg_match('/^( *)([*+-]|\d{1,9}\.)\s+/', $line, $match)) return null
return obj(indent: strlen($match[1]), ordered: ctype_digit($match[2][0]))static
manual :: mdBreaks (?obj $here, obj $marker):bool
line 329
$here === null || ($here->indent <= $marker->indent && $here->ordered !== $marker->ordered)static
manual :: mdPara (array &$para, array &$blocks):void
line 331
$text = trim(implode(lf, $para))
$para = []
if ($text !== void) $blocks[] = obj(type: 'para', text: $text)static
manual :: mdCells (string $line):array
line 337
array_map('trim', explode(pipe, trim(trim($line), pipe)))static
manual :: mdItems (array $lines, int $indent):array
line 339
$groups = []
$group = null
foreach ($lines AS $line){
if (preg_match('/^( *)([*+-]|\d{1,9}\.)\s+(.*)$/', $line, $match) && strlen($match[1]) <= $indent){
if ($group !== null) $groups[] = $group
$group = [$match[3]]
continue
}
if ($group === null) continue
$group[] = preg_replace('/^ {1,'.($indent + 2).'}/', void, $line)
}
if ($group !== null) $groups[] = $group
$items = []
foreach ($groups AS $group){
$raw = trim(implode(lf, $group))
$checked = null
if (preg_match('/^\[([ xX])\]\s*(.*)$/s', $raw, $match)){
$checked = strtolower($match[1]) === 'x'
$raw = $match[2]
}
$sub = static::mdBlocks($raw)
$text = void
if ($sub && $sub[0]->type === 'para') $text = array_shift($sub)->text
$items[] = obj(text: $text, checked: $checked, blocks: $sub)
}
return $itemsstatic
manual :: mdInline (string $text):string
line 368
$out = void
foreach (preg_split('/(`+[^`]*`+)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE) AS $index => $part){
if ($index % 2) $out .= '<code>'.esc(trim($part, bt)).'</code>'
else $out .= static::mdFormat($part)
}
return $outstatic
manual :: mdFormat (string $text):string
line 377
$out = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', esc($text))
$out = preg_replace('/(?<![*\w])\*([^*\n]+)\*(?![*\w])/', '<em>$1</em>', $out)
return preg_replace('/\[([^\]]+)\]\(\s*((?:https?:|mailto:|[\/#])\S*?)\s*\)/', '<a href="$2">$1</a>', $out)route
route both GET manual
line 383
%app->title = static::appName()
%app->css = []
%app->js = []
%app->defer = []
$body = %manual->page()
$page = view($body, title: %manual->label('title'), css: [], js: [], inline: true, ns: 'manual')
static::store($body, $page)
return $pageview
%manual -> page
line 394
<main.docs>
<header.docs__head>
<h1>{{ manual::appName() }}</h1>
<p.docs__sub>{{ %manual->label('lead') }} {{ %manual->label('state') }} {{ static::head() ?: %manual->label('unknown') }}.</p>
</header>
<div#docs-summary>{{ %manual->summaryBlock }}</div>
{{ %manual->infoBlock }}
{{ %manual->filesBlock }}
{{ %manual->routesBlock }}
{{ %manual->commitsBlock }}
</main>view
%manual -> summaryBlock
line 407
$row = static::summary()
if ($row->missing) return indentView(lf.'<section class="docs__box docs__box--quiet"><p>'.esc(%manual->label('noKey')).'</p></section>'.lf)
if ((string)($row->error ?? void) !== void) return indentView(lf.'<section class="docs__box docs__box--quiet"><p>'.esc(%manual->label('noSummary')).esc((string)$row->error).'</p></section>'.lf)
return indentView(lf.$this->summaryText($row).lf)view
%manual -> summaryText ($row)
line 414
<section.docs__box>
<h2>{{ %manual->label('summary') }}</h2>
<p.docs__lead>{{ $row->text }}</p>
</section>view
%manual -> infoBlock
line 420
$info = static::appInfo()
if ($info === void) return indentView(lf.'<section class="docs__box"><p>'.esc(%manual->label('noInfo')).'</p></section>'.lf)
return indentView(lf.$this->infoText(static::mdBlocks(preg_replace('~^#[^\n]*\n~', void, $info))).lf)view
%manual -> infoText (array $blocks)
line 426
<details.docs__box open>
<summary.docs__summary>{{ %manual->label('about') }}</summary>
<div.docs__md>
<foreach $blocks AS $block>
{{ %manual->mdBlock($block) }}
</foreach>
</div>
</details>view
%manual -> mdBlock ($block)
line 436
<if $block->type === 'heading' && $block->depth === 2>
<h2>{{ %manual->mdInline($block->text) }}</h2>
<elseif $block->type === 'heading' && $block->depth === 3>
<h3>{{ %manual->mdInline($block->text) }}</h3>
<elseif $block->type === 'heading'>
<h4>{{ %manual->mdInline($block->text) }}</h4>
<elseif $block->type === 'code'>
<pre.docs__pre>{[ $block->text ]}</pre>
<elseif $block->type === 'quote'>
<blockquote>{{ %manual->mdInline($block->text) }}</blockquote>
<elseif $block->type === 'hr'>
<hr>
<elseif $block->type === 'list'>
{{ %manual->mdList($block) }}
<elseif $block->type === 'table'>
{{ %manual->mdTable($block) }}
<else>
<p>{{ %manual->mdInline($block->text) }}</p>
</if>view
%manual -> mdList ($block)
line 457
$items = $block->items
return $block->ordered ? $this->mdOrdered($items) : $this->mdBullets($items)view
%manual -> mdBullets (array $items)
line 462
<ul>
<foreach $items AS $item>
{{ %manual->mdItem($item) }}
</foreach>
</ul>view
%manual -> mdOrdered (array $items)
line 469
<ol>
<foreach $items AS $item>
{{ %manual->mdItem($item) }}
</foreach>
</ol>view
%manual -> mdItem ($item)
line 476
<if $item->checked === true>
<li.docs__task.docs__task--done>{{ %manual->mdBody($item) }}</li>
<elseif $item->checked === false>
<li.docs__task>{{ %manual->mdBody($item) }}</li>
<else>
<li>{{ %manual->mdBody($item) }}</li>
</if>view
%manual -> mdBody ($item)
line 485
$out = static::mdInline((string)$item->text)
foreach ($item->blocks AS $block) $out .= lf.$this->mdBlock($block)
return $outview
%manual -> mdTable ($block)
line 491
<table.docs__table>
<thead>
<tr>
<foreach $block->head AS $cell>
<th>{{ %manual->mdInline($cell) }}</th>
</foreach>
</tr>
</thead>
<tbody>
<foreach $block->rows AS $row>
<tr>
<foreach $row AS $cell>
<td>{{ %manual->mdInline($cell) }}</td>
</foreach>
</tr>
</foreach>
</tbody>
</table>view
%manual -> filesBlock
line 511
$groups = static::sources()
if (!$groups) return void
return indentView(lf.$this->filesTable($groups).lf)view
%manual -> filesTable (array $groups)
line 517
<details.docs__box>
<summary.docs__summary>{{ %manual->label('files') }} ({{ array_sum(array_map('count', $groups)) }})</summary>
<foreach $groups AS $label => $files>
<h3>{{ $label }} <span.docs__num>{{ count($files) }}</span></h3>
<foreach $files AS $file>
{{ %manual->fileNodes($file) }}
</foreach>
</foreach>
</details>view
%manual -> fileNodes ($file)
line 528
<details.docs__file>
<summary.docs__summary>
<span.docs__mono>{{ $file->name }}</span>
<span.docs__filesum>{{ $file->summary ?: void }}</span>
<span.docs__num>{{ count($file->nodes) }}</span>
</summary>
<if $file->advice !== void>
<p.docs__meta>{{ $file->advice }}</p>
</if>
<if !$file->nodes>
<p.docs__meta>{{ %manual->label('noNodes') }}</p>
</if>
<foreach $file->nodes AS $node>
<div.docs__node>
<span.docs__kind>{{ $node->type }}</span>
<span.docs__mono>{{ $node->name }}{( $node->args !== void ? '('.$node->args.')' : void )}{( $node->ret !== void ? ': '.$node->ret : void )}</span>
<if $node->summary !== void>
<p.docs__meta>{{ mb_substr($node->summary, 0, 240) }}</p>
</if>
</div>
</foreach>
</details>view
%manual -> routesBlock
line 552
$groups = static::routeGroups()
if (!$groups) return void
return indentView(lf.$this->routesTable($groups).lf)view
%manual -> routesTable (array $groups)
line 558
<details.docs__box>
<summary.docs__summary>{{ %manual->label('routes') }} ({{ array_sum(array_map('count', $groups)) }})</summary>
<foreach $groups AS $file => $routes>
<h3>{{ $file }} <span.docs__num>{{ count($routes) }}</span></h3>
<foreach $routes AS $route>
{{ %manual->routeRow($route) }}
</foreach>
</foreach>
</details>view
%manual -> routeRow (array $route)
line 569
$whole = (string)($route['route'] ?? void)
$space = strpos($whole, space)
$method = $space === false ? void : substr($whole, 0, $space)
$path = $space === false ? $whole : substr($whole, $space + 1)
return indentView(lf.$this->routeLine($method, $path, (string)($route['summary'] ?? void)).lf)view
%manual -> routeLine (string $method, string $path, string $summary)
line 577
<div.docs__route>
<span.docs__origin>{{ $method ?: 'GET' }}</span>
<span.docs__path>/{{ str_replace(space, slash, trim($path)) }}</span>
<if $summary !== void>
<p.docs__meta>{{ $summary }}</p>
</if>
</div>view
%manual -> commitsBlock
line 586
$commits = static::commitList()
if (!$commits) return void
return indentView(lf.$this->commitsList($commits).lf)view
%manual -> commitsList (array $commits)
line 592
<details.docs__box>
<summary.docs__summary>{{ %manual->label('changes') }} ({{ count($commits) }})</summary>
<foreach $commits AS $commit>
<article.docs__commit>
<h4>{{ $commit->subject }}</h4>
<p.docs__meta>
<span.docs__origin>{{ $commit->origin }}</span>
{{ $commit->date }} · <span.docs__mono>{{ $commit->hash }}</span>
</p>
<if $commit->body !== void>
<details.docs__more>
<summary.docs__summary>{{ %manual->label('why') }}</summary>
<pre.docs__pre>{{ $commit->body }}</pre>
</details>
</if>
</article>
</foreach>
</details>view
style
line 612
.docs {
max-width: 54rem
margin: 0 auto
padding: 2rem 1.2rem 4rem
font: 16px/1.6 system-ui, sans-serif
color: #1d2126
}
.docs__head {
margin-bottom: 1.4rem
}
.docs h1 {
margin: 0 0 .3rem
font-size: 1.5rem
}
.docs__sub {
margin: 0 0 .9rem
color: #68727d
font-size: .92rem
}
.docs__box {
margin: 0 0 1rem
padding: 1rem 1.1rem
border: 1px solid #e2e6ea
border-radius: 10px
background: #fff
}
.docs__box--quiet {
background: #f6f8f9
color: #68727d
font-size: .92rem
}
.docs__box h2 {
margin: 0 0 .5rem
font-size: 1.05rem
}
.docs__summary {
cursor: pointer
font-weight: 600
font-size: .98rem
display: flex
align-items: baseline
gap: .5rem
list-style: none
\::-webkit-details-marker: display: none
}
.docs__summary::before {
content: '\25b8'
color: #9aa4ae
font-weight: 400
transition: transform .12s ease
}
.docs__file[open] > .docs__summary::before {
transform: rotate(90deg)
}
.docs__lead {
margin: 0 0 .8rem
}
.docs__md h2 {
margin: 1.1rem 0 .4rem
font-size: 1rem
}
.docs__md p, .docs__md ul {
margin: 0 0 .7rem
}
.docs__task {
list-style: none
margin-left: -1.1rem
}
.docs__task::before {
content: '\25cb\a0'
color: #68727d
}
.docs__task--done::before {
content: '\25cf\a0'
color: #1d2126
}
.docs__table {
width: 100%
border-collapse: collapse
font-size: .9rem
margin: .4rem 0 1rem
}
.docs__table th, .docs__table td {
padding: .28rem .5rem .28rem 0
border-bottom: 1px solid #eef1f3
vertical-align: top
}
.docs__table th {
text-align: left
color: #68727d
}
.docs__mono {
font-family: ui-monospace, monospace
font-size: .86rem
white-space: nowrap
}
.docs__num {
text-align: right
color: #68727d
}
.docs h3, .docs__md h4 {
margin: 1rem 0 .2rem
font-size: .9rem
color: #68727d
}
.docs__file {
margin: .15rem 0
padding: .3rem .5rem
border: 1px solid #eef1f3
border-radius: 8px
}
.docs__filesum {
color: #68727d
font-size: .86rem
font-weight: 400
margin-left: auto
text-align: right
}
.docs__node {
padding: .3rem 0 .3rem .8rem
border-left: 2px solid #eef1f3
margin: .3rem 0
}
.docs__kind {
display: inline-block
min-width: 4rem
color: #68727d
font-size: .78rem
text-transform: uppercase
}
.docs__origin {
display: inline-block
margin-right: .4rem
padding: .05rem .4rem
border-radius: 4px
background: #eef1f3
color: #4a5560
font-size: .74rem
text-transform: uppercase
letter-spacing: .02em
}
.docs__route {
padding: .3rem 0
border-bottom: 1px solid #eef1f3
}
.docs__path {
font-family: ui-monospace, monospace
font-size: .86rem
word-break: break-word
}
.docs__commit {
padding: .6rem 0
border-bottom: 1px solid #eef1f3
}
.docs__commit h4 {
margin: 0 0 .15rem
font-size: .95rem
}
.docs__meta {
margin: 0
color: #68727d
font-size: .84rem
}
.docs__pre {
margin: .4rem 0 0
padding: .6rem .7rem
border-radius: 8px
background: #f6f8f9
font-family: ui-monospace, monospace
font-size: .84rem
white-space: pre-wrap
}
.docs__more {
margin-top: .3rem
}
.docs__btn {
padding: .35rem .8rem
border: 1px solid #d6dade
border-radius: 6px
background: #fff
font-size: .88rem
cursor: pointer
}object
%payload
/phlo/resources/payload.phlo
method
%payload -> controller
line 11
contentType = %req->contentType
if (in_array(phlo('req')->method, ['POST', 'PUT', 'PATCH', 'QUERY']) && str_starts_with($contentType, 'application/json')){
$data = json_decode((string)file_get_contents('php://input'))
return $this->objData = is_object($data) ? get_object_vars($data) : (is_array($data) ? $data : [])
}
if ($_POST) loop($_POST, fn($value, $key) => $this->$key = $value)
elseif (in_array(phlo('req')->method, ['PUT', 'PATCH', 'QUERY']) && str_starts_with($contentType, 'application/x-www-form-urlencoded')){
$body = file_get_contents('php://input')
$data = []
parse_str($body, $data)
if ($data) loop($data, fn($value, $key) => $this->$key = $value)
}
elseif (in_array(phlo('req')->method, ['PUT', 'PATCH', 'QUERY']) && str_starts_with($contentType, 'multipart/form-data')){
$match = regex('/boundary="?([^";]+)"?/', $contentType)
if (!$match) return
$boundary = '--'.$match[1]
$arrays = []
$raw = file_get_contents('php://input')
foreach (explode($boundary, $raw) AS $part){
if (!trim($part) || $part === '--' || !str_contains($part, nl.nl)) continue
$headers = []
[$rawHeaders, $body] = explode(nl.nl, $part, 2)
foreach (explode(nl, trim($rawHeaders)) AS $header){
if (str_contains($header, colon)){
[$key, $value] = explode(colon, $header, 2)
$headers[strtolower(trim($key))] = trim($value)
}
}
if (!isset($headers['content-disposition'])) continue
if (!preg_match('/name="([^"]+)"/', $headers['content-disposition'], $match)) continue
$name = $match[1]
$body = rtrim($body, nl)
if ($body === void) $body = null
$base = $name
$keys = []
$hasEmptyIndex = false
if (preg_match('/^([^\[]+)((?:\[[^\]]*\])*)$/', $name, $m)){
$base = $m[1]
$brackets = $m[2]
if ($brackets){
preg_match_all('/\[([^\]]*)\]/', $brackets, $mm)
$keys = $mm[1]
$hasEmptyIndex = in_array(void, $keys, true)
}
}
if ($hasEmptyIndex) $arrays[] = $base
$assign = function($value) use ($base, $keys){
if ($keys){
if (!isset($this->objData[$base]) || !is_array($this->objData[$base])) $this->objData[$base] = []
$ref =& $this->objData[$base]
$count = count($keys)
foreach ($keys AS $i => $k){
$last = $i === $count - 1
if ($k === void){
if ($last) $ref[] = $value
else {
$ref[] = []
end($ref)
$idx = key($ref)
$ref =& $ref[$idx]
}
}
else {
if ($last) $ref[$k] = $value
else {
if (!isset($ref[$k]) || !is_array($ref[$k])) $ref[$k] = []
$ref =& $ref[$k]
}
}
}
}
else $this->objData[$base] = $value
};
if (preg_match('/filename="([^"]*)"/', $headers['content-disposition'], $f)){
if ($f[1] === void || $body === null){
if (!$hasEmptyIndex) $assign(null)
continue
}
$filename = $f[1]
$file = %file(tempnam(sys_get_temp_dir(), 'phlo'), $filename, $body)
$assign($file)
}
else $assign($body)
}
foreach ($this->objData AS $key => $val){
if (str_ends_with($key, '[]')){
unset($this->objData[$key])
$this->objData[substr($key, 0, -2)] = is_array($val) ? array_values(array_filter($val, fn($v) => $v !== null)) : [$val]
}
elseif (!is_array($val) && substr($key, -2) === '[]') $this->objData[$key] = [$val]
}
foreach (array_unique($arrays) AS $key) if (!isset($this->objData[$key])) $this->objData[$key] = []
}
if ($_FILES) loop($_FILES, fn($f, $key) => $this->$key = is_array($f['name']) ? loop(array_keys($f['name']), fn($i) => $f['error'][$i] ? null : %file($f['tmp_name'][$i], $f['name'][$i], mime: $f['type'][$i], size: $f['size'][$i])) : ($f['error'] ? null : %file($f['tmp_name'], $f['name'], mime: $f['type'], size: $f['size'])))object
%seo
/phlo/resources/seo.phlo
route
route GET sitemap.xml
line 11
该路由在通过GET请求访问时生成用于SEO的站点地图。
output($this)route
route GET robots.txt
line 13
定义一个路由,响应GET请求,通过以UTF-8字符集的纯文本格式输出robots文件的内容。
output($this->robots(), type: 'text/plain; charset=utf-8')method
%seo -> robots:string
line 15
根据应用程序的设置生成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):string
line 23
从应用程序的 slugs 中检索 URI 的国际化版本,如果未找到翻译,则返回原始 URI。
(%app->slugs ?? [])[$uri] ?? $urimethod
%seo -> uri ($page):string
line 28
A sitemap entry is a uri string, or an object carrying that uri plus more.
is_string($page) ? $page : (string)($this->field($page, 'uri') ?? void)method
%seo -> field ($page, $key):mixed
line 30
if (is_string($page)) return null
return is_array($page) ? ($page[$key] ?? null) : ($page->$key ?? null)method
%seo -> lastmod ($page):?string
line 40
The date for one entry, or nothing at all when it does not parse.
$mod = $this->field($page, 'lastmod')
($mod === null || $mod === void) && class_exists('lastmod') && $mod = %lastmod->for($this->uri($page))
if ($mod === null || $mod === void) return null
if (is_string($mod) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $mod)) return $mod
$ts = is_int($mod) ? $mod : strtotime((string)$mod)
return $ts ? date('c', $ts) : nullmethod
%seo -> locale:string
line 49
此函数检索与应用程序语言设置对应的区域代码,如果未设置,则默认为'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:string
line 55
这使用 title() 函数检索用于 SEO 目的的 Open Graph 标题。
title()prop
%seo -> ogDescr
line 56
获取应用程序的描述,如果未设置则返回 void。
%app->description ?? voidprop
%seo -> ogImageFile:string
line 57
(string)(%app->image ?? (is_file(www.'icon.webp') ? 'icon.webp' : void))prop
%seo -> ogImage:string
line 58
$this->ogImageFile === void ? void : %req->base.slash.ltrim($this->ogImageFile, slash)prop
%seo -> canonical:string
line 59
这从请求对象中检索用于SEO目的的规范URL。
%req->urlprop
%seo -> ogType:string
line 60
为SEO目的设置Open Graph类型,具体为'website'。
'website'prop
%seo -> sitemapPages:array
line 61
(array)(%app->pages ?? [''])prop
%seo -> sitemapLangs:array
line 62
array_keys((array)(%app->langs ?? []))prop
%seo -> siteName:string
line 63
这个表达式将应用的标题或id的值赋给变量$siteName,以用于SEO目的。
%app->title ?? idprop
%seo -> twitterCard:bool
line 64
seo->$twitterCard 属性用于配置视图的 Twitter Card 元数据,从而更好地与 Twitter 分享功能集成。
falseprop
%seo -> structuredData
line 69
Schema.org data for this page, as '@type' plus what only your app knows.
%app->structuredData ?? nullmethod
%seo -> schemaData:?array
line 71
if (!$this->structuredData) return null
$base = [
'@context' => 'https://schema.org',
'url' => $this->canonical,
'inLanguage' => %app->lang ?? 'en',
'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => $this->canonical],
'publisher' => ['@type' => 'Organization', 'name' => $this->siteName, 'url' => %req->base],
]
$this->ogDescr && $base['description'] = $this->ogDescr
$this->ogImage && $base['image'] = $this->ogImage
return array_replace($base, (array)$this->structuredData)prop
%seo -> noIndex:bool
line 84
seo->$noIndex 表达式在 %app->noLink 和 %app->noIndex 都未设置时评估为 true,否则返回 false。
%app->noLink ?? %app->noIndex ?? falseview
%seo -> view
line 86
<?xml version=1.0 encoding="UTF-8"?>
<urlset xmlns=http://www.sitemaps.org/schemas/sitemap/0.9 xmlns:xhtml=http://www.w3.org/1999/xhtml xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation=http://www.sitemaps.org/schemas/sitemap/0.9+http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd>
<foreach $this->sitemapPages AS $page>
{{ $this->page($page) }}
</foreach>
</urlset>view
%seo -> page ($page)
line 94
<url>
<loc>%req->base{( ($uri = $this->uri($page)) ?: slash )}</loc>
<if $stamp = $this->lastmod($page)>
<lastmod>$stamp</lastmod>
</if>
<foreach $this->sitemapLangs AS $lang>
<if $lang === %app->lang>
{{ $this->xlink('x-default', $uri ?: slash) }}
</if>
{{ $this->xlink($lang, $lang === %app->lang ? ($uri ?: slash) : "/$lang".($this->intl($uri) ?: void)) }}
</foreach>
</url>view
%seo -> xlink ($lang, $uri)
line 108
生成一个用于SEO目的的替代链接元素,指定当前视图的语言和URL。
<xhtml:link rel=alternate hreflang="$lang" href="%req->base$uri"{{ slash }}>view
%seo -> link ($lang, $uri)
line 109
生成一个用于SEO目的的替代链接标签,指定资源的语言和URL。
<link rel=alternate hreflang="$lang" href="%req->base$uri">view
%seo -> head
line 111
<if $this->noIndex>
<meta name=robots content=noindex,follow>
</if>
<if $this->ogDescr>
<meta name=description content="$this->ogDescr">
</if>
<meta property=og:site_name content="$this->siteName">
<meta property=og:title content="{[ $this->ogTitle ]}">
<meta property=og:description content="$this->ogDescr">
<meta property=og:type content="$this->ogType">
<meta property=og:url content="{[ $this->canonical ]}">
<if $this->ogImage>
<meta property=og:image content="$this->ogImage">
</if>
<meta property=og:locale content="$this->locale">
<if $this->twitterCard>
<meta name=twitter:card content=summary_large_image>
<meta name=twitter:title content="{[ $this->ogTitle ]}">
<meta name=twitter:description content="$this->ogDescr">
<if $this->ogImage>
<meta name=twitter:image content="$this->ogImage">
</if>
</if>
<if $data = $this->schemaData>
<script type="application/ld+json">{{ json_encode($data, JSON_UNESCAPED_UNICODE) }}</script>
</if>
<if !$this->noIndex>
<link rel=canonical href="{[ $this->canonical ]}">
</if>object
%session
/phlo/resources/session.phlo
static
session :: options:array
line 10
[]method
%session -> controller
line 12
ession_start(static::options())
$this->objData = $_SESSIONmethod
%session -> __set ($key, $value)
line 15
将指定键的会话变量设置为给定值。
$_SESSION[$key] = $this->objData[$key] = $valuemethod
%session -> __unset ($key)
line 16
从会话数据和内部对象数据中删除指定的键。
unset($this->objData[$key], $_SESSION[$key])method
%session -> __isset ($key):bool
line 17
检查由给定键标识的会话变量是否已设置且不为 null。
isset($this->objData[$key])method
%session -> objRegenerateId ($deleteOld = true):void
line 19
为当前会话重新生成会话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 11
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid stream()')
if (!$res->streaming){
$res->streaming = true
$res->type = $type
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->header('X-Accel-Buffering', 'no')
$name === null || $res->header('Content-Disposition', 'attachment; filename="'.str_replace('"', '', $name).'"')
$res->render()
}
if ($data === null) return
foreach (is_iterable($data) ? $data : [$data] as $part){
print((string)$part)
$cli || [@ob_flush(), flush()]
}view
script
line 31
const streamFilename = disposition => {
const extended = disposition.match(/(?:^|;)\s*filename\*\s*=\s*UTF-8'[^']*'([^;]+)/i)
const plain = disposition.match(/(?:^|;)\s*filename\s*=\s*(?:"((?:\\.|[^"])*)"|([^;]+))/i)
const value = extended?.[1] ?? plain?.[1] ?? plain?.[2]
if (!value) return null
const name = value.trim().replace(/\\(.)/g, '$1')
try { return decodeURIComponent(name) }
catch (_) { return name }
}
app.stream = async (path, onData = null, async = false, type = null, data = null) => {
const url = `${location.origin}/${path}`
const headers = {}
const csrf = obj('meta[name="csrf"]')?.content
csrf && (headers['X-CSRF-Token'] = csrf)
async && (headers['X-Requested-With'] = 'phlo')
let body = data
if (body !== null && !(body instanceof FormData) && !(body instanceof Blob)) [body = JSON.stringify(body), headers['Content-Type'] = 'application/json']
phlo.log(`⇣ APP.STREAM ${url}`)
const res = await fetch(url, {method: body === null ? 'GET' : 'POST', credentials: 'same-origin', headers, body})
if (!res.ok) throw new Error(`stream ${url} 🔴 ${res.status}`)
const mime = (res.headers.get('content-type') ?? '').split(';')[0].trim()
const kind = type ?? (mime === 'application/x-ndjson' ? 'ndjson' : mime === 'application/json' ? 'json' : mime === 'text/event-stream' ? 'sse' : mime.startsWith('text/') ? 'text' : 'raw')
const emit = value => onData && onData(value)
if (kind === 'json'){
const value = await res.json()
emit(value)
return value
}
if (kind === 'blob'){
const blob = await res.blob()
const name = streamFilename(res.headers.get('content-disposition') ?? '')
const value = name ? new File([blob], name) : blob
emit(value)
return value
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
const out = []
const parts = []
let text = ''
let buffer = ''
const line = raw => {
if (!raw) return
const value = JSON.parse(raw)
out.push(value)
emit(value)
}
const frame = raw => {
if (!raw.trim()) return
let event = 'message'
const data = []
raw.split('\n').forEach(l => l.startsWith('event:') ? event = l.slice(6).trim() : l.startsWith('data:') && data.push(l.slice(5).replace(/^ /, '')))
const value = {event, data: data.join('\n')}
out.push(value)
emit(value)
}
while (true){
const {done, value} = await reader.read()
if (done) break
if (kind === 'raw'){
parts.push(value)
emit(value)
continue
}
const piece = decoder.decode(value, {stream: true})
if (kind === 'text'){
text += piece
emit(piece)
continue
}
buffer += piece
if (kind === 'sse'){
const hold = buffer.endsWith('\r') ? '\r' : ''
buffer = buffer.slice(0, buffer.length - hold.length).replace(/\r\n?/g, '\n') + hold
}
const pieces = buffer.split(kind === 'sse' ? '\n\n' : '\n')
buffer = pieces.pop()
pieces.forEach(kind === 'sse' ? frame : line)
}
if (kind === 'raw') return new Blob(parts, {type: mime || 'application/octet-stream'})
const tail = decoder.decode()
if (kind === 'text'){
tail && [text += tail, emit(tail)]
return text
}
buffer += tail
buffer && (kind === 'sse' ? frame(buffer.replace(/\r\n?/g, '\n')) : line(buffer))
return out
}object
%tasks
/phlo/resources/tasks.phlo
static
tasks :: dir:string
line 10
访问任务的目录路径,特别指向'tasks/'。
data.'tasks/'static
tasks :: run:void
line 12
通过检查任务的到期状态并锁定它们以防止并发执行来执行计划任务。它保存运行细节,并在执行后将任务标记为完成。
is_dir(static::dir()) || mkdir(static::dir(), 0755, true)
$now = time()
foreach (%app->tasks ?? [] AS $name => $task){
$task = (object)$task
if (!static::due($name, $task, $now)) continue
if (!static::lock($name)) continue
$schedule = array_intersect_key((array)$task, array_flip(['every', 'daily', 'weekly']))
$do = is_string($task->do) ? $task->do : null
static::saveRun($name, $do, $schedule, static::fire($task->do))
static::markRun($name, $now)
static::unlock($name)
}static
tasks :: saveRun ($name, $do, $schedule, $return)
line 27
将运行数据保存到指定目录中的JSON文件,使用提供的名称、do、schedule和return值。
json_write(static::dir().$name.'.json', arr(do: $do, schedule: $schedule, return: $return))static
tasks :: due ($name, $task, $now):bool
line 33
daily and weekly match the exact minute the task names.
$last = static::lastRun($name)
if (isset($task->every)){
$every = preg_match('/^\d/', $task->every) ? $task->every : '1 '.$task->every
$seconds = strtotime("+$every", 0) ?: 0
return $seconds > 0 && ($now - $last) >= $seconds
}
if (isset($task->daily)){
if (date('H:i', $now) !== $task->daily) return false
return $last < strtotime('today 00:00', $now)
}
if (isset($task->weekly)){
if (date('D H:i', $now) !== date('D H:i', strtotime($task->weekly, $now))) return false
return $last < strtotime('monday this week', $now)
}
return falsestatic
tasks :: fire ($do)
line 51
执行由 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):int
line 61
从文件中检索任务的最后运行时间戳,如果文件不存在则返回0。
$file = static::dir().$name.'.last'
return is_file($file) ? (int)file_get_contents($file) : 0static
tasks :: markRun ($name, $ts):int|false
line 66
将任务最后运行的时间戳写入指定目录中以任务命名的文件,使用独占锁定以防止并发写入。
file_put_contents(static::dir().$name.'.last', (string)$ts, LOCK_EX)static
tasks :: lock ($name):bool
line 68
如果任务的锁文件不存在或超过一个小时,则创建一个锁文件。
$file = static::dir().$name.'.lock'
if (is_file($file) && (time() - filemtime($file)) < 3600) return false
touch($file)
return truestatic
tasks :: unlock ($name):bool
line 75
删除与任务相关的锁定文件,从而允许其再次执行。
@unlink(static::dir().$name.'.lock')object
%useragent
/phlo/resources/useragent.phlo
prop
%useragent -> source:?string
line 10
该表达式从请求对象中获取用户代理字符串,如果未设置则返回null。
%req->userAgent ?: nullprop
%useragent -> os:string
line 12
通过将用户代理字符串与预定义模式进行匹配来确定操作系统。
if (!$this->source) return 'Unknown'
$list = [
'Android' => '/Android/i',
'iPadOS' => '/iPad.*OS/i',
'iOS' => '/iPhone|iPod/i',
'Windows' => '/Windows NT/i',
'macOS' => '/Mac OS X/i',
'ChromeOS' => '/CrOS/i',
'Linux' => '/Linux/i',
]
foreach ($list AS $n => $r) if (preg_match($r, $this->source)) return $n
if (preg_match('/iPad/i',$this->source) && preg_match('/Mac OS X/i',$this->source)) return 'iPadOS'
return 'Unknown'prop
%useragent -> osV:string
line 28
从用户代理字符串中提取操作系统版本(如果可用),并以清洁格式返回。
if (!$this->source) return void
if (preg_match('/(?:Android|OS X|OS|Windows NT)\s*([0-9._]+)/i', $this->source, $m)){
$v = strtr($m[1], [us => dot])
$v = preg_replace('/[^0-9.].*/', void, $v)
$v = preg_replace('/(?:\.0)+$/', void, $v)
return $v
}
return voidprop
%useragent -> osFull:string
line 39
返回完整的操作系统名称及其版本(如果可用);否则返回'未知'或仅操作系统名称。
if (!$this->OS) return 'Unknown'
if ($this->OS === 'Windows') return 'Windows'
$v = $this->osV
if (!$v) return $this->OS
$short = preg_replace('/^(\d+\.\d+).*/','$1',$v)
if (preg_match('/\.0$/',$short)) $short = preg_replace('/\.0$/', void, $short)
return trim($this->OS.space.$short)prop
%useragent -> name:string
line 49
根据提供的用户代理字符串确定网页浏览器的名称。它检查各种模式以识别流行的浏览器,如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:string
line 66
从用户代理字符串中提取版本号,如果匹配特定浏览器模式,则返回清理后的版本号或在未找到匹配时返回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:string
line 77
返回完整的用户代理字符串,包括用户代理的名称和版本,格式化以排除不必要的部分。
if (!$this->name) return 'Unknown'
$v = $this->version
if (!$v) return $this->name
$short = preg_replace('/^(\d+\.\d+).*/','$1',$v)
if (preg_match('/\.0$/',$short)) $short = preg_replace('/\.0$/', void, $short)
return rtrim($this->name.space.$short)prop
%useragent -> device:string
line 86
根据存储在 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:string
line 12
'visitors::$table' 指的是与 Phlo 中的 'visitors' 资源相关联的数据库表。
'visitors'static
visitors :: columns
line 13
定义了访客资源的列,指定了在数据操作中使用的属性。
'id,token,host,page,lang,IP,browser,os,device,active_seconds,state,width,height,referrer,created,changed'static
visitors :: history:array
line 15
检索访客数量的历史记录,按日期分组并计算独特的令牌和总访问次数。
static::records(columns: 'FROM_UNIXTIME(changed, "%Y-%m-%d") AS date,COUNT(DISTINCT token) AS visitors,COUNT(id) AS visits', group: 'date', order: 'date DESC')static
visitors :: online:int
line 16
这获取在过去9秒内发生变化的独特在线访客的数量。
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 9)')static
visitors :: lastHour:int
line 17
检索在过去一小时内发生变化的独特访客(令牌)的数量。
static::item(columns: 'COUNT(DISTINCT token)', where: 'changed >= (UNIX_TIMESTAMP() - 3600)')static
visitors :: isBot (?string $ua):bool
line 19
通过将用户代理字符串与预定义的正则表达式模式进行匹配,来判断访问者是否为机器人。
if (!$ua) return false
return (bool)preg_match('/bot|crawl|spider|slurp|baiduspider|facebookexternalhit|twitterbot|linkedinbot|curl|wget|python-requests|go-http-client|java\//i', $ua)static
visitors :: parseReferrer (string $url):string
line 24
解析引用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 31
处理更新访客心跳信息的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 52
通过测量活跃时间并将心跳数据发送到服务器来跟踪页面上的用户活动,包括用户状态和可见性状态。
let curpath = app.path
let pv = phlo.token(12)
let hbTimer, activeMs = 0, activeAt = null, prev = null
const visible = () => document.visibilityState === 'visible'
const accrue = () => {
if (activeAt === null) return
activeMs += performance.now() - activeAt
activeAt = visible() ? performance.now() : null
}
const flush = beacon => {
accrue()
const consent = document.cookie.includes('cookieChoice=all')
window.name ||= phlo.token(8)
const s = Math.floor(activeMs / 1000)
activeMs -= s * 1000
const body = JSON.stringify({n: window.name, v: app.state, l: obj('html').lang ?? 'en', u: app.path, w: innerWidth, h: innerHeight, a: app.mode, p: pv, r: (r = document.referrer) ? (r === `${location.origin}/` ? null : r) : null, c: consent ? 1 : 0, s, pp: prev ? prev.p : null, ps: prev ? prev.s : null})
prev = null
fetch('/heartbeat', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body, keepalive: !!beacon})
}
const heartbeat = () => delay('heartbeat', 333, () => {
clearTimeout(hbTimer)
flush()
if (visible()) hbTimer = setTimeout(heartbeat, 20000)
})
document.addEventListener('visibilitychange', () => {
if (!visible()) return flush(true)
activeAt = performance.now()
heartbeat()
})
addEventListener('resize', heartbeat)
addEventListener('pagehide', () => flush(true))
app.updates.push(() => {
if (curpath === app.path) return
if (prev) flush()
accrue()
prev = {p: pv, s: Math.floor(activeMs / 1000)}
pv = phlo.token(12)
activeMs = 0
curpath = app.path
heartbeat()
})
if (visible()) activeAt = performance.now()
heartbeat()object
%websocket
/phlo/resources/websocket.phlo
static
websocket :: connect ($wsHost, $wsToken, $wsSocket):bool
line 10
使用指定的主机、令牌和套接字参数建立WebSocket连接。
!function_exists('wsConnect') || wsConnect($wsHost, $wsToken, $wsSocket)static
websocket :: auth ($wsHost, $wsToken, $wsSocket):bool
line 11
检查 'wsAuth' 函数是否存在,如果不存在,则使用提供的参数调用它进行 WebSocket 身份验证。
!function_exists('wsAuth') || wsAuth($wsHost, $wsToken, $wsSocket)static
websocket :: receive ($wsHost, $wsToken, $wsSocket, $data):bool
line 12
使用指定的主机、令牌和套接字从WebSocket连接接收数据,并将传入数据处理为JSON。
function_exists('wsReceive') && wsReceive($wsHost, $wsToken, $wsSocket, ...json_decode($data, true))static
websocket :: close ($wsHost, $wsToken, $wsSocket):bool
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 11
使用指定的URL和密钥初始化WhatsApp实例,确保URL以斜杠结尾。
$this->url = rtrim($url, slash).slashstatic
WhatsApp :: channel ($channel):static
line 13
使用提供的URL和密钥创建WhatsApp频道的新实例,如果未指定,则默认为'http://localhost:8081'和'void'。
new static($channel->configData->url ?? 'http://localhost:8081', $channel->secretData->secret ?? void)method
%WhatsApp -> number ($contact):string
line 15
从WhatsApp联系人字符串中提取电话号码,如果格式无效则返回错误。
($pos = strpos($contact, '@')) ? substr($contact, 0, $pos) : error('Invalid contact: '.esc($contact))method
%WhatsApp -> isGroup ($contact):bool
line 16
通过验证联系人是否包含 '@g' 后缀来检查指定的联系人是否为 WhatsApp 群组。
last($this->number($contact), (bool)strpos($contact, '@g'))method
%WhatsApp -> status:obj
line 18
使用GET请求从WhatsApp获取当前状态。
$this->request('status', GET: true)method
%WhatsApp -> health:obj
line 19
通过发送 GET 请求检查 WhatsApp 服务的健康状态。
$this->request('health', GET: true)method
%WhatsApp -> qr:obj
line 20
发送请求以检索WhatsApp身份验证的二维码。
$this->request('qr', GET: true)method
%WhatsApp -> disconnect:obj
line 21
通过发送断开请求来断开当前的WhatsApp会话。
$this->request('disconnect')method
%WhatsApp -> read ($chat):obj
line 23
发送请求以读取指定WhatsApp聊天中的消息。
$this->request('read', chat: $chat)method
%WhatsApp -> reaction ($msg, $emoji):obj
line 24
向WhatsApp中的指定消息发送反应表情符号。
$this->request('reaction', msg: $msg, emoji: $emoji)method
%WhatsApp -> text ($to, $text):obj
line 26
通过WhatsApp向指定的收件人发送文本消息。
$this->request('text', to: $to, text: $text)method
%WhatsApp -> image ($to, file $file, $text = void):obj
line 27
通过WhatsApp向指定的收件人发送图像消息,选项包括附加文本消息。
$this->request('image', to: $to, filename: $file->name, image: $file->src, text: $text)method
%WhatsApp -> location ($to, $lat, $lon, $text):obj
line 28
通过WhatsApp向指定的收件人发送带有纬度、经度和可选文本的位置消息。
$this->request('location', to: $to, lat: $lat, lon: $lon, text: $text)method
%WhatsApp -> document ($to, file $file, $text = void):obj
line 29
通过WhatsApp将文档发送给指定的收件人,包括可选的文本消息。
$this->request('document', to: $to, filename: $file->name, document: $file->src, text: $text)method
%WhatsApp -> audio ($to, file $file):obj
line 31
使用提供的音频文件向指定的接收者发送音频消息。
$this->request('audio', to: $to, audio: $file->src)method
%WhatsApp -> voice ($to, file $file):obj
line 32
使用提供的音频文件向指定的收件人发送语音消息。
$this->request('voice', to: $to, audio: $file->src)method
%WhatsApp -> poll ($to, $name, array $options, bool $multi = false):obj
line 34
向指定的WhatsApp接收者发送带有给定选项的投票消息,如果指定,则允许多选。
$this->request('poll', to: $to, name: $name, options: $options, multi: $multi)method
%WhatsApp -> startTyping ($to):obj
line 36
为WhatsApp中的指定收件人启动输入指示器。
$this->request('typing/start', to: $to)method
%WhatsApp -> stopTyping ($to):obj
line 37
停止WhatsApp中特定收件人的输入指示器。
$this->request('typing/stop', to: $to)method
%WhatsApp -> request ($action, ...$data):obj
line 39
向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):string
/phlo/resources/active.phlo line 9
为HTML元素生成类属性字符串,如果条件为真,则将'active'添加到指定的类列表中。
$cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : voidfunction
age(int $time):int
/phlo/resources/age.phlo line 9
通过从当前时间中减去给定的时间来计算年龄。
time() - $timefunction
age_human(int $age):string
/phlo/resources/age.human.phlo line 10
根据给定的年龄(以秒为单位)计算可读的时间持续时间。
time_human(time() - $age)function
apcu($key, $cb, int $duration = 3600, bool $log = true)
/phlo/resources/apcu.phlo line 14
apcu_entry holds a lock on the key for the length of the callback.
first($value = apcu_entry($key, $cb, $duration), $log && debug('C: '.(strlen($key) > 58 ? substr($key, 0, 55).'...' : $key).(is_array($value) ? ' ('.count($value).')' : (is_numeric($value) ? ":$value" : (is_string($value) ? ':string:'.strlen($value) : colon.gettype($value))))))function
await(...$jobs):array
/phlo/resources/await.phlo line 11
等待多个作业完成,处理它们的输出和错误,并返回结果。它管理子进程,并确保它们不会无限期阻塞。
if (daemon) return daemon::await($jobs)
$children = []
$open = []
foreach ($jobs AS $i => $job){
[$cb, $args] = is_array($job) ? [$job[0], array_slice($job, 1)] : [$job, []]
$cmd = cli.space.escapeshellarg($_SERVER['SCRIPT_FILENAME']).space.escapeshellarg($cb).loop($args, fn($a) => space.escapeshellarg((string)$a), void)
$desc = [0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']]
$proc = proc_open($cmd, $desc, $pipes)
fclose($pipes[0])
stream_set_blocking($pipes[1], false)
stream_set_blocking($pipes[2], false)
$children[$i] = obj(proc: $proc, out: $pipes[1], err: $pipes[2], stdout: void, stderr: void)
$open['o'.$i] = $pipes[1]
$open['e'.$i] = $pipes[2]
}
// Drain every child's stdout AND stderr together: reading one stream to EOF before the
// other deadlocks a child that fills the unread pipe. Bound the whole wait so a hung
// child cannot block the caller forever.
$deadline = time() + (defined('await_timeout') ? await_timeout : 300)
while ($open){
$read = $open
$write = $except = []
if (@stream_select($read, $write, $except, 1) === false) break
foreach ($read AS $key => $stream){
$chunk = fread($stream, 65536)
if ($chunk === void || $chunk === false){
feof($stream) && $open = array_diff_key($open, [$key => 1])
continue
}
$i = substr($key, 1)
if ($key[0] === 'o') $children[$i]->stdout .= $chunk
else $children[$i]->stderr .= $chunk
}
if (time() >= $deadline) break
}
// Terminate any child still running (deadline or a stream_select error). SIGKILL cannot
// be ignored, so proc_close below will not block on a process that drops SIGTERM.
foreach ($children AS $child) (proc_get_status($child->proc)['running'] ?? false) && proc_terminate($child->proc, 9)
$results = []
foreach ($children AS $i => $child){
fclose($child->out)
fclose($child->err)
$code = proc_close($child->proc)
$err = trim($child->stderr)
if ($err !== void){
$ej = json_decode($err, true)
$results[$i] = json_last_error() === JSON_ERROR_NONE ? $ej : $err
continue
}
if ($code !== 0){
$results[$i] = obj(error: 'CLI process failed', code: $code)
continue
}
$json = json_decode($child->stdout, true)
$results[$i] = json_last_error() === JSON_ERROR_NONE ? $json : $child->stdout
}
return $resultsfunction
button(...$args):string
/phlo/resources/tags.form.phlo line 11
创建一个按钮元素,使用作为 props 传递的指定参数。
tag('button', ...$args)function
camel(string $text):string
/phlo/resources/camel.phlo line 9
通过将每个单词的首字母大写并去除空格,将给定字符串转换为驼峰式。
lcfirst(str_replace(space, void, ucwords(lcfirst($text))))function
chunk(...$cmds):void
/phlo/resources/chunk.phlo line 10
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid chunk()')
if (debug){
$res->dump && [$cmds['dump'] = $res->dump, $res->dump = []]
$res->debug && [$cmds['debug'] = $res->debug, $res->debug = []]
}
if (!$res->streaming){
$res->streaming = true
$res->type = 'application/x-ndjson'
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->render()
}
print(json_encode($cmds, jsonFlat).lf)
$cli || [@ob_flush(), flush()]function
create(iterable $items, \Closure $keyCb, ?\Closure $valueCb = null):array
/phlo/resources/create.phlo line 9
通过使用可迭代对象中的值作为键,并使用可选的值回调来确定相应的值,创建一个关联数组。
array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items)function
en($text, ...$args):string
/phlo/resources/lang.phlo line 12
此函数检索指定文本的英文翻译,并可以选择性地使用附加参数进行格式化。
%lang->translation('en', $text, ...$args)function
exec_stream(string $cmd, ?int $timeoutSec = 0):Generator
/phlo/resources/exec.stream.phlo line 13
Reads stdout and stderr in one select loop rather than one after the other.
$desc = [0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']]
$proc = proc_open($cmd, $desc, $pipes)
if (!is_resource($proc)) return
stream_set_blocking($pipes[1], false)
stream_set_blocking($pipes[2], false)
$bufOut = void
$bufErr = void
while (true){
$status = proc_get_status($proc)
$running = $status['running']
$read = []
$w = null
$e = null
if (!feof($pipes[1])) $read[] = $pipes[1]
if (!feof($pipes[2])) $read[] = $pipes[2]
if ($read) @stream_select($read, $w, $e, 0, 200000)
foreach ($read AS $r){
$chunk = fread($r, 8192)
if ($chunk === void || $chunk === false) continue
if ($r === $pipes[1]){
$bufOut .= $chunk
while (($pos = strpos($bufOut, lf)) !== false){
$line = substr($bufOut, 0, $pos)
$bufOut = substr($bufOut, $pos + 1)
yield obj(data: $line)
}
}
else {
$bufErr .= $chunk
while (($pos = strpos($bufErr, lf)) !== false){
$line = substr($bufErr, 0, $pos)
$bufErr = substr($bufErr, $pos + 1)
yield obj(data: $line, error: true)
}
}
}
if (!$running) break
if ($timeoutSec > 0 && ($status['running_time'] ?? 0) > $timeoutSec){
proc_terminate($proc)
yield obj(data: 'process timeout', error: true)
break
}
}
if ($bufOut !== void) yield obj(data: $bufOut)
if ($bufErr !== void) yield obj(data: $bufErr, error: true)
foreach ($pipes AS $p) @fclose($p)
proc_close($proc)function
HTTP(string $url, array $headers = [], bool $JSON = false, $POST = null, $PUT = null, $PATCH = null, $QUERY = null, bool $DELETE = false, string|bool|null $agent = null, string|bool $cookies = false, int $timeout = 15, &$response = null)
/phlo/resources/HTTP.phlo line 15
Follows redirects on its own and raises only when the transport itself fails.
$curl = curl_init($url)
if ($POST !== null || $PUT !== null || $PATCH !== null || $QUERY !== null){
if (!is_null($POST)) [$method = 'POST', $content = $POST]
elseif (!is_null($PUT)) [$method = 'PUT', $content = $PUT]
elseif (!is_null($PATCH)) [$method = 'PATCH', $content = $PATCH]
elseif (!is_null($QUERY)) [$method = 'QUERY', $content = $QUERY]
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method)
if ($JSON) [!is_string($content) && $content = json_encode($content), array_push($headers, 'Content-Type: application/json', 'Content-Length: '.strlen($content))]
curl_setopt($curl, CURLOPT_POSTFIELDS, $content)
}
elseif ($DELETE) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE')
$agent && curl_setopt($curl, CURLOPT_USERAGENT, $agent === true ? phlo('req')->userAgent : $agent)
if ($cookies !== false) [$jar = $cookies === true ? data.'cookies.txt' : $cookies, curl_setopt($curl, CURLOPT_COOKIEFILE, $jar), curl_setopt($curl, CURLOPT_COOKIEJAR, $jar)]
$resHeaders = []
curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => $timeout, CURLOPT_ENCODING => void, CURLOPT_HEADERFUNCTION => function($ch, $line) use (&$resHeaders){
$parts = explode(colon, $line, 2)
count($parts) === 2 && $resHeaders[strtolower(trim($parts[0]))] = trim($parts[1])
return strlen($line)
}])
$res = curl_exec($curl)
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE)
$response = obj(ok: $res !== false && $status >= 200 && $status < 300, status: $status, headers: $resHeaders, error: $res === false ? curl_error($curl) : null)
if ($res === false) error('HTTP error: '.curl_error($curl))
return $resfunction
input(...$args):string
/phlo/resources/tags.form.phlo line 12
在视图中创建一个具有指定参数的输入元素。
tag('input', ...$args)function
n8n($webhook, ?array $data = null, $test = false)
/phlo/resources/n8n.phlo line 10
向 n8n webhook 发送一个可选数据和测试标志的 HTTP POST 请求。
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)function
nl($text, ...$args):string
/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 9
通过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 241
根据提供的名称和参数创建或检索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 39
通过设置环境、加载必要的类和函数,并根据提供的参数配置各种运行时参数来初始化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):bool
/phlo/resources/phlo.async.phlo line 10
异步执行回调函数,传递任何附加参数,如果成功则返回进程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 191
if (!$args) return;
$target = array_shift($args);
$result = phlo_dispatch($target, $args);
if (isset($result)) print(json_encode($result, jsonFlat).lf);function
phlo_dispatch(string $target, array $args = []):mixed
/phlo/phlo.php line 178
根据提供的目标字符串,将方法调用分发到对象或类的静态方法,可选择性地传递参数。
if (str_contains($target, dot)){
[$object, $method] = explode(dot, $target, 2);
$handle = phlo($object);
return $args ? $handle->$method(...$args) : ($handle->hasMethod($method) ? $handle->$method() : $handle->$method);
}
if (str_contains($target, '::')){
[$class, $method] = explode('::', $target, 2);
return $class::$method(...$args);
}
return $target(...$args);function
phlo_exception(Throwable $e):void
/phlo/phlo.php line 34
通过将Throwable对象传递给phlo_error_handle函数来处理异常。
require_once engine.'error.php';
phlo_error_handle($e);function
phlo_exists(string $obj):bool
/phlo/resources/phlo.exists.phlo line 9
检查指定的 PHP 文件是否存在于给定的对象路径中。
is_file(php.strtr($obj, [us => dot]).'.php')function
phlo_load(bool $http):void
/phlo/phlo.php line 154
加载应用程序所需的运行时文件,确保应用程序仅加载一次,并为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 198
ini_set('display_errors', 'stderr');
stream_set_blocking(STDIN, true);
fwrite(STDOUT, json_encode(['t' => 'ready']).lf);
while (($line = fgets(STDIN)) !== false){
$line = trim($line);
if ($line === void) continue;
$msg = json_decode($line, true) ?: [];
$id = $msg['id'] ?? null;
$target = (string)($msg['target'] ?? void);
$args = (array)($msg['args'] ?? []);
$stream = (bool)($msg['stream'] ?? false);
$lineBuf = void;
$emit = static function(string $chunk) use (&$lineBuf, $id):string {
$lineBuf .= $chunk;
while (($pos = strpos($lineBuf, lf)) !== false){
$out = substr($lineBuf, 0, $pos);
$lineBuf = substr($lineBuf, $pos + 1);
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'line', 'data' => $out], jsonFlat).lf);
}
return void;
};
try {
if ($target === void) error('No target');
if ($stream){
ob_start($emit, 1);
$result = phlo_dispatch($target, $args);
while (ob_get_level()) ob_end_flush();
if ($lineBuf !== void) fwrite(STDOUT, json_encode(['id' => $id, 't' => 'line', 'data' => $lineBuf], jsonFlat).lf);
}
else $result = phlo_dispatch($target, $args);
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'done', 'result' => $result], jsonFlat).lf);
}
catch (Throwable $e){
while (ob_get_level()) ob_end_clean();
fwrite(STDOUT, json_encode(['id' => $id, 't' => 'error', 'message' => $e->getMessage()], jsonFlat).lf);
}
phlo('tech/reset');
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
gc_collect_cycles();
}function
phlo_stream(string $cb, ...$args):Generator
/phlo/resources/phlo.stream.phlo line 11
从回调函数流式输出,允许异步处理数据。它可以在守护进程模式下运行或在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 10
以同步方式执行回调函数,并将任何附加参数传递给它,返回结果作为 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 116
try {
$req = phlo('req');
if ($req->cli){
$target = $req->args[0] ?? void;
if (str_starts_with($target, 'build::') || str_starts_with($target, 'reflect::')){
phlo_cli($req->args);
return;
}
phlo_load(false);
phlo('app');
phlo_cli($req->args);
return;
}
$isControl = build && debug && control && str_starts_with($req->path.slash, control.slash);
if (auth && !$isControl){
phlo_auth('site', 'Phlo App - '.host);
if (phlo('res')->done) return;
}
if ($isControl){
require_once engine.'control.php';
phlo_control::handle(substr($req->path, strlen(control) + 1));
phlo('res')->render();
return;
}
phlo_load(true);
phlo('app');
phlo('res')->render();
}
catch (RuntimeException $e){
if ($e->getMessage() === 'PhloDump') return;
phlo_exception($e);
}
catch (Throwable $e){
phlo_exception($e);
}function
select(...$args):string
/phlo/resources/tags.form.phlo line 13
创建一个 'select' HTML 元素,使用提供的参数作为属性和选项。
tag('select', ...$args)function
setting(?string $key = null, $value = null):mixed
/phlo/resources/settings.phlo line 11
$store = %JSON('settings', assoc: true)
if ($key === null) return $store->objData
if (func_num_args() > 1){
$store->$key = $value
$store->objChanged && $store->objWrite($store->objData)
}
return $store->$keyfunction
slug(string $text):string
/phlo/resources/slug.phlo line 9
将给定字符串转换为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 11
$res = %res
$cli = %req->cli
!$res->streaming && $res->done && error('Output already started, invalid stream()')
if (!$res->streaming){
$res->streaming = true
$res->type = $type
$res->header('Cache-Control', 'no-store')
$res->header('X-Content-Type-Options', 'nosniff')
$res->header('X-Accel-Buffering', 'no')
$name === null || $res->header('Content-Disposition', 'attachment; filename="'.str_replace('"', '', $name).'"')
$res->render()
}
if ($data === null) return
foreach (is_iterable($data) ? $data : [$data] as $part){
print((string)$part)
$cli || [@ob_flush(), flush()]
}function
tag(string $tagName, ?string $inner = null, ...$args):string
/phlo/resources/tag.phlo line 10
生成一个具有指定名称、可选内部内容和附加属性的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 14
创建一个带有指定参数的 'textarea' HTML 元素。
tag('textarea', ...$args)function
time_human(?int $time = null):string
/phlo/resources/time.human.phlo line 9
将给定的时间戳转换为人类可读的时间差格式,例如 '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, $wsExcept = void, ...$data)
/phlo/resources/wsCast.phlo line 10
HTTP (
'http://127.0.0.1:'.$wsPort.'/message',
JSON: true,
POST: arr (
host: $wsHost,
target: $wsTarget,
except: $wsExcept,
data: $data,
),
)最近更新于 2026年8月23日