EN | NL | 中文

Libs

object

%cookies

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

%cookies -> controller

line 7
Deze code wijst de inhoud van alle cookies toe aan een objecteigenschap, waardoor deze later in de code hergebruikt kan worden.
this->objData = $_COOKIE
prop

%cookies -> lifetimeDays

line 5
Dit is een code die de waarde van het cookie's levensduur in dagen instelt op 180 dagen.
180
method

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

line 9
Deze code zorgt ervoor dat een waarde wordt opgeslagen in de interne data, de globale cookies en via een HTTP-only, secure cookie met een bepaalde levensduur.
$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
Verwijdert een cookie en de bijbehorende gegevens uit de objecten, door de waarde te unsetten en de cookie te verwijderen met een vervallen timestamp.
unset($this->objData[$key], $_COOKIE[$key])
setcookie($key, void, time() - 86400, slash, $_SERVER['HTTP_HOST'], true, true)
object

%creds

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

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

line 5
In deze constructor worden, indien geen waarden worden meegegeven, gegevens uit een INI-bestand ingeladen. Vervolgens wordt voor elke sleutel-waarde combinatie uit dat array een dynamische eigenschap aangemaakt: als de waarde een array is, wordt er een nieuwe instantie van de klasse zelf gemaakt, anders wordt een SensitiveParameterValue-object gecreëerd.
$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
Deze code controleert of de parameter `$key` gelijk is aan `'toArray'`. In dat geval wordt een recursieve loop uitgevoerd over `objData`, waarbij waarden die van het type `'SensitiveParameterValue'` zijn, worden omgezet door hun `getValue()` methode. Als `$key` niet `'toArray'` is, wordt gecontroleerd of er een element bestaat onder `$key` in `objData` dat van het type `'SensitiveParameterValue'` is, en wordt dan de waarde via `getValue()` geretourneerd.
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
Maakt door de invoer heen een lijst waarbij elke waarde wordt gecontroleerd op het type 'SensitiveParameterValue'. Als dat het geval is, wordt de waarde vervangen door een reeks van sterretjes met dezelfde lengte, anders blijft de waarde ongewijzigd.
loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)
object

%encryption

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

function encrypt ($data, $key):string

line 5
Deze code genereert een willekeurige nonce, versleutelt de data met behulp van een geheimsleutelfunctie, en encodeert het resultaat vervolgens in Base64 voor veilige transmissie of opslag.
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
Decodeert de base64-gecodeerde input en controleert of de decodering succesvol is en de lengte voldoende is. Zo ja, wordt de nonce uit de eerste bytes gehaald en wordt de rest gebruikt als ciphertext. Vervolgens wordt de geheime data ontsleuteld met een hash van de opgegeven sleutel en de sodium_crypto_secretbox_open-functie. Als een van de controles faalt, wordt false teruggegeven.
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
object

%field

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

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

line 6
Dit definieert een functie die een nieuw veld aanmaakt, waarbij de functie naam dynamisch wordt opgebouwd op basis van het type. Het gebruikt het `$type` argument en extra argumenten om een veld van dat specifieke type te genereren.
phlo("field_$type", ...$args, type: $type)
static

field :: __handle

line 8
Deze node geeft altijd een null-waarde terug, ongeacht de invoer of context.
null
prop

%field -> title

line 10
Het maakt de eerste letter van de waarde van de variabele $name hoofdletter.
ucfirst($this->name)
method

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

line 12
Maakt een invoerveld aan met het type, naam en andere attributen gebaseerd op de eigenschappen van het object en vult de waarde in uit het record of een standaardwaarde.
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
Deze code geeft de waarde van een property terug uit het record, gebaseerd op de naam van het veld.
$record->{$this->name};
object

%form_tags

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

function button (...$args):string

line 5
Creëert een 'button'-element met de meegegeven arguments als attributen of inhoud.
tag('button', ...$args)
function

function input (...$args):string

line 6
Deze node genereert een HTML `<input>` element met de opgegeven argumenten als attributen of inhoud.
tag('input', ...$args)
function

function select (...$args):string

line 7
Maakt een 'select' element aan met de gegeven argumenten.
tag('select', ...$args)
function

function textarea (...$args):string

line 8
Maakt een HTML `<textarea>` element aan met de meegegeven argumenten.
tag('textarea', ...$args)
object

%lang

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

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

line 8
Translate de meegegeven tekst naar het Nederlands, eventueel met extra argumenten voor interpolatie of context.
%lang->translation('nl', $text, ...$args)
function

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

line 9
Deze node vertaalt de invoertekst naar het Engels, gebruikmakend van dezelfde vertaalfunctie met de opgegeven tekst en argumenten.
%lang->translation('en', $text, ...$args)
view

%lang ->

line 11
Dit stuk code voert een vertaling uit door de taalinstelling van de applicatie te gebruiken, waardoor de juiste taal wordt geladen voor de gebruiker.
%app->lang
prop

%lang -> browser

line 13
Deze code selecteert de meest geschikte taalcode uit de 'HTTP_ACCEPT_LANGUAGE'-header van de gebruiker door de eerste taal in de header te vergelijken met beschikbare talen in de applicatie. Als er geen geschikte taal wordt gevonden, wordt de standaardtaal uit de eerste overeenkomstige taal geselecteerd of null als er geen matches zijn.
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
Controleert of de taal uit de cookies bestaat in de beschikbare talen. Als dat het geval is, wordt de taalwaarde teruggegeven; anders wordt null geretourneerd.
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : null
prop

%lang -> model

line 16
Een korte toelichting is dat deze node waarschijnlijk een instelling of parameter bevat voor het gebruik van een kleiner of geoptimaliseerd GPT-4 model, geschikt voor minder veeleisende taken of snellere verwerking.
'gpt-4o-mini'
method

%lang -> translations

line 17
Initialiseert de vertalingen voor de huidige taal, met gebruik van de instellingen of gegevens uit '%app->lang' en 'langs'.
%INI(%app->lang, langs)
method

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

line 19
De code voert een chat-aanvraag uit bij een AI-model om de taal van een tekst te identificeren en die identificatie terug te geven als een ISO 639-1 code. Als de response geen geldige 2-karaktercode is, wordt een fallback-taal gebruikt.
$res = %OpenAI->chat (
	model: $this->model,
	system: 'Analyseer welke taal deze tekst is en geef alleen de ISO 639-1 code van de taal terug, zonder andere data!',
	user: $text.lf.lf.'De ISO 639-1 code van de taal is: ',
	temperature: 0,
)->answer
return strlen($res) === 2 ? strtolower($res) : $fallback
method

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

line 29
Creëert een korte, unieke hash door eerst een samenvatting te maken van de tekst met een regex en hoofdletters, het eerste deel hiervan te gebruiken als prefix, en vervolgens een MD5-hash van de volledige tekst toe te voegen, zodat de resulterende code zowel herkenbaar als uniek is.
$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
Deze methode vertaalt tekst van een opgegeven taal naar de actieve taal. Als de bron- en doeltaal gelijk zijn, wordt de tekst direct geretourneerd. Anders wordt de tekst opgesplitst in regels, voor elke regel wordt een hash gemaakt en gezocht naar een vertaling. Als die niet gevonden wordt, wordt een asynchrone vertaling aangevraagd. De vertalingen worden samengevoegd en teruggegeven, met optionele formattering als er argumenten zijn meegegeven.
if ($from === %app->lang) $translation = $text
else {
	$translation = []
	foreach (explode(lf, $text) AS $line){
		if (trim($line)){
			$hash = $this->hash($from, $line)
			if (!$item = $this->translations->$hash) phlo_async('lang', 'asyncTranslation', $from, %app->lang, $item = $line)
		}
		else $item = void
		$translation[] = $item
	}
	$translation = implode(lf, $translation)
}
$translation = strtr($translation, ['\n' => lf])
return $args ? sprintf($translation, ...$args) : $translation
static

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

line 49
Deze code wijzigt de taal van de applicatie, genereert een unieke hash op basis van de originele taal en tekst, vertaalt de tekst en slaat de vertaling op in een translatiestructuur met de hash als sleutel.
%app->lang = $to
$hash = $this->hash($from, $text)
$translation = $this->translate($from, $to, $text)
return $this->translations->$hash = $translation
method

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

line 56
Deze functie controleert eerst of de bron- en doeltaal hetzelfde zijn en retourneert in dat geval de oorspronkelijke tekst. Anders maakt het een chat-aanroep naar OpenAI, waarbij het model wordt geïnstrueerd om de tekst te vertalen tussen de opgegeven talen, met behoud van markdown, interpunctie en hoofdlettergebruik, en geeft alleen de vertaalde tekst terug.
if ($from === $to) return $text
return %OpenAI->chat (
	model: $this->model,
	system: "You will be provided with a word, sentence or (markdown) text in ISO 639-1 language $from, and your task is to translate this string into ISO 639-1 language $to. Respect markdown, missing interpunction and specific use of capitals. Give only the translation.",
	user: $text,
	temperature: 0,
)->answer
object

%model

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

model :: DB

line 8
Verbindt met een MySQL database.
%MySQL
static

model :: objRecords

line 9
Een lege array, betekent dat er momenteel geen records of items in deze node aanwezig zijn.
[]
static

model :: objLoaded

line 10
Het lijkt een lege array te retourneren, mogelijk bedoeld om aan te geven dat er geen gegevens geladen zijn of dat er geen actie is uitgevoerd.
[]
static

model :: objCache

line 11
Deze code retourneert altijd de waarde 'false'.
false
static

model :: columns

line 13
Controleert of de statische property `$columns` bestaat; zo ja, wordt die geretourneerd. Als niet, wordt gekeken of er een methode `schema` bestaat; zo ja, wordt deze methode aangeroepen via `_columns()`. Als geen van beide het geval is, wordt de string '*' teruggegeven.
isset(static::$columns) ? static::$columns : (method_exists(static::class, 'schema') ? static::_columns() : '*')
static

model :: _columns

line 14
Deze code genereert een gequoteerde lijst van kolomnamen voor databasegebruik, waarbij alleen velden met specifieke types ('child', 'many', 'virtual') worden uitgesloten. Het combineert velddefinities en voegt de juiste quotes toe voor gebruik in een 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).$fq
static

model :: fields

line 19
Controleert of er een 'schema'-methode bestaat in de klasse. Als dat het geval is, wordt de output van '_fields()' gebruikt; anders wordt gezocht naar de statische eigenschap '$fields' of een lege array als die niet bestaat.
method_exists(static::class, 'schema') ? static::_fields() : (static::$fields ?? [])
static

model :: _fields

line 20
Loopt door de schema-velden en selecteert voor elk veld de laatste waarde uit een keten van alternatieven: of de naam, of het object (bij voorwaarden met type 'parent'), of het veld zelf.
loop(static::schema(), fn($field, $column) => last($field->name ??= $column, $field->type === 'parent' && $field->obj ??= $column, $field))
static

model :: field ($name)

line 21
Geeft de veldconfiguratie terug uit de array van alle velden, gebaseerd op de opgegeven naam.
static::fields()[$name]
static

model :: create (...$args)

line 23
Deze code maakt een nieuw record aan en retourneert dit vervolgens, mogelijk voor verdere verwerking of als resultaat van de functie.
static::record(id: static::createRecord(...$args))
static

model :: createRecord (...$args)

line 24
Maakt een nieuw databaserecord aan in de opgegeven tabel met de meegegeven arguments.
static::DB()->create(static::$table, ...$args)
static

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

line 25
Deze code roept een statische methode 'change' op die waarschijnlijk een wijziging doorvoert in de database voor een specifiek tabel. Het gebruikt de database-verbinding en tabelnaam uit statische eigenschappen en accepteert voorwaarden en extra argumenten om de wijziging uit te voeren.
static::DB()->change(static::$table, $where, ...$args)
static

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

line 26
Voert een delete-bewerking uit op de database voor de opgegeven tabel, gebaseerd op de meegegeven voorwaarden en eventuele extra argumenten.
static::DB()->delete(static::$table, $where, ...$args)
method

%model -> objSave

line 28
Controleren of er een id is; indien niet, een foutmelding geven. Als een record met die id bestaat, wordt deze bijgewerkt en geretourneerd. Anders wordt een nieuw record aangemaakt en geretourneerd.
$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
Laadt meerdere records en retourneert enkel de kolomwaarden, zonder Object-Of-Array structuur.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_COLUMN])
static

model :: item (...$args)

line 38
Laadt records met de opgegeven argumenten, gebruikt de 'fetch' modus en haalt enkel één kolom op.
static::recordsLoad($args, 'fetch', [PDO::FETCH_COLUMN])
static

model :: pair (...$args)

line 39
Laadt meerdere records en retourneert ze als een key-value paar, waarbij de fetch-modus is ingesteld op FETCH_KEY_PAIR voor snelle en eenvoudige mappen.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_KEY_PAIR])
static

model :: records (...$args)

line 40
Laadt meerdere records uit de database en retourneert ze als unieke objecten van de class, met alle records samengevoegd in een enkele collectie.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, static::class], true)
static

model :: recordCount (...$args)

line 41
Deze code telt het aantal records door een telling uit te voeren op de kolom 'id'.
static::item(...$args, columns: 'COUNT(id)')
static

model :: record (...$args)

line 42
Berekent het aantal records en geeft een foutmelding bij meer dan één record; anders retourneert het het eerste record of null als er geen records zijn.
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
Deze code bouwt dynamisch query-parameters op voor het laden van records uit een database, inclusief condities, joins, selecties en groepsregels, afhankelijk van de statische eigenschappen en argumenten. Vervolgens wordt gebruik gemaakt van caching met APCu op basis van een cachekey en duur, waardoor herhaald laden efficiënter verloopt. Tot slot worden de geladen records opgeslagen voor hergebruik en geretourneerd.
$args['table'] ??= static::$table
$saveRelations && $args['columns'] ??= static::$table.'.id as _,'.static::columns()
isset(static::$joins) && $args['joins'] = static::$joins.(isset($args['joins']) ? " $args[joins]" : void)
method_exists(static::class, 'where') && $args['where'] = static::where().(isset($args['where']) ? " AND $args[where]" : void)
isset(static::$group) && $args['group'] ??= static::$group
isset(static::$order) && $args['order'] ??= static::$order
if ($cacheKey = $args['cacheKey'] ?? null) unset($args['cacheKey'])
if ($duration = static::$objCache) $records = apcu($cacheKey ?? static::class.slash.md5(json_encode($args)), fn() => static::DB()->load(...$args)->$fetch(...$fetchMode), $duration === true ? 86400 : $duration)
else $records = static::DB()->load(...$args)->$fetch(...$fetchMode)
if ($saveRelations && $records) self::$objRecords[static::class] = (self::$objRecords[static::class] ?? []) + array_column($records, null, 'id')
return $records
static

model :: objRel ($key)

line 58
Geeft de waarde van een statisch eigenschap of methode of, indien niet aanwezig, een standaardwaarde (lege array).
static::$classProps[static::class][$key] ??= method_exists(static::class, $key) ? static::$key() : static::$$key ?? []
prop

%model -> objState

line 60
Initialiseert een structuur met drie lege collecties: 'parents', 'children' en 'many'.
['parents' => [], 'children' => [], 'many' => []]
method

%model -> objGet ($key)

