EN | NL | 中文

Libs

object

%cookies

/phlo/libs/cookies.phlo
version 1.0
creator q-ai.nl
description Cookies data object
method

%cookies -> controller

line 7
此代码将所有cookie的内容分配给一个对象属性,从而可以在代码中稍后重复使用。
this->objData = $_COOKIE
prop

%cookies -> lifetimeDays

line 5
这是一段代码,将cookie的存活时间设置为180天。
180
method

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

line 9
此代码确保将值存储在内部数据、全局cookie以及具有特定存续期的HTTP-only、安全cookie中。
$this->objData[$key] = $value
$_COOKIE[$key] = $value
setcookie($key, $value, time() + $this->lifetimeDays * 86400, slash, $_SERVER['HTTP_HOST'], true, true)
method

%cookies -> __unset ($key)

line 15
删除一个cookie及其相关数据,通过取消设置其值并用过期的时间戳删除cookie。
unset($this->objData[$key], $_COOKIE[$key])
setcookie($key, void, time() - 86400, slash, $_SERVER['HTTP_HOST'], true, true)
object

%creds

/phlo/libs/creds.phlo
version 1.0
creator q-ai.nl
description INI file credentials handler
method

%creds -> __construct (?array $values = null)

line 5
在此构造函数中,如果未提供值,则会从INI文件中加载数据。然后,为该数组中的每个键值对创建一个动态属性:如果值是数组,则创建该类的一个新实例,否则创建一个SensitiveParameterValue对象。
$values ??= parse_ini_file(data.'creds.ini', true)
foreach ($values AS $key => $value){
	$this->$key = is_array($value) ? new static($value) : new SensitiveParameterValue($value)
}
method

%creds -> objGet ($key)

line 12
此代码检查参数 `$key` 是否等于 `'toArray'`。如果是,则对 `objData` 进行递归遍历,将类型为 `'SensitiveParameterValue'` 的值通过其 `getValue()` 方法转换。如果 `$key` 不等于 `'toArray'`,则检查 `objData` 中是否存在 `$key` 对应的元素,并且该元素的类型为 `'SensitiveParameterValue'`,然后通过 `getValue()` 返回其值。
if ($key === 'toArray') return loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? $value->getValue() : $value)
if (isset($this->objData[$key]) && is_a($this->objData[$key], 'SensitiveParameterValue')) return $this->objData[$key]->getValue()
method

%creds -> objInfo

line 17
通过输入创建一个列表,其中每个值都被检查是否为'SensitiveParameterValue'类型。如果是,则将该值替换为相同长度的星号序列,否则保持不变。
loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)
object

%encryption

/phlo/libs/encryption.phlo
version 1.0
creator q-ai.nl
description Simple encryption implementation
function

function encrypt ($data, $key):string

line 5
此代码生成一个随机的nonce,使用密钥函数对数据进行加密,然后将结果编码为Base64以便安全传输或存储。
base64_encode(($nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)).sodium_crypto_secretbox($data, $nonce, hash('sha256', $key, true)))
function

function decrypt ($encrypted, $key):string

line 6
解码base64编码的输入并检查解码是否成功以及长度是否足够。如果是,则从前几个字节中提取nonce,并将其余部分用作密文。然后,使用提供的密钥的哈希值和sodium_crypto_secretbox_open函数对机密数据进行解密。如果任何检查失败,则返回false。
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
object

%field

/phlo/libs/field.phlo
type abstract class
version 1.0
creator q-ai.nl
description ORM field
function

function field ($type, ...$args)

line 6
这定义了一个函数,用于创建一个新字段,其中函数名称根据类型动态构建。它使用 `$type` 参数和额外参数来生成该特定类型的字段。
phlo("field_$type", ...$args, type: $type)
static

field :: __handle

line 8
这个节点总是返回一个空值,无论输入或上下文如何。
null
prop

%field -> title

line 10
它将变量 $name 的值的第一个字母大写。
ucfirst($this->name)
method

%field -> input ($record, $CMS)

line 12
根据对象的属性创建一个具有类型、名称和其他属性的输入字段,并从记录或默认值中填充值。
input(type: $this->type, name: $this->name, value: $record->{$this->name} ?? $this->default, maxlength: $this->length, placeholder: $this->placeholder, class: 'field')
method

%field -> label ($record, $CMS)

line 13
此代码根据字段名称返回记录中某个属性的值。
$record->{$this->name};
object

%form_tags

/phlo/libs/form.tags.phlo
version 1.0
creator q-ai.nl
description DOM form tags for button, input, select, textarea
function

function button (...$args):string

line 5
创建一个带有给定参数作为属性或内容的 'button' 元素。
tag('button', ...$args)
function

function input (...$args):string

line 6
此节点生成一个带有指定参数作为属性或内容的HTML `<input>` 元素。
tag('input', ...$args)
function

function select (...$args):string

line 7
创建一个带有给定参数的 'select' 元素。
tag('select', ...$args)
function

function textarea (...$args):string

line 8
创建一个带有给定参数的 HTML `<textarea>` 元素。
tag('textarea', ...$args)
object

%lang

/phlo/libs/lang.phlo
version 1.0
creator q-ai.nl
description Language/translation library
requires @cookies @JSON @OpenAI @INI
advice Use %lang in views to show current app lang (for example in links)
function

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

line 8
将提供的文本翻译成荷兰语,必要时加入额外的论据或上下文。
%lang->translation('nl', $text, ...$args)
function

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

line 9
此节点将输入文本翻译成英语,使用相同的翻译功能,带有提供的文本和参数。
%lang->translation('en', $text, ...$args)
view

%lang ->

line 11
这段代码通过使用应用程序的语言设置来进行翻译,从而为用户加载正确的语言。
%app->lang
prop

%lang -> browser

line 13
此代码通过将用户的“HTTP_ACCEPT_LANGUAGE”头中的第一种语言与应用程序中可用的语言进行比较,选择最合适的语言代码。如果未找到合适的语言,则选择第一个匹配的默认语言,或者如果没有匹配项,则返回null。
last($langs = array_filter(explode(comma, $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? void), fn($lang) => isset(%app->langs[substr($lang, 0, 2)])), $langs ? substr(current($langs), 0, 2) : null)
method

%lang -> cookie

line 14
检查cookie中的语言是否存在于可用语言中。如果存在,则返回语言值;否则返回null。
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : null
prop

%lang -> model

line 16
简要说明是该节点可能包含用于使用较小或优化的GPT-4模型的设置或参数,适用于较少要求的任务或更快的处理。
'gpt-4o-mini'
method

%lang -> translations

line 17
初始化当前语言的翻译,使用“%app->lang”和“langs”中的设置或数据。
%INI(%app->lang, langs)
method

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

line 19
代码向AI模型发出聊天请求,以识别文本的语言,并将该识别结果作为ISO 639-1代码返回。如果响应不是有效的两字符代码,则使用备用语言。
$res = %OpenAI->chat (
	model: $this->model,
	system: 'Analyseer welke taal deze tekst is en geef alleen de ISO 639-1 code van de taal terug, zonder andere data!',
	user: $text.lf.lf.'De ISO 639-1 code van de taal is: ',
	temperature: 0,
)->answer
return strlen($res) === 2 ? strtolower($res) : $fallback
method

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

line 29
通过先用正则表达式和大写字母对文本进行摘要,使用其前缀部分创建一个简短的唯一哈希,然后添加完整文本的MD5哈希,从而生成的代码既易识别又唯一。
$from.($short = substr(implode(regex_all('/[A-z0-9]+/', ucwords($text))[0]), 0, 8)).substr(md5($text), 0, 10 - strlen($short))
method

%lang -> translation ($from, $text, ...$args)

line 31
此方法将文本从指定语言翻译成目标语言。如果源语言和目标语言相同,则直接返回文本。否则,将文本拆分为行,为每行生成哈希并查找翻译。如果未找到,则请求异步翻译。翻译结果将被合并并返回,如果提供了参数,还可以进行可选格式化。
if ($from === %app->lang) $translation = $text
else {
	$translation = []
	foreach (explode(lf, $text) AS $line){
		if (trim($line)){
			$hash = $this->hash($from, $line)
			if (!$item = $this->translations->$hash) phlo_async('lang', 'asyncTranslation', $from, %app->lang, $item = $line)
		}
		else $item = void
		$translation[] = $item
	}
	$translation = implode(lf, $translation)
}
$translation = strtr($translation, ['\n' => lf])
return $args ? sprintf($translation, ...$args) : $translation
static

lang :: asyncTranslation ($from, $to, $text)

line 49
此代码更改应用程序的语言,基于原始语言和文本生成唯一的哈希,翻译文本并将翻译存储在以哈希为键的翻译结构中。
%app->lang = $to
$hash = $this->hash($from, $text)
$translation = $this->translate($from, $to, $text)
return $this->translations->$hash = $translation
method

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

line 56
此功能首先检查源语言和目标语言是否相同,在这种情况下返回原始文本。否则,它会向OpenAI发起聊天请求,指示模型在指定的语言之间翻译文本,保持markdown、标点和大小写,并仅返回翻译后的文本。
if ($from === $to) return $text
return %OpenAI->chat (
	model: $this->model,
	system: "You will be provided with a word, sentence or (markdown) text in ISO 639-1 language $from, and your task is to translate this string into ISO 639-1 language $to. Respect markdown, missing interpunction and specific use of capitals. Give only the translation.",
	user: $text,
	temperature: 0,
)->answer
object

%model

/phlo/libs/model.phlo
version 1.1
creator q-ai.nl
description Phlo ORM class (unified columns/schema)
type abstract class
requires @DB @MySQL apcu?
static

model :: DB

line 8
连接到MySQL数据库。
%MySQL
static

model :: objRecords

line 9
一个空数组,意味着此节点目前没有记录或项目。
[]
static

model :: objLoaded

line 10
它似乎返回一个空数组,可能是用来表示没有加载数据或没有执行任何操作。
[]
static

model :: objCache

line 11
此代码总是返回值“false”。
false
static

model :: columns

line 13
检查静态属性 `$columns` 是否存在;如果存在,则返回它。如果不存在,则检查是否存在方法 `schema`;如果存在,则通过 `_columns()` 调用该方法。如果两者都不存在,则返回字符串 `*`。
isset(static::$columns) ? static::$columns : (method_exists(static::class, 'schema') ? static::_columns() : '*')
static

model :: _columns

line 14
此代码生成用于数据库的带引号的列名列表,仅排除特定类型('child'、'many'、'virtual')的字段。它结合字段定义并添加正确的引号以用于SQL查询。
$fq = static::DB()->fieldQuotes
$list = array_merge(...array_values(array_filter(loop(static::fields(), fn($field, $column) => in_array($field->type, ['child', 'many', 'virtual']) ? null : ($field->columns ?: [static::$table."$fq.$fq".$column])))))
return $fq.implode("$fq,$fq", $list).$fq
static

model :: fields

line 19
检查类中是否存在“schema”方法。如果存在,则使用'_fields()'的输出;否则,查找静态属性'$fields',如果不存在则使用空数组。
method_exists(static::class, 'schema') ? static::_fields() : (static::$fields ?? [])
static

model :: _fields

line 20
遍历模式字段,为每个字段选择最后一个值,来自一系列备选项:名称、对象(在条件类型为“父级”时)或字段本身。
loop(static::schema(), fn($field, $column) => last($field->name ??= $column, $field->type === 'parent' && $field->obj ??= $column, $field))
static

model :: field ($name)

line 21
根据提供的名称,从所有字段的数组中返回字段配置。
static::fields()[$name]
static

model :: create (...$args)

line 23
此代码创建一个新记录并返回它,可能用于进一步处理或作为函数的结果。
static::record(id: static::createRecord(...$args))
static

model :: createRecord (...$args)

line 24
在指定的表中创建一个新的数据库记录,并使用提供的参数。
static::DB()->create(static::$table, ...$args)
static

model :: change ($where, ...$args)

line 25
此代码调用一个静态方法“change”,可能在数据库中对特定表进行更改。它使用静态属性中的数据库连接和表名,并接受条件和额外参数以执行更改。
static::DB()->change(static::$table, $where, ...$args)
static

model :: delete ($where, ...$args)

line 26
对指定表在数据库中执行删除操作,基于提供的条件和可能的额外参数。
static::DB()->delete(static::$table, $where, ...$args)
method

%model -> objSave

line 28
检查是否存在ID;如果没有,显示错误信息。如果存在具有该ID的记录,则更新并返回该记录。否则,创建并返回一个新记录。
$this->id || error('Can\'t save '.static::class.' record without an id')
if (static::item(id: $this->id, columns: 'id')){
	static::change('id=?', $this->id, ...$this)
	return static::record(id: $this->id)
}
else return static::create(...$this)
static

model :: column (...$args)

line 37
加载多个记录并仅返回列值,没有对象或数组结构。
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_COLUMN])
static

model :: item (...$args)

line 38
加载具有指定参数的记录,使用“fetch”模式,仅提取一列。
static::recordsLoad($args, 'fetch', [PDO::FETCH_COLUMN])
static

model :: pair (...$args)

line 39
加载多个记录并将它们作为键值对返回,其中fetch模式设置为FETCH_KEY_PAIR,以实现快速简便的映射。
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_KEY_PAIR])
static

model :: records (...$args)

line 40
从数据库加载多个记录,并将它们作为类的唯一对象返回,所有记录合并为一个集合。
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, static::class], true)
static

model :: recordCount (...$args)

line 41
此代码通过对“id”列进行计数来统计记录数。
static::item(...$args, columns: 'COUNT(id)')
static

model :: record (...$args)

line 42
计算记录数,并在超过一条记录时显示错误消息;否则返回第一条记录或如果没有记录则返回null。
count($records = static::records(...$args)) > 1 ? error('Multiple records for '.static::class) : (current($records) ?: null)
static

model :: recordsLoad ($args, $fetch, $fetchMode, $saveRelations = false)

line 44
此代码动态构建用于从数据库加载记录的查询参数,包括条件、连接、选择和分组规则,取决于静态属性和参数。然后,使用基于缓存键和持续时间的APCu缓存,提高重复加载的效率。最后,加载的记录被存储以供重复使用并返回。
$args['table'] ??= static::$table
$saveRelations && $args['columns'] ??= static::$table.'.id as _,'.static::columns()
isset(static::$joins) && $args['joins'] = static::$joins.(isset($args['joins']) ? " $args[joins]" : void)
method_exists(static::class, 'where') && $args['where'] = static::where().(isset($args['where']) ? " AND $args[where]" : void)
isset(static::$group) && $args['group'] ??= static::$group
isset(static::$order) && $args['order'] ??= static::$order
if ($cacheKey = $args['cacheKey'] ?? null) unset($args['cacheKey'])
if ($duration = static::$objCache) $records = apcu($cacheKey ?? static::class.slash.md5(json_encode($args)), fn() => static::DB()->load(...$args)->$fetch(...$fetchMode), $duration === true ? 86400 : $duration)
else $records = static::DB()->load(...$args)->$fetch(...$fetchMode)
if ($saveRelations && $records) self::$objRecords[static::class] = (self::$objRecords[static::class] ?? []) + array_column($records, null, 'id')
return $records
static

model :: objRel ($key)

line 58
提供静态属性或方法的值,或者如果不存在,则提供默认值(空数组)。
static::$classProps[static::class][$key] ??= method_exists(static::class, $key) ? static::$key() : static::$$key ?? []
prop

%model -> objState

line 60
初始化一个包含三个空集合的结构:'parents'、'children' 和 'many'。
['parents' => [], 'children' => [], 'many' => []]
method

%model -> objGet ($key)

line 61
首先在父级中搜索,然后在子级中搜索,如果没有找到结果,再搜索多个项目。
$this->getParent($key) ?? $this->getChildren($key) ?? $this->getMany($key)
method

%model -> objIn ($ids)

line 62
将ID数组转换为用逗号分隔的字符串(用引号括起来),如果数组不存在或为空,则返回'NULL'。
$ids ? dq.implode(dq.comma.dq, $ids).dq : 'NULL'
method

%model -> getParent ($key)

line 64
根据键返回直接的父对象,检查之前加载的关系,并在未加载相关记录时动态获取。
if (array_key_exists($key, $this->objState['parents'])) return $this->objState['parents'][$key]
$parents = self::objRel('objParents')
if (!$relation = $parents[$key] ?? null) return
$isArray = is_array($relation)
$class = $isArray ? $relation['obj'] : $relation
$column = $isArray ? $relation['key'] ?? $key : $key
if (!$parentId = $this->objData[$column] ?? null) return $this->objState['parents'][$key] = null
if (!isset(self::$objRecords[$class][$parentId])){
	$idsToLoad = [$parentId => true]
	$allObjData = array_map(fn($record) => $record->objData, self::$objRecords[static::class] ?? [])
	foreach ($parents as $pKey => $pRelation){
		$pIsArray = is_array($pRelation)
		$pClass = $pIsArray ? $pRelation['obj'] : $pRelation
		if ($pClass === $class) foreach (array_column($allObjData, $pIsArray ? $pRelation['key'] ?? $pKey : $pKey) as $pId) $pId && !isset(self::$objRecords[$class][$pId]) && $idsToLoad[$pId] = true
	}
	if ($idsToLoad = array_keys($idsToLoad)) $class::records(where: 'id IN ('.$this->objIn($idsToLoad).')')
}
$parentObject = self::$objRecords[$class][$parentId] ?? null
return $this->objState['parents'][$key] = $parentObject
method

%model -> getChildren ($key)

line 86
验证是否已加载给定键的子项;如果没有,将请求关系并加载相关记录,然后存储在状态中;最后返回 children 数组。
if (array_key_exists($key, $this->objState['children'])) return $this->objState['children'][$key]
if (!$relation = self::objRel('objChildren')[$key] ?? null) return
$isArray = is_array($relation)
$class = $isArray ? $relation['obj'] : $relation
$column = $isArray ? $relation['key'] : static::class
if (!isset(self::$objLoaded[static::class]['children'][$key])){
	$parentIds = array_keys(self::$objRecords[static::class] ?? [])
	if ($parentIds){
		$children = $class::records(where: '`'.$column.'` IN ('.$this->objIn($parentIds).')')
		foreach (self::$objRecords[static::class] AS $parentRecord) $parentRecord->objState['children'][$key] = []
		foreach ($children AS $childId => $child) !is_null($pId = $child->objData[$column] ?? null) && isset(self::$objRecords[static::class][$pId]) && self::$objRecords[static::class][$pId]->objState['children'][$key][$childId] = $child
	}
	self::$objLoaded[static::class]['children'][$key] = true
}
return $this->objState['children'][$key] ?? []
method

%model -> getMany ($key)

line 104
如果尚未加载,则从数据库中加载多个相关记录,并将其缓存在对象状态中,以便在下一次调用时可以直接使用这些记录。
if (array_key_exists($key, $this->objState['many'])) return $this->objState['many'][$key]
if (!$relation = self::objRel('objMany')[$key] ?? null) return
$class = $relation['obj']
if (!isset(self::$objLoaded[static::class]['many'][$key])){
	$parentIds = array_keys(self::$objRecords[static::class] ?? [])
	if ($parentIds){
		$targetTable = $class::$table
		$records = $class::recordsLoad(arr(table: $relation['table'], columns: "`$targetTable`.*, `$relation[table]`.`$relation[localKey]` as _local_key", joins: "INNER JOIN `$targetTable` ON `$relation[table]`.`$relation[foreignKey]` = `$targetTable`.`id`", where: "`$relation[table]`.`$relation[localKey]` IN (".$this->objIn($parentIds).")"), 'fetchAll', [PDO::FETCH_CLASS, $class])
		foreach (self::$objRecords[static::class] AS $parentRecord) $parentRecord->objState['many'][$key] = []
		foreach ($records AS $record){
			$recordId = $record->id
			$parentId = $record->_local_key
			unset($record->_local_key)
			if (isset(self::$objRecords[static::class][$parentId])) self::$objRecords[static::class][$parentId]->objState['many'][$key][$recordId] = $record
		}
	}
	self::$objLoaded[static::class]['many'][$key] = true
}
return $this->objState['many'][$key] ?? []
method

%model -> getCount ($key)

