Dopamedia/StateMachine

View on GitHub
Model/Graph/Drawer.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php
/**
 * Created by PhpStorm.
 * User: pandi
 * Date: 25.07.16
 * Time: 10:32
 */

namespace Dopamedia\StateMachine\Model\Graph;

use Dopamedia\StateMachine\Api\ProcessProcessInterface;
use Dopamedia\StateMachine\Api\ProcessStateInterface;
use Dopamedia\StateMachine\Api\ProcessTransitionInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Phrase;
use Dopamedia\StateMachine\Helper\Generator\StringGenerator;

class Drawer implements DrawerInterface
{
    const ATTRIBUTE_FONT_SIZE = 'fontsize';

    const EDGE_UPPER_HALF = 'upper half';
    const EDGE_LOWER_HALF = 'lower half';
    const EDGE_FULL = 'edge full';
    const HIGHLIGHT_COLOR = '#FFFFCC';
    const HAPPY_PATH_COLOR = '#70ab28';

    /**
     * @var array
     */
    protected $attributesProcess = [
        'fontname' => 'Verdana',
        'fillcolor' => '#cfcfcf',
        'style' => 'filled',
        'color' => '#ffffff',
        'fontsize' => 12,
        'fontcolor' => 'black'
    ];

    /**
     * @var array
     */
    protected $attributesState = [
        'fontname' => 'Verdana',
        'fontsize' => 14,
        'style' => 'filled',
        'fillcolor' => '#f9f9f9'
    ];

    /**
     * @var array
     */
    protected $attributesDiamond = [
        'fontname' => 'Verdana',
        'label' => '?',
        'shape' => 'diamond',
        'fontcolor' => 'white',
        'fontsize' => '1',
        'style' => 'filled',
        'fillcolor' => '#f9f9f9'
    ];

    /**
     * @var array
     */
    protected $attributesTransition = [
        'fontname' => 'Verdana',
        'fontsize' => 12
    ];

    /**
     * @var string
     */
    protected $brLeft = '<br align="left" />  ';

    /**
     * @var string
     */
    protected $notImplemented = '<font color="red">(not implemented)</font>';

    /**
     * @var string
     */
    protected $br = '<br/>';

    /**
     * @var string
     */
    protected $format = 'svg';

    /**
     * @var int
     */
    protected $fontSizeBig = null;

    /**
     * @var int
     */
    protected $fontSizeSmall = null;

    /**
     * @var \Dopamedia\StateMachine\Model\Graph\GraphInterface
     */
    protected $graph;

    /**
     * @var StringGenerator
     */
    protected $stringGenerator;

    /**
     * @param \Dopamedia\StateMachine\Model\Graph\GraphInterface $graph
     */
    public function __construct(
        \Dopamedia\StateMachine\Model\Graph\GraphInterface $graph,
        \Dopamedia\StateMachine\Helper\Generator\StringGenerator $stringGenerator
    )
    {
        $this->graph = $graph;
        $this->stringGenerator = $stringGenerator;
    }

    /**
     * @inheritDoc
     */
    public function draw(ProcessProcessInterface $process, $highlightState = null, $format = null, $fontSize = null)
    {
        $this->init($format, $fontSize);
        $this->drawClusters($process);
        $this->drawStates($process, $highlightState);
        $this->drawTransitions($process);

        return $this->graph->render($this->format);
    }

    /**
     * @inheritDoc
     */
    public function drawStates(ProcessProcessInterface $process, $highlightState = null)
    {
        $states = $process->getAllStates();
        foreach ($states as $state) {
            $isHighlighted = $highlightState === $state->getName();
            $this->addNode($state, [], null, $isHighlighted);
        }
    }

    /**
     * @inheritDoc
     */
    public function drawTransitions(ProcessProcessInterface $process)
    {
        $states = $process->getAllStates();
        foreach ($states as $state) {
            $this->drawTransitionsEvents($state);
            $this->drawTransitionsConditions($state);
        }
    }

    /**
     * @return string
     */
    protected function getDiamondId()
    {
        return $this->stringGenerator->generateRandomString(16);
    }