line 61
Zoek eerst in de ouder, dan in de kinderen, en als geen resultaat wordt gevonden, zoek vervolgens meerdere items.
$this->getParent($key) ?? $this->getChildren($key) ?? $this->getMany($key)
method

%model -> objIn ($ids)

line 62
Converteert een array van IDs naar een door komma's gescheiden string tussen aanhalingstekens, of retourneert 'NULL' als de array niet bestaat of leeg is.
$ids ? dq.implode(dq.comma.dq, $ids).dq : 'NULL'
method

%model -> getParent ($key)

line 64
Retourneert het directe ouderobject gebaseerd op een sleutel, met controle op eerder geladen relaties en het dynamisch ophalen van gerelateerde records als die nog niet geladen zijn.
if (array_key_exists($key, $this->objState['parents'])) return $this->objState['parents'][$key]
$parents = self::objRel('objParents')
if (!$relation = $parents[$key] ?? null) return
$isArray = is_array($relation)
$class = $isArray ? $relation['obj'] : $relation
$column = $isArray ? $relation['key'] ?? $key : $key
if (!$parentId = $this->objData[$column] ?? null) return $this->objState['parents'][$key] = null
if (!isset(self::$objRecords[$class][$parentId])){
	$idsToLoad = [$parentId => true]
	$allObjData = array_map(fn($record) => $record->objData, self::$objRecords[static::class] ?? [])
	foreach ($parents as $pKey => $pRelation){
		$pIsArray = is_array($pRelation)
		$pClass = $pIsArray ? $pRelation['obj'] : $pRelation
		if ($pClass === $class) foreach (array_column($allObjData, $pIsArray ? $pRelation['key'] ?? $pKey : $pKey) as $pId) $pId && !isset(self::$objRecords[$class][$pId]) && $idsToLoad[$pId] = true
	}
	if ($idsToLoad = array_keys($idsToLoad)) $class::records(where: 'id IN ('.$this->objIn($idsToLoad).')')
}
$parentObject = self::$objRecords[$class][$parentId] ?? null
return $this->objState['parents'][$key] = $parentObject
method

%model -> getChildren ($key)

line 86
Verifieert of de children voor een opgegeven sleutel al zijn geladen; indien niet, wordt de relatie opgevraagd en worden de bijbehorende records ingeladen en in de state opgeslagen; tot slot wordt de children-array geretourneerd.
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
Laadt meerdere gerelateerde records uit de database indien deze nog niet eerder geladen zijn en cachet deze in de objectstaat, zodat bij volgende aanroepen de records direct beschikbaar zijn.
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
Deze code controleert eerst of de telling al in de interne staat is opgeslagen, en retourneert die indien beschikbaar. Vervolgens identificeert het of de telling te maken heeft met een kind-relatie ('objChildren') of een meervoudige relatie ('objMany'). Als het een kind-relatie is, haalt het de IDs van gekoppelde records op, voert een geaggregeerde query uit om de telling te bepalen en cachet deze resultaten. Voor een 'many'-relatie wordt vergelijkbare logica toegepast, maar dan via een aparte tabel en kolom. Als geen relatie wordt gevonden, wordt 0 geretourneerd.
if (array_key_exists($key, $this->objState['counts'] ?? [])) return $this->objState['counts'][$key]
if ($relation = self::objRel('objChildren')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['children_count'][$key])){
		$parentIds = array_keys(self::$objRecords[static::class] ?? [])
		if ($parentIds){
			$isArray = is_array($relation)
			$class = $isArray ? $relation['obj'] : $relation
			$column = $isArray ? $relation['key'] : static::class
			$counts = $class::pair(columns: "`$column`, COUNT(*)", where: '`'.$column.'` IN ('.$this->objIn($parentIds).')', group: "`$column`")
			foreach (self::$objRecords[static::class] as $id => $record) $record->objState['counts'][$key] = (int)($counts[$id] ?? 0)
		}
		self::$objLoaded[static::class]['children_count'][$key] = true
	}
	return $this->objState['counts'][$key] ?? 0
}
if ($relation = self::objRel('objMany')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['many_count'][$key])){
		$parentIds = array_keys(self::$objRecords[static::class] ?? [])
		if ($parentIds){
			$counts = static::DB()->load(table: $relation['table'], columns: "`$relation[localKey]`,COUNT(*)", where: '`'.$relation['localKey'].'` IN ('.$this->objIn($parentIds).')', group: "`$relation[localKey]`")->fetchAll(PDO::FETCH_KEY_PAIR)
			foreach (self::$objRecords[static::class] as $id => $record) $record->objState['counts'][$key] = (int)($counts[$id] ?? 0)
		}
		self::$objLoaded[static::class]['many_count'][$key] = true
	}
	return $this->objState['counts'][$key] ?? 0
}
return 0
method

%model -> getLast ($key)

line 156
Zoekt in de objectstatus of de laatste kind gekoppeld aan de opgegeven sleutel al geladen is; indien niet, wordt via relatiegegevens en records de laatste kind opgevraagd, opgeslagen in de statestatus en teruggegeven.
if (array_key_exists($key, $this->objState['last_child'] ?? [])) return $this->objState['last_child'][$key]
if ($relation = self::objRel('objChildren')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['last_child'][$key])){
		if ($parentIds = array_keys(self::$objRecords[static::class] ?? [])){
			$isArray = is_array($relation)
			$class = $isArray ? $relation['obj'] : $relation
			$column = $isArray ? $relation['key'] : static::class
			$childTable = $class::$table
			$whereClause = "`$column` IN (".$this->objIn($parentIds).") AND `$childTable`.`id` = (SELECT `id` FROM `$childTable` AS lc WHERE lc.`$column`=`$childTable`.`$column` ORDER BY `id` DESC LIMIT 1)"
			$lastChildren = $class::records(where: $whereClause)
			foreach (self::$objRecords[static::class] as $record) $record->objState['last_child'][$key] = null
			foreach ($lastChildren as $child) if (isset(self::$objRecords[static::class][$parentId = $child->objData[$column]])) self::$objRecords[static::class][$parentId]->objState['last_child'][$key] = $child
		}
		self::$objLoaded[static::class]['last_child'][$key] = true
	}
	return $this->objState['last_child'][$key] ?? null
}
return null
static

model :: objParents

line 177
Haalt de statische eigenschap op als deze bestaat; anders retourneert het een lege array als de ‘schema’ methode niet bestaat; indien wel, filtert het de velden op type ‘parent’ en maakt het een array, waarbij het voor elk veld een object en optioneel een sleutel teruggeeft of de objectnaam gebruikt.
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
Deze code controleert of de statische property 'objChildren' bestaat en retourneert deze indien aanwezig. Als de 'schema'-methode niet bestaat, geeft hij een lege array terug. Anders wordt een array gefilterd op velden met type 'child', en worden deze vervolgens verwerkt door een loop-functie die voor elk veld beslist of een object en mogelijk een sleutel retourneert, of gewoon het object zelf.
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
Geeft de waarde van de statische eigenschap `objMany` terug als die bestaat; anders, als er geen methode `schema` is, geeft het een lege array terug; anders filtert het de velden op het type 'many' en bouwt een array met object- en tabelleninformatie, inclusief de local en 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
Controleert of de methode schema() bestaat, anders wordt een fout gegooid. Daarna wordt een SQL CREATE TABLE statement opgebouwd door de veldgegevens te itereren en te formatteren, inclusief eventuele NOT NULL of NULL specificaties, en voegt een primaire sleutel op 'id' toe.
method_exists(static::class, 'schema') || error(static::class.' has no schema()')
return 'CREATE TABLE `'.static::$table.'` ('.lf.tab.implode(",\n\t", array_merge(...array_values(array_filter(loop(static::fields(), fn($field) => loop((array)$field->sql, fn($sql) => $sql.($field->required || $field->nullable === false ? ' NOT' : void).' NULL')))))).",\n\tPRIMARY KEY (`id`)\n)"
object

%n8n

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

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

line 5
Maakt een POST-verzoek naar een webhook-URL, gebaseerd op servergegevens en testmodus, met optionele data mee.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

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

line 6
Roep een functie genaamd 'n8n' aan met de webhook, een optionele data-array en een boolean parameter (waarschijnlijk voor een speciale modus of debug).
n8n($webhook, $data, true)
object

%payload

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

%payload -> controller

line 7
Parses JSON request bodies voor POST, PUT, PATCH methoden en voert import uit; behandelt multipart/form-data uploads met bestandsgegevens, slaat bestanden tijdelijk op, en stelt de data structureren in, inclusief array-collecties; verwerkt ook standaard $_FILES data door 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
version 1.0
creator q-ai.nl
description Generic security library
prop

%security -> nonce

line 5
Generert een willekeurige token van 8 tekens en slaat deze op in de property `$nonce`.
%app->nonce = token(8)
method

%security -> full

line 7
Geeft een verzameling beveiligingsheaders of -instellingen door verschillende properties te controleren of toe te wijzen, vermoedelijk gericht op webbeveiliging en privacy.
$this->COOP
$this->CORP
$this->CORS
$this->CSP
$this->Referrer
$this->X_content
$this->X_frame
method

%security -> CSRF

line 17
Genereert en slaat een nieuwe CSRF-token van 12 tekens op in de sessie.
%session->csrf = token(12)
method

%security -> COOP

line 18
Stelt een HTTP-header in die bepaalt dat de pagina alleen geopend mag worden door documenten van dezelfde oorsprong, wat beveiligingsverbeteringen biedt tegen bepaalde aanvalstechnieken zoals side-channel attacks.
header('Cross-Origin-Opener-Policy: same-origin')
method

%security -> CORP

line 19
Voegt een HTTP-header toe die aangeeft dat Cross-Origin Resource Sharing alleen toestaat vanaf dezelfde oorsprong.
header('Cross-Origin-Resource-Policy: same-origin')
method

%security -> CORS ($host = host)

line 20
Stelt de toegestane origin in voor cross-origin resource sharing (CORS) door een header te zetten die enkel toegang toestaat vanaf de opgegeven host.
header("Access-Control-Allow-Origin: https://$host")
method

%security -> CSP

line 21
Verzendt een Content-Security-Policy header die beperkingen oplegt aan bronnen voor content, scripts, styles, afbeeldingen, fonts, connecties, en frame-ancestors, inclusief een nonce voor dynamisch geladen 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
Het stelt een HTTP-header in die bepaalt dat de referrer-informatie alleen wordt meegestuurd naar dezelfde oorsprong of naar derden via een volledige URL, maar niet bij cross-origin requests, waardoor de privacy wordt verbeterd.
header('Referrer-Policy: strict-origin-when-cross-origin')
method

%security -> X_content

line 23
Voegt een HTTP-header toe die voorkomt dat browsers het content-type van de respons gaan raden, wat bijdraagt aan verbeterde beveiliging door het voorkomen van content-sniffing.
header('X-Content-Type-Options: nosniff')
method

%security -> X_frame

line 24
Deze code zet de HTTP-header 'X-Frame-Options' op 'DENY', waardoor voorkomt dat de pagina in een frame of iframe wordt geladen, wat bijdraagt aan beveiliging tegen clickjacking.
header('X-Frame-Options: DENY')
object

%session

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

%session -> controller

line 5
Start de sessie en slaat de inhoud van de $_SESSION-variabele op in een property voor later gebruik.
ession_start()
$this->objData = $_SESSION
method

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

line 8
Deze code wijzigt twee arrays tegelijk: een globale sessie-array en een interne data-array van het object, door de opgegeven waarde toe te wijzen aan de gegeven sleutel in beide arrays.
$_SESSION[$key] = $this->objData[$key] = $value
method

%session -> __unset ($key)

line 9
Verwijdert een sleutel uit zowel de interne gegevensstructuur als uit de sessiegegevens.
unset($this->objData[$key], $_SESSION[$key])

AI

object

%Claude

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

Claude :: context (...$args)

line 7
Voegt 'assistant' en 'user' inhoud toe aan de 'messages'-array indien aanwezig, en verwijdert deze sleutels daarna uit de 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 $args
method

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

