Skip to content

Commit 6a641c9

Browse files
smnandrefabpot
authored andcommitted
[Console] Add a Tree Helper + multiple Styles
1 parent ecb9728 commit 6a641c9

File tree

9 files changed

+1045
-0
lines changed

9 files changed

+1045
-0
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ CHANGELOG
44
7.3
55
---
66

7+
* Add `TreeHelper` and `TreeStyle` to display tree-like structures
8+
* Add `SymfonyStyle::createTree()`
79
* Add support for invokable commands and add `#[Argument]` and `#[Option]` attributes to define input arguments and options
810
* Deprecate not declaring the parameter type in callable commands defined through `setCode` method
911
* Add support for help definition via `AsCommand` attribute
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Helper;
13+
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
/**
17+
* The TreeHelper class provides methods to display tree-like structures.
18+
*
19+
* @author Simon André <smn.andre@gmail.com>
20+
*
21+
* @implements \RecursiveIterator<int, TreeNode>
22+
*/
23+
final class TreeHelper implements \RecursiveIterator
24+
{
25+
/**
26+
* @var \Iterator<int, TreeNode>
27+
*/
28+
private \Iterator $children;
29+
30+
private function __construct(
31+
private readonly OutputInterface $output,
32+
private readonly TreeNode $node,
33+
private readonly TreeStyle $style,
34+
) {
35+
$this->children = new \IteratorIterator($this->node->getChildren());
36+
$this->children->rewind();
37+
}
38+
39+
public static function createTree(OutputInterface $output, string|TreeNode|null $root = null, iterable $values = [], ?TreeStyle $style = null): self
40+
{
41+
$node = $root instanceof TreeNode ? $root : new TreeNode($root ?? '');
42+
43+
return new self($output, TreeNode::fromValues($values, $node), $style ?? TreeStyle::default());
44+
}
45+
46+
public function current(): TreeNode
47+
{
48+
return $this->children->current();
49+
}
50+
51+
public function key(): int
52+
{
53+
return $this->children->key();
54+
}
55+
56+
public function next(): void
57+
{
58+
$this->children->next();
59+
}
60+
61+
public function rewind(): void
62+
{
63+
$this->children->rewind();
64+
}
65+
66+
public function valid(): bool
67+
{
68+
return $this->children->valid();
69+
}
70+
71+
public function hasChildren(): bool
72+
{
73+
if (null === $current = $this->current()) {
74+
return false;
75+
}
76+
77+
foreach ($current->getChildren() as $child) {
78+
return true;
79+
}
80+
81+
return false;
82+
}
83+
84+
public function getChildren(): \RecursiveIterator
85+
{
86+
return new self($this->output, $this->current(), $this->style);
87+
}
88+
89+
/**
90+
* Recursively renders the tree to the output, applying the tree style.
91+
*/
92+
public function render(): void
93+
{
94+
$treeIterator = new \RecursiveTreeIterator($this);
95+
96+
$this->style->applyPrefixes($treeIterator);
97+
98+
$this->output->writeln($this->node->getValue());
99+
100+
$visited = new \SplObjectStorage();
101+
foreach ($treeIterator as $node) {
102+
$currentNode = $node instanceof TreeNode ? $node : $treeIterator->getInnerIterator()->current();
103+
if ($visited->contains($currentNode)) {
104+
throw new \LogicException(\sprintf('Cycle detected at node: "%s".', $currentNode->getValue()));
105+
}
106+
$visited->attach($currentNode);
107+
108+
$this->output->writeln($node);
109+
}
110+
}
111+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Helper;
13+
14+
/**
15+
* @implements \IteratorAggregate<TreeNode>
16+
*
17+
* @author Simon André <smn.andre@gmail.com>
18+
*/
19+
final class TreeNode implements \Countable, \IteratorAggregate
20+
{
21+
/**
22+
* @var array<TreeNode|callable(): \Generator>
23+
*/
24+
private array $children = [];
25+
26+
public function __construct(
27+
private readonly string $value = '',
28+
iterable $children = [],
29+
) {
30+
foreach ($children as $child) {
31+
$this->addChild($child);
32+
}
33+
}
34+
35+
public static function fromValues(iterable $nodes, ?self $node = null): self
36+
{
37+
$node ??= new self();
38+
foreach ($nodes as $key => $value) {
39+
if (is_iterable($value)) {
40+
$child = new self($key);
41+
self::fromValues($value, $child);
42+
$node->addChild($child);
43+
} elseif ($value instanceof self) {
44+
$node->addChild($value);
45+
} else {
46+
$node->addChild(new self($value));
47+
}
48+
}
49+
50+
return $node;
51+
}
52+
53+
public function getValue(): string
54+
{
55+
return $this->value;
56+
}
57+
58+
public function addChild(self|string|callable $node): self
59+
{
60+
if (\is_string($node)) {
61+
$node = new self($node, $this);
62+
}
63+
64+
$this->children[] = $node;
65+
66+
return $this;
67+
}
68+
69+
/**
70+
* @return \Traversable<int, TreeNode>
71+
*/
72+
public function getChildren(): \Traversable
73+
{
74+
foreach ($this->children as $child) {
75+
if (\is_callable($child)) {
76+
yield from $child();
77+
} elseif ($child instanceof self) {
78+
yield $child;
79+
}
80+
}
81+
}
82+
83+
/**
84+
* @return \Traversable<int, TreeNode>
85+
*/
86+
public function getIterator(): \Traversable
87+
{
88+
return $this->getChildren();
89+
}
90+
91+
public function count(): int
92+
{
93+
$count = 0;
94+
foreach ($this->getChildren() as $child) {
95+
++$count;
96+
}
97+
98+
return $count;
99+
}
100+
101+
public function __toString(): string
102+
{
103+
return $this->value;
104+
}
105+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Helper;
13+
14+
/**
15+
* Configures the output of the Tree helper.
16+
*
17+
* @author Simon André <smn.andre@gmail.com>
18+
*/
19+
final class TreeStyle
20+
{
21+
public function __construct(
22+
private readonly string $prefixEndHasNext,
23+
private readonly string $prefixEndLast,
24+
private readonly string $prefixLeft,
25+
private readonly string $prefixMidHasNext,
26+
private readonly string $prefixMidLast,
27+
private readonly string $prefixRight,
28+
) {
29+
}
30+
31+
public static function box(): self
32+
{
33+
return new self('┃╸ ', '┗╸ ', '', '', ' ', '');
34+
}
35+
36+
public static function boxDouble(): self
37+
{
38+
return new self('╠═ ', '╚═ ', '', '', ' ', '');
39+
}
40+
41+
public static function compact(): self
42+
{
43+
return new self('', '', '', '', ' ', '');
44+
}
45+
46+
public static function default(): self
47+
{
48+
return new self('├── ', '└── ', '', '', ' ', '');
49+
}
50+
51+
public static function light(): self
52+
{
53+
return new self('|-- ', '`-- ', '', '| ', ' ', '');
54+
}
55+
56+
public static function minimal(): self
57+
{
58+
return new self('. ', '. ', '', '. ', ' ', '');
59+
}
60+
61+
public static function rounded(): self
62+
{
63+
return new self('├─ ', '╰─ ', '', '', ' ', '');
64+
}
65+
66+
/**
67+
* @internal
68+
*/
69+
public function applyPrefixes(\RecursiveTreeIterator $iterator): void
70+
{
71+
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_LEFT, $this->prefixLeft);
72+
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_HAS_NEXT, $this->prefixMidHasNext);
73+
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_LAST, $this->prefixMidLast);
74+
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_HAS_NEXT, $this->prefixEndHasNext);
75+
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_LAST, $this->prefixEndLast);
76+
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_RIGHT, $this->prefixRight);
77+
}
78+
}

src/Symfony/Component/Console/Style/SymfonyStyle.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
use Symfony\Component\Console\Helper\Table;
2222
use Symfony\Component\Console\Helper\TableCell;
2323
use Symfony\Component\Console\Helper\TableSeparator;
24+
use Symfony\Component\Console\Helper\TreeHelper;
25+
use Symfony\Component\Console\Helper\TreeNode;
26+
use Symfony\Component\Console\Helper\TreeStyle;
2427
use Symfony\Component\Console\Input\InputInterface;
2528
use Symfony\Component\Console\Output\ConsoleOutputInterface;
2629
use Symfony\Component\Console\Output\ConsoleSectionOutput;
@@ -369,6 +372,24 @@ private function getProgressBar(): ProgressBar
369372
?? throw new RuntimeException('The ProgressBar is not started.');
370373
}
371374

375+
/**
376+
* @param iterable<string, iterable|string|TreeNode> $nodes
377+
*/
378+
public function tree(iterable $nodes, string $root = ''): void
379+
{
380+
$this->createTree($nodes, $root)->render();
381+
}
382+
383+
/**
384+
* @param iterable<string, iterable|string|TreeNode> $nodes
385+
*/
386+
public function createTree(iterable $nodes, string $root = ''): TreeHelper
387+
{
388+
$output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output;
389+
390+
return TreeHelper::createTree($output, $root, $nodes, TreeStyle::default());
391+
}
392+
372393
private function autoPrependBlock(): void
373394
{
374395
$chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy