Skip to content

Commit

Permalink
SDK-812: Added interactive workflow transition. (#196)
Browse files Browse the repository at this point in the history
* SDK-832 refactor workflow listener

* SDK-832 update composer dependency and revert some changes

* SDK-832 review fixes

* SDK-812: Interactive workflow resolver

* SDK-812: Created test for resolver

* SDK-723: updated composer

* SDK-812: Fixed issue after merging

* SDK-812: Created test, fixed code issues.

* SDK-812: Updated doc block

* SDK-812: Updated doc block

* SDK-812: Fixed run inited workflows

* SDK-812: Fixed message

Co-authored-by: Sergey Romankov <[email protected]>
  • Loading branch information
vol4onok and sergeyspryker authored Sep 15, 2022
1 parent a18c7dd commit 346c3cb
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 12 deletions.
14 changes: 14 additions & 0 deletions extension/Hello/config/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ framework:
settings:
failed: bye # transition name for filed result
successful: world # transition name for successful result
hello-interactive:
from: start
to: middle
metadata:
task: ~ # not neccessary to have a task for the interactive transition.
transitionResolver: # Resolver needs for resolve next transition
name: interactive # By setting interactive transitionResolver name current transition will be recognized as interactive and appropriate actions will be done.
settings:
question: 'What would you like to execute next?'
choices:
world: # Transition name should be enabled as a next transition in the current workflow.
description: Executes hello:php task # (optional). Will be printer in the console to provide more information to the user.
bye:
description: Executes bye:world task # (optional). Will be printer in the console to provide more information to the user.
world:
from: middle
to: end
Expand Down
14 changes: 14 additions & 0 deletions src/Extension/Exception/UniqueValueException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* Copyright © 2019-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace SprykerSdk\Sdk\Extension\Exception;

use LogicException;

class UniqueValueException extends LogicException
{
}
5 changes: 5 additions & 0 deletions src/Extension/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ services:
transition_boolean_resolver:
class: SprykerSdk\Sdk\Extension\Workflow\TransitionBooleanResolver
tags: ['workflow.transition_resolver']
interaction_answer_based_transition_resolver:
class: SprykerSdk\Sdk\Extension\Workflow\InteractionAnswerBasedTransitionResolver
arguments:
- '@cli_value_receiver'
tags: [ 'workflow.transition_resolver' ]

uname_env_var_processor:
class: SprykerSdk\Sdk\Extension\DependencyInjection\EnvVarProcessor\UnameEnvVarProcessor
Expand Down
109 changes: 109 additions & 0 deletions src/Extension/Workflow/InteractionAnswerBasedTransitionResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

/**
* Copyright © 2019-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace SprykerSdk\Sdk\Extension\Workflow;

use SprykerSdk\Sdk\Core\Application\Dto\ReceiverValue;
use SprykerSdk\Sdk\Extension\Exception\UniqueValueException;
use SprykerSdk\SdkContracts\Entity\ContextInterface;
use SprykerSdk\SdkContracts\ValueReceiver\ValueReceiverInterface;
use SprykerSdk\SdkContracts\Workflow\TransitionResolverInterface;

class InteractionAnswerBasedTransitionResolver implements TransitionResolverInterface
{
/**
* @var string
*/
public const QUESTION = 'question';

/**
* @var string
*/
public const CHOICES = 'choices';

/**
* @var string
*/
public const RESOLVER_NAME = 'interactive';

/**
* @var \SprykerSdk\SdkContracts\ValueReceiver\ValueReceiverInterface
*/
protected ValueReceiverInterface $cliValueReceiver;

/**
* @param \SprykerSdk\SdkContracts\ValueReceiver\ValueReceiverInterface $cliValueReceiver
*/
public function __construct(ValueReceiverInterface $cliValueReceiver)
{
$this->cliValueReceiver = $cliValueReceiver;
}

/**
* @return string
*/
public function getName(): string
{
return static::RESOLVER_NAME;
}

/**
* @param \SprykerSdk\SdkContracts\Entity\ContextInterface $context
* @param array $settings
*
* @return string|null
*/
public function resolveTransition(ContextInterface $context, array $settings): ?string
{
$choiceValues = $this->getChoiceValues($settings);
$flippedValues = array_flip($choiceValues);

$answer = $this->cliValueReceiver->receiveValue(new ReceiverValue(
$settings[static::QUESTION] ?? '',
null,
'string',
array_values($choiceValues),
));

return $flippedValues[$answer];
}

/**
* @param array $settings
*
* @throws \SprykerSdk\Sdk\Extension\Exception\UniqueValueException
*
* @return array<string, string>
*/
protected function getChoiceValues(array $settings): array
{
$choices = $settings[static::CHOICES] ?? [];
$choiceValues = [];
foreach ($choices as $transition => $choice) {
$choiceValues[(string)$transition] = (string)$choice['description'];
}

$duplicates = $this->getDuplicateDescriptions($choiceValues);
if ($duplicates) {
throw new UniqueValueException(sprintf('Descriptions for choices must be unique: `%s`', implode('`,`', $duplicates)));
}

return $choiceValues;
}

/**
* @param array<string, string> $choiceValues
*
* @return array<string>
*/
protected function getDuplicateDescriptions(array $choiceValues): array
{
$duplicates = array_diff_assoc($choiceValues, array_unique($choiceValues));

return array_unique($duplicates);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,6 @@ public function execute(TransitionEvent $event): void
protected function tryRunTask(TransitionEvent $event, WorkflowTransitionInterface $transition): void
{
$task = $this->getTransitionMeta($event, static::META_KEY_TASK);
if (!$task) {
return;
}

$shouldRunTask = in_array($transition->getState(), [
WorkflowTransitionInterface::WORKFLOW_TRANSITION_STARTED,
Expand All @@ -167,8 +164,9 @@ protected function tryRunTask(TransitionEvent $event, WorkflowTransitionInterfac
$allowToFail = $this->getTransitionMeta($event, static::META_ALLOW_TO_FAIL);

$context = $this->getContext($event);

$context = $this->taskExecutor->execute($context, $task);
if ($task) {
$context = $this->taskExecutor->execute($context, $task);
}
$resolvedNextTransition = $this->resolverNextTransition($event, $context);

if (!$allowToFail && !$resolvedNextTransition && $context->getExitCode() !== ContextInterface::SUCCESS_EXIT_CODE) {
Expand Down
22 changes: 15 additions & 7 deletions src/Presentation/Console/Command/RunWorkflowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace SprykerSdk\Sdk\Presentation\Console\Command;

use SprykerSdk\Sdk\Core\Application\Dependency\ContextFactoryInterface;
use SprykerSdk\Sdk\Core\Application\Dependency\ProjectSettingRepositoryInterface;
use SprykerSdk\Sdk\Core\Application\Dto\ReceiverValue;
use SprykerSdk\Sdk\Core\Application\Service\ProjectWorkflow;
use SprykerSdk\Sdk\Infrastructure\Service\CliValueReceiver;
Expand Down Expand Up @@ -62,22 +63,31 @@ class RunWorkflowCommand extends Command
*/
protected ContextFactoryInterface $contextFactory;

/**
* @var \SprykerSdk\Sdk\Core\Application\Dependency\ProjectSettingRepositoryInterface
*/
protected ProjectSettingRepositoryInterface $projectSettingRepository;

/**
* @param \SprykerSdk\Sdk\Core\Application\Service\ProjectWorkflow $projectWorkflow
* @param \SprykerSdk\Sdk\Infrastructure\Service\CliValueReceiver $cliValueReceiver
* @param \SprykerSdk\Sdk\Infrastructure\Service\WorkflowRunner $workflowRunner
* @param \SprykerSdk\Sdk\Core\Application\Dependency\ContextFactoryInterface $contextFactory
* @param \SprykerSdk\Sdk\Core\Application\Dependency\ProjectSettingRepositoryInterface $projectSettingRepository
*/
public function __construct(
ProjectWorkflow $projectWorkflow,
CliValueReceiver $cliValueReceiver,
WorkflowRunner $workflowRunner,
ContextFactoryInterface $contextFactory
ContextFactoryInterface $contextFactory,
ProjectSettingRepositoryInterface $projectSettingRepository
) {
$this->projectWorkflow = $projectWorkflow;
$this->cliValueReceiver = $cliValueReceiver;
$this->workflowRunner = $workflowRunner;
$this->contextFactory = $contextFactory;
$this->projectSettingRepository = $projectSettingRepository;

parent::__construct(static::NAME);
}

Expand Down Expand Up @@ -106,18 +116,16 @@ public function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string|null $workflowName */
$workflowName = $input->getArgument(static::ARG_WORKFLOW_NAME);
$projectWorkflows = (array)$this->projectSettingRepository->getOneByPath(ProjectWorkflow::WORKFLOW)->getValues();

$initializedWorkflows = $this->projectWorkflow->findInitializedWorkflows();

if ($workflowName && $initializedWorkflows && !in_array($workflowName, $initializedWorkflows)) {
$output->writeln('<error>You haven\'t initialized a workflow with "sdk:workflow:init"' .
' or you don\'t have any open tasks defined</error>');
if ($workflowName && !in_array($workflowName, $projectWorkflows)) {
$output->writeln('<error>You don\'t have any active a workflows".</error>');

return static::FAILURE;
}

if (!$workflowName) {
$workflows = $initializedWorkflows ?: $this->projectWorkflow->getProjectWorkflows() ?: $this->projectWorkflow->getAll();
$workflows = $projectWorkflows ?: $this->projectWorkflow->getProjectWorkflows() ?: $this->projectWorkflow->getAll();
$workflowName = count($workflows) > 1 ? $this->cliValueReceiver->receiveValue(
new ReceiverValue(
'You have more than one initialized workflow. You have to select one.',
Expand Down
1 change: 1 addition & 0 deletions src/Presentation/Console/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
- "@cli_value_receiver"
- "@workflow_runner"
- "@context_factory"
- "@project_setting_repository"
qa_automation_command:
autowire: false
class: SprykerSdk\Sdk\Presentation\Console\Command\QaAutomationCommand
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* Copyright © 2019-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace SprykerSdk\Sdk\Unit\Extension\Workflow;

use Codeception\Test\Unit;
use SprykerSdk\Sdk\Extension\Exception\UniqueValueException;
use SprykerSdk\Sdk\Extension\Workflow\InteractionAnswerBasedTransitionResolver;
use SprykerSdk\SdkContracts\Entity\ContextInterface;
use SprykerSdk\SdkContracts\ValueReceiver\ValueReceiverInterface;

/**
* @group Sdk
* @group Extension
* @group Workflow
* @group InteractionAnswerBasedTransitionResolverTest
*/
class InteractionAnswerBasedTransitionResolverTest extends Unit
{
/**
* @return void
*/
public function testResolveTransitionFailed(): void
{
// Arrange
$valueReceiver = $this->createMock(ValueReceiverInterface::class);
$valueReceiver->expects($this->once())
->method('receiveValue')
->willReturn('Choice two description.');
$context = $this->createMock(ContextInterface::class);

$resolveTransition = new InteractionAnswerBasedTransitionResolver($valueReceiver);

$settings = [
InteractionAnswerBasedTransitionResolver::QUESTION => 'Interactive question',
InteractionAnswerBasedTransitionResolver::CHOICES => [
'choice one' => [
'description' => 'Choice one description.',
],
'choice two' => [
'description' => 'Choice two description.',
],
],
];

// Act
$transition = $resolveTransition->resolveTransition($context, $settings);

// Assert
$this->assertSame('choice two', $transition);
}

/**
* @return void
*/
public function testResolveTransitionWithEqualDescription(): void
{
// Arrange
$valueReceiver = $this->createMock(ValueReceiverInterface::class);
$context = $this->createMock(ContextInterface::class);

$resolveTransition = new InteractionAnswerBasedTransitionResolver($valueReceiver);

$settings = [
InteractionAnswerBasedTransitionResolver::QUESTION => 'Interactive question',
InteractionAnswerBasedTransitionResolver::CHOICES => [
'choice one' => [
'description' => 'Choice one description.',
],
'choice two' => [
'description' => 'Choice one description.',
],
],
];

// Assert
$this->expectException(UniqueValueException::class);

// Act
$resolveTransition->resolveTransition($context, $settings);
}
}

0 comments on commit 346c3cb

Please sign in to comment.