files

object

%CSV

/phlo/resources/files/CSV.phlo

CSV reader resource

Reads the first line as the header and picks its own separator by counting: more commas than semicolons and it is a comma, otherwise a semicolon. Every row comes back keyed by header name, so a file with duplicate or empty headers loses columns. It reads, it does not write.

filecsvreaderimport
static

CSV :: __handle

line 10
CSV::$__handle is a reference to the file handle used for reading or writing CSV data located at the specified path and filename.
"CSV/$path$filename"
method

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

line 11
Initializes a CSV object by constructing the file path from the given filename and optional path, defaulting to 'data'. If the constructed file is readable, it calls the objRead method to read the CSV data.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.csv'
if (is_readable($this->objFile)) $this->objRead()
readonly

%CSV -> objFile:string

line 17
Converts a CSV file into an obj representation for easier manipulation within Phlo.
method

%CSV -> objRead:void

line 19
Reads a CSV file and converts its contents into an associative array of objects, using the first row as headers.
$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, dq, void)) $this->objData[] = array_combine($headers, $row)
fclose($fp)
object

%DOCX

/phlo/resources/files/DOCX.phlo

DOCX reader resource

Pulls the plain text out of a .docx and nothing more: no styling, no tables as tables, no images. Paragraphs come out as a list, which is what makes it usable for search and for feeding a model, and DOCX::toText() is the one-liner for that.

filedocxwordreader
method

%DOCX -> __construct (string $file)

line 11
Initializes a DOCX object by opening the specified DOCX file, extracting the text from 'word/document.xml', and processing it into paragraphs.
$zip = new ZipArchive()
if ($zip->open($file) !== true) dx('error opening docx', $file)
$xml = $zip->getFromName('word/document.xml')
$zip->close()
if (!$xml) dx('error reading document.xml')
$text = preg_replace('/<\/w:p>/', lf, $xml)
$text = strip_tags($text)
$text = html_entity_decode($text, ENT_QUOTES | ENT_XML1, 'UTF-8')
$this->text = trim(preg_replace('/[ \t]+/', space, $text))
$this->paragraphs = array_values(array_filter(explode(lf, $this->text), fn($p) => trim($p) !== void))
static

DOCX :: toText (string $file):string

line 24
Converts the contents of a DOCX file into plain text format.
(new static($file))->text
object

%file

/phlo/resources/files/file.phlo

File resource

Wrap a path in %file and everything about it is one call away: contents, size, mime, hashes, human dates, and a token() derived from the file's sha1, so the same content always yields the same token. Watch the difference between file and name: file is where it sits, name is what it is called, and ext and mime read the name. An upload therefore keeps its extension while the temp path has none.

filefilesystemio
static

file :: __handle

line 10
This expression constructs a file path by concatenating 'file/$file' with the optional name, if provided.
"file/$file".($name ? "/$name" : void)
method

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

line 11
Initializes a file object with a specified filename, an optional name, and optional contents, while also allowing additional arguments for object import.
$name && $this->name = $name
is_string($contents) && $this->write($contents)
$args && $this->objImport(...$args)
method

%file -> append (string $data):int|false

line 17
Appends the specified string data to the end of the file, ensuring exclusive access during the operation.
file_put_contents($this->file, $data, FILE_APPEND | LOCK_EX)
prop

%file -> basename:string

line 18
Returns the base name of the file from the given file path.
pathinfo($this->file, PATHINFO_BASENAME)
method

%file -> base64:string

line 19
Encodes the contents of a file into a Base64 string.
base64_encode($this->contents)
method

%file -> contents:string|false

line 20
Reads the entire contents of a file into a string.
file_get_contents($this->file)
method

%file -> contentsINI (bool $parse = true):array|false

line 21
Reads the contents of a file and parses it as an INI string, returning an associative array. The $parse parameter determines whether to use typed or raw scanning for the INI data.
parse_ini_string($this->contents, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW)
method

%file -> contentsJSON ($assoc = null)

line 22
Decodes the JSON string stored in the contents property into a PHP variable, optionally returning an associative array if the $assoc parameter is set to true.
json_decode($this->contents, $assoc)
method

%file -> copy ($to):bool

line 23
Copies the file represented by the current object to the specified destination path.
copy($this->file, $to)
method

%file -> created:int|false

line 24
Returns the Unix timestamp of the last time the file was created.
filectime($this->file)
method

%file -> createdAge:int

line 25
Returns the age of the file based on its creation timestamp.
age($this->created)
method

%file -> createdHuman:string

line 26
Returns a human-readable representation of the file's creation time.
time_human($this->created)
method

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

line 27
Creates a new CURLFile object for file uploads in a cURL request, allowing specification of the file type and filename.
new CURLFile($this->file, $type, $filename)
method

%file -> delete:bool

line 28
Deletes the specified file if it exists and returns a debug message indicating the success or failure of the operation.
first($deleted = $this->exists && unlink($this->file), debug($deleted ? "Deleted $this->basename" : "Could not delete $this->basename"))
method

%file -> exists:bool

line 29
Checks if the specified file exists in the filesystem.
file_exists($this->file)
prop

%file -> ext:string

line 30
Retrieves the file extension from the file name stored in the 'name' property using PHP's pathinfo function.
pathinfo($this->name, PATHINFO_EXTENSION)
prop

%file -> filename:string

line 31
Extracts the filename from the file path stored in the 'file' property.
pathinfo($this->file, PATHINFO_FILENAME)
method

%file -> getLine:string|false

line 32
Retrieves a single line from a file pointer, returning false on failure or the line without trailing whitespace on success.
($line = fgets($this->pointer)) === false ? false : rtrim($line)
method

%file -> getLength (int $length):string|false

line 33
Retrieves the length of the file by reading a specified number of bytes from the file pointer.
fread($this->pointer, $length)
method

%file -> is (string $file):bool

line 34
Checks if the specified file matches the current file instance.
$file === $this->file
method

%file -> md5:string|false

line 35
Calculates the MD5 hash of the specified file.
md5_file($this->file)
prop

%file -> mime:string

line 36
Returns the MIME type of the file based on its name.
mime($this->name)
method

%file -> modified:int|false

line 37
Returns the last modified time of the specified file as a Unix timestamp.
filemtime($this->file)
method

%file -> modifiedAge:int

line 38
Returns the age of the file based on its last modified timestamp.
age($this->modified)
method

%file -> modifiedHuman:string

line 39
Returns a human-readable representation of the file's last modified time.
time_human($this->modified)
method

%file -> move ($to):bool

line 40
Moves the current file to a new location specified by $to, updating the file reference upon success.
rename($this->file, $to) && $this->file = $to
prop

%file -> name:string

line 41
This retrieves the basename of the file represented by the object, which is the name of the file without any directory path.
$this->basename
method

%file -> output ($download = false)

line 42
Outputs the contents of a file, allowing the option to download it if specified.
output($this->contents, $this->name, $download)
prop

%file -> path:string

line 43
This retrieves the directory path of the specified file and appends a slash to it.
realpath(pathinfo($this->file, PATHINFO_DIRNAME)).slash
prop

%file -> pathRel:string

line 44
This expression returns the relative path of a file by checking if the file path starts with 'app' and, if so, removes that prefix; otherwise, it returns the original file path.
str_starts_with($this->file, app) ? substr($this->file, strlen(app)) : $this->file
prop

%file -> pointer

line 45
Opens the specified file in read and write mode, returning a pointer to the file resource.
fopen($this->file, 'r+')
method

%file -> readable:bool

line 46
Checks if the specified file is readable.
is_readable($this->file)
method

%file -> src:string

line 47
This expression generates a data URI for a file, combining its MIME type and base64-encoded content.
"data:$this->mime;base64,$this->base64"
method

%file -> size:int|false

line 48
Returns the size of the specified file in bytes.
filesize($this->file)
method

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

line 49
Converts a file size in bytes to a human-readable format, with an optional precision for decimal places.
size_human($this->size, $precision)
method

%file -> sha1:string|false

line 50
Calculates the SHA-1 hash of the specified file.
sha1_file($this->file)
method

%file -> shortenTo (int $length):string

line 51
Shortens the file name to a specified length while preserving the file extension, adding ellipsis if the name is truncated.
strlen($this->name) <= $length ? $this->name : substr($this->name, 0, $length - strlen($this->ext) - 3).dot.dot.dot.$this->ext
method

%file -> title:string

line 52
Extracts the title from the file name by converting it to a human-readable format, replacing underscores with spaces and capitalizing the first letter.
ucfirst(strtr(pathinfo($this->name, PATHINFO_FILENAME), [us => space]))
method

%file -> token ($length = 20):string

line 53
Generates a token of the specified length using SHA-1 hashing.
token($length, $this->sha1)
method

%file -> type:string

line 54
Extracts the type of a file from its MIME type by retrieving the substring before the first slash.
substr($this->mime, 0, strpos($this->mime, slash))
method

%file -> touch:bool

line 55
Creates a new file or updates the timestamp of an existing file specified by the file path.
touch($this->file)
method

%file -> writable:bool

line 56
Checks if the specified file is writable.
is_writable($this->file)
method

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

line 57
Writes data to an INI file format. The optional parameter allows for the deletion of empty sections if set to true.
$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):bool

line 58
$this->write(!$deleteEmpty || $data ? json_encode($data, jsonPretty) : void, $deleteEmpty)
method

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

line 59
Writes the provided data as a JSON string to a file, optionally deleting empty entries based on the deleteEmpty flag.
$this->write(!$deleteEmpty || $data ? json_encode($data) : void, $deleteEmpty)
method

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

line 60
Writes the specified string data to a file, with an option to delete the file if the data is empty.
if (!$data && $deleteEmpty) return $this->delete
if ($written = file_put_contents($this->file, $data, LOCK_EX) !== false) debug('Written '.$this->basename.' ('.$this->sizeHuman.')')
else error('Could not write '.$this->file)
return $written
method

%file -> objInfo:array

line 67
Creates an object containing information about a file, including its name, existence, and optionally its size, creation date, modification date, and MIME type if it exists.
array_combine($keys = array_merge(['file', 'name', 'exists'], $this->exists ? ['sizeHuman', 'createdHuman', 'modifiedHuman', 'mime'] : []), loop($keys, fn($arg) => $this->$arg))
object

%img

/phlo/resources/files/img.phlo

GD image resource

Only scales down, never up: a request larger than the original returns the image untouched, so a thumbnail never looks stretched. The output format follows the extension you save to, so saving a .jpg writes JPEG at quality 85 whatever came in. Pass crop with a width and a height to fill the frame instead of fitting inside it, and top or bottom to choose which part survives.

imagegdfilegraphics
static

img :: detect ($data):?string

line 10
Detects the image format of a given binary data by examining its header and returns the corresponding format as a string.
$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 :: __handle

line 20
This retrieves the image resource from the specified file path.
"img/$file"
method

%img -> __construct (public string $file)

line 21
Initializes an img object with a public string property $file representing the file path of the image.
prop

%img -> src:GdImage

line 23
This function creates an image resource from a string containing image data, typically loaded from a file.
imagecreatefromstring(file_get_contents($this->file))
prop

%img -> width:int

line 24
Retrieves the width of an image resource specified by the `src` property.
imagesx($this->src)
prop

%img -> height:int

line 25
Returns the height of the image specified by the `src` property.
imagesy($this->src)
method

%img -> scale ($width = null, $height = null, $crop = false):static