    /**
     * @inheritDoc
     */
    public function drawTransitionsEvents(ProcessStateInterface $state)
    {
        $events = $state->getEvents();
        foreach ($events as $event) {
            $transitions = $state->getOutgoingTransitionsByEvent($event);

            $currentTransition = current($transitions);
            if (!$currentTransition) {
                throw new LocalizedException(
                    new Phrase(
                        'Transitions container seems to be empty.'
                    )
                );
            }

            if (count($transitions) > 1) {
                $diamondId = $this->getDiamondId();

                $this->graph->addNode($diamondId, $this->attributesDiamond, $state->getProcess()->getName());
                $this->addEdge($currentTransition, self::EDGE_UPPER_HALF, [], null, $diamondId);

                foreach ($transitions as $transition) {
                    $this->addEdge($transition, self::EDGE_LOWER_HALF, [], $diamondId);
                }
            } else {
                $this->addEdge($currentTransition, self::EDGE_FULL);
            }
        }
    }

    /**
     * @param ProcessStateInterface $state
     *
     * @return void
     */
    public function drawTransitionsConditions(ProcessStateInterface $state)
    {
        $transitions = $state->getOutgoingTransitions();
        foreach ($transitions as $transition) {
            if ($transition->hasEvent()) {
                continue;
            }
            $this->addEdge($transition);
        }
    }

    /**
     * @inheritDoc
     */
    public function drawClusters(ProcessProcessInterface $process)
    {
        $processes = $process->getAllProcesses();
        foreach ($processes as $subProcess) {
            $group = $subProcess->getName();
            $attributes = $this->attributesProcess;
            $attributes['label'] = $group;
            $this->graph->addCluster($group, $attributes);
        }
    }

    /**
     * @param ProcessStateInterface $state
     * @param array $attributes
     * @param string|null $name
     * @param bool $highlighted
     *
     * @return void
     */
    protected function addNode(ProcessStateInterface $state, $attributes = [], $name = null, $highlighted = false)
    {
        $name = $name === null ? $state->getName() : $name;

        $label = [];
        $label[] = str_replace(' ', $this->br, trim($name));

        if ($state->hasFlags()) {
            $flags = implode(', ', $state->getFlags());
            $label[] = '<font color="violet" point-size="' . $this->fontSizeSmall . '">' . $flags . '</font>';
        }

        $attributes['label'] = implode($this->br, $label);

        if (!$state->hasOutgoingTransitions() || $this->hasOnlySelfReferences($state)) {
            $attributes['peripheries'] = 2;
        }

        if ($highlighted) {
            $attributes['fillcolor'] = self::HIGHLIGHT_COLOR;
        }

        $attributes = array_merge($this->attributesState, $attributes);
        $this->graph->addNode($name, $attributes, $state->getProcess()->getName());
    }

    /**
     * @param ProcessStateInterface $state
     *
     * @return bool
     */
    protected function hasOnlySelfReferences(ProcessStateInterface $state)
    {
        $hasOnlySelfReferences = true;
        $transitions = $state->getOutgoingTransitions();
        foreach ($transitions as $transition) {
            if ($transition->getTargetState()->getName() !== $state->getName()) {
                $hasOnlySelfReferences = false;
                break;
            }
        }
        return $hasOnlySelfReferences;
    }

    /**
     * @param ProcessTransitionInterface $transition
     * @param string $type
     * @param array $attributes
     * @param string|null $fromName
     * @param string|null $toName
     *
     * @return void
     */
    protected function addEdge(ProcessTransitionInterface $transition, $type = self::EDGE_FULL, $attributes = [], $fromName = null, $toName = null)
    {
        $label = [];

        if ($type !== self::EDGE_LOWER_HALF) {
            $label = $this->addEdgeEventText($transition, $label);
        }

        if ($type !== self::EDGE_UPPER_HALF) {
            $label = $this->addEdgeConditionText($transition, $label);
        }

        $label = $this->addEdgeElse($label);
        $fromName = $this->addEdgeFromState($transition, $fromName);
        $toName = $this->addEdgeToState($transition, $toName);
        $attributes = $this->addEdgeAttributes($transition, $attributes, $label, $type);

        $this->graph->addEdge($fromName, $toName, $attributes);
    }

