├── renovate.json ├── phpbench.json.dist ├── benchmarks ├── AbstractBench.php └── DrupolPhpTreeBench.php ├── phpspec.yml.dist ├── grumphp.yml.dist ├── src ├── Builder │ ├── BuilderInterface.php │ └── Random.php ├── Node │ ├── ValueNodeInterface.php │ ├── MerkleNodeInterface.php │ ├── NaryNodeInterface.php │ ├── ValueNode.php │ ├── ABNode.php │ ├── AttributeNodeInterface.php │ ├── AttributeNode.php │ ├── TrieNode.php │ ├── MerkleNode.php │ ├── NaryNode.php │ ├── NodeInterface.php │ └── Node.php ├── Exporter │ ├── ExporterInterface.php │ ├── Text.php │ ├── SimpleArray.php │ ├── Ascii.php │ ├── Graph.php │ ├── Image.php │ └── Gv.php ├── Importer │ ├── ImporterInterface.php │ ├── SimpleArray.php │ ├── MicrosoftTolerantPhpParser.php │ ├── NikicPhpAst.php │ ├── NikicPhpParser.php │ └── Text.php ├── Traverser │ ├── TraverserInterface.php │ ├── BreadthFirst.php │ ├── PostOrder.php │ ├── PreOrder.php │ └── InOrder.php └── Modifier │ ├── Reverse.php │ ├── ModifierInterface.php │ ├── Apply.php │ ├── Filter.php │ ├── FulfillCapacity.php │ └── RemoveNullNode.php ├── phpstan.neon ├── infection.json.dist └── composer.json /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /phpbench.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap": "vendor/autoload.php", 3 | "path": "benchmarks", 4 | "xml_storage_path": "build/benchmarks", 5 | "progress": "verbose", 6 | "extensions": [ 7 | "PhpBench\\Extensions\\XDebug\\XDebugExtension" 8 | ] 9 | } -------------------------------------------------------------------------------- /benchmarks/AbstractBench.php: -------------------------------------------------------------------------------- 1 | > $nodes 18 | */ 19 | public static function create(iterable $nodes): ?NodeInterface; 20 | } 21 | -------------------------------------------------------------------------------- /src/Node/ValueNodeInterface.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function traverse(NodeInterface $node): Traversable; 30 | } 31 | -------------------------------------------------------------------------------- /src/Modifier/Reverse.php: -------------------------------------------------------------------------------- 1 | children() as $child) { 22 | $children->append($this->modify($child)); 23 | } 24 | 25 | return $tree->withChildren(...array_reverse( 26 | $children->getArrayCopy() 27 | )); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exporter/Text.php: -------------------------------------------------------------------------------- 1 | children() as $child) { 22 | $children[] = $this->export($child); 23 | } 24 | 25 | return [] === $children ? 26 | sprintf('[%s]', $node->label()) : 27 | sprintf('[%s%s]', $node->label(), implode('', $children)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exporter/SimpleArray.php: -------------------------------------------------------------------------------- 1 | children() as $child) { 22 | $children[] = $this->export($child); 23 | } 24 | 25 | return [] === $children ? 26 | ['value' => $node->label()] : 27 | ['value' => $node->label(), 'children' => $children]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Traverser/BreadthFirst.php: -------------------------------------------------------------------------------- 1 | enqueue($node); 22 | 23 | yield $node; 24 | 25 | while (0 < $queue->count()) { 26 | foreach ($queue->dequeue()->children() as $child) { 27 | $queue->enqueue($child); 28 | 29 | yield $child; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Node/NaryNodeInterface.php: -------------------------------------------------------------------------------- 1 | value = $value; 27 | } 28 | 29 | public function getValue() 30 | { 31 | return $this->value; 32 | } 33 | 34 | public function label(): string 35 | { 36 | return (string) $this->getValue(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /benchmarks/DrupolPhpTreeBench.php: -------------------------------------------------------------------------------- 1 | tree = new ValueNode('root', 2); 29 | 30 | foreach ($this->getData() as $value) { 31 | $this->tree->add(new ValueNode($value, 2)); 32 | } 33 | } 34 | 35 | /** 36 | * Init the object. 37 | */ 38 | public function initObject(): void 39 | { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Traverser/PostOrder.php: -------------------------------------------------------------------------------- 1 | index = 0; 25 | 26 | return $this->doTraverse($node); 27 | } 28 | 29 | /** 30 | * @return Traversable 31 | */ 32 | private function doTraverse(NodeInterface $node): Traversable 33 | { 34 | foreach ($node->children() as $child) { 35 | yield from $this->doTraverse($child); 36 | ++$this->index; 37 | } 38 | 39 | yield $this->index => $node; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Traverser/PreOrder.php: -------------------------------------------------------------------------------- 1 | index = 0; 25 | 26 | return $this->doTraverse($node); 27 | } 28 | 29 | /** 30 | * @return Traversable 31 | */ 32 | private function doTraverse(NodeInterface $node): Traversable 33 | { 34 | yield $this->index => $node; 35 | 36 | foreach ($node->children() as $child) { 37 | ++$this->index; 38 | 39 | yield from $this->doTraverse($child); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Modifier/Apply.php: -------------------------------------------------------------------------------- 1 | apply = $apply; 31 | $this->traverser = $traverser ?? new PostOrder(); 32 | } 33 | 34 | public function modify(NodeInterface $tree): NodeInterface 35 | { 36 | foreach ($this->traverser->traverse($tree) as $item) { 37 | ($this->apply)($item); 38 | } 39 | 40 | return $tree; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Node/ABNode.php: -------------------------------------------------------------------------------- 1 | count()) { 18 | parent::add($node); 19 | 20 | continue; 21 | } 22 | 23 | $count = []; 24 | 25 | foreach ($this->children() as $child) { 26 | $count[$child->count()] = $child; 27 | } 28 | 29 | $keys = array_keys($count); 30 | $keys[] = 0; 31 | 32 | if (min($keys) === max($keys)) { 33 | parent::add($node); 34 | 35 | continue; 36 | } 37 | 38 | ksort($count); 39 | 40 | if (null !== $child = array_shift($count)) { 41 | $child->add($node); 42 | } 43 | } 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Traverser/InOrder.php: -------------------------------------------------------------------------------- 1 | index = 0; 25 | 26 | return $this->doTraverse($node); 27 | } 28 | 29 | /** 30 | * @return Traversable 31 | */ 32 | private function doTraverse(NodeInterface $node): Traversable 33 | { 34 | $middle = floor($node->degree() / 2); 35 | 36 | foreach ($node->children() as $key => $child) { 37 | if ((int) $key === (int) $middle) { 38 | yield $this->index => $node; 39 | ++$this->index; 40 | } 41 | 42 | if ($child->isLeaf()) { 43 | yield $this->index => $child; 44 | ++$this->index; 45 | } 46 | 47 | yield from $this->doTraverse($child); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Modifier/Filter.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 32 | $this->traverser = $traverser ?? new PostOrder(); 33 | } 34 | 35 | public function modify(NodeInterface $tree): NodeInterface 36 | { 37 | foreach ($this->traverser->traverse($tree) as $item) { 38 | if (null === $parent = $item->getParent()) { 39 | continue; 40 | } 41 | 42 | if (!(bool) ($this->filter)($item)) { 43 | continue; 44 | } 45 | 46 | $parent->remove($item); 47 | } 48 | 49 | return $tree; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Modifier/FulfillCapacity.php: -------------------------------------------------------------------------------- 1 | traverser = $traverser ?? new PostOrder(); 28 | } 29 | 30 | public function modify(NodeInterface $tree): NodeInterface 31 | { 32 | /** @var NaryNodeInterface $item */ 33 | foreach ($this->traverser->traverse($tree) as $item) { 34 | $capacity = $item->capacity(); 35 | 36 | if (!$item->isLeaf()) { 37 | $children = iterator_to_array($item->children()); 38 | 39 | while ($item->degree() !== $capacity) { 40 | $item->add(current($children)->clone()); 41 | } 42 | } 43 | } 44 | 45 | return $tree; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Modifier/RemoveNullNode.php: -------------------------------------------------------------------------------- 1 | traverser = $traverser ?? new PostOrder(); 28 | } 29 | 30 | public function modify(NodeInterface $tree): NodeInterface 31 | { 32 | /** @var ValueNodeInterface $item */ 33 | foreach ($this->traverser->traverse($tree) as $item) { 34 | if (null === $parent = $item->getParent()) { 35 | continue; 36 | } 37 | 38 | if (!$item->isLeaf()) { 39 | continue; 40 | } 41 | 42 | if (null !== $item->getValue()) { 43 | continue; 44 | } 45 | 46 | $parent->remove($item); 47 | } 48 | 49 | return $tree; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Node/AttributeNodeInterface.php: -------------------------------------------------------------------------------- 1 | 29 | * The attributes. 30 | */ 31 | public function getAttributes(): array; 32 | 33 | /** 34 | * Set an attribute. 35 | * 36 | * @param string $key 37 | * The attribute key. 38 | * @param mixed $value 39 | * The attribute value. 40 | * 41 | * @return \loophp\phptree\Node\AttributeNodeInterface 42 | * The node. 43 | */ 44 | public function setAttribute(string $key, $value): AttributeNodeInterface; 45 | 46 | /** 47 | * Set the attributes. 48 | * 49 | * @param array $attributes 50 | * The attributes. 51 | * 52 | * @return \loophp\phptree\Node\AttributeNodeInterface 53 | * The node. 54 | */ 55 | public function setAttributes(array $attributes): AttributeNodeInterface; 56 | } 57 | -------------------------------------------------------------------------------- /src/Node/AttributeNode.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $attributes; 20 | 21 | public function __construct( 22 | array $attributes = [], 23 | int $capacity = 0, 24 | ?TraverserInterface $traverser = null, 25 | ?NodeInterface $parent = null 26 | ) { 27 | parent::__construct($capacity, $traverser, $parent); 28 | 29 | $this->attributes = $attributes; 30 | } 31 | 32 | public function getAttribute(string $key) 33 | { 34 | return $this->getAttributes()[$key] ?? null; 35 | } 36 | 37 | public function getAttributes(): array 38 | { 39 | return $this->attributes; 40 | } 41 | 42 | public function label(): string 43 | { 44 | return (string) $this->getAttribute('label'); 45 | } 46 | 47 | public function setAttribute(string $key, $value): AttributeNodeInterface 48 | { 49 | $this->attributes[$key] = $value; 50 | 51 | return $this; 52 | } 53 | 54 | public function setAttributes(array $attributes): AttributeNodeInterface 55 | { 56 | $this->attributes = $attributes; 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Importer/SimpleArray.php: -------------------------------------------------------------------------------- 1 | parseNode(new AttributeNode(['label' => 'root']), $data); 21 | } 22 | 23 | /** 24 | * Create a node. 25 | * 26 | * @param mixed $data 27 | * The arguments 28 | * 29 | * @return AttributeNodeInterface 30 | * The node 31 | */ 32 | private function createNode($data): AttributeNodeInterface 33 | { 34 | return new AttributeNode([ 35 | 'data' => $data, 36 | ]); 37 | } 38 | 39 | private function parseNode(AttributeNodeInterface $parent, array ...$nodes): NodeInterface 40 | { 41 | return array_reduce( 42 | $nodes, 43 | function (AttributeNodeInterface $carry, array $node): NodeInterface { 44 | $node += ['children' => []]; 45 | 46 | return $carry 47 | ->add( 48 | $this->parseNode( 49 | $this->createNode($node), 50 | ...$node['children'] 51 | ) 52 | ); 53 | }, 54 | $parent 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Exporter/Ascii.php: -------------------------------------------------------------------------------- 1 | export($node) 28 | ), 29 | RecursiveTreeIterator::SELF_FIRST, 30 | CachingIterator::CATCH_GET_CHILD, 31 | RecursiveTreeIterator::SELF_FIRST 32 | ); 33 | 34 | $tree->setPrefixPart(RecursiveTreeIterator::PREFIX_LEFT, ''); 35 | $tree->setPrefixPart(RecursiveTreeIterator::PREFIX_END_HAS_NEXT, '├─'); 36 | $tree->setPrefixPart(RecursiveTreeIterator::PREFIX_END_LAST, '└─'); 37 | $tree->setPrefixPart(RecursiveTreeIterator::PREFIX_RIGHT, ''); 38 | $tree->setPrefixPart(RecursiveTreeIterator::PREFIX_MID_LAST, ' '); 39 | $tree->setPrefixPart(RecursiveTreeIterator::PREFIX_MID_HAS_NEXT, '│ '); 40 | 41 | $output = ''; 42 | 43 | foreach ($tree as $value) { 44 | $entry = ('Array' === $entry = $tree->getEntry()) ? 45 | '┐' : 46 | ' ' . $entry; 47 | 48 | $output .= $tree->getPrefix() . $entry . $tree->getPostfix() . PHP_EOL; 49 | } 50 | 51 | return $output; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Importer/MicrosoftTolerantPhpParser.php: -------------------------------------------------------------------------------- 1 | parseNode($this->createNode(['label' => 'root']), $data); 29 | } 30 | 31 | private function createNode(array $attributes): AttributeNodeInterface 32 | { 33 | return new AttributeNode($attributes); 34 | } 35 | 36 | private function parseNode(AttributeNodeInterface $parent, Node ...$astNodes): NodeInterface 37 | { 38 | return array_reduce( 39 | $astNodes, 40 | function (AttributeNodeInterface $carry, Node $astNode): NodeInterface { 41 | return $carry 42 | ->add( 43 | $this->parseNode( 44 | $this->createNode([ 45 | 'label' => $astNode->getNodeKindName(), 46 | 'astNode' => $astNode, 47 | ]), 48 | ...$astNode->getChildNodes() 49 | ) 50 | ); 51 | }, 52 | $parent 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Importer/NikicPhpAst.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private array $metadata; 26 | 27 | /** 28 | * @param Node $data 29 | * 30 | * @throws Exception 31 | */ 32 | public function import($data): NodeInterface 33 | { 34 | $this->metadata = get_metadata(); 35 | 36 | return $this->parseNode($this->createNode(['label' => 'root']), $data); 37 | } 38 | 39 | private function createNode(array $attributes): AttributeNodeInterface 40 | { 41 | return new AttributeNode($attributes); 42 | } 43 | 44 | private function parseNode(AttributeNodeInterface $parent, Node ...$astNodes): NodeInterface 45 | { 46 | return array_reduce( 47 | $astNodes, 48 | function (AttributeNodeInterface $carry, Node $astNode): NodeInterface { 49 | return $carry 50 | ->add( 51 | $this->parseNode( 52 | $this->createNode([ 53 | 'label' => $this->metadata[$astNode->kind]->name, 54 | 'astNode' => $astNode, 55 | ]), 56 | ...array_values(array_filter($astNode->children, 'is_object')) 57 | ) 58 | ); 59 | }, 60 | $parent 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Exporter/Graph.php: -------------------------------------------------------------------------------- 1 | all() as $node_visited) { 24 | $vertexFrom = $this->createVertex($node_visited, $graph); 25 | 26 | foreach ($node_visited->children() as $child) { 27 | $vertexTo = $this->createVertex($child, $graph); 28 | $vertexFrom->createEdgeTo($vertexTo); 29 | } 30 | } 31 | 32 | return $graph; 33 | } 34 | 35 | /** 36 | * Create a vertex. 37 | * 38 | * @param NodeInterface $node 39 | * The node 40 | * 41 | * @return Vertex 42 | * A vertex 43 | */ 44 | private function createVertex(NodeInterface $node, OriginalGraph $graph): Vertex 45 | { 46 | /** @var int $vertexId */ 47 | $vertexId = $this->createVertexId($node); 48 | 49 | if (!$graph->hasVertex($vertexId)) { 50 | $vertex = $graph->createVertex($vertexId); 51 | 52 | $vertex->setAttribute( 53 | 'graphviz.label', 54 | $node->label() 55 | ); 56 | 57 | if ($node instanceof AttributeNodeInterface) { 58 | foreach ($node->getAttributes() as $key => $value) { 59 | $vertex->setAttribute((string) $key, $value); 60 | } 61 | } 62 | } 63 | 64 | return $graph->getVertex($vertexId); 65 | } 66 | 67 | private function createVertexId(NodeInterface $node): string 68 | { 69 | return sha1(spl_object_hash($node)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Node/TrieNode.php: -------------------------------------------------------------------------------- 1 | getValue(); 23 | 24 | $hash = hash('sha256', $key . $data); 25 | 26 | $node = new self([$hash, mb_substr($data, 0, 1)]); 27 | $parent = $this->append($node); 28 | 29 | $dataWithoutFirstLetter = mb_substr($data, 1); 30 | 31 | if ('' < $dataWithoutFirstLetter) { 32 | $parent->add(new self([$hash, $dataWithoutFirstLetter])); 33 | } else { 34 | $values = [$node->getValue()[1]]; 35 | 36 | /** @var ValueNode $ancestor */ 37 | foreach ($node->getAncestors() as $ancestor) { 38 | $values[] = $ancestor->getValue()[1]; 39 | } 40 | array_pop($nodes); 41 | $node->append(new self([$hash, $this->mbStrRev(implode('', $values))])); 42 | } 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @throws Exception 50 | * 51 | * @return NodeInterface|ValueNodeInterface 52 | */ 53 | private function append(ValueNodeInterface $node) 54 | { 55 | /** @var ValueNodeInterface $child */ 56 | foreach ($this->children() as $child) { 57 | if ($node->getValue()[1] === $child->getValue()[1]) { 58 | return $child; 59 | } 60 | } 61 | 62 | parent::add($node); 63 | 64 | return $node; 65 | } 66 | 67 | private function mbStrRev(string $string): string 68 | { 69 | $chars = mb_str_split($string); 70 | 71 | return implode('', array_reverse($chars)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Builder/Random.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | if (0 === $key) { 24 | $root = self::createNode($value); 25 | 26 | continue; 27 | } 28 | 29 | if (!$root instanceof NodeInterface) { 30 | continue; 31 | } 32 | 33 | self::pickRandomNode($root)->add(self::createNode($value)); 34 | } 35 | 36 | return $root; 37 | } 38 | 39 | /** 40 | * @param array $parameters 41 | */ 42 | private static function createNode(array $parameters = []): NodeInterface 43 | { 44 | $parameters = array_map( 45 | /** 46 | * @param class-string|callable():(NodeInterface)|mixed $parameter 47 | * 48 | * @return class-string|mixed 49 | */ 50 | static function ($parameter) { 51 | if (is_callable($parameter)) { 52 | return $parameter(); 53 | } 54 | 55 | return $parameter; 56 | }, 57 | $parameters 58 | ); 59 | 60 | $class = array_shift($parameters); 61 | 62 | return new $class(...$parameters); 63 | } 64 | 65 | private static function pickRandomNode(NodeInterface $node): NodeInterface 66 | { 67 | $pick = (int) random_int(0, $node->count()); 68 | 69 | $i = 0; 70 | 71 | foreach ($node->all() as $child) { 72 | if (++$i === $pick) { 73 | return $child; 74 | } 75 | } 76 | 77 | return $node; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Exporter/Image.php: -------------------------------------------------------------------------------- 1 | executable = 'dot.exe'; 33 | } 34 | } 35 | 36 | /** 37 | * @throws Exception 38 | */ 39 | public function export(NodeInterface $node): string 40 | { 41 | if (!($tmp = tempnam(sys_get_temp_dir(), 'phptree-export-'))) { 42 | return ''; 43 | } 44 | 45 | file_put_contents($tmp, (new Gv())->export($node)); 46 | 47 | return (string) shell_exec($this->getConvertCommand($tmp)); 48 | } 49 | 50 | /** 51 | * Get the executable to use. 52 | */ 53 | public function getExecutable(): string 54 | { 55 | return $this->executable; 56 | } 57 | 58 | public function getFormat(): string 59 | { 60 | return $this->format; 61 | } 62 | 63 | /** 64 | * Change the executable to use. 65 | * 66 | * @return \loophp\phptree\Exporter\Image 67 | */ 68 | public function setExecutable(string $executable): self 69 | { 70 | $this->executable = $executable; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return \loophp\phptree\Exporter\Image 77 | */ 78 | public function setFormat(string $format): self 79 | { 80 | $this->format = $format; 81 | 82 | return $this; 83 | } 84 | 85 | private function getConvertCommand(string $path): string 86 | { 87 | return sprintf( 88 | '%s -T%s %s', 89 | $this->getExecutable(), 90 | $this->getFormat(), 91 | $path 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Node/MerkleNode.php: -------------------------------------------------------------------------------- 1 | hasher = $hasher ?? new DoubleSha256(); 35 | $this->modifiers = [ 36 | new RemoveNullNode(), 37 | new FulfillCapacity(), 38 | ]; 39 | } 40 | 41 | public function hash(): string 42 | { 43 | return $this->hasher->unpack($this->doHash($this->normalize())); 44 | } 45 | 46 | public function label(): string 47 | { 48 | return $this->isLeaf() ? 49 | $this->getValue() : 50 | $this->hash(); 51 | } 52 | 53 | public function normalize(): MerkleNodeInterface 54 | { 55 | return array_reduce( 56 | $this->modifiers, 57 | static function (MerkleNodeInterface $tree, ModifierInterface $modifier): MerkleNodeInterface { 58 | return $modifier->modify($tree); 59 | }, 60 | $this->clone() 61 | ); 62 | } 63 | 64 | private function doHash(MerkleNodeInterface $node): string 65 | { 66 | // If node is a leaf, then compute its hash from its value. 67 | if ($node->isLeaf()) { 68 | return $this->hasher->hash($node->getValue()); 69 | } 70 | 71 | $hash = ''; 72 | /** @var MerkleNodeInterface $child */ 73 | foreach ($node->children() as $child) { 74 | $hash .= $this->doHash($child); 75 | } 76 | 77 | return $this->hasher->hash($hash); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loophp/phptree", 3 | "description": "An implementation of tree data structure", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "tree", 8 | "php", 9 | "data structure", 10 | "binary tree" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Pol Dellaiera", 15 | "email": "pol.dellaiera@protonmail.com" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/loophp/phptree/issues", 20 | "source": "https://github.com/loophp/phptree" 21 | }, 22 | "funding": [ 23 | { 24 | "type": "github", 25 | "url": "https://github.com/drupol" 26 | } 27 | ], 28 | "require": { 29 | "php": ">= 8.1" 30 | }, 31 | "require-dev": { 32 | "ext-ast": "*", 33 | "ext-pcov": "*", 34 | "drupol/launcher": "^2.2", 35 | "drupol/php-conventions": "^6", 36 | "drupol/phpmerkle": "^2.2", 37 | "friends-of-phpspec/phpspec-code-coverage": "^6", 38 | "graphp/graphviz": "^0.2", 39 | "infection/infection": "^0.29", 40 | "infection/phpspec-adapter": "^0.2", 41 | "loophp/launcher": "^2.2.2", 42 | "microsoft/tolerant-php-parser": "^0.1", 43 | "nikic/php-parser": "^4", 44 | "phpspec/phpspec": "^7" 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "loophp\\phptree\\": "src/" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "loophp\\phptree\\tests\\": "tests/src/", 54 | "loophp\\phptree\\benchmarks\\": "benchmarks/", 55 | "spec\\loophp\\phptree\\": "spec/loophp/phptree/" 56 | } 57 | }, 58 | "config": { 59 | "allow-plugins": { 60 | "composer/package-versions-deprecated": true, 61 | "phpstan/extension-installer": true, 62 | "ergebnis/composer-normalize": true, 63 | "phpro/grumphp": true, 64 | "infection/extension-installer": true 65 | }, 66 | "sort-packages": true 67 | }, 68 | "scripts": { 69 | "changelog-unreleased": "auto_changelog -c .auto-changelog -u", 70 | "changelog-version": "auto_changelog -c .auto-changelog -v", 71 | "grumphp": "./vendor/bin/grumphp run", 72 | "infection": "./vendor/bin/infection run -j 1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Importer/NikicPhpParser.php: -------------------------------------------------------------------------------- 1 | parseNode($this->createNode(['label' => 'root']), ...$data); 30 | } 31 | 32 | private function createNode(array $attributes): AttributeNodeInterface 33 | { 34 | return new AttributeNode($attributes); 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | private function getAllNodeChildren(Node $astNode): array 41 | { 42 | /** @var array> $astNodes */ 43 | $astNodes = array_map( 44 | static function (string $subNodeName) use ($astNode): array { 45 | $subNodes = $astNode->{$subNodeName}; 46 | 47 | if (!is_array($subNodes)) { 48 | $subNodes = [$subNodes]; 49 | } 50 | 51 | return array_filter( 52 | $subNodes, 53 | 'is_object' 54 | ); 55 | }, 56 | $astNode->getSubNodeNames() 57 | ); 58 | 59 | return [] === $astNodes ? 60 | [] : 61 | array_merge(...$astNodes); 62 | } 63 | 64 | private function parseNode(AttributeNodeInterface $parent, Node ...$astNodes): NodeInterface 65 | { 66 | return array_reduce( 67 | $astNodes, 68 | function (AttributeNodeInterface $carry, Node $astNode): NodeInterface { 69 | return $carry 70 | ->add( 71 | $this->parseNode( 72 | $this->createNode([ 73 | 'label' => $astNode->getType(), 74 | 'astNode' => $astNode, 75 | ]), 76 | ...$this->getAllNodeChildren($astNode) 77 | ) 78 | ); 79 | }, 80 | $parent 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Importer/Text.php: -------------------------------------------------------------------------------- 1 | parseNode(new AttributeNode(['label' => 'root']), $data); 21 | } 22 | 23 | /** 24 | * Create a node. 25 | * 26 | * @param string $label 27 | * The node label 28 | * 29 | * @return AttributeNodeInterface 30 | * The node 31 | */ 32 | private function createNode(string $label): AttributeNodeInterface 33 | { 34 | return new AttributeNode([ 35 | 'label' => $label, 36 | ]); 37 | } 38 | 39 | /** 40 | * Parse a string into an array. 41 | * 42 | * @param string $subject 43 | * The subject string 44 | * 45 | * @return array 46 | * The array 47 | */ 48 | private function parse(string $subject): array 49 | { 50 | $result = [ 51 | 'value' => mb_substr($subject, 1, mb_strpos($subject, '[', 1) - 1), 52 | 'children' => [], 53 | ]; 54 | 55 | if (false === $nextBracket = mb_strpos($subject, '[', 1)) { 56 | return $result; 57 | } 58 | 59 | // Todo: Improve the regex. 60 | preg_match_all('#[^\[\]]+|\[(?(?R)*)]#u', mb_substr($subject, $nextBracket, -1), $matches); 61 | 62 | $result['children'] = array_map( 63 | static function (string $match): string { 64 | return sprintf('[%s]', $match); 65 | }, 66 | array_filter((array) $matches['nested']) 67 | ); 68 | 69 | return $result; 70 | } 71 | 72 | private function parseNode(AttributeNodeInterface $parent, string ...$nodes): NodeInterface 73 | { 74 | return array_reduce( 75 | $nodes, 76 | function (AttributeNodeInterface $carry, string $node): NodeInterface { 77 | $data = $this->parse($node); 78 | 79 | return $carry 80 | ->add( 81 | $this->parseNode( 82 | $this->createNode($data['value']), 83 | ...$data['children'] 84 | ) 85 | ); 86 | }, 87 | $parent 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Node/NaryNode.php: -------------------------------------------------------------------------------- 1 | capacity = $capacity; 36 | $this->traverser = $traverser ?? new BreadthFirst(); 37 | } 38 | 39 | public function add(NodeInterface ...$nodes): NodeInterface 40 | { 41 | $capacity = $this->capacity(); 42 | 43 | if (0 === $capacity) { 44 | return parent::add(...$nodes); 45 | } 46 | 47 | foreach ($nodes as $node) { 48 | if ($this->degree() < $capacity) { 49 | parent::add($node); 50 | 51 | continue; 52 | } 53 | 54 | if (null !== $parent = $this->findFirstAvailableNode($this)) { 55 | $parent->add($node); 56 | } else { 57 | throw new Exception('Unable to add the node to the tree.'); 58 | } 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | public function capacity(): int 65 | { 66 | return $this->capacity; 67 | } 68 | 69 | public function getTraverser(): TraverserInterface 70 | { 71 | return $this->traverser; 72 | } 73 | 74 | public function offsetSet($offset, $value): void 75 | { 76 | if (null === $offset) { 77 | $this->add($value); 78 | } else { 79 | if (0 !== $this->capacity() && $this->capacity() - 1 < $offset) { 80 | throw new OutOfBoundsException('The offset is out of range.'); 81 | } 82 | 83 | parent::offsetSet($offset, $value); 84 | } 85 | } 86 | 87 | /** 88 | * Find the first available node in the tree. 89 | * 90 | * When adding nodes to a NaryNode based tree, you must traverse the tree 91 | * and find the first node that can be used as a parent for the node to add. 92 | * 93 | * @param NodeInterface $tree 94 | * The base node. 95 | * 96 | * @return NodeInterface|null 97 | * A node, null if none are found. 98 | */ 99 | protected function findFirstAvailableNode(NodeInterface $tree): ?NodeInterface 100 | { 101 | foreach ($this->getTraverser()->traverse($tree) as $candidate) { 102 | if (!($candidate instanceof NaryNodeInterface)) { 103 | continue; 104 | } 105 | 106 | $capacity = $candidate->capacity(); 107 | 108 | if (null === $capacity) { 109 | continue; 110 | } 111 | 112 | if (0 !== $capacity && $candidate->degree() >= $capacity) { 113 | continue; 114 | } 115 | 116 | return $candidate; 117 | } 118 | 119 | return null; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Node/NodeInterface.php: -------------------------------------------------------------------------------- 1 | 21 | * @template-extends IteratorAggregate 22 | */ 23 | interface NodeInterface extends ArrayAccess, Countable, IteratorAggregate 24 | { 25 | /** 26 | * The node to add. 27 | * 28 | * @param NodeInterface ...$nodes 29 | * The nodes to add. 30 | * 31 | * @return NodeInterface 32 | * The node 33 | */ 34 | public function add(NodeInterface ...$nodes): NodeInterface; 35 | 36 | /** 37 | * Get all the nodes of a tree including the parent node itself. 38 | * 39 | * @return Traversable<\loophp\phptree\Node\NodeInterface> 40 | * The node. 41 | */ 42 | public function all(): Traversable; 43 | 44 | /** 45 | * Get the children. 46 | * 47 | * @return Traversable<\loophp\phptree\Node\NodeInterface> 48 | * The children 49 | */ 50 | public function children(): Traversable; 51 | 52 | /** 53 | * Clone the tree and all of its children. 54 | * 55 | * @return \loophp\phptree\Node\NodeInterface 56 | * The tree. 57 | */ 58 | public function clone(): NodeInterface; 59 | 60 | /** 61 | * The amount of children a node has. 62 | * 63 | * @return int 64 | * The amount of children 65 | */ 66 | public function degree(): int; 67 | 68 | /** 69 | * Remove a node (and its subnodes) from a tree. 70 | * 71 | * @param \loophp\phptree\Node\NodeInterface $node 72 | * The node to remove. 73 | * 74 | * @return \loophp\phptree\Node\NodeInterface|null 75 | * The node that has been removed without parent, null otherwise. 76 | */ 77 | public function delete(NodeInterface $node): ?NodeInterface; 78 | 79 | /** 80 | * Get the node depth from the root node. 81 | * 82 | * @return int 83 | * The depth is the number of nodes before root 84 | */ 85 | public function depth(): int; 86 | 87 | /** 88 | * Check if a node is find is in the tree. 89 | * 90 | * @param \loophp\phptree\Node\NodeInterface $node 91 | * The node to find. 92 | * 93 | * @return \loophp\phptree\Node\NodeInterface|null 94 | * The node if found, false otherwise. 95 | */ 96 | public function find(NodeInterface $node): ?NodeInterface; 97 | 98 | /** 99 | * Get the ancestors of a node. 100 | * 101 | * @return Traversable<\loophp\phptree\Node\NodeInterface> 102 | * The ancestors 103 | */ 104 | public function getAncestors(): Traversable; 105 | 106 | /** 107 | * Get the parent node. 108 | * 109 | * @return NodeInterface|null 110 | * The parent node if any, null otherwise 111 | */ 112 | public function getParent(): ?NodeInterface; 113 | 114 | /** 115 | * Get the node's sibblings. 116 | * 117 | * @return Traversable<\loophp\phptree\Node\NodeInterface> 118 | * The sibblings 119 | */ 120 | public function getSibblings(): Traversable; 121 | 122 | /** 123 | * Get the tree height. 124 | * 125 | * @return int 126 | * The tree height 127 | */ 128 | public function height(): int; 129 | 130 | /** 131 | * Check if the node has children, then it's a leaf. 132 | * 133 | * @return bool 134 | * True if it has children, false otherwise 135 | */ 136 | public function isLeaf(): bool; 137 | 138 | /** 139 | * Check if the node is the root node (Node parent is null). 140 | * 141 | * @return bool 142 | * True if it's a root node, false otherwise 143 | */ 144 | public function isRoot(): bool; 145 | 146 | public function label(): string; 147 | 148 | /** 149 | * @return Traversable<\loophp\phptree\Node\NodeInterface> 150 | */ 151 | public function level(int $level): Traversable; 152 | 153 | /** 154 | * Remove children. 155 | * 156 | * @return NodeInterface 157 | * The node 158 | */ 159 | public function remove(NodeInterface ...$nodes): NodeInterface; 160 | 161 | /** 162 | * Replace a node with another one and return the parent. 163 | * 164 | * It may also return null if the replace failed. 165 | * 166 | * @param \loophp\phptree\Node\NodeInterface $node 167 | * The replacement node. 168 | * 169 | * @return \loophp\phptree\Node\NodeInterface|null 170 | * The node parent if successful, null otherwise. 171 | */ 172 | public function replace(NodeInterface $node): ?NodeInterface; 173 | 174 | /** 175 | * Set the parent. 176 | * 177 | * @param NodeInterface|null $node 178 | * The parent node 179 | * 180 | * @return NodeInterface 181 | * The node 182 | */ 183 | public function setParent(?NodeInterface $node): NodeInterface; 184 | 185 | /** 186 | * Get a clone of the object with or without children. 187 | * 188 | * @param \loophp\phptree\Node\NodeInterface|null ...$nodes 189 | * The children that the clone will have. 190 | * 191 | * @return \loophp\phptree\Node\NodeInterface 192 | * The new object 193 | */ 194 | public function withChildren(?NodeInterface ...$nodes): NodeInterface; 195 | } 196 | -------------------------------------------------------------------------------- /src/Exporter/Gv.php: -------------------------------------------------------------------------------- 1 | attributesArrayToText($data) 46 | ); 47 | } 48 | 49 | return sprintf( 50 | ' %s = %s', 51 | $key, 52 | $data 53 | ); 54 | }, 55 | array_keys($this->attributes), 56 | $this->attributes 57 | ); 58 | 59 | $nodes = []; 60 | 61 | foreach ($node->all() as $child) { 62 | $nodes[] = sprintf( 63 | ' "%s" %s', 64 | $this->getHash($child), 65 | $this->attributesArrayToText($this->getNodeAttributes($child)) 66 | ); 67 | } 68 | 69 | $edges = []; 70 | 71 | foreach ($this->findEdges($node) as $parent => $child) { 72 | $edges[] = sprintf( 73 | ' "%s" %s "%s";', 74 | $this->getHash($parent), 75 | $this->getDirected() ? '->' : '--', 76 | $this->getHash($child) 77 | ); 78 | } 79 | 80 | return $this->getGv( 81 | implode(PHP_EOL, $attributes), 82 | implode(PHP_EOL, $nodes), 83 | implode(PHP_EOL, $edges) 84 | ); 85 | } 86 | 87 | /** 88 | * Check if the graph is directed or undirected. 89 | * 90 | * @return bool 91 | * True if directed, false otherwise. 92 | */ 93 | public function getDirected(): bool 94 | { 95 | return $this->directed; 96 | } 97 | 98 | /** 99 | * Set the graph type, directed or undirected. 100 | * 101 | * @param bool $directed 102 | * True for a directed graph, false otherwise. 103 | * 104 | * @return \loophp\phptree\Exporter\Gv 105 | * The exporter. 106 | */ 107 | public function setDirected(bool $directed = true): self 108 | { 109 | $this->directed = $directed; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set the graph attributes. 116 | * 117 | * @param array $attributes 118 | * The graph attributes. 119 | * 120 | * @return \loophp\phptree\Exporter\Gv 121 | * The exporter. 122 | */ 123 | public function setGraphAttributes(array $attributes): self 124 | { 125 | $this->attributes = $attributes; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * Converts an attributes array to string. 132 | * 133 | * @param array $attributes 134 | * The attributes. 135 | * 136 | * @return string 137 | * The attributes as string. 138 | */ 139 | private function attributesArrayToText(array $attributes): string 140 | { 141 | $attributesText = array_filter( 142 | array_map( 143 | static function ($key, $value) { 144 | if (null === $value || is_scalar($value) || method_exists($value, '__toString')) { 145 | $value = (string) $value; 146 | } else { 147 | return null; 148 | } 149 | 150 | return sprintf('%s="%s"', $key, $value); 151 | }, 152 | array_keys($attributes), 153 | $attributes 154 | ) 155 | ); 156 | 157 | return '[' . implode(' ', $attributesText) . ']'; 158 | } 159 | 160 | /** 161 | * Recursively find all the edges in a tree. 162 | * 163 | * @param \loophp\phptree\Node\NodeInterface $node 164 | * The root node. 165 | * 166 | * @return Generator 167 | * Yield the parent and child node. 168 | */ 169 | private function findEdges(NodeInterface $node): iterable 170 | { 171 | foreach ($node->children() as $child) { 172 | yield $node => $child; 173 | 174 | yield from $this->findEdges($child); 175 | } 176 | } 177 | 178 | /** 179 | * Get the default GV file content. 180 | * 181 | * @param string $edges 182 | * The edges. 183 | * 184 | * @return string 185 | * The content of the .gv file. 186 | */ 187 | private function getGv(string $attributes = '', string $nodes = '', string $edges = ''): string 188 | { 189 | $graphType = $this->getDirected() ? 190 | 'digraph' : 191 | 'graph'; 192 | 193 | return implode( 194 | PHP_EOL, 195 | [ 196 | sprintf('%s PHPTreeGraph {', $graphType), 197 | '// The graph attributes.', 198 | $attributes, 199 | '', 200 | '// The graph nodes.', 201 | $nodes, 202 | '', 203 | '// The graph edges.', 204 | $edges, 205 | '}', 206 | ] 207 | ); 208 | } 209 | 210 | /** 211 | * Get the hash of a node. 212 | * 213 | * @param \loophp\phptree\Node\NodeInterface $node 214 | * The node. 215 | * 216 | * @return string 217 | * The hash of the node. 218 | */ 219 | private function getHash(NodeInterface $node): string 220 | { 221 | return sha1(spl_object_hash($node)); 222 | } 223 | 224 | /** 225 | * Get the node attributes. 226 | * 227 | * @param \loophp\phptree\Node\NodeInterface $node 228 | * The node interface. 229 | * 230 | * @return array 231 | * The attributes as an array. 232 | */ 233 | private function getNodeAttributes(NodeInterface $node): array 234 | { 235 | $attributes = [ 236 | 'label' => $node->label(), 237 | ]; 238 | 239 | if ($node instanceof AttributeNodeInterface) { 240 | foreach ($node->getAttributes() as $key => $value) { 241 | $attributes[$key] = $value; 242 | } 243 | } 244 | 245 | return $attributes; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Node/Node.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private array $children; 26 | 27 | private ?NodeInterface $parent; 28 | 29 | public function __construct(?NodeInterface $parent = null) 30 | { 31 | $this->parent = $parent; 32 | $this->children = []; 33 | } 34 | 35 | public function __clone() 36 | { 37 | /** @var NodeInterface $child */ 38 | foreach ($this->children as $id => $child) { 39 | $this->children[$id] = $child->clone()->setParent($this); 40 | } 41 | } 42 | 43 | public function add(NodeInterface ...$nodes): NodeInterface 44 | { 45 | foreach ($nodes as $node) { 46 | $this->children[] = $node->setParent($this); 47 | } 48 | 49 | return $this; 50 | } 51 | 52 | public function all(): Traversable 53 | { 54 | yield $this; 55 | 56 | /** @var NodeInterface $child */ 57 | foreach ($this->children() as $child) { 58 | yield from $child->all(); 59 | } 60 | } 61 | 62 | public function children(): Traversable 63 | { 64 | yield from $this->children; 65 | } 66 | 67 | public function clone(): NodeInterface 68 | { 69 | return clone $this; 70 | } 71 | 72 | public function count(): int 73 | { 74 | return iterator_count($this->all()) - 1; 75 | } 76 | 77 | public function degree(): int 78 | { 79 | return count($this->children); 80 | } 81 | 82 | public function delete(NodeInterface $node, ?NodeInterface $root = null): ?NodeInterface 83 | { 84 | $root = $root ?? $this; 85 | 86 | if (null !== ($candidate = $this->find($node))) { 87 | if ($candidate === $root) { 88 | throw new InvalidArgumentException('Unable to delete root node.'); 89 | } 90 | 91 | if (null !== $parent = $candidate->getParent()) { 92 | $parent->remove($node); 93 | } 94 | 95 | return $candidate->setParent(null); 96 | } 97 | 98 | return null; 99 | } 100 | 101 | public function depth(): int 102 | { 103 | return iterator_count($this->getAncestors()); 104 | } 105 | 106 | public function find(NodeInterface $node): ?NodeInterface 107 | { 108 | /** @var NodeInterface $candidate */ 109 | foreach ($this->all() as $candidate) { 110 | if ($candidate === $node) { 111 | return $node; 112 | } 113 | } 114 | 115 | return null; 116 | } 117 | 118 | public function getAncestors(): Traversable 119 | { 120 | $node = $this; 121 | 122 | while ($node = $node->getParent()) { 123 | yield $node; 124 | } 125 | } 126 | 127 | /** 128 | * @return Traversable 129 | */ 130 | public function getIterator(): Traversable 131 | { 132 | yield from $this->all(); 133 | } 134 | 135 | public function getParent(): ?NodeInterface 136 | { 137 | return $this->parent; 138 | } 139 | 140 | public function getSibblings(): Traversable 141 | { 142 | $parent = $this->parent; 143 | 144 | if (null === $parent) { 145 | return []; 146 | } 147 | 148 | foreach ($parent->children() as $child) { 149 | if ($child === $this) { 150 | continue; 151 | } 152 | 153 | yield $child; 154 | } 155 | } 156 | 157 | public function height(): int 158 | { 159 | $height = $this->depth(); 160 | 161 | /** @var NodeInterface $child */ 162 | foreach ($this->children() as $child) { 163 | $height = max($height, $child->height()); 164 | } 165 | 166 | return $height; 167 | } 168 | 169 | public function isLeaf(): bool 170 | { 171 | return 0 === $this->degree(); 172 | } 173 | 174 | public function isRoot(): bool 175 | { 176 | return null === $this->parent; 177 | } 178 | 179 | public function label(): string 180 | { 181 | return sha1(spl_object_hash($this)); 182 | } 183 | 184 | public function level(int $level): Traversable 185 | { 186 | /** @var NodeInterface $node */ 187 | foreach ($this->all() as $node) { 188 | if ($node->depth() === $level) { 189 | yield $node; 190 | } 191 | } 192 | } 193 | 194 | /** 195 | * @param mixed $offset 196 | */ 197 | #[ReturnTypeWillChange] 198 | public function offsetExists($offset): bool 199 | { 200 | return array_key_exists($offset, $this->children); 201 | } 202 | 203 | /** 204 | * @param mixed $offset 205 | */ 206 | #[ReturnTypeWillChange] 207 | public function offsetGet($offset): NodeInterface 208 | { 209 | return $this->children[$offset]; 210 | } 211 | 212 | /** 213 | * @param mixed $offset 214 | * @param mixed $value 215 | */ 216 | public function offsetSet($offset, $value): void 217 | { 218 | if (!($value instanceof NodeInterface)) { 219 | throw new InvalidArgumentException( 220 | 'The value must implements NodeInterface.' 221 | ); 222 | } 223 | 224 | $this->children[$offset] = $value->setParent($this); 225 | } 226 | 227 | /** 228 | * @param mixed $offset 229 | */ 230 | #[ReturnTypeWillChange] 231 | public function offsetUnset($offset): void 232 | { 233 | unset($this->children[$offset]); 234 | } 235 | 236 | public function remove(NodeInterface ...$nodes): NodeInterface 237 | { 238 | $this->children = 239 | array_filter( 240 | $this->children, 241 | static function ($child) use ($nodes) { 242 | return !in_array($child, $nodes, true); 243 | } 244 | ); 245 | 246 | return $this; 247 | } 248 | 249 | public function replace(NodeInterface $node): ?NodeInterface 250 | { 251 | if (null === $parent = $this->getParent()) { 252 | return null; 253 | } 254 | 255 | // Find the key of the current node in the parent. 256 | foreach ($parent->children() as $key => $child) { 257 | if ($this === $child) { 258 | $parent[$key] = $node; 259 | 260 | break; 261 | } 262 | } 263 | 264 | return $parent; 265 | } 266 | 267 | public function setParent(?NodeInterface $node): NodeInterface 268 | { 269 | $this->parent = $node; 270 | 271 | return $this; 272 | } 273 | 274 | public function withChildren(?NodeInterface ...$nodes): NodeInterface 275 | { 276 | $clone = clone $this; 277 | $clone->children = []; 278 | 279 | return $clone->add(...array_filter($nodes)); 280 | } 281 | } 282 | --------------------------------------------------------------------------------