* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Workflow\Dumper; use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Exception\InvalidArgumentException; use Symfony\Component\Workflow\Marking; class MermaidDumper implements DumperInterface { 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'; private 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 const TRANSITION_TYPE_STATEMACHINE = 'statemachine'; public const TRANSITION_TYPE_WORKFLOW = 'workflow'; private const VALID_TRANSITION_TYPES = [ self::TRANSITION_TYPE_STATEMACHINE, self::TRANSITION_TYPE_WORKFLOW, ]; /** * @var string */ private $direction; /** * @var string */ private $transitionType; /** * Just tracking the transition id is in some cases inaccurate to * get the link's number for styling purposes. * * @var int */ private $linkCount; public function __construct(string $transitionType, string $direction = self::DIRECTION_LEFT_TO_RIGHT) { $this->validateDirection($direction); $this->validateTransitionType($transitionType); $this->direction = $direction; $this->transitionType = $transitionType; } public function dump(Definition $definition, Marking $marking = null, array $options = []): string { $this->linkCount = 0; $placeNameMap = []; $placeId = 0; $output = ['graph '.$this->direction]; $meta = $definition->getMetadataStore(); foreach ($definition->getPlaces() as $place) { [$placeNodeName, $placeNode, $placeStyle] = $this->preparePlace( $placeId, $place, $meta->getPlaceMetadata($place), \in_array($place, $definition->getInitialPlaces()), null !== $marking && $marking->has($place) ); $output[] = $placeNode; if ('' !== $placeStyle) { $output[] = $placeStyle; } $placeNameMap[$place] = $placeNodeName; ++$placeId; } foreach ($definition->getTransitions() as $transitionId => $transition) { $transitionMeta = $meta->getTransitionMetadata($transition); $transitionLabel = $transition->getName(); if (\array_key_exists('label', $transitionMeta)) { $transitionLabel = $transitionMeta['label']; } foreach ($transition->getFroms() as $from) { $from = $placeNameMap[$from]; foreach ($transition->getTos() as $to) { $to = $placeNameMap[$to]; if (self::TRANSITION_TYPE_STATEMACHINE === $this->transitionType) { $transitionOutput = $this->styleStatemachineTransition( $from, $to, $transitionId, $transitionLabel, $transitionMeta ); } else { $transitionOutput = $this->styleWorkflowTransition( $from, $to, $transitionId, $transitionLabel, $transitionMeta ); } foreach ($transitionOutput as $line) { if (\in_array($line, $output)) { // additional links must be decremented again to align the styling if (0 < strpos($line, '-->')) { --$this->linkCount; } continue; } $output[] = $line; } } } } return implode("\n", $output); } private function preparePlace(int $placeId, string $placeName, array $meta, bool $isInitial, bool $hasMarking): array { $placeLabel = $placeName; if (\array_key_exists('label', $meta)) { $placeLabel = $meta['label']; } $placeLabel = $this->escape($placeLabel); $labelShape = '((%s))'; if ($isInitial) { $labelShape = '([%s])'; } $placeNodeName = 'place'.$placeId; $placeNodeFormat = '%s'.$labelShape; $placeNode = sprintf($placeNodeFormat, $placeNodeName, $placeLabel); $placeStyle = $this->styleNode($meta, $placeNodeName, $hasMarking); return [$placeNodeName, $placeNode, $placeStyle]; } private function styleNode(array $meta, string $nodeName, bool $hasMarking = false): string { $nodeStyles = []; if (\array_key_exists('bg_color', $meta)) { $nodeStyles[] = sprintf( 'fill:%s', $meta['bg_color'] ); } if ($hasMarking) { $nodeStyles[] = 'stroke-width:4px'; } if (0 === \count($nodeStyles)) { return ''; } return sprintf('style %s %s', $nodeName, implode(',', $nodeStyles)); } /** * Replace double quotes with the mermaid escape syntax and * ensure all other characters are properly escaped. */ private function escape(string $label) { $label = str_replace('"', '#quot;', $label); return sprintf('"%s"', $label); } public function validateDirection(string $direction): void { 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))); } } private function validateTransitionType(string $transitionType): void { if (!\in_array($transitionType, self::VALID_TRANSITION_TYPES, true)) { throw new InvalidArgumentException(sprintf('Transition type "%s" is not valid, valid types are: "%s".', $transitionType, implode(', ', self::VALID_TRANSITION_TYPES))); } } private function styleStatemachineTransition( string $from, string $to, int $transitionId, string $transitionLabel, array $transitionMeta ): array { $transitionOutput = [sprintf('%s-->|%s|%s', $from, str_replace("\n", ' ', $this->escape($transitionLabel)), $to)]; $linkStyle = $this->styleLink($transitionMeta); if ('' !== $linkStyle) { $transitionOutput[] = $linkStyle; } ++$this->linkCount; return $transitionOutput; } private function styleWorkflowTransition( string $from, string $to, int $transitionId, string $transitionLabel, array $transitionMeta ) { $transitionOutput = []; $transitionLabel = $this->escape($transitionLabel); $transitionNodeName = 'transition'.$transitionId; $transitionOutput[] = sprintf('%s[%s]', $transitionNodeName, $transitionLabel); $transitionNodeStyle = $this->styleNode($transitionMeta, $transitionNodeName); if ('' !== $transitionNodeStyle) { $transitionOutput[] = $transitionNodeStyle; } $connectionStyle = '%s-->%s'; $transitionOutput[] = sprintf($connectionStyle, $from, $transitionNodeName); $linkStyle = $this->styleLink($transitionMeta); if ('' !== $linkStyle) { $transitionOutput[] = $linkStyle; } ++$this->linkCount; $transitionOutput[] = sprintf($connectionStyle, $transitionNodeName, $to); $linkStyle = $this->styleLink($transitionMeta); if ('' !== $linkStyle) { $transitionOutput[] = $linkStyle; } ++$this->linkCount; return $transitionOutput; } private function styleLink(array $transitionMeta): string { if (\array_key_exists('color', $transitionMeta)) { return sprintf('linkStyle %d stroke:%s', $this->linkCount, $transitionMeta['color']); } return ''; } }