line 126
此代码首先检查内部状态中是否已存储计数,并在可用时返回该值。然后它识别该计数是否与子关系('objChildren')或多重关系('objMany')有关。如果是子关系,它会获取相关记录的ID,执行聚合查询以确定计数,并缓存这些结果。对于'many'关系,则应用类似的逻辑,但通过单独的表和列实现。如果未找到关系,则返回0。
if (array_key_exists($key, $this->objState['counts'] ?? [])) return $this->objState['counts'][$key]
if ($relation = self::objRel('objChildren')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['children_count'][$key])){
		$parentIds = array_keys(self::$objRecords[static::class] ?? [])
		if ($parentIds){
			$isArray = is_array($relation)
			$class = $isArray ? $relation['obj'] : $relation
			$column = $isArray ? $relation['key'] : static::class
			$counts = $class::pair(columns: "`$column`, COUNT(*)", where: '`'.$column.'` IN ('.$this->objIn($parentIds).')', group: "`$column`")
			foreach (self::$objRecords[static::class] as $id => $record) $record->objState['counts'][$key] = (int)($counts[$id] ?? 0)
		}
		self::$objLoaded[static::class]['children_count'][$key] = true
	}
	return $this->objState['counts'][$key] ?? 0
}
if ($relation = self::objRel('objMany')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['many_count'][$key])){
		$parentIds = array_keys(self::$objRecords[static::class] ?? [])
		if ($parentIds){
			$counts = static::DB()->load(table: $relation['table'], columns: "`$relation[localKey]`,COUNT(*)", where: '`'.$relation['localKey'].'` IN ('.$this->objIn($parentIds).')', group: "`$relation[localKey]`")->fetchAll(PDO::FETCH_KEY_PAIR)
			foreach (self::$objRecords[static::class] as $id => $record) $record->objState['counts'][$key] = (int)($counts[$id] ?? 0)
		}
		self::$objLoaded[static::class]['many_count'][$key] = true
	}
	return $this->objState['counts'][$key] ?? 0
}
return 0
method

%model -> getLast ($key)

line 156
在对象状态中查找最后一个子项是否已与给定的键关联;如果没有,则通过关系数据和记录获取最后一个子项,存储在状态中并返回。
if (array_key_exists($key, $this->objState['last_child'] ?? [])) return $this->objState['last_child'][$key]
if ($relation = self::objRel('objChildren')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['last_child'][$key])){
		if ($parentIds = array_keys(self::$objRecords[static::class] ?? [])){
			$isArray = is_array($relation)
			$class = $isArray ? $relation['obj'] : $relation
			$column = $isArray ? $relation['key'] : static::class
			$childTable = $class::$table
			$whereClause = "`$column` IN (".$this->objIn($parentIds).") AND `$childTable`.`id` = (SELECT `id` FROM `$childTable` AS lc WHERE lc.`$column`=`$childTable`.`$column` ORDER BY `id` DESC LIMIT 1)"
			$lastChildren = $class::records(where: $whereClause)
			foreach (self::$objRecords[static::class] as $record) $record->objState['last_child'][$key] = null
			foreach ($lastChildren as $child) if (isset(self::$objRecords[static::class][$parentId = $child->objData[$column]])) self::$objRecords[static::class][$parentId]->objState['last_child'][$key] = $child
		}
		self::$objLoaded[static::class]['last_child'][$key] = true
	}
	return $this->objState['last_child'][$key] ?? null
}
return null
static

model :: objParents

line 177
如果存在,则获取静态属性;否则,如果不存在“schema”方法,则返回一个空数组;如果存在,则根据类型“parent”过滤字段,并生成一个数组,为每个字段返回一个对象和可选的键,指示是否使用对象名。
if (property_exists(static::class, 'objParents')) return static::$objParents
if (!method_exists(static::class, 'schema')) return []
return loop(array_filter(static::fields(), fn($f) => $f->type === 'parent'), fn($f, $c) => $f->key ? arr(obj: $f->obj, key: $f->key) : ($f->obj ?? $c))
static

model :: objChildren

line 183
此代码检查静态属性'objChildren'是否存在,并在存在时返回它。如果不存在'schema'方法,则返回一个空数组。否则,将数组过滤出类型为'child'的字段,然后通过循环函数处理这些字段,判断是否返回一个对象和可能的键,或者仅返回对象本身。
if (property_exists(static::class, 'objChildren')) return static::$objChildren
if (!method_exists(static::class, 'schema')) return []
return loop(array_filter(static::fields(), fn($f) => $f->type === 'child'), fn($f, $c) => $f->key ? arr(obj: $f->obj, key: $f->key) : ($f->obj ?? $c))
static

model :: objMany

line 189
如果存在,则返回静态属性`objMany`的值;否则,如果没有`schema`方法,则返回一个空数组;否则,筛选出类型为'many'的字段,并构建一个包含对象和表格信息的数组,包括本地和外键。
if (property_exists(static::class, 'objMany')) return static::$objMany
if (!method_exists(static::class, 'schema')) return []
return loop(array_filter(static::fields(), fn($f) => $f->type === 'many'), fn($f) => arr(obj: $f->obj, table: $f->table, localKey: $f->localKey ?? static::class, foreignKey: $f->foreignKey ?? $f->obj))
static

model :: createTable

line 195
检查是否存在方法 schema(),否则会抛出错误。然后通过迭代和格式化字段数据(包括任何 NOT NULL 或 NULL 规范)来构建 SQL CREATE TABLE 语句,并在 'id' 上添加主键。
method_exists(static::class, 'schema') || error(static::class.' has no schema()')
return 'CREATE TABLE `'.static::$table.'` ('.lf.tab.implode(",\n\t", array_merge(...array_values(array_filter(loop(static::fields(), fn($field) => loop((array)$field->sql, fn($sql) => $sql.($field->required || $field->nullable === false ? ' NOT' : void).' NULL')))))).",\n\tPRIMARY KEY (`id`)\n)"
object

%n8n

/phlo/libs/n8n.phlo
version 1.0
creator q-ai.nl
description Simple n8n functions
function

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

line 5
根据服务器信息和测试模式,向Webhook URL发起POST请求,并可选地附加数据。
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

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

line 6
调用名为 'n8n' 的函数,带有 webhook、一个可选的数据数组和一个布尔参数(可能用于特殊模式或调试)。
n8n($webhook, $data, true)
object

%payload

/phlo/libs/payload.phlo
version 1.0
creator q-ai.nl
description POST/PUT/PATCH and file-upload data object
requires @file
method

%payload -> controller

line 7
解析用于POST、PUT、PATCH方法的JSON请求体并执行导入;处理带有文件数据的multipart/form-data上传,临时保存文件,并将数据结构化,包括数组集合;还通过导入处理标准的$_FILES数据。
f (in_array(method, ['POST', 'PUT', 'PATCH']) && str_starts_with($_SERVER['CONTENT_TYPE'] ?? void, 'application/json')) return $this->objData = get_object_vars(json_read('php://input'))
if ($_POST) $this->objImport(...$_POST)
elseif (method === 'PUT' && str_starts_with($_SERVER['CONTENT_TYPE'], 'multipart/form-data')){
$boundary = '--'.regex('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'])[1]
$arrays = []
foreach (explode($boundary, file_get_contents('php://input')) AS $part){
	if (!trim($part) || $part === '--' || !str_contains($part, nl.nl)) continue
	[$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) ?: null
	if (str_ends_with($name, '[]')) $arrays[] = substr($name, 0, -2)
	if (preg_match('/filename="([^"]*)"/', $headers['content-disposition'], $f)){
		if ($f[1] === void || $body === null){
			if (!str_ends_with($name, '[]')) $this->objData[$name] = null
			continue
		}
		$filename = $f[1]
		$file = %file(tempnam(sys_get_temp_dir(), 'phlo'), $filename, $body)
		if (str_ends_with($name, '[]')) $this->objData[substr($name, 0, -2)][] = $file
		else $this->objData[$name] = $file
	}
	else {
		if (str_ends_with($name, '[]')) $this->objData[substr($name, 0, -2)][] = $body
		else $this->objData[$name] = $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

%security

/phlo/libs/security.phlo
version 1.0
creator q-ai.nl
description Generic security library
prop

%security -> nonce

line 5
生成一个随机的8个字符的令牌并将其存储在 `$nonce` 属性中。
%app->nonce = token(8)
method

%security -> full

line 7
提供一组安全头或设置,通过不同的属性进行检查或分配,可能针对网页安全和隐私。
$this->COOP
$this->CORP
$this->CORS
$this->CSP
$this->Referrer
$this->X_content
$this->X_frame
method

%security -> CSRF

line 17
生成并将一个12个字符的新CSRF令牌存储在会话中
%session->csrf = token(12)
method

%security -> COOP

line 18
设置一个HTTP头,规定该页面只能由相同源的文档打开,从而提供针对某些攻击技术(如侧信道攻击)的安全增强。
header('Cross-Origin-Opener-Policy: same-origin')
method

%security -> CORP

line 19
添加一个HTTP头,指示只允许来自相同源的跨源资源共享。
header('Cross-Origin-Resource-Policy: same-origin')
method

%security -> CORS ($host = host)

line 20
通过设置一个只允许来自指定主机的访问的头部,定义跨源资源共享(CORS)中的允许源。
header("Access-Control-Allow-Origin: https://$host")
method

%security -> CSP

line 21
发送一个Content-Security-Policy头,限制内容、脚本、样式、图片、字体、连接和frame-ancestors的资源,包括用于动态加载脚本的nonce。
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-$this->nonce' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'")
method

%security -> Referrer

line 22
它设置了一个HTTP头,规定referrer信息仅在发送到相同源或通过完整URL发送到第三方时才会被传送,但在跨域请求中不会传送,从而提高了隐私。
header('Referrer-Policy: strict-origin-when-cross-origin')
method

%security -> X_content

line 23
添加一个HTTP头,防止浏览器猜测响应的内容类型,从而通过防止内容嗅探来增强安全性。
header('X-Content-Type-Options: nosniff')
method

%security -> X_frame

line 24
此代码将HTTP标头“X-Frame-Options”设置为“DENY”,防止页面在框架或iframe中加载,从而增强对点击劫持的防护。
header('X-Frame-Options: DENY')
object

%session

/phlo/libs/session.phlo
version 1.0
creator q-ai.nl
description Session data object
method

%session -> controller

line 5
开始会话并将 $_SESSION 变量的内容存储在一个属性中以供以后使用。
ession_start()
$this->objData = $_SESSION
method

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

line 8
此代码同时修改两个数组:一个全局会话数组和对象的内部数据数组,通过将指定的值赋给两个数组中给定的键。
$_SESSION[$key] = $this->objData[$key] = $value
method

%session -> __unset ($key)

line 9
同时从内部数据结构和会话数据中删除一个密钥。
unset($this->objData[$key], $_SESSION[$key])

AI

object

%Claude

/phlo/libs/AI/Claude.phlo
version 1.0
creator q-ai.nl
description Antrophic Claude API (beta)
requires creds:Claude
static

Claude :: context (...$args)

line 7
如果存在,则将“assistant”和“user”的内容添加到“messages”数组中,然后删除这些键。
$args['messages'] ??= []
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
method

%Claude -> chat (...$args)

line 14
此代码创建了一个配置的请求到聊天模型,设置了模型和最大令牌数的默认值。然后发出请求,并将答案作为包含`answer`字段的对象返回。
$args = static::context(...$args)
$args['model'] ??= 'claude-3-5-sonnet-latest'
$args['max_tokens'] ??= 3333
$res = $this->request('messages', true, POST: $args)
return obj(answer: $res->content[0]->text)
method

%Claude -> stream (...$args)

line 22
此功能建立与API端点的流式连接,发送带有指定参数的POST请求,并通过回调实时处理接收的数据。如果未提供回调,则自动建立事件流并直接显示输出数据。响应由接收的增量文本组成,并作为完整文本返回。
%app->streaming = true
$args = static::context(...$args)
$args['model'] ??= 'claude-3-5-sonnet-latest'
$args['max_tokens'] ??= 3333
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($data) => last(($text = $data->delta->text ?? void) === void || [print($text), cli || [ob_flush(), flush()]], $text)
}
$answer = void
$curl = curl_init('https://api.anthropic.com/v1/messages')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $data = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['anthropic-version: 2023-06-01', 'Content-Type: application/json', 'Content-Length: '.strlen($data), 'X-Api-Key: '.%creds->Claude])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$answer){
	foreach (array_filter(explode(lf.lf, $data)) AS $chunk){
		$obj = json_decode(substr($chunk, strpos($chunk, 'data: {') + 6))
		$res = $cb($obj, $obj->delta->text ?? null)
		if (is_string($res)) $answer .= $res
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

%Claude -> request ($uri, $JSON = true, ...$args)

line 53
向指定的API端点发送带有默认头和API密钥的HTTP请求,解码JSON结果,检查错误,并返回解码后的对象。
$res = json_decode(HTTP("https://api.anthropic.com/v1/$uri", ['anthropic-version: 2023-06-01', 'Content-Type: application/json', 'X-Api-Key: '.%creds->Claude], $JSON, ...$args))
if (isset($res->error)) error('Claude Request error: '.$res->error->message)
return $res
object

%DeepSeek

/phlo/libs/AI/DeepSeek.phlo
version 1.0
creator q-ai.nl
description Basic DeepSeek functions
requires creds:DeepSeek
static

DeepSeek :: context (...$args)

line 7
将默认值添加到'messages'数组中,并根据提供的参数添加对话角色,如'system'、'assistant'和'user',然后清理原始参数。
$args['messages'] ??= []
if (isset($args['system']) && array_unshift($args['messages'], arr(role: 'system', content: $args['system']))) unset($args['system'])
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
method

%DeepSeek -> chat (...$args)

line 15
处理聊天请求,包括准备参数、向聊天完成端点发送请求,并返回包含模型信息、完成原因、令牌使用情况以及内容或工具调用的对象。
$args = static::context(...$args)
$res = $this->request('chat/completions', POST: $args)
$return = obj(model: $res->model, finish: $res->choices[0]->finish_reason, tokens: $res->usage->total_tokens, tokens_in: $res->usage->prompt_tokens, tokens_out: $res->usage->completion_tokens)
if (isset($res->choices[0]->message->tool_calls)) $return->tools = loop($res->choices[0]->message->tool_calls, fn($tool) => obj(name: $tool->function->name, args: json_decode($tool->function->arguments, true)))
else $return->answer = $res->choices[0]->message->content
return $return
method

%DeepSeek -> stream (...$args)

line 24
该方法建立与API的流连接,通过cURL处理临时数据上传,并使用回调函数实时处理数据并合并成最终答案。
%app->streaming = true
$args = static::context(...$args)
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($data) => last(($text = $data->choices[0]->delta->content ?? void) === void || [print($text), cli || [ob_flush(), flush()]], $text)
}
$answer = void
$buffer = void
$curl = curl_init('https://api.deepseek.com/v1/chat/completions')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $data = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.%creds->DeepSeek, 'Content-Type: application/json', 'Content-Length: '.strlen($data)])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$buffer, &$answer){
	$chunks = trim($buffer.$data)
	$buffer = void
	foreach (explode(lf.lf, $chunks) AS $chunk){
		if (!str_starts_with($chunk, 'data: ')){
			$buffer = $chunk
			continue
		}
		if ($obj = json_decode(substr($chunk, 6))){
			$res = $cb($obj)
			if (is_string($res)) $answer .= $res
		}
		else $buffer = $chunk
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

%DeepSeek -> request ($uri, $JSON = true, ...$args)

line 62
向DeepSeek API端点发送带有指定URI和身份验证的HTTP请求。然后解码JSON响应并检查是否有错误;如果存在,则生成一个错误。之后返回响应。
$res = json_decode(HTTP("https://api.deepseek.com/v1/$uri", ['Authorization: Bearer '.%creds->DeepSeek], $JSON, ...$args))
if (isset($res->error)) error('DeepSeek Request error: '.$res->error->message)
return $res
object

%Gemini

/phlo/libs/AI/Gemini.phlo
version 1.0
creator q-ai.nl
description Basic Gemini image functions (experimental)
requires creds:Gemini
method

%Gemini -> config ($modalities)

line 7
配置参数以设置语言输出,例如温度、topK、topP、最大令牌数,以及响应模式和 -mime 类型。
arr(temperature: 1, topK: 40, topP: .95, maxOutputTokens: 8192, response_modalities: $modalities, response_mime_type: 'text/plain')
method

%Gemini -> change ($prompt, $base64, $type = 'image/jpeg')

line 9
向Google API发送带有指定提示和base64图像的POST请求以生成图像,接收JSON响应并将其作为结果返回。
$this->respond (
	json_decode (
		HTTP (
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key='.%creds->Gemini,
			JSON: true,
			POST: arr (
				contents: [
					arr(role: 'user', parts: [arr(inlineData: arr(data: $base64, mimeType: $type))]),
					arr(role: 'user', parts: [arr(text: $prompt)]),
				],
				generationConfig: $this->config(['image', 'text']),
			),
		),
	),
)
method

%Gemini -> create ($prompt)

line 25
向Google生成式语言API发送POST请求以根据提示生成内容,并处理JSON响应以将其返回。
$this->respond (
	json_decode (
		HTTP (
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key='.%creds->Gemini,
			JSON: true,
			POST: arr (
				contents: [
					arr(role: 'user', parts: [arr(text: $prompt)]),
				],
				generationConfig: $this->config(['image', 'text']),
			),
		),
	),
)
method

%Gemini -> info ($prompt, $base64, $type = 'image/jpeg')

line 40
向Google API发送POST请求,用于生成图像内容,输入包括base64图像和提示,并在返回之前解码JSON响应。
$this->respond (
	json_decode (
		HTTP (
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key='.%creds->Gemini,
			JSON: true,
			POST: arr (
				contents: [
					arr(role: 'user', parts: [arr(inlineData: arr(data: $base64, mimeType: $type))]),
					arr(role: 'user', parts: [arr(text: $prompt)]),
				],
				generationConfig: $this->config(['text']),
			),
		),
	),
)
method

%Gemini -> respond ($res)

line 56
检查答案是否以“IMAGE_SAFETY”评级结束,并在这种情况下发出警告。如果没有,则返回第一个找到的文本内容或内联数据。如果没有有效信息,则返回错误消息。
if ($res->candidates[0]->finishReason === 'IMAGE_SAFETY') return 'I\'m affraid I can\'t process your image or command, it seems unsafe.'
if ($text = $res->candidates[0]->content->parts[0]->text ?? null) return $text
if ($data = $res->candidates[0]->content->parts[0]->inlineData ?? null) return $data
return 'Some error occured processing your request, please try again later.'
object

%OpenAI

/phlo/libs/AI/OpenAI.phlo
version 1.0
creator q-ai.nl
description Basic OpenAI functions
requires creds:OpenAI
const

OpenAI :: model

line 7
这是一个模型设置的规范,表明它使用的是GPT-4迷你模型。
'gpt-4o-mini'
const

OpenAI :: voices

line 8
这是一份包含不同语音或声音名称的字符串元素列表,可能用于文本转语音功能或语音选择模块。
['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']
static

OpenAI :: context (...$args):array

line 10
根据输入参数(系统、助手、用户)将消息添加到消息列表中,确保消息正确排序,并在添加后删除原始参数。
$args['messages'] ??= []
if (isset($args['system']) && array_unshift($args['messages'], arr(role: 'system', content: $args['system']))) unset($args['system'])
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
static

OpenAI :: tool ($tool):array

line 18
建立一个具有名称、描述和参数的描述性对象结构,其中参数由参数列表生成,并且只包含特定属性,所有其他附加属性都被排除。
arr (
	type: 'function',
	function: arr (
		name: $tool->name,
		description: $tool->desc,
		parameters: arr (
			type: 'object',
			properties: loop($tool->args, fn($data, $arg) => array_filter($data, fn($key) => in_array($key, ['type', 'enum', 'desc']), ARRAY_FILTER_USE_KEY)),
			additionalProperties: false,
			required: array_keys($tool->args),
		),
		strict: true,
	),
)
method

%OpenAI -> chat (...$args):obj

line 33
处理聊天请求以确定模型和上下文,向API发送请求,并返回包含模型、终止原因、令牌以及内容或工具调用详细信息的对象,具体取决于响应。
$args['model'] ??= static::model
$args = static::context(...$args)
$res = $this->request('chat/completions', POST: $args)
$return = obj(model: $res->model, finish: $res->choices[0]->finish_reason, tokens: $res->usage->total_tokens, tokens_in: $res->usage->prompt_tokens, tokens_out: $res->usage->completion_tokens)
if (isset($res->choices[0]->message->tool_calls)) $return->tools = loop($res->choices[0]->message->tool_calls, fn($tool) => obj(name: $tool->function->name, args: json_decode($tool->function->arguments, true)))
else $return->answer = $res->choices[0]->message->content
return $return
method

%OpenAI -> embedding ($input, $model = 'text-embedding-3-small')

line 43
对'embeddings'端点执行POST请求,包含输入和模型,并返回响应中的第一个嵌入。
$this->request('embeddings', POST: arr(input: $input, model: $model))->data[0]->embedding
method

%OpenAI -> stream (...$args):string

line 45
建立与OpenAI API的流式连接,发送聊天请求,并通过回调实时处理接收的数据,逐步构建并返回文本片段。
%app->streaming = true
$args['model'] ??= static::model
$args = static::context(...$args)
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($text) => last(print($text), cli || [@ob_flush(), flush()], $text)
}
$answer = void
$buffer = void
$curl = curl_init('https://api.openai.com/v1/chat/completions')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $payload = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.%creds->OpenAI, 'Content-Type: application/json', 'Content-Length: '.strlen($payload)])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$buffer, &$answer){
	$chunks = trim($buffer.$data)
	$buffer = void
	foreach (explode(lf.lf, $chunks) AS $chunk){
		if ($obj = json_decode(substr($chunk, 6))) in_array($text = $obj->choices[0]->delta->content ?? null, [null, void]) || $answer .= $cb($text, $obj)
		else $buffer = $chunk
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

%OpenAI -> transcribe ($file, $model = 'whisper-1', ...$args):obj

line 77
将音频文件转换为CURLFile(如有必要),并将其发送到用于转录的API,使用所选模型,并返回一个包含模型名称、时长、语言和转录文本的对象。
if (is_string($file)) $file = new CURLFile($file)
elseif (is_a($file, 'file')) $file = $file->curl
$res = $this->request('audio/transcriptions', false, POST: arr(...$args, model: $model, file: $file, response_format: 'verbose_json'))
return obj (
	model: $model,
	duration: $res->duration,
	lang: $res->language,
	text: $res->text,
)
method

%OpenAI -> vision ($text, $image, $stream = false, ...$args):obj

line 89
创建包含文本和可选图片的消息,并根据stream参数选择连续流或聊天回复。
$args['model'] ??= static::model
$messages = [arr(role: 'user', content: [arr(type: 'text', text: $text), arr(type: 'image_url', image_url: arr(url: $image))])]
if ($stream) return $this->stream(...$args, messages: $messages)
else return $this->chat(...$args, messages: $messages)
method

%OpenAI -> request ($uri, $JSON = true, ...$args)

line 96
向OpenAI API发送带有给定URI和选项的HTTP请求。解码JSON响应,检查错误,并返回结果。
$res = json_decode(HTTP("https://api.openai.com/v1/$uri", ['Authorization: Bearer '.%creds->OpenAI], $JSON, ...$args))
if (isset($res->error)) error('OpenAI Request error:'.lf.$res->error->message)
return $res

CSS

object

%basics

/phlo/libs/CSS/basics.phlo
version 1.0
creator q-ai.nl
description Single Page App basic CSS boilerplate
view

style

line 5
静态分配的CSS类选择器,包含文本对齐、浮动、定位、边距、内边距、溢出、显示、光标、文本装饰和宽度的不同样式规则,旨在重复使用和保持一致的样式。
.left: text-align: left
.center: text-align: center
.right: text-align: right

.float-left: float: left
.float-right: float: right

.absolute: position: absolute
.fixed: position: fixed
.relative: position: relative
.sticky: position: sticky

.padded: padding: 1rem
.margin: margin: 1rem
.margin-auto: margin: auto

.overflow: overflow: auto
.overflow-x: overflow-x: auto
.overflow-y: overflow-y: auto
.hide-x: overflow-x: hidden
.hide-y: overflow-y: hidden

.block: display: block
.hidden: display: none

.pointer: cursor: pointer
.underline: text-decoration: underline

.fit: width: fit-content
.full: width: 100%
.wide: max-width: 1200px
object

%fixes

/phlo/libs/CSS/fixes.phlo
version 1.0
creator q-ai.nl
description Single Page App basic CSS boilerplate fixes
view

style

line 5
将 box-sizing 添加到所有元素和伪元素,以使它们包括内边距和边框。移除诸如链接和按钮等元素的默认触控操作。通过删除输入元素的轮廓来修复焦点样式。移除 WebKit 和 Gecko 浏览器中数字输入的旋转按钮。通过启用 border-collapse,确保表格没有双重边框。移除 IE 中 select 元素的默认展开箭头。
*, ::before, ::after: box-sizing: border-box
a, area, button, input, label, select, summary, textarea, [tabindex]: touch-action: manipulation
button:focus, input:focus, select:focus, textarea:focus, [contenteditable]:focus: outline: 0
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button: -webkit-appearance: none
input[type="number"]: -moz-appearance: textfield
table: border-collapse: collapse
::-ms-expand: display: none
object

%flex

/phlo/libs/CSS/flex.phlo
version 1.0
creator q-ai.nl
description Single Page App CSS flex(box) boilerplate
view

style

line 5
设置具有不同flex属性的弹性布局规则,例如display、flex-wrap、gap和flex-direction,包括在更大屏幕上的响应式调整。
.flex: display: flex
.flex.auto > *: flex: auto
.flex.gap: gap: 1rem
.flex.row: flex-direction: column
.flex.wrap: flex-wrap: wrap
.grow: flex-grow: 1

@media (min-width:768px){
	.flex.col: flex-direction: column
	.flex.col.row: flex-direction: row
}
object

%grid

/phlo/libs/CSS/grid.phlo
version 1.0
creator q-ai.nl
description Single Page App CSS grid boilerplate
view

style

line 5
此代码定义了一个网格布局,根据可用宽度自动调整列数,每列最少300像素。
.grid {
	display: grid
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))
}

