8000 [FrameworkBundle][Workflow] Add metadata dumping support for `GraphvizDumper` by Louis-Proffit · Pull Request #50621 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FrameworkBundle][Workflow] Add metadata dumping support for GraphvizDumper #50621

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

Merged
merged 1 commit into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ protected function configure(): void
new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'),
new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'),
new InputOption('label', 'l', InputOption::VALUE_REQUIRED, 'Label a graph'),
new InputOption('with-metadata', null, InputOption::VALUE_NONE, 'Include the workflow\'s metadata in the dumped graph', null),
new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format ['.implode('|', self::DUMP_FORMAT_OPTIONS).']', 'dot'),
])
->setHelp(<<<'EOF'
Expand Down Expand Up @@ -134,10 +135,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$options = [
'name' => $workflowName,
'with-metadata' => $input->getOption('with-metadata'),
'nofooter' => true,
'graph' => [
'label' => $input->getOption('label'),
],
'label' => $input->getOption('label'),
Copy link
Member
@xabbuh xabbuh Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a BC break if someone uses the dumper in their own code (as they would have to change the options being passed to the dump() method)?

];
$output->writeln($dumper->dump($definition, $marking, $options));

Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/Workflow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
CHANGELOG
=========

6.4
---

* Add `with-metadata` option to the `workflow:dump` command to include places,
transitions and workflow's metadata into dumped graph

6.2
---

Expand Down
110 changes: 97 additions & 13 deletions src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,27 @@ class GraphvizDumper implements DumperInterface
*/
public function dump(Definition $definition, Marking $marking = null, array $options = []): string
{
$places = $this->findPlaces($definition, $marking);
$transitions = $this->findTransitions($definition);
$withMetadata = $options['with-metadata'] ?? false;

$places = $this->findPlaces($definition, $withMetadata, $marking);
$transitions = $this->findTransitions($definition, $withMetadata);
$edges = $this->findEdges($definition);

$options = array_replace_recursive(self::$defaultOptions, $options);

return $this->startDot($options)
.$this->addPlaces($places)
.$this->addTransitions($transitions)
$label = $this->formatLabel($definition, $withMetadata, $options);

return $this->startDot($options, $label)
.$this->addPlaces($places, $withMetadata)
.$this->addTransitions($transitions, $withMetadata)
.$this->addEdges($edges)
.$this->endDot();
}

/**
* @internal
*/
protected function findPlaces(Definition $definition, Marking $marking = null): array
protected function findPlaces(Definition $definition, bool $withMetadata, Marking $marking = null): array
{
$workflowMetadata = $definition->getMetadataStore();

Expand All @@ -80,9 +84,16 @@ protected function findPlaces(Definition $definition, Marking $marking = null):
$attributes['style'] = 'filled';
$attributes['fillcolor'] = $backgroundColor;
}
if ($withMetadata) {
$attributes['metadata'] = $workflowMetadata->getPlaceMetadata($place);
}
$label = $workflowMetadata->getMetadata('label', $place);
if (null !== $label) {
$attributes['name'] = $label;
if ($withMetadata) {
// Don't include label in metadata if already used as name
unset($attributes['metadata']['label']);
}
}
$places[$place] = [
'attributes' => $attributes,
Expand All @@ -95,7 +106,7 @@ protected function findPlaces(Definition $definition, Marking $marking = null):
/**
* @internal
*/
protected function findTransitions(Definition $definition): array
protected function findTransitions(Definition $definition, bool $withMetadata): array
{
$workflowMetadata = $definition->getMetadataStore();

Expand All @@ -111,9 +122,16 @@ protected function findTransitions(Definition $definition): array
}
$name = $workflowMetadata->getMetadata('label', $transition) ?? $transition->getName();

$metadata = [];
if ($withMetadata) {
$metadata = $workflowMetadata->getTransitionMetadata($transition);
unset($metadata['label']);
}

$transitions[] = [
'attributes' => $attributes,
'name' => $name,
'metadata' => $metadata,
];
}

Expand All @@ -123,7 +141,7 @@ protected function findTransitions(Definition $definition): array
/**
* @internal
*/
protected function addPlaces(array $places): string
protected function addPlaces(array $places, float $withMetadata): string
{
$code = '';

Expand All @@ -135,7 +153,15 @@ protected function addPlaces(array $places): string
$placeName = $id;
}

$code .= sprintf(" place_%s [label=\"%s\", shape=circle%s];\n", $this->dotize($id), $this->escape($placeName), $this->addAttributes($place['attributes']));
if ($withMetadata) {
$escapedLabel = sprintf('<<B>%s</B>%s>', $this->escape($placeName), $this->addMetadata($place['attributes']['metadata']));
// Don't include metadata in default attributes used to format the place
unset($place['attributes']['metadata']);
} else {
$escapedLabel = sprintf('"%s"', $this->escape($placeName));
}

$code .= sprintf(" place_%s [label=%s, shape=circle%s];\n", $this->dotize($id), $escapedLabel, $this->addAttributes($place['attributes']));
}