    /**
     * @param ProcessTransitionInterface $transition
     * @param array $label
     *
     * @return array
     */
    protected function addEdgeConditionText(ProcessTransitionInterface $transition, array $label)
    {
        if ($transition->hasCondition()) {
            $conditionLabel = $transition->getCondition();

            #if (!isset($this->stateMachineHandler->getConditionPlugins()[$transition->getCondition()])) {
                $conditionLabel .= ' ' . $this->notImplemented;
            #}

            $label[] = $conditionLabel;
        }

        return $label;
    }

    /**
     * @param ProcessTransitionInterface $transition
     * @param array $label
     *
     * @return array
     */
    protected function addEdgeEventText(ProcessTransitionInterface $transition, array $label)
    {
        if ($transition->hasEvent()) {
            $event = $transition->getEvent();

            if ($event->isOnEnter()) {
                $label[] = '<b>' . $event->getName() . ' (on enter)</b>';
            } else {
                $label[] = '<b>' . $event->getName() . '</b>';
            }

            if ($event->hasTimeout()) {
                $label[] = 'timeout: ' . $event->getTimeout();
            }

            if ($event->hasCommand()) {
                $commandLabel = 'command:' . $event->getCommand();

                #if (!isset($this->stateMachineHandler->getCommandPlugins()[$event->getCommand()])) {
                    $commandLabel .= ' ' . $this->notImplemented;
                #}
                $label[] = $commandLabel;
            }

            if ($event->isManual()) {
                $label[] = 'manually executable';
            }
        } else {
            $label[] = '&infin;';
        }

        return $label;
    }

    /**
     * @param array $label
     *
     * @return string
     */
    protected function addEdgeElse(array $label)
    {
        if (!empty($label)) {
            $label = implode($this->brLeft, $label);
        } else {
            $label = 'else';
        }

        return $label;
    }

    /**
     * @param ProcessTransitionInterface $transition
     * @param array $attributes
     * @param string $label
     * @param string $type
     *
     * @return array
     */
    protected function addEdgeAttributes(ProcessTransitionInterface $transition, array $attributes, $label, $type = self::EDGE_FULL)
    {
        $attributes = array_merge($this->attributesTransition, $attributes);
        $attributes['label'] = '  ' . $label;

        if ($transition->hasEvent() === false) {
            $attributes['style'] = 'dashed';
        }

        if ($type === self::EDGE_FULL || $type === self::EDGE_UPPER_HALF) {
            if ($transition->hasEvent() && $transition->getEvent()->isOnEnter()) {
                $attributes['arrowtail'] = 'crow';
                $attributes['dir'] = 'both';
            }
        }

        if ($transition->isHappyCase()) {
            $attributes['weight'] = '100';
            $attributes['color'] = self::HAPPY_PATH_COLOR;
        } elseif ($transition->hasEvent()) {
            $attributes['weight'] = '10';
        } else {
            $attributes['weight'] = '1';
        }

        return $attributes;
    }

    /**
     * @param ProcessTransitionInterface $transition
     * @param string $fromName
     *
     * @return string
     */
    protected function addEdgeFromState(ProcessTransitionInterface $transition, $fromName)
    {
        $fromName = $fromName !== null ? $fromName : $transition->getSourceState()->getName();

        return $fromName;
    }

    /**
     * @param ProcessTransitionInterface $transition
     * @param string|null $toName
     *
     * @return string
     */
    protected function addEdgeToState(ProcessTransitionInterface $transition, $toName)
    {
        $toName = $toName !== null ? $toName : $transition->getTargetState()->getName();

        return $toName;
    }

    /**
     * @param string|null $format
     * @param int|null $fontSize
     */
    protected function init($format, $fontSize)
    {
        if ($format !== null) {
            $this->format = $format;
        }

        if ($fontSize !== null) {
            $this->attributesState[self::ATTRIBUTE_FONT_SIZE] = $fontSize;
            $this->attributesProcess[self::ATTRIBUTE_FONT_SIZE] = $fontSize - 2;
            $this->attributesTransition[self::ATTRIBUTE_FONT_SIZE] = $fontSize - 2;
            $this->fontSizeBig = $fontSize;
            $this->fontSizeSmall = $fontSize - 2;
        }
    }
}