Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Biblioteka TreeStructInfo PHP

Logo TreeStructInfo

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
Ważne
  • 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,
      ),
    ),
  ),
)
Ostrzeżenie

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"
Notatka

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 asocjacyjna
  • as_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 oraz TreeStructInfoExporter
  • 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 danego group, 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:

  1. Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

  2. 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.

  3. 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

15 lipca 2025 16

Kategorie

programowanie

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.