line 14
Deze code maakt een geconfigureerde aanvraag naar een chatmodel, waarbij standaardwaarden voor het model en het maximale aantal tokens worden ingesteld. Vervolgens wordt een verzoek gedaan en wordt het antwoord als een object met een `answer`-veld geretourneerd.
$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
Deze functie zet een streaming-verbinding op met een API-endpoint, verstuurt een POST-verzoek met gespecificeerde parameters, en verwerkt de ontvangen data in real-time via een callback. Indien geen callback is meegegeven, wordt automatisch een event-stream opgezet en worden uitgaande data direct weergegeven. De response wordt opgebouwd uit de ontvangen delta-teksten en geretouneerd als volledige tekst.
%app->streaming = true
$args = static::context(...$args)
$args['model'] ??= 'claude-3-5-sonnet-latest'
$args['max_tokens'] ??= 3333
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($data) => last(($text = $data->delta->text ?? void) === void || [print($text), cli || [ob_flush(), flush()]], $text)
}
$answer = void
$curl = curl_init('https://api.anthropic.com/v1/messages')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $data = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['anthropic-version: 2023-06-01', 'Content-Type: application/json', 'Content-Length: '.strlen($data), 'X-Api-Key: '.%creds->Claude])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$answer){
	foreach (array_filter(explode(lf.lf, $data)) AS $chunk){
		$obj = json_decode(substr($chunk, strpos($chunk, 'data: {') + 6))
		$res = $cb($obj, $obj->delta->text ?? null)
		if (is_string($res)) $answer .= $res
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

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

line 53
Verzendt een HTTP-verzoek naar een opgegeven API-endpoint met standaardheaders en API-sleutel, decodeert het JSON-resultaat, controleert op fouten, en retourneert het gedecodeerde 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 $res
object

%DeepSeek

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

DeepSeek :: context (...$args)

line 7
Voegt standaardwaarden toe aan de 'messages' array en voegt gespreksrollen toe zoals 'system', 'assistant' en 'user' op basis van de meegegeven argumenten, waarna de originele argumenten worden opgeschoond.
$args['messages'] ??= []
if (isset($args['system']) && array_unshift($args['messages'], arr(role: 'system', content: $args['system']))) unset($args['system'])
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
method

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

line 15
Verwerkt een chat-aanvraag door het voorbereiden van argumenten, verstuurt een request naar het chat-completions eindepunt, en retourneert een object met modelinformatie, finish-reden, tokengebruik en inhoud of tool-aanroepen.
$args = static::context(...$args)
$res = $this->request('chat/completions', POST: $args)
$return = obj(model: $res->model, finish: $res->choices[0]->finish_reason, tokens: $res->usage->total_tokens, tokens_in: $res->usage->prompt_tokens, tokens_out: $res->usage->completion_tokens)
if (isset($res->choices[0]->message->tool_calls)) $return->tools = loop($res->choices[0]->message->tool_calls, fn($tool) => obj(name: $tool->function->name, args: json_decode($tool->function->arguments, true)))
else $return->answer = $res->choices[0]->message->content
return $return
method

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

line 24
Deze methode stelt een streamverbinding op met een API, verwerkt tijdelijke data-uploads via cURL, en gebruikt een callback om real-time data te verwerken en samen te voegen tot een eindantwoord.
%app->streaming = true
$args = static::context(...$args)
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($data) => last(($text = $data->choices[0]->delta->content ?? void) === void || [print($text), cli || [ob_flush(), flush()]], $text)
}
$answer = void
$buffer = void
$curl = curl_init('https://api.deepseek.com/v1/chat/completions')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $data = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.%creds->DeepSeek, 'Content-Type: application/json', 'Content-Length: '.strlen($data)])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$buffer, &$answer){
	$chunks = trim($buffer.$data)
	$buffer = void
	foreach (explode(lf.lf, $chunks) AS $chunk){
		if (!str_starts_with($chunk, 'data: ')){
			$buffer = $chunk
			continue
		}
		if ($obj = json_decode(substr($chunk, 6))){
			$res = $cb($obj)
			if (is_string($res)) $answer .= $res
		}
		else $buffer = $chunk
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

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

line 62
Verstuurt een HTTP-verzoek naar een DeepSeek API-endpoint met een opgegeven URI en authenticatie. Vervolgens decodeert het de JSON-respons en controleert op fouten; indien aanwezig, wordt een error gegenereerd. Daarna wordt de response teruggegeven.
$res = json_decode(HTTP("https://api.deepseek.com/v1/$uri", ['Authorization: Bearer '.%creds->DeepSeek], $JSON, ...$args))
if (isset($res->error)) error('DeepSeek Request error: '.$res->error->message)
return $res
object

%Gemini

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

%Gemini -> config ($modalities)

line 7
Stelt een configuratie samen met parameters voor taaloutput, zoals temperatuur, topK, topP, maximale tokens, en response-modi en -mime-type.
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
Verstuurt een POST-verzoek naar de Google API voor generatief beeldmateriaal met het opgegeven prompt en base64-afbeelding, ontvangt de JSON-respons en geeft die als resultaat terug.
$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
Verstuurt een POST-verzoek naar de API van Google generative language voor het genereren van content op basis van een prompt, en verwerkt de JSON-respons om deze vervolgens te retourneren.
$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
Verstuurt een POST-verzoek naar de Google API voor generatieve beeldcontent, met base64-afbeelding en prompt als input, en decodeert de JSON-respons voordat deze wordt geretourneerd.
$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
Controleert of het antwoord eindigt op een 'IMAGE_SAFETY'-beoordeling en geeft een waarschuwing indien dat het geval is. Indien niet, retourneert het eerst gevonden tekstuele inhoud of inline data. Als er geen geldige informatie is, geeft het een foutmelding terug.
if ($res->candidates[0]->finishReason === 'IMAGE_SAFETY') return 'I\'m affraid I can\'t process your image or command, it seems unsafe.'
if ($text = $res->candidates[0]->content->parts[0]->text ?? null) return $text
if ($data = $res->candidates[0]->content->parts[0]->inlineData ?? null) return $data
return 'Some error occured processing your request, please try again later.'
object

%OpenAI

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

OpenAI :: model

line 7
Dit is een specificatie voor een modelinstelling die aangeeft dat het gebruik maakt van het GPT-4 mini-model.
'gpt-4o-mini'
const

OpenAI :: voices

line 8
Dit is een lijst met string-elementen die verschillende stem- of voice-namen bevat, mogelijk voor gebruik in een tekst-naar-spraakfunctie of stemkeuzemodule.
['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']
static

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

line 10
Voegt berichten toe aan de berichtenlijst op basis van de inputparameters (system, assistant, user), zorgt ervoor dat de berichten correct worden geordend en verwijdert de oorspronkelijke parameters na het toevoegen.
$args['messages'] ??= []
if (isset($args['system']) && array_unshift($args['messages'], arr(role: 'system', content: $args['system']))) unset($args['system'])
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
static

OpenAI :: tool ($tool):array

line 18
Maakt een beschrijvende objectstructuur met naam, beschrijving en parameters, waarbij parameters worden gegenereerd uit een lijst met argumenten en alleen specifieke eigenschappen bevatten, en alle aanvullende eigenschappen worden uitgesloten.
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
Verwerkt een chat-aanvraag door model en context te bepalen, verstuurt een verzoek naar de API, en retourneert een object met details over het model, de reden van afronding, tokens en de inhoud of tool-aanroepen, afhankelijk van de 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 $return
method

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

line 43
Voert een POST-verzoek uit naar de 'embeddings'-endpoint met de invoer en het model, en retourneert de eerste embedding uit de respons.
$this->request('embeddings', POST: arr(input: $input, model: $model))->data[0]->embedding
method

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

line 45
Stelt een streaming-verbinding op met de OpenAI API, verstuurt een chat-aanvraag, en verwerkt de ontvangen data door deze in realtime via een callback door te geven, waarbij tekstuele fragmenten worden opgebouwd en geretourneerd.
%app->streaming = true
$args['model'] ??= static::model
$args = static::context(...$args)
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($text) => last(print($text), cli || [@ob_flush(), flush()], $text)
}
$answer = void
$buffer = void
$curl = curl_init('https://api.openai.com/v1/chat/completions')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $payload = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.%creds->OpenAI, 'Content-Type: application/json', 'Content-Length: '.strlen($payload)])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$buffer, &$answer){
	$chunks = trim($buffer.$data)
	$buffer = void
	foreach (explode(lf.lf, $chunks) AS $chunk){
		if ($obj = json_decode(substr($chunk, 6))) in_array($text = $obj->choices[0]->delta->content ?? null, [null, void]) || $answer .= $cb($text, $obj)
		else $buffer = $chunk
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

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

line 77
Verwerkt een geluidsbestand door het te converteren naar CURLFile indien nodig, verstuurt het naar de API voor transcriptie met het gekozen model, en retourneert een object met modelnaam, duur, taal en transcriptie tekst.
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
Stelt een bericht samen met tekst en optioneel een afbeelding, en kiest op basis van de stream-parameter voor een doorlopende stream of een chat-antwoord.
$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
Verstuurt een HTTP-verzoek naar de OpenAI API met gegeven URI en opties. Decodeert de JSON-response, controleert op fouten, en geeft het resultaat terug.
$res = json_decode(HTTP("https://api.openai.com/v1/$uri", ['Authorization: Bearer '.%creds->OpenAI], $JSON, ...$args))
if (isset($res->error)) error('OpenAI Request error:'.lf.$res->error->message)
return $res

CSS

object

%basics

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

style

line 5
Statisch toegewezen CSS-klasse-selector die verschillende stijlregels voor tekstuitlijning, floats, positionering, marges, paddings, overflow, display, cursor, tekstdecoratie en breedte bevat, bedoeld voor hergebruik en consistente 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: 1200px
object

%fixes

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

style

line 5
Past de box-sizing toe aan alle elementen en pseudo-elementen, zodat ze inclusief padding en border zijn. Verwijdert de standaard touch-acties voor elements zoals links en buttons. Verhelpt focus-styling door outline te verwijderen voor invoerelementen. Verwijdert de spin-buttons van numerieke invoeren in WebKit- en Gecko-browsers. Zorgt dat tabellen geen dubbele randen krijgen door border-collapse in te schakelen. Verwijdert de standaard uitvouwpijl van select-elementen 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: none
object

%flex

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

style

line 5
Stelt flexibele lay-outregels in met verschillende flex-eigenschappen, zoals display, flex-wrap, gap, en flex-direction, inclusief responsieve aanpassingen bij grotere schermen.
.flex: display: flex
.flex.auto > *: flex: auto
.flex.gap: gap: 1rem
.flex.row: flex-direction: column
.flex.wrap: flex-wrap: wrap
.grow: flex-grow: 1

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

%grid

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

style

line 5
Deze code definieert een grid-lay-out die automatisch het aantal kolommen aanpast op basis van de beschikbare breedte, met minimaal 300px per kolom.
.grid {
	display: grid
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))
}

DB

object

%DB

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

%DB -> PDO

line 6
Geeft een foutmelding als er geen PDO-verbinding is gedefinieerd.
error('No PDO connector defined')
prop

%DB -> fieldQuotes

line 7
Geeft een boolean waarde terug die aangeeft of er quotes (aanhalingstekens) rondom de gegevens staan, mogelijk om te controleren of het veld al gequoteerd is.
bt
method

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

line 9
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
Voert een databasequery uit met ondersteuning voor prepared statements bij invoerparameters, en bevat debugfunctionaliteit die querytype, tablename en optioneel de WHERE-clausule toont samen met het aantal geretourneerde rijen. Bij een fout wordt een gedetailleerde foutmelding inclusief trace gegenereerd.
try {
	if (!$args) $stmt = $this->PDO->query($query)
	else {
		$stmt = $this->PDO->prepare($query)
		$stmt->execute($args)
	}
	if (debug){
		$match = regex('/\b(UPDATE|INSERT INTO|DELETE FROM|FROM)\b\s+([`"\[]?\w+[`"\]]?)/i', strtr($query, [$this->fieldQuotes => void]))
		$where = strtr(regex('/\bWHERE (\b.+)/is', $query)[1] ?? void, [' ORDER BY' => void])
		$match && debug("Q: $match[1] $match[2]".strtr(rtrim(" $where "), [dq => void])." (".$stmt->rowCount().")")
	}
}
catch (PDOException $e){
	error('Database error'.(debug ? colon.lf.$query.lf.lf.$e->getMessage().lf.lf.loop(array_slice(explode(lf, $e->getTraceAsString()), 2, -2), fn($line) => last($match = regex('/([^\/]+\.php)\(([0-9]+)\): (.*)/', $line), "$match[3] $match[1]:$match[2]"), lf) : void))
}
return $stmt
method

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

line 39
Laadt de gegevens met de opgegeven argumenten en retourneert een lijst van één kolom uit alle rijen.
$this->load(...$args)->fetchAll(PDO::FETCH_COLUMN)
method

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

line 40
Haalt een geladen database-gegevens op en retourneert de eerste kolom van de eerste rij, tenzij deze niet bestaat, dan null.
$this->load(...$args)->fetch(PDO::FETCH_COLUMN) ?: null
method

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

line 41
Haalt gegevens op en geeft ze terug als een key-value paar array.
$this->load(...$args)->fetchAll(PDO::FETCH_KEY_PAIR)
method

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

line 42
Deze code laadt gegevens met de meegegeven argumenten, haalt alle resultaten op en groepeert ze op basis van een kolom of veld, waarbij elke groep wordt omgezet in objecten van de klasse 'obj'.
$this->load(...$args)->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_CLASS, 'obj')
method

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

line 43
Laadt gegevens met de opgegeven argumenten en haalt alle resultaten op, waarbij elke rij wordt omgezet in een object van het opgegeven klasse, met unieke resultaten op basis van de eerste kolom.
$this->load(...$args)->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, 'obj')
method

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

line 44
Levert een object op basis van geladen gegevens, of null als er geen resultaat is.
$this->load(...$args)->fetchObject('obj') ?: null
method

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

line 46
Voert een insert-query uit waarbij indien de 'ignore'-waarde in de data waar is, de 'IGNORE' optie wordt toegevoegd. Bouwt kolomnamen en bijbehorende waarden correct op, vult de waarden aan met placeholders en vervangt deze door de daadwerkelijke data. Voert de query uit en geeft de laatst ingevoerde ID of een optionele ID terug.
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
Deze functie bouwt dynamisch een SQL UPDATE statement op door de gegevens uit `$data` te gebruiken. Het telt het aantal placeholders in `$where` om te bepalen welke data voor de WHERE-voorwaarde en welke voor de SET-clausule zijn. Vervolgens worden de kolomnamen en waarden gescheiden en geordend, en wordt de query uitgevoerd met de juiste parameters, waarna het aantal gewijzigde rijen wordt geretourneerd.
$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
Voert een DELETE-query uit op een opgegeven tabel met een specifieke WHERE-voorwaarde en retourneert het aantal verwijderde rijen.
$this->query("DELETE FROM $table WHERE $where", ...$args)->rowCount()
object

%MySQL

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

%MySQL -> PDO

line 8
Creëert een nieuwe PDO-verbinding met de MySQL-database, waarbij de verbindingsgegevens worden gehaald uit een 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
version 1.0
creator q-ai.nl
description PostgreSQL library
extends DB
requires @DB creds:postgresql
prop

%PostgreSQL -> PDO

line 8
Maakt een nieuwe PDO-verbinding aan met een PostgreSQL-database, met gebruik van geconfigureerde inloggegevens.
new PDO('pgsql:host='.%creds->postgresql->host.';dbname='.%creds->postgresql->database, %creds->postgresql->user, %creds->postgresql->password)
prop

%PostgreSQL -> fieldQuotes

line 9
Deze code definieert een functie die waarschijnlijk een tekstuele vervanging of bewerking toepast op de input, mogelijk met betrekking tot veld- of tabelnaamquotes voor PostgreSQL, om SQL-injecties en syntaxisfouten te voorkomen.
dq
object

%Qdrant

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

%Qdrant -> get (string $input)

line 5
Slaat een embedding op in APCu-cache, met een key gebaseerd op de input, en haalt deze op of genereert en cachet deze voor 28 dagen.
apcu('embedding/'.token(input: $input), fn($input) => %OpenAI->embedding($input), 86400 * 28)
method

%Qdrant -> collections

line 7
Haalt de namen op uit de collecties en geeft deze terug als een array.
array_column($this->request('collections')->result->collections, 'name')
method

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

line 8
Maakt een nieuwe collectie aan met opgegeven naam, vector grootte en afstandsmeting, en controleert of de aanmaak succesvol was.
$this->request("collections/$collection", PUT: arr(vectors: arr(size: $size, distance: $distance)))->status === 'ok'
method

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

line 9
Voert een HTTP PUT-verzoek uit naar de endpoint voor het updaten of toevoegen van een punt in een collectie. Het stuurt een array met puntgegevens, waaronder ID, vector en optioneel(payload). Het resultaat is de operatie-ID van de uitgevoerde operatie.
$this->request("collections/$collection/points", PUT: arr(points: [arr(id: $id, vector: $this->get($input), payload: $payload ?: null)]))->result->operation_id
method

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

line 10
Verstuurt een POST-verzoek naar de API om specifieke punten te verwijderen uit een collectie door hun ID's te specificeren, met de endpoint 'collections/{collection}/points/delete'.
$this->request("collections/$collection/points/delete", POST: arr(points: $ids))->result
method

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

line 11
Voert een zoekactie uit in een verzameling door punten te versturen naar de API, met standaard- of meegegeven input. Resultaten worden gefilterd en teruggegeven met specifieke ID's en payload-gegevens, waarbij ontbrekende input wordt ingevuld met nullen in een vector van 1536 elementen.
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
Verwijdert een collectie met de opgegeven naam door een DELETE-verzoek naar de server te sturen en het resultaat te retourneren.
$this->request("collections/$collection", DELETE: true)->result
method

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

line 14
Verstuurt een HTTP-verzoek naar een Qdrant-server met de opgegeven URI en data, inclusief indien nodig een API-sleutel, en decodeert vervolgens de JSON-respons naar een 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
version 1.0
creator q-ai.nl
description SQLite library
extends DB
requires @DB
method

%SQLite -> controller

line 8
Maakt een verbinding met een SQLite-database met de bestandsnaam die in `$file` staat.
andle => "SQLite/$file"
method

%SQLite -> __construct (private string $file)

line 9
Dit is de constructor van de class, die een padas van het type string als parameter ontvangt en waarschijnlijk gebruikt om het pad of de naam van de SQLite-database op te slaan of te initialiseren.
prop

%SQLite -> PDO

