Libs
object
%cookies
/phlo/libs/cookies.phlo
method
%cookies -> controller
line 7
This code assigns the contents of all cookies to an object property, allowing it to be reused later in the code.
this->objData = $_COOKIEprop
%cookies -> lifetimeDays
line 5
This is a code that sets the value of the cookie's lifespan to 180 days.
180method
%cookies -> __set ($key, $value)
line 9
This code ensures that a value is stored in the internal data, the global cookies, and via an HTTP-only, secure cookie with a certain lifespan.
$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
Removes a cookie and the associated data from the objects by unsetting the value and deleting the cookie with an expired timestamp.
unset($this->objData[$key], $_COOKIE[$key])
setcookie($key, void, time() - 86400, slash, $_SERVER['HTTP_HOST'], true, true)object
%creds
/phlo/libs/creds.phlo
method
%creds -> __construct (?array $values = null)
line 5
In this constructor, if no values are provided, data is loaded from an INI file. Then, for each key-value pair in that array, a dynamic property is created: if the value is an array, a new instance of the class itself is created; otherwise, a SensitiveParameterValue object is created.
$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
This code checks whether the parameter `$key` is equal to `'toArray'`. In that case, a recursive loop is performed over `objData`, converting values of type `'SensitiveParameterValue'` using their `getValue()` method. If `$key` is not `'toArray'`, it checks whether an element exists under `$key` in `objData` that is of type `'SensitiveParameterValue'`, and then returns the value via `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
Create a list through the input where each value is checked for the type 'SensitiveParameterValue'. If so, the value is replaced with a series of asterisks of the same length; otherwise, the value remains unchanged.
loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)object
%encryption
/phlo/libs/encryption.phlo
function
function encrypt ($data, $key):string
line 5
This code generates a random nonce, encrypts the data using a secret key function, and then encodes the result in Base64 for secure transmission or storage.
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
Decode the base64-encoded input and check if the decoding is successful and the length is sufficient. If so, the nonce is extracted from the first bytes and the rest is used as ciphertext. Then, the secret data is decrypted using a hash of the provided key and the sodium_crypto_secretbox_open function. If any of the checks fail, false is returned.
($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)) : falseobject
%field
/phlo/libs/field.phlo
function
function field ($type, ...$args)
line 6
This defines a function that creates a new field, where the function name is dynamically constructed based on the type. It uses the `$type` argument and additional arguments to generate a field of that specific type.
phlo("field_$type", ...$args, type: $type)static
field :: __handle
line 8
This node always returns a null value, regardless of the input or context.
nullprop
%field -> title
line 10
It makes the first letter of the value of the variable $name uppercase.
ucfirst($this->name)method
%field -> input ($record, $CMS)
line 12
Creates an input field with the type, name, and other attributes based on the properties of the object and fills in the value from the record or a default value.
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
This code returns the value of a property from the record, based on the name of the field.
$record->{$this->name};object
%form_tags
/phlo/libs/form.tags.phlo
function
function button (...$args):string
line 5
Creates a 'button' element with the given arguments as attributes or content.
tag('button', ...$args)function
function input (...$args):string
line 6
This node generates an HTML `<input>` element with the specified arguments as attributes or content.
tag('input', ...$args)function
function select (...$args):string
line 7
Create a 'select' element with the given arguments.
tag('select', ...$args)function
function textarea (...$args):string
line 8
Creates an HTML `<textarea>` element with the given arguments.
tag('textarea', ...$args)object
%lang
/phlo/libs/lang.phlo
function
function nl ($text, ...$args)
line 8
Translate the provided text into Dutch, possibly with additional arguments for interpolation or context.
%lang->translation('nl', $text, ...$args)function
function en ($text, ...$args)
line 9
This node translates the input text into English, using the same translation function with the provided text and arguments.
%lang->translation('en', $text, ...$args)view
%lang ->
line 11
This piece of code performs a translation by using the application's language setting, ensuring the correct language is loaded for the user.
%app->langprop
%lang -> browser
line 13
This code selects the most suitable language code from the user's 'HTTP_ACCEPT_LANGUAGE' header by comparing the first language in the header with available languages in the application. If no suitable language is found, the default language from the first matching language is selected or null if there are no matches.
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
Checks whether the language from the cookies exists in the available languages. If so, the language value is returned; otherwise, null is returned.
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : nullprop
%lang -> model
line 16
A brief explanation is that this node probably contains a setting or parameter for using a smaller or optimized GPT-4 model, suitable for less demanding tasks or faster processing.
'gpt-4o-mini'method
%lang -> translations
line 17
Initializes the translations for the current language, using the settings or data from '%app->lang' and 'langs'.
%INI(%app->lang, langs)method
%lang -> detect ($text, $fallback = 'en')
line 19
The code performs a chat request to an AI model to identify the language of a text and return that identification as an ISO 639-1 code. If the response is not a valid 2-character code, a fallback language is used.
$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) : $fallbackmethod
%lang -> hash ($from, $text)
line 29
Creates a short, unique hash by first summarizing the text with a regex and uppercase letters, using the first part as a prefix, and then adding an MD5 hash of the entire text, so that the resulting code is both recognizable and unique.
$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
This method translates text from a specified language to the active language. If the source and target languages are the same, the text is returned directly. Otherwise, the text is split into lines, a hash is created for each line, and a translation is searched for. If none is found, an asynchronous translation is requested. The translations are combined and returned, with optional formatting if arguments are provided.
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) : $translationstatic
lang :: asyncTranslation ($from, $to, $text)
line 49
This code changes the application's language, generates a unique hash based on the original language and text, translates the text, and stores the translation in a translation structure with the hash as the key.
%app->lang = $to
$hash = $this->hash($from, $text)
$translation = $this->translate($from, $to, $text)
return $this->translations->$hash = $translationmethod
%lang -> translate ($from, $to, $text)
line 56
This function first checks if the source and target languages are the same and returns the original text in that case. Otherwise, it makes a chat call to OpenAI, instructing the model to translate the text between the specified languages, preserving markdown, punctuation, and capitalization, and only returns the translated text.
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,
)->answerobject
%model
/phlo/libs/model.phlo
static
model :: DB
line 8
Connects to a MySQL database.
%MySQLstatic
model :: objRecords
line 9
An empty array means that there are currently no records or items present in this node.
[]static
model :: objLoaded
line 10
It seems to return an empty array, possibly intended to indicate that no data has been loaded or that no action has been performed.
[]static
model :: objCache
line 11
This code always returns the value 'false'.
falsestatic
model :: columns
line 13
Checks if the static property `$columns` exists; if so, it is returned. If not, it checks whether a `schema` method exists; if so, this method is called via `_columns()`. If neither is the case, the string '*' is returned.
isset(static::$columns) ? static::$columns : (method_exists(static::class, 'schema') ? static::_columns() : '*')static
model :: _columns
line 14
This code generates a quoted list of column names for database use, excluding only fields with specific types ('child', 'many', 'virtual'). It combines field definitions and adds the correct quotes for use in an SQL query.
$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).$fqstatic
model :: fields
line 19
Checks if a 'schema' method exists in the class. If so, the output of '_fields()' is used; otherwise, it looks for the static property '$fields' or an empty array if it does not exist.
method_exists(static::class, 'schema') ? static::_fields() : (static::$fields ?? [])static
model :: _fields
line 20
Loops through the schema fields and selects for each field the latest value from a chain of alternatives: either the name, or the object (for conditions with type 'parent'), or the field itself.
loop(static::schema(), fn($field, $column) => last($field->name ??= $column, $field->type === 'parent' && $field->obj ??= $column, $field))static
model :: field ($name)
line 21
Returns the field configuration from the array of all fields, based on the specified name.
static::fields()[$name]static
model :: create (...$args)
line 23
This code creates a new record and then returns it, possibly for further processing or as the result of the function.
static::record(id: static::createRecord(...$args))static
model :: createRecord (...$args)
line 24
Creates a new database record in the specified table with the provided arguments.
static::DB()->create(static::$table, ...$args)static
model :: change ($where, ...$args)
line 25
This code calls a static method 'change' that likely performs an update in the database for a specific table. It uses the database connection and table name from static properties and accepts conditions and additional arguments to execute the change.
static::DB()->change(static::$table, $where, ...$args)static
model :: delete ($where, ...$args)
line 26
Performs a delete operation on the database for the specified table, based on the given conditions and any additional arguments.
static::DB()->delete(static::$table, $where, ...$args)method
%model -> objSave
line 28
Check if an id exists; if not, display an error message. If a record with that id exists, it is updated and returned. Otherwise, a new record is created and returned.
$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
Loads multiple records and returns only the column values, without Object-Or-Array structure.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_COLUMN])static
model :: item (...$args)
line 38
Loads records with the specified arguments, uses the 'fetch' mode, and retrieves only one column.
static::recordsLoad($args, 'fetch', [PDO::FETCH_COLUMN])static
model :: pair (...$args)
line 39
Loads multiple records and returns them as a key-value pair, with the fetch mode set to FETCH_KEY_PAIR for quick and easy maps.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_KEY_PAIR])static
model :: records (...$args)
line 40
Loads multiple records from the database and returns them as unique objects of the class, with all records merged into a single collection.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, static::class], true)static
model :: recordCount (...$args)
line 41
This code counts the number of records by performing a count on the 'id' column.
static::item(...$args, columns: 'COUNT(id)')static
model :: record (...$args)
line 42
Calculates the number of records and returns an error message if there is more than one record; otherwise, it returns the first record or null if there are no records.
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
This code dynamically constructs query parameters for loading records from a database, including conditions, joins, selections, and grouping rules, depending on static properties and arguments. It then uses caching with APCu based on a cache key and duration, making repeated loading more efficient. Finally, the loaded records are stored for reuse and returned.
$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 $recordsstatic
model :: objRel ($key)
line 58
Provides the value of a static property or method or, if not present, a default value (empty array).
static::$classProps[static::class][$key] ??= method_exists(static::class, $key) ? static::$key() : static::$$key ?? []prop
%model -> objState
line 60
Initializes a structure with three empty collections: 'parents', 'children', and 'many'.
['parents' => [], 'children' => [], 'many' => []]method
%model -> objGet ($key)
line 61
First search in the parent, then in the children, and if no result is found, then search multiple items.
$this->getParent($key) ?? $this->getChildren($key) ?? $this->getMany($key)method
%model -> objIn ($ids)
line 62
Converts an array of IDs into a comma-separated string enclosed in quotes, or returns 'NULL' if the array does not exist or is empty.
$ids ? dq.implode(dq.comma.dq, $ids).dq : 'NULL'method
%model -> getParent ($key)
line 64
Returns the direct parent object based on a key, with checks on previously loaded relationships and dynamically fetching related records if they are not yet loaded.
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] = $parentObjectmethod
%model -> getChildren ($key)
line 86
Verifies whether the children for a given key are already loaded; if not, the relationship is requested and the corresponding records are loaded and stored in the state; finally, the children array is returned.
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
Load multiple related records from the database if they have not been loaded before and cache them in the object state, so that subsequent calls have the records available immediately.
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
This code first checks if the count is already stored in the internal state and returns it if available. Then it identifies whether the count relates to a child relationship ('objChildren') or a multiple relationship ('objMany'). If it is a child relationship, it retrieves the IDs of linked records, performs an aggregated query to determine the count, and caches these results. For a 'many' relationship, similar logic is applied but through a separate table and column. If no relationship is found, 0 is returned.
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 0method
%model -> getLast ($key)
line 156
Searches in the object status whether the last child linked to the specified key has already been loaded; if not, the last child is retrieved via relationship data and records, stored in the status, and returned.
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 nullstatic
model :: objParents
line 177
Retrieves the static property if it exists; otherwise, it returns an empty array if the 'schema' method does not exist; if it does, it filters the fields of type 'parent' and creates an array, returning an object for each field with an optional key or using the object name.
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
This code checks if the static property 'objChildren' exists and returns it if present. If the 'schema' method does not exist, it returns an empty array. Otherwise, an array is filtered for fields with type 'child', and these are then processed by a loop function that determines whether to return an object and possibly a key, or just the object itself.
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
Returns the value of the static property `objMany` if it exists; otherwise, if there is no `schema` method, it returns an empty array; otherwise, it filters the fields of type 'many' and constructs an array with object and table information, including the local and foreign keys.
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
Checks if the method schema() exists, otherwise an error is thrown. Then a SQL CREATE TABLE statement is constructed by iterating over and formatting the field data, including any NOT NULL or NULL specifications, and adds a primary key on '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
function
function n8n ($webhook, ?array $data = null, $test = false)
line 5
Makes a POST request to a webhook URL, based on server data and test mode, with optional data included.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)function
function n8n_test ($webhook, ?array $data = null)
line 6
Call a function named 'n8n' with the webhook, an optional data array, and a boolean parameter (probably for a special mode or debug).
n8n($webhook, $data, true)object
%payload
/phlo/libs/payload.phlo
method
%payload -> controller
line 7
Parses JSON request bodies for POST, PUT, PATCH methods and performs import; handles multipart/form-data uploads with file data, temporarily stores files, and structures the data, including array collections; also processes standard $_FILES data through import.
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
prop
%security -> nonce
line 5
Generate a random 8-character token and store it in the `$nonce` property.
%app->nonce = token(8)method
%security -> full
line 7
Provides a collection of security headers or settings by checking or assigning various properties, presumably focused on web security and privacy.
$this->COOP
$this->CORP
$this->CORS
$this->CSP
$this->Referrer
$this->X_content
$this->X_framemethod
%security -> CSRF
line 17
Generates and stores a new 12-character CSRF token in the session.
%session->csrf = token(12)method
%security -> COOP
line 18
Sets an HTTP header that specifies that the page can only be opened by documents from the same origin, providing security improvements against certain attack techniques such as side-channel attacks.
header('Cross-Origin-Opener-Policy: same-origin')method
%security -> CORP
line 19
Adds an HTTP header indicating that Cross-Origin Resource Sharing is only allowed from the same origin.
header('Cross-Origin-Resource-Policy: same-origin')method
%security -> CORS ($host = host)
line 20
Sets the allowed origin for cross-origin resource sharing (CORS) by adding a header that only permits access from the specified host.
header("Access-Control-Allow-Origin: https://$host")method
%security -> CSP
line 21
Sends a Content-Security-Policy header that imposes restrictions on sources for content, scripts, styles, images, fonts, connections, and frame-ancestors, including a nonce for dynamically loaded scripts.
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
It sets an HTTP header that specifies that the referrer information is only sent to the same origin or to third parties via a full URL, but not in cross-origin requests, thereby enhancing privacy.
header('Referrer-Policy: strict-origin-when-cross-origin')method
%security -> X_content
line 23
Adds an HTTP header that prevents browsers from guessing the response's content-type, contributing to improved security by preventing content sniffing.
header('X-Content-Type-Options: nosniff')method
%security -> X_frame
line 24
This code sets the HTTP header 'X-Frame-Options' to 'DENY', preventing the page from being loaded in a frame or iframe, which contributes to protection against clickjacking.
header('X-Frame-Options: DENY')object
%session
/phlo/libs/session.phlo
method
%session -> controller
line 5
Start the session and store the contents of the $_SESSION variable in a property for later use.
ession_start()
$this->objData = $_SESSIONmethod
%session -> __set ($key, $value)
line 8
This code modifies two arrays simultaneously: a global session array and an internal data array of the object, by assigning the given value to the specified key in both arrays.
$_SESSION[$key] = $this->objData[$key] = $valuemethod
%session -> __unset ($key)
line 9
Removes a key from both the internal data structure and the session data.
unset($this->objData[$key], $_SESSION[$key])AI
object
%Claude
/phlo/libs/AI/Claude.phlo
static
Claude :: context (...$args)
line 7
Add 'assistant' and 'user' content to the 'messages' array if present, then remove these keys from the array.
$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 $argsmethod
%Claude -> chat (...$args)
line 14
This code makes a configured request to a chat model, setting default values for the model and the maximum number of tokens. Then, a request is made and the response is returned as an object with an `answer` field.
$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
This function establishes a streaming connection with an API endpoint, sends a POST request with specified parameters, and processes the received data in real-time via a callback. If no callback is provided, an event stream is automatically set up and outgoing data is displayed directly. The response is constructed from the received delta texts and returned as complete text.
%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 $answermethod
%Claude -> request ($uri, $JSON = true, ...$args)
line 53
Sends an HTTP request to a specified API endpoint with default headers and API key, decodes the JSON result, checks for errors, and returns the decoded object.
$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 $resobject
%DeepSeek
/phlo/libs/AI/DeepSeek.phlo
static
DeepSeek :: context (...$args)
line 7
Adds default values to the 'messages' array and adds conversation roles such as 'system', 'assistant', and 'user' based on the provided arguments, then cleans up the original arguments.
$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 $argsmethod
%DeepSeek -> chat (...$args)
line 15
Processes a chat request by preparing arguments, sends a request to the chat completions endpoint, and returns an object with model information, finish reason, token usage, and content or tool calls.
$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 $returnmethod
%DeepSeek -> stream (...$args)
line 24
This method establishes a streaming connection with an API, processes temporary data uploads via cURL, and uses a callback to process and merge real-time data into a final response.
%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 $answermethod
%DeepSeek -> request ($uri, $JSON = true, ...$args)
line 62
Sends an HTTP request to a DeepSeek API endpoint with a specified URI and authentication. Then decodes the JSON response and checks for errors; if present, an error is generated. Afterwards, the response is returned.
$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 $resobject
%Gemini
/phlo/libs/AI/Gemini.phlo
method
%Gemini -> config ($modalities)
line 7
Assembles a configuration with parameters for language output, such as temperature, topK, topP, maximum tokens, and response modes and mime-types.
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
Sends a POST request to the Google API for generative images with the specified prompt and base64 image, receives the JSON response, and returns it as the result.
$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
Sends a POST request to the Google generative language API for content generation based on a prompt, and processes the JSON response to return it.
$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
Sends a POST request to the Google API for generative image content, with base64 image and prompt as input, and decodes the JSON response before returning it.
$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
Checks if the answer ends with an 'IMAGE_SAFETY' rating and issues a warning if so. If not, returns the first found textual content or inline data. If there is no valid information, returns an error message.
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
const
OpenAI :: model
line 7
This is a specification for a model setting that indicates it uses the GPT-4 mini model.
'gpt-4o-mini'const
OpenAI :: voices
line 8
This is a list of string elements containing different voice or voice names, possibly for use in a text-to-speech function or voice selection module.
['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']static
OpenAI :: context (...$args):array
line 10
Adds messages to the message list based on the input parameters (system, assistant, user), ensures that the messages are correctly ordered, and removes the original parameters after adding.
$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 $argsstatic
OpenAI :: tool ($tool):array
line 18
Create a descriptive object structure with name, description, and parameters, where parameters are generated from a list of arguments and contain only specific properties, excluding all additional properties.
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
Processes a chat request by determining the model and context, sends a request to the API, and returns an object with details about the model, the reason for completion, tokens, and the content or tool calls, depending on the response.
$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 $returnmethod
%OpenAI -> embedding ($input, $model = 'text-embedding-3-small')
line 43
Performs a POST request to the 'embeddings' endpoint with the input and the model, and returns the first embedding from the response.
$this->request('embeddings', POST: arr(input: $input, model: $model))->data[0]->embeddingmethod
%OpenAI -> stream (...$args):string
line 45
Establishes a streaming connection with the OpenAI API, sends a chat request, and processes the received data by passing it in real-time via a callback, building and returning textual fragments.
%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 $answermethod
%OpenAI -> transcribe ($file, $model = 'whisper-1', ...$args):obj
line 77
Processes an audio file by converting it to CURLFile if necessary, sends it to the API for transcription with the selected model, and returns an object with model name, duration, language, and transcription text.
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
Compose a message with text and optionally an image, and choose based on the stream parameter for a continuous stream or a chat response.
$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
Sends an HTTP request to the OpenAI API with the given URI and options. Decodes the JSON response, checks for errors, and returns the result.
$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 $resCSS
object
%basics
/phlo/libs/CSS/basics.phlo
view
style
line 5
Statically assigned CSS class selector that contains various style rules for text alignment, floats, positioning, margins, paddings, overflow, display, cursor, text decoration, and width, intended for reuse and consistent styling.
.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: 1200pxobject
%fixes
/phlo/libs/CSS/fixes.phlo
view
style
line 5
Apply box-sizing to all elements and pseudo-elements so that they include padding and border. Remove default touch actions for elements such as links and buttons. Fix focus styling by removing the outline for input elements. Remove spin buttons from numeric inputs in WebKit and Gecko browsers. Ensure tables do not have double borders by enabling border-collapse. Remove the default dropdown arrow from select elements in IE.
*, ::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: noneobject
%flex
/phlo/libs/CSS/flex.phlo
view
style
line 5
Sets flexible layout rules with various flex properties, such as display, flex-wrap, gap, and flex-direction, including responsive adjustments for larger screens.
.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
view
style
line 5
This code defines a grid layout that automatically adjusts the number of columns based on the available width, with a minimum of 300px per column.
.grid {
display: grid
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))
}DB
object
%DB
/phlo/libs/DB/DB.phlo
prop
%DB -> PDO
line 6
Displays an error message if no PDO connection is defined.
error('No PDO connector defined')prop
%DB -> fieldQuotes
line 7
Returns a boolean value indicating whether quotes (quotation marks) are around the data, possibly to check if the field is already quoted.
btmethod
%DB -> load (string $table, string $columns = '*', string $where = void, string $joins = void, string $group = void, string $limit = null, string $order = void, ...$args)
line 9
Constructs an SQL SELECT statement dynamically based on provided parameters and additional arguments, including automatic WHERE clause generation from key-value pairs if not specified.
!$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
Executes a database query with support for prepared statements for input parameters, and includes debug functionality that displays the query type, tablename, and optionally the WHERE clause along with the number of rows returned. In case of an error, a detailed error message including trace is generated.
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 $stmtmethod
%DB -> column (...$args)
line 39
Loads the data with the specified arguments and returns a list of one column from all rows.
$this->load(...$args)->fetchAll(PDO::FETCH_COLUMN)method
%DB -> item (...$args)
line 40
Retrieves a loaded database data and returns the first column of the first row, unless it does not exist, then null.
$this->load(...$args)->fetch(PDO::FETCH_COLUMN) ?: nullmethod
%DB -> pair (...$args)
line 41
Retrieves data and returns it as a key-value pair array.
$this->load(...$args)->fetchAll(PDO::FETCH_KEY_PAIR)method
%DB -> group (...$args)
line 42
This code loads data with the given arguments, retrieves all results, and groups them based on a column or field, with each group being converted into objects of the 'obj' class.
$this->load(...$args)->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_CLASS, 'obj')method
%DB -> records (...$args)
line 43
Loads data with the specified arguments and retrieves all results, converting each row into an object of the specified class, with unique results based on the first column.
$this->load(...$args)->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, 'obj')method
%DB -> record (...$args)
line 44
Returns an object based on loaded data, or null if there is no result.
$this->load(...$args)->fetchObject('obj') ?: nullmethod
%DB -> create (string $table, ...$data)
line 46
Execute an insert query where, if the 'ignore' value in the data is true, the 'IGNORE' option is added. Properly construct column names and corresponding values, fill the values with placeholders, and replace them with the actual data. Execute the query and return the last inserted ID or an optional 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
This function dynamically constructs an SQL UPDATE statement using the data from `$data`. It counts the number of placeholders in `$where` to determine which data is for the WHERE condition and which is for the SET clause. Then, the column names and values are separated and ordered, and the query is executed with the correct parameters, after which the number of affected rows is returned.
$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
Executes a DELETE query on a specified table with a specific WHERE condition and returns the number of rows deleted.
$this->query("DELETE FROM $table WHERE $where", ...$args)->rowCount()object
%MySQL
/phlo/libs/DB/MySQL.phlo
prop
%MySQL -> PDO
line 8
Creates a new PDO connection to the MySQL database, with the connection details retrieved from a credentials object.
new PDO('mysql:host='.%creds->mysql->host.';dbname='.%creds->mysql->database, %creds->mysql->user, %creds->mysql->password)object
%PostgreSQL
/phlo/libs/DB/PostgreSQL.phlo
prop
%PostgreSQL -> PDO
line 8
Creates a new PDO connection to a PostgreSQL database using configured login credentials.
new PDO('pgsql:host='.%creds->postgresql->host.';dbname='.%creds->postgresql->database, %creds->postgresql->user, %creds->postgresql->password)prop
%PostgreSQL -> fieldQuotes
line 9
This code defines a function that likely applies a textual replacement or modification to the input, possibly related to field or table name quotes for PostgreSQL, to prevent SQL injections and syntax errors.
dqobject
%Qdrant
/phlo/libs/DB/Qdrant.phlo
method
%Qdrant -> get (string $input)
line 5
Stores an embedding in APCu-cache, with a key based on the input, and retrieves it or generates and caches it for 28 days.
apcu('embedding/'.token(input: $input), fn($input) => %OpenAI->embedding($input), 86400 * 28)method
%Qdrant -> collections
line 7
Retrieves the names from the collections and returns them as an array.
array_column($this->request('collections')->result->collections, 'name')method
%Qdrant -> create ($collection, $size = 1536, $distance = 'Cosine')
line 8
Creates a new collection with the specified name, vector size, and distance metric, and checks if the creation was successful.
$this->request("collections/$collection", PUT: arr(vectors: arr(size: $size, distance: $distance)))->status === 'ok'method
%Qdrant -> upsert ($collection, $id, $input, ...$payload)
line 9
Performs an HTTP PUT request to the endpoint for updating or adding a point in a collection. It sends an array with point data, including ID, vector, and optionally (payload). The result is the operation ID of the performed operation.
$this->request("collections/$collection/points", PUT: arr(points: [arr(id: $id, vector: $this->get($input), payload: $payload ?: null)]))->result->operation_idmethod
%Qdrant -> delete ($collection, ...$ids)
line 10
Sends a POST request to the API to delete specific points from a collection by specifying their IDs, with the endpoint 'collections/{collection}/points/delete'.
$this->request("collections/$collection/points/delete", POST: arr(points: $ids))->resultmethod
%Qdrant -> search ($collection, $input = null, $top = 100)
line 11
Performs a search operation in a collection by sending points to the API, with default or provided input. Results are filtered and returned with specific IDs and payload data, with missing input filled with zeros in a vector of 1536 elements.
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
Deletes a collection with the specified name by sending a DELETE request to the server and returning the result.
$this->request("collections/$collection", DELETE: true)->resultmethod
%Qdrant -> request ($uri, ...$data)
line 14
Sends an HTTP request to a Qdrant server with the specified URI and data, including an API key if necessary, and then decodes the JSON response into an associative array.
json_decode(HTTP(%creds->qdrant->server.$uri, %creds->qdrant->key ? ['api-key: '.%creds->qdrant->key] : [], true, ...$data))object
%SQLite
/phlo/libs/DB/SQLite.phlo
method
%SQLite -> controller
line 8
Creates a connection to an SQLite database with the filename specified in `$file`.
andle => "SQLite/$file"method
%SQLite -> __construct (private string $file)
line 9
This is the constructor of the class, which receives a path of type string as a parameter and is likely used to store or initialize the path or name of the SQLite database.
prop
%SQLite -> PDO
line 11
Creates a new PDO instance with an SQLite database, using the database filename from a property of the object.
new PDO('sqlite:'.$this->file)DOM
object
%CSS_var
/phlo/libs/DOM/CSS.var.phlo
view
script
line 5
This code defines a property that provides access to CSS variables on the document via a proxy. When read, the value of the CSS variable is retrieved, and when written, it is modified.
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
view
script
line 5
This code responds to click events on elements with specific data-attributes and determines the HTTP method and URI based on that. Then, the appropriate API call is made via an app function, with optional data passed along.
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
view
script
line 5
This code defines a set of dialog functions (alert, confirm, prompt) that use a dynamically created HTML dialog. The 'phlo.dialog' function creates a dialog window based on the type and displays it, waits for the user, and then returns the appropriate value. Additionally, a click event listener is created that, upon detecting a 'data-confirm' attribute, shows a confirmation dialog and takes further actions based on the response.
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
view
script
line 5
This code defines a functionality to detect whether elements exist: it maintains a list of elements and their associated callbacks, and executes these callbacks when the elements have not been previously registered as existing, using a WeakMap to keep track of already registered elements.
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
view
script
line 5
This code contains a function that submits form data via an AJAX call based on the method name, and an event listener that synchronizes the form values with attributes and saves the internal state upon input or change. Additionally, it provides a custom submit handler for forms with the class 'async' to prevent default behavior and send the data via the function.
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
view
script
line 5
Loads an image, adjusts the dimensions based on maximum width and height while maintaining aspect ratio, and generates a resized datalink via a callback.
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
%link
/phlo/libs/DOM/link.phlo
view
script
line 5
In the situation where clicking on a link with the class 'async', depending on keystrokes or confirmation status, blocks the default navigation and an asynchronous fetch action is performed.
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
view
script
line 5
Parse transformation for Markdown text to HTML with support for GFM functionality. Markers are recognized and converted, such as headings, lists, tables, code blocks, blockquotes, and inline elements. Sanitization is applied to hyperlinks, images, and text decorations, and HTML is embedded securely. References and ID generation logic are also used for headings and tables.
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 => ({'&':'&','<':'<','>':'>','"':'"'}[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, "&").replace(/<(?!\/?[A-Za-z][^>]*>)/g, "<")
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
view
script
line 5
These functions are short helper functions that add event listeners for 'change', 'click', and 'input' events, with fewer arguments needed.
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
view
script
line 5
This code implements a reactive data storage and coupling system with observation and automatic updates. It includes methods for retrieving and setting data via paths, managing signals, handling dependencies between calculations, and updating DOM elements based on the store. Additionally, it uses proxies for dynamic access and modification of data, and ensures synchronization between DOM and store via data-attributes and event handlers. The structure supports automatic evaluation of calculations and efficient re-calculation upon changes.
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
view
script
line 6
This function executes a template function for each row in 'rows', passing the values of the row as arguments.
app.mod.template = (template, rows) => rows.forEach(row => templates[template](...Object.values(row))),
const templates = {}object
%timestamps
/phlo/libs/DOM/timestamps.phlo
view
script
line 6
This code defines a function that calculates the age of elements with a specific data label (dataset.ts) in seconds and converts it into a readable time display, based on a set of time intervals. The function updates this every second and upon startup, providing live updates of elapsed time.
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
view
script
line 5
This code defines an observation system that responds to element visibility. It registers observations, periodically checks via an update callback, and uses an IntersectionObserver to detect when elements become visible. When observed, the appropriate callback (entering or leaving view) is executed.
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
prop
%field_bool -> true
line 7
This node always returns the value '✅', regardless of input or context.
'✅'prop
%field_bool -> false
line 8
Displays an error message or negative status.
'❌'method
%field_bool -> label ($record, $CMS)
line 10
Displays a value based on whether the field `$record->{$this->name}` is true (truthy), by returning the 'true' value, otherwise the 'false' value.
$record->{$this->name} ? $this->true : $this->falsemethod
%field_bool -> input ($record, $CMS)
line 11
Creates a checkbox input with a label and a styled slider. The checkbox is checked if the record's value is not true.
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
This code converts the value of a field in `$payload` to a boolean and assigns it to the corresponding field in `$record`.
$record->{$this->name} = (bool)%payload->{$this->name};method
%field_bool -> sql
line 14
Returns an SQL statement that defines a column named from the property with a datatype of tinyint(1) unsigned, suitable for boolean values.
"`$this->name` tinyint(1) unsigned"method
%field_bool -> nullable
line 15
This node always returns the value false, regardless of the input or context.
falseobject
%field_child
/phlo/libs/Fields/child.phlo
prop
%field_child -> list
line 7
It counts the number of elements in a list or collection.
'count'prop
%field_child -> change
line 8
This code always returns false, regardless of the input or situation.
falseprop
%field_child -> create
line 9
This code always returns a negative result, preventing a certain action or creation from being carried out.
falseprop
%field_child -> record
line 10
This is a property of a method that returns a list, probably consisting of child fields or related records.
'list'method
%field_child -> count ($record, $CMS)
line 12
Creates an HTML link that points to a specific record, with a dynamic URL based on record and object data, and displays the number of related items next to a title.
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
Stores the latest value of a specific field within a record by retrieving the most recent value that matches the field name.
$record->getLast($this->name)method
%field_child -> label ($record, $CMS)
line 14
This code generates a concatenated list of links for each element in a collection, separated by no separator, and displays a dash if the list is empty or falsy.
implode(loop($record->{$this->name}, fn($child) => $this->link($child, $CMS))) ?: dashmethod
%field_child -> input ($record, $CMS)
line 15
Returns the label of the record.
$this->label($record)method
%field_child -> link ($record, $CMS)
line 16
Creates a URL based on the record data and adds an HTML link with the generated href, including a CSS class for asynchronous functionality.
$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
prop
%field_date -> handle
line 7
This node always returns `true`, regardless of the input or context.
truemethod
%field_date -> sql
line 9
This indicates that the value is a non-negative date, with the DATE type in SQL and negative values not allowed.
"`$this->name` DATE unsigned"object
%field_datetime
/phlo/libs/Fields/datetime.phlo
prop
%field_datetime -> handle
line 7
This node always outputs the value true.
trueprop
%field_datetime -> change
line 8
Checks if the name is not equal to 'created', 'changed', or 'updated'.
!in_array($this->name, ['created', 'changed', 'updated'])prop
%field_datetime -> create
line 9
Check if the name is not equal to 'created', 'changed', or 'updated'.
!in_array($this->name, ['created', 'changed', 'updated'])method
%field_datetime -> label ($record, $CMS)
line 11
Displays a clock icon and a human-readable time if the value exists; otherwise, a dash is shown.
($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})) : dashmethod
%field_datetime -> labelIconClass ($value)
line 12
The code block determines the color based on the value: red if the value is greater than 86400, yellow if it is between 3600 and 86400, and blue for lower values.
$value > 86400 ? 'red' : ($value > 3600 ? 'yellow' : 'blue')method
%field_datetime -> input ($record, $CMS)
line 13
Creates a datetime input field with type 'datetime-local', displays a styled input, with the value set to a formatted date and time value from the record.
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
This code processes dates for a record. If the field name is 'created' and has no value yet, the current time is set. For 'changed' or 'updated', the current time is assigned. If a payload value exists, it is converted to a timestamp with strtotime and assigned to the record.
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
The code defines a database column named `$this->name`, of type unsigned integer with a length of 10.
"`$this->name` int(10) unsigned"object
%field_email
/phlo/libs/Fields/email.phlo
view
%field_email -> label ($record, $CMS)
line 8
This code generates a mailto link with the email address from the record, allowing users to send an email directly by clicking on the link.
<a href="mailto:{{ $record->{$this->name} }}">{{ $record->{$this->name} }}</a>object
%field_many
/phlo/libs/Fields/many.phlo
prop
%field_many -> list
line 7
This piece of code returns the text 'label'.
'label'prop
%field_many -> record
line 8
This node simply passes through the value of 'label', without further processing or context.
'label'prop
%field_many -> create
line 9
This code always returns true.
falseprop
%field_many -> change
line 10
This node always returns the value `false`, regardless of the input.
falsemethod
%field_many -> count ($record)
line 12
This code calls the `getCount` method on the `$record` object, with the field name as the parameter, and thus probably returns the number of item(s) or the number of times a specific field appears.
$record->getCount($this->name)method
%field_many -> label ($record, $CMS)
line 13
Loop through the elements in the relationship, create a link for each, and return the result; if empty, a hyphen is displayed.
loop($record->{$this->name}, fn($relation) => $this->link($relation), lf) ?: dashmethod
%field_many -> link ($record)
line 14
Create a hyperlink with a dynamic URL based on record data, including a class for asynchronous action, and display the record as the link text.
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $record)method
%field_many -> input ($record, $CMS)
line 15
Displays a label based on the record and CMS.
$this->label($record, $CMS)method
%field_many -> sql
line 17
The result is an empty array, indicating that this method or function does not return data or perform any operations.
[]object
%field_number
/phlo/libs/Fields/number.phlo
prop
%field_number -> decimals
line 7
Returns the number with zero decimals, or an integer value.
prop
%field_number -> length
line 8
This code simply returns the number 5, which probably indicates the length of a field or a similar value.
5prop
%field_number -> min
line 9
This code always returns 0.
method
%field_number -> label ($record, $CMS)
line 11
Formats a number from the record with a specified number of decimals, using a comma as the decimal separator and a period as the thousand separator.
number_format($record->{$this->name}, $this->decimals, comma, dot)method
%field_number -> input ($record, $CMS)
line 12
Creates a numeric input field with dynamic value, default value, step size based on decimals, minimum value, and a CSS class.
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
This code generates an SQL field definition that defines a numeric field, depending on the presence of decimals. If there are decimals, a 'decimal' data type is used with a custom precision and scale; otherwise, 'smallint' is used. In both cases, the unsigned option is added.
"`$this->name` ".($this->decimals ? 'decimal('.($this->length + $this->decimals).comma.$this->decimals.')' : "smallint($this->length)").' unsigned'object
%field_parent
/phlo/libs/Fields/parent.phlo
method
%field_parent -> label ($record, $CMS)
line 7
Checks whether a field is a model object; if so, it generates a link, otherwise a dash is displayed.
is_a($obj = $record->{$this->name}, 'model') ? $this->link($obj) : dashmethod
%field_parent -> input ($record, $CMS)
line 8
This code generates an HTML select element with options based on a list of options. For each option, it checks if it matches a value from the record, and if so, the 'selected' attribute is added.
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
%field_parent -> link ($record, $content = null)
line 9
Create an HTML anchor link with a dynamically generated URL and add the content or use the record as content, with a CSS class 'async'.
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $content ?? $record)prop
%field_parent -> options
line 10
Retrieves all available records within the context of the class or object.
$this->obj::records()method
%field_parent -> sql
line 12
Defines a varchar field named `$this->name` that can contain up to 10 characters.
"`$this->name` varchar(10)"object
%field_password
/phlo/libs/Fields/password.phlo
prop
%field_password -> list
line 7
This node always returns the value false, regardless of any input or context.
falseprop
%field_password -> required
line 8
This node always returns the value 'true', which means that the field is always marked as required.
trueprop
%field_password -> minlength
line 9
A minimum length of 8 characters for a password.
8prop
%field_password -> placeholder
line 10
It returns a text label indicating that it is the field for the new password.
'Nieuw wachtwoord'method
%field_password -> input ($record, $CMS)
line 12
Creates an input field with type, name, placeholder, and a fixed CSS class.
input(type: $this->type, name: $this->name, placeholder: $this->placeholder, class: 'field')method
%field_password -> label ($record, $CMS)
line 13
Displays a fixed, encrypted, or hidden password label, regardless of input or context.
'••••••••'method
%field_password -> parse ($record)
line 14
This code retrieves the password value from the payload, removes any spaces, and if a password is provided, hashes it with bcrypt and stores it in the record.
($password = trim(%payload->{$this->name})) && $record->{$this->name} = password_hash($password, PASSWORD_BCRYPT)method
%field_password -> sql
line 16
Indicates that the 'name' field is defined as a string with a maximum length of 60 characters in an SQL table.
"`$this->name` char(60)"object
%field_price
/phlo/libs/Fields/price.phlo
prop
%field_price -> decimals
line 7
This code returns the number 2, which probably indicates that the number should be displayed or used with 2 decimal places.
2object
%field_select
/phlo/libs/Fields/select.phlo
method
%field_select -> input ($record, $CMS)
line 7
Generates a `<select>` element with a dynamic name and options, marking the option as selected based on the record.
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 defines an SQL column named `$this->name` that is of ENUM type, including options concatenated into a list like `'option1','option2',...`.
"`$this->name` enum('".implode("','", $this->options)."')"object
%field_text
/phlo/libs/Fields/text.phlo
prop
%field_text -> length
line 7
This node returns the value 100, probably the length of a text or a similar numerical value.
100prop
%field_text -> multiline
line 8
This property checks whether the length of a text is more than 250 characters.
$this->length > 250method
%field_text -> input ($record, $CMS)
line 10
Selects whether to use a multiple input field or a single input field based on a condition.
$this->multiline ? $this->inputMulti($record) : $this->inputField($record)method
%field_text -> inputField ($record)
line 11
Create an HTML input field with the type, name, value, maximum length, placeholder, and CSS class based on the object properties. The value is escaped if it exists; otherwise, a default value is used.
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
Creates a textarea element with name, placeholder, and class. If a value exists in the record for this name, it is escaped and filled in; otherwise, a default value is used if available.
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
Defines an SQL field named `$this->name`, data type varchar, and length `$this->length`.
"`$this->name` varchar($this->length)"object
%field_token
/phlo/libs/Fields/token.phlo
prop
%field_token -> length
line 7
Returns the length of the content, in this case 8.
8prop
%field_token -> default
line 8
Pick up a token with a length equal to the value of `$this->length`.
token($this->length)prop
%field_token -> create
line 9
This code always returns false, regardless of the input or circumstances.
falseprop
%field_token -> change
line 10
This code always returns `false`, regardless of the context or input.
falseprop
%field_token -> search
line 11
This code ensures that a position is always returned, regardless of the input. It acts as a confirmation that the search was successful or that the result is always true.
truemethod
%field_token -> label ($record, $CMS)
line 13
Creates an HTML-div element with the escaped value of a field from the record as its content.
tag('div', inner: esc($record->{$this->name}))method
%field_token -> parse ($record)
line 15
Sets the value of a field to the value in the record if it does not already exist, or changes it otherwise to a default value.
$record->{$this->name} ??= $this->defaultmethod
%field_token -> sql
line 17
This code defines a varchar column with a name determined by the value of `$this->name` and a length determined by `$this->length`.
"`$this->name` char($this->length)"object
%field_virtual
/phlo/libs/Fields/virtual.phlo
prop
%field_virtual -> create
line 7
This method or function always returns false, probably intended to prevent or mark a certain operation or validation as unsuccessful.
falseprop
%field_virtual -> change
line 8
This code will always return false.
falsemethod
%field_virtual -> sql
line 10
This node returns an empty array without further processing or data.
[]Files
object
%CSV
/phlo/libs/Files/CSV.phlo
static
CSV :: __handle
line 5
This appears to be a property that returns a file or parent reference with a dynamic path constructed from variables for the path and filename.
"CSV/$path$filename"method
%CSV -> __construct (string $filename, ?string $path = null)
line 6
Check if the path is set, and if not, use a default path; assemble the filename and extension; check if the CSV file is readable and if so, call the read function.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.csv'
if (is_readable($this->objFile)) $this->objRead()readonly
%CSV -> objFile:string
line 12
This node reads a CSV file and links it to a variable so that the data can be processed further.
method
%CSV -> objRead
line 14
Process a CSV file by opening it, reading the first row (headers), determining the correct delimiter based on the presence of commas or semicolons, splitting the headers, and then reading all rows. Each row is associated with the headers and stored in an array.
$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
static
file :: __handle
line 5
This code constructs a path by combining 'file/' with a variable '$file' and, if present, an optional name '$name' that is added with a slash.
"file/$file".($name ? "/$name" : void)method
%file -> __construct (public string $file, ?string $name = null, $contents = null, ...$args)
line 6
Sets a name if provided, writes content to the file if content is a string, and imports additional objects or arguments if they are present.
$name && $this->name = $name
is_string($contents) && $this->write($contents)
$args && $this->objImport(...$args)method
%file -> append (string $data)
line 12
Add the specified text to the end of the file without overwriting it.
file_put_contents($this->file, $data, FILE_APPEND)prop
%file -> basename
line 13
Retrieves the base name (filename without path) of the specified file.
pathinfo($this->file, PATHINFO_BASENAME)method
%file -> base64
line 14
Converts the content of the file to a Base64-encoded string.
base64_encode($this->contents)method
%file -> contents
line 15
Read the contents of the file specified by the filename in a property.
file_get_contents($this->file)method
%file -> contentsINI (bool $parse = true)
line 16
Creates an INI string, parses the content depending on the parameter, and returns the result with type transparency if `$parse` is true.
parse_ini_string($this->contents, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW)method
%file -> contentsJSON ($assoc = null)
line 17
Converts the content of a JSON string into a PHP variable, returning the result as an array or object depending on the parameter.
json_decode($this->contents, $assoc)method
%file -> copy ($to)
line 18
Creates a copy of the file and saves it at the specified location.
copy($this->file, $to)method
%file -> created
line 19
Returns the creation time of the file.
filectime($this->file)method
%file -> createdAge
line 20
This node calculates the age since creation by comparing the creation date with the current date.
age($this->created)method
%file -> createdHuman
line 21
This code converts a timestamp into a human-readable format.
time_human($this->created)method
%file -> curl ($type = null, $filename = null)
line 22
Creates a new CURLFile object with the specified filename, type, and optionally a different filename for the upload.
new CURLFile($this->file, $type, $filename)method
%file -> delete
line 23
Performs a check to see if the file exists and attempts to delete it. Then a debug message is logged indicating whether the deletion was successful or not.
first($deleted = $this->exists && unlink($this->file), debug($deleted ? "Deleted $this->basename" : "Could not delete $this->basename"))method
%file -> exists
line 24
Checks whether the file stored in the variable `$this->file` exists on the system.
file_exists($this->file)prop
%file -> ext
line 25
Extracts the file extension from the filename.
pathinfo($this->name, PATHINFO_EXTENSION)prop
%file -> filename
line 26
Extracts the filename without extension from the path or filename stored in the variable.
pathinfo($this->file, PATHINFO_FILENAME)method
%file -> getLine
line 27
Takes a line from the file, removes any whitespace at the end, and returns it; if the end of the file is reached, returns false.
($line = fgets($this->pointer)) === false ? false : rtrim($line)method
%file -> getLength (int $length)
line 28
Reads a certain number of bytes ($length) from the file via the specified pointer.
fread($this->pointer, $length)method
%file -> is (string $file)
line 29
Compares whether the specified filename is equal to the internal property `$file`.
$file === $this->filemethod
%file -> md5
line 30
This code calculates the MD5 hash of the file displayed by the `$file` property.
md5_file($this->file)prop
%file -> mime
line 31
Determines the MIME type of a file based on the filename.
mime($this->name)method
%file -> modified
line 32
Returns the last modification date and time of the file.
filemtime($this->file)method
%file -> modifiedAge
line 33
Returns the age based on the modified date.
age($this->modified)method
%file -> modifiedHuman
line 34
This node returns the most recent change in a human-readable time format.
time_human($this->modified)method
%file -> move ($to)
line 35
Move the file to a new location and update the internal reference to the changed filename or path.
rename($this->file, $to) && $this->file = $toprop
%file -> name
line 36
This retrieves the filename without the path, usually used to get the name without the directory.
$this->basenamemethod
%file -> output ($download = false)
line 37
Creates an output with the content, name, and optionally downloads a file.
output($this->contents, $this->name, $download)prop
%file -> path
line 38
This code returns the absolute path to the directory containing the file, followed by a slash.
realpath(pathinfo($this->file, PATHINFO_DIRNAME)).slashprop
%file -> pathRel
line 39
Checks whether the file path starts with a specific relative root path, and if so, returns the path from that point; otherwise, the full path is displayed.
str_starts_with($this->file, $relRoot = dirname(dirname($_SERVER['DOCUMENT_ROOT'])).slash) ? substr($this->file, strlen($relRoot)) : $this->fileprop
%file -> pointer
line 40
This code opens a file in read and write modes.
fopen($this->file, 'r+')method
%file -> readable
line 41
Checks whether the file linked to the variable 'file' is readable.
is_readable($this->file)method
%file -> src
line 42
Creates a data-URI with the MIME type and the base64 data, suitable for use in image or file references.
"data:$this->mime;base64,$this->base64"method
%file -> size
line 43
Returns the size of the file.
filesize($this->file)method
%file -> sizeHuman (int $precision = 0)
line 44
Converts the size of a file to a human-readable string with optional precision.
size_human($this->size, $precision)method
%file -> sha1
line 45
This method calculates the SHA-1 hash of the file.
sha1_file($this->file)method
%file -> shortenTo (int $length)
line 46
This method shortens a filename to a specified length. If the name is already shorter than the length, the original name is returned. Otherwise, an ellipsis is added so that the final length does not exceed the specified value, including the file extension.
strlen($this->name) <= $length ? $this->name : substr($this->name, 0, $length - strlen($this->ext) - 3).dot.dot.dot.$this->extmethod
%file -> title
line 47
Convert the filename without extension into a title by capitalizing the first letter and replacing underscores or other non-letters with spaces.
ucfirst(strtr(pathinfo($this->name, PATHINFO_FILENAME), [us => space]))method
%file -> token ($length = 20)
line 48
Generates a token with a specified length, using the SHA-1 hash function by default to create the token.
token($length, sha1: $this->sha1)method
%file -> type
line 49
Extracts the part of the MIME value before the slash, for example the type such as 'image' or 'video'.
substr($this->mime, 0, strpos($this->mime, slash))method
%file -> touch
line 50
Creates the file with the specified name (provided by `$this->file`) if it does not already exist or updates the file's access time.
touch($this->file)method
%file -> writeable
line 51
checks if the file is writable.
is_writeable($this->file)method
%file -> writeINI ($data, bool $deleteEmpty = false)
line 52
Writes data to an INI file, where empty sections are not removed unless specified, and processes the data by escaping each value and formatting as key-value pairs with double quotes, separated by new lines.
$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
This code calls a method to write data. If `$deleteEmpty` is not true or `$data` is present, the data is JSON-encoded and passed; otherwise, no data is written.
$this->write(!$deleteEmpty || $data ? json_encode($data, jsonFlags) : void, $deleteEmpty)method
%file -> writeJSONplain ($data, bool $deleteEmpty = false)
line 54
This code checks whether empty data should be written. If $deleteEmpty is false or $data is not empty, the data is evaluated as JSON code and passed to a write function; otherwise, nothing is written.
$this->write(!$deleteEmpty || $data ? json_encode($data) : void, $deleteEmpty)method
%file -> write (string $data, bool $deleteEmpty = false)
line 55
This method writes data to a file and deletes the file if the data is empty and the parameter is set to true. On successful write, a debug message is logged; otherwise, an error is reported.
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 $writtenmethod
%file -> objInfo
line 62
This code creates an array where keys are combined and values are derived from the current object properties. It adds basic information and, depending on a condition, also includes additional file information such as size, creation and modification dates, and MIME type.
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
static
img :: detect ($data)
line 5
This code detects the image format based on the first bytes of the data and returns the corresponding file extension ('jpg', 'png', 'gif', 'webp', 'bmp', 'tiff') by comparing specific headers.
$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
Searches for image results on Google based on a formatted search term, retrieves multiple base64-encoded JPEG data from the HTML, sorts them by size, selects a few, randomly chooses one, and finally decodes it into an image file.
$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 $sourcestatic
img :: __handle
line 27
Outputs the value of the variable `$file` before the string "img/".
"img/$file"method
%img -> __construct (public string $file)
line 28
The constructor accepts a string parameter that is stored in a public property, probably intended to represent a filename or path for the node.
prop
%img -> src:GdImage
line 30
Load an image from a file by reading its contents and then converting it into an image resource.
imagecreatefromstring(file_get_contents($this->file))prop
%img -> width
line 31
This node displays the width of the image by retrieving the width of the image using the `imagesx` function on the source image.
imagesx($this->src)prop
%img -> height
line 32
This node returns the width of an image by retrieving the width of the image with `imagesx()` on the source (`$this->src`).
imagesx($this->src)method
%img -> scale ($width, $height = false, $crop = true)
line 34
Performs a scale operation based on specified width and height, including optional cropping and aspect ratio preservation, with correct image resampling and transparency support.
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 $thismethod
%img -> source
line 95
Captures the output of the `imagejpeg()` function into a buffer and returns it as a string, making the image available in memory for further processing.
ob_start()
imagejpeg($this->src)
return ob_get_clean()method
%img -> save ($file = null)
line 101
Saves the image file, depending on the extension; for 'png' and 'webp' compression is applied, for 'gif' it is saved without compression, and by default JPEG is used with a quality of 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
prop
%INI -> objFile:string
line 5
This node possibly initializes a file or object, depending on the context, and performs a setup or initialization for the selected file or object.
static
INI :: __handle
line 7
This code constructs a filename by combining a path and filename, and adds '/0' if the parameter `$parse` is not true.
"INI/$path$filename".(!$parse ? '/0' : void)method
%INI -> __construct (string $filename, ?string $path = null, bool $parse = true)
line 8
This constructor initializes a filename based on an optional path and the name, by applying dot replacement. It then checks if the ini file is readable and reads it if possible, depending on the parse parameter.
$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
This code reads an INI file with parse_ini_file, storing the contents in objData. Then, the last value of that array is evaluated, and the status of objChanged is set to false. The result of the last function is returned.
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
Writes the normalized data to a file by formatting each key-value pair, properly escaping, and then writing everything together with new lines.
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
Executes the write method when changes have occurred.
$this->objChanged && $this->objWrite()object
%JSON
/phlo/libs/Files/JSON.phlo
static
JSON :: __handle
line 5
Concatenates a path and filename with a condition that, if `$assoc` is a boolean value, a slash and the integer version of `$assoc` are added; otherwise, nothing happens.
"JSON/$path$filename".(is_bool($assoc) ? slash.(int)$assoc : void)method
%JSON -> __construct (string $filename, ?string $path = null, $assoc = null)
line 6
Checks if the $path variable is null and sets it to 'data' if necessary. Builds the full path to the JSON file. If the file is readable, a read function is called with the $assoc parameter.
$path ??= data
$this->objFile = "$path$filename.json"
if (is_readable($this->objFile)) $this->objRead($assoc)readonly
%JSON -> objFile:string
line 12
This code fetches a JSON file and converts it into an object, which is then stored in a variable or passed on for further processing.
method
%JSON -> objTouch
line 14
This node indicates that changes have been made, probably to later check if updates are needed or actions need to be taken.
$this->objChanged = truemethod
%JSON -> objRead ($assoc = null)
line 15
Reads JSON data from a file, stores it in a class property, and returns the class itself for further method chaining.
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
Performs a JSON display to a file and updates the object's status based on the result of the write operation.
first($written = json_write($this->objFile, $data, $flags), $written && $this->objChanged = false)method
%JSON -> __destruct
line 18
Checks for any changes and then saves the data if there are any.
$this->objChanged && $this->objWrite($this->objData)object%PDF
/phlo/libs/Files/PDF.phlo
static
PDF :: toText (string $file):string
line 5
Executes the 'pdftotext' command on a PDF file via a subprocess, reads the output and error messages, checks for errors, and returns the text without leftover form feed characters.
$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
This node does not pass any value, which possibly means that there is no title or that the title is not set.
nullprop
%PDF -> author
line 18
This code always returns null, regardless of the input.
nullprop
%PDF -> subject
line 19
This node always returns null, regardless of the input or other conditions.
nullprop
%PDF -> keywords
line 20
This node does not return any data and therefore returns a null value.
nullprop
%PDF -> creator
line 21
This node returns a string consisting of the text 'Phlo ' followed by the content of the variable or property 'phlo', and then the URL '(https://phlo.tech/)'.
'Phlo '.phlo.' (https://phlo.tech/)'prop
%PDF -> filename
line 23
Returns a file named "Download.pdf" for download.
'Download.pdf'prop
%PDF -> mode
line 24
This node returns the value 'D'.
'D'method
%PDF -> fromHTML ($HTML)
line 26
Creates a new mpdf instance, sets metadata based on existing properties, saves the HTML content, and returns the generated PDF file with the specified filename and output mode.
$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
method
%XLSX -> __construct (string $file)
line 5
Loads an XLSX file by unpacking the ZIP structure, reads cell data and shared strings, and converts the data into an array structure with headers. Processing of worksheets, rows, and cells is done with regex and string manipulation, ensuring shared strings are correctly evaluated and cell references are converted into indices.
$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
Creates a 'button' element with the given arguments as attributes or content.
tag('button', ...$args)function
decrypt($encrypted, $key):string
/phlo/libs/encryption.phlo line 6
Decode the base64-encoded input and check if the decoding is successful and the length is sufficient. If so, the nonce is extracted from the first bytes and the rest is used as ciphertext. Then, the secret data is decrypted using a hash of the provided key and the sodium_crypto_secretbox_open function. If any of the checks fail, false is returned.
($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)) : falsefunction
en($text, ...$args)
/phlo/libs/lang.phlo line 9
This node translates the input text into English, using the same translation function with the provided text and arguments.
%lang->translation('en', $text, ...$args)function
encrypt($data, $key):string
/phlo/libs/encryption.phlo line 5
This code generates a random nonce, encrypts the data using a secret key function, and then encodes the result in Base64 for secure transmission or storage.
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
This defines a function that creates a new field, where the function name is dynamically constructed based on the type. It uses the `$type` argument and additional arguments to generate a field of that specific type.
phlo("field_$type", ...$args, type: $type)function
input(...$args):string
/phlo/libs/form.tags.phlo line 6
This node generates an HTML `<input>` element with the specified arguments as attributes or content.
tag('input', ...$args)function
n8n($webhook, ?array $data = null, $test = false)
/phlo/libs/n8n.phlo line 5
Makes a POST request to a webhook URL, based on server data and test mode, with optional data included.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)function
n8n_test($webhook, ?array $data = null)
/phlo/libs/n8n.phlo line 6
Call a function named 'n8n' with the webhook, an optional data array, and a boolean parameter (probably for a special mode or debug).
n8n($webhook, $data, true)function
nl($text, ...$args)
/phlo/libs/lang.phlo line 8
Translate the provided text into Dutch, possibly with additional arguments for interpolation or context.
%lang->translation('nl', $text, ...$args)function
select(...$args):string
/phlo/libs/form.tags.phlo line 7
Create a 'select' element with the given arguments.
tag('select', ...$args)function
textarea(...$args):string
/phlo/libs/form.tags.phlo line 8
Creates an HTML `<textarea>` element with the given arguments.
tag('textarea', ...$args)Phlo Functions
function
active(bool $cond, string $classList = void):string
/phlo/phlo.php line 265
Executes a condition where, if `$cond` is true or `$classList` is not empty, a class attribute is added with the value of `$classList` and possibly 'active' afterwards, separated by a space if `$classList` already exists and `$cond` is true.
return $cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : void;function
age(int $time):int
/phlo/phlo.php line 266
Returns the number of seconds since the specified time.
return time() - $time;function
age_human(int $age):string
/phlo/phlo.php line 267
Converts an age in seconds to a human-understandable age display using another function.
time_human(time() - $age);function
apcu($key, $cb, int $duration = 3600, bool $log = true):mixed
/phlo/phlo.php line 268
This code attempts to retrieve a value from the APCu cache or generate it via a callback. The value is then returned, and if logging is enabled, a debug message is sent with details about the cache key and the contents or type of the value.
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
Execute a JSON-encoded response with the results of `$cmds`, after checking the CLI mode, or when headers have already been sent, or in streaming context. Apply debug to `$cmds` if enabled, and then stop processing.
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
Does not remove elements and returns the received array directly.
return $array;function
auth_log(string $user):int|false
/phlo/phlo.php line 270
Write a line to the log file with the current date and time, the username, and the visitor's IP address, and append it to the end of the file.
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
This function converts a text to camelCase by removing spaces and starting words with a capital letter, then making the first letter of the result lowercase.
return lcfirst(str_replace(space, void, ucwords(lcfirst($text))));function
chunk(...$cmds):void
/phlo/phlo.php line 222
Sets a static header if it has not been set yet, and then sends the given commands as JSON-streamed data to the client, followed by flushing the output buffers.
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
This function combines the results of two iterations: one with the key callback and one with the value callback (or the items themselves if no value callback is specified), and creates an associative array from them.
return array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items);function
debug(?string $msg = null)
/phlo/phlo.php line 273
Adds the message to a static debug array if the debug mode is enabled or disabled; returns the entire debug collection if no message is provided.
if (!debug) return;
static $debug = [];
if (is_null($msg)) return $debug;
$debug[] = $msg;function
dirs(string $path):array|false
/phlo/phlo.php line 279
This code returns an array of directory paths that start with the specified `$path`, including the trailing slash for each directory.
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
This node generates a complete HTML page with doctype, html tag with language, head section, and body section, with the content dynamically filled in via parameters.
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
Calculates the duration since the start of the request and displays it with the specified number of decimal places; optionally, the result is rounded and a speed indicator is added for short durations.
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
Make an exception with a specific message and optionally a code, default to 500.
throw new PhloException($msg, $code);function
esc(string $string):string
/phlo/phlo.php line 281
This ensures that special HTML characters in the string are encoded so that they are displayed safely in an HTML context, including quotes and replacing invalid characters.
return htmlspecialchars((string)$string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');function
files(string|array $paths, string $ext = '*.*'):array
/phlo/phlo.php line 283
Find all files with the specified extension in the given paths and combine the results into one array.
return array_merge(...loop((array)$paths, fn($path) => glob("$path$ext")));function
first(...$args):mixed
/phlo/phlo.php line 284
Gets the first element from the list of arguments.
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
Processes the input parameters to build a cURL request with the correct HTTP method, headers, body, and agent. Executes the request and returns the response.
$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
Performs text indentation by repeatedly adding tab characters based on the depth, and ensures that existing lines are correctly aligned without extra spaces at the end.
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
Adds indentations to a string based on depth, with extra tab indentations for new lines starting with '<'.
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
Loads the content of the specified file, decodes the JSON data, and returns it. If decoding fails or the file cannot be read, an error message is generated.
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
Write the JSON-encoded data to a file with an exclusive lock to ensure the data is stored securely.
return file_put_contents($file, json_encode($data, $flags ?? jsonFlags), LOCK_EX);function
last(...$args):mixed
/phlo/phlo.php line 287
Returns the last element of the given arguments.
return end($args);function
location(?string $location = null):never
/phlo/phlo.php line 288
This code defines an asynchronous function that, depending on whether a location is specified, redirects the user to that location or back to the previous page. If no location is provided, a default path is used.
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
This method applies a callback to each element of an iterable, where the callback can consist of an array with an object and a method or just a closure. The result is an array with the processed values, or a concatenated string if an implode parameter is provided.
$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
Returns the MIME type based on the file extension from the name, or via mime_content_type if the file exists; otherwise defaults to '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
Creates a new object with the given data as constructor parameters.
return new obj(...$data);function
output(?string $content = null, ?string $filename = null, ?bool $attachment = null, ?string $file = null):never
/phlo/phlo.php line 229
Sets the HTTP headers for content-type, content-length, and content-disposition (attachment or inline), and then sends the content (file or string) to the output before the script stops.
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
This function manages and returns objects of a certain name, supported by a cache. If no name is provided, it returns the list of available names. For a specific name, it checks whether a special handle method exists and calls it, or creates a new object and stores it in the cache. Then, if present, the controller method is called before the object is returned.
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
Initializes default values for build, debug, data, php, and www based on input parameters; defines constant values and configures class autoloading via a class map and fallback to file paths; loads dependencies such as Composer autoload; sets up a required redirect and build check; registers error and exception handlers; executes the 'app' node and handles CLI output or exceptions.
$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
Loads the contents of a file, replaces a specific string to adjust the path, decodes the JSON data, and then calls a function with this data and the application name.
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
Performs an asynchronous operation by calling an external 'phlo_exec' function with the given object name, call name, and any additional arguments, without waiting for the result.
return phlo_exec(www, $obj, $call, false, ...$args);function
phlo_build_check():bool
/phlo/phlo.php line 162
This code checks whether the file 'app.php' does not exist or if the last modified time of this file is older than the most recent modification time of the sources in '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
Adds an error message to a JSON log file by generating a unique ID based on the path and message; keeps track of how many times the same error occurs, and records the last time the error occurred.
$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
Error handling function that processes a Throwable by logging the message, conditions for debugging and development environment, and then generating the appropriate output (console, API, or HTML page) based on the context.
$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
Executes a PHP script via the command line with specified parameters, and returns the output or executes the command asynchronously without output.
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
Check if a PHP file exists that matches the name by replacing the dot and then checking for the existence of the file.
return is_file(php.strtr($obj, [us => dot]).'.php');function
phlo_sources():array
/phlo/phlo.php line 163
Collects .phlo files from a specified source folder or app folder, then adds extra library files if they exist, sorts the list case-insensitively, and returns the ordered list.
$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
This is a function that calls another function or method with a specified object and call, using a global output function.
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
Executes a regular expression on a string and returns the matches as an array; returns an empty array if no match is found.
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
Executes a regular expression on the text and returns all found matches in an array; if no matches are found, an empty array is returned.
return preg_match_all($pattern, $subject, $matches, $flags, $offset) ? $matches : [];function
req(int $index, ?int $length = null):mixed
/phlo/phlo.php line 299
Extracts a specific part from a URL by splitting the string, and returns that part. If a length is specified, multiple parts are concatenated into one string.
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
The code validates and processes an incoming request based on method, asynchronous flag, payload data, and URL path segments. It filters the request, extracts variables from the path, checks for patterns such as parameters with default values, fixed values, or defined lists, and collects these into arguments for a callback. If all checks pass, the callback is invoked with the collected arguments.
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
It retrieves the second to last used part and the last part of a path, separated by a slash, unless the path has fewer than two parts, then only the last part is returned. For empty input, 'unknown' is returned.
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
This function converts a size in bytes to a human-readable format by dividing the number by 1024 until it falls within the appropriate range, then appends the correct scale (B, KB, MB, GB, TB) with the specified precision.
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
This code converts text into a URL-friendly slug: it transforms text to lowercase, removes accents, replaces non-alphanumeric characters with hyphens, and removes unnecessary hyphens from both ends.
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
Creates an HTML tag with the specified tag name and optional content, and adds non-NULL extra attributes as tag or attribute values.
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
This code calculates a human-readable time display by comparing a given timestamp with the current time and converting it into an appropriate time format (such as days, weeks, months). It uses a list of labels and corresponding multipliers to scale the age into a comprehensible unit, and stops when the age falls below a certain threshold.
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
Provides a static list of titles, adds a given title if present, or uses the current app title or a default value, and combines the list into a single string with a specified separator.
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
Generates a random token of a specified length by first creating a SHA1 hash based on input, a number, or a random number, and then concatenating letters based on the hash values until the desired length is reached.
$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
This node handles the generation of an HTML page and the configuration of resources and metadata. It adds CSS and JS files, sets meta-titles and other head elements, manages preloading and defer options, and provides the HTML structure including body attributes. In async processing, important content is passed and applied via `$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));