Biblioteka TreeStructInfo PHP
Wprowadzenie
TreeStructInfo to prosty, czytelny format tekstowy przeznaczony do opisu zagnieżdżonych struktur danych w postaci drzew. Format ten umożliwia wygodne definiowanie atrybutów, węzłów oraz referencji, dzięki czemu świetnie nadaje się do konfigurowalnych aplikacji, testów i analiz danych. Udostępnione tutaj klasy PHP – parser i eksporter – umożliwiają konwersję danych między formatem TreeStructInfo a natywnymi strukturami PHP w obu kierunkach.
Parser TreeStructInfo (TreeStructInfoParser.php
)
Opis:
Klasa TreeStructInfoParser
służy do wczytywania i analizowania plików w formacie TreeStructInfo 2.0 – specjalnego, tekstowego formatu opisu drzewiastych struktur danych. Parser konwertuje taki plik do zagnieżdżonej tablicy PHP, która zawiera trzy główne klucze:
version
– wersja formatu,name
– opcjonalna nazwa struktury,data
– właściwa zawartość drzewa danych jako zagnieżdżone obiekty lub tablice.
Zastosowanie:
- Wczytywanie danych z plików
.tsinfo
w aplikacjach PHP, - Przetwarzanie strukturalnych danych konfiguracyjnych,
- Ułatwiona konwersja danych do innych formatów (np. JSON, YAML, bazy danych).
Kod źródłowy parsera
<?php
if (!function_exists('str_starts_with'))
{
function str_starts_with($haystack, $needle)
{
return strpos($haystack, $needle) === 0;
}
}
/**
* TreeStructInfoParser
*
* A parser for the TreeStructInfo text-based structured format.
* Supports attribute parsing, nested nodes, reference attributes,
* auto type casting, and caching.
* Licensed under the BSD 3-Clause License.
* See the LICENSE.md file for full license text.
*/
class TreeStructInfoParser
{
private $lines;
private $refLines;
private $currentLine = 0;
private $currentRefLine = 0;
private $references = [];
private $nodeReferences = [];
private $autoCasting = false;
private $multilineJoinChar = PHP_EOL;
private $trueValues = ['true'];
private $falseValues = ['false'];
/**
* Enables or disables automatic value casting.
*
* @param bool $value
*/
public function setAutoCasting($value)
{
$this->autoCasting = $value;
}
/**
* Sets the character or string used to join multiline attribute values.
*
* @param string $char
*/
public function setMultilineJoinChar($char)
{
$this->multilineJoinChar = $char;
}
/**
* Sets the boolean string representations used for auto casting.
*
* @param array $trueValues
* @param array $falseValues
* @throws Exception
*/
public function setBoolValues($trueValues, $falseValues)
{
if (empty($trueValues)) {
throw new Exception("Empty True values");
}
if (empty($falseValues)) {
throw new Exception("Empty False values");
}
$this->trueValues = $trueValues;
$this->falseValues = $falseValues;
}
/**
* Parses a TreeStructInfo string and returns a structured array.
*
* @param string $input
* @return array
* @throws Exception
*/
public function parse($input)
{
$allLines = array_values(array_filter(array_map('trim', explode("\n", $input))));
$treeLines = [];
$i = 0;
$lastRefAttrName = null;
$lastRefNodeName = null;
$endTree = false;
$this->refLines = [];
$this->currentRefLine = 0;
while ($i < count($allLines)) {
$line = $allLines[$i];
// ref attr Name "Value"
if (preg_match('/^ref attr (.+?)\s+"([^"]+)"$/', $line, $matches)) {
$this->references[$matches[1]] = $matches[2];
$lastRefAttrName = $matches[1];
$i++;
continue;
}
// next line values on ref attr Name
if (($lastRefAttrName !== null) && (str_starts_with($line, '"')))
{
$this->references[$lastRefAttrName] .= $this->multilineJoinChar . trim($line, '"');
$i++;
continue;
}
if (str_starts_with($line, 'end tree'))
{
$endTree = true;
}
// ref node Some ...
if ($endTree && preg_match('/^ref node (.+?)$/', $line, $matches)) {
$refName = $matches[1];
$this->refLines[] = 'node '.$refName;
$i++; // Skip ref node line
while ($i < count($allLines) && trim($allLines[$i]) !== 'end ref node') {
$this->refLines[] = $allLines[$i];
$i++;
}
$this->refLines[] = 'end node';
if (!isset($allLines[$i]) || trim($allLines[$i]) !== 'end ref node') {
throw new Exception("Missing 'end ref node' for ref node $refName");
}
$i++; // Skip 'end ref node'
$refNode = $this->parseAnonymousNode();
$this->nodeReferences[$refName] = $refNode[$refName];
continue;
}
$treeLines[] = $line;
$i++;
}
$this->lines = $treeLines;
$this->currentLine = 0;
return $this->parseTree();
}
/**
* Parses a TreeStructInfo file and caches the result as PHP.
* If cache is newer than source, returns the cached version.
*
* @param string $inputFile
* @param string $cacheFile
* @return array
*/
public function parseWithCache($inputFile, $cacheFile)
{
if (file_exists($cacheFile) && filemtime($cacheFile) >= filemtime($inputFile)) {
return include $cacheFile;
}
$input = file_get_contents($inputFile);
$parsed = $this->parse($input);
$export = var_export($parsed, true);
file_put_contents($cacheFile, "<?php\nreturn $export;\n");
return $parsed;
}
/**
* Parses a reference node structure (with optional children).
*
* @return array
* @throws Exception
*/
private function parseAnonymousNode()
{
$line = $this->nextRefLine();
if (!preg_match('/^node\s+(.+?)$/', $line, $matches)) {
throw new Exception("Invalid node start: $line");
}
$name = $matches[1];
$node[$name] = [];
$lastname = null;
while (($line = $this->peekRefLine()) !== null && $line !== 'end node') {
if (str_starts_with($line, '::')) {
$this->nextRefLine();
} elseif (str_starts_with($line, 'attr ')) {
$attr = $this->parseAttr($this->nextRefLine());
$node[$name][$attr['name']] = $attr['value'];
$lastname = $attr['name'];
} elseif (str_starts_with($line, '"')) {
$attr = $this->nextRefLine();
if ($lastname) {
$node[$name][$lastname] .= $this->multilineJoinChar . trim($attr, '"');
}
} elseif (str_starts_with($line, 'ref attr ')) {
$attr = $this->resolveRefAttr($this->nextRefLine());
$node[$name][$attr['name']] = $attr['value'];
} elseif (str_starts_with($line, 'ref node ')) {
$refnode = $this->resolveRefNode($this->nextRefLine());
$childname = $refnode['name'];
$childvalue = $refnode['value'];
$node[$name][$childname] = $childvalue;
} elseif (str_starts_with($line, 'node ')) {
$childnode = $this->parseAnonymousNode();
$node[$name] = (isset($node[$name]) && is_array($node[$name])) ? array_merge($node[$name], $childnode) : $childnode;
} else {
throw new Exception("Unexpected line in node: $line");
}
}
$this->expectRefLine('end node');
return $node;
}
/**
* Parses the main tree structure.
*
* @return array
* @throws Exception
*/
private function parseTree()
{
$header = $this->nextLine();
while (str_starts_with($header, '::')) {
$header = $this->nextLine();
}
if (!preg_match('/^treestructinfo\s+"(\d+\.\d+)"(?:\s+name\s+"([^"]+)")?$/', $header, $matches)) {
throw new Exception("Invalid header format");
}
$result = [];
$result['version'] = $matches[1];
$result['name'] = isset($matches[2]) ? $matches[2] : null;
$tree = [];
$lastname = null;
while (($line = $this->peekLine()) !== null && $line !== 'end tree') {
if (str_starts_with($line, '::')) {
$this->nextLine(); // skip comment
} elseif (str_starts_with($line, 'attr ')) {
$attr = $this->parseAttr($this->nextLine());
$tree[$attr['name']] = $attr['value'];
$lastname = $attr['name'];
} elseif (str_starts_with($line, '"')) {
$attr = $this->nextLine();
if ($lastname) {
$tree[$lastname] .= $this->multilineJoinChar . trim($attr, '"');
}
} elseif (str_starts_with($line, 'ref attr ')) {
$attr = $this->resolveRefAttr($this->nextLine());
$tree[$attr['name']] = $attr['value'];
} elseif (str_starts_with($line, 'ref node ')) {
$refnode = $this->resolveRefNode($this->nextLine());
$tree[$refnode['name']] = $refnode['value'];
} elseif (str_starts_with($line, 'node ')) {
$node = $this->parseNode();
$tree = array_merge($tree, $node);
} else {
throw new Exception("Unexpected line in tree: $line");
}
}
$this->expectLine('end tree');
$result['data'] = $tree;
return $result;
}
/**
* Parses a single node structure (with optional children).
*
* @return array
* @throws Exception
*/
private function parseNode()
{
$line = $this->nextLine();
if (!preg_match('/^node\s+(.+?)$/', $line, $matches)) {
throw new Exception("Invalid node start: $line");
}
$name = $matches[1];
$node[$name] = [];
$lastname = null;
while (($line = $this->peekLine()) !== null && $line !== 'end node') {
if (str_starts_with($line, '::')) {
$this->nextLine();
} elseif (str_starts_with($line, 'attr ')) {
$attr = $this->parseAttr($this->nextLine());
$node[$name][$attr['name']] = $attr['value'];
$lastname = $attr['name'];
} elseif (str_starts_with($line, '"')) {
$attr = $this->nextLine();
if ($lastname) {
$node[$name][$lastname] .= $this->multilineJoinChar . trim($attr, '"');
}
} elseif (str_starts_with($line, 'ref attr ')) {
$attr = $this->resolveRefAttr($this->nextLine());
$node[$name][$attr['name']] = $attr['value'];
} elseif (str_starts_with($line, 'ref node ')) {
$refnode = $this->resolveRefNode($this->nextLine());
$node[$name][$refnode['name']] = $refnode['value'];
} elseif (str_starts_with($line, 'node ')) {
$childnode = $this->parseNode();
$node[$name] = (isset($node[$name]) && is_array($node[$name])) ? array_merge($node[$name], $childnode) : $childnode;
} else {
throw new Exception("Unexpected line in node: $line");
}
}
$this->expectLine('end node');
return $node;
}
/**
* Parses a single attribute line.
*
* @param string $line
* @return array
* @throws Exception
*/
private function parseAttr($line)
{
if (!preg_match('/^attr\s+(.+?)\s+"([^"]*)"?$/', $line, $matches)) {
throw new Exception("Invalid attribute: $line");
}
$name = $matches[1];
$value = isset($matches[2]) ? $matches[2] : '';
return [
'name' => $name,
'value' => $this->autoCasting ? $this->autoCast($value) : $value,
];
}
/**
* Resolves a reference attribute from previously stored values.
*
* @param string $line
* @return array
* @throws Exception
*/
private function resolveRefAttr($line)
{
if (!preg_match('/^ref attr (.+)$/', $line, $matches)) {
throw new Exception("Invalid ref attr usage: $line");
}
$refName = $matches[1];
if (!isset($this->references[$refName])) {
throw new Exception("Undefined reference attribute: $refName");
}
return [
'name' => $refName,
'value' => $this->autoCasting ? $this->autoCast($this->references[$refName]) : $this->references[$refName],
];
}
/**
* Resolves a reference node from previously stored nodes.
*
* @param string $line
* @return array
* @throws Exception
*/
private function resolveRefNode($line)
{
if (!preg_match('/^ref node (.+)$/', $line, $matches)) {
throw new Exception("Invalid ref node usage: $line");
}
$refName = $matches[1];
if (!isset($this->nodeReferences[$refName])) {
throw new Exception("Undefined reference node: $refName");
}
return [
'name' => $refName,
'value' => $this->nodeReferences[$refName],
];
}
/**
* Peeks at the current line without advancing the line pointer.
*
* @return string|null
*/
private function peekLine()
{
return isset($this->lines[$this->currentLine]) ? $this->lines[$this->currentLine] : null;
}
/**
* Peeks at the current ref line without advancing the line pointer.
*
* @return string|null
*/
private function peekRefLine()
{
return isset($this->refLines[$this->currentRefLine]) ? $this->refLines[$this->currentRefLine] : null;
}
/**
* Retrieves the current line and advances the line pointer.
*
* @return string|null
*/
private function nextLine()
{
$line = $this->lines[$this->currentLine++];
return isset($line) ? $line : null;
}
/**
* Retrieves the current ref line and advances the line pointer.
*
* @return string|null
*/
private function nextRefLine()
{
$line = $this->refLines[$this->currentRefLine++];
return isset($line) ? $line : null;
}
/**
* Ensures the next line matches the expected value.
*
* @param string $expected
* @throws Exception
*/
private function expectLine($expected)
{
$line = $this->nextLine();
if ($line !== $expected) {
throw new Exception("Expected '$expected' but found '$line'");
}
}
/**
* Ensures the next ref line matches the expected value.
*
* @param string $expected
* @throws Exception
*/
private function expectRefLine($expected)
{
$line = $this->nextRefLine();
if ($line !== $expected) {
throw new Exception("Expected '$expected' but found '$line'");
}
}
/**
* Attempts to auto-cast a string to bool, int, float, or binary if applicable.
*
* @param string $value
* @return mixed
*/
public function autoCast($value)
{
$trimmed = trim($value);
// Coords check
if (preg_match('/^\+?(0[xob])?[0-9a-fA-F]+,\+?(0[xob])?[0-9a-fA-F]+$/i', $trimmed)) {
$parts = explode(',', $trimmed);
return array_map([$this, 'autoCast'], $parts);
}
// Logic check
$condTrue = false;
foreach ($this->trueValues as $v) {
$condTrue = $condTrue || (strcasecmp($trimmed, $v) === 0);
}
$condFalse = false;
foreach ($this->falseValues as $v) {
$condFalse = $condFalse || (strcasecmp($trimmed, $v) === 0);
}
if ($condTrue) return true;
if ($condFalse) return false;
// Hex (prefixed with 0x)
if (preg_match('/^0x[0-9a-fA-F]+$/', $trimmed)) return hexdec($trimmed);
// Binary (prefixed with 0b)
if (preg_match('/^0b[01]+$/', $trimmed)) return bindec($trimmed);
// Octal (prefixed with 0o or 0)
if (preg_match('/^0o[0-7]+$/i', $trimmed)) return octdec($trimmed);
// Decimal integer
if (preg_match('/^[+-]?\d+$/', $trimmed)) return (int) $trimmed;
// Decimal float
if (preg_match('/^[+-]?\d+[.,]\d+([eE][+-]?\d+)?$/', $trimmed)) {
return (float) str_replace(',', '.', $trimmed);
}
// Binary data as hex string
if (preg_match('/^[a-fA-F0-9]{4,}$/', $trimmed) && strlen($trimmed) % 2 === 0) {
return hex2bin($trimmed);
}
// Base64-encoded string
if (base64_encode(base64_decode($trimmed, true)) === $trimmed) {
return base64_decode($trimmed);
}
return $trimmed;
}
}
Użycie klasy parsera
$content = file_get_contents('example.tsinfo');
$parser = new TreeStructInfoParser();
$parser->setAutoCasting(true);
$parser->setMultilineJoinChar(' ');
$parser->setBoolValues(['true', 'on', 't'], ['false', 'off', 'n']);
$tree = $parser->parse($content);
Przykład formatu TreeStructInfo 2.0
:: Example TreeStructInfo 2.0
treestructinfo "2.0" name "Test Sample Tree"
:: Various root tree attributes
attr StringAttr "Example String Attribute"
attr BoolAttr "true"
attr IntBoolAttr "1"
attr IntAttr "42"
attr FloatAttr "3.1415"
attr EngineeringAttr "1.23E+03"
attr BinaryDataAttr "54726565537472756374496E666F202D"
attr Base64DataAttr "U29tZSBiYXNlNjQgZGF0YQ=="
attr MultilineDataAttr "First Line"
"Second Line"
node Coords
attr DecCoords "100,120"
attr HexCoords "0xA0,0x80"
attr OctCoords "0o200,0o100"
attr BinCoords "0b10110011,0b11001101"
end node
node Meta
attr Author "John Doe"
ref attr SharedNote
ref node ParentRefNode
end node
node Items
node ItemA
attr Price "12,50 zł"
attr Available "false"
end node
node ItemB
attr Price "20,00 zł"
attr Available "true"
end node
end node
end tree
:: Ref attributes and ref nodes must be defined after end tree
:: Ref attrubites must be defined before ref nodes
ref attr SharedNote "This is a shared note used across the tree."
:: Child ref node must by defined before parent ref node
ref node ChildRefNode
attr Name "This Is Child Ref Node"
node ChildNode
attr ExampleValue "123456"
end node
end ref node
ref node ParentRefNode
attr Name "This Is Parent Ref Node"
node Child
attr ChildExampleAttr "This is child node attr"
ref attr SharedNote
end node
ref node ChildRefNode
end ref node
- w pliku konfiguracyjnym musi być zachowana odpowiednia kolejność definiowania danych
- atrybuty i węzły referencjonowane muszą być zdefiniowane po całym drzewie z danymi czyli po
end tree
- atrybuty referencjonowane muszą być zdefiniowane przed węzłami referencjonowanymi
- węzły referencjonowane występujące w innych węzłach referencjonowanych muszą być zdefiniowane przed nimi
- przy włączonym automatycznym rzutowaniu typów separatorem miejsc dziesiętnych musi być kropka
Wynik parsowania
array (
'version' => '2.0',
'name' => 'Test Sample Tree',
'data' =>
array (
'StringAttr' => 'Example String Attribute',
'BoolAttr' => true,
'IntBoolAttr' => 1,
'IntAttr' => 42,
'FloatAttr' => 3.1415,
'EngineeringAttr' => 1230,
'BinaryDataAttr' => 'TreeStructInfo -',
'Base64DataAttr' => 'Some base64 data',
'MultilineDataAttr' => 'First LineSecond Line',
'Coords' =>
array (
'DecCoords' =>
array (
0 => 100,
1 => 120,
),
'HexCoords' =>
array (
0 => 160,
1 => 128,
),
'OctCoords' =>
array (
0 => 128,
1 => 64,
),
'BinCoords' =>
array (
0 => 179,
1 => 205,
),
),
'Meta' =>
array (
'Author' => 'John Doe',
'SharedNote' => 'This is a shared note used across the tree.',
'ParentRefNode' =>
array (
'Name' => 'This Is Parent Ref Node',
'Child' =>
array (
'ChildExampleAttr' => 'This is child node attr',
'SharedNote' => 'This is a shared note used across the tree.',
),
'ChildRefNode' =>
array (
'Name' => 'This Is Child Ref Node',
'ChildNode' =>
array (
'ExampleValue' => 123456,
),
),
),
),
'Items' =>
array (
'ItemA' =>
array (
'Price' => '12,50 zł',
'Available' => false,
),
'ItemB' =>
array (
'Price' => '20,00 zł',
'Available' => true,
),
),
),
)
Automatyczne rzutowanie może powodować nieprawidłową interpretację danych. Jeśli wartości float będą miały przecinek jako miejsce dziesiętne, zostanie to zinterpretowane jako współrzędne. Niektóre atrybuty mogą także zwracać null.
Eksporter TreeStructInfo (TreeStructInfoExporter.php
)
Opis:
Klasa TreeStructInfoExporter
wykonuje odwrotną operację – przekształca obiekt PHP (stdClass
), zawierający dane struktury (version
, name
, data
), z powrotem do formatu TreeStructInfo 2.0. Eksporter generuje tekstową reprezentację drzewa, zachowując oryginalne atrybuty, węzły, typy danych.
Zastosowanie:
- Eksport danych strukturalnych do plików
.tsinfo
, - Generowanie edytowalnych plików tekstowych do dalszej obróbki,
- Przygotowanie danych do parsowania przez inne narzędzia lub aplikacje.
Kod źródłowy eksportera
<?php
/**
* TreeStructInfo PHP Exporter
* Copyright (c) 2025 Dariusz Rorat
* Licensed under the BSD 3-Clause License.
* See the LICENSE.md file for full license text.
*/
class TreeStructInfoExporter
{
private $indent = ' ';
private $refAttributes = [];
/**
* Sets the string used for indentation in the exported output.
*
* @param string $indentString The indentation string (e.g., spaces or tabs).
*/
public function setIndent($indentString)
{
$this->indent = $indentString;
}
/**
* Sets the reference attributes to be appended after the tree structure.
*
* @param array $refs An associative array of reference attribute key-value pairs.
*/
public function setRefAttributes($refs)
{
$this->refAttributes = $refs;
}
/**
* Exports the TreeStructInfo array into a textual representation.
*
* @param array $obj The TreeStructInfo array containing version, name, data, etc.
* @return string The serialized text representation of the tree structure.
* @throws InvalidArgumentException If the input array is invalid or missing version.
*/
public function export($obj)
{
if (!is_array($obj) || empty($obj['version'])) {
throw new InvalidArgumentException("Invalid TreeStructInfo object");
}
$output = [];
// Header
$header = 'treestructinfo "' . $this->escape($obj['version']) . '"';
if (!empty($obj['name'])) {
$header .= ' name "' . $this->escape($obj['name']) . '"';
}
$output[] = $header;
// Tree
$data = isset($obj['data']) ? $obj['data'] : [];
$output = array_merge($output, $this->exportTree($data, 1));
$output[] = 'end tree';
// Reference attributes after tree
foreach ($this->refAttributes as $key => $val) {
$output[] = 'ref attr ' . $key . ' "' . $this->escape($val) . '"';
}
return implode("\n", $output) . "\n";
}
/**
* Recursively exports the tree structure with proper indentation.
*
* @param array $data The tree node or subtree to export.
* @param int $level The indentation level for the current node.
* @return array An array of strings representing the tree structure lines.
*/
private function exportTree($data, $level)
{
$lines = [];
$indent = str_repeat($this->indent, $level);
foreach ($data as $key => $value) {
if (is_array($value) && $this->isAssoc($value)) {
// Treat as node
$lines[] = $indent . 'node ' . $key;
$lines = array_merge($lines, $this->exportTree($value, $level + 1));
$lines[] = $indent . 'end node';
} else {
// Treat as attribute(s)
if (is_array($value)) {
foreach ($value as $v) {
$lines[] = $indent . 'attr ' . $key . ' "' . $this->stringify($v) . '"';
}
} else {
$lines[] = $indent . 'attr ' . $key . ' "' . $this->stringify($value) . '"';
}
}
}
return $lines;
}
/**
* Converts a value to its string representation for export.
*
* @param mixed $value The value to stringify.
* @return string The string representation of the value.
*/
private function stringify($value)
{
if (is_bool($value)) {
return $value ? 'true' : 'false';
} elseif (is_int($value) || is_float($value)) {
return (string)$value;
} elseif (is_string($value)) {
return $this->escape($value);
} else {
return $this->escape((string)$value);
}
}
/**
* Escapes double quotes in a string to be safely used in quoted output.
*
* @param string $str The input string.
* @return string The escaped string.
*/
private function escape($str)
{
return str_replace('"', '\\"', $str);
}
/**
* Determines if an array is associative.
*
* @param array $array The array to check.
* @return bool True if the array is associative, false if it's a list.
*/
private function isAssoc($array)
{
return array_keys($array) !== range(0, count($array) - 1);
}
}
Użycie klasy eksportera
$exporter = new TreeStructInfoExporter();
$exporter->setRefAttributes(['target' => 'sometarget']);
$output = $exporter->export([
'version' => '2.0',
'name' => 'example',
'data' => [
'id' => 'abc123',
'target' => 'node42',
'label' => 'Main Node',
'settings' => [
'source' => 'sensor01',
'threshold' => 0.5
]
]
]);
Wynik eksportu
treestructinfo "2.0" name "example"
attr id "abc123"
attr target "node42"
attr label "Main Node"
node settings
attr source "sensor01"
attr threshold "0,5"
end node
end tree
ref attr target "sometarget"
W tym przypadku widać że brakuje w strukturze drzewa atrybutu referencjonowanego target
i trzeba go później dopisać ręcznie.
Obiektowe mapowanie plików .tsinfo
(Active Record)
Cel i zastosowanie
Klasa TSInfo
służy jako obiektowy interfejs do plików konfiguracyjnych zapisanych w formacie TreeStructInfo 2.0, działając podobnie do wzorca ORM (Object-Relational Mapping) – ale dla danych plikowych, nie relacyjnych. Jej zadaniem jest załadowanie, modyfikacja, zapis i usuwanie plików .tsinfo
w sposób obiektowy, intuicyjny i spójny z filozofią Kohana/Koseven.
Klasa implementuje wzorzec Active Record, co pozwala pracować z danymi w pliku jak z obiektem: modyfikować je, odczytywać, zapisywać i kasować.
Budowa klasy
Klasa składa się z trzech głównych komponentów:
$_meta
– przechowuje metadane (version
,name
)$_data
– dane właściwe użytkownika (klucz-wartość lub zagnieżdżone struktury)$_file
i$_loaded
– kontrola ścieżki i statusu wczytania
<?php defined('SYSPATH') or die('No direct script access.');
/**
* TSInfo class.
*
* @package Kohana
* @category ORM
* @author Dariusz Rorat
* @copyright (c) 2015 Dariusz Rorat
* @license BSD 3-Clause
*/
class Kohana_TSInfo {
protected $_file = NULL;
protected $_loaded = FALSE;
protected $_meta = [];
protected $_data = [];
protected $_config = [
'auto_casting' => TRUE,
'multiline_join_char' => PHP_EOL,
'bool_values' => [
['true'],
['false'],
],
];
/**
* Create a new TSInfo instance.
*
* $helper = TSInfo::factory($name);
*
* @param string helper name
* @return TSInfo
*/
public static function factory($file)
{
return new TSInfo($file);
}
public function __construct($file)
{
$this->_file = $file;
$this->load();
}
public function config($config)
{
$this->_config = $config;
return $this;
}
public function load()
{
if (!realpath($this->_file))
{
$this->_loaded = false;
return;
}
include_once Kohana::find_file('vendor/tsinfo', 'parser');
$string = file_get_contents($this->_file);
$parser = new TreeStructInfoParser();
$parser->setAutoCasting(Arr::get($this->_config, 'auto_casting', false));
$parser->setMultilineJoinChar(Arr::get($this->_config, 'multiline_join_char', ''));
$parser->setBoolValues(Arr::path($this->_config, 'bool_values.0', ['true']),
Arr::path($this->_config, 'bool_values.1', ['false']));
$data = $parser->parse($string);
Arr::set_path($this->_meta, 'version', Arr::get($data, 'version', '2.0'));
Arr::set_path($this->_meta, 'name', Arr::get($data, 'name'));
$this->set(Arr::get($data, 'data', []));
$this->_loaded = true;
}
public function save()
{
include_once Kohana::find_file('vendor/tsinfo', 'exporter');
$data = [
'version' => Arr::get($this->_meta, 'version', '2.0'),
'name' => Arr::get($this->_meta, 'name'),
'data' => json_decode(json_encode($this->_data), true),
];
$exporter = new TreeStructInfoExporter();
$output = $exporter->export($data);
file_put_contents($this->_file, $output);
}
public function delete()
{
if (realpath($this->_file))
{
unlink($this->_file);
}
}
public function loaded()
{
return $this->_loaded;
}
public function meta()
{
return $this->_meta;
}
public function data()
{
return $this->_data;
}
public function as_object()
{
return Arr::to_object($this->_data);
}
public function as_array()
{
return Arr::to_array($this->_data);
}
public function get($key, $default = NULL)
{
return Arr::get($this->_data, $key, $default);
}
public function path($path, $default = NULL, $delimiter = NULL)
{
return Arr::path($this->_data, $path, $default, $delimiter);
}
public function set($key, $value = NULL)
{
if (is_array($key))
{
$this->_data = $key;
} else
{
$this->_data[$key] = $value;
}
return $this;
}
public function set_path($path, $value, $delimiter = NULL)
{
Arr::set_path($this->_data, $path, $value, $delimiter);
return $this;
}
public function bind($key, & $value)
{
$this->_data[$key] =& $value;
return $this;
}
public function __set($key, $value)
{
$this->set($key, $value);
}
public function & __get($key)
{
if (array_key_exists($key, $this->_data))
{
return $this->_data[$key];
} else
{
throw new Kohana_Exception('Data variable is not set: :var', array(':var' => $key));
}
}
public function __isset($key)
{
return isset($this->_data[$key]);
}
}// End TSInfo
Kluczowe metody i ich działanie
public static function factory($file)
Fabryka tworząca instancję klasy na podstawie ścieżki do pliku .tsinfo
.
$tsinfo = Kohana_TSInfo::factory('/configs/site.tsinfo');
public function config()
Ustawia konfigurację parsera TreeStructInfo. Domyślne ustawienia to:
auto_casting = false
multiline_join_char = PHP_EOL
bool_values = [['true'], ['false']]
public function load()
Ładuje dane z pliku i parsuje je do formy obiektowej. Używa TreeStructInfoParser
, który przetwarza strukturę tekstową .tsinfo
na obiekt PHP (array
+ version
+ name
). Automatyczne rzutowanie danych (true
, 123
, itp.) jest domyślnie włączone.
public function save()
Zapisuje bieżący stan danych obiektu do pliku .tsinfo
, korzystając z klasy TreeStructInfoExporter
. Dane są eksportowane w pełnej formie version
, name
, data
.
public function delete()
Usuwa plik z systemu plików, jeśli istnieje.
public function get($key)
i public function path($path)
Służą do odczytu danych z obiektu – get
to bezpośredni klucz, path
wspiera zagnieżdżenia (np. user.profile.name
).
public function set($key, $value)
oraz set_path($path, $value)
Pozwalają ustawić dane w obiekcie – zarówno płaskie (set
) jak i zagnieżdżone (set_path
).
public function bind($key, &$value)
Wiążę referencję zmiennej z danym kluczem – zmiana wartości w zmiennej automatycznie aktualizuje dane w obiekcie.
public function meta()
i data()
Zwracają odpowiednio metadane (version
, name
) oraz dane użytkownika ($_data
).
public function as_object()
i as_array()
Konwertują dane obiektu do odpowiednich form:
as_array()
– tablica asocjacyjnaas_object()
– obiekt (stdClass
) dla łatwego dostępu właściwościowego
Dostęp magiczny
Klasa wspiera dostęp do danych za pomocą metod magicznych:
$tsinfo->title = 'Nowy tytuł'; // __set
echo $tsinfo->title; // __get
isset($tsinfo->title); // __isset
Zależności i wymagania
- Kohana/Koseven
- Klasy
TreeStructInfoParser
orazTreeStructInfoExporter
- Pomocnicze funkcje z
Arr
(Arr::get
,Arr::set_path
,Arr::to_object
, itd.)
Przykład użycia
$tsinfo = Kohana_TSInfo::factory(APPPATH.'configs/site.tsinfo');
if ($tsinfo->loaded()) {
echo $tsinfo->get('title');
$tsinfo->set_path('meta.author', 'Jan Kowalski');
$tsinfo->flag = true;
$tsinfo->save();
}
Koseven + TSInfo: Elastyczne zarządzanie konfiguracją
Praca z konfiguracjami w Koseven (forku Kohany) bywa prosta, dopóki nie pojawi się potrzeba bardziej zaawansowanego formatu – np. konfiguracji opartej o drzewa, z obsługą typów danych, wartości wieloliniowych czy cachowania. W odpowiedzi na takie potrzeby powstała klasa Kohana_Config_TSInfo_Reader
, która pozwala na integrację parsera TSInfo z mechanizmem Kohana_Config
.
Czym jest Config_TSInfo_Reader
?
Kohana_Config_TSInfo_Reader
to czytnik konfiguracji, który korzysta z parsera TreeStructInfo (TSInfo) – narzędzia pozwalającego odczytywać pliki konfiguracyjne z bogatszą strukturą danych. Klasa ta implementuje interfejs Kohana_Config_Reader
, dzięki czemu może być łatwo podpięta do systemu konfiguracji Koseven.
Kod źródłowy klasy
Klasa Kohana_Config_TSInfo
<?php
/**
* TSINfo-based configuration reader. Multiple configuration directories can be
* used by attaching multiple instances of this class to [Config].
*
* @package Kohana
* @category Configuration
* @author Dariusz Rorat
* @copyright (c) Kohana Team
* @license BSD 3-Clause
*/
class Kohana_Config_TSInfo extends Kohana_Config_TSInfo_Reader
{
}
Klasa Kohana_Config_TSInfo_Reader
<?php
/**
* TSInfo-based configuration reader. Multiple configuration directories can be
* used by attaching multiple instances of this class to [Kohana_Config].
*
* @package Kohana
* @category Configuration
* @author Dariusz Rorat
* @copyright (c) Kohana Team
* @license BSD 3-Clause
*/
class Kohana_Config_TSInfo_Reader implements Kohana_Config_Reader {
/**
* The directory where config files are located
* @var string
*/
protected $_directory = '';
protected $_cache_dir = '';
protected $_auto_casting = TRUE;
protected $_multiline_join_char = '';
protected $_bool_values = [];
protected $_parser;
/**
* Creates a new tsinfo reader using the given directory as a config source
*
* @param string $directory Configuration directory to search
*/
public function __construct($directory = 'config')
{
// Set the configuration directory name
$this->_directory = trim($directory, '/');
include_once Kohana::find_file('vendor/tsinfo', 'parser');
$config = [];
$files = Kohana::find_file($this->_directory, 'tsinfo', NULL, TRUE);
foreach ($files as $file)
{
$data = Kohana::load($file);
$config = Arr::merge($config, $data);
}
$this->_cache_dir = Arr::path($config, 'parser.cache_dir');
$this->_auto_casting = Arr::path($config, 'parser.auto_casting');
$this->_multiline_join_char = Arr::path($config, 'parser.multiline_join_char');
$this->_bool_values = Arr::path($config, 'parser.bool_values');
$this->_parser = new TreeStructInfoParser();
$this->_parser->setAutoCasting($this->_auto_casting);
$this->_parser->setMultilineJoinChar($this->_multiline_join_char);
$this->_parser->setBoolValues(Arr::get($this->_bool_values, 0, ['true']), Arr::get($this->_bool_values, 1, ['false']));
}
/**
* Load and merge all of the configuration files in this group.
*
* $config->load($name);
*
* @param string $group configuration group name
* @return $this current object
* @uses Kohana::load
*/
public function load($group)
{
$config = [];
if ($files = Kohana::find_file($this->_directory, $group, 'tsinfo', TRUE))
{
$i = 0;
foreach ($files as $file)
{
$cache_file = $this->_cache_dir . DIRECTORY_SEPARATOR . $group . '_' . $i . '.cache';
$tree = $this->_parser->parseWithCache($file, $cache_file);
$data = Arr::get($tree, 'data', []);
// Merge each file to the configuration array
$config = Arr::merge($config, $data);
$i++;
}
}
return $config;
}
}
Możliwości i funkcje klasy
Wsparcie dla wielu plików konfiguracyjnych
Klasa wykorzystuje Kohana::find_file
, aby odszukać wszystkie pliki konfiguracyjne o podanym prefiksie i rozszerzeniu .tsinfo
, a następnie scala ich dane w jedną strukturę PHP. Dzięki temu możesz rozdzielać konfigurację na mniejsze moduły lub środowiska.
Automatyczne castowanie typów
Parser TSInfo potrafi automatycznie rzutować wartości do odpowiednich typów PHP (np. true
, false
, 123
, 1.23
). Obsługuje też konfigurowalne wartości logiczne (bool_values
), co pozwala dopasować parser do niestandardowych reprezentacji booleanów.
Obsługa wartości wieloliniowych
Dzięki opcji multiline_join_char
, parser pozwala zachować czytelność plików konfiguracyjnych i scalać wieloliniowe wpisy do pojedynczych wartości.
Cache dla szybszego parsowania
Pliki są parsowane z wykorzystaniem mechanizmu cache – parser generuje pliki .cache
, które są przechowywane w katalogu wskazanym w konfiguracji. To znacznie przyspiesza odczyt konfiguracji przy kolejnych uruchomieniach.
Przykład konfiguracji
Plik config/tsinfo.php
dostarcza ustawień do inicjalizacji parsera:
return [
'parser' => [
'cache_dir' => APPPATH . 'storage/cache/tsinfo',
'auto_casting' => TRUE,
'multiline_join_char' => PHP_EOL,
'bool_values' => [
['true'], // wartości uznawane za TRUE
['false'], // wartości uznawane za FALSE
],
],
];
Jak to działa pod maską?
- Konstruktor klasy wczytuje konfigurację TSInfo (z pliku
tsinfo.php
) i inicjalizuje parser. - Metoda
load($group)
wyszukuje wszystkie pliki.tsinfo
dla danegogroup
, parsuje je i scala ich zawartość. - Parser korzysta z pliku cache, jeśli jest dostępny, aby uniknąć wielokrotnego parsowania tych samych danych.
Przykład użycia
Aby korzystać z Config_TSInfo_Reader
, należy zarejestrować go jako źródło konfiguracji w swoim bootstrapie:
Kohana::$config->attach(new Kohana_Config_TSInfo('config'));
Następnie można korzystać z konfiguracji jak zwykle:
$config = Kohana::$config->load('example');
echo $config['my_setting'];
Bezpieczeństwo i dobre praktyki
- Upewnij się, że katalog cache jest zapisywalny i nieudostępniany publicznie.
- Konfiguracja TSInfo nie powinna zawierać danych wrażliwych – jeśli musisz przechowywać hasła lub tokeny, rozważ szyfrowanie lub użycie zmiennych środowiskowych.
Dokumentacja
http://www.dariuszrorat.ugu.pl/dokumenty/14-dokumentacja-treestructinfoparser-php
http://www.dariuszrorat.ugu.pl/dokumenty/15-dokumentacja-treestructinfoexporter-php
BSD-3-Clause License Agreement
BSD-3-Clause
Сopyright (c) 2025 Dariusz Rorat
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. -
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. - Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Podsumowanie
Zestaw tych dwóch klas pozwala z łatwością wczytywać i generować dane w formacie TreeStructInfo bez potrzeby korzystania z zewnętrznych bibliotek. Dzięki temu możesz integrować TreeStructInfo w swoich aplikacjach PHP – zarówno do odczytu danych, jak i ich zapisu, debugowania czy dalszego przetwarzania (np. konwersji do YAML lub JSON). To lekkie i niezależne rozwiązanie, gotowe do użycia w Twoich projektach.
Klasa TSInfo
pełni funkcję warstwy obiektowego dostępu do danych TreeStructInfo – dokładnie tak, jak ORM pełni funkcję dostępu do danych w bazach danych. Pozwala na:
- odczyt/zapis danych z plików
.tsinfo
- pracę na danych jak na obiektach (z możliwością rzutowania)
- integrację z aplikacjami opartymi na Kohana/Koseven
- czysty i skalowalny kod
To idealne rozwiązanie, gdy chcesz przechowywać konfiguracje lub dane strukturalne bez konieczności użycia bazy danych – w formacie czytelnym zarówno dla człowieka, jak i maszyny.
Kohana_Config_TSInfo_Reader
to eleganckie rozszerzenie systemu konfiguracji Koseven. Dzięki integracji z parserem TSInfo zyskujemy:
- większą elastyczność struktury plików konfiguracyjnych,
- automatyczne rzutowanie wartości,
- obsługę wieloliniowych danych,
- przyspieszenie dzięki cache'owaniu,
- i pełną integrację z
Kohana_Config
.
To świetne narzędzie dla tych, którzy potrzebują czegoś więcej niż klasyczny format .php
w konfiguracjach aplikacji PHP.
Strona formatu TreeStructInfo:
https://tsinfo.4programmers.net/pl/index.htm
Moja aplikacja online do testowania formatu:
http://www.dariuszrorat.ugu.pl/aplikacje/treestructinfo-tester
Kody źródłowe do pobrania:
http://www.dariuszrorat.ugu.pl/uploads/media/2025/07/tsinfo.zip