line 11
Maakt een nieuwe PDO-instantie aan met een SQLite-database, waarbij de databasebestandsnaam wordt gebruikt vanuit een eigenschap van het object.
new PDO('sqlite:'.$this->file)

DOM

object

%CSS_var

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

script

line 5
Deze code definieert een property die via een proxy toegang biedt tot CSS-variabelen op het document. Bij lezen wordt de waarde van de CSS-variabele opgehaald, en bij schrijven wordt deze aangepast.
Object.defineProperty(app, 'var', {get(){return new Proxy({}, {get(_, key){return getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim()}, set(_, key, value){ return document.documentElement.style.setProperty(`--${key}`, value)}})}, configurable: true})
object

%datatags

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

script

line 5
Deze code reageert op klikgebeurtenissen op elementen met specifieke data-attributes en bepaalt op basis daarvan de HTTP-methode en URI. Vervolgens wordt de juiste API-aanroep gedaan via een app-functie, met optioneel meegegeven data.
on('click', '[data-get], [data-post], [data-put], [data-patch], [data-delete]', (el, e) => {
	if (el.dataset.confirm) return
	e.preventDefault()
	let method, uri, data = null
	if ((uri = el.dataset.get) !== undefined) method = 'get'
	else if ((uri = el.dataset.delete) !== undefined) method = 'delete'
	else [data = {}, Object.keys(el.dataset).forEach((key) => key === 'post' || key === 'put' || key === 'patch' ? [method = key, uri = el.dataset[key]] : data[key] = el.dataset[key])]
	app[method](uri, data)
})
object

%dialog

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

script

line 5
Deze code definieert een set van dialoogfuncties (alert, confirm, prompt) die gebruik maken van een dynamisch aangemaakte HTML-dialog. De functie 'phlo.dialog' maakt een dialoogvenster op basis van het type en toont deze, wacht op de gebruiker, en geeft vervolgens de juiste waarde terug. Daarnaast wordt een click-eventlistener aangemaakt die, bij het aantreffen van een 'data-confirm'-attribuut, een bevestigingsdialoog toont en op basis van de reactie verdere acties onderneemt.
app.alert = (msg) => phlo.dialog('alert', msg)
app.confirm = (msg) => phlo.dialog('confirm', msg)
app.prompt = (msg, def) => phlo.dialog('prompt', msg, def)

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

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

%exists

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

script

line 5
Deze code definieert een functionaliteit om te detecteren of elementen bestaan: het houdt een lijst bij van elementen en hun bijbehorende callbacks, en voert deze callbacks uit wanneer de elementen nog niet eerder zijn geregistreerd als bestaand, waarbij het gebruikmaakt van een WeakMap om reeds geregistreerde elementen bij te houden.
phlo.exist = []
phlo.existing = new WeakMap

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

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

%form

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

script

line 5
Deze code bevat een functie die formuliergegevens verzendt via een AJAX-aanroep op basis van de methodenaam, en een event listener die bij invoer of verandering de formulierwaarden synchroniseert met attributen en de interne staat opslaat. Daarnaast verzorgt het een aangepaste submit-handler voor formulieren met de klasse 'async' om het standaard gedrag te voorkomen en de gegevens via de functie te verzenden.
const submitForm = (form) => {
	const method = (form.attributes.method?.value ?? 'GET').toLowerCase()
	app[method](new URL(form.action).pathname.substr(1), new FormData(form))
}
on('input change', 'input, select, textarea', input => {
	if (input.tagName === 'SELECT') input.querySelectorAll('option').forEach((option, index) => option.selected ? option.setAttribute('selected', '') : option.removeAttribute('selected'))
	if (input.type === 'checkbox') input.checked ? input.setAttribute('checked', '') : input.removeAttribute('checked')
	if (input.type === 'text' && input.value !== input.getAttribute('value')) input.setAttribute('value', input.value)
	if (input.type === 'textarea' && input.value !== input.innerHTML) input.innerHTML = input.value
	phlo.state.save()
	return false
})
on('submit', 'form.async', (form, e) => {
	e.preventDefault()
	submitForm(form)
})
object

%image_resizer

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

script

line 5
Laadt een afbeelding, past de afmetingen aan op basis van maximale breedte en hoogte met behoud van aspect ratio, en genereert een geredimensioneerde datalink via een 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
/phlo/libs/DOM/link.phlo
version 1.0
creator q-ai.nl
description Single Page App link.async handler
view

script

line 5
Verkeert in de situatie dat bij het klikken op een link met de klasse 'async', afhankelijk van toetsaanslagen of bevestigingsstatus, de standaard navigatie wordt geblokkeerd en een asynchrone fetchactie wordt uitgevoerd.
on('click', 'a.async', (a, e) => e.ctrlKey || e.shiftKey || e.metaKey || a.dataset.confirm || [e.preventDefault(), app.get(a.attributes.href.value.substr(1))])
object

%markdown

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

script

line 5
Parsetransformatie voor Markdown-tekst naar HTML met ondersteuning voor GFM-functionaliteit. Markeringen worden herkend en geconverteerd, zoals koppen, lijsten, tabellen, codeblokken, blokquotes en inline-elementen. Bij hyperlinks, afbeeldingen en tekstdecoraties wordt sanitatie toegepast en HTML wordt veilig ingebed. Ook worden referenties en ID-genereerlogic gebruikt voor koppen en tabellen.
function parse_markdown(md, opts = {}){
  const o = {
    gfm: opts.gfm !== false,
    breaks: !!opts.breaks,
    headerIds: opts.headerIds !== false,
    headerPrefix: opts.headerPrefix || '',
    smartypants: !!opts.smartypants
  }
  const unnull = x => (x == null ? '' : String(x))
  let src = unnull(md).replace(/\r\n?/g, "\n")
  const escHtml = s => s.replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))
  const trimEndNL = s => s.replace(/\s+$/,'')
  const isBlank = s => /^\s*$/.test(s)
  const slugmap = new Map()
  const slug = (t) => {
    let s = t.toLowerCase().replace(/<\/?[^>]+>/g, '').replace(/[^\p{L}\p{N}\- _]+/gu, '').trim().replace(/[\s_]+/g, '-')
    const base = o.headerPrefix + s
    let k = base, i = 1
    while (slugmap.has(k)) k = `${base}-${++i}`
    slugmap.set(k, true)
    return k
  }
  const smart = s => {
    if (!o.smartypants) return s
    return s.replace(/---/g, "—").replace(/--/g, "–").replace(/(^|[\s"(\[])(?=')/g, "$1‘").replace(/'/g, "’").replace(/(^|[\s(\[])(?=")/g, "$1“").replace(/"/g, "”").replace(/\.{3}/g, "…")
  }
  const refs = Object.create(null)
  src = src.replace(
    /^ {0,3}\[([^\]]+)\]:\s*<?([^\s>]+)>?(?:\s+(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*$/gm,
    (_, label, url, t1, t2, t3) => {
      const key = label.trim().replace(/\s+/g, ' ').toLowerCase()
      if (!refs[key]) refs[key] = { href: url, title: t1 || t2 || t3 || '' }
      return ''
    }
  )
  const tokens = []
  const lines = src.split("\n")
  function takeWhile(start, pred){
    let end = start
    while (end < lines.length && pred(lines[end], end)) end++
    return { start, end }
  }
  function pushParagraph(buf){
    const text = buf.join("\n").trimEnd()
    if (text) tokens.push({ type: "paragraph", text })
    buf.length = 0
  }
  function parseBlock(start = 0, end = lines.length){
    const para = []
    let l = start
    while (l < end){
      const line = lines[l]
      if (isBlank(line)){ pushParagraph(para); l++; continue; }
      let m = line.match(/^ {0,3}(`{3,}|~{3,})([^\n]*)$/)
      if (m){
        pushParagraph(para)
        const fenceLen = m[1].length
        const info = (m[2] || '').trim()
        let body = []
        l++
        while (l < end){
          const s = lines[l]
          const close = s.match(new RegExp(`^ {0,3}${m[1][0]}{${fenceLen},}\\s*$`))
          if (close){ l++; break; }
          body.push(s)
          l++
        }
        tokens.push({ type: "code", lang: info.split(/\s+/)[0] || '', text: trimEndNL(body.join("\n")) })
        continue
      }
      if (/^(?: {4}|\t)/.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, s => /^(?: {4}|\t)/.test(s) || isBlank(s))
        const block = lines.slice(l, j).map(s => s.replace(/^(?: {4}|\t)/, '')).join("\n")
        tokens.push({ type: "code", lang: '', text: trimEndNL(block) })
        l = j; continue
      }
      if (/^ {0,3}<(?:!--|\/?(?:html|head|body|pre|script|style|table|thead|tbody|tfoot|tr|td|th|div|p|h[1-6]|blockquote|ul|ol|li|section|article|aside|details|summary|figure|figcaption)\b)/i.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, (s, idx) => !(idx > l && isBlank(lines[idx-1]) && isBlank(s)))
        const html = lines.slice(l, j).join("\n")
        tokens.push({ type: "html", text: html })
        l = j; continue
      }
      if (/^ {0,3}(?:-+\s*|-{3,}|_{3,}|\*{3,})\s*$/.test(line)){
        pushParagraph(para)
        tokens.push({ type: "hr" })
        l++; continue
      }
      m = line.match(/^ {0,3}(#{1,6})[ \t]*([^#\n]*?)[ \t#]*$/)
      if (m){
        pushParagraph(para)
        tokens.push({ type: "heading", depth: m[1].length, text: m[2].trim() })
        l++; continue
      }
      if (l + 1 < end && /^[^\s].*$/.test(line) && /^ {0,3}(=+|-+)\s*$/.test(lines[l + 1])){
        pushParagraph(para)
        const depth = lines[l + 1].trim().startsWith("=") ? 1 : 2
        tokens.push({ type: "heading", depth, text: line.trim() })
        l += 2; continue
      }
      if (/^ {0,3}>\s?/.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, s => /^ {0,3}>\s?/.test(s) || isBlank(s))
        const inner = lines.slice(l, j).map(s => s.replace(/^ {0,3}>\s?/, '')).join("\n")
        const sub = parse_markdown(inner, { ...o })
        tokens.push({ type: "blockquote", html: sub })
        l = j; continue
      }
      m = line.match(/^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/)
      if (m){
        pushParagraph(para)
        const bulletRe = /^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/
        const { end: j } = takeWhile(l, (s, idx) =>
          bulletRe.test(s) ||
          (/^(?: {4}|\t)/.test(s)) ||
          (!isBlank(s) && idx > l && !/^(?: {0,3}(?:[*+-]|\d{1,9}[.)])\s+)/.test(s))
        )
        const block = lines.slice(l, j)
        const ordered = /^\d/.test(m[1])
        const items = []
        let cur = []
        for (let k = 0; k < block.length; k++){
          const ln = block[k]
          const head = ln.match(bulletRe)
          if (head){
            if (cur.length) items.push(cur), cur = []
            cur.push(ln.replace(bulletRe, ''))
          } else {
            cur.push(ln.replace(/^(?: {4}|\t)/, ''))
          }
        }
        if (cur.length) items.push(cur)
        const parsedItems = items.map(linesArr => {
          let raw = linesArr.join("\n").replace(/\n\s+$/,'')
          let checked = null
          if (o.gfm){
            const t = raw.match(/^\[([ xX])\][ \t]+/)
            if (t){ checked = t[1].toLowerCase() === 'x'; raw = raw.replace(/^\[[ xX]\][ \t]+/, ''); }
          }
          const html = parse_markdown(raw, o)
          return { html, checked }
        })
        tokens.push({ type: "list", ordered, items: parsedItems })
        l = j; continue
      }
      if (o.gfm){
        const hdr = line
        const alignLn = lines[l + 1] || ''
        if (/\|/.test(hdr) && /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/.test(alignLn)){
          pushParagraph(para)
          const aligns = alignLn
            .trim().replace(/^(\|)|(\|)$/g,'')
            .split("|").map(s => s.trim()).map(s => s.startsWith(":-") && s.endsWith("-:") ? "center" : s.endsWith("-:") ? "right" : s.startsWith(":-") ? "left" : null)
          const headerCells = hdr.trim().replace(/^(\|)|(\|)$/g,'').split("|").map(s => s.trim())
          l += 2
          const rows = []
          while (l < end && /\|/.test(lines[l]) && !isBlank(lines[l])){
            rows.push(lines[l].trim().replace(/^(\|)|(\|)$/g,'').split("|").map(s => s.trim()))
            l++
          }
          tokens.push({ type: "table", header: headerCells, aligns, rows })
          continue
        }
      }
      para.push(line)
      const next = lines[l + 1] || ''
      const endPara =
        isBlank(next) ||
        /^ {0,3}(?:`{3,}|~{3,})/.test(next) ||
        /^(?: {4}|\t)/.test(next) ||
        /^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/.test(next) ||
        /^ {0,3}(#{1,6})/.test(next) ||
        /^ {0,3}>\s?/.test(next) ||
        /^ {0,3}(?:-+\s*|-{3,}|_{3,}|\*{3,})\s*$/.test(next) ||
        (o.gfm && /\|/.test(next) && /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/.test(lines[l + 2] || ''))
      if (endPara) pushParagraph(para)
      l++
    }
    pushParagraph(para)
  }
  parseBlock(0, lines.length)
  function renderInline(s){
    if (!s) return ''
    s = s.replace(/(`+)([^`]|[^`][\s\S]*?[^`])\1/g, (_, ticks, code) => `<code>${escHtml(code)}</code>`)
    s = s.replace(/!\[([^\]]*)\]\(\s*<?([^\s)<>]+)>?\s*(?:(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*\)/g,
      (_, alt, url, t1, t2, t3) => `<img src="${escHtml(url)}" alt="${escHtml(alt)}"${t1||t2||t3?` title="${escHtml(t1||t2||t3)}"`:''}>`)
    s = s.replace(/!\[([^\]]*)\]\[([^\]]*)\]/g, (_, alt, id) => {
      const ref = refs[(id || alt).trim().replace(/\s+/g,' ').toLowerCase()]
      return ref ? `<img src="${escHtml(ref.href)}" alt="${escHtml(alt)}"${ref.title?` title="${escHtml(ref.title)}"`:''}>` : _
    })
    s = s.replace(/\[([^\]]+)\]\(\s*<?([^\s)<>]+)>?\s*(?:(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*\)/g,
      (_, text, url, t1, t2, t3) => `<a href="${escHtml(url)}"${t1||t2||t3?` title="${escHtml(t1||t2||t3)}"`:''}>${text}</a>`)
    s = s.replace(/\[([^\]]+)\]\s*\[([^\]]*)\]/g, (_, text, id) => {
      const key = (id || text).trim().replace(/\s+/g,' ').toLowerCase()
      const ref = refs[key]
      return ref ? `<a href="${escHtml(ref.href)}"${ref.title?` title="${escHtml(ref.title)}"`:''}>${text}</a>` : _
    })
    s = s.replace(/<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^ <>"']+)>/g, (_, url) => `<a href="${escHtml(url)}">${escHtml(url)}</a>`)
    s = s.replace(/<([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/g, (_, mail) => `<a href="mailto:${escHtml(mail)}">${escHtml(mail)}</a>`)
    if (o.gfm){
      s = s.replace(/(?:(?<=\s)|^)(https?:\/\/[^\s<]+)(?=\s|$)/g, '<a href="$1">$1</a>')
      s = s.replace(/(?:(?<=\s)|^)(www\.[^\s<]+)(?=\s|$)/g, '<a href="http://$1">$1</a>')
    }
    s = s.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>').replace(/__([\s\S]+?)__/g, '<strong>$1</strong>')
    s = s.replace(/\*([^*\n]+?)\*/g, '<em>$1</em>').replace(/_([^_\n]+?)_/g, '<em>$1</em>')
    if (o.gfm) s = s.replace(/~~([\s\S]+?)~~/g, '<del>$1</del>')
    s = s.replace(/ {2,}\n/g, "<br>\n")
    if (o.breaks) s = s.replace(/\n/g, "<br>\n")
    s = s.replace(/&(?!#?\w+;)/g, "&amp;").replace(/<(?!\/?[A-Za-z][^>]*>)/g, "&lt;")
    return smart(s)
  }
  let out = ''
  for (const t of tokens){
    switch (t.type){
      case "paragraph":
        out += `<p>${renderInline(t.text)}</p>\n`
        break
      case "heading": {
        const text = renderInline(t.text)
        const id = o.headerIds ? slug(text.replace(/<[^>]+>/g, '')) : null
        out += id ? `<h${t.depth} id="${id}">${text}</h${t.depth}>\n` : `<h${t.depth}>${text}</h${t.depth}>\n`
        break
      }
      case "code": {
        const cls = t.lang ? ` class="language-${escHtml(t.lang)}"` : ''
        out += `<pre><code${cls}>${escHtml(t.text)}</code></pre>\n`
        break
      }
      case "blockquote":
        out += `<blockquote>\n${t.html.trim()}\n</blockquote>\n`
        break
      case "list": {
        const tag = t.ordered ? "ol" : "ul"
        out += `<${tag}>\n`
        for (const it of t.items){
          const task = it.checked === null ? '' : `<input ${it.checked ? 'checked="" ' : ''}disabled="" type="checkbox"> `
          const body = it.html.trim().replace(/^<p>/, task + "<p>")
          out += `<li>${body}</li>\n`
        }
        out += `</${tag}>\n`
        break
      }
      case "table": {
        const ths = t.header.map((h, i) => {
          const a = t.aligns[i]
          return a ? `<th align="${a}">${renderInline(h)}</th>` : `<th>${renderInline(h)}</th>`
        }).join("\n")
        let body = ''
        for (const row of t.rows){
          const tds = row.map((cell, i) => {
            const a = t.aligns[i]
            return a ? `<td align="${a}">${renderInline(cell)}</td>` : `<td>${renderInline(cell)}</td>`
          }).join("\n")
          body += `<tr>\n${tds}\n</tr>\n`
        }
        out += `<table>\n<thead>\n<tr>\n${ths}\n</tr>\n</thead>\n` + (body ? `<tbody>\n${body}</tbody>\n` : '') + `</table>\n`
        break
      }
      case "hr":
        out += "<hr>\n"
        break
      case "html":
        out += t.text + "\n"
        break
    }
  }
  return out.trim()
}
object

%shorthands

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

script

line 5
Deze functies zijn korte hulpfuncties die eventlisteners toevoegen voor respectievelijk 'change', 'click' en 'input' gebeurtenissen, met minder argumenten nodig.
function onChange(els, cb){ on('change', els, cb) }
function onClick(els, cb){ on('click', els, cb) }
function onInput(els, cb){ on('input', els, cb) }
object

%store

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

script

line 5
Deze code implementeert een reactief gegevensopslag- en koppelsysteem met observatie en automatische updates. Het bevat methoden voor het ophalen en instellen van gegevens via paden, het beheren van signalen, het afhandelen van afhankelijkheden tussen berekeningen, en het bijwerken van DOM-elementen op basis van de store. Daarnaast maakt het gebruik van proxies voor dynamische toegang en wijziging van gegevens, en zorgt het voor synchronisatie tussen DOM en store via data-attributes en eventhandlers. De structuur ondersteunt het automatisch evalueren van berekeningen en het efficient herberekenen bij wijzigingen.
phlo.store = {
	signals: {},
	listeners: {},
	calcs: {},
	calcDeps: {},
	calcVals: {},
	calcTick: false,
	split: (path) => path.replace(/\]/g, '').split(/\.|\[/),
	get(path){
		if (!path) return undefined
		let ctx = phlo.store.signals
		const keys = phlo.store.split(path)
		for (let i = 0; i < keys.length; i++){
			if (ctx == null) return undefined
			ctx = ctx[keys[i]]
		}
		return ctx
	},
	setPath(path, value){
		let keys = phlo.store.split(path)
		let ctx = phlo.store.signals
		while (keys.length > 1){
			const k = keys.shift()
			ctx[k] ??= isNaN(keys[0]) ? {} : []
			ctx = ctx[k]
		}
		const k = keys[0]
		const old = ctx[k]
		if (old === value) return false
		ctx[k] = value
		return true
	},
	set(path, value){
		if (!phlo.store.setPath(path, value)) return
		phlo.store.notify(path, phlo.store.get(path))
		phlo.store.recalc(path)
		phlo.store.schedule()
	},
	on(path, cb){ (phlo.store.listeners[path] ??= new Set).add(cb) },
	off(path, cb){ phlo.store.listeners[path] && phlo.store.listeners[path].delete(cb) },
	reset(){
		phlo.store.signals = {}
		phlo.store.listeners = {}
		phlo.store.calcs = {}
		phlo.store.calcDeps = {}
		phlo.store.calcVals = {}
		phlo.store.calcTick = false
	},
	signal(path, initial){
		if (phlo.store.get(path) === undefined) phlo.store.set(path, initial)
		return { subscribe: (cb) => phlo.store.on(path, cb), unsubscribe: (cb) => phlo.store.off(path, cb) }
	},
	notify(path, val){
		const set = phlo.store.listeners[path]
		if (set) set.forEach(cb => cb(val))
	},
	match(dep, changed){
		if (!dep) return false
		if (dep === changed) return true
		return changed.startsWith(dep + '.') || changed.startsWith(dep + '[') || dep.startsWith(changed + '.') || dep.startsWith(changed + '[')
	},
	depsReady(list){
		const arr = Array.isArray(list) ? list : (list ? [list] : [])
		return arr.every(d => phlo.store.get(d) !== undefined)
	},
	evalCalc(name){
		const fn = phlo.store.calcs[name]
		if (!fn) return
		let deps = []
		let val
		try {
			const out = fn()
			if (Array.isArray(out) && out.length === 2) deps = out[0], val = out[1]
			else val = out
		}
		catch(e){
			deps = []
			val = undefined
		}
		const list = Array.isArray(deps) ? deps : (deps ? [deps] : [])
		phlo.store.calcDeps[name] = list
		if (!phlo.store.depsReady(list)) return
		const old = phlo.store.calcVals[name]
		if (old !== val){
			phlo.store.calcVals[name] = val
			const p = `calc.${name}`
			phlo.store.setPath(p, val)
			phlo.store.notify(p, val)
		}
	},
	recalc(changed){
		const names = Object.keys(phlo.store.calcs)
		for (let i = 0; i < names.length; i++){
			const name = names[i]
			const deps = phlo.store.calcDeps[name] || []
			for (let j = 0; j < deps.length; j++){
				if (phlo.store.match(deps[j], changed)){
					phlo.store.evalCalc(name)
					break
				}
			}
		}
	},
	recalcAll(){
		const names = Object.keys(phlo.store.calcs)
		for (let i = 0; i < names.length; i++) phlo.store.evalCalc(names[i])
	},
	schedule(){
		if (phlo.store.calcTick) return
		phlo.store.calcTick = true
		setTimeout(() => {
			phlo.store.calcTick = false
			phlo.store.recalcAll()
		})
	},
	proxy(base){
		return new Proxy({}, {
			get(t, k){
				if (typeof k === 'symbol') return undefined
				const seg = /^\d+$/.test(k) ? `[${k}]` : (base ? `.${k}` : String(k))
				const path = base + seg
				const v = phlo.store.get(path)
				if (v !== undefined && (typeof v !== 'object' || v === null)) return v
				return phlo.store.proxy(path)
			},
			set(t, k, v){
				const seg = /^\d+$/.test(k) ? `[${k}]` : (base ? `.${k}` : String(k))
				phlo.store.set(base + seg, v)
				return true
			},
			has(t, k){ return phlo.store.get(base + (base ? '.' : '') + String(k)) !== undefined },
			ownKeys(){ return Object.keys(phlo.store.get(base) || {}) },
			getOwnPropertyDescriptor(){ return { enumerable: true, configurable: true } }
		})
	}
}

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

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

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

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

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

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

%template

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

script

line 6
Deze functie voert voor elke rij in 'rows' een templatefunctie uit, waarbij de waarden van de rij als argumenten worden doorgegeven.
app.mod.template = (template, rows) => rows.forEach(row => templates[template](...Object.values(row))),
const templates = {}
object

%timestamps

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

script

line 6
Deze code definieert een functie die de leeftijd van elementen met een bepaalde datalabel (dataset.ts) in seconden berekent en deze omzet naar een leesbare tijdsweergave, gebaseerd op een set tijdsintervallen. De functie vernieuwt dit elke seconde en bij het opstarten, waardoor live updates van tijdsago wordt getoond.
app.tsBase = {seconds: 60, minutes: 60, hours: 24, days: 7, weeks: 4, months: 13, years: 1}
const tsUpdate = () => (ranges = app.tsLabels && (tsValues = Object.values(app.tsBase)) ? Object.fromEntries(app.tsLabels.map((k, i) => [k, tsValues[i]])) : app.tsBase) && objects('[data-ts]').forEach(el => {
	let age = Math.round(Date.now() / 1000) - Number(el.dataset.ts), text = ''
	const future = age < 0
	if (future) age = -age
	for (const [range, multiplier] of Object.entries(ranges)){
		if (text) continue
		if (age / multiplier < 1.6583) text = `${Math.round(age)} ${range}`
		age /= multiplier
	}
	text ||= `${Math.round(age)} ${Object.keys(ranges).at(-1)}`
	text = `${future ? '-' : ''}${text}`
	el.innerText === text || (el.innerText = text)
})
setInterval(() => app.state === 'active' && tsUpdate(), 1000)
setTimeout(tsUpdate, 1)
object

%visible

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

script

line 5
Deze code definieert een observatiesysteem dat reageert op zichtbaarheid van elementen. Het registreert observaties, controleert periodiek via een update callback, en gebruikt een IntersectionObserver om te detecteren wanneer elementen zichtbaar worden. Bij observatie wordt de juiste callback (in- of uit zicht) uitgevoerd.
phlo.observe = []
phlo.observing = new WeakMap

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

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

Fields

object

%field_bool

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

%field_bool -> true

line 7
Deze node geeft altijd de waarde '✅' terug, ongeacht de invoer of context.
'✅'
prop

%field_bool -> false

line 8
Geeft een foutmelding of negatieve status weer.
'❌'
method

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

line 10
Toont een waarde gebaseerd op of het veld `$record->{$this->name}` waar (waarheid) is, door de 'true'-waarde te retourneren, anders de 'false'-waarde.
$record->{$this->name} ? $this->true : $this->false
method

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

line 11
Maakt een checkbox input met een label en een gestyleerde slider. De checkbox is gecontroleerd als de waarde van de record niet waar is.
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
Deze code converteert de waarde van een veld in `$payload` naar een boolean en wijst deze toe aan het corresponderende veld in `$record`.
$record->{$this->name} = (bool)%payload->{$this->name};
method

%field_bool -> sql

line 14
Geeft een SQL-uitspraken terug die een kolom definieert met de naam uit de property en een datatype van tinyint(1) unsigned, passend voor booleaanse waarden.
"`$this->name` tinyint(1) unsigned"
method

%field_bool -> nullable

line 15
Deze node retourneert altijd de waarde false, ongeacht de invoer of context.
false
object

%field_child

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

%field_child -> list

line 7
Het telt het aantal elementen in een lijst of collectie.
'count'
prop

%field_child -> change

line 8
Deze code geeft altijd onwaar terug, ongeacht de invoer of situatie.
false
prop

%field_child -> create

line 9
Deze code geeft altijd een negatieve uitkomst terug, waardoor een bepaalde actie of creatie niet wordt uitgevoerd.
false
prop

%field_child -> record

line 10
Dit is een property of methode die een lijst retourneert, waarschijnlijk bestaande uit child-velden of gerelateerde records.
'list'
method

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

line 12
Creëert een HTML-link die verwijst naar een specifiek record, met dynamische URL gebaseerd op record- en objectgegevens, en toont het aantal gerelateerde items naast een titel.
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
Woont de laatste waarde op van een specifiek veld binnen een record, door de laatste waarde op te halen dat overeenkomt met de naam van het veld.
$record->getLast($this->name)
method

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

line 14
Deze code genereert een samengevoegde lijst van links voor elk element in een collectie, gescheiden door geen separator, en toont een streepje als de lijst leeg of onwaarachtig is.
implode(loop($record->{$this->name}, fn($child) => $this->link($child, $CMS))) ?: dash
method

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

line 15
Geeft het label van het record terug.
$this->label($record)
method
line 16
Maakt een URL op basis van de recordgegevens en voegt een HTML-link toe met de gegenereerde href, inclusief een CSS-klasse voor asynchrone functionaliteit.
$parentPath = $CMS->uriRecord.'/'.$record->{$CMS->model}->id;
$relationshipName = $this->name;
$href = "/{$parentPath}/{$relationshipName}/{$record->id}";
return tag('a', href: $href, class: 'async', inner: $record);
object

%field_date

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

%field_date -> handle

line 7
Deze node geeft altijd `true` terug, ongeacht de invoer of context.
true
method

%field_date -> sql

line 9
Dit geeft aan dat de waarde een niet-negatieve datum is, met het type DATE in SQL en geen negatieve waarden toegestaan.
"`$this->name` DATE unsigned"
object

%field_datetime

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

%field_datetime -> handle

line 7
Deze node geeft altijd de waarde waar is (true).
true
prop

%field_datetime -> change

line 8
Controleert of de naam niet gelijk is aan 'created', 'changed' of 'updated'.
!in_array($this->name, ['created', 'changed', 'updated'])
prop

%field_datetime -> create

line 9
Controleert of de naam niet gelijk is aan 'created', 'changed' of 'updated'.
!in_array($this->name, ['created', 'changed', 'updated'])
method

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

line 11
Toont een klok-icoon en een menselijke tijdsaanduiding als de waarde bestaat; anders wordt een streepje weergegeven.
($value = $record->{$this->name}) ? tag('i', class: 'icon clock-'.$this->labelIconClass(time() - $value), inner: void).tag('span', data_ts: $record->{$this->name}, inner: time_human($record->{$this->name})) : dash
method

%field_datetime -> labelIconClass ($value)

line 12
Het codeblok bepaalt de kleur op basis van de waarde: rood als de waarde groter dan 86400 is, geel als hij tussen 3600 en 86400 ligt, en blauw voor lagere waarden.
$value > 86400 ? 'red' : ($value > 3600 ? 'yellow' : 'blue')
method

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

line 13
Maakt een datetime-invoerveld met type 'datetime-local', geeft een gestileerde invoer weer, waarbij de waarde wordt ingesteld op een geformatteerde datum- en tijdwaarde uit het 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
Deze code verwerkt datums voor een record. Als de veldnaam 'created' is en nog geen waarde heeft, wordt de huidige tijd ingesteld. Voor 'changed' of 'updated' wordt de huidige tijd toegewezen. Als er een payload-waarde bestaat, wordt deze omgezet naar een timestamp met strtotime en aan het record toegewezen.
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
De code definieert een databasekolom met de naam `$this->name`, van het type unsigned integer met een lengte van 10.
"`$this->name` int(10) unsigned"
object

%field_email

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

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

line 8
Deze code genereert een mailto-link met het e-mailadres uit de record, waardoor gebruikers direct een e-mail kunnen sturen door op de link te klikken.
<a href="mailto:{{ $record->{$this->name} }}">{{ $record->{$this->name} }}</a>
object

%field_many

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

%field_many -> list

line 7
Dit stuk code retourneert de tekst 'label'.
'label'
prop

%field_many -> record

line 8
Deze node geeft eenvoudigweg de waarde van 'label' door, zonder verdere verwerking of context.
'label'
prop

%field_many -> create

line 9
Deze code retourneert altijd true.
false
prop

%field_many -> change

line 10
Deze node geeft altijd de waarde `false` terug, ongeacht de invoer.
false
method

%field_many -> count ($record)

line 12
Deze code roept de methode `getCount` op het `$record` object, met als parameter de naam van het veld, en geeft daarmee waarschijnlijk het aantal item(s) of het aantal keer dat een bepaald veld voorkomt terug.
$record->getCount($this->name)
method

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

line 13
Loop door de elementen in de relatie, maak voor elk een link aan, en retourneer het resultaat; indien leeg, wordt een koppelteken weergegeven.
loop($record->{$this->name}, fn($relation) => $this->link($relation), lf) ?: dash
method
line 14
Maakt een hyperlink aan met een dynamische URL gebaseerd op recordgegevens, inclusief een class voor asynchrone actie en toont het record als de linktekst.
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $record)
method

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

line 15
Geeft een label weer gebaseerd op het record en CMS.
$this->label($record, $CMS)
method

%field_many -> sql

line 17
Het resultaat is een lege array, wat aangeeft dat deze methode of functie geen data teruggeeft of geen bewerkingen uitvoert.
[]
object

%field_number

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

%field_number -> decimals

line 7
Returneert het getal met nul decimalen, oftewel een gehele waarde.
prop

%field_number -> length

line 8
Deze code geeft simpelweg het getal 5 terug, wat waarschijnlijk de lengte van een veld of een similar waarde aangeeft.
5
prop

%field_number -> min

line 9
Deze code retourneert altijd 0.
method

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

line 11
Formatteert een getal uit het record met een bepaald aantal decimalen, gebruikt komma als decimaalscheiding en punt als duizendtalseparator.
number_format($record->{$this->name}, $this->decimals, comma, dot)
method

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

line 12
Maakt een numeriek invoerveld met dynamische waarde, standaardwaarde, stapgrootte op basis van decimalen, minimale waarde en een CSS-klasse.
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
Deze code genereert een SQL-velddefinitie dat een numeriek veld definieert, afhankelijk van de aanwezigheid van decimalen. Als er decimalen zijn, wordt een 'decimal' datatype gebruikt met een aangepaste precisie en schaal; anders wordt 'smallint' gebruikt. In beide gevallen wordt de unsigned-optie toegevoegd.
"`$this->name` ".($this->decimals ? 'decimal('.($this->length + $this->decimals).comma.$this->decimals.')' : "smallint($this->length)").' unsigned'
object

%field_parent

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

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

line 7
Controleert of een veld een modelobject is; indien ja, genereert het een link, anders wordt een streepje weergegeven.
is_a($obj = $record->{$this->name}, 'model') ? $this->link($obj) : dash
method

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

line 8
Deze code genereert een HTML select-element met opties gebaseerd op een lijst van opties. Voor elke optie wordt gecontroleerd of deze overeenkomt met een waarde uit het record, en indien ja, wordt de 'selected'-attribuut toegevoegd.
select(name: $this->name, inner: loop($this->options, fn($parent) => '<option'.($parent->id === $record->{$this->name}?->id ? ' selected' : void).' value="'.$parent->id.'">'.$parent, void))
method
line 9
Maakt een HTML-ankerlink met een dynamisch gegenereerde URL en voegt de inhoud toe of gebruikt het record als inhoud, met een CSS-klasse 'async'.
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $content ?? $record)
prop

%field_parent -> options

line 10
Haalt alle beschikbare records op binnen de context van de klasse of het object.
$this->obj::records()
method

%field_parent -> sql

line 12
Definieert een varchar-veld met de naam `$this->name` dat maximaal 10 tekens kan bevatten.
"`$this->name` varchar(10)"
object

%field_password

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

%field_password -> list

line 7
Deze node geeft altijd de waarde false terug, ongeacht eventuele invoer of context.
false
prop

%field_password -> required

line 8
Deze node geeft altijd de waarde 'true' terug, wat betekent dat het veld altijd als verplicht wordt gemarkeerd.
true
prop

%field_password -> minlength

line 9
Een minimumlengte van 8 karakters voor een wachtwoord.
8
prop

%field_password -> placeholder

line 10
Het retourneert een tekstlabel dat aangeeft dat het veld voor het nieuwe wachtwoord is.
'Nieuw wachtwoord'
method

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

line 12
Maakt een invoerveld aan met type, naam, placeholder en een vaste CSS-klasse.
input(type: $this->type, name: $this->name, placeholder: $this->placeholder, class: 'field')
method

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

line 13
Toont een vaste, versleutelde of verborgen wachtwoordlabel, ongeacht de invoer of context.
'••••••••'
method

%field_password -> parse ($record)

line 14
Deze code haalt de wachtwoordwaarde op uit de payload, verwijdert eventuele spaties, en als er een wachtwoord is opgegeven, wordt het gehasht met bcrypt en opgeslagen in het record.
($password = trim(%payload->{$this->name})) && $record->{$this->name} = password_hash($password, PASSWORD_BCRYPT)
method

%field_password -> sql

line 16
Geeft aan dat het veld 'name' wordt gedefinieerd als een tekenreeks met een maximale lengte van 60 karakters in een SQL-tabel.
"`$this->name` char(60)"
object

%field_price

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

%field_price -> decimals

line 7
Deze code geeft het getal 2 terug, wat waarschijnlijk aangeeft dat het getal 2 decimalen moet worden weergegeven of gebruikt.
2
object

%field_select

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

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

line 7
Genereert een `<select>` element met een dynamische naam en opties, waarbij het de optie markeert als geselecteerd op basis van het 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
Dit definieert een SQL-kolom met de naam `$this->name` die een ENUM-type is, inclusief opties die worden samengevoegd in een lijst zoals `'option1','option2',...`.
"`$this->name` enum('".implode("','", $this->options)."')"
object

%field_text

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

%field_text -> length

line 7
Deze node geeft de waarde 100 terug, waarschijnlijk de lengte van een tekst of een vergelijkbare numerieke waarde.
100
prop

%field_text -> multiline

line 8
Deze eigenschap controleert of de lengte van een tekst meer dan 250 tekens is.
$this->length > 250
method

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

line 10
Selecteert op basis van een voorwaarde ofwel een meervoudig invoerveld of een enkel invoerveld te gebruiken.
$this->multiline ? $this->inputMulti($record) : $this->inputField($record)
method

%field_text -> inputField ($record)

line 11
Maakt een HTML-inputveld aan met het type, naam, waarde, maximale lengte, placeholder en CSS-klasse die gebaseerd zijn op de objecteigenschappen. De waarde wordt geëscaped weergegeven als deze bestaat; anders wordt een standaardwaarde gebruikt.
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
Maakt een textarea-element met naam, placeholder en klasse. Als er een waarde in het record bestaat voor deze naam, wordt die ge-escape'd en ingevuld; anders wordt een default-waarde gebruikt indien aanwezig.
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
Definieert een SQL-veld met de naam `$this->name`, datatype varchar en de lengte `$this->length`.
"`$this->name` varchar($this->length)"
object

%field_token

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

%field_token -> length

line 7
Geeft de lengte van de inhoud terug, in dit geval 8.
8
prop

%field_token -> default

line 8
Haalt een token op met een lengte gelijk aan de waarde van `$this->length`.
token($this->length)
prop

%field_token -> create

line 9
Deze code retourneert altijd false, ongeacht de input of omstandigheden.
false
prop

%field_token -> change

line 10
Deze code geeft altijd `false` terug, ongeacht de context of invoer.
false
prop

%field_token -> search

line 11
Deze code zorgt ervoor dat altijd een positie wordt teruggegeven, ongeacht de invoer. Het fungeert als een bevestiging dat de zoekactie succesvol is of dat het resultaat altijd waar is.
true
method

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

line 13
Maakt een HTML-div element aan met als inhoud de ge-escape-de waarde van een veld uit het record.
tag('div', inner: esc($record->{$this->name}))
method

%field_token -> parse ($record)

line 15
Stelt de waarde van een veld in op de waarde in het record als deze nog niet bestaat, of wijzigt deze anders naar een standaardwaarde.
$record->{$this->name} ??= $this->default
method

%field_token -> sql

line 17
Deze code definieert een varchar-kolom met een naam die wordt bepaald door de waarde van `$this->name` en een lengte die wordt bepaald door `$this->length`.
"`$this->name` char($this->length)"
object

%field_virtual

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

%field_virtual -> create

line 7
Deze methode of functie always false retourneert, waarschijnlijk bedoeld om een bepaalde bewerking of validatie te voorkomen of te markeren als niet succesvol.
false
prop

%field_virtual -> change

line 8
Deze code zal altijd onwaar teruggeven.
false
method

%field_virtual -> sql

line 10
Deze node retourneert een lege array zonder verdere bewerkingen of data.
[]

Files

object

%CSV

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

CSV :: __handle

line 5
Dit lijkt een property die een bestands- of padreferentie retourneert met een dynamisch pad dat wordt opgebouwd uit variabelen voor pad en bestandsnaam.
"CSV/$path$filename"
method

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

line 6
Controleren of het pad is ingesteld, en indien niet, een standaardpad gebruiken; samenstellen van de bestandsnaam en extensie; controleren of het CSV-bestand leesbaar is en indien ja, de leesfunctie aanroepen.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.csv'
if (is_readable($this->objFile)) $this->objRead()
readonly

%CSV -> objFile:string

line 12
Deze node leest een CSV-bestand in en koppelt het aan een variabele, zodat de gegevens verder verwerkt kunnen worden.
method

%CSV -> objRead

line 14
Verwerkt een CSV-bestand door het te openen, de eerste rij (headers) te lezen, het juiste scheidingsteken te bepalen op basis van het voorkomen van komma's of puntkomma's, de headers te splitsen en vervolgens alle rijen in te lezen. Elke rij wordt gekoppeld aan de headers en opgeslagen in een 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
version 1.0
creator q-ai.nl
description File library
static

file :: __handle

line 5
Deze code bouwt een pad op door 'file/' te combineren met een variabele '$file' en, indien aanwezig, een optionele naam '$name' die wordt toegevoegd met een schuine streep.
"file/$file".($name ? "/$name" : void)
method

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

line 6
Stelt een naam in als deze meegegeven is, schrijft inhoud naar het bestand als inhoud een string is, en importeert aanvullende objecten of argumenten als die aanwezig zijn.
$name && $this->name = $name
is_string($contents) && $this->write($contents)
$args && $this->objImport(...$args)
method

%file -> append (string $data)

line 12
Voegt de opgegeven tekst toe aan het einde van het bestand zonder het te overschrijven.
file_put_contents($this->file, $data, FILE_APPEND)
prop

%file -> basename

line 13
Haalt de basisnaam (bestandsnaam zonder pad) op van het opgegeven bestand.
pathinfo($this->file, PATHINFO_BASENAME)
method

%file -> base64

line 14
Converteert de inhoud van het bestand naar een Base64-gecodeerde string.
base64_encode($this->contents)
method

%file -> contents

line 15
Leest de inhoud van het bestand dat wordt aangegeven door de bestandsnaam in een property.
file_get_contents($this->file)
method

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

line 16
Maakt een INI-string aan, parseert de inhoud afhankelijk van de parameter en retourneert het resultaat met typetransparantie als `$parse` waar is.
parse_ini_string($this->contents, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW)
method

%file -> contentsJSON ($assoc = null)

line 17
Converteert de inhoud van een JSON-string naar een PHP-variabele, waarbij het resultaat als array of object wordt geretourneerd afhankelijk van de parameter.
json_decode($this->contents, $assoc)
method

%file -> copy ($to)

line 18
Maakt een kopie van het bestand en slaat het op de opgegeven locatie op.
copy($this->file, $to)
method

%file -> created

line 19
Geeft de creatietijd van het bestand terug.
filectime($this->file)
method

%file -> createdAge

line 20
Deze node berekent de leeftijd sinds de creatie door de datum van creatie te vergelijken met de huidige datum.
age($this->created)
method

%file -> createdHuman

line 21
Deze code zet een timestamp om naar een leesbaar formaat voor mensen.
time_human($this->created)
method

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

line 22
Creëert een nieuw CURLFile-object met de opgegeven bestandsnaam, type en optioneel een andere bestandsnaam voor de upload.
new CURLFile($this->file, $type, $filename)
method

%file -> delete

line 23
Voert een controle uit of het bestand bestaat en probeert het te verwijderen. Vervolgens wordt een debug-bericht gelogd dat aangeeft of het verwijderen is gelukt of niet.
first($deleted = $this->exists && unlink($this->file), debug($deleted ? "Deleted $this->basename" : "Could not delete $this->basename"))
method

%file -> exists

line 24
Controleert of het bestand dat in de variabele `$this->file` is opgeslagen, bestaat op het systeem.
file_exists($this->file)
prop

%file -> ext

line 25
Haalt de bestandsextensie op uit de naam van het bestand.
pathinfo($this->name, PATHINFO_EXTENSION)
prop

%file -> filename

line 26
Haalt de bestandsnaam zonder extensie op uit het pad of de bestandsnaam opgeslagen in de variabele.
pathinfo($this->file, PATHINFO_FILENAME)
method

%file -> getLine

line 27
Haalt een regel uit het bestand, verwijdert eventuele witte ruimte aan het einde, en geeft deze terug; als het einde van het bestand is bereikt, geeft het false terug.
($line = fgets($this->pointer)) === false ? false : rtrim($line)
method

%file -> getLength (int $length)

line 28
Leest een bepaald aantal bytes ($length) uit het bestand via de gespecificeerde pointer.
fread($this->pointer, $length)
method

%file -> is (string $file)

line 29
Vergelijkt of de opgegeven bestandsnaam gelijk is aan de interne property `$file`.
$file === $this->file
method

%file -> md5

line 30
Deze code berekent de MD5-hash van het bestand dat wordt weergegeven door de property `$file`.
md5_file($this->file)
prop

%file -> mime

line 31
Bepaalt de MIME-type van een bestand op basis van de bestandsnaam.
mime($this->name)
method

%file -> modified

line 32
Geeft de laatste wijzigingsdatum en -tijd van het bestand terug.
filemtime($this->file)
method

%file -> modifiedAge

line 33
Geeft de leeftijd terug op basis van de gewijzigde datum.
age($this->modified)
method

%file -> modifiedHuman

line 34
Deze node retourneert de meest recente wijziging in een menselijk leesbaar tijdformaat.
time_human($this->modified)
method

%file -> move ($to)

line 35
Verplaatst het bestand naar een nieuwe locatie en update de internal referentie naar de gewijzigde bestandsnaam of pad.
rename($this->file, $to) && $this->file = $to
prop

%file -> name

line 36
Dit haalt de bestandsnaam op zonder pad, meestal gebruikt om de naam zonder directory te verkrijgen.
$this->basename
method

%file -> output ($download = false)

line 37
Maakt een output met de inhoud, naam en optioneel downloaden van een bestand.
output($this->contents, $this->name, $download)
prop

%file -> path

line 38
Deze code retourneert het absolute pad naar de directory waarin het bestand zich bevindt, gevolgd door een schuine streep.
realpath(pathinfo($this->file, PATHINFO_DIRNAME)).slash
prop

%file -> pathRel

line 39
Controleert of het pad van het bestand begint met een bepaald relatieve rootpad, en indien ja, geeft het pad terug vanaf dat punt; anders wordt het volledige pad weergegeven.
str_starts_with($this->file, $relRoot = dirname(dirname($_SERVER['DOCUMENT_ROOT'])).slash) ? substr($this->file, strlen($relRoot)) : $this->file
prop

%file -> pointer

line 40
Deze code opent een bestand in lees- en schrijfmodes.
fopen($this->file, 'r+')
method

%file -> readable

line 41
Controleert of het bestand dat gekoppeld is aan de variabele 'file' leesbaar is.
is_readable($this->file)
method

%file -> src

line 42
Maakt een data-URI aan met het MIME-type en de base64-gegevens, geschikt voor gebruik in bijvoorbeeld afbeelding- of bestandsverwijzingen.
"data:$this->mime;base64,$this->base64"
method

%file -> size

line 43
Returns de grootte van het bestand.
filesize($this->file)
method

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

line 44
Converteert de omvang van een bestand naar een voor mensen leesbare string met een optionele precisie.
size_human($this->size, $precision)
method

%file -> sha1

line 45
Deze methode berekent de SHA-1 hash van het bestand.
sha1_file($this->file)
method

%file -> shortenTo (int $length)

line 46
Deze methode verkort een bestandsnaam tot een opgegeven lengte. Als de naam al korter is dan de lengte, wordt de originele naam teruggegeven. Anders wordt een afkapping toegevoegd met drie puntjes, zodat de uiteindelijke lengte niet groter is dan de opgegeven waarde, inclusief de bestandsextensie.
strlen($this->name) <= $length ? $this->name : substr($this->name, 0, $length - strlen($this->ext) - 3).dot.dot.dot.$this->ext
method

%file -> title

line 47
Convert de bestandsnaam zonder extensie naar een titel door de eerste letter hoofdletter te maken en underscores of andere niet-letters te vervangen door spaties.
ucfirst(strtr(pathinfo($this->name, PATHINFO_FILENAME), [us => space]))
method

%file -> token ($length = 20)

line 48
Genereert een token met een opgegeven lengte, waarbij standaard de SHA-1 hashfunctie wordt gebruikt om het token te maken.
token($length, sha1: $this->sha1)
method

%file -> type

line 49
Haalt het gedeelte van de MIME-waarde voordat de slash op, bijvoorbeeld het type zoals 'image' of 'video'.
substr($this->mime, 0, strpos($this->mime, slash))
method

%file -> touch

line 50
Maakt het bestand met de opgegeven naam (gegevn door `$this->file`) aan als het nog niet bestaat of update de toegangstijd van het bestand.
touch($this->file)
method

%file -> writeable

line 51
controleert of het bestand schrijfbaar is.
is_writeable($this->file)
method

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

line 52
Schrijft data naar een INI-bestand, waarbij lege secties niet worden verwijderd tenzij aangegeven, en verwerkt de data door elke waarde te ontsnappen en te formatteren als key-value paren met dubbele quotes, gescheiden door nieuwe regels.
$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
Deze code roept een methode aan om gegevens weg te schrijven. Als `$deleteEmpty` niet waar is of `$data` aanwezig is, wordt de data JSON-gecodeerd en doorgegeven; anders wordt er geen data geschreven.
$this->write(!$deleteEmpty || $data ? json_encode($data, jsonFlags) : void, $deleteEmpty)
method

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

line 54
Deze code controleert of lege data moet worden geschreven. Als $deleteEmpty valse is of $data niet leeg, wordt de data als JSON-code geëvalueerd en doorgegeven aan een write-functie; anders wordt niets geschreven.
$this->write(!$deleteEmpty || $data ? json_encode($data) : void, $deleteEmpty)
method

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

line 55
Deze methode schrijft data naar een bestand en verwijdert het bestand indien de data leeg is en de parameter op true staat. Bij succesvolle schrijven wordt een debugbericht gelogd, anders wordt een fout gemeld.
if (!$data && $deleteEmpty) return $this->delete
if ($written = file_put_contents($this->file, $data) !== false) debug('Written '.$this->basename.' ('.$this->sizeHuman.')')
else error('Could not write '.$this->file)
return $written
method

%file -> objInfo

line 62
Deze code maakt een array waarbij keys worden samengevoegd en de waarden worden afgeleid uit de huidige objecteigenschappen. Het voegt basisgegevens toe en, afhankelijk van een voorwaarde, ook aanvullende bestandsinformatie zoals grootte, aanmaak- en wijzigingsdatum, en 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
version 1.0
creator q-ai.nl
description GD/img library
static

img :: detect ($data)

line 5
Deze code detecteert het afbeeldingsformaat op basis van de eerste bytes van de gegevens en retourneert de overeenkomstige bestandsindeling ('jpg', 'png', 'gif', 'webp', 'bmp', 'tiff') door specifieke headers te vergelijken.
$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
Zoekt naar afbeeldingsresultaten op Google op basis van een geformatteerde zoekterm, haalt meerdere base64-gecodeerde JPEG-gegevens op uit de HTML, sorteert deze op grootte, selecteert er een paar, kiest er één willekeurig uit en decodeert deze uiteindelijk naar een afbeeldingsbestand.
$q = strtr($search, [dash => '+'])
$DOM = HTTP("https://www.google.com/search?q=$q&source=lnms&tbm=isch&tbs=", agent: true)
$sources = regex_all('/var s=\'data:image\/jpeg;base64,([^\']{1000,})/', $DOM)[1]
usort($sources, fn($a, $b) => strlen($b) <=> strlen($a))
$sources = array_slice($sources, 0, 5)
shuffle($sources)
$source = current($sources)
$source = base64_decode($source)
return $source
static

img :: __handle

line 27
Geeft de waarde van de variabele `$file` door, voorafgaand aan de string "img/".
"img/$file"
method

%img -> __construct (public string $file)

line 28
De constructor accepteert een stringparameter die wordt opgeslagen in een publieke property, waarschijnlijk bedoeld om een bestandsnaam of pad te representeren voor de node.
prop

%img -> src:GdImage

line 30
Laadt een afbeelding vanuit een bestand door de inhoud te lezen en deze vervolgens te converteren naar een afbeeldingsresource.
imagecreatefromstring(file_get_contents($this->file))
prop

%img -> width

line 31
Deze node geeft de breedte van de afbeelding weer door de breedte van de afbeelding te halen via de functie `imagesx` op de bronafbeelding.
imagesx($this->src)
prop

%img -> height

line 32
Deze node geeft de brede (breedte) van een afbeelding terug door de breedte van de afbeelding te ophalen met `imagesx()` op de bron (`$this->src`).
imagesx($this->src)
method

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

line 34
Maakt een schaalbewerking op basis van opgegeven breedte en hoogte, inclusief optionele cropping en verhoudingsbehoud, met correcte image resampling en transparantie ondersteuning.
if (!$width && !$height) return $this
$scale = 1
if ($width && $height) $scale = $crop ? max($width / $this->width, $height / $this->height) : min($width / $this->width, $height / $this->height)
elseif ($width) $scale = $width / $this->width
elseif ($height) $scale = $height / $this->height
if ($scale > 1) return $this
if ($width && $height && $this->width / $this->height != $width / $height){
	if ($width / $height > $this->width / $this->height){
		if ($crop){
			$destX = $width
			$destY = round($width / $this->width * $this->height)
			if ($crop === 'top') $offsetY = 0
			elseif ($crop === 'bottom') $offsetY = -$destY - -$height
			else $offsetY = -round(($destY - $height) / 2)
		}
		else {
			$destX = round($this->width * $height / $this->height)
			$destY = $height
			$width = $destX
			$offsetY = 0
		}
		$offsetX = 0
	}
	else {
		if ($crop){
			$destX = round($height / $this->height * $this->width)
			$destY = $height
			$offsetX = -round (($destX - $width) / 2)
		}
		else {
			$destX = $width
			$destY = round($this->height * $width / $this->width)
			$height = $destY
			$offsetX = 0
		}
		$offsetY = 0
	}
	$destImg = imagecreatetruecolor($width, $height)
	imagealphablending($destImg, false)
	imagesavealpha($destImg, true)
	imagecopyresampled($destImg, $this->src, $offsetX, $offsetY, 0, 0, $destX, $destY, $this->width, $this->height)
}
else {
	if ($width){
		$destX = $width
		$destY = $width / $this->width * $this->height
	}
	elseif ($height){
		$destX = $height / $this->height * $this->width
		$destY = $height
	}
	$destImg = imagecreatetruecolor($destX, $destY)
	imagealphablending($destImg, false)
	imagesavealpha($destImg, true)
	imagecopyresampled($destImg, $this->src, 0, 0, 0, 0, $destX, $destY, $this->width, $this->height)
}
$this->src = $destImg
return $this
method

%img -> source

line 95
Captureert de uitvoer van de `imagejpeg()`-functie in een buffer en retourneert deze als een string, waardoor de afbeelding in geheugen beschikbaar wordt gesteld voor verdere verwerking.
ob_start()
imagejpeg($this->src)
return ob_get_clean()
method

%img -> save ($file = null)

line 101
Slaat het afbeeldingsbestand op, afhankelijk van de extensie; bij 'png' en 'webp' wordt compressie toegepast, bij 'gif' wordt zonder compressie opgeslagen, en standaard wordt JPEG gebruikt met een kwaliteit van 85%.
$file && $this->file = $file
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION))
if ($ext === 'png') return imagepng($this->src, $this->file, 8)
if ($ext === 'gif') return imagegif($this->src, $this->file)
if ($ext === 'webp'){
	imageistruecolor($this->src) || imagepalettetotruecolor($this->src)
	return imagewebp($this->src, $this->file)
}
return imagejpeg($this->src, $this->file, 85)
object