line 27
Scales the image to the specified width and height, optionally cropping it based on the provided parameters.
if (!$width && !$height) return $this
$srcW = $this->width
$srcH = $this->height
$doCrop = ($crop && $width && $height)
if ($width && $height) $scale = $doCrop ? max($width / $srcW, $height / $srcH) : min($width / $srcW, $height / $srcH)
elseif ($width) $scale = $width / $srcW
else $scale = $height / $srcH
if ($scale >= 1) return $this
$scaledW = (int)round($srcW * $scale)
$scaledH = (int)round($srcH * $scale)
$destW = ($width && $height && $doCrop) ? (int)$width : $scaledW
$destH = ($width && $height && $doCrop) ? (int)$height : $scaledH
$offsetX = 0
$offsetY = 0
if ($width && $height && $doCrop){
	$offsetX = (int)-round(($scaledW - $destW) / 2)
	$offsetY = (int)-round(($scaledH - $destH) / 2)
	if ($crop === 'top') $offsetY = 0
	elseif ($crop === 'bottom') $offsetY = (int)-($scaledH - $destH)
}
$destImg = imagecreatetruecolor($destW, $destH)
imagealphablending($destImg, false)
imagesavealpha($destImg, true)
imagecopyresampled($destImg, $this->src, $offsetX, $offsetY, 0, 0, $scaledW, $scaledH, $srcW, $srcH)
$this->src = $destImg
$this->width = $destW
$this->height = $destH
return $this
method

%img -> ext ($file = null):string

line 58
Returns the file extension of the specified file in lowercase. If no file is provided, it uses the instance's default file.
strtolower(pathinfo($file ?? $this->file, PATHINFO_EXTENSION))
method

%img -> source ($format = null):string

line 60
Retrieves the source of an image in the specified format, or the default format if none is provided, by capturing the output of the write method.
ob_start()
$this->write($format)
return ob_get_clean()
method

%img -> save ($file = null):bool

line 66
Saves the image to the specified file path if provided; otherwise, it writes the image to a default location.
$file && $this->file = $file
return $this->write(null, $this->file)
method

%img -> write ($format = null, $file = null)

line 71
Writes the image to a file in the specified format, defaulting to JPEG if no format is provided. Supported formats include PNG, GIF, WebP, and JPEG.
$format ??= $this->ext()
if ($format === 'png') return imagepng($this->src, $file, 8)
if ($format === 'gif') return imagegif($this->src, $file)
if ($format === 'webp'){
	imageistruecolor($this->src) || imagepalettetotruecolor($this->src)
	return imagewebp($this->src, $file)
}
return imagejpeg($this->src, $file, 85)
object

%INI

/phlo/resources/files/INI.phlo

Generic INI resource

The same shape as the JSON resource, saved when the object goes out of scope, but writing flattens the file: comments and section headers do not survive a round trip. Keep it to values a program owns; a file a person edits by hand deserves to be read rather than rewritten.

fileiniconfigparser
prop

%INI -> objFile:string

line 10
INI->$objFile is used to create or access an INI file object for reading or writing configuration settings.
static

INI :: __handle

line 12
INI::$__handle retrieves the handle for the specified INI file, optionally parsing it based on the provided parameters.
"INI/$path$filename".(!$parse ? '/0' : void)
method

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

line 13
Initializes an INI object with a specified filename and optional path, and reads the file if it is accessible.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.ini'
if (is_readable($this->objFile)) $this->objRead($parse)
method

%INI -> objRead ($parse = true)

line 19
Reads an INI file and converts it into an obj, optionally parsing the values into their respective types.
last($this->objData = parse_ini_file($this->objFile, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW), $this->objChanged = false, $this)
method

%INI -> objWrite:int|false

line 20
Writes the current object data to an INI file format, ensuring that special characters are properly escaped and the file is locked during the write operation.
file_put_contents($this->objFile, loop($this->objData, fn($value, $key) => $key.' = '.dq.strtr($value, [dq => bs.dq, lf => '\n']).dq, lf).lf, LOCK_EX)
method

%INI -> __destruct

line 22
The INI->__destruct method checks if the object has changed and calls the objWrite method if necessary to handle cleanup before the object is destroyed.
$this->objChanged && $this->objWrite()
object

%JSON

/phlo/resources/files/JSON.phlo

Generic JSON resource

