├── .gitignore ├── .travis.yml ├── Bridge └── Twig │ └── Extension │ └── WorkflowExtension.php ├── Bundle └── WorkflowBundle │ ├── Command │ └── WorkflowDumpCommand.php │ ├── DependencyInjection │ ├── Compiler │ │ └── ValidateWorkflowsPass.php │ ├── Configuration.php │ └── WorkflowExtension.php │ ├── Resources │ ├── config │ │ └── workflow.xml │ └── schema │ │ └── workflow-1.0.xsd │ ├── Tests │ └── DependencyInjection │ │ ├── Fixtures │ │ ├── php │ │ │ ├── workflow_with_arguments_and_service.php │ │ │ ├── workflow_with_type_and_service.php │ │ │ └── workflows.php │ │ ├── xml │ │ │ ├── workflow_with_arguments_and_service.xml │ │ │ ├── workflow_with_type_and_service.xml │ │ │ └── workflows.xml │ │ └── yml │ │ │ ├── workflow_with_arguments_and_service.yml │ │ │ ├── workflow_with_type_and_service.yml │ │ │ └── workflows.yml │ │ ├── PhpWorkflowExtensionTest.php │ │ ├── WorkflowExtensionTest.php │ │ ├── XmlWorkflowExtensionTest.php │ │ └── YamlWorkflowExtensionTest.php │ └── WorkflowBundle.php ├── LICENSE ├── README.md ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - hhvm-nightly 7 | 8 | matrix: 9 | allow_failures: 10 | - php: hhvm-nightly 11 | 12 | before_script: 13 | - COMPOSER_ROOT_VERSION=dev-master composer --prefer-source --dev install 14 | 15 | script: 16 | - phpunit --coverage-text -------------------------------------------------------------------------------- /Bridge/Twig/Extension/WorkflowExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bridge\Twig\Extension; 13 | 14 | use Symfony\Component\Workflow\Registry; 15 | 16 | /** 17 | * WorkflowExtension. 18 | * 19 | * @author Grégoire Pineau 20 | */ 21 | class WorkflowExtension extends \Twig_Extension 22 | { 23 | private $workflowRegistry; 24 | 25 | public function __construct(Registry $workflowRegistry) 26 | { 27 | $this->workflowRegistry = $workflowRegistry; 28 | } 29 | 30 | public function getFunctions() 31 | { 32 | return array( 33 | new \Twig_SimpleFunction('workflow_can', array($this, 'canTransition')), 34 | new \Twig_SimpleFunction('workflow_transitions', array($this, 'getEnabledTransitions')), 35 | ); 36 | } 37 | 38 | public function canTransition($object, $transition, $name = null) 39 | { 40 | return $this->workflowRegistry->get($object, $name)->can($object, $transition); 41 | } 42 | 43 | public function getEnabledTransitions($object, $name = null) 44 | { 45 | return $this->workflowRegistry->get($object, $name)->getEnabledTransitions($object); 46 | } 47 | 48 | public function getName() 49 | { 50 | return 'workflow'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Command/WorkflowDumpCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\Command; 13 | 14 | use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; 15 | use Symfony\Component\Console\Input\InputArgument; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | use Symfony\Component\Workflow\Dumper\GraphvizDumper; 19 | use Symfony\Component\Workflow\Marking; 20 | 21 | /** 22 | * @author Grégoire Pineau 23 | */ 24 | class WorkflowDumpCommand extends ContainerAwareCommand 25 | { 26 | public function isEnabled() 27 | { 28 | return $this->getContainer()->has('workflow.registry'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function configure() 35 | { 36 | $this 37 | ->setName('workflow:dump') 38 | ->setDefinition(array( 39 | new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'), 40 | new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'), 41 | )) 42 | ->setDescription('Dump a workflow') 43 | ->setHelp(<<<'EOF' 44 | The %command.name% command dumps the graphical representation of a 45 | workflow in DOT format 46 | 47 | %command.full_name% | dot -Tpng > workflow.png 48 | 49 | EOF 50 | ) 51 | ; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function execute(InputInterface $input, OutputInterface $output) 58 | { 59 | $container = $this->getContainer(); 60 | $serviceId = $input->getArgument('name'); 61 | if ($container->has('workflow.'.$serviceId)) { 62 | $workflow = $container->get('workflow.'.$serviceId); 63 | } elseif ($container->has('state_machine.'.$serviceId)) { 64 | $workflow = $container->get('state_machine.'.$serviceId); 65 | } else { 66 | throw new \InvalidArgumentException(sprintf('No service found for "workflow.%1$s" nor "state_machine.%1$s".', $serviceId)); 67 | } 68 | 69 | $dumper = new GraphvizDumper(); 70 | $marking = new Marking(); 71 | 72 | foreach ($input->getArgument('marking') as $place) { 73 | $marking->mark($place); 74 | } 75 | 76 | $output->writeln($dumper->dump($workflow->getDefinition(), $marking)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Exception\RuntimeException; 17 | use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface; 18 | use Symfony\Component\Workflow\Validator\StateMachineValidator; 19 | use Symfony\Component\Workflow\Validator\WorkflowValidator; 20 | 21 | /** 22 | * @author Tobias Nyholm 23 | */ 24 | class ValidateWorkflowsPass implements CompilerPassInterface 25 | { 26 | public function process(ContainerBuilder $container) 27 | { 28 | $taggedServices = $container->findTaggedServiceIds('workflow.definition'); 29 | foreach ($taggedServices as $id => $tags) { 30 | $definition = $container->get($id); 31 | foreach ($tags as $tag) { 32 | if (!array_key_exists('name', $tag)) { 33 | throw new RuntimeException(sprintf('The "name" for the tag "workflow.definition" of service "%s" must be set.', $id)); 34 | } 35 | if (!array_key_exists('type', $tag)) { 36 | throw new RuntimeException(sprintf('The "type" for the tag "workflow.definition" of service "%s" must be set.', $id)); 37 | } 38 | if (!array_key_exists('marking_store', $tag)) { 39 | throw new RuntimeException(sprintf('The "marking_store" for the tag "workflow.definition" of service "%s" must be set.', $id)); 40 | } 41 | 42 | $this->createValidator($tag)->validate($definition, $tag['name']); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * @param array $tag 49 | * 50 | * @return DefinitionValidatorInterface 51 | */ 52 | private function createValidator($tag) 53 | { 54 | if ('state_machine' === $tag['type']) { 55 | return new StateMachineValidator(); 56 | } 57 | 58 | if ('single_state' === $tag['marking_store']) { 59 | return new WorkflowValidator(true); 60 | } 61 | 62 | return new WorkflowValidator(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | 17 | /** 18 | * FrameworkExtension configuration structure. 19 | * 20 | * @author Grégoire Pineau 21 | */ 22 | class Configuration implements ConfigurationInterface 23 | { 24 | /** 25 | * Generates the configuration tree builder. 26 | * 27 | * @return TreeBuilder The tree builder 28 | */ 29 | public function getConfigTreeBuilder() 30 | { 31 | $treeBuilder = new TreeBuilder(); 32 | $rootNode = $treeBuilder->root('workflow'); 33 | 34 | $rootNode 35 | ->fixXmlConfig('workflow') 36 | ->children() 37 | ->arrayNode('workflows') 38 | ->useAttributeAsKey('name') 39 | ->prototype('array') 40 | ->fixXmlConfig('support') 41 | ->fixXmlConfig('place') 42 | ->fixXmlConfig('transition') 43 | ->children() 44 | ->enumNode('type') 45 | ->values(array('workflow', 'state_machine')) 46 | ->defaultValue('workflow') 47 | ->end() 48 | ->arrayNode('marking_store') 49 | ->fixXmlConfig('argument') 50 | ->children() 51 | ->enumNode('type') 52 | ->values(array('multiple_state', 'single_state')) 53 | ->end() 54 | ->arrayNode('arguments') 55 | ->beforeNormalization() 56 | ->ifString() 57 | ->then(function ($v) { return array($v); }) 58 | ->end() 59 | ->requiresAtLeastOneElement() 60 | ->prototype('scalar') 61 | ->end() 62 | ->end() 63 | ->scalarNode('service') 64 | ->cannotBeEmpty() 65 | ->end() 66 | ->end() 67 | ->validate() 68 | ->ifTrue(function ($v) { return isset($v['type']) && isset($v['service']); }) 69 | ->thenInvalid('"type" and "service" cannot be used together.') 70 | ->end() 71 | ->validate() 72 | ->ifTrue(function ($v) { return !empty($v['arguments']) && isset($v['service']); }) 73 | ->thenInvalid('"arguments" and "service" cannot be used together.') 74 | ->end() 75 | ->end() 76 | ->arrayNode('supports') 77 | ->isRequired() 78 | ->beforeNormalization() 79 | ->ifString() 80 | ->then(function ($v) { return array($v); }) 81 | ->end() 82 | ->prototype('scalar') 83 | ->cannotBeEmpty() 84 | ->validate() 85 | ->ifTrue(function ($v) { return !class_exists($v); }) 86 | ->thenInvalid('The supported class %s does not exist.') 87 | ->end() 88 | ->end() 89 | ->end() 90 | ->scalarNode('initial_place')->defaultNull()->end() 91 | ->arrayNode('places') 92 | ->isRequired() 93 | ->requiresAtLeastOneElement() 94 | ->prototype('scalar') 95 | ->cannotBeEmpty() 96 | ->end() 97 | ->end() 98 | ->arrayNode('transitions') 99 | ->useAttributeAsKey('name') 100 | ->isRequired() 101 | ->requiresAtLeastOneElement() 102 | ->prototype('array') 103 | ->children() 104 | ->arrayNode('from') 105 | ->beforeNormalization() 106 | ->ifString() 107 | ->then(function ($v) { return array($v); }) 108 | ->end() 109 | ->requiresAtLeastOneElement() 110 | ->prototype('scalar') 111 | ->cannotBeEmpty() 112 | ->end() 113 | ->end() 114 | ->arrayNode('to') 115 | ->beforeNormalization() 116 | ->ifString() 117 | ->then(function ($v) { return array($v); }) 118 | ->end() 119 | ->requiresAtLeastOneElement() 120 | ->prototype('scalar') 121 | ->cannotBeEmpty() 122 | ->end() 123 | ->end() 124 | ->end() 125 | ->end() 126 | ->end() 127 | ->end() 128 | ->end() 129 | ->end() 130 | ->end() 131 | ; 132 | 133 | return $treeBuilder; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/DependencyInjection/WorkflowExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\DependencyInjection; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Definition; 16 | use Symfony\Component\DependencyInjection\DefinitionDecorator; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 19 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 20 | use Symfony\Component\Config\FileLocator; 21 | use Symfony\Component\Workflow; 22 | 23 | /** 24 | * WorkflowExtension. 25 | * 26 | * @author Grégoire Pineau 27 | */ 28 | class WorkflowExtension extends Extension 29 | { 30 | /** 31 | * Responds to the app.config configuration parameter. 32 | * 33 | * @param array $configs 34 | * @param ContainerBuilder $container 35 | */ 36 | public function load(array $configs, ContainerBuilder $container) 37 | { 38 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 39 | 40 | $configuration = $this->getConfiguration($configs, $container); 41 | $config = $this->processConfiguration($configuration, $configs); 42 | 43 | $this->registerWorkflowConfiguration($config['workflows'], $container, $loader); 44 | } 45 | 46 | /** 47 | * Loads the workflow configuration. 48 | * 49 | * @param array $workflows A workflow configuration array 50 | * @param ContainerBuilder $container A ContainerBuilder instance 51 | * @param XmlFileLoader $loader An XmlFileLoader instance 52 | */ 53 | private function registerWorkflowConfiguration(array $workflows, ContainerBuilder $container, XmlFileLoader $loader) 54 | { 55 | if (!$workflows) { 56 | return; 57 | } 58 | $loader->load('workflow.xml'); 59 | $registryDefinition = $container->getDefinition('workflow.registry'); 60 | foreach ($workflows as $name => $workflow) { 61 | $type = $workflow['type']; 62 | $transitions = array(); 63 | foreach ($workflow['transitions'] as $transitionName => $transition) { 64 | if ($type === 'workflow') { 65 | $transitions[] = new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to'])); 66 | } elseif ($type === 'state_machine') { 67 | foreach ($transition['from'] as $from) { 68 | foreach ($transition['to'] as $to) { 69 | $transitions[] = new Definition(Workflow\Transition::class, array($transitionName, $from, $to)); 70 | } 71 | } 72 | } 73 | } 74 | // Create a Definition 75 | $definitionDefinition = new Definition(Workflow\Definition::class); 76 | $definitionDefinition->setPublic(false); 77 | $definitionDefinition->addArgument($workflow['places']); 78 | $definitionDefinition->addArgument($transitions); 79 | $definitionDefinition->addTag('workflow.definition', array( 80 | 'name' => $name, 81 | 'type' => $type, 82 | 'marking_store' => isset($workflow['marking_store']['type']) ? $workflow['marking_store']['type'] : null, 83 | )); 84 | if (isset($workflow['initial_place'])) { 85 | $definitionDefinition->addArgument($workflow['initial_place']); 86 | } 87 | // Create MarkingStore 88 | if (isset($workflow['marking_store']['type'])) { 89 | $parentDefinitionId = 'workflow.marking_store.' . $workflow['marking_store']['type']; 90 | $markingStoreDefinition = new DefinitionDecorator($parentDefinitionId); 91 | foreach ($workflow['marking_store']['arguments'] as $argument) { 92 | $markingStoreDefinition->addArgument($argument); 93 | } 94 | // explicitly set parent class to decorated definition in order to fix inconsistent behavior for <=2.7 95 | // see https://github.com/symfony/symfony/issues/17353 and https://github.com/symfony/symfony/pull/15096 96 | $markingStoreDefinition->setClass($container->getDefinition($parentDefinitionId)->getClass()); 97 | } elseif (isset($workflow['marking_store']['service'])) { 98 | $markingStoreDefinition = new Reference($workflow['marking_store']['service']); 99 | } 100 | 101 | // Create Workflow 102 | $workflowDefinition = new DefinitionDecorator(sprintf('%s.abstract', $type)); 103 | $workflowDefinition->replaceArgument(0, $definitionDefinition); 104 | if (isset($markingStoreDefinition)) { 105 | $workflowDefinition->replaceArgument(1, $markingStoreDefinition); 106 | } 107 | $workflowDefinition->replaceArgument(3, $name); 108 | // Store to container 109 | $workflowId = sprintf('%s.%s', $type, $name); 110 | $container->setDefinition($workflowId, $workflowDefinition); 111 | $container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition); 112 | // Add workflow to Registry 113 | foreach ($workflow['supports'] as $supportedClass) { 114 | $registryDefinition->addMethodCall('add', array(new Reference($workflowId), $supportedClass)); 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Returns the base path for the XSD files. 121 | * 122 | * @return string The XSD base path 123 | */ 124 | public function getXsdValidationBasePath() 125 | { 126 | return __DIR__.'/../Resources/config/schema'; 127 | } 128 | 129 | public function getNamespace() 130 | { 131 | return 'http://symfony.com/schema/dic/workflow'; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Resources/config/workflow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | null 11 | 12 | 13 | 14 | 15 | 16 | null 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Resources/schema/workflow-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_arguments_and_service.php: -------------------------------------------------------------------------------- 1 | loadFromExtension('workflow', array( 6 | 'workflows' => array( 7 | 'my_workflow' => array( 8 | 'marking_store' => array( 9 | 'arguments' => array('a', 'b'), 10 | 'service' => 'workflow_service', 11 | ), 12 | 'supports' => array( 13 | FrameworkExtensionTest::class, 14 | ), 15 | 'places' => array( 16 | 'first', 17 | 'last', 18 | ), 19 | 'transitions' => array( 20 | 'go' => array( 21 | 'from' => array( 22 | 'first', 23 | ), 24 | 'to' => array( 25 | 'last', 26 | ), 27 | ), 28 | ), 29 | ), 30 | ), 31 | )); 32 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_type_and_service.php: -------------------------------------------------------------------------------- 1 | loadFromExtension('workflow', array( 6 | 'workflows' => array( 7 | 'my_workflow' => array( 8 | 'marking_store' => array( 9 | 'type' => 'multiple_state', 10 | 'service' => 'workflow_service', 11 | ), 12 | 'supports' => array( 13 | FrameworkExtensionTest::class, 14 | ), 15 | 'places' => array( 16 | 'first', 17 | 'last', 18 | ), 19 | 'transitions' => array( 20 | 'go' => array( 21 | 'from' => array( 22 | 'first', 23 | ), 24 | 'to' => array( 25 | 'last', 26 | ), 27 | ), 28 | ), 29 | ), 30 | ), 31 | )); 32 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/php/workflows.php: -------------------------------------------------------------------------------- 1 | loadFromExtension('workflow', array( 6 | 'workflows' => array( 7 | 'article' => array( 8 | 'type' => 'workflow', 9 | 'marking_store' => array( 10 | 'type' => 'multiple_state', 11 | ), 12 | 'supports' => array( 13 | FrameworkExtensionTest::class, 14 | ), 15 | 'initial_place' => 'draft', 16 | 'places' => array( 17 | 'draft', 18 | 'wait_for_journalist', 19 | 'approved_by_journalist', 20 | 'wait_for_spellchecker', 21 | 'approved_by_spellchecker', 22 | 'published', 23 | ), 24 | 'transitions' => array( 25 | 'request_review' => array( 26 | 'from' => 'draft', 27 | 'to' => array('wait_for_journalist', 'wait_for_spellchecker'), 28 | ), 29 | 'journalist_approval' => array( 30 | 'from' => 'wait_for_journalist', 31 | 'to' => 'approved_by_journalist', 32 | ), 33 | 'spellchecker_approval' => array( 34 | 'from' => 'wait_for_spellchecker', 35 | 'to' => 'approved_by_spellchecker', 36 | ), 37 | 'publish' => array( 38 | 'from' => array('approved_by_journalist', 'approved_by_spellchecker'), 39 | 'to' => 'published', 40 | ), 41 | ), 42 | ), 43 | 'pull_request' => array( 44 | 'type' => 'state_machine', 45 | 'marking_store' => array( 46 | 'type' => 'single_state', 47 | ), 48 | 'supports' => array( 49 | FrameworkExtensionTest::class, 50 | ), 51 | 'initial_place' => 'start', 52 | 'places' => array( 53 | 'start', 54 | 'coding', 55 | 'travis', 56 | 'review', 57 | 'merged', 58 | 'closed', 59 | ), 60 | 'transitions' => array( 61 | 'submit' => array( 62 | 'from' => 'start', 63 | 'to' => 'travis', 64 | ), 65 | 'update' => array( 66 | 'from' => array('coding', 'travis', 'review'), 67 | 'to' => 'travis', 68 | ), 69 | 'wait_for_review' => array( 70 | 'from' => 'travis', 71 | 'to' => 'review', 72 | ), 73 | 'request_change' => array( 74 | 'from' => 'review', 75 | 'to' => 'coding', 76 | ), 77 | 'accept' => array( 78 | 'from' => 'review', 79 | 'to' => 'merged', 80 | ), 81 | 'reject' => array( 82 | 'from' => 'review', 83 | 'to' => 'closed', 84 | ), 85 | 'reopen' => array( 86 | 'from' => 'closed', 87 | 'to' => 'review', 88 | ), 89 | ), 90 | ), 91 | 'service_marking_store_workflow' => array( 92 | 'marking_store' => array( 93 | 'service' => 'workflow_service', 94 | ), 95 | 'supports' => array( 96 | FrameworkExtensionTest::class, 97 | ), 98 | 'places' => array( 99 | 'first', 100 | 'last', 101 | ), 102 | 'transitions' => array( 103 | 'go' => array( 104 | 'from' => 'first', 105 | 'to' => 'last', 106 | ), 107 | ), 108 | ), 109 | ), 110 | )); 111 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_arguments_and_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | a 12 | a 13 | 14 | Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 15 | first 16 | last 17 | 18 | a 19 | a 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_type_and_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 12 | first 13 | last 14 | 15 | a 16 | a 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | a 12 | a 13 | 14 | Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 15 | draft 16 | wait_for_journalist 17 | approved_by_journalist 18 | wait_for_spellchecker 19 | approved_by_spellchecker 20 | published 21 | 22 | draft 23 | wait_for_journalist 24 | wait_for_spellchecker 25 | 26 | 27 | wait_for_journalist 28 | approved_by_journalist 29 | 30 | 31 | wait_for_spellcheker 32 | approved_by_spellchker 33 | 34 | 35 | approved_by_journalist 36 | approved_by_spellchker 37 | published 38 | 39 | 40 | 41 | 42 | 43 | Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 44 | start 45 | coding 46 | travis 47 | review 48 | merged 49 | closed 50 | 51 | start 52 | travis 53 | 54 | 55 | coding 56 | travis 57 | review 58 | travis 59 | 60 | 61 | travis 62 | review 63 | 64 | 65 | review 66 | coding 67 | 68 | 69 | review 70 | merged 71 | 72 | 73 | review 74 | closed 75 | 76 | 77 | closed 78 | review 79 | 80 | 81 | 82 | 83 | 84 | Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 85 | first 86 | last 87 | 88 | first 89 | last 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_arguments_and_service.yml: -------------------------------------------------------------------------------- 1 | workflow: 2 | workflows: 3 | my_workflow: 4 | marking_store: 5 | arguments: 6 | - a 7 | - b 8 | service: workflow_service 9 | supports: 10 | - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 11 | places: 12 | - first 13 | - last 14 | transitions: 15 | go: 16 | from: 17 | - first 18 | to: 19 | - last 20 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_type_and_service.yml: -------------------------------------------------------------------------------- 1 | workflow: 2 | workflows: 3 | my_workflow: 4 | marking_store: 5 | type: multiple_state 6 | service: workflow_service 7 | supports: 8 | - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 9 | places: 10 | - first 11 | - last 12 | transitions: 13 | go: 14 | from: 15 | - first 16 | to: 17 | - last 18 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml: -------------------------------------------------------------------------------- 1 | workflow: 2 | workflows: 3 | article: 4 | type: workflow 5 | marking_store: 6 | type: multiple_state 7 | supports: 8 | - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 9 | initial_place: draft 10 | places: 11 | - draft 12 | - wait_for_journalist 13 | - approved_by_journalist 14 | - wait_for_spellchecker 15 | - approved_by_spellchecker 16 | - published 17 | transitions: 18 | request_review: 19 | from: [draft] 20 | to: [wait_for_journalist, wait_for_spellchecker] 21 | journalist_approval: 22 | from: [wait_for_journalist] 23 | to: [approved_by_journalist] 24 | spellchecker_approval: 25 | from: [wait_for_spellchecker] 26 | to: [approved_by_spellchecker] 27 | publish: 28 | from: [approved_by_journalist, approved_by_spellchecker] 29 | to: [published] 30 | pull_request: 31 | type: state_machine 32 | marking_store: 33 | type: single_state 34 | supports: 35 | - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 36 | initial_place: start 37 | places: 38 | - start 39 | - coding 40 | - travis 41 | - review 42 | - merged 43 | - closed 44 | transitions: 45 | submit: 46 | from: start 47 | to: travis 48 | update: 49 | from: [coding, travis, review] 50 | to: travis 51 | wait_for_review: 52 | from: travis 53 | to: review 54 | request_change: 55 | from: review 56 | to: coding 57 | accept: 58 | from: review 59 | to: merged 60 | reject: 61 | from: review 62 | to: closed 63 | reopen: 64 | from: closed 65 | to: review 66 | service_marking_store_workflow: 67 | marking_store: 68 | service: workflow_service 69 | supports: 70 | - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest 71 | places: 72 | - first 73 | - last 74 | transitions: 75 | go: 76 | from: 77 | - first 78 | to: 79 | - last 80 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/PhpWorkflowExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\Tests\DependencyInjection; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 16 | use Symfony\Component\Config\FileLocator; 17 | 18 | class PhpWorkflowExtensionTest extends WorkflowExtensionTest 19 | { 20 | protected function loadFromFile(ContainerBuilder $container, $file) 21 | { 22 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/Fixtures/php')); 23 | $loader->load($file.'.php'); 24 | } 25 | } -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/WorkflowExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\Tests\DependencyInjection; 13 | 14 | use Symfony\Bundle\WorkflowBundle\DependencyInjection\WorkflowExtension; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | 19 | abstract class WorkflowExtensionTest extends \PHPUnit_Framework_TestCase 20 | { 21 | private static $containerCache = array(); 22 | 23 | abstract protected function loadFromFile(ContainerBuilder $container, $file); 24 | 25 | public function testWorkflows() 26 | { 27 | $container = $this->createContainerFromFile('workflows'); 28 | $this->assertTrue($container->hasDefinition('workflow.article', 'Workflow is registered as a service')); 29 | $this->assertTrue($container->hasDefinition('workflow.article.definition', 'Workflow definition is registered as a service')); 30 | $workflowDefinition = $container->getDefinition('workflow.article.definition'); 31 | $this->assertSame( 32 | array( 33 | 'draft', 34 | 'wait_for_journalist', 35 | 'approved_by_journalist', 36 | 'wait_for_spellchecker', 37 | 'approved_by_spellchecker', 38 | 'published', 39 | ), 40 | $workflowDefinition->getArgument(0), 41 | 'Places are passed to the workflow definition' 42 | ); 43 | $this->assertSame(array('workflow.definition' => array(array('name' => 'article', 'type' => 'workflow', 'marking_store' => 'multiple_state'))), $workflowDefinition->getTags()); 44 | $this->assertTrue($container->hasDefinition('state_machine.pull_request', 'State machine is registered as a service')); 45 | $this->assertTrue($container->hasDefinition('state_machine.pull_request.definition', 'State machine definition is registered as a service')); 46 | $this->assertCount(4, $workflowDefinition->getArgument(1)); 47 | $this->assertSame('draft', $workflowDefinition->getArgument(2)); 48 | $stateMachineDefinition = $container->getDefinition('state_machine.pull_request.definition'); 49 | $this->assertSame( 50 | array( 51 | 'start', 52 | 'coding', 53 | 'travis', 54 | 'review', 55 | 'merged', 56 | 'closed', 57 | ), 58 | $stateMachineDefinition->getArgument(0), 59 | 'Places are passed to the state machine definition' 60 | ); 61 | $this->assertSame(array('workflow.definition' => array(array('name' => 'pull_request', 'type' => 'state_machine', 'marking_store' => 'single_state'))), $stateMachineDefinition->getTags()); 62 | $this->assertCount(9, $stateMachineDefinition->getArgument(1)); 63 | $this->assertSame('start', $stateMachineDefinition->getArgument(2)); 64 | 65 | $serviceMarkingStoreWorkflowDefinition = $container->getDefinition('workflow.service_marking_store_workflow'); 66 | /** @var Reference $markingStoreRef */ 67 | $markingStoreRef = $serviceMarkingStoreWorkflowDefinition->getArgument(1); 68 | $this->assertInstanceOf(Reference::class, $markingStoreRef); 69 | $this->assertEquals('workflow_service', (string) $markingStoreRef); 70 | } 71 | /** 72 | * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException 73 | * @expectedExceptionMessage "type" and "service" cannot be used together. 74 | */ 75 | public function testWorkflowCannotHaveBothTypeAndService() 76 | { 77 | $this->createContainerFromFile('workflow_with_type_and_service'); 78 | } 79 | /** 80 | * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException 81 | * @expectedExceptionMessage "arguments" and "service" cannot be used together. 82 | */ 83 | public function testWorkflowCannotHaveBothArgumentsAndService() 84 | { 85 | $this->createContainerFromFile('workflow_with_arguments_and_service'); 86 | } 87 | 88 | 89 | protected function createContainer(array $data = array()) 90 | { 91 | return new ContainerBuilder(new ParameterBag(array_merge(array( 92 | 'kernel.bundles' => array('FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'), 93 | 'kernel.cache_dir' => __DIR__, 94 | 'kernel.debug' => false, 95 | 'kernel.environment' => 'test', 96 | 'kernel.name' => 'kernel', 97 | 'kernel.root_dir' => __DIR__, 98 | ), $data))); 99 | } 100 | 101 | protected function createContainerFromFile($file, $data = array(), $resetCompilerPasses = true) 102 | { 103 | $cacheKey = md5(get_class($this).$file.serialize($data)); 104 | if (isset(self::$containerCache[$cacheKey])) { 105 | return self::$containerCache[$cacheKey]; 106 | } 107 | $container = $this->createContainer($data); 108 | $container->registerExtension(new WorkflowExtension()); 109 | $this->loadFromFile($container, $file); 110 | 111 | if ($resetCompilerPasses) { 112 | $container->getCompilerPassConfig()->setOptimizationPasses(array()); 113 | $container->getCompilerPassConfig()->setRemovingPasses(array()); 114 | } 115 | $container->compile(); 116 | 117 | return self::$containerCache[$cacheKey] = $container; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/XmlWorkflowExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\Tests\DependencyInjection; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 16 | use Symfony\Component\Config\FileLocator; 17 | 18 | class XmlWorkflowExtensionTest extends WorkflowExtensionTest 19 | { 20 | protected function loadFromFile(ContainerBuilder $container, $file) 21 | { 22 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/Fixtures/xml')); 23 | $loader->load($file.'.xml'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/Tests/DependencyInjection/YamlWorkflowExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle\Tests\DependencyInjection; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 16 | use Symfony\Component\Config\FileLocator; 17 | 18 | class YamlWorkflowExtensionTest extends WorkflowExtensionTest 19 | { 20 | protected function loadFromFile(ContainerBuilder $container, $file) 21 | { 22 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/Fixtures/yml')); 23 | $loader->load($file.'.yml'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Bundle/WorkflowBundle/WorkflowBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Bundle\WorkflowBundle; 13 | 14 | use Symfony\Bundle\WorkflowBundle\DependencyInjection\Compiler\ValidateWorkflowsPass; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\HttpKernel\Bundle\Bundle; 17 | 18 | /** 19 | * Bundle class 20 | */ 21 | class WorkflowBundle extends Bundle 22 | { 23 | public function build(ContainerBuilder $container) 24 | { 25 | parent::build($container); 26 | $container->addCompilerPass(new ValidateWorkflowsPass()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2016 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Workflow-bundle backported for Symfony 2.3+ 2 | =========================================== 3 | 4 | [![Build Status](https://travis-ci.org/fduch/workflow-bundle.svg?branch=master)](https://travis-ci.org/fduch/workflow-bundle) 5 | 6 | Bundle for https://github.com/symfony/workflow component backported for Symfony 2.3+ and <3.2 from Symfony 3.2's FrameworkBundle. 7 | The main difference with original workflow management in Symfony 3.2+ applications is that 8 | workflow configuration must be set under `workflow` section instead of `framework` section in Symfony 3.2+. 9 | Such difference is caused by the fact that workflow configurations are handled by 10 | separate WorkflowBundle introduced by this package instead of FrameworkBundle in Symfony 3.2+. 11 | 12 | 13 | > This version of the Bundle uses stable 3.2+ version of the [symfony/workflow](https://github.com/symfony/workflow) component. 14 | > Due to BC-breaks introduced in Workflow component and FrameworkBundle inside 3.2-branch (https://github.com/symfony/symfony/pull/20462) 15 | > please use ([1.x](https://github.com/fduch/workflow-bundle/tree/1.x)) branch of the fduch/workflow-bundle with [symfony/workflow](https://github.com/symfony/workflow) component in old versions up to [cdddaeec794e4096f2f80f0298fc1a4b5bfacb83](https://github.com/symfony/workflow/commit/cdddaeec794e4096f2f80f0298fc1a4b5bfacb83) (non-including). 16 | > Unfortunately there is no way to define such version constraint restrictions on the composer.json level: it can be done nether with `require` nor `conflict` sections, so you should check it manually. 17 | 18 | 19 | Usage 20 | ===== 21 | Please install the bundle using composer: 22 | ``` 23 | composer require fduch/workflow-bundle 24 | ``` 25 | 26 | and register the bundle in your AppKernel class: 27 | ```php 28 | public function registerBundles() 29 | { 30 | $bundles = array( 31 | // ... 32 | new \Symfony\Bundle\WorkflowBundle\WorkflowBundle(), 33 | ); 34 | } 35 | ``` 36 | 37 | You can also check project [fduch/symfony-standard-workflow](https://github.com/fduch/symfony-standard-workflow) to see application example based on Symfony Standard Edition with workflow configured using `fduch/workflow-bundle` 38 | 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fduch/workflow-bundle", 3 | "type": "library", 4 | "license": "MIT", 5 | "keywords": ["workflow"], 6 | "homepage": "https://github.com/fduch/workflow-bundle", 7 | "description": "workflow bundle for https://github.com/symfony/workflow component backported for Symfony 2.3+", 8 | "authors": [ 9 | { 10 | "name": "Grégoire Pineau", 11 | "email": "lyrixx@lyrixx.info" 12 | }, 13 | { 14 | "name": "fduch", 15 | "email": "alex.medwedew@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.5.9", 20 | "symfony/workflow": "^3.2", 21 | "symfony/console": "^2.3|^3.0", 22 | "symfony/framework-bundle": "^2.3|^3.0" 23 | }, 24 | "require-dev": { 25 | "twig/twig": "^1.11", 26 | "symfony/yaml": "^2.0.5|^3.0" 27 | }, 28 | "conflict": { 29 | "symfony/framework-bundle": ">=3.2" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Symfony\\Bundle\\": "Bundle/", 34 | "Symfony\\Bridge\\": "Bridge/" 35 | } 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "2.x-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | ./Bundle/*/Tests/ 9 | 10 | 11 | --------------------------------------------------------------------------------