Skip to content

[Security][SecurityBundle] Dump role hierarchy as mermaid chart #61034

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

Open
wants to merge 13 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
review: use MermaidEnumDirection instead of string
  • Loading branch information
damienfern committed Jul 9, 2025
commit b21d28d529d9aaed55b65aacad79b53dfe77ad3e
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Security\Core\Dumper\MermaidDirectionEnum;
use Symfony\Component\Security\Core\Dumper\MermaidDumper;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;

Expand All @@ -39,7 +41,7 @@ protected function configure(): void
{
$this
->setDefinition([
new InputOption('direction', 'd', InputOption::VALUE_REQUIRED, 'The direction of the flowchart ['.implode('|', MermaidDumper::VALID_DIRECTIONS).']', MermaidDumper::DIRECTION_TOP_TO_BOTTOM)
new InputOption('direction', 'd', InputOption::VALUE_REQUIRED, 'The direction of the flowchart ['. implode('|', $this->getAvailableDirections()) .']', MermaidDirectionEnum::TOP_TO_BOTTOM->value)
])
->setHelp(<<<'USAGE'
The <info>%command.name%</info> command dumps the role hierarchy in Mermaid format.
Expand All @@ -53,27 +55,21 @@ protected function configure(): void

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

if (null === $this->roleHierarchy) {
$output->writeln('<comment>No role hierarchy is configured.</comment>');
$io->getErrorStyle()->writeln('<comment>No role hierarchy is configured.</comment>');
return Command::SUCCESS;
}

$direction = $input->getOption('direction');

if (!\in_array($direction, MermaidDumper::VALID_DIRECTIONS, true)) {
$output->writeln('<error>Invalid direction. Available options: '.implode(', ', MermaidDumper::VALID_DIRECTIONS).'</error>');
if (! MermaidDirectionEnum::tryFrom($direction)) {
$io->getErrorStyle()->writeln('<error>Invalid direction. Available options: '. implode('|', $this->getAvailableDirections()) .'</error>');
return Command::FAILURE;
}

$directionMap = [
'TB' => MermaidDumper::DIRECTION_TOP_TO_BOTTOM,
'TD' => MermaidDumper::DIRECTION_TOP_DOWN,
'BT' => MermaidDumper::DIRECTION_BOTTOM_TO_TOP,
'RL' => MermaidDumper::DIRECTION_RIGHT_TO_LEFT,
'LR' => MermaidDumper::DIRECTION_LEFT_TO_RIGHT,
];

$dumper = new MermaidDumper($directionMap[$direction]);
$dumper = new MermaidDumper(MermaidDirectionEnum::from($direction));
$mermaidOutput = $dumper->dump($this->roleHierarchy);

$output->writeln($mermaidOutput);
Expand All @@ -84,7 +80,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('direction')) {
$suggestions->suggestValues(MermaidDumper::VALID_DIRECTIONS);
$suggestions->suggestValues(
$this->getAvailableDirections()
);
}
}

/**
* @return string[]
*/
private function getAvailableDirections(): array
{
return array_map(fn($case) => $case->value, MermaidDirectionEnum::cases());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Symfony\Component\Security\Core\Dumper;

enum MermaidDirectionEnum: string
{
case TOP_TO_BOTTOM = 'TB';
case TOP_DOWN = 'TD';
case BOTTOM_TO_TOP = 'BT';
case RIGHT_TO_LEFT = 'RL';
case LEFT_TO_RIGHT = 'LR';
}
27 changes: 3 additions & 24 deletions src/Symfony/Component/Security/Core/Dumper/MermaidDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,9 @@
*/
class MermaidDumper
{
public const DIRECTION_TOP_TO_BOTTOM = 'TB';
public const DIRECTION_TOP_DOWN = 'TD';
public const DIRECTION_BOTTOM_TO_TOP = 'BT';
public const DIRECTION_RIGHT_TO_LEFT = 'RL';
public const DIRECTION_LEFT_TO_RIGHT = 'LR';

public const VALID_DIRECTIONS = [
self::DIRECTION_TOP_TO_BOTTOM,
self::DIRECTION_TOP_DOWN,
self::DIRECTION_BOTTOM_TO_TOP,
self::DIRECTION_RIGHT_TO_LEFT,
self::DIRECTION_LEFT_TO_RIGHT,
];

public function __construct(
private readonly string $direction = self::DIRECTION_TOP_TO_BOTTOM,
private readonly MermaidDirectionEnum $direction = MermaidDirectionEnum::TOP_TO_BOTTOM,
) {
if (!\in_array($direction, self::VALID_DIRECTIONS, true)) {
throw new \InvalidArgumentException(\sprintf(
'Direction "%s" is not valid, valid directions are: "%s".',
$direction,
implode('", "', self::VALID_DIRECTIONS)
));
}
}

/**
Expand All @@ -56,10 +35,10 @@ public function dump(RoleHierarchyInterface $roleHierarchy): string
$hierarchy = $this->extractHierarchy($roleHierarchy);

if (!$hierarchy) {
return "graph {$this->direction}\n classDef default fill:#e1f5fe;";
return "graph {$this->direction->value}\n classDef default fill:#e1f5fe;";
}

$output = ["graph {$this->direction}"];
$output = ["graph {$this->direction->value}"];
$allRoles = $this->getAllRoles($hierarchy);

foreach ($allRoles as $role) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Security\Core\Tests\Dumper;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Dumper\MermaidDirectionEnum;
use Symfony\Component\Security\Core\Dumper\MermaidDumper;
use Symfony\Component\Security\Core\Role\RoleHierarchy;

Expand Down Expand Up @@ -44,7 +45,7 @@ public function testDumpWithDirection()
];

$roleHierarchy = new RoleHierarchy($hierarchy);
$dumper = new MermaidDumper(MermaidDumper::DIRECTION_LEFT_TO_RIGHT);
$dumper = new MermaidDumper(MermaidDirectionEnum::LEFT_TO_RIGHT);
$output = $dumper->dump($roleHierarchy);

$this->assertStringContainsString('graph LR', $output);
Expand Down Expand Up @@ -89,25 +90,31 @@ public function testDumpComplexHierarchy()

public function testInvalidDirection()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Direction "INVALID" is not valid');
$this->expectException(\TypeError::class);

new MermaidDumper('INVALID');
}

/**
* @testWith ["TB"]
* ["TD"]
* ["BT"]
* ["RL"]
* ["LR"]
* @dataProvider dataProviderValidDirection
*/
public function testValidDirections(string $direction)
public function testValidDirections(MermaidDirectionEnum $direction)
{
$this->expectNotToPerformAssertions();
new MermaidDumper($direction);
}

public static function dataProviderValidDirection()
{
return [
[MermaidDirectionEnum::TOP_TO_BOTTOM],
[MermaidDirectionEnum::TOP_DOWN],
[MermaidDirectionEnum::BOTTOM_TO_TOP],
[MermaidDirectionEnum::RIGHT_TO_LEFT],
[MermaidDirectionEnum::LEFT_TO_RIGHT],
];
}

public function testRoleNameEscaping()
{
$hierarchy = [
Expand Down
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