%INI

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

%INI -> objFile:string

line 5
Deze node initialiseert mogelijk een bestand of object, afhankelijk van de context, en voert een setup of initialisatie uit voor het geselecteerde bestand of object.
static

INI :: __handle

line 7
Deze code bouwt een bestandsnaam door een pad en bestandsnaam te combineren, en voegt '/0' toe als de parameter `$parse` niet waar is.
"INI/$path$filename".(!$parse ? '/0' : void)
method

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

line 8
Deze constructor initialiseert een bestandsnaam op basis van een optioneel pad en de naam, door puntvervanging toe te passen. Vervolgens controleert het of het ini-bestand leesbaar is en leest het indien mogelijk, afhankelijk van de 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
Deze code leest een INI-bestand met parse_ini_file, waarbij de inhoud wordt opgeslagen in objData. Vervolgens wordt de laatste waarde van die array geëvalueerd, en wordt de status van objChanged op false gezet. Het resultaat van de last-functie wordt geretourneerd.
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
Schrijft de genormaliseerde data naar een bestand door elke key-waarde combinatie te formatteren, correct te ontsnappen, en vervolgens alles samen met nieuwe regels weg te schrijven.
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
Voert de write-methode uit als er wijzigingen zijn opgetreden.
$this->objChanged && $this->objWrite()
object

%JSON

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

JSON :: __handle

line 5
Voegt een pad en bestandsnaam samen met een voorwaarde dat, indien `$assoc` een booleaanse waarde is, er een schuine streep en de integer-versie van `$assoc` worden toegevoegd; anders gebeurt er niets.
"JSON/$path$filename".(is_bool($assoc) ? slash.(int)$assoc : void)
method

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

line 6
Controleert of de $path-variabele null is en stelt deze in op 'data' indien nodig. Bouwt het volledige pad naar het JSON-bestand. Als het bestand leesbaar is, wordt een leesfunctie aangeroepen met de $assoc-parameter.
$path ??= data
$this->objFile = "$path$filename.json"
if (is_readable($this->objFile)) $this->objRead($assoc)
readonly

%JSON -> objFile:string

line 12
Deze code haalt een JSON-bestand op en converteert het naar een object, dat vervolgens wordt opgeslagen in een variabele of wordt doorgegeven voor verdere verwerking.
method

%JSON -> objTouch

line 14
Deze node markeert dat er veranderingen zijn aangebracht, waarschijnlijk om later te controleren of er updates nodig zijn of acties ondernomen moeten worden.
$this->objChanged = true
method

%JSON -> objRead ($assoc = null)

line 15
Leest JSON-gegevens uit een bestand, slaat deze op in een klasse-eigenschap, en retourneert de klasse zelf voor verdere 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
Voert een JSON-weergave weg naar een bestand en wijzigt de status van het object op basis van het resultaat van de schrijfbewerking.
first($written = json_write($this->objFile, $data, $flags), $written && $this->objChanged = false)
method

%JSON -> __destruct

line 18
Controleert of er veranderingen zijn en schrijft vervolgens de data weg als dat het geval is.
$this->objChanged && $this->objWrite($this->objData)
object

%PDF

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

PDF :: toText (string $file):string

line 5
Voert het 'pdftotext' commando uit op een PDF-bestand via een subprocess, leest de uitvoer en foutmeldingen, controleert op fouten, en geeft de tekst terug zonder achtergebleven form feed karakters.
$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
Deze node geeft geen waarde door, mogelijk betekent dit dat er geen titel is of dat de titel niet is ingesteld.
null
prop

%PDF -> author

line 18
Deze code geeft altijd null terug, ongeacht de invoer.
null
prop

%PDF -> subject

line 19
Deze node geeft altijd null terug, ongeacht de invoer of andere voorwaarden.
null
prop

%PDF -> keywords

line 20
Deze node retourneert geen gegevens en geeft dus een null-waarde terug.
null
prop

%PDF -> creator

line 21
Deze node geeft een string terug die bestaat uit de tekst 'Phlo ' gevolgd door de inhoud van de variabele of property 'phlo', en dan de URL '(https://phlo.tech/)'.
'Phlo '.phlo.' (https://phlo.tech/)'
prop

%PDF -> filename

line 23
Geeft een bestand met de naam "Download.pdf" terug voor download.
'Download.pdf'
prop

%PDF -> mode

line 24
Deze node retourneert de waarde 'D'.
'D'
method

%PDF -> fromHTML ($HTML)

line 26
Maakt een nieuwe mpdf-instantie aan, stelt metadata in op basis van aanwezige eigenschappen, schrijft de HTML-inhoud weg en geeft het gegenereerde PDF-bestand terug met opgegeven bestandsnaam en uitvoermodus.
$mpdf = new \Mpdf\Mpdf
$this->title && $mpdf->SetTitle($this->title)
$this->author && $mpdf->SetAuthor($this->author)
$this->subject && $mpdf->SetSubject($this->subject)
$this->keywords && $mpdf->SetKeywords($this->keywords)
$this->creator && $mpdf->SetCreator($this->creator)
$mpdf->WriteHTML($HTML)
return $mpdf->Output($this->filename, $this->mode)
object

