8000 [Console] Add a Tree Helper + multiple Styles · symfony/symfony@6a641c9 · GitHub
[go: up one dir, main page]

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;
< 5BF2 code>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)
0