return $code;
Expand All @@ -144,12 +170,18 @@ protected function addPlaces(array $places): string
/**
* @internal
*/
protected function addTransitions(array $transitions): string
protected function addTransitions(array $transitions, bool $withMetadata): string
{
$code = '';

foreach ($transitions as $i => $place) {
$code .= sprintf(" transition_%s [label=\"%s\",%s];\n", $this->dotize($i), $this->escape($place['name']), $this->addAttributes($place['attributes']));
if ($withMetadata) {
$escapedLabel = sprintf('<<B>%s</B>%s>', $this->escape($place['name']), $this->addMetadata($place['metadata']));
} else {
$escapedLabel = '"'.$this->escape($place['name']).'"';
}

$code .= sprintf(" transition_%s [label=%s,%s];\n", $this->dotize($i), $escapedLabel, $this->addAttributes($place['attributes']));
}

return $code;
Expand Down Expand Up @@ -215,10 +247,11 @@ protected function addEdges(array $edges): string
/**
* @internal
*/
protected function startDot(array $options): string
protected function startDot(array $options, string $label): string
{
return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n",
return sprintf("digraph workflow {\n %s%s\n node [%s];\n edge [%s];\n\n",
$this->addOptions($options['graph']),
'""' !== $label && '<>' !== $label ? sprintf(' label=%s', $label) : '',
$this->addOptions($options['node']),
$this->addOptions($options['edge'])
);
Expand Down 6D40 Expand Up @@ -248,6 +281,9 @@ protected function escape(string|bool $value): string
return \is_bool($value) ? ($value ? '1' : '0') : addslashes($value);
}

/**
* @internal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make this internal?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because all methods are in this class

*/
protected function addAttributes(array $attributes): string
{
$code = [];
Expand All @@ -259,6 +295,33 @@ protected function addAttributes(array $attributes): string
return $code ? ' '.implode(' ', $code) : '';
}

/**
* Handles the label of the graph depending on whether a label was set in CLI,
* if metadata should be included and if there are any.
*
* The produced label must be escaped.
*
* @internal
*/
protected function formatLabel(Definition $definition, string $withMetadata, array $options): string
{
$currentLabel = $options['label'] ?? '';

if (!$withMetadata) {
// Only currentLabel to handle. If null, will be translated to empty string
return sprintf('"%s"', $this->escape($currentLabel));
}
$workflowMetadata = $definition->getMetadataStore()->getWorkflowMetadata();

if ('' === $currentLabel) {
// Only metadata to handle
return sprintf('<%s>', $this->addMetadata($workflowMetadata, false));
}

// currentLabel and metadata to handle
return sprintf('<<B>%s</B>%s>', $this->escape($currentLabel), $this->addMetadata($workflowMetadata));
}

private function addOptions(array $options): string
{
$code = [];
Expand All @@ -269,4 +332,25 @@ private function addOptions(array $options): string

return implode(' ', $code);
}

/**
* @param bool $lineBreakFirstIfNotEmpty Whether to add a separator in the first place when metadata is not empty
*/
private function addMetadata(array $metadata, bool $lineBreakFirstIfNotEmpty = true): string
{
$code = [];

$skipSeparator = !$lineBreakFirstIfNotEmpty;

foreach ($metadata as $key => $value) {
if ($skipSeparator) {
$code[] = sprintf('%s: %s', $this->escape($key), $this->escape($value));
$skipSeparator = false;
} else {
$code[] = sprintf('%s%s: %s', '<BR/>', $this->escape($key), $this->escape($value));
}
}

return $code ? implode('', $code) : '';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ class StateMachineGraphvizDumper extends GraphvizDumper
*/
public function dump(Definition $definition, Marking $marking = null, array $options = []): string
{
$places = $this->findPlaces($definition, $marking);
$withMetadata = $options['with-metadata'] ?? false;

$places = $this->findPlaces($definition, $withMetadata, $marking);
$edges = $this->findEdges($definition);

$options = array_replace_recursive(self::$defaultOptions, $options);

return $this->startDot($options)
.$this->addPlaces($places)
$label = $this->formatLabel($definition, $withMetadata, $options);

return $this->startDot($options, $label)
.$this->addPlaces($places, $withMetadata)
.$this->addEdges($edges)
.$this->endDot()
;
.$this->endDot();
}

/**
Expand Down
Loading
0