The file behaves as an object: read a key, write a key, and the file is saved when the object goes out of scope. That last part is the trap: nothing is written until then, so call objWrite() yourself when the request may end otherwise. Names are read against data/ and a slash in the name becomes a dot, so no name can escape the directory.

filejsonstorageparser
static

JSON :: __handle

line 11
This function constructs a JSON file path based on the provided filename and an associative boolean flag, determining the format of the path.
"JSON/$path$filename".(is_bool($assoc) ? slash.(int)$assoc : void)
method

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

line 12
Constructs a JSON object from a specified filename and optional path, creating the full file path and reading the JSON data if the file is accessible.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.json'
if (is_readable($this->objFile)) $this->objRead($assoc)
readonly

%JSON -> objFile:string

line 18
Converts a JSON string into a Phlo obj for further manipulation or processing.
method

%JSON -> objTouch:bool

line 20
Sets the objChanged property to true, indicating that the object has been modified.
$this->objChanged = true
method

%JSON -> objRead ($assoc = null)

line 21
Reads a JSON file and converts it into an object or an associative array based on the provided parameter.
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 22
Writes JSON data to an object file, optionally using specified flags to modify the write behavior.
first($written = json_write($this->objFile, $data, $flags), $written && $this->objChanged = false)
method

%JSON -> __destruct

line 24
This method is called when an object is destroyed, ensuring that any changes to the object's data are written before it is removed from memory.
$this->objChanged && $this->objWrite($this->objData)
object

%PDF

/phlo/resources/files/PDF.phlo

PDF generator and reader

Two halves that share a name. Reading uses the pdftotext binary, so it needs poppler-utils on the machine and gives nothing on a scan without a text layer. Writing renders HTML through mPDF, and mode is the mPDF one: D sends a download, I shows it in the browser, S returns the bytes as a string, so leaving the default on an API route pushes a download at your caller.

filepdfreadergenerator
static

PDF :: toText (string $file):string

line 10
$process = proc_open('pdftotext '.escapeshellarg($file).' -', [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes)
if (!is_resource($process)) error('PDFToText Error: could not start pdftotext')
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:?string

line 22
Generates a PDF document with the specified title.
null
prop

%PDF -> author:?string

line 23
Retrieves the author of the PDF document.
null
prop

%PDF -> subject:?string

line 24
Generates a PDF document based on the specified subject.
null
prop

%PDF -> keywords:?string

line 25
Generates a list of keywords extracted from a PDF document.
null
prop

%PDF -> creator:string

line 26
The PDF->$creator property retrieves the creator information of the PDF document.
'Phlo '.phlo.' (https://phlo.tech/)'
prop

%PDF -> filename:string

line 28
This item specifies the filename 'Download.pdf' for a PDF resource.
'Download.pdf'
prop

%PDF -> mode:string

line 29
Sets the mode for PDF processing, where 'D' indicates a specific mode of operation.
'D'
method

%PDF -> fromHTML ($HTML):string

line 31
$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 (string)$mpdf->Output($this->filename, $this->mode)
object

%UBL

/phlo/resources/files/UBL.phlo

UBL 2.1 invoice XML (PEPPOL BIS Billing 3.0) from a normalized invoice structure. Use UBL::invoice($data).

Builds the invoice XML that PEPPOL expects from a plain array with supplier, customer and lines. Tax is grouped by rate and each group gets its own subtotal, so lines at 21 and 9 percent land in the right boxes on their own. It formats and escapes the amounts you hand it but checks nothing: a total that does not match its lines is written out as given, and a receiver will reject it.

ublpeppolinvoicexmle-invoicingexport
static

UBL :: invoice (array $data):string

line 10
Generates a UBL invoice XML structure based on provided invoice data, including supplier and customer information, line items, tax calculations, and total amounts.
$currency = strtoupper((string)($data['currency'] ?? 'EUR'))
$issue = (string)($data['issue_date'] ?? void)
$due = (string)($data['due_date'] ?? void) ?: $issue
$lines = []
$linesSum = 0
$taxSubtotals = []
foreach ((array)($data['lines'] ?? []) AS $i => $line){
	$qty = (float)($line['quantity'] ?? 0)
	$net = round($qty * (float)($line['unit_price'] ?? 0), 2)
	$rate = (float)($line['tax_rate'] ?? 0)
	$lines[] = static::xmlLine($i + 1, $line, $qty, $net, $rate, $currency)
	$linesSum += $net
	$key = number_format($rate, 2, dot, void)
	$taxSubtotals[$key] = ($taxSubtotals[$key] ?? 0) + $net
}
$tax = (float)($data['tax_amount'] ?? 0)
$total = (float)($data['total_amount'] ?? 0)
$taxXml = void
foreach ($taxSubtotals AS $rate => $taxable){
	$amount = round($taxable * (float)$rate / 100, 2)
	$category = (float)$rate === 0.0 ? 'Z' : 'S'
	$taxXml .= implode(void, [
		'<cac:TaxSubtotal>',
		'<cbc:TaxableAmount currencyID="'.$currency.'">'.static::n($taxable).'</cbc:TaxableAmount>',
		'<cbc:TaxAmount currencyID="'.$currency.'">'.static::n($amount).'</cbc:TaxAmount>',
		'<cac:TaxCategory><cbc:ID>'.$category.'</cbc:ID><cbc:Percent>'.static::n((float)$rate).'</cbc:Percent>',
		'<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:TaxCategory>',
		'</cac:TaxSubtotal>',
	])
}
$supplier = (array)($data['supplier'] ?? [])
$customer = (array)($data['customer'] ?? [])
return implode(void, [
	'<?xml version="1.0" encoding="UTF-8"?>',
	'<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">',
	'<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>',
	'<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>',
	'<cbc:ID>'.static::esc((string)($data['number'] ?? void)).'</cbc:ID>',
	'<cbc:IssueDate>'.static::esc($issue).'</cbc:IssueDate>',
	'<cbc:DueDate>'.static::esc($due).'</cbc:DueDate>',
	'<cbc:InvoiceTypeCode>'.(!empty($data['credit_note']) ? '381' : '380').'</cbc:InvoiceTypeCode>',
	'<cbc:DocumentCurrencyCode>'.$currency.'</cbc:DocumentCurrencyCode>',
	static::partyXml('AccountingSupplierParty', (string)($supplier['name'] ?? void), $supplier, (string)($supplier['vat'] ?? void)),
	static::partyXml('AccountingCustomerParty', (string)($customer['name'] ?? void), $customer, (string)($customer['vat'] ?? void)),
	'<cac:TaxTotal><cbc:TaxAmount currencyID="'.$currency.'">'.static::n($tax).'</cbc:TaxAmount>'.$taxXml.'</cac:TaxTotal>',
	'<cac:LegalMonetaryTotal>',
	'<cbc:LineExtensionAmount currencyID="'.$currency.'">'.static::n($linesSum).'</cbc:LineExtensionAmount>',
	'<cbc:TaxExclusiveAmount currencyID="'.$currency.'">'.static::n($linesSum).'</cbc:TaxExclusiveAmount>',
	'<cbc:TaxInclusiveAmount currencyID="'.$currency.'">'.static::n($total).'</cbc:TaxInclusiveAmount>',
	'<cbc:PayableAmount currencyID="'.$currency.'">'.static::n($total).'</cbc:PayableAmount>',
	'</cac:LegalMonetaryTotal>',
	implode(void, $lines),
	'</Invoice>',
])
static

UBL :: xmlLine ($idx, $line, $qty, $net, $rate, $currency = 'EUR'):string

line 67
Generates an XML representation of an invoice line item, including details such as quantity, amount, item description, tax category, and price.
$category = (float)$rate === 0.0 ? 'Z' : 'S'
return implode(void, [
	'<cac:InvoiceLine>',
	'<cbc:ID>'.$idx.'</cbc:ID>',
	'<cbc:InvoicedQuantity unitCode="EA">'.static::n($qty).'</cbc:InvoicedQuantity>',
	'<cbc:LineExtensionAmount currencyID="'.$currency.'">'.static::n($net).'</cbc:LineExtensionAmount>',
	'<cac:Item><cbc:Name>'.static::esc((string)($line['description'] ?? void)).'</cbc:Name>',
	'<cac:ClassifiedTaxCategory><cbc:ID>'.$category.'</cbc:ID><cbc:Percent>'.static::n((float)$rate).'</cbc:Percent>',
	'<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:ClassifiedTaxCategory></cac:Item>',
	'<cac:Price><cbc:PriceAmount currencyID="'.$currency.'">'.static::n((float)($line['unit_price'] ?? 0)).'</cbc:PriceAmount></cac:Price>',
	'</cac:InvoiceLine>',
])
static

UBL :: partyXml ($wrapper, $name, $info, $vatNumber):string

line 82
Generates XML representation of a party element for UBL, including details like name, address, city, postal code, country, and tax information.
$address = (string)($info['address'] ?? void)
$postal = (string)($info['postal_code'] ?? void)
$city = (string)($info['city'] ?? void)
$country = (string)($info['country'] ?? void) ?: 'NL'
$tax = $vatNumber ? '<cac:PartyTaxScheme><cbc:CompanyID>'.static::esc($vatNumber).'</cbc:CompanyID><cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:PartyTaxScheme>' : void
return implode(void, [
	'<cac:'.$wrapper.'><cac:Party>',
	'<cac:PartyName><cbc:Name>'.static::esc($name).'</cbc:Name></cac:PartyName>',
	'<cac:PostalAddress>',
	'<cbc:StreetName>'.static::esc($address).'</cbc:StreetName>',
	'<cbc:CityName>'.static::esc($city).'</cbc:CityName>',
	'<cbc:PostalZone>'.static::esc($postal).'</cbc:PostalZone>',
	'<cac:Country><cbc:IdentificationCode>'.static::esc(strtoupper($country)).'</cbc:IdentificationCode></cac:Country>',
	'</cac:PostalAddress>',
	$tax,
	'<cac:PartyLegalEntity><cbc:RegistrationName>'.static::esc($name).'</cbc:RegistrationName></cac:PartyLegalEntity>',
	'</cac:Party></cac:'.$wrapper.'>',
])
static

UBL :: n ($v):string

line 103
Formats a number to two decimal places using the specified decimal point and thousands separator.
number_format((float)$v, 2, dot, void)
static

UBL :: esc ($v):string

line 104
Converts special characters to HTML entities for XML output, ensuring proper encoding for UTF-8 strings.
htmlspecialchars((string)$v, ENT_XML1 | ENT_QUOTES, 'UTF-8')
object

%XLSX

/phlo/resources/files/XLSX.phlo

XLSX reader resource

Reads a workbook without a library by unpacking the zip itself, and gives every sheet by name with the first row as the header. Values arrive as the sheet stored them, so a date is the serial number Excel keeps and a percentage is a fraction; convert those yourself. Formulas give the last calculated value, so a sheet that was never opened after an edit hands you the old one.

filexlsxexcelreader
method

%XLSX -> __construct (string $file)

line 16
Reads the sheets straight out of the zip with regular expressions instead of an XML parser.
$sheets = []
$shared = []
$sheetNames = []
$zip = new ZipArchive()
if ($zip->open($file) !== true) dx('error opening zip', $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)) dx('error reading shared lib')
		$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)) dx('error reading 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)) dx('error parsing 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)) dx('error parsing row', $rowXml)
		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 !== void ? $valV : ($valIS !== null && $valIS !== void ? html_entity_decode($valIS, ENT_QUOTES | ENT_XML1, 'UTF-8') : null)
				$txt = $isShared($attrs) ? ($shared[$val] ?? null) : $val
				$headerMap[$col] = $txt !== null && $txt !== void ? $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 !== void ? $valV : ($valIS !== null && $valIS !== void ? 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
		}
	}
}

Last updated on 23 August 2026

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.