<?php
error_reporting(-1);

class Tree
{
    const FORMAT_TEXT = 0;
    const FORMAT_HTML = 1;

    protected $rootIds;
    
    protected $template;
    
    /** @var array Template, indexed by nodes ids */
    protected $nodes;
    
    protected $adjacencyList;
    
    
    public function __construct($template)
    {
        $this->setTemplate($template);
    }
    
    public function setTemplate($template)
    {
        $this->template = $template;
        $this->nodes = $this->getNodes($template);
        $this->rootIds = $this->getRootIds($template);
        $this->adjacencyList = $this->buildAdjacencyList($template);
    }
    
    public function format($format)
    {
        $output = '';
        
        foreach ($this->rootIds as $rootId) {
            $this->walkTree($this->nodes[$rootId], $output, $format);
        }
        
        return $output;
    }
    
    public function __toString()
    {
        $this->format(static::FORMAT_TEXT);
    }
    
    protected function buildAdjacencyList($template)
    {
        $adjacencyList = [];
        
        foreach ($template as $node) {
            if ($node['parentId'] !== null) {
                if (isset($adjacencyList[$node['parentId']])) {
                    $adjacencyList[$node['parentId']][] = $node['id'];
                } else {
                    $adjacencyList[$node['parentId']] = [$node['id']];
                }
            }
        }
        
        return $adjacencyList;
    }
    
    protected function getNodes($template)
    {
        $nodes = [];
        
        foreach ($template as $node) {
            $nodes[$node['id']] = $node;
        }
        
        return $nodes;
    }
    
    protected function getRootIds($template)
    {
        $rootIds = [];
        
        foreach ($template as $node) {
            if ($node['parentId'] === null) {
                $rootIds[] = $node['id'];
            }
        }
        
        if ($rootIds !== []) {
            return $rootIds;
        } else {
            throw new Exception('Parent node do not exists.');
        }
    }

    protected function walkTree($node, &$output, $format, $depth = 0)
    {
        switch ($format) {
            case Tree::FORMAT_TEXT:
                $output .= $this->formatNodeAsText($node['text'], $depth);
                break;
            case Tree::FORMAT_HTML:
                $output .= $this->formatNodeAsHtml($node['text'], $depth);
                break;
            default:
                $output .= $this->formatNodeAsText($node['text'], $depth);
        }
        $depth += 1;
        
        if (isset($this->adjacencyList[$node['id']])) {
        	foreach ($this->adjacencyList[$node['id']] as $childId) {
            	$this->walkTree($this->nodes[$childId], $output, $format, $depth);
        	}
        }
    }
    
    protected function formatNodeAsText($nodeText, $depth)
    {
        return str_repeat('    ', $depth) . $nodeText . "\r\n";
    }
    
    protected function formatNodeAsHtml($nodeText, $depth)
    {
        return '<p class="offset-' . $depth . '">' . $nodeText . '</p>';
    }
}

$template = array(
    array("id" => 1, "parentId" => null, "text" => "Первая строка"), 
    array("id" => 2, "parentId" => 3,    "text" => "Вторая строка"), 
    array("id" => 3, "parentId" => 1,    "text" => "Третья строка"),
    array("id" => 4, "parentId" => 1,    "text" => "Четвертая строка"),
    
    array("id" => 5, "parentId" => null, "text" => "Пятая строка"), 
    array("id" => 6, "parentId" => 5,    "text" => "Шестая строка")
);

$tree = new Tree($template);
echo $tree->format(Tree::FORMAT_TEXT);