DB

object

%DB

/phlo/libs/DB/DB.phlo
version 1.0
creator q-ai.nl
description Database engine class
type abstract class
prop

%DB -> PDO

line 6
如果没有定义PDO连接,则会显示错误信息。
error('No PDO connector defined')
prop

%DB -> fieldQuotes

line 7
返回一个布尔值,指示数据周围是否有引号(引号),可能用于检查字段是否已经加引号。
bt
method

%DB -> load (string $table, string $columns = '*', string $where = void, string $joins = void, string $group = void, string $limit = null, string $order = void, ...$args)

line 9
根据提供的参数和附加参数动态构建SQL SELECT语句,包括在未指定时根据键值对自动生成WHERE子句。
!$where && $args && $where = loop(array_keys($args), fn($column) => "$table.$column=?", ' AND ')
$joins && $joins = " $joins"
$where && $where = " WHERE $where"
$group && $group = " GROUP BY $group"
$order && $order = " ORDER BY $order"
$limit && $limit = " LIMIT $limit"
$query = "SELECT $columns FROM $table$joins$where$group$order$limit"
return $this->query($query, ...array_values($args))
method

%DB -> query ($query, ...$args)

line 20
执行支持预处理语句的数据库查询,输入参数,并包含调试功能,显示查询类型、表名以及可选的 WHERE 子句和返回的行数。出错时会生成包含追踪的详细错误信息。
try {
	if (!$args) $stmt = $this->PDO->query($query)
	else {
		$stmt = $this->PDO->prepare($query)
		$stmt->execute($args)
	}
	if (debug){
		$match = regex('/\b(UPDATE|INSERT INTO|DELETE FROM|FROM)\b\s+([`"\[]?\w+[`"\]]?)/i', strtr($query, [$this->fieldQuotes => void]))
		$where = strtr(regex('/\bWHERE (\b.+)/is', $query)[1] ?? void, [' ORDER BY' => void])
		$match && debug("Q: $match[1] $match[2]".strtr(rtrim(" $where "), [dq => void])." (".$stmt->rowCount().")")
	}
}
catch (PDOException $e){
	error('Database error'.(debug ? colon.lf.$query.lf.lf.$e->getMessage().lf.lf.loop(array_slice(explode(lf, $e->getTraceAsString()), 2, -2), fn($line) => last($match = regex('/([^\/]+\.php)\(([0-9]+)\): (.*)/', $line), "$match[3] $match[1]:$match[2]"), lf) : void))
}
return $stmt
method

%DB -> column (...$args)

line 39
加载具有指定参数的数据,并返回所有行中的一列列表。
$this->load(...$args)->fetchAll(PDO::FETCH_COLUMN)
method

%DB -> item (...$args)

line 40
获取已加载的数据库数据,并返回第一行的第一列,除非不存在,则返回null。
$this->load(...$args)->fetch(PDO::FETCH_COLUMN) ?: null
method

%DB -> pair (...$args)

line 41
获取数据并以键值对数组的形式返回。
$this->load(...$args)->fetchAll(PDO::FETCH_KEY_PAIR)
method

%DB -> group (...$args)

line 42
此代码使用提供的参数加载数据,获取所有结果,并根据某一列或字段对它们进行分组,每个组都转换为'obj'类的对象。
$this->load(...$args)->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_CLASS, 'obj')
method

%DB -> records (...$args)

line 43
加载带有指定参数的数据并获取所有结果,将每一行转换为指定类的对象,基于第一列确保结果唯一。
$this->load(...$args)->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, 'obj')
method

%DB -> record (...$args)

line 44
根据加载的数据返回一个对象,或在没有结果时返回null。
$this->load(...$args)->fetchObject('obj') ?: null
method

%DB -> create (string $table, ...$data)

line 46
执行一个插入查询,如果数据中的“ignore”值为真,则添加“IGNORE”选项。正确构建列名及其对应的值,用占位符补充值并用实际数据替换。执行该查询并返回最后插入的ID或一个可选的ID。
if ($ignore = $data['ignore'] ?? false) unset($data['ignore'])
$columns = $this->fieldQuotes.implode($this->fieldQuotes.comma.$this->fieldQuotes, array_keys($data)).$this->fieldQuotes
$values = implode(comma, array_fill(0, count($data), qm))
$query = "INSERT".($ignore ? ' IGNORE' : void)." INTO $table ($columns) VALUES ($values)"
$this->query($query, ...array_values(loop($data, fn($value) => is_a($value, 'obj') ? $value->id : $value)))
return $this->PDO->lastInsertId() ?: ($data['id'] ?? null)
method

%DB -> change (string $table, string $where, ...$data)

line 55
此功能通过使用 `$data` 中的数据动态构建 SQL UPDATE 语句。它计算 `$where` 中的占位符数量,以确定哪些数据用于 WHERE 条件,哪些用于 SET 子句。然后,将列名和对应的值分离并排序,并使用正确的参数执行查询,最后返回受影响的行数。
$whereCount = substr_count($where, qm)
$updates = isset($data['updates']) ? $data['updates'] : void
unset($data['updates'])
$updates .= (($wheres = array_slice(array_keys($data), $whereCount)) && $updates ? comma : void).loop($wheres, fn($key) => $key.'=?', comma)
$query = "UPDATE $table SET $updates WHERE $where"
$args = array_values([...array_slice($data, $whereCount), ...array_slice($data, 0, $whereCount)])
return $this->query($query, ...$args)->rowCount()
method

%DB -> delete (string $table, string $where, ...$args)

line 65
对指定表执行DELETE查询,带有特定的WHERE条件,并返回删除的行数。
$this->query("DELETE FROM $table WHERE $where", ...$args)->rowCount()
object

%MySQL

/phlo/libs/DB/MySQL.phlo
version 1.0
creator q-ai.nl
description MySQL handler via DB class
extends DB
requires @DB creds:mysql
prop

%MySQL -> PDO

line 8
创建一个新的PDO连接到MySQL数据库,连接信息来自一个凭据对象。
new PDO('mysql:host='.%creds->mysql->host.';dbname='.%creds->mysql->database, %creds->mysql->user, %creds->mysql->password)
object

%PostgreSQL

/phlo/libs/DB/PostgreSQL.phlo
version 1.0
creator q-ai.nl
description PostgreSQL library
extends DB
requires @DB creds:postgresql
prop

%PostgreSQL -> PDO

line 8
使用配置的登录信息创建一个新的PDO连接到PostgreSQL数据库。
new PDO('pgsql:host='.%creds->postgresql->host.';dbname='.%creds->postgresql->database, %creds->postgresql->user, %creds->postgresql->password)
prop

%PostgreSQL -> fieldQuotes

line 9
此代码定义了一个函数,可能对输入进行文本替换或处理,可能涉及PostgreSQL的字段或表名引号,以防止SQL注入和语法错误。
dq
object

%Qdrant

/phlo/libs/DB/Qdrant.phlo
version 1.0
creator q-ai.nl
description Embeddings library with Qdrant
method

%Qdrant -> get (string $input)

line 5
在APCu缓存中存储一个嵌入,使用基于输入的键,并在28天内检索或生成并缓存它。
apcu('embedding/'.token(input: $input), fn($input) => %OpenAI->embedding($input), 86400 * 28)
method

%Qdrant -> collections

line 7
从集合中提取名称并以数组形式返回。
array_column($this->request('collections')->result->collections, 'name')
method

%Qdrant -> create ($collection, $size = 1536, $distance = 'Cosine')

line 8
创建一个具有指定名称、向量大小和距离度量的新集合,并检查是否成功创建。
$this->request("collections/$collection", PUT: arr(vectors: arr(size: $size, distance: $distance)))->status === 'ok'
method

%Qdrant -> upsert ($collection, $id, $input, ...$payload)

line 9
对用于更新或添加集合中点的端点执行HTTP PUT请求。它会发送一个包含点数据的数组,包括ID、向量和可选的(payload)。结果是已执行操作的操作ID。
$this->request("collections/$collection/points", PUT: arr(points: [arr(id: $id, vector: $this->get($input), payload: $payload ?: null)]))->result->operation_id
method

%Qdrant -> delete ($collection, ...$ids)

line 10
向API发送POST请求,通过指定ID使用端点'collections/{collection}/points/delete'删除集合中的特定点
$this->request("collections/$collection/points/delete", POST: arr(points: $ids))->result
method

%Qdrant -> search ($collection, $input = null, $top = 100)

line 11
在集合中执行搜索操作,通过向API发送点,使用默认或提供的输入。结果将被过滤并返回,包含特定的ID和有效载荷数据,缺失的输入用长度为1536的向量中的零填充。
create($this->request("collections/$collection/points/search", POST: arr(vector: is_null($input) ? array_fill(0, 1536, 0) : $this->get($input), top: $top, with_payload: true))->result, fn($record) => $record->id, fn($record) => last($record = array_merge(get_object_vars($record), get_object_vars($record->payload)), obj(...array_filter($record, fn($key) => $key !== 'payload', ARRAY_FILTER_USE_KEY))))
method

%Qdrant -> drop ($collection)

line 12
通过向服务器发送DELETE请求并返回结果,删除具有指定名称的集合。
$this->request("collections/$collection", DELETE: true)->result
method

%Qdrant -> request ($uri, ...$data)

line 14
向具有指定URI和数据的Qdrant服务器发送HTTP请求,包括必要时的API密钥,然后将JSON响应解码为关联数组。
json_decode(HTTP(%creds->qdrant->server.$uri, %creds->qdrant->key ? ['api-key: '.%creds->qdrant->key] : [], true, ...$data))
object

%SQLite

/phlo/libs/DB/SQLite.phlo
version 1.0
creator q-ai.nl
description SQLite library
extends DB
requires @DB
method

%SQLite -> controller

line 8
建立與名為 `$file` 的 SQLite 數據庫的連接。
andle => "SQLite/$file"
method

%SQLite -> __construct (private string $file)

line 9
这是类的构造函数,它接受一个类型为字符串的参数,可能用于存储或初始化SQLite数据库的路径或名称。
prop

%SQLite -> PDO

line 11
创建一个新的PDO实例,使用来自对象属性的数据库文件名的SQLite数据库。
new PDO('sqlite:'.$this->file)

DOM

object

%CSS_var

/phlo/libs/DOM/CSS.var.phlo
version 1.0
creator q-ai.nl
description CSS variable name proxy via app.var
view

script

line 5
此代码定义了一个属性,通过代理访问文档中的CSS变量。读取时获取CSS变量的值,写入时进行修改。
Object.defineProperty(app, 'var', {get(){return new Proxy({}, {get(_, key){return getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim()}, set(_, key, value){ return document.documentElement.style.setProperty(`--${key}`, value)}})}, configurable: true})
object

%datatags

/phlo/libs/DOM/datatags.phlo
version 1.0
creator q-ai.nl
description Single Page App datatag plugin
view

script

line 5
此代码响应具有特定 data-attributes 元素的点击事件,并据此确定 HTTP 方法和 URI。然后通过应用程序函数进行正确的 API 调用,并可选地传递数据。
on('click', '[data-get], [data-post], [data-put], [data-patch], [data-delete]', (el, e) => {
	if (el.dataset.confirm) return
	e.preventDefault()
	let method, uri, data = null
	if ((uri = el.dataset.get) !== undefined) method = 'get'
	else if ((uri = el.dataset.delete) !== undefined) method = 'delete'
	else [data = {}, Object.keys(el.dataset).forEach((key) => key === 'post' || key === 'put' || key === 'patch' ? [method = key, uri = el.dataset[key]] : data[key] = el.dataset[key])]
	app[method](uri, data)
})
object

%dialog

/phlo/libs/DOM/dialog.phlo
version 1.0
creator q-ai.nl
description Single Page App dialog library
view

script

line 5
此代码定义了一组对话功能(alert、confirm、prompt),它们使用动态创建的HTML对话框。函数'phlo.dialog'根据类型创建对话窗口并显示,等待用户操作,然后返回正确的值。此外,还创建了一个点击事件监听器,当检测到'data-confirm'属性时,会显示确认对话框,并根据反应采取后续操作。
app.alert = (msg) => phlo.dialog('alert', msg)
app.confirm = (msg) => phlo.dialog('confirm', msg)
app.prompt = (msg, def) => phlo.dialog('prompt', msg, def)

phlo.dialog = async (type, message, defaultValue = '') => new Promise((resolve) => {
	app.mod.append('body', ['<dialog>', '<form method="dialog">', `<p>${message}</p>`, type === 'prompt' ? `<input name="value" value="${defaultValue}">` : '', '<menu>', '<button value="1" autofocus>OK</button>', type !== 'alert' ? ' <button value="0">Cancel</button>' : '', '</menu>', '</form>', '</dialog>'].join(''))
	const dialog = obj('dialog')
	dialog.showModal()
	dialog.addEventListener('close', () => {
		const value = dialog.returnValue
		const input = dialog.querySelector('input')
		dialog.remove()
		if (type === 'alert') return resolve()
		if (type === 'confirm') return resolve(value === '1')
		if (type === 'prompt') return resolve(value === '1' ? input.value : null)
	})
})

on('click', '[data-confirm]', async (el, e) => {
	e.preventDefault()
	if (!await app.confirm(el.dataset.confirm)) return
	delete el.dataset.confirm
	app.update()
	el.click()
})
object

%exists

/phlo/libs/DOM/exists.phlo
version 1.0
creator q-ai.nl
description onExist function for initialising DOM elements in a dynamic SPA environment
view

script

line 5
此代码定义了检测元素是否存在的功能:它维护一个元素及其相关回调的列表,并在元素尚未注册为存在时执行这些回调,使用WeakMap来跟踪已注册的元素。
phlo.exist = []
phlo.existing = new WeakMap

const onExist = (els, cb) => phlo.exist.push({els, cb})

app.updates.push(() => {
	const existing = []
	phlo.exist.forEach(item => objects(item.els).forEach(el => phlo.existing.has(el) || existing.push({el, cb: item.cb})))
	existing.forEach(item => [phlo.existing.has(item.el) || phlo.existing.set(item.el, 'exist'), item.cb(item.el)])
})
object

%form

/phlo/libs/DOM/form.phlo
version 1.0
creator q-ai.nl
description Single Page App form handler and input state saver
view

script

line 5
此代码包含一个根据方法名通过AJAX调用提交表单数据的函数,以及一个在输入或更改时将表单值与属性同步并保存内部状态的事件监听器。此外,它还为具有“async”类的表单提供了一个自定义的提交处理程序,以防止默认行为并通过该函数发送数据。
const submitForm = (form) => {
	const method = (form.attributes.method?.value ?? 'GET').toLowerCase()
	app[method](new URL(form.action).pathname.substr(1), new FormData(form))
}
on('input change', 'input, select, textarea', input => {
	if (input.tagName === 'SELECT') input.querySelectorAll('option').forEach((option, index) => option.selected ? option.setAttribute('selected', '') : option.removeAttribute('selected'))
	if (input.type === 'checkbox') input.checked ? input.setAttribute('checked', '') : input.removeAttribute('checked')
	if (input.type === 'text' && input.value !== input.getAttribute('value')) input.setAttribute('value', input.value)
	if (input.type === 'textarea' && input.value !== input.innerHTML) input.innerHTML = input.value
	phlo.state.save()
	return false
})
on('submit', 'form.async', (form, e) => {
	e.preventDefault()
	submitForm(form)
})
object

%image_resizer

/phlo/libs/DOM/image.resizer.phlo
version 1.0
creator q-ai.nl
description Clientside file upload image resizer
view

script

line 5
加载一张图片,根据最大宽度和高度调整尺寸以保持纵横比,并通过回调生成一个重新调整大小的数据链接。
const imageResizer = (file, maxWidth, maxHeight, cb, quality = .8) => {
	const img = new Image
	img.onload = () => {
		let width = img.width, height = img.height
		const aspectRatio = width / height
		if (width > maxWidth || height > maxHeight){
			if (width > height){
				width = maxWidth
				height = Math.round(maxWidth / aspectRatio)
			}
			else {
				height = maxHeight
				width = Math.round(maxHeight * aspectRatio)
			}
		}
		const canvas = document.createElement('canvas')
		canvas.width = width
		canvas.height = height
		canvas.getContext('2d').drawImage(img, 0, 0, width, height)
		cb(canvas.toDataURL(file.type, quality))
	}
	img.src = URL.createObjectURL(file)
}
object
/phlo/libs/DOM/link.phlo
version 1.0
creator q-ai.nl
description Single Page App link.async handler
view

script

line 5
在点击具有'class'为'async'的链接时,根据按键操作或确认状态,阻止默认导航并执行异步获取操作的情况。
on('click', 'a.async', (a, e) => e.ctrlKey || e.shiftKey || e.metaKey || a.dataset.confirm || [e.preventDefault(), app.get(a.attributes.href.value.substr(1))])
object

%markdown

/phlo/libs/DOM/markdown.phlo
version 1.0
creator q-ai.nl
description Clientside markdown parser
view

script

line 5
解析转换将Markdown文本转换为支持GFM功能的HTML。标记被识别并转换,例如标题、列表、表格、代码块、引用和内联元素。对于超链接、图片和文本装饰,进行安全处理并安全嵌入HTML。还使用引用和ID生成逻辑为标题和表格生成ID。
function parse_markdown(md, opts = {}){
  const o = {
    gfm: opts.gfm !== false,
    breaks: !!opts.breaks,
    headerIds: opts.headerIds !== false,
    headerPrefix: opts.headerPrefix || '',
    smartypants: !!opts.smartypants
  }
  const unnull = x => (x == null ? '' : String(x))
  let src = unnull(md).replace(/\r\n?/g, "\n")
  const escHtml = s => s.replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))
  const trimEndNL = s => s.replace(/\s+$/,'')
  const isBlank = s => /^\s*$/.test(s)
  const slugmap = new Map()
  const slug = (t) => {
    let s = t.toLowerCase().replace(/<\/?[^>]+>/g, '').replace(/[^\p{L}\p{N}\- _]+/gu, '').trim().replace(/[\s_]+/g, '-')
    const base = o.headerPrefix + s
    let k = base, i = 1
    while (slugmap.has(k)) k = `${base}-${++i}`
    slugmap.set(k, true)
    return k
  }
  const smart = s => {
    if (!o.smartypants) return s
    return s.replace(/---/g, "—").replace(/--/g, "–").replace(/(^|[\s"(\[])(?=')/g, "$1‘").replace(/'/g, "’").replace(/(^|[\s(\[])(?=")/g, "$1“").replace(/"/g, "”").replace(/\.{3}/g, "…")
  }
  const refs = Object.create(null)
  src = src.replace(
    /^ {0,3}\[([^\]]+)\]:\s*<?([^\s>]+)>?(?:\s+(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*$/gm,
    (_, label, url, t1, t2, t3) => {
      const key = label.trim().replace(/\s+/g, ' ').toLowerCase()
      if (!refs[key]) refs[key] = { href: url, title: t1 || t2 || t3 || '' }
      return ''
    }
  )
  const tokens = []
  const lines = src.split("\n")
  function takeWhile(start, pred){
    let end = start
    while (end < lines.length && pred(lines[end], end)) end++
    return { start, end }
  }
  function pushParagraph(buf){
    const text = buf.join("\n").trimEnd()
    if (text) tokens.push({ type: "paragraph", text })
    buf.length = 0
  }
  function parseBlock(start = 0, end = lines.length){
    const para = []
    let l = start
    while (l < end){
      const line = lines[l]
      if (isBlank(line)){ pushParagraph(para); l++; continue; }
      let m = line.match(/^ {0,3}(`{3,}|~{3,})([^\n]*)$/)
      if (m){
        pushParagraph(para)
        const fenceLen = m[1].length
        const info = (m[2] || '').trim()
        let body = []
        l++
        while (l < end){
          const s = lines[l]
          const close = s.match(new RegExp(`^ {0,3}${m[1][0]}{${fenceLen},}\\s*$`))
          if (close){ l++; break; }
          body.push(s)
          l++
        }
        tokens.push({ type: "code", lang: info.split(/\s+/)[0] || '', text: trimEndNL(body.join("\n")) })
        continue
      }
      if (/^(?: {4}|\t)/.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, s => /^(?: {4}|\t)/.test(s) || isBlank(s))
        const block = lines.slice(l, j).map(s => s.replace(/^(?: {4}|\t)/, '')).join("\n")
        tokens.push({ type: "code", lang: '', text: trimEndNL(block) })
        l = j; continue
      }
      if (/^ {0,3}<(?:!--|\/?(?:html|head|body|pre|script|style|table|thead|tbody|tfoot|tr|td|th|div|p|h[1-6]|blockquote|ul|ol|li|section|article|aside|details|summary|figure|figcaption)\b)/i.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, (s, idx) => !(idx > l && isBlank(lines[idx-1]) && isBlank(s)))
        const html = lines.slice(l, j).join("\n")
        tokens.push({ type: "html", text: html })
        l = j; continue
      }
      if (/^ {0,3}(?:-+\s*|-{3,}|_{3,}|\*{3,})\s*$/.test(line)){
        pushParagraph(para)
        tokens.push({ type: "hr" })
        l++; continue
      }
      m = line.match(/^ {0,3}(#{1,6})[ \t]*([^#\n]*?)[ \t#]*$/)
      if (m){
        pushParagraph(para)
        tokens.push({ type: "heading", depth: m[1].length, text: m[2].trim() })
        l++; continue
      }
      if (l + 1 < end && /^[^\s].*$/.test(line) && /^ {0,3}(=+|-+)\s*$/.test(lines[l + 1])){
        pushParagraph(para)
        const depth = lines[l + 1].trim().startsWith("=") ? 1 : 2
        tokens.push({ type: "heading", depth, text: line.trim() })
        l += 2; continue
      }
      if (/^ {0,3}>\s?/.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, s => /^ {0,3}>\s?/.test(s) || isBlank(s))
        const inner = lines.slice(l, j).map(s => s.replace(/^ {0,3}>\s?/, '')).join("\n")
        const sub = parse_markdown(inner, { ...o })
        tokens.push({ type: "blockquote", html: sub })
        l = j; continue
      }
      m = line.match(/^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/)
      if (m){
        pushParagraph(para)
        const bulletRe = /^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/
        const { end: j } = takeWhile(l, (s, idx) =>
          bulletRe.test(s) ||
          (/^(?: {4}|\t)/.test(s)) ||
          (!isBlank(s) && idx > l && !/^(?: {0,3}(?:[*+-]|\d{1,9}[.)])\s+)/.test(s))
        )
        const block = lines.slice(l, j)
        const ordered = /^\d/.test(m[1])
        const items = []
        let cur = []
        for (let k = 0; k < block.length; k++){
          const ln = block[k]
          const head = ln.match(bulletRe)
          if (head){
            if (cur.length) items.push(cur), cur = []
            cur.push(ln.replace(bulletRe, ''))
          } else {
            cur.push(ln.replace(/^(?: {4}|\t)/, ''))
          }
        }
        if (cur.length) items.push(cur)
        const parsedItems = items.map(linesArr => {
          let raw = linesArr.join("\n").replace(/\n\s+$/,'')
          let checked = null
          if (o.gfm){
            const t = raw.match(/^\[([ xX])\][ \t]+/)
            if (t){ checked = t[1].toLowerCase() === 'x'; raw = raw.replace(/^\[[ xX]\][ \t]+/, ''); }
          }
          const html = parse_markdown(raw, o)
          return { html, checked }
        })
        tokens.push({ type: "list", ordered, items: parsedItems })
        l = j; continue
      }
      if (o.gfm){
        const hdr = line
        const alignLn = lines[l + 1] || ''
        if (/\|/.test(hdr) && /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/.test(alignLn)){
          pushParagraph(para)
          const aligns = alignLn
            .trim().replace(/^(\|)|(\|)$/g,'')
            .split("|").map(s => s.trim()).map(s => s.startsWith(":-") && s.endsWith("-:") ? "center" : s.endsWith("-:") ? "right" : s.startsWith(":-") ? "left" : null)
          const headerCells = hdr.trim().replace(/^(\|)|(\|)$/g,'').split("|").map(s => s.trim())
          l += 2
          const rows = []
          while (l < end && /\|/.test(lines[l]) && !isBlank(lines[l])){
            rows.push(lines[l].trim().replace(/^(\|)|(\|)$/g,'').split("|").map(s => s.trim()))
            l++
          }
          tokens.push({ type: "table", header: headerCells, aligns, rows })
          continue
        }
      }
      para.push(line)
      const next = lines[l + 1] || ''
      const endPara =
        isBlank(next) ||
        /^ {0,3}(?:`{3,}|~{3,})/.test(next) ||
        /^(?: {4}|\t)/.test(next) ||
        /^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/.test(next) ||
        /^ {0,3}(#{1,6})/.test(next) ||
        /^ {0,3}>\s?/.test(next) ||
        /^ {0,3}(?:-+\s*|-{3,}|_{3,}|\*{3,})\s*$/.test(next) ||
        (o.gfm && /\|/.test(next) && /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/.test(lines[l + 2] || ''))
      if (endPara) pushParagraph(para)
      l++
    }
    pushParagraph(para)
  }
  parseBlock(0, lines.length)
  function renderInline(s){
    if (!s) return ''
    s = s.replace(/(`+)([^`]|[^`][\s\S]*?[^`])\1/g, (_, ticks, code) => `<code>${escHtml(code)}</code>`)
    s = s.replace(/!\[([^\]]*)\]\(\s*<?([^\s)<>]+)>?\s*(?:(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*\)/g,
      (_, alt, url, t1, t2, t3) => `<img src="${escHtml(url)}" alt="${escHtml(alt)}"${t1||t2||t3?` title="${escHtml(t1||t2||t3)}"`:''}>`)
    s = s.replace(/!\[([^\]]*)\]\[([^\]]*)\]/g, (_, alt, id) => {
      const ref = refs[(id || alt).trim().replace(/\s+/g,' ').toLowerCase()]
      return ref ? `<img src="${escHtml(ref.href)}" alt="${escHtml(alt)}"${ref.title?` title="${escHtml(ref.title)}"`:''}>` : _
    })
    s = s.replace(/\[([^\]]+)\]\(\s*<?([^\s)<>]+)>?\s*(?:(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*\)/g,
      (_, text, url, t1, t2, t3) => `<a href="${escHtml(url)}"${t1||t2||t3?` title="${escHtml(t1||t2||t3)}"`:''}>${text}</a>`)
    s = s.replace(/\[([^\]]+)\]\s*\[([^\]]*)\]/g, (_, text, id) => {
      const key = (id || text).trim().replace(/\s+/g,' ').toLowerCase()
      const ref = refs[key]
      return ref ? `<a href="${escHtml(ref.href)}"${ref.title?` title="${escHtml(ref.title)}"`:''}>${text}</a>` : _
    })
    s = s.replace(/<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^ <>"']+)>/g, (_, url) => `<a href="${escHtml(url)}">${escHtml(url)}</a>`)
    s = s.replace(/<([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/g, (_, mail) => `<a href="mailto:${escHtml(mail)}">${escHtml(mail)}</a>`)
    if (o.gfm){
      s = s.replace(/(?:(?<=\s)|^)(https?:\/\/[^\s<]+)(?=\s|$)/g, '<a href="$1">$1</a>')
      s = s.replace(/(?:(?<=\s)|^)(www\.[^\s<]+)(?=\s|$)/g, '<a href="http://$1">$1</a>')
    }
    s = s.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>').replace(/__([\s\S]+?)__/g, '<strong>$1</strong>')
    s = s.replace(/\*([^*\n]+?)\*/g, '<em>$1</em>').replace(/_([^_\n]+?)_/g, '<em>$1</em>')
    if (o.gfm) s = s.replace(/~~([\s\S]+?)~~/g, '<del>$1</del>')
    s = s.replace(/ {2,}\n/g, "<br>\n")
    if (o.breaks) s = s.replace(/\n/g, "<br>\n")
    s = s.replace(/&(?!#?\w+;)/g, "&amp;").replace(/<(?!\/?[A-Za-z][^>]*>)/g, "&lt;")
    return smart(s)
  }
  let out = ''
  for (const t of tokens){
    switch (t.type){
      case "paragraph":
        out += `<p>${renderInline(t.text)}</p>\n`
        break
      case "heading": {
        const text = renderInline(t.text)
        const id = o.headerIds ? slug(text.replace(/<[^>]+>/g, '')) : null
        out += id ? `<h${t.depth} id="${id}">${text}</h${t.depth}>\n` : `<h${t.depth}>${text}</h${t.depth}>\n`
        break
      }
      case "code": {
        const cls = t.lang ? ` class="language-${escHtml(t.lang)}"` : ''
        out += `<pre><code${cls}>${escHtml(t.text)}</code></pre>\n`
        break
      }
      case "blockquote":
        out += `<blockquote>\n${t.html.trim()}\n</blockquote>\n`
        break
      case "list": {
        const tag = t.ordered ? "ol" : "ul"
        out += `<${tag}>\n`
        for (const it of t.items){
          const task = it.checked === null ? '' : `<input ${it.checked ? 'checked="" ' : ''}disabled="" type="checkbox"> `
          const body = it.html.trim().replace(/^<p>/, task + "<p>")
          out += `<li>${body}</li>\n`
        }
        out += `</${tag}>\n`
        break
      }
      case "table": {
        const ths = t.header.map((h, i) => {
          const a = t.aligns[i]
          return a ? `<th align="${a}">${renderInline(h)}</th>` : `<th>${renderInline(h)}</th>`
        }).join("\n")
        let body = ''
        for (const row of t.rows){
          const tds = row.map((cell, i) => {
            const a = t.aligns[i]
            return a ? `<td align="${a}">${renderInline(cell)}</td>` : `<td>${renderInline(cell)}</td>`
          }).join("\n")
          body += `<tr>\n${tds}\n</tr>\n`
        }
        out += `<table>\n<thead>\n<tr>\n${ths}\n</tr>\n</thead>\n` + (body ? `<tbody>\n${body}</tbody>\n` : '') + `</table>\n`
        break
      }
      case "hr":
        out += "<hr>\n"
        break
      case "html":
        out += t.text + "\n"
        break
    }
  }
  return out.trim()
}
object

%shorthands

/phlo/libs/DOM/shorthands.phlo
version 1.0
creator q-ai.nl
description onChange, onClick and onInput event shorthands
view

script

line 5
这些函数是简短的辅助函数,用于为“change”、“click”和“input”事件添加事件监听器,所需参数较少。
function onChange(els, cb){ on('change', els, cb) }
function onClick(els, cb){ on('click', els, cb) }
function onInput(els, cb){ on('input', els, cb) }
object

%store

/phlo/libs/DOM/store.phlo
version 1.1
creator q-ai.nl
description Statefull/binding engine
view

script

line 5
此代码实现了一个具有观察和自动更新功能的反应式数据存储和绑定系统。它包含通过路径获取和设置数据的方法,信号管理,处理计算之间的依赖关系,以及根据存储更新DOM元素。此外,它还使用代理实现数据的动态访问和修改,并通过数据属性和事件处理程序确保DOM与存储之间的同步。该结构支持自动计算的评估以及在更改时的高效重新计算。
phlo.store = {
	signals: {},
	listeners: {},
	calcs: {},
	calcDeps: {},
	calcVals: {},
	calcTick: false,
	split: (path) => path.replace(/\]/g, '').split(/\.|\[/),
	get(path){
		if (!path) return undefined
		let ctx = phlo.store.signals
		const keys = phlo.store.split(path)
		for (let i = 0; i < keys.length; i++){
			if (ctx == null) return undefined
			ctx = ctx[keys[i]]
		}
		return ctx
	},
	setPath(path, value){
		let keys = phlo.store.split(path)
		let ctx = phlo.store.signals
		while (keys.length > 1){
			const k = keys.shift()
			ctx[k] ??= isNaN(keys[0]) ? {} : []
			ctx = ctx[k]
		}
		const k = keys[0]
		const old = ctx[k]
		if (old === value) return false
		ctx[k] = value
		return true
	},
	set(path, value){
		if (!phlo.store.setPath(path, value)) return
		phlo.store.notify(path, phlo.store.get(path))
		phlo.store.recalc(path)
		phlo.store.schedule()
	},
	on(path, cb){ (phlo.store.listeners[path] ??= new Set).add(cb) },
	off(path, cb){ phlo.store.listeners[path] && phlo.store.listeners[path].delete(cb) },
	reset(){
		phlo.store.signals = {}
		phlo.store.listeners = {}
		phlo.store.calcs = {}
		phlo.store.calcDeps = {}
		phlo.store.calcVals = {}
		phlo.store.calcTick = false
	},
	signal(path, initial){
		if (phlo.store.get(path) === undefined) phlo.store.set(path, initial)
		return { subscribe: (cb) => phlo.store.on(path, cb), unsubscribe: (cb) => phlo.store.off(path, cb) }
	},
	notify(path, val){
		const set = phlo.store.listeners[path]
		if (set) set.forEach(cb => cb(val))
	},
	match(dep, changed){
		if (!dep) return false
		if (dep === changed) return true
		return changed.startsWith(dep + '.') || changed.startsWith(dep + '[') || dep.startsWith(changed + '.') || dep.startsWith(changed + '[')
	},
	depsReady(list){
		const arr = Array.isArray(list) ? list : (list ? [list] : [])
		return arr.every(d => phlo.store.get(d) !== undefined)
	},
	evalCalc(name){
		const fn = phlo.store.calcs[name]
		if (!fn) return
		let deps = []
		let val
		try {
			const out = fn()
			if (Array.isArray(out) && out.length === 2) deps = out[0], val = out[1]
			else val = out
		}
		catch(e){
			deps = []
			val = undefined
		}
		const list = Array.isArray(deps) ? deps : (deps ? [deps] : [])
		phlo.store.calcDeps[name] = list
		if (!phlo.store.depsReady(list)) return
		const old = phlo.store.calcVals[name]
		if (old !== val){
			phlo.store.calcVals[name] = val
			const p = `calc.${name}`
			phlo.store.setPath(p, val)
			phlo.store.notify(p, val)
		}
	},
	recalc(changed){
		const names = Object.keys(phlo.store.calcs)
		for (let i = 0; i < names.length; i++){
			const name = names[i]
			const deps = phlo.store.calcDeps[name] || []
			for (let j = 0; j < deps.length; j++){
				if (phlo.store.match(deps[j], changed)){
					phlo.store.evalCalc(name)
					break
				}
			}
		}
	},
	recalcAll(){
		const names = Object.keys(phlo.store.calcs)
		for (let i = 0; i < names.length; i++) phlo.store.evalCalc(names[i])
	},
	schedule(){
		if (phlo.store.calcTick) return
		phlo.store.calcTick = true
		setTimeout(() => {
			phlo.store.calcTick = false
			phlo.store.recalcAll()
		})
	},
	proxy(base){
		return new Proxy({}, {
			get(t, k){
				if (typeof k === 'symbol') return undefined
				const seg = /^\d+$/.test(k) ? `[${k}]` : (base ? `.${k}` : String(k))
				const path = base + seg
				const v = phlo.store.get(path)
				if (v !== undefined && (typeof v !== 'object' || v === null)) return v
				return phlo.store.proxy(path)
			},
			set(t, k, v){
				const seg = /^\d+$/.test(k) ? `[${k}]` : (base ? `.${k}` : String(k))
				phlo.store.set(base + seg, v)
				return true
			},
			has(t, k){ return phlo.store.get(base + (base ? '.' : '') + String(k)) !== undefined },
			ownKeys(){ return Object.keys(phlo.store.get(base) || {}) },
			getOwnPropertyDescriptor(){ return { enumerable: true, configurable: true } }
		})
	}
}

app.store = phlo.store.proxy('')

app.mod.store = (key, value) => {
	const cur = phlo.store.get(key)
	if (JSON.stringify(cur) === JSON.stringify(value)) return
	const walk = (base, obj) => {
		if (typeof obj !== 'object' || obj === null) return phlo.store.set(base, obj)
		Object.entries(obj).forEach(([k, v]) => walk(isNaN(k) ? `${base}.${k}` : `${base}[${k}]`, v))
	}
	walk(key, value)
}

phlo.calc = new Proxy({}, {
	set(t, k, fn){
		if (typeof fn !== 'function') return false
		phlo.store.calcs[k] = fn
		phlo.store.evalCalc(k)
		setTimeout(() => phlo.store.evalCalc(k))
		return true
	},
	get(t, k){ return phlo.store.calcs[k] },
	has(t, k){ return k in phlo.store.calcs },
	deleteProperty(t, k){
		delete phlo.store.calcs[k]
		delete phlo.store.calcDeps[k]
		delete phlo.store.calcVals[k]
		return true
	}
})

app.calc = new Proxy({}, {
	get(t, k){ return phlo.store.calcVals[k] },
	has(t, k){ return k in phlo.store.calcVals },
	ownKeys(){ return Object.keys(phlo.store.calcVals) },
	getOwnPropertyDescriptor(){ return { enumerable: true, configurable: true } }
})

onExist('[data-bind]', (el) => {
	const key = el.dataset.bind
	const isCalc = key.startsWith('calc.')
	const isInput = el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA'
	const fromDom = isInput ? el.value : el.textContent
	const fromStore = phlo.store.get(key)
	const domNonEmpty = (fromDom ?? '').trim() !== ''
	const storeEmpty = fromStore === undefined || (typeof fromStore === 'string' && fromStore.trim() === '')
	const domLeads = !isCalc && domNonEmpty && storeEmpty
	const S = (v) => v == null ? '' : (typeof v === 'object' ? '' : String(v))
	const apply = (v) => {
		const s = S(v)
		if (isInput) el.value = s
		else el.textContent = s
	}
	phlo.store.on(key, apply)
	if (domLeads) phlo.store.set(key, fromDom)
	const initial = domLeads ? fromDom : fromStore
	apply(initial)
	if (!isCalc && isInput) el.oninput = (e) => phlo.store.set(key, e.target.value)
})

onExist('[data-bind-attr]', (el) => {
	const spec = el.getAttribute('data-bind-attr')
	if (!spec) return
	const BOOL = new Set(['disabled','checked','hidden','required','readonly','selected','autofocus','multiple'])
	let meta = phlo.existing.get(el)
	if (!meta || typeof meta !== 'object'){
		meta = { exist: true }
		phlo.existing.set(el, meta)
	}
	meta.attr || (meta.attr = {})
	meta.attr.cls || (meta.attr.cls = [])
	spec.split(/\s*,\s*/).filter(Boolean).forEach(pair => {
		const m = pair.match(/^\s*([^:]+)\s*:\s*(.+)\s*$/)
		if (!m) return
		const name = m[1]
		const path = m[2]
		const isCalc = path.startsWith('calc.')
		const isInputVal = name === 'value' && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')
		const domVal =
			name === 'text' ? el.textContent :
			name === 'html' ? el.innerHTML :
			name === 'value' ? el.value :
			(name === 'class' ? null : el.getAttribute(name))
		const fromStore = phlo.store.get(path)
		const domNonEmpty = (domVal ?? '').trim() !== ''
		const storeEmpty = fromStore === undefined || (typeof fromStore === 'string' && fromStore.trim() === '')
		const domLeads = !isCalc && name !== 'class' && domNonEmpty && storeEmpty
		const S = (v) => v == null ? '' : (typeof v === 'object' ? '' : String(v))
		const apply = (v) => {
			if (name === 'text') el.textContent = S(v)
			else if (name === 'html') app.mod.inner(el, S(v))
			else if (name === 'value') app.mod.value(el, S(v))
			else if (name === 'class'){
				const next = Array.isArray(v) ? v : (v && typeof v === 'object') ? Object.keys(v).filter(k => v[k]) : String(v ?? '').split(/\s+/)
				const uniq = [...new Set(next.filter(Boolean))]
				const prev = meta.attr.cls
				for (let i = 0; i < prev.length; i++) el.classList.remove(prev[i])
				for (let i = 0; i < uniq.length; i++) el.classList.add(uniq[i])
				meta.attr.cls = uniq
			}
			else if (BOOL.has(name)){
				const on = !!v
				app.mod.attr(el, { [name]: on ? '' : null })
				if (name in el) el[name] = on
			}
			else app.mod.attr(el, { [name]: S(v) })
		}
		phlo.store.on(path, apply)
		if (domLeads) phlo.store.set(path, domVal)
		const initial = domLeads ? domVal : fromStore
		apply(initial)
		if (!isCalc && isInputVal) el.oninput = (e) => phlo.store.set(path, e.target.value)
	})
})
object

%template

/phlo/libs/DOM/template.phlo
version 1.0
creator q-ai.nl
description Single Page App client side templating
advice Add cb's to the templates object and output via apply(template: [$name => $rows, $name2 => $rows2, etc])
view

script

line 6
此函数对“rows”中的每一行执行模板函数,将行的值作为参数传递。
app.mod.template = (template, rows) => rows.forEach(row => templates[template](...Object.values(row))),
const templates = {}
object

%timestamps

/phlo/libs/DOM/timestamps.phlo
version 1.0
creator q-ai.nl
description DOM live timestamps
advice Create an app.tsLabels array to overwrite the tsBase labels in any language
view

script

line 6
此代码定义了一个函数,用于计算具有特定数据标签(dataset.ts)元素的年龄(以秒为单位),并将其转换为可读的时间显示,基于一组时间间隔。该函数每秒刷新一次,并在启动时更新,从而显示实时的时间流逝。
app.tsBase = {seconds: 60, minutes: 60, hours: 24, days: 7, weeks: 4, months: 13, years: 1}
const tsUpdate = () => (ranges = app.tsLabels && (tsValues = Object.values(app.tsBase)) ? Object.fromEntries(app.tsLabels.map((k, i) => [k, tsValues[i]])) : app.tsBase) && objects('[data-ts]').forEach(el => {
	let age = Math.round(Date.now() / 1000) - Number(el.dataset.ts), text = ''
	const future = age < 0
	if (future) age = -age
	for (const [range, multiplier] of Object.entries(ranges)){
		if (text) continue
		if (age / multiplier < 1.6583) text = `${Math.round(age)} ${range}`
		age /= multiplier
	}
	text ||= `${Math.round(age)} ${Object.keys(ranges).at(-1)}`
	text = `${future ? '-' : ''}${text}`
	el.innerText === text || (el.innerText = text)
})
setInterval(() => app.state === 'active' && tsUpdate(), 1000)
setTimeout(tsUpdate, 1)
object

%visible

/phlo/libs/DOM/visible.phlo
version 1.0
creator q-ai.nl
description onVisible and onVisibleIn (for nested scrolling DOM elements) functions
view

script

line 5
此代码定义了一个观察系统,用于响应元素的可见性。它记录观察,定期通过更新回调进行检查,并使用IntersectionObserver检测元素何时变得可见。在观察时,执行相应的回调(显示或隐藏)。
phlo.observe = []
phlo.observing = new WeakMap

const onVisible = (els, cbIn, cbOut) => onVisibleIn(els, null, cbIn, cbOut)
const onVisibleIn = (els, root, cbIn, cbOut) => phlo.observe.push({els, root, cbIn, cbOut})

app.updates.push(() => {
	const observers = []
	phlo.observe.forEach(item => objects(item.els).forEach(el => phlo.observing.has(el) || observers.push({el, root: item.root, cbIn: item.cbIn, cbOut: item.cbOut})))
	observers.forEach(item => [phlo.observing.has(item.el) || phlo.observing.set(item.el, 'observe'), (observer = new IntersectionObserver(entries => entries.forEach(entry => entry.isIntersecting ? !item.cbIn && item.cbOut ? [observer.unobserve(entry.target), item.cbOut(entry.target)] : item.cbIn(entry.target) : item.cbIn && item.cbOut && item.cbOut(entry.target)), {root: obj(item.root), threshold: .1})).observe(item.el)])
})

Fields

object

%field_bool

/phlo/libs/Fields/bool.phlo
extends field
class field_bool
version 1.0
creator q-ai.nl
description CMS boolean field
prop

%field_bool -> true

line 7
此节点始终返回值“✅”,无论输入或上下文如何。
'✅'
prop

%field_bool -> false

line 8
显示错误信息或负面状态。
'❌'
method

%field_bool -> label ($record, $CMS)

line 10
显示一个基于字段 `$record->{$this->name}` 是否为真(真值)的值,通过返回 `'true'`,否则返回 `'false'`。
$record->{$this->name} ? $this->true : $this->false
method

%field_bool -> input ($record, $CMS)

line 11
创建一个带有标签的复选框输入和一个样式化的滑块。当记录的值不为真时,复选框被选中。
tag('label', inner: input(type: 'checkbox', name: $this->name, value: 1, checked: $record->{$this->name} ? false : null).tag('span', class: 'slider', inner: void))
method

%field_bool -> parse ($record)

line 12
此代码将 `$payload` 中字段的值转换为布尔值,并将其分配给 `$record` 中的相应字段。
$record->{$this->name} = (bool)%payload->{$this->name};
method

%field_bool -> sql

line 14
返回一个SQL语句,定义一个列名为property的列,数据类型为tinyint(1) unsigned,适用于布尔值。
"`$this->name` tinyint(1) unsigned"
method

%field_bool -> nullable

line 15
此节点始终返回值 false,无论输入或上下文如何。
false
object

%field_child

/phlo/libs/Fields/child.phlo
extends field
class field_child
version 1.0
creator q-ai.nl
description CMS model child relation field
prop

%field_child -> list

line 7
它计算列表或集合中的元素数量。
'count'
prop

%field_child -> change

line 8
这段代码总是返回假,无论输入或情况如何。
false
prop

%field_child -> create

line 9
此代码总是返回负结果,因此不会执行某个操作或创建。
false
prop

%field_child -> record

line 10
这是一个返回列表的方法的属性,可能由子字段或相关记录组成。
'list'
method

%field_child -> count ($record, $CMS)

line 12
创建一个HTML链接,指向特定记录,基于记录和对象数据的动态URL,并在标题旁显示相关项目的数量。
tag('a', href: slash.$CMS->uriRecord.slash.$record->id.slash.$this->name, class: 'async', inner: $record->getCount($this->name).space.$this->title)
method

%field_child -> last ($record)

line 13
在记录中存储特定字段的最新值,通过获取与字段名称匹配的最新值。
$record->getLast($this->name)
method

%field_child -> label ($record, $CMS)

line 14
此代码生成一个合并的链接列表,适用于集合中的每个元素,元素之间不使用分隔符,并在列表为空或为假时显示一个短横线。
implode(loop($record->{$this->name}, fn($child) => $this->link($child, $CMS))) ?: dash
method

%field_child -> input ($record, $CMS)

line 15
返回记录的标签。
$this->label($record)
method
line 16
根据记录数据生成一个URL,并添加一个带有生成的href的HTML链接,包括用于异步功能的CSS类。
$parentPath = $CMS->uriRecord.'/'.$record->{$CMS->model}->id;
$relationshipName = $this->name;
$href = "/{$parentPath}/{$relationshipName}/{$record->id}";
return tag('a', href: $href, class: 'async', inner: $record);
object

%field_date

/phlo/libs/Fields/date.phlo
extends field
class field_date
version 1.0
creator q-ai.nl
description CMS date field
prop

%field_date -> handle

line 7
此节点始终返回 `true`,无论输入或上下文如何。
true
method

%field_date -> sql

line 9
这表示该值是一个非负日期,在SQL中类型为DATE,不允许负值。
"`$this->name` DATE unsigned"
object

%field_datetime

/phlo/libs/Fields/datetime.phlo
extends field
class field_datetime
version 1.0
creator q-ai.nl
description CMS date-time field
prop

%field_datetime -> handle

line 7
此节点始终返回值为真(true)。
true
prop

%field_datetime -> change

line 8
检查名称是否不等于“created”、“changed”或“updated”。
!in_array($this->name, ['created', 'changed', 'updated'])
prop

%field_datetime -> create

line 9
检查名称是否不等于“created”、“changed”或“updated”。
!in_array($this->name, ['created', 'changed', 'updated'])
method

%field_datetime -> label ($record, $CMS)

line 11
显示一个时钟图标和一个人类时间指示,如果值存在;否则显示一条横线。
($value = $record->{$this->name}) ? tag('i', class: 'icon clock-'.$this->labelIconClass(time() - $value), inner: void).tag('span', data_ts: $record->{$this->name}, inner: time_human($record->{$this->name})) : dash
method

%field_datetime -> labelIconClass ($value)

line 12
代码块根据值设置颜色:如果值大于86400则为红色,介于3600和86400之间则为黄色,较低的值则为蓝色。
$value > 86400 ? 'red' : ($value > 3600 ? 'yellow' : 'blue')
method

%field_datetime -> input ($record, $CMS)

line 13
创建一个类型为'datetime-local'的日期时间输入字段,显示一个样式化的输入框,其值设置为记录中的格式化日期和时间值。
input(type: 'datetime-local', name: $this->name, value: date('Y-m-d\TH:i', $record->{$this->name}), class: 'field')
method

%field_datetime -> parse ($record)

line 14
此代码处理记录的日期。如果字段名为“created”且尚无值,则设置为当前时间。对于“changed”或“updated”,则分配当前时间。如果存在payload值,则将其转换为时间戳(使用strtotime)并分配给记录。
if ($this->name === 'created') $record->created ??= time()
elseif (in_array($this->name, ['changed', 'updated'])) $record->{$this->name} = time()
elseif ($payload = %payload->{$this->name}) $record->{$this->name} = strtotime($payload)
method

%field_datetime -> sql

line 20
该代码定义了一个名为 `$this->name` 的数据库列,类型为无符号整数,长度为10。
"`$this->name` int(10) unsigned"
object

%field_email

/phlo/libs/Fields/email.phlo
extends field_text
class field_email
version 1.0
creator q-ai.nl
description CMS email field
view

%field_email -> label ($record, $CMS)

line 8
此代码生成一个带有记录中电子邮件地址的mailto链接,用户只需点击链接即可直接发送电子邮件。
<a href="mailto:{{ $record->{$this->name} }}">{{ $record->{$this->name} }}</a>
object

%field_many

/phlo/libs/Fields/many.phlo
extends field
class field_many
version 1.0
creator q-ai.nl
description CMS model many-to-many relation field
prop

%field_many -> list

line 7
这段代码返回文本“label”。
'label'
prop

%field_many -> record

line 8
此节点仅传递“label”的值,未进行任何处理或上下文。
'label'
prop

%field_many -> create

line 9
此代码总是返回true。
false
prop

%field_many -> change

line 10
此节点始终返回值 `false`,无论输入如何。
false
method

%field_many -> count ($record)

line 12
这段代码调用 `$record` 对象的 `getCount` 方法,参数是字段的名称,可能返回项目的数量或某个字段出现的次数。
$record->getCount($this->name)
method

%field_many -> label ($record, $CMS)

line 13
遍历关系中的元素,为每个元素创建一个链接,并返回结果;如果为空,则显示一个连字符。
loop($record->{$this->name}, fn($relation) => $this->link($relation), lf) ?: dash
method
line 14
创建一个带有基于记录数据的动态URL的超链接,包括用于异步操作的类,并将记录显示为链接文本。
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $record)
method

%field_many -> input ($record, $CMS)

line 15
根据记录和CMS显示标签。
$this->label($record, $CMS)
method

%field_many -> sql

line 17
结果是一个空数组,表示该方法或函数没有返回数据或没有执行任何操作。
[]
object

%field_number

/phlo/libs/Fields/number.phlo
extends field
class field_number
version 1.0
creator q-ai.nl
description CMS number field
prop

%field_number -> decimals

line 7
返回带有零小数位的数字,即整数值。
prop

%field_number -> length

line 8
这段代码简单地返回数字5,可能表示一个字段的长度或类似的值。
5
prop

%field_number -> min

line 9
此代码总是返回0。
method

%field_number -> label ($record, $CMS)

line 11
格式化记录中的数字,保留特定的小数位数,使用逗号作为小数分隔符,点作为千位分隔符。
number_format($record->{$this->name}, $this->decimals, comma, dot)
method

%field_number -> input ($record, $CMS)

line 12
创建一个具有动态值、默认值、基于小数点的步长、最小值和CSS类的数字输入字段。
input(type: 'number', name: $this->name, value: $record->{$this->name} ?? $this->default, step: $this->decimals ? dot.str_repeat('0', $this->decimals - 1).'1' : null, min: $this->min, class: 'field')
method

%field_number -> sql

line 14
此代码生成一个SQL字段定义,根据是否存在小数来定义数字字段。如果有小数,则使用带有自定义精度和标度的“decimal”数据类型;否则使用“smallint”。在两种情况下都添加了无符号选项。
"`$this->name` ".($this->decimals ? 'decimal('.($this->length + $this->decimals).comma.$this->decimals.')' : "smallint($this->length)").' unsigned'
object

%field_parent

/phlo/libs/Fields/parent.phlo
extends field
class field_parent
version 1.0
creator q-ai.nl
description CMS model parent relation field
method

%field_parent -> label ($record, $CMS)

line 7
检查一个字段是否是模型对象;如果是,则生成一个链接,否则显示一个短横线。
is_a($obj = $record->{$this->name}, 'model') ? $this->link($obj) : dash
method

%field_parent -> input ($record, $CMS)

line 8
此代码生成一个基于选项列表的HTML选择元素。对于每个选项,都会检查它是否与记录中的值匹配,如果匹配,则添加“selected”属性。
select(name: $this->name, inner: loop($this->options, fn($parent) => '<option'.($parent->id === $record->{$this->name}?->id ? ' selected' : void).' value="'.$parent->id.'">'.$parent, void))
method
line 9
创建一个具有动态生成URL的HTML锚点链接,并添加内容或使用记录作为内容,带有CSS类“async”。
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $content ?? $record)
prop

%field_parent -> options

line 10
在类或对象的上下文中检索所有可用的记录。
$this->obj::records()
method

%field_parent -> sql

line 12
定义一个名为 `$this->name` 的 varchar 字段,最大长度为 10 个字符。
"`$this->name` varchar(10)"
object

%field_password

/phlo/libs/Fields/password.phlo
extends field
class field_password
version 1.0
creator q-ai.nl
description CMS password field
prop

%field_password -> list

line 7
此节点始终返回值 false,无论输入或上下文如何。
false
prop

%field_password -> required

line 8
此节点始终返回值“true”,这意味着该字段始终被标记为必填。
true
prop

%field_password -> minlength

line 9
密码的最小长度为8个字符。
8
prop

%field_password -> placeholder

line 10
它返回一个文本标签,指示新密码字段。
'Nieuw wachtwoord'
method

%field_password -> input ($record, $CMS)

line 12
创建一个具有类型、名称、占位符和固定CSS类的输入字段。
input(type: $this->type, name: $this->name, placeholder: $this->placeholder, class: 'field')
method

%field_password -> label ($record, $CMS)

line 13
显示一个固定的、加密的或隐藏的密码标签,无论输入或上下文如何。
'••••••••'
method

%field_password -> parse ($record)

line 14
此代码从有效载荷中获取密码值,删除任何空格,如果提供了密码,则使用bcrypt进行哈希并存储在记录中。
($password = trim(%payload->{$this->name})) && $record->{$this->name} = password_hash($password, PASSWORD_BCRYPT)
method

%field_password -> sql

line 16
表示字段“name”在SQL表中被定义为最大长度为60个字符的字符串。
"`$this->name` char(60)"
object

%field_price

/phlo/libs/Fields/price.phlo
class field_price
version 1.0
creator q-ai.nl
description CMS price field
extends field_number
prop

%field_price -> decimals

line 7
此代码返回数字2,可能表示应显示或使用2个小数位。
2
object

%field_select

/phlo/libs/Fields/select.phlo
extends field
class field_select
version 1.0
creator q-ai.nl
description CMS select field
method

%field_select -> input ($record, $CMS)

line 7
生成一个具有动态名称和选项的 `<select>` 元素,并根据记录将相应的选项标记为已选择。
select(name: $this->name, inner: loop($this->options, fn($option) => "<option".($record->{$this->name} === $option ? ' selected' : void).">$option", void))
method

%field_select -> sql

line 9
这定义了一个名为 `$this->name` 的SQL列,它是一个ENUM类型,包括被合并成列表的选项,如 `'option1','option2',...`。
"`$this->name` enum('".implode("','", $this->options)."')"
object

%field_text

/phlo/libs/Fields/text.phlo
extends field
class field_text
version 1.0
creator q-ai.nl
description CMS text field
prop

%field_text -> length

line 7
此节点返回值为100,可能是文本的长度或类似的数值。
100
prop

%field_text -> multiline

line 8
此属性检查文本长度是否超过250个字符。
$this->length > 250
method

%field_text -> input ($record, $CMS)

line 10
根据条件选择使用多重输入字段或单一输入字段。
$this->multiline ? $this->inputMulti($record) : $this->inputField($record)
method

%field_text -> inputField ($record)

line 11
创建一个具有类型、名称、值、最大长度、占位符和CSS类的HTML输入字段,这些属性基于对象属性。如果存在,则对值进行转义;否则使用默认值。
input(type: $this->type, name: $this->name, value: ($value = $record->{$this->name}) ? esc($value) : $this->default, maxlength: $this->length, placeholder: $this->placeholder, class: 'field')
method

%field_text -> inputMulti ($record)

line 12
创建一个具有名称、占位符和类的文本区域元素。如果记录中存在该名称的值,则对其进行转义并填充;否则,如果存在默认值,则使用默认值。
textarea(name: $this->name, inner: ($value = $record->{$this->name}) ? esc($value) : $this->default ?? void, placeholder: $this->placeholder, class: 'field')
method

%field_text -> sql

line 14
定义一个名为 `$this->name` 的SQL字段,数据类型为varchar,长度为 `$this->length`。
"`$this->name` varchar($this->length)"
object

%field_token

/phlo/libs/Fields/token.phlo
extends field
class field_token
version 1.0
creator q-ai.nl
description CMS token type field
prop

%field_token -> length

line 7
返回内容的长度,在这种情况下为8。
8
prop

%field_token -> default

line 8
用长度等于 `$this->length` 的值获取一个令牌。
token($this->length)
prop

%field_token -> create

line 9
此代码始终返回false,无论输入或条件如何。
false
prop

%field_token -> change

line 10
此代码始终返回`false`,无论上下文或输入如何。
false
prop

%field_token -> search

line 11
此代码确保始终返回一个位置,无论输入如何。它充当搜索成功的确认,或者结果始终为真。
true
method

%field_token -> label ($record, $CMS)

line 13
创建一个HTML-div元素,其内容为记录中字段的转义值。
tag('div', inner: esc($record->{$this->name}))
method

%field_token -> parse ($record)

line 15
如果该字段的值尚不存在,则将其设置为记录中的值,否则将其更改为默认值。
$record->{$this->name} ??= $this->default
method

%field_token -> sql

line 17
此代码定义了一个 varchar 列,其名称由 `$this->name` 的值确定,长度由 `$this->length` 决定。
"`$this->name` char($this->length)"
object

%field_virtual

/phlo/libs/Fields/virtual.phlo
extends field
class field_virtual
version 1.0
creator q-ai.nl
description CMS virtual field
prop

%field_virtual -> create

line 7
此方法或函数始终返回 false,可能旨在防止某个操作或验证,或将其标记为不成功。
false
prop

%field_virtual -> change

line 8
这段代码将始终返回假。
false
method

%field_virtual -> sql

line 10
此节点返回一个空数组,没有其他操作或数据。
[]

Files

object

%CSV

/phlo/libs/Files/CSV.phlo
version 1.0
creator q-ai.nl
description CSV reader library
static

CSV :: __handle

line 5
这似乎是一个属性,返回一个具有动态路径的文件或父引用,该路径由路径和文件名的变量组成。
"CSV/$path$filename"
method

%CSV -> __construct (string $filename, ?string $path = null)

line 6
检查路径是否已设置,如果没有,则使用默认路径;组合文件名和扩展名;检查CSV文件是否可读,如果可以,则调用读取函数。
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.csv'
if (is_readable($this->objFile)) $this->objRead()
readonly

%CSV -> objFile:string

line 12
此节点读取一个CSV文件并将其连接到一个变量,以便可以进一步处理数据。
method

%CSV -> objRead

line 14
处理一个CSV文件,通过打开它,读取第一行(标题),根据逗号或分号的出现确定正确的分隔符,拆分标题,然后读取所有行。每一行与标题关联并存储在数组中。
$fp = fopen($this->objFile, 'r+')
$headers = str_replace([dq, cr, lf], void, fgets($fp))
$delimiter = substr_count($headers, comma) > substr_count($headers, semi) ? comma : semi
$headers =  explode($delimiter, $headers)
while ($row = fgetcsv($fp, null, $delimiter)) $this->objData[] = array_combine($headers, $row)
fclose($fp)
object

%file

/phlo/libs/Files/file.phlo
version 1.0
creator q-ai.nl
description File library
static

file :: __handle

line 5
此代码通过将'file/'与变量'$file'结合来构建路径,并在存在时添加一个可选的名称'$name',用斜杠连接。
"file/$file".($name ? "/$name" : void)
method

%file -> __construct (public string $file, ?string $name = null, $contents = null, ...$args)

line 6
如果提供了名称,则将其设置为该名称;如果内容是字符串,则将内容写入文件;如果存在其他对象或参数,则导入它们。
$name && $this->name = $name
is_string($contents) && $this->write($contents)
$args && $this->objImport(...$args)
method

%file -> append (string $data)

line 12
将提供的文本添加到文件末尾而不覆盖它
file_put_contents($this->file, $data, FILE_APPEND)
prop

%file -> basename

line 13
获取指定文件的基础名称(不带路径的文件名)。
pathinfo($this->file, PATHINFO_BASENAME)
method

%file -> base64

line 14
将文件内容转换为Base64编码的字符串。
base64_encode($this->contents)
method

%file -> contents

line 15
读取由属性中的文件名指定的文件内容
file_get_contents($this->file)
method

%file -> contentsINI (bool $parse = true)

line 16
创建一个INI字符串,根据参数解析内容,并在 `$parse` 为真时返回带有类型透明度的结果。
parse_ini_string($this->contents, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW)
method

%file -> contentsJSON ($assoc = null)

line 17
将JSON字符串的内容转换为PHP变量,结果根据参数返回数组或对象。
json_decode($this->contents, $assoc)
method

%file -> copy ($to)

line 18
创建文件的副本并将其保存到指定位置。
copy($this->file, $to)
method

%file -> created

line 19
返回文件的创建时间。
filectime($this->file)
method

%file -> createdAge

line 20
此节点计算自创建以来的年龄,通过将创建日期与当前日期进行比较。
age($this->created)
method

%file -> createdHuman

line 21
此代码将时间戳转换为人类可读的格式。
time_human($this->created)
method

%file -> curl ($type = null, $filename = null)

line 22
创建一个带有指定文件名、类型和可选上传时使用的另一个文件名的新的 CURLFile 对象。
new CURLFile($this->file, $type, $filename)
method

%file -> delete

line 23
执行检查以确认文件是否存在,并尝试删除它。然后记录一条调试信息,指示删除是否成功。
first($deleted = $this->exists && unlink($this->file), debug($deleted ? "Deleted $this->basename" : "Could not delete $this->basename"))
method

%file -> exists

line 24
检查存储在变量 `$this->file` 中的文件是否存在于系统中。
file_exists($this->file)
prop

%file -> ext

line 25
从文件名中获取文件扩展名。
pathinfo($this->name, PATHINFO_EXTENSION)
prop

%file -> filename

line 26
从存储在变量中的路径或文件名中提取不带扩展名的文件名
pathinfo($this->file, PATHINFO_FILENAME)
method

%file -> getLine

line 27
从文件中提取一行,删除末尾的空白字符,并返回;如果已到达文件末尾,则返回false。
($line = fgets($this->pointer)) === false ? false : rtrim($line)
method

%file -> getLength (int $length)

line 28
从指定的指针读取一定数量的字节($length)文件。
fread($this->pointer, $length)
method

%file -> is (string $file)

line 29
比较所提供的文件名是否等于内部属性 `$file`。
$file === $this->file
method

%file -> md5

line 30
此代码计算由属性 `$file` 显示的文件的MD5哈希值。
md5_file($this->file)
prop

%file -> mime

line 31
根据文件名确定文件的MIME类型。
mime($this->name)
method

%file -> modified

line 32
返回文件的最后修改日期和时间。
filemtime($this->file)
method

%file -> modifiedAge

line 33
根据修改后的日期返回年龄
age($this->modified)
method

%file -> modifiedHuman

line 34
此节点返回以人类可读的时间格式显示的最新更改。
time_human($this->modified)
method

%file -> move ($to)

line 35
将文件移动到新位置并更新内部引用以匹配更改后的文件名或路径。
rename($this->file, $to) && $this->file = $to
prop

%file -> name

line 36
这会获取文件名而不带路径,通常用于获取不带目录的名称。
$this->basename
method

%file -> output ($download = false)

line 37
生成一个包含内容、名称和可选下载的文件的输出。
output($this->contents, $this->name, $download)
prop

%file -> path

line 38
此代码返回包含该文件的目录的绝对路径,后跟一个斜杠。
realpath(pathinfo($this->file, PATHINFO_DIRNAME)).slash
prop

%file -> pathRel

line 39
检查文件路径是否以特定的相对根路径开头,如果是,则返回从该点开始的路径;否则显示完整路径。
str_starts_with($this->file, $relRoot = dirname(dirname($_SERVER['DOCUMENT_ROOT'])).slash) ? substr($this->file, strlen($relRoot)) : $this->file
prop

%file -> pointer

line 40
此代码以读写模式打开一个文件。
fopen($this->file, 'r+')
method

%file -> readable

line 41
检查与变量'file'关联的文件是否可读。
is_readable($this->file)
method

%file -> src

line 42
创建一个带有MIME类型和base64数据的数据URI,适用于例如图像或文件引用。
"data:$this->mime;base64,$this->base64"
method

%file -> size

line 43
返回文件的大小。
filesize($this->file)
method

%file -> sizeHuman (int $precision = 0)

line 44
将文件大小转换为人类可读的字符串,带有可选的精确度。
size_human($this->size, $precision)
method

%file -> sha1

line 45
此方法计算文件的SHA-1哈希值。
sha1_file($this->file)
method

%file -> shortenTo (int $length)

line 46
此方法将文件名缩短到指定的长度。如果名称已经比长度短,则返回原始名称。否则,将添加省略号,以确保最终长度不超过指定值,包括文件扩展名。
strlen($this->name) <= $length ? $this->name : substr($this->name, 0, $length - strlen($this->ext) - 3).dot.dot.dot.$this->ext
method

%file -> title

line 47
将不带扩展名的文件名转换为标题,方法是将第一个字母大写,并用空格替换下划线或其他非字母字符
ucfirst(strtr(pathinfo($this->name, PATHINFO_FILENAME), [us => space]))
method

%file -> token ($length = 20)

line 48
生成一个具有指定长度的令牌,默认使用SHA-1哈希函数来生成该令牌。
token($length, sha1: $this->sha1)
method

%file -> type

line 49
提取MIME值中斜杠之前的部分,例如类型如“image”或“video”。
substr($this->mime, 0, strpos($this->mime, slash))
method

%file -> touch

line 50
如果尚不存在,则创建具有指定名称(由 `$this->file` 提供)的文件,或更新该文件的访问时间。
touch($this->file)
method

%file -> writeable

line 51
检查文件是否可写。
is_writeable($this->file)
method

%file -> writeINI ($data, bool $deleteEmpty = false)

line 52
将数据写入INI文件,不删除空节,除非另行说明,并通过转义每个值并将其格式化为用双引号括起来的键值对,用换行符分隔。
$this->write(!$deleteEmpty || $data ? loop($data, fn($value, $key) => $key.' = '.dq.strtr($value, [dq => bs.dq, lf => '\n']).dq, lf).lf : void, $deleteEmpty)
method

%file -> writeJSON ($data, bool $deleteEmpty = false)

line 53
此代码调用一个方法来写入数据。如果 `$deleteEmpty` 不为真或 `$data` 存在,则将数据进行 JSON 编码并传递;否则不写入任何数据。
$this->write(!$deleteEmpty || $data ? json_encode($data, jsonFlags) : void, $deleteEmpty)
method

%file -> writeJSONplain ($data, bool $deleteEmpty = false)

line 54
此代码检查是否应写入空数据。如果$deleteEmpty为假或$data不为空,则将数据作为JSON代码进行评估并传递给写入函数;否则不写入任何内容。
$this->write(!$deleteEmpty || $data ? json_encode($data) : void, $deleteEmpty)
method

%file -> write (string $data, bool $deleteEmpty = false)

line 55
此方法将数据写入文件,如果数据为空且参数为true,则删除该文件。写入成功后会记录调试信息,否则会报告错误。
if (!$data && $deleteEmpty) return $this->delete
if ($written = file_put_contents($this->file, $data) !== false) debug('Written '.$this->basename.' ('.$this->sizeHuman.')')
else error('Could not write '.$this->file)
return $written
method

%file -> objInfo

line 62
此代码创建一个数组,将键合并,值取自当前对象属性。它添加基本信息,并根据条件还添加额外的文件信息,如大小、创建和修改日期,以及MIME类型。
array_combine($keys = array_merge(['file', 'name', 'exists'], $this->exists ? ['sizeHuman', 'createdHuman', 'modifiedHuman', 'mime'] : []), loop($keys, fn($arg) => $this->$arg))
object

%img

/phlo/libs/Files/img.phlo
version 1.0
creator q-ai.nl
description GD/img library
static

img :: detect ($data)

line 5
此代码根据数据的前几个字节检测图像格式,并通过比较特定的头信息返回相应的文件类型('jpg'、'png'、'gif'、'webp'、'bmp'、'tiff')。
$header = substr($data, 0, 12)
if (substr($header, 0, 3) === "\xFF\xD8\xFF") return 'jpg'
if (substr($header, 0, 8) === "\x89PNG\x0D\x0A\x1A\x0A") return 'png'
if (substr($header, 0, 6) === 'GIF87a' || substr($header, 0, 6) === 'GIF89a') return 'gif'
if (substr($header, 0, 4) === 'RIFF' && substr($header, 8, 4) === 'WEBP') return 'webp'
if (substr($header, 0, 2) === "BM") return 'bmp'
if (substr($header, 0, 4) === "\x49\x49\x2A\x00" || substr($header, 0, 4) === "\x4D\x4D\x00\x2A") return 'tiff'
static

img :: search ($search)

line 15
根据格式化的搜索词在谷歌上搜索图片结果,从HTML中提取多个base64编码的JPEG数据,按大小排序,选择几个,然后随机挑选一个,最终解码为图片文件。
$q = strtr($search, [dash => '+'])
$DOM = HTTP("https://www.google.com/search?q=$q&source=lnms&tbm=isch&tbs=", agent: true)
$sources = regex_all('/var s=\'data:image\/jpeg;base64,([^\']{1000,})/', $DOM)[1]
usort($sources, fn($a, $b) => strlen($b) <=> strlen($a))
$sources = array_slice($sources, 0, 5)
shuffle($sources)
$source = current($sources)
$source = base64_decode($source)
return $source
static

img :: __handle

line 27
在字符串 "img/" 之前输出变量 `$file` 的值
"img/$file"
method

%img -> __construct (public string $file)

line 28
构造函数接受一个字符串参数,该参数存储在公共属性中,可能用于表示节点的文件名或路径。
prop

%img -> src:GdImage

line 30
从文件中加载一张图片,读取内容并将其转换为图像资源。
imagecreatefromstring(file_get_contents($this->file))
prop

%img -> width

line 31
此节点通过在源图像上使用 `imagesx` 函数获取图像的宽度,从而显示图像的宽度。
imagesx($this->src)
prop

%img -> height

line 32
此节点通过调用 `imagesx()` 获取源(`$this->src`)的宽度,返回图像的宽度(宽度)。
imagesx($this->src)
method

%img -> scale ($width, $height = false, $crop = true)

line 34
基于提供的宽度和高度进行比例缩放,包括可选的裁剪和保持比例,支持正确的图像重采样和透明度。
if (!$width && !$height) return $this
$scale = 1
if ($width && $height) $scale = $crop ? max($width / $this->width, $height / $this->height) : min($width / $this->width, $height / $this->height)
elseif ($width) $scale = $width / $this->width
elseif ($height) $scale = $height / $this->height
if ($scale > 1) return $this
if ($width && $height && $this->width / $this->height != $width / $height){
	if ($width / $height > $this->width / $this->height){
		if ($crop){
			$destX = $width
			$destY = round($width / $this->width * $this->height)
			if ($crop === 'top') $offsetY = 0
			elseif ($crop === 'bottom') $offsetY = -$destY - -$height
			else $offsetY = -round(($destY - $height) / 2)
		}
		else {
			$destX = round($this->width * $height / $this->height)
			$destY = $height
			$width = $destX
			$offsetY = 0
		}
		$offsetX = 0
	}
	else {
		if ($crop){
			$destX = round($height / $this->height * $this->width)
			$destY = $height
			$offsetX = -round (($destX - $width) / 2)
		}
		else {
			$destX = $width
			$destY = round($this->height * $width / $this->width)
			$height = $destY
			$offsetX = 0
		}
		$offsetY = 0
	}
	$destImg = imagecreatetruecolor($width, $height)
	imagealphablending($destImg, false)
	imagesavealpha($destImg, true)
	imagecopyresampled($destImg, $this->src, $offsetX, $offsetY, 0, 0, $destX, $destY, $this->width, $this->height)
}
else {
	if ($width){
		$destX = $width
		$destY = $width / $this->width * $this->height
	}
	elseif ($height){
		$destX = $height / $this->height * $this->width
		$destY = $height
	}
	$destImg = imagecreatetruecolor($destX, $destY)
	imagealphablending($destImg, false)
	imagesavealpha($destImg, true)
	imagecopyresampled($destImg, $this->src, 0, 0, 0, 0, $destX, $destY, $this->width, $this->height)
}
$this->src = $destImg
return $this
method

%img -> source

line 95
将`imagejpeg()`函数的输出捕获到缓冲区并作为字符串返回,从而使图像在内存中可用于进一步处理。
ob_start()
imagejpeg($this->src)
return ob_get_clean()
method

%img -> save ($file = null)

line 101
根据扩展名保存图像文件;对于“png”和“webp”应用压缩,"gif"则不压缩,默认使用JPEG,质量为85%。
$file && $this->file = $file
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION))
if ($ext === 'png') return imagepng($this->src, $this->file, 8)
if ($ext === 'gif') return imagegif($this->src, $this->file)
if ($ext === 'webp'){
	imageistruecolor($this->src) || imagepalettetotruecolor($this->src)
	return imagewebp($this->src, $this->file)
}
return imagejpeg($this->src, $this->file, 85)
object

%INI

/phlo/libs/Files/INI.phlo
version 1.0
creator q-ai.nl
description Generic INI library
prop

%INI -> objFile:string

line 5
此节点可能初始化一个文件或对象,取决于上下文,并为所选文件或对象执行设置或初始化。
static

INI :: __handle

line 7
此代码通过组合路径和文件名来构建文件名,并在参数 `$parse` 不为真时添加 '/0'。
"INI/$path$filename".(!$parse ? '/0' : void)
method

%INI -> __construct (string $filename, ?string $path = null, bool $parse = true)

line 8
此构造函数根据可选路径和名称初始化文件名,通过应用点替换。然后它检查ini文件是否可读,并根据解析参数的不同进行读取。
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.ini'
if (is_readable($this->objFile)) $this->objRead($parse)
method

%INI -> objRead ($parse = true)

line 14
此代码使用parse_ini_file读取一个INI文件,并将内容存储在objData中。然后对该数组的最后一个值进行评估,并将objChanged的状态设置为false。最后返回last函数的结果。
last($this->objData = parse_ini_file($this->objFile, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW), $this->objChanged = false, $this)
method

%INI -> objWrite

line 15
将规范化的数据写入文件,通过格式化每个键值对、正确转义,然后将所有内容连同换行符一起写入。
file_put_contents($this->objFile, loop($this->objData, fn($value, $key) => $key.' = '.dq.strtr($value, [dq => bs.dq, lf => '\n']).dq, lf).lf)
method

%INI -> __destruct

line 17
在发生更改时执行write方法。
$this->objChanged && $this->objWrite()
object

%JSON

/phlo/libs/Files/JSON.phlo
version 1.0
creator q-ai.nl
description Generic JSON library
static

JSON :: __handle

line 5
将路径和文件名连接起来,如果 `$assoc` 是布尔值,则在其后添加一个斜杠和 `$assoc` 的整数版本;否则不做任何操作。
"JSON/$path$filename".(is_bool($assoc) ? slash.(int)$assoc : void)
method

%JSON -> __construct (string $filename, ?string $path = null, $assoc = null)

line 6
检查 $path 变量是否为空,并在必要时将其设置为 'data'。构建到 JSON 文件的完整路径。如果文件可读,则调用带有 $assoc 参数的读取函数。
$path ??= data
$this->objFile = "$path$filename.json"
if (is_readable($this->objFile)) $this->objRead($assoc)
readonly

%JSON -> objFile:string

line 12
此代码获取一个JSON文件并将其转换为一个对象,然后将其存储在变量中或传递以进行进一步处理。
method

%JSON -> objTouch

line 14
此节点标志着已进行更改,可能是为了以后检查是否需要更新或采取行动。
$this->objChanged = true
method

%JSON -> objRead ($assoc = null)

line 15
读取JSON数据文件,将其存储在类属性中,并返回类本身以进行后续的方法链。
last($data = json_read($this->objFile, $assoc), $this->objData = $assoc || is_array($data) ? $data : get_object_vars($data), $this->objChanged = false, $this)
method

%JSON -> objWrite ($data, $flags = null)

line 16
将 JSON 表示导出到文件并根据写入操作的结果更改对象的状态。
first($written = json_write($this->objFile, $data, $flags), $written && $this->objChanged = false)
method

%JSON -> __destruct

line 18
检查是否有变化,然后在有变化时保存数据。
$this->objChanged && $this->objWrite($this->objData)
object

%PDF

/phlo/libs/Files/PDF.phlo
version 1.0
creator q-ai.nl
description PDF generator en reader
static

PDF :: toText (string $file):string

line 5
通过子进程执行 'pdftotext' 命令处理 PDF 文件,读取输出和错误信息,检查错误,并返回没有残留换页符的文本。
$process = proc_open('pdftotext '.escapeshellarg($file).' -', [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes)
if (!is_resource($process)) return null
fclose($pipes[0])
$text = stream_get_contents($pipes[1])
fclose($pipes[1])
$error = stream_get_contents($pipes[2])
fclose($pipes[2])
($code = proc_close($process)) && error("PDFToText Error: pdftotext command failed with code $code. Error: $error")
return rtrim($text, "\f")
prop

%PDF -> title

line 17
此节点不传递任何值,可能意味着没有标题或未设置标题。
null
prop

%PDF -> author

line 18
此代码始终返回null,无论输入如何。
null
prop

%PDF -> subject

line 19
此节点始终返回null,无论输入或其他条件如何。
null
prop

%PDF -> keywords

line 20
此节点不返回任何数据,因此返回空值。
null
prop

%PDF -> creator

line 21
此节点返回一个字符串,由文本“Phlo ”、变量或属性“phlo”的内容,以及URL“(https://phlo.tech/)”组成。
'Phlo '.phlo.' (https://phlo.tech/)'
prop

%PDF -> filename

line 23
返回名为“Download.pdf”的文件以供下载。
'Download.pdf'
prop

%PDF -> mode

line 24
此节点返回值为 'D'。
'D'
method

%PDF -> fromHTML ($HTML)

line 26
创建一个新的mpdf实例,根据现有属性设置元数据,保存HTML内容,并返回生成的PDF文件,带有指定的文件名和输出模式。
$mpdf = new \Mpdf\Mpdf
$this->title && $mpdf->SetTitle($this->title)
$this->author && $mpdf->SetAuthor($this->author)
$this->subject && $mpdf->SetSubject($this->subject)
$this->keywords && $mpdf->SetKeywords($this->keywords)
$this->creator && $mpdf->SetCreator($this->creator)
$mpdf->WriteHTML($HTML)
return $mpdf->Output($this->filename, $this->mode)
object

%XLSX

/phlo/libs/Files/XLSX.phlo
version 1.0
creator q-ai.nl
description XLSX reader library
method

%XLSX -> __construct (string $file)

line 5
通过解压ZIP结构加载XLSX文件,读取单元格数据和共享字符串,并将数据转换为带有标题的数组结构。使用正则表达式和字符串操作处理工作表、行和单元格,正确评估共享字符串并将单元格引用转换为索引。
$sheets = []
$shared = []
$sheetNames = []
$zip = new ZipArchive()
if ($zip->open($file) !== true) error('Error opening XLSX: '.esc($file))
for ($i = 0; $i < $zip->numFiles; $i++){
	$name = $zip->getNameIndex($i)
	if ($name === false) continue
	if (dirname($name) === 'xl/worksheets') $sheets[filter_var($name, FILTER_SANITIZE_NUMBER_INT)] = $zip->getFromIndex($i)
	elseif ($name === 'xl/sharedStrings.xml'){
		$xml = $zip->getFromIndex($i)
		if (!preg_match_all('/<t[^>]*>(.*?)<\/t>/s', $xml, $m)) error('Error reading shared lib in XLSX')
		$shared = array_map(fn($t) => html_entity_decode($t, ENT_QUOTES | ENT_XML1, 'UTF-8'), $m[1])
	}
	elseif ($name === 'xl/workbook.xml'){
		$xml = $zip->getFromIndex($i)
		if (!preg_match_all('/<sheet[^>]*name="([^"]+)"[^>]*sheetId="([0-9]+)"/', $xml, $m)) error('Error reading XLSX workbook')
		$sheetNames = $m[1]
	}
}
$zip->close()
$toIndex = fn($letters) => array_reduce(str_split(strtoupper($letters)), fn($n, $c) => $n * 26 + ord($c) - 64, 0) - 1
$isShared = fn($attrs) => preg_match('/\bt="s"\b/', $attrs) === 1
foreach ($sheets AS $sheetID => $sheet){
	$name = $sheetNames[$sheetID - 1] ?? 'Sheet '.$sheetID
	if (!preg_match('/<row[^>]*>(.+)<\/row>/s', $sheet, $m)) error('Error parsing XLSX sheet')
	$rowsXml = preg_split('/<\/row><row[^>]*>/', $m[1]) ?: []
	$headerMap = []
	$isHeader = true
	foreach ($rowsXml AS $rowXml){
		$rowXml = preg_replace('/<c([^>]*)\/>/', '<c$1></c>', $rowXml)
		if (!preg_match_all('/<c r="([A-Z]+)[0-9]+"([^>]*)>(?:<f\b[^>]*\/?>)?(?:(?:<v>([^<]*)<\/v>)|(?:<is>.*?<t[^>]*>(.*?)<\/t>.*?<\/is>))?<\/c>/s', $rowXml, $mm)) error('Error parsing XLSX row')
		if ($isHeader){
			foreach (array_keys($mm[0]) AS $i){
				$col = $toIndex($mm[1][$i])
				$attrs = $mm[2][$i]
				$valV = $mm[3][$i] ?? null
				$valIS = $mm[4][$i] ?? null
				$val = $valV !== null && $valV !== '' ? $valV : ($valIS !== null && $valIS !== '' ? html_entity_decode($valIS, ENT_QUOTES | ENT_XML1, 'UTF-8') : null)
				$txt = $isShared($attrs) ? ($shared[$val] ?? null) : $val
				$headerMap[$col] = $txt !== null && $txt !== '' ? $txt : 'col'.$col
			}
			$isHeader = false
		}
		else {
			$rowArr = []
			foreach (array_keys($mm[0]) AS $i){
				$col = $toIndex($mm[1][$i])
				$attrs = $mm[2][$i]
				$valV = $mm[3][$i] ?? null
				$valIS = $mm[4][$i] ?? null
				$val = $valV !== null && $valV !== '' ? $valV : ($valIS !== null && $valIS !== '' ? html_entity_decode($valIS, ENT_QUOTES | ENT_XML1, 'UTF-8') : null)
				$key = $headerMap[$col] ?? 'col'.$col
				$rowArr[$key] = $isShared($attrs) ? ($shared[$val] ?? null) : $val
			}
			$this->objData[$name][] = $rowArr
		}
	}
}

Lib Functions

function

button(...$args):string

/phlo/libs/form.tags.phlo line 5
创建一个带有给定参数作为属性或内容的 'button' 元素。
tag('button', ...$args)
function

decrypt($encrypted, $key):string

/phlo/libs/encryption.phlo line 6
解码base64编码的输入并检查解码是否成功以及长度是否足够。如果是,则从前几个字节中提取nonce,并将其余部分用作密文。然后,使用提供的密钥的哈希值和sodium_crypto_secretbox_open函数对机密数据进行解密。如果任何检查失败,则返回false。
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
function

en($text, ...$args)

/phlo/libs/lang.phlo line 9
此节点将输入文本翻译成英语,使用相同的翻译功能,带有提供的文本和参数。
%lang->translation('en', $text, ...$args)
function

encrypt($data, $key):string

/phlo/libs/encryption.phlo line 5
此代码生成一个随机的nonce,使用密钥函数对数据进行加密,然后将结果编码为Base64以便安全传输或存储。
base64_encode(($nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)).sodium_crypto_secretbox($data, $nonce, hash('sha256', $key, true)))
function

field($type, ...$args)

/phlo/libs/field.phlo line 6
这定义了一个函数,用于创建一个新字段,其中函数名称根据类型动态构建。它使用 `$type` 参数和额外参数来生成该特定类型的字段。
phlo("field_$type", ...$args, type: $type)
function

input(...$args):string

/phlo/libs/form.tags.phlo line 6
此节点生成一个带有指定参数作为属性或内容的HTML `<input>` 元素。
tag('input', ...$args)
function

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

/phlo/libs/n8n.phlo line 5
根据服务器信息和测试模式,向Webhook URL发起POST请求,并可选地附加数据。
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

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

/phlo/libs/n8n.phlo line 6
调用名为 'n8n' 的函数,带有 webhook、一个可选的数据数组和一个布尔参数(可能用于特殊模式或调试)。
n8n($webhook, $data, true)
function

nl($text, ...$args)

/phlo/libs/lang.phlo line 8
将提供的文本翻译成荷兰语,必要时加入额外的论据或上下文。
%lang->translation('nl', $text, ...$args)
function

select(...$args):string

/phlo/libs/form.tags.phlo line 7
创建一个带有给定参数的 'select' 元素。
tag('select', ...$args)
function

textarea(...$args):string

/phlo/libs/form.tags.phlo line 8
创建一个带有给定参数的 HTML `<textarea>` 元素。
tag('textarea', ...$args)

Phlo Functions

function

active(bool $cond, string $classList = void):string

/phlo/phlo.php line 265
执行一个条件,如果 `$cond` 为真或 `$classList` 不为空,则添加一个 class 属性,其值为 `$classList`,如果 `$cond` 为真且 `$classList` 已存在,则在后面加上 'active',用空格分隔
return $cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : void;
function

age(int $time):int

/phlo/phlo.php line 266
返回自指定时间以来的秒数。
return time() - $time;
function

age_human(int $age):string

/phlo/phlo.php line 267
将以秒为单位的年龄转换成人类可理解的年龄显示,使用另一个函数。
time_human(time() - $age);
function

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

/phlo/phlo.php line 268
此代码尝试从APCu缓存中获取一个值,或通过回调函数生成该值。然后返回该值,如果启用了日志记录,则会发送一条调试信息,包含关于缓存键和内容或值类型的详细信息。
return 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

apply(...$cmds):never

/phlo/phlo.php line 217
执行一个包含 `$cmds` 结果的 JSON 编码响应,在检查 CLI 模式后,或当头部已发送,或在流式上下文中。如果启用调试,则对 `$cmds` 应用调试,然后停止处理。
cli || headers_sent() || phlo('app')->streaming || [header('Content-Type: application/json'), header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'), header('Pragma: no-cache'), header('X-Content-Type-Options: nosniff')];
debug && $cmds = debug_apply($cmds);
die(json_encode($cmds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
function

arr(...$array):array

/phlo/phlo.php line 269
不删除任何元素,直接返回接收到的数组。
return $array;
function

auth_log(string $user):int|false

/phlo/phlo.php line 270
将一行写入日志文件,内容包括当前日期和时间、用户名以及访客的IP地址,并将其添加到文件的末尾。
return file_put_contents(data.'access.log', date('j-n-Y H:i:s')." - $user - $_SERVER[REMOTE_ADDR]\n", FILE_APPEND);
function

camel(string $text):string

/phlo/phlo.php line 271
此功能将文本转换为驼峰式大小写,通过删除空格并将单词以大写字母开头,然后将结果的第一个字母变为小写。
return lcfirst(str_replace(space, void, ucwords(lcfirst($text))));
function

chunk(...$cmds):void

/phlo/phlo.php line 222
如果尚未设置静态头,则设置它,然后将提供的命令作为JSON流式数据发送到客户端,接着刷新输出缓冲区。
static $header;
$header ??= first(true, cli || headers_sent() || [http_response_code(206), header('Content-Type: text/event-stream'), header('Cache-Control: no-store'), header('X-Content-Type-Options: nosniff'), phlo('app')->streaming = true]);
echo json_encode($cmds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf;
cli || [@ob_flush(), flush()];
function

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

/phlo/phlo.php line 272
此函数将两个迭代的结果结合起来:一个使用键回调,另一个使用值回调(或如果未提供值回调,则使用项目本身),并从中创建一个关联数组。
return array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items);
function

debug(?string $msg = null)

/phlo/phlo.php line 273
将消息添加到静态调试数组中,如果调试模式开启或关闭;如果未提供消息,则返回整个调试集合。
if (!debug) return;
static $debug = [];
if (is_null($msg)) return $debug;
$debug[] = $msg;
function

dirs(string $path):array|false

/phlo/phlo.php line 279
此代码返回一个目录路径数组,这些路径以指定的 `$path` 开头,包括每个目录的结尾斜杠。
return glob("$path*", GLOB_MARK | GLOB_ONLYDIR);
function

DOM(string $body = void, string $head = void, string $lang = 'en', string $bodyAttrs = void):string

/phlo/phlo.php line 228
此节点生成一个完整的HTML页面,包含DOCTYPE、带有语言的html标签、head部分和body部分,内容通过参数动态填充。
return "<!DOCTYPE html>\n<html lang=\"$lang\">\n<head>\n$head</head>\n<body$bodyAttrs>\n$body\n</body>\n</html>";
function

duration(int $decimals = 5, bool $float = false):string|float

/phlo/phlo.php line 280
计算自请求开始以来的持续时间,并以指定的小数位数显示;可选地对结果进行四舍五入,并在短时间内添加速度指示。
return last($d = microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true)), $float ? round($d, $decimals) : rtrim(rtrim(sprintf("%.{$decimals}f", $d), '0'), dot).'s'.($d > 0 && $d < .5 ? ' ('.round(1 / $d).'/s)' : void));
function

error(string $msg, int $code = 500):never

/phlo/phlo.php line 282
抛出一个带有特定消息和可选代码的异常,默认为500。
throw new PhloException($msg, $code);
function

esc(string $string):string

/phlo/phlo.php line 281
这确保字符串中的特殊HTML字符被编码,以便在HTML环境中安全显示,包括引号,并替换无效字符。
return htmlspecialchars((string)$string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
function

files(string|array $paths, string $ext = '*.*'):array

/phlo/phlo.php line 283
在给定路径中查找所有具有指定扩展名的文件,并将结果合并为一个数组。
return array_merge(...loop((array)$paths, fn($path) => glob("$path$ext")));
function

first(...$args):mixed

/phlo/phlo.php line 284
提取参数列表中的第一个元素。
return current($args);
function

HTTP(string $url, array $headers = [], bool $JSON = false, $POST = null, $PUT = null, $PATCH = null, bool $DELETE = false, ?string $agent = null):string|false

/phlo/phlo.php line 247
处理输入参数以构建带有正确HTTP方法、头部、正文和代理的cURL请求。执行请求并返回响应。
$curl = curl_init($url);
if ($POST || $PUT || $PATCH){
	if (!is_null($POST)) [$method = 'POST', $content = $POST];
	elseif (!is_null($PUT)) [$method = 'PUT', $content = $PUT];
	elseif (!is_null($PATCH)) [$method = 'PATCH', $content = $PATCH];
	curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
	if ($JSON) [!is_string($content) && $content = json_encode($content), array_push($headers, 'Content-Type: application/json', 'Content-Length: '.strlen($content))];
	curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
}
elseif ($DELETE) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
$agent && curl_setopt($curl, CURLOPT_USERAGENT, $agent === true ? $_SERVER['HTTP_USER_AGENT'] : $agent);
curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15, CURLOPT_ENCODING => void]);
$res = curl_exec($curl);
curl_close($curl);
return $res;
function

indent(string $string, int $depth = 1):string

/phlo/phlo.php line 285
通过添加重复的制表符,根据深度进行文本缩进,并确保现有行正确对齐而不在末尾添加额外的空格。
return ($tab = str_repeat(tab, $depth)).rtrim(strtr($string, [lf => lf.$tab]), tab);
function

indentView(string $string, int $depth = 1):string

/phlo/phlo.php line 286
根据深度为字符串添加缩进,对于以 '<' 开头的新行添加额外的制表符缩进
return last($tab = str_repeat(tab, $depth), rtrim(preg_replace('/\n(\t*)</', "\n$1$tab<", $string), tab));
function

json_read(string $file, ?bool $assoc = null):mixed

/phlo/phlo.php line 244
加载指定文件的内容,解码JSON数据并返回。如果解码失败或无法读取文件,则会生成错误消息。
return json_decode(file_get_contents($file), $assoc) ?? error('Error reading '.esc($file));
function

json_write(string $file, $data, $flags = null):int|false

/phlo/phlo.php line 245
将JSON编码的数据写入具有排他锁的文件,以确保数据安全存储。
return file_put_contents($file, json_encode($data, $flags ?? jsonFlags), LOCK_EX);
function

last(...$args):mixed

/phlo/phlo.php line 287
返回所提供参数的最后一个元素。
return end($args);
function

location(?string $location = null):never

/phlo/phlo.php line 288
此代码定义了一个异步函数,根据是否提供了位置,将用户重定向到该位置或返回上一页。如果未提供位置,则使用默认路径。
async ? apply(location: $location ?? true) : [header('Location: '.($location ?? ($_SERVER['HTTP_REFERER'] ?? slash))), exit];
function

loop(iterable $data, closure|array $cb, ?string $implode = null):mixed

/phlo/phlo.php line 289
此方法对可迭代的每个元素应用回调函数,回调可以由包含对象和方法的数组组成,也可以只是一个闭包。结果是一个包含处理后值的数组,或者如果提供了implode参数,则是一个拼接的字符串。
$return = [];
$isArray = is_array($cb);
foreach ($data AS $key => $value) $return[$key] = $isArray ? $cb[0]->{$cb[1]}($value, $key) : $cb($value, $key);
return is_null($implode) ? $return : implode($implode, $return);
function

mime(string $filename):string

/phlo/phlo.php line 295
根据文件扩展名或存在时通过 mime_content_type 返回 MIME 类型,否则默认为 'application/octet-stream'。
return ['html' => 'text/html', 'css' => 'text/css', 'gif' => 'image/gif', 'ico' => 'image/x-icon', 'ini' => 'text/plain', 'js' => 'application/javascript', 'json' => 'application/json', 'jpg' => 'image/jpeg', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jfif' => 'image/jpeg', 'ogg' => 'audio/ogg', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'pdf' => 'application/pdf', 'phlo' => 'application/phlo', 'php' => 'application/x-httpd-php', 'png' => 'image/png', 'svg' => 'image/svg+xml', 'txt' => 'text/plain', 'webp' => 'image/webp'][pathinfo($filename, PATHINFO_EXTENSION)] ?? (is_file($filename) ? mime_content_type($filename) : 'application/octet-stream');
function

obj(...$data):obj

/phlo/phlo.php line 296
创建一个新对象,使用提供的数据作为构造函数参数。
return new obj(...$data);
function

output(?string $content = null, ?string $filename = null, ?bool $attachment = null, ?string $file = null):never

/phlo/phlo.php line 229
设置HTTP头部内容类型、内容长度和内容显示(附件或内联),然后将内容(文件或字符串)发送到输出,之后脚本停止。
header('Content-Type: '.mime($filename ?? basename($file ?? req)));
header('Content-Length: '.($file ? filesize($file) : strlen($content)));
if (is_bool($attachment) || $filename) header('Content-Disposition: '.($attachment ? 'attachment' : 'inline').';filename='.rawurlencode($filename ?? basename($file ?? req)));
$file ? readfile($file) : print($content);
exit;
function

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

/phlo/phlo.php line 142
此功能管理并返回具有特定名称的对象,支持缓存。如果未传递名称,则返回可用名称列表。对于特定名称,检查是否存在特殊的句柄方法并调用它,或者创建一个新对象并将其存储在缓存中。然后,如果存在,调用控制器方法,然后返回对象。
static $list = [];
if (is_null($phloName)) return array_keys($list);
$phloName = strtr($phloName, [slash => us]);
$handle = method_exists($phloName, '__handle') ? $phloName::__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];
$phlo = new $phloName(...$args);
if ($handle) $list[$handle] = $phlo;
if ($phlo->hasMethod('controller') && (!cli || $phloName !== 'app')) $phlo->controller();
return $phlo;
function

phlo_app(...$args)

/phlo/phlo.php line 112
初始化构建、调试、数据、PHP 和 www 的默认值,基于输入参数;定义常量值并通过类映射和备用文件路径配置类的自动加载;加载依赖项,如 Composer 自动加载;设置重定向和构建控制;注册错误和异常处理程序;执行“app”节点并处理 CLI 输出或异常。
$args['build'] ??= false;
$args['debug'] ??= false;
$args['data'] ??= "$args[app]data/";
$args['php'] ??= $args['app'].($args['build'] ?? null ? 'php/' : void);
$args['www'] ??= "$args[app]www/";
foreach ($args AS $key => $value) define($key, $value);
spl_autoload_register(static function($class){
	static $map;
	$map ??= is_file($file = php.'classmap.php') ? require($file) : [];
	if (isset($map[$class])) return require(php.$map[$class]);
	if ($map) return build && debug && ($lib = phlo_find_lib('class', $class)) && phlo_activate_lib($lib);
	if (is_file($file = php.strtr($class, [us => dot]).'.php')) return require($file);
});
defined('composer') && require_once(composer.'vendor/autoload.php');
define('req', cli ? implode(slash, $cli = array_slice($_SERVER['argv'], 1)) : rawurldecode(substr(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), 1)));
debug && require(__DIR__.'/debug.php');
build && phlo_build_check() && (build['auto'] ?? true) && [require_once(__DIR__.'/build.php'), phlo_build()];
set_error_handler(static function($severity, $message, $file = null, $line = 0){
	if (!(error_reporting() & $severity)) return false;
	throw new PhloException($message, $severity, ['file' => $file, 'line' => $line]);
});
set_exception_handler('phlo_exception');
try {
	phlo('app');
	cli && print((strpos($cb = array_shift($cli), '::') ? $cb(...$cli) : [phlo($cb), array_shift($cli)](...$cli)).lf);
}
catch (Throwable $e){ phlo_exception($e); }
function

phlo_app_jsonfile(string $app, string $file)

/phlo/phlo.php line 111
加载文件内容,替换特定字符串以调整路径,解码JSON数据,然后用这些数据和应用程序的名称调用一个函数。
phlo_app(...json_decode(strtr(file_get_contents($file), ['"%app/' => dq.$app]), true), app: $app);
function

phlo_async(string $obj, string $call, ...$args):bool

/phlo/phlo.php line 160
通过调用外部的 'phlo_exec' 函数,使用提供的对象名、调用名和可能的额外参数,执行异步操作,而不等待结果。
return phlo_exec(www, $obj, $call, false, ...$args);
function

phlo_build_check():bool

/phlo/phlo.php line 162
此代码检查文件 'app.php' 是否不存在,或该文件的最后修改时间是否早于 'phlo_sources()' 中资源的最新修改时间。
return !is_file($app = php.'app.php') || filemtime($app) < array_reduce(phlo_sources(), fn($a, $f) => max($a, @filemtime($f)), 0);
function

phlo_error_log(string $path, string $msg):int|false

/phlo/phlo.php line 95
通过生成基于路径和消息的唯一ID,将错误消息添加到JSON日志文件中;跟踪相同错误发生的次数,并记录最后一次发生的时间。
$file = data.'errors.json';
$now = date('j-n-Y G:i:s');
$id = md5($path.preg_replace('/\s+/', void, trim(preg_replace('~(?:[A-Za-z]:)?[\\/](?:[^\s:/\\\\]+[\\/])*(?:([^\s:/\\\\]+\.[A-Za-z0-9]{1,8})|[^\s:/\\\\]+)(?::\d+)?~', '$1', $msg))));
$map = is_file($file) ? (json_read($file, true) ?: []) : [];
$row = $map[$id] ?? [];
$row['file'] = $path;
$row['req'] = req;
$row['msg'] = $msg;
$row['count'] = ($map[$id]['count'] ?? 0) + 1;
$row['lastOccured'] = $now;
unset($map[$id]);
$map = [...[$id => $row], ...$map];
return json_write($file, $map);
function

phlo_exception(Throwable $e):never

/phlo/phlo.php line 67
处理异常的函数,通过记录消息、调试和开发环境的条件,然后根据上下文生成正确的输出(控制台、API或HTML页面)。
$msg = $e->getMessage();
static $retried = false;
if (build && debug && preg_match('/^Call to undefined function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/', $msg, $m) && !$retried && ($lib = phlo_find_lib('function', $m[1])) && phlo_activate_lib($lib)) location(slash.req);
$code = (int)$e->getCode();
$payload = $e instanceof PhloException ? $e->payload() : ['error' => $msg, 'code' => ($code ?: 500), 'type' => get_class($e), 'data' => ['file' => $e->getFile(), 'line' => $e->getLine()]];
if (phlo('app')->hasMethod('errorPage')) phlo('app')->errorPage($msg, (int)($payload['code'] ?? 500));
$d = is_array($payload['data'] ?? null) ? $payload['data'] : [];
$file = $d['file'] ?? $e->getFile();
$line = (int)($d['line'] ?? $e->getLine());
$short = shortpath($file).colon.$line;
phlo_error_log($short, $msg);
if (debug) debug_error($e);
if (cli || async){
	$text = ($payload['type'] ?? 'Error').colon.space.$msg;
	if (async) apply(error: $text);
	fwrite(STDERR, $text.lf);
	exit(1);
}
http_response_code($code = $payload['code'] ?? 500);
header('X-Content-Type-Options: nosniff');
$title = "Phlo $code Error";
$CSS = 'body{background:black;color:white;font-family:sans-serif;text-align:center;margin-top:18dvh}pre{white-space:pre-wrap}';
$body = '<h1>'.esc($title).'</h1><pre>'.esc(($payload['type'] ?? 'Error').colon.space.$msg).'</pre>';
print(DOM($body, tag('title', $title).lf.tag('style', $CSS)));
exit(1);
function

phlo_exec(string $path, string $obj, string $call, bool $sync = true, ...$args):string|bool

/phlo/phlo.php line 158
通过命令行执行带有指定参数的PHP脚本,并返回输出,或异步执行命令而不输出。
return last(exec('/usr/bin/php '.escapeshellarg(rtrim($path, slash).'/app.php').' '.escapeshellarg($obj).space.escapeshellarg($call).loop($args, fn($arg) => space.escapeshellarg((string)$arg), void).($sync ? void : ' > /dev/null 2>&1 &'), $res), $sync ? implode(lf, $res) : true);
function

phlo_exists(string $obj):bool

/phlo/phlo.php line 161
检查是否存在与名称对应的PHP文件,通过执行点替换然后检查文件是否存在。
return is_file(php.strtr($obj, [us => dot]).'.php');
function

phlo_sources():array

/phlo/phlo.php line 163
收集来自指定源文件夹或应用程序文件夹的 .phlo 文件,然后添加存在的额外库文件,按不区分大小写的顺序排序列表并返回排序后的列表。
$sources = files(isset(build['sources']) ? build['sources'] : app, '*.phlo');
foreach (build['libs'] AS $lib) $sources[] = is_file($file = __DIR__."/libs/$lib.phlo") ? $file : error('Build Error: Library not found '.esc($lib));
natcasesort($sources);
return $sources;
function

phlo_sync(string $obj, string $call, ...$args):string

/phlo/phlo.php line 159
这是一個调用其他函数或方法的函数,使用指定的对象和调用,通过使用全局输出函数。
return phlo_exec(www, $obj, $call, true, ...$args);
function

regex(string $pattern, string $subject, int $flags = 0, int $offset = 0):array

/phlo/phlo.php line 297
对字符串执行正则表达式,并以数组形式返回匹配项;如果未找到匹配项,则返回空数组。
return preg_match($pattern, $subject, $match, $flags, $offset) ? $match : [];
function

regex_all(string $pattern, string $subject, int $flags = 0, int $offset = 0):array

/phlo/phlo.php line 298
对文本执行正则表达式并返回所有匹配项的数组;如果未找到匹配项,则返回一个空数组。
return preg_match_all($pattern, $subject, $matches, $flags, $offset) ? $matches : [];
function

req(int $index, ?int $length = null):mixed

/phlo/phlo.php line 299
从 URL 中拆分出特定部分,并返回该部分。如果指定了长度,则将多个部分合并为一个字符串。
static $parts;
$parts ??= explode(slash, req);
return is_null($length) ? ($parts[$index] ?? null) : (implode(slash, array_slice($parts, $index, $length < 0 ? null : $length)) ?: null);
function

route(?string $method = null, string $path = void, ?bool $async = null, ?string $data = null, ?string $cb = null)

/phlo/phlo.php line 304
该代码根据方法、异步标志、有效载荷数据和URL路径部分验证并处理传入请求。它过滤请求,从路径中提取变量,检查诸如带有默认值的参数、固定值或定义的列表等模式,并将它们收集到回调的参数中。如果所有检查通过,则用收集的参数调用回调。
if ($method && $method !== method) return;
if (!is_null($async) && $async !== async) return;
if ($data && phlo('payload')->objKeys !== explode(comma, $data)) return;
$req = array_filter(explode(slash, req));
$cbArgs = [];
$index = -1;
foreach (array_filter(explode(space, $path)) AS $index => $item){
	$reqItem = req($index);
	if (strpos($item, '$') === 0){
		$item = substr($item, 1);
		if (str_ends_with($item, '=*')){
			$cbArgs[substr($item, 0, -2)] = implode(slash, array_slice($req, $index));
			$index = count($req) - 1;
			break;
		}
		elseif (str_ends_with($item, qm)){
			$item = substr($item, 0, -1);
			if ($reqItem && $item !== $reqItem) return;
			$reqItem = $item === $reqItem;
		}
		elseif (str_contains($item, eq)){
			list ($item, $default) = explode(eq, $item, 2);
			$default = $default ?: null;
		}
		elseif (is_null($reqItem)) return;
		if (str_contains($item, dot) && (list($item, $length) = explode(dot, $item, 2)) && strlen($reqItem) != $length) return false;
		if (str_contains($item, colon)){
			(list ($item, $list) = explode(colon, $item, 2)) && $list = explode(comma, $list);
			if (!$reqItem || in_array($reqItem, $list)) $cbArgs[$item] = $reqItem ?: $default ?? null;
			else return;
		}
		else $cbArgs[$item] = $reqItem ?? $default;
	}
	elseif ($item !== $reqItem) return;
}
if (isset($req[$index + 1])) return;
if (!$cb) return obj(...$cbArgs);
if ($cb(...$cbArgs) === false) return;
exit;
function

shortpath(?string $file):string

/phlo/phlo.php line 345
它获取路径中倒数第二个部分和最后一个部分,用斜杠分隔,除非路径少于两个部分,否则只返回最后一个部分。空输入时返回“unknown”。
if (!$file) return 'unknown';
$p = explode(slash, str_replace(bs, slash, $file));
$n = count($p);
return $n >= 2 ? $p[$n - 2].slash.$p[$n - 1] : end($p);
function

size_human(int $size, int $precision = 0):string

/phlo/phlo.php line 351
此功能将字节大小转换为人类可读的格式,通过将数字除以1024,直到其在合适的范围内,然后根据指定的精度添加正确的单位(B、KB、MB、GB、TB)。
foreach (['b', 'Kb', 'Mb', 'Gb', 'Tb'] AS $range){
	if ($size / 1024 < 1) break;
	$size /= 1024;
}
return round($size, $precision).$range;
function

slug(string $text):string

/phlo/phlo.php line 358
此代码将文本转换为URL友好的短语:将文本转换为小写,去除重音符号,用连字符替换非字母数字字符,并删除两端多余的连字符。
return trim(preg_replace('/[^a-z0-9]+/', dash, strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text))), dash);
function

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

/phlo/phlo.php line 236
创建一个具有指定标签名和可选内容的HTML标签,并添加非空的额外属性作为标签或属性值。
return "<$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

time_human(?int $time = null):string

/phlo/phlo.php line 359
此代码通过将给定的时间戳与当前时间进行比较,并将其转换为合适的时间单位(如天、周、月)来计算人类可读的时间显示。它使用标签及其对应的倍数列表,将年龄缩放到易于理解的单位,并在年龄低于某个阈值时停止。
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

title(?string $title = null, string $implode = ' - '):string

/phlo/phlo.php line 237
在静态标题列表中添加一个给定的标题(如果存在),否则使用当前应用标题或默认值,并将列表合并为一个用指定分隔符的字符串。
static $titles = [];
if ($title) return $titles[] = $title;
$titles[] = phlo('app')->title ?: 'Phlo '.phlo;
return implode($implode, $titles);
function

token(int $length = 8, ?string $input = null, ?string $sha1 = null):string

/phlo/phlo.php line 369
通过先基于输入、数字或随机数字创建SHA1哈希,然后根据哈希值连接字母,直到达到所需长度,从而生成一个随机的指定长度的令牌。
$sha1 ??= sha1($input ?? random_int(date('Y'), PHP_INT_MAX), true);
$token = void;
for ($i = 0; strlen($token) < $length; $i++) $token .= chr(ord('a') + (ord($sha1[$i % 20]) % 26));
return $token;
function

view(?string $body = null, ?string $title = null, array|string $css = [], array|string $js = [], array|string $defer = [], array|string $options = [], array $settings = [], ?string $ns = null, bool|string $uri = req, ...$cmds):void

/phlo/phlo.php line 170
此节点处理生成HTML页面以及配置资源和元数据。它添加CSS和JS文件,设置元标题和其他头部元素,处理预加载和延迟选项,并负责HTML结构,包括body属性。在异步处理时,重要内容通过 `$cmds` 传递并应用。
if (cli) return;
!async && !is_bool($uri) && $uri !== req && location("/$uri");
$app = phlo('app');
$title && title($title);
$css = array_merge((array)$css, (array)$app->css);
$js = array_merge((array)$js, (array)$app->js);
$defer = array_merge((array)$defer, (array)$app->defer);
$options = implode(space, array_merge((array)$options, (array)$app->options, debug ? ['debug'] : []));
$settings = array_merge($settings, (array)$app->settings);
if (async){
	$uri !== false && $cmds['uri'] = $uri;
	$cmds['trans'] ??= true;
	$cmds['title'] = title();
	$css && $cmds['css'] = $css;
	$js && $cmds['js'] = $js;
	$defer && $cmds['defer'] = $defer;
	$cmds['options'] = $options;
	$cmds['settings'] = $settings;
	!is_null($body) && $cmds['inner']['body'] = $body;
	apply(...$cmds);
}
$body ??= $cmds['main'] ?? void;
debug && $body .= lf.debug_render();
$ns ??= $app->ns ?? 'app';
$link = $app->link ?: [];
$head = tag('title', inner: title()).lf;
$head .= '<meta name="viewport" content="'.($cmds['viewport'] ?? $app->viewport ?? 'width=device-width').'">'.lf;
$app->description && $head .= "<meta name=\"description\" content=\"$app->description\">\n";
$app->themeColor && $head .= "<meta name=\"theme-color\" content=\"$app->themeColor\">\n";
$app->image && $head .= "<meta property=\"og:image\" content=\"$app->image\">\n";
is_file(www.$filename = 'favicon.ico') && $head .= "<link rel=\"favicon\" href=\"/$filename?".version."\">\n";
is_file(www.$filename = 'manifest.json') && $head .= "<link rel=\"manifest\" href=\"/$filename?".version."\">\n";
is_file(www.$filename = 'icons.png') && $link[] = "</$filename?".version.">; rel=preload; as=image";
$app->head && $head .= $app->head.lf;
foreach ($css AS $item) $head .= '<link rel="stylesheet" href="'.esc($item).'">'.lf;
is_file(www.$filename = "$ns.css") && [$link[] = "</$filename?".version.">; rel=preload; as=style", $head .= '<link rel="stylesheet" href="'.esc(slash.$filename.qm.version).'">'.lf];
foreach ($js AS $item) $head .= '<script src="'.esc($item).'"></script>'.lf;
foreach ($defer AS $item) $head .= '<script src="'.esc($item).'" defer></script>'.lf;
is_file(www.$filename = "$ns.js")  && [$link[] = "</$ns.js?".version.">; rel=preload; as=script", $head .= '<script src="'.esc(slash.$filename.qm.version).'" defer></script>'.lf];
!build && $link && header('Link: '.implode(comma, $link), false);
if ($lang = $cmds['lang'] ?? $app->lang ?? 'en') unset($cmds['lang']);
$bodyAttrs = void;
$options && $bodyAttrs .= " class=\"$options\"";
$settings && $bodyAttrs .= loop($settings, fn($value, $key) => ' data-'.$key.'="'.esc($value).'"', void);
die(DOM($body, $head, $lang, $bodyAttrs));