%XLSX

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

%XLSX -> __construct (string $file)

line 5
Laadt een XLSX-bestand door het uitpakken van de ZIP-structuur, leest celgegevens en gedeelde strings, en converteert de gegevens naar een arraystructuur met headers. Verwerken van werkbladen, rijen en cellen gebeurt met regex en string-manipulatie, waarbij gedeelde strings correct worden geëvalueerd en cellreferrers worden omgezet in 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
Creëert een 'button'-element met de meegegeven arguments als attributen of inhoud.
tag('button', ...$args)
function

decrypt($encrypted, $key):string

/phlo/libs/encryption.phlo line 6
Decodeert de base64-gecodeerde input en controleert of de decodering succesvol is en de lengte voldoende is. Zo ja, wordt de nonce uit de eerste bytes gehaald en wordt de rest gebruikt als ciphertext. Vervolgens wordt de geheime data ontsleuteld met een hash van de opgegeven sleutel en de sodium_crypto_secretbox_open-functie. Als een van de controles faalt, wordt false teruggegeven.
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
function

en($text, ...$args)

/phlo/libs/lang.phlo line 9
Deze node vertaalt de invoertekst naar het Engels, gebruikmakend van dezelfde vertaalfunctie met de opgegeven tekst en argumenten.
%lang->translation('en', $text, ...$args)
function

encrypt($data, $key):string

/phlo/libs/encryption.phlo line 5
Deze code genereert een willekeurige nonce, versleutelt de data met behulp van een geheimsleutelfunctie, en encodeert het resultaat vervolgens in Base64 voor veilige transmissie of opslag.
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
Dit definieert een functie die een nieuw veld aanmaakt, waarbij de functie naam dynamisch wordt opgebouwd op basis van het type. Het gebruikt het `$type` argument en extra argumenten om een veld van dat specifieke type te genereren.
phlo("field_$type", ...$args, type: $type)
function

input(...$args):string

/phlo/libs/form.tags.phlo line 6
Deze node genereert een HTML `<input>` element met de opgegeven argumenten als attributen of inhoud.
tag('input', ...$args)
function

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

/phlo/libs/n8n.phlo line 5
Maakt een POST-verzoek naar een webhook-URL, gebaseerd op servergegevens en testmodus, met optionele data mee.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

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

/phlo/libs/n8n.phlo line 6
Roep een functie genaamd 'n8n' aan met de webhook, een optionele data-array en een boolean parameter (waarschijnlijk voor een speciale modus of debug).
n8n($webhook, $data, true)
function

nl($text, ...$args)

/phlo/libs/lang.phlo line 8
Translate de meegegeven tekst naar het Nederlands, eventueel met extra argumenten voor interpolatie of context.
%lang->translation('nl', $text, ...$args)
function

select(...$args):string

/phlo/libs/form.tags.phlo line 7
Maakt een 'select' element aan met de gegeven argumenten.
tag('select', ...$args)
function

textarea(...$args):string

/phlo/libs/form.tags.phlo line 8
Maakt een HTML `<textarea>` element aan met de meegegeven argumenten.
tag('textarea', ...$args)

Phlo Functions

function

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

/phlo/phlo.php line 265
Voert een voorwaarde uit waarbij, indien `$cond` waar is of `$classList` niet leeg, een class-attribuut wordt toegevoegd met de waarde van `$classList` en eventueel 'active' erachter, gescheiden door een spatie als `$classList` al bestaat en `$cond` waar is.
return $cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : void;
function

age(int $time):int

/phlo/phlo.php line 266
Geeft het aantal seconden terug sinds het opgegeven tijdstip.
return time() - $time;
function

age_human(int $age):string

/phlo/phlo.php line 267
Converteert een leeftijd in seconden naar een menselijk begrijpelijke weergave van leeftijd met behulp van een andere functie.
time_human(time() - $age);
function

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

/phlo/phlo.php line 268
Deze code probeert een waarde op te halen uit APCu-cache of deze te genereren via een callback. Vervolgens wordt de waarde geretourneerd en indien loggen is ingeschakeld, wordt een debugbericht gestuurd met details over de cache sleutel en de inhoud of het type van de waarde.
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
Voert een JSON-encoded response uit met de resultaten van `$cmds`, na het controleren van de CLI-modus, of wanneer headers al verzonden zijn, of bij streaming-context. Past debug toe op `$cmds` indien ingeschakeld, en stopt de verwerking daarna.
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
Verwijdert geen elementen en geeft de ontvangen array direct terug.
return $array;
function

auth_log(string $user):int|false

/phlo/phlo.php line 270
Schrijft een regel naar het logbestand met de huidige datum en tijd, de gebruikersnaam en het IP-adres van de bezoeker, en voegt deze toe aan het einde van het bestand.
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
Deze functie converteert een tekst naar camelCase door spaties te verwijderen en woorden met een hoofdletter te starten, waarna de eerste letter van het resultaat kleine wordt gemaakt.
return lcfirst(str_replace(space, void, ucwords(lcfirst($text))));
function

chunk(...$cmds):void

/phlo/phlo.php line 222
Stelt een statische header in als deze nog niet is gezet, en stuurt vervolgens de meegegeven commando's als JSON-gestreamde data naar de client, gevolgd door het flushen van 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
Deze functie combineert de resultaten van twee iteraties: één met de key callback en één met de waarde callback (of de items zelf als geen waarde callback is opgegeven), en maakt daaruit een associatief array.
return array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items);
function

debug(?string $msg = null)

/phlo/phlo.php line 273
Voegt de boodschap toe aan een statische debug-array als de debug-stand in- of uitgeschakeld is; retourneert de hele debug-collectie als geen bericht wordt meegegeven.
if (!debug) return;
static $debug = [];
if (is_null($msg)) return $debug;
$debug[] = $msg;
function

dirs(string $path):array|false

/phlo/phlo.php line 279
Deze code retourneert een array van directorypaden die beginnen met de opgegeven `$path`, inclusief de afsluitende schuine streep voor elke 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
Deze node genereert een volledige HTML-pagina met doctype, html-tag met taal, head-sectie en body-sectie, waarbij de inhoud dynamisch wordt ingevuld 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
Berekent de duur sinds het begin van de request en geeft deze weer met het aantal decimalen dat is aangegeven; optioneel wordt het resultaat afgerond en wordt een snelleheidsindicatie toegevoegd bij korte duur.
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
Worp een uitzondering op met een specifiek bericht en optioneel een code, standard op 500.
throw new PhloException($msg, $code);
function

esc(string $string):string

/phlo/phlo.php line 281
Dit zorgt ervoor dat speciale HTML-tekens in de string worden gecodeerd, zodat ze veilig weergegeven worden in een HTML-context, inclusief quotes en met vervanging van ongeldige tekens.
return htmlspecialchars((string)$string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
function

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

/phlo/phlo.php line 283
Zoek alle bestanden met de opgegeven extensie in de gegeven paden en combineer de resultaten in één array.
return array_merge(...loop((array)$paths, fn($path) => glob("$path$ext")));
function

first(...$args):mixed

/phlo/phlo.php line 284
Haalt het eerste element uit de lijst met argumenten.
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
Verwerkt de invoerparameters om een cURL-verzoek op te bouwen met de juiste HTTP-methode, headers, body en agent. Voert het verzoek uit en retourneert de 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
Voert tekst-indenting uit door herhaald tab-teken toe te voegen op basis van de diepte, en zorgt dat bestaande regels correct worden uitgelijnd zonder extra spaties aan het einde.
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
Voegt inspringingen toe aan een string op basis van de diepte, met extra tab-indelingen voor nieuwe regels die beginnen met een '<'.
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
Laadt de inhoud van het opgegeven bestand, decodeert de JSON-gegevens en retourneert deze. Als het decoderen mislukt of het bestand niet gelezen kan worden, wordt een foutmelding gegenereerd.
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
Schrijft de JSON-encoded data naar een bestand met exclusieve lock, zodat de gegevens veilig worden opgeslagen.
return file_put_contents($file, json_encode($data, $flags ?? jsonFlags), LOCK_EX);
function

last(...$args):mixed

/phlo/phlo.php line 287
Geeft het laatste element van de opgegeven argumenten terug.
return end($args);
function

location(?string $location = null):never

/phlo/phlo.php line 288
Deze code definieert een asynchrone functie die, afhankelijk van of een locatie is opgegeven, de gebruiker doorverwijst naar die locatie of terug naar de vorige pagina. Als geen locatie is meegegeven, wordt een standaardpad gebruikt.
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
Deze methode past een callback toe op elk element van een iterable, waarbij de callback kan bestaan uit een array met een object en een methode of gewoon een closure. Het resultaat is een array met de bewerkte waarden, of een geimplodeerde string als er een implode-parameter is opgegeven.
$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
Geeft de MIME-type terug op basis van de bestandsextensie uit de naam, of via mime_content_type als het bestand bestaat; anders standaard '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
Maakt een nieuw object aan met de meegegeven data als 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
Stelt de HTTP-headers in voor content-type, content-length en content-disposition (attachment of inline), en stuurt daarna de inhoud (bestand of string) naar de output voordat de script stopt.
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
Deze functie beheert en retourneert objecten van een bepaalde naam, ondersteund door een cache. Als geen naam wordt doorgegeven, geeft het de lijst van beschikbare namen. Bij een specifieke naam wordt gecontroleerd of een speciale handle-methode bestaat en wordt deze aangeroepen, of wordt een nieuw object gemaakt en opgeslagen in de cache. Vervolgens wordt indien aanwezig de controller Methode aangeroepen voordat het object wordt geretourneerd.
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
Initialiseert standaardwaarden voor build, debug, data, php en www op basis van invoerparameters; definieert constante waarden en configureert autoloading van klassen via een class map en fallback naar bestandspaden; laadt afhankelijkheden zoals Composer autoload; stelt een requireredirectie en buildcontrole in; registreert fout- en uitzonderingshandlers; voert de 'app' node uit en behandelt CLI-uitvoer of uitzonderingen.
$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
Laadt de inhoud van een bestand, vervangt een specifieke string om het pad aan te passen, decodeert de JSON-gegevens en roept vervolgens een functie aan met deze gegevens en de naam van de applicatie.
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
Voert een asynchrone bewerking uit door een externe 'phlo_exec' functie aan te roepen met de meegegeven objectnaam, call-naam en eventuele extra argumenten, zonder te wachten op het resultaat.
return phlo_exec(www, $obj, $call, false, ...$args);
function

phlo_build_check():bool

/phlo/phlo.php line 162
Deze code controleert of het bestand 'app.php' niet bestaat of dat de laatst gewijzigde tijd van dit bestand ouder is dan de meest recente wijzigingstijd van de bronnen 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
Voegt een foutmelding toe aan een JSON-logbestand door een unieke ID te genereren op basis van het pad en bericht; houdt het aantal keren dezelfde fout voorkomt bij, en registreert de laatste keer dat de fout optrad.
$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
Foutafhandelingsfunctie die een Throwable verwerkt door melding te loggen, condities voor debugging en ontwikkelomgeving, en vervolgens de juiste output (console, API, of HTML-pagina) te genereren op basis van de 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
Voert een PHP-script uit via de commandoregel met opgegeven parameters, en retourneert de output of voert de opdracht asynchroon uit zonder 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
Controleert of er een PHP-bestand bestaat dat overeenkomt met de naam, door de punt-vervanging uit te voeren en vervolgens te controleren op het bestaan van het bestand.
return is_file(php.strtr($obj, [us => dot]).'.php');
function

phlo_sources():array

/phlo/phlo.php line 163
Verzamelt .phlo-bestanden uit een opgegeven bronmap of app-map, voegt vervolgens extra bibliotheekbestanden toe als ze bestaan, sorteert de lijst hoofdletterongevoelig en geeft de geordende lijst terug.
$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
Dit is een functie die een andere functie of methode aanroept met een gespecificeerd object en call, door gebruik te maken van een globale uitvoerfunctie.
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
Voert een reguliere expressie uit op een string en geeft de matches terug als array; retourneert een lege array als geen match wordt gevonden.
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
Voert een reguliere expressie uit op de tekst en retourneert alle gevonden matches in een array; indien geen matches worden gevonden, wordt een lege array geretourneerd.
return preg_match_all($pattern, $subject, $matches, $flags, $offset) ? $matches : [];
function

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

/phlo/phlo.php line 299
Haalt een specifiek deel uit een URL door de string op te splitsen, en geeft dat deel terug. Als een lengte is opgegeven, worden meerdere delen samengevoegd tot één 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
De code valideert en verwerkt een inkomend verzoek op basis van method, asynchronous vlag, payload-gegevens en URL-paadonderdelen. Het filtert het verzoek, haalt variabelen uit het pad, controleert op patronen zoals parameters met default-waarden, vaste waarden, of gedefinieerde lijsten, en verzamelt deze in argumenten voor een callback. Als alle controles slagen, wordt de callback aangeroepen met de verzamelde argumenten.
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
Het haalt het tweede laatst gebruikte deel en het laatste deel van een pad op, gescheiden door een slash, tenzij het pad minder dan twee onderdelen heeft, dan wordt alleen het laatste deel geretourneerd. Bij lege invoer wordt 'unknown' teruggegeven.
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
Deze functie converteert een grootte in bytes naar een menselijke leesbare formaat, door het getal te delen door 1024 totdat het binnen het juiste bereik valt, en voegt vervolgens de juiste schaal (B, KB, MB, GB, TB) toe met de opgegeven precisie.
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
Deze code converteert tekst naar een URL-vriendelijke slug: het zet tekst om naar kleine letters, verwijdert accenten, vervangt niet-alfanu­merieke tekens door koppeltekens en verwijdert overbodige koppeltekens aan beide zijden.
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
Maakt een HTML-tag met opgegeven tagnaam en optionele inhoud, en voegt niet-NULL extra attributen toe als tag- of attribuutwaarden.
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
Deze code berekent een menselijke tijdweergave door een gegeven timestamp te vergelijken met de huidige tijd en deze om te zetten in een geschikt tijdsindeling (zoals dagen, weken, maanden). Het gebruikt een lijst van labels en bijbehorende multipliers om de leeftijd te schalen naar een begrijpelijke eenheid, en stopt wanneer de leeftijd onder een bepaalde drempel ligt.
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
Voert een statische lijst van titels bij, voegt een meegegeven titel toe als die aanwezig is, of gebruikt de huidige app-titel of een standaardwaarde en combineert de lijst tot een enkele string met een gespecificeerd scheidingsteken.
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
Genereert een willekeurig token van een opgegeven lengte door eerst een SHA1-hash te creëren op basis van input, een getal, of een random getal, en vervolgens letters te concatenëren op basis van de hash-waarden totdat de gewenste lengte bereikt is.
$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
Deze node handelt het genereren van een HTML-pagina en het configureren van resources en metadata. Het voegt CSS- en JS-bestanden toe, stelt meta-titels en andere head-elementen in, handelt preloading en defer opties af, en verzorgt de HTML-structuur inclusief body-attributen. Bij async verwerking wordt belangrijke conta nt via `$cmds` meegegeven en toegepast.
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));