-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Console] Add a Tree Helper + multiple Styles #59588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Console\Helper; | ||
|
||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
/** | ||
* The TreeHelper class provides methods to display tree-like structures. | ||
* | ||
* @author Simon André <smn.andre@gmail.com> | ||
* | ||
* @implements \RecursiveIterator<int, TreeNode> | ||
*/ | ||
final class TreeHelper implements \RecursiveIterator | ||
{ | ||
/** | ||
* @var \Iterator<int, TreeNode> | ||
*/ | ||
private \Iterator $children; | ||
|
||
private function __construct( | ||
private readonly OutputInterface $output, | ||
private readonly TreeNode $node, | ||
private readonly TreeStyle $style, | ||
) { | ||
$this->children = new \IteratorIterator($this->node->getChildren()); | ||
$this->children->rewind(); | ||
} | ||
|
||
public static function createTree(OutputInterface $output, string|TreeNode|null $root = null, iterable $values = [], ?TreeStyle $style = null): self | ||
{ | ||
$node = $root instanceof TreeNode ? $root : new TreeNode($root ?? ''); | ||
|
||
return new self($output, TreeNode::fromValues($values, $node), $style ?? TreeStyle::default()); | ||
} | ||
|
||
public function current(): TreeNode | ||
{ | ||
return $this->children->current(); | ||
} | ||
|
||
public function key(): int | ||
{ | ||
return $this->children->key(); | ||
} | ||
|
||
public function next(): void | ||
{ | ||
$this->children->next(); | ||
} | ||
|
||
public function rewind(): void | ||
{ | ||
$this->children->rewind(); | ||
} | ||
|
||
public function valid(): bool | ||
{ | ||
return $this->children->valid(); | ||
} | ||
|
||
public function hasChildren(): bool | ||
{ | ||
if (null === $current = $this->current()) { | ||
return false; | ||
} | ||
|
||
foreach ($current->getChildren() as $child) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
public function getChildren(): \RecursiveIterator | ||
{ | ||
return new self($this->output, $this->current(), $this->style); | ||
} | ||
|
||
/** | ||
* Recursively renders the tree to the output, applying the tree style. | ||
*/ | ||
public function render(): void | ||
{ | ||
$treeIterator = new \RecursiveTreeIterator($this); | ||
|
||
$this->style->applyPrefixes($treeIterator); | ||
|
||
$this->output->writeln($this->node->getValue()); | ||
|
||
$visited = new \SplObjectStorage(); | ||
foreach ($treeIterator as $node) { | ||
$currentNode = $node instanceof TreeNode ? $node : $treeIterator->getInnerIterator()->current(); | ||
if ($visited->contains($currentNode)) { | ||
throw new \LogicException(\sprintf('Cycle detected at node: "%s".', $currentNode->getValue())); | ||
} | ||
$visited->attach($currentNode); | ||
|
||
$this->output->writeln($node); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Console\Helper; | ||
|
||
/** | ||
* @implements \IteratorAggregate<TreeNode> | ||
* | ||
* @author Simon André <smn.andre@gmail.com> | ||
*/ | ||
final class TreeNode implements \Countable, \IteratorAggregate | ||
{ | ||
/** | ||
* @var array<TreeNode|callable(): \Generator> | ||
*/ | ||
private array $children = []; | ||
|
||
public function __construct( | ||
private readonly string $value = '', | ||
iterable $children = [], | ||
) { | ||
foreach ($children as $child) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. iterating in the constructor breaks lazy evaluation of the iterable, is that justified? |
||
$this->addChild($child); | ||
} | ||
} | ||
|
||
public static function fromValues(iterable $nodes, ?self $node = null): self | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
$node ??= new self(); | ||
foreach ($nodes as $key => $value) { | ||
if (is_iterable($value)) { | ||
$child = new self($key); | ||
self::fromValues($value, $child); | ||
$node->addChild($child); | ||
} elseif ($value instanceof self) { | ||
$node->addChild($value); | ||
} else { | ||
$node->addChild(new self($value)); | ||
} | ||
} | ||
|
||
return $node; | ||
} | ||
|
||
public function getValue(): string | ||
{ | ||
return $this->value; | ||
} | ||
|
||
public function addChild(self|string|callable $node): self | ||
{ | ||
if (\is_string($node)) { | ||
$node = new self($node, $this); | ||
} | ||
|
||
$this->children[] = $node; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* @return \Traversable<int, TreeNode> | ||
*/ | ||
public function getChildren(): \Traversable | ||
{ | ||
foreach ($this->children as $child) { | ||
if (\is_callable($child)) { | ||
yield from $child(); | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} elseif ($child instanceof self) { | ||
yield $child; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @return \Traversable<int, TreeNode> | ||
*/ | ||
public function getIterator(): \Traversable | ||
{ | ||
return $this->getChildren(); | ||
} | ||
|
||
public function count(): int | ||
{ | ||
$count = 0; | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
foreach ($this->getChildren() as $child) { | ||
++$count; | ||
} | ||
|
||
return $count; | ||
} | ||
|
||
public function __toString(): string | ||
{ | ||
return $this->value; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Console\Helper; | ||
|
||
/** | ||
* Configures the output of the Tree helper. | ||
* | ||
* @author Simon André <smn.andre@gmail.com> | ||
*/ | ||
final class TreeStyle | ||
{ | ||
public function __construct( | ||
private readonly string $prefixEndHasNext, | ||
private readonly string $prefixEndLast, | ||
private readonly string $prefixLeft, | ||
private readonly string $prefixMidHasNext, | ||
private readonly string $prefixMidLast, | ||
private readonly string $prefixRight, | ||
) { | ||
} | ||
|
||
public static function box(): self | ||
{ | ||
return new self('┃╸ ', '┗╸ ', '', '┃ ', ' ', ''); | ||
} | ||
|
||
public static function boxDouble(): self | ||
{ | ||
return new self('╠═ ', '╚═ ', '', '║ ', ' ', ''); | ||
} | ||
|
||
public static function compact(): self | ||
{ | ||
return new self('├ ', '└ ', '', '│ ', ' ', ''); | ||
} | ||
|
||
public static function default(): self | ||
{ | ||
return new self('├── ', '└── ', '', '│ ', ' ', ''); | ||
} | ||
|
||
public static function light(): self | ||
{ | ||
return new self('|-- ', '`-- ', '', '| ', ' ', ''); | ||
} | ||
|
||
public static function minimal(): self | ||
{ | ||
return new self('. ', '. ', '', '. ', ' ', ''); | ||
} | ||
|
||
public static function rounded(): self | ||
{ | ||
return new self('├─ ', '╰─ ', '', '│ ', ' ', ''); | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public function applyPrefixes(\RecursiveTreeIterator $iterator): void | ||
{ | ||
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_LEFT, $this->prefixLeft); | ||
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_HAS_NEXT, $this->prefixMidHasNext); | ||
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_LAST, $this->prefixMidLast); | ||
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_HAS_NEXT, $this->prefixEndHasNext); | ||
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_LAST, $this->prefixEndLast); | ||
$iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_RIGHT, $this->prefixRight); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,9 @@ | |
use Symfony\Component\Console\Helper\Table; | ||
use Symfony\Component\Console\Helper\TableCell; | ||
use Symfony\Component\Console\Helper\TableSeparator; | ||
use Symfony\Component\Console\Helper\TreeHelper; | ||
use Symfony\Component\Console\Helper\TreeNode; | ||
use Symfony\Component\Console\Helper\TreeStyle; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\ConsoleOutputInterface; | ||
use Symfony\Component\Console\Output\ConsoleSectionOutput; | ||
|
@@ -369,6 +372,24 @@ private function getProgressBar(): ProgressBar | |
?? throw new RuntimeException('The ProgressBar is not started.'); | ||
} | ||
|
||
/** | ||
* @param iterable<string, iterable|string|TreeNode> $nodes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some words or example about what can be passed would be nice either here or in TreeHelper or both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HA ! It is to avoid back and forth (with a future link to the doc) :), a sample like : $treeHelper = new TreeHelper($output, TreeNode::fromValues([
'a' => [
'a1',
'a2'
],
'b',
'c' => [
'c1',
'c2' => [
'c21' => [
'c211',
'c212',
],
],
],
],
new TreeNode($rootNode),
), TreeStyle::rounded());
$treeHelper->render(); |
||
*/ | ||
public function tree(iterable $nodes, string $root = ''): void | ||
{ | ||
$this->createTree($nodes, $root)->render(); | ||
} | ||
|
||
/** | ||
* @param iterable<string, iterable|string|TreeNode> $nodes | ||
*/ | ||
public function createTree(iterable $nodes, string $root = ''): TreeHelper | ||
{ | ||
$output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output; | ||
|
||
return TreeHelper::createTree($output, $root, $nodes, TreeStyle::default()); | ||
} | ||
|
||
private function autoPrependBlock(): void | ||
{ | ||
$chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); | ||
|
Uh oh!
There was an error while loading. Please reload this page.