├── .gitignore ├── AbstractWorkflowDefinition.php ├── AbstractWorkflowExecution.php ├── AbstractWorkflowExecutionFactory.php ├── Action └── AbstractWorkflowAction.php ├── Command ├── GenerateWorkflowCommand.php ├── ResumeWorkflowCommand.php └── Validators.php ├── Entity ├── Execution.php ├── ExecutionState.php ├── Node.php ├── NodeConnection.php ├── VariableHandler.php └── Workflow.php ├── Generator └── WorkflowGenerator.php ├── README.md ├── Resources └── skeleton │ └── workflow │ ├── Action │ ├── CheckResultAction.php.twig │ ├── DoSomethingAction.php.twig │ ├── WorkflowBeginAction.php.twig │ └── WorkflowEndAction.php.twig │ ├── Bundle.php.twig │ ├── Command │ └── GetWorkflowImageCommand.php.twig │ ├── DependencyInjection │ ├── Configuration.php.twig │ └── Extension.php.twig │ ├── Resources │ └── config │ │ └── services.yml.twig │ ├── Service │ └── WorkflowService.php.twig │ ├── Tests │ └── ServiceTest.php.twig │ ├── WorkflowDefinition.php.twig │ ├── WorkflowExecution.php.twig │ └── WorkflowExecutionFactory.php.twig ├── Service └── AbstractWorkflowService.php ├── Tests ├── AbstractFunctionalTest.php ├── AbstractIntegrationTest.php └── AbstractUnitTest.php ├── WorkflowBundle.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | vendor -------------------------------------------------------------------------------- /AbstractWorkflowDefinition.php: -------------------------------------------------------------------------------- 1 | getWorkflowName()); 34 | 35 | $pluginFinalNode = $this->define(); 36 | $pluginFinalNode->addOutNode($this->endNode); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AbstractWorkflowExecution.php: -------------------------------------------------------------------------------- 1 | workflowService = $workflowService; 36 | } 37 | 38 | /** 39 | * @return \YuriTeixeira\WorkflowBundle\Service\AbstractWorkflowService 40 | */ 41 | public function getWorkflowService() 42 | { 43 | return $this->workflowService; 44 | } 45 | 46 | /** 47 | * Overides the default start to attach the workflow definition, save it on database and run execution after all. 48 | */ 49 | public function start($parentId = null, AbstractWorkflowDefinition $workflowDefinition = null) 50 | { 51 | $this->workflow = $workflowDefinition ?: $this->getWorkflowDefinitionInstance(); 52 | $storage = new \ezcWorkflowDatabaseDefinitionStorage($this->db); 53 | $storage->save($this->workflow); 54 | 55 | return parent::start($parentId); 56 | } 57 | 58 | /** 59 | * Loads the state of execution 60 | * 61 | * @param $executionId 62 | */ 63 | public function load($executionId) 64 | { 65 | $this->loadExecution($executionId); 66 | } 67 | 68 | /** 69 | * @return \YuriTeixeira\WorkflowBundle\AbstractWorkflowDefinition 70 | */ 71 | abstract protected function getWorkflowDefinitionInstance(); 72 | } 73 | -------------------------------------------------------------------------------- /AbstractWorkflowExecutionFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class AbstractWorkflowExecutionFactory 14 | { 15 | /** 16 | * @var \ezcDbHandlerMysql 17 | */ 18 | protected $ezcDbHandler; 19 | 20 | /** 21 | * @var \YuriTeixeira\WorkflowBundle\Plugin\Service\AbstractPluginService 22 | */ 23 | protected $workflowService; 24 | 25 | public function __construct( 26 | $databaseHost, 27 | $databaseName, 28 | $databaseUser, 29 | $databasePassword, 30 | AbstractWorkflowService $workflowService 31 | ) { 32 | $this->ezcDbHandler = new \ezcDbHandlerMysql( 33 | array( 34 | 'host' => $databaseHost, 35 | 'dbname' => $databaseName, 36 | 'user' => $databaseUser, 37 | 'password' => $databasePassword, 38 | ) 39 | ); 40 | 41 | $this->workflowService = $workflowService; 42 | } 43 | 44 | /** 45 | * @param int $executionId 46 | * @return \YuriTeixeira\WorkflowBundle\AbstractWorkflowExecution 47 | */ 48 | abstract protected function getWorkflowExecution($executionId = null); 49 | 50 | /** 51 | * Returns an instance of plugin's workflow execution 52 | * 53 | * @param int $executionId 54 | * 55 | * @return \YuriTeixeira\WorkflowBundle\AbstractWorkflowExecution 56 | */ 57 | public function create($executionId = null) 58 | { 59 | $executionId = $executionId ? (int) $executionId : null; 60 | return $this->getWorkflowExecution($executionId); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Action/AbstractWorkflowAction.php: -------------------------------------------------------------------------------- 1 | execution = $execution; 53 | $this->workflowService = $execution->getWorkflowService(); 54 | 55 | return $this->run($execution); 56 | } 57 | 58 | /** 59 | * Executes a Workflow Execution (needed to avoid execution type checks with "instanceof") 60 | * 61 | * @return bool 62 | */ 63 | abstract protected function run(); 64 | } 65 | -------------------------------------------------------------------------------- /Command/GenerateWorkflowCommand.php: -------------------------------------------------------------------------------- 1 | generate:workflow command helps you generates new workflows." . 36 | "By default, the command interacts with the developer to tweak the generation." . 37 | "Note that the workflow namespace must end with \"Workflow\"."; 38 | 39 | $this 40 | ->setName('yuriteixeira:workflow:generate') 41 | ->setAliases(array('workflow:generate')) 42 | ->setDescription('Generates the core structure for a new workflow') 43 | ->setHelp($help) 44 | ->setDefinition(array( 45 | new InputOption(static::OPTION_NAME, '', InputOption::VALUE_REQUIRED, 'The workflow name'), 46 | new InputOption(static::OPTION_NAMESPACE, '', InputOption::VALUE_REQUIRED, 'The workflow namespace'), 47 | )); 48 | } 49 | 50 | /** 51 | * Execute CLI command 52 | * 53 | * @throws \InvalidArgumentException When namespace doesn't end with Bundle 54 | * @throws \RuntimeException When bundle can't be executed 55 | */ 56 | protected function execute(InputInterface $input, OutputInterface $output) 57 | { 58 | $dialog = $this->confirmGeneration($input, $output); 59 | 60 | // Getting needed info 61 | $workflowName = $this->getWorkflowName($input); 62 | $workflowNamespace = $this->getWorkflowNamespace($input); 63 | $workflowDir = $this->getWorkflowDir($workflowNamespace); 64 | 65 | // Generation 66 | $dialog->writeSection($output, 'Workflow generation'); 67 | 68 | $generator = $this->getGenerator(); 69 | $generator->generate($workflowNamespace, $workflowName, $workflowDir); 70 | 71 | $output->writeln('Generating the workflow stub code: OK'); 72 | 73 | $errors = array(); 74 | $runner = $dialog->getRunner($output, $errors); 75 | 76 | // App kernal update 77 | $this->updateKernel($dialog, $input, $output, $this->getContainer()->get('kernel'), $workflowNamespace, $workflowName); 78 | 79 | // Check that the namespace is already autoloaded 80 | $runner($this->checkAutoloader($output, $workflowNamespace, $workflowName, $workflowDir)); 81 | 82 | // Check cache 83 | $command = $this->getApplication()->find('cache:clear'); 84 | $cacheInput = new ArrayInput(array('command' => 'cache:clear')); 85 | $command->run($cacheInput, $output); 86 | 87 | // Summary 88 | $dialog->writeGeneratorSummary($output, $errors); 89 | } 90 | 91 | /** 92 | * Gets workflow dir 93 | * 94 | * @param $workflowNamespace 95 | * 96 | * @return string 97 | */ 98 | protected function getWorkflowDir($workflowNamespace) 99 | { 100 | $workflowDir = 101 | $this->getContainer()->get('kernel')->getRootDir() . 102 | '/../src/' . 103 | str_replace('\\', DIRECTORY_SEPARATOR, $workflowNamespace); 104 | 105 | return $workflowDir; 106 | } 107 | 108 | /** 109 | * Gets workflow namespace 110 | * 111 | * @param InputInterface $input 112 | * 113 | * @return string 114 | * @throws \RuntimeException 115 | */ 116 | protected function getWorkflowNamespace(InputInterface $input) 117 | { 118 | $workflowNamespace = $input->getOption(static::OPTION_NAMESPACE); 119 | 120 | if (!$workflowNamespace) { 121 | throw new \RuntimeException('Inform a valid workflow namespace (--namespace).'); 122 | } 123 | 124 | return $workflowNamespace; 125 | } 126 | 127 | /** 128 | * Gets workflow name 129 | * 130 | * @param InputInterface $input 131 | * 132 | * @return string 133 | * @throws \RuntimeException 134 | */ 135 | protected function getWorkflowName(InputInterface $input) 136 | { 137 | $workflowName = $input->getOption(static::OPTION_NAME); 138 | 139 | if (!$workflowName && !Validators::validateWorkflowName($workflowName)) { 140 | throw new \RuntimeException('Inform a valid workflow name (--name).'); 141 | } 142 | 143 | return $workflowName; 144 | } 145 | 146 | /** 147 | * Confirming generation 148 | * 149 | * @param InputInterface $input 150 | * @param OutputInterface $output 151 | * 152 | * @return DialogHelper|\Symfony\Component\Console\Helper\HelperInterface 153 | * @throws \RuntimeException 154 | */ 155 | protected function confirmGeneration(InputInterface $input, OutputInterface $output) 156 | { 157 | $dialog = $this->getDialogHelper(); 158 | 159 | $confirmed = $dialog->askConfirmation( 160 | $output, 161 | $dialog->getQuestion('Do you confirm generation', 'yes', '?'), 162 | true 163 | ); 164 | 165 | if ($input->isInteractive() && !$confirmed) { 166 | throw new \RuntimeException('Command aborted.'); 167 | } 168 | 169 | return $dialog; 170 | } 171 | 172 | /** 173 | * Defines interaction with CLI command 174 | * 175 | * @param InputInterface $input 176 | * @param OutputInterface $output 177 | */ 178 | protected function interact(InputInterface $input, OutputInterface $output) 179 | { 180 | $dialog = $this->getDialogHelper(); 181 | $dialog->writeSection($output, 'Welcome to the workflow generator'); 182 | 183 | $collectors[static::OPTION_NAME] = array( 184 | 'question' => 'Workflow name', 185 | 'value' => $input->getOption(static::OPTION_NAME), 186 | 'default' => null, 187 | 'message_lines' => array( 188 | '', 189 | 'The workflow name is required. Please, use a CamelCase name with the "Workflow" suffix.', 190 | 'Example: PaypalWorkflow or MegaWorkflow.', 191 | '' 192 | ) 193 | ); 194 | 195 | $collectors[static::OPTION_NAMESPACE] = array( 196 | 'question' => 'Workflow namespace', 197 | 'value' => $input->getOption(static::OPTION_NAMESPACE), 198 | 'default' => function() use (&$collectors) { return "Your\\Namespace\\Workflow\\{$collectors[static::OPTION_NAME]['value']}"; }, 199 | 'message_lines' => array( 200 | '', 201 | 'The workflow namespace is required. Please, use a CamelCase name.', 202 | 'Example: Your\Namespace\Workflow.', 203 | '' 204 | ) 205 | ); 206 | 207 | foreach ($collectors as $collectorKey => $collector) { 208 | if (!$collector['value']) { 209 | $output->writeln($collector['message_lines']); 210 | 211 | $default = is_callable($collector['default']) ? $collector['default']() : $collector['default']; 212 | 213 | $collectors[$collectorKey]['value'] = 214 | $dialog->ask( 215 | $output, 216 | $dialog->getQuestion($collector['question'], $default), 217 | $default 218 | ); 219 | 220 | $input->setOption($collectorKey, $collectors[$collectorKey]['value']); 221 | } 222 | } 223 | 224 | $summaryMessage = 225 | 'You are going to generate Workflow "%1$s"'. PHP_EOL . 226 | 'This workflow will be stored on %2$s'; 227 | 228 | $workflowName = $collectors[static::OPTION_NAME]['value']; 229 | $workflowDir = $this->getWorkflowDir($collectors[static::OPTION_NAMESPACE]['value']); 230 | 231 | $output->writeln(array( 232 | '', 233 | $this->getHelper('formatter')->formatBlock('Summary before generation', 'bg=blue;fg=white', true), 234 | '', 235 | sprintf( 236 | $summaryMessage, 237 | $workflowName, 238 | $workflowDir 239 | ), 240 | '', 241 | )); 242 | } 243 | 244 | /** 245 | * Check if workflow will be autoloaded correctly 246 | * 247 | * @param OutputInterface $output 248 | * @param $namespace 249 | * @param $bundle 250 | * 251 | * @return array List of messages that will be displayed 252 | */ 253 | protected function checkAutoloader(OutputInterface $output, $namespace, $bundle) 254 | { 255 | $output->write('Checking that the workflow bundle will be autoloaded correctly: '); 256 | 257 | if (!class_exists($namespace . '\\' . $bundle)) { 258 | return array( 259 | '- Edit the composer.json file and register the bundle', 260 | ' namespace in the "autoload" section:', 261 | '', 262 | ); 263 | } 264 | } 265 | 266 | /** 267 | * Returns the generator instance 268 | * 269 | * @return WorkflowGenerator 270 | */ 271 | protected function getGenerator() 272 | { 273 | if (null === $this->generator) { 274 | $this->generator = new WorkflowGenerator( 275 | $this->getContainer()->get('filesystem'), 276 | __DIR__ . '/../Resources/skeleton/workflow' 277 | ); 278 | } 279 | 280 | return $this->generator; 281 | } 282 | 283 | /** 284 | * Returns a dialog helper instance 285 | * 286 | * @return DialogHelper|\Symfony\Component\Console\Helper\HelperInterface 287 | */ 288 | protected function getDialogHelper() 289 | { 290 | $dialog = $this->getHelperSet()->get('dialog'); 291 | 292 | if (!$dialog || ! $dialog instanceof DialogHelper) { 293 | $this->getHelperSet()->set($dialog = new DialogHelper()); 294 | } 295 | 296 | return $dialog; 297 | } 298 | 299 | /** 300 | * Update AppKernel class (small modifications over Fabien Potencier implementation found on GenerateBundleCommand) 301 | * 302 | * @param $dialog 303 | * @param InputInterface $input 304 | * @param OutputInterface $output 305 | * @param KernelInterface $kernel 306 | * @param $namespace 307 | * @param $bundle 308 | * 309 | * @return array 310 | */ 311 | protected function updateKernel($dialog, InputInterface $input, OutputInterface $output, KernelInterface $kernel, $namespace, $bundle) 312 | { 313 | $auto = true; 314 | 315 | if ($input->isInteractive()) { 316 | $auto = $dialog->askConfirmation($output, $dialog->getQuestion('Confirm automatic update of your Kernel', 'yes', '?'), true); 317 | } 318 | 319 | $output->write('Enabling the bundle inside the Kernel: '); 320 | $manip = new KernelManipulator($kernel); 321 | 322 | try { 323 | $ret = $auto ? $manip->addBundle($namespace.'\\'.$bundle) : false; 324 | 325 | if (!$ret) { 326 | $reflected = new \ReflectionObject($kernel); 327 | 328 | return array( 329 | sprintf('- Edit %s', $reflected->getFilename()), 330 | ' and add the following bundle in the AppKernel::registerBundles() method:', 331 | '', 332 | sprintf(' new %s(),', $namespace.'\\'.$bundle), 333 | '', 334 | ); 335 | } 336 | } catch (\RuntimeException $e) { 337 | return array( 338 | sprintf('Bundle %s is already defined in AppKernel::registerBundles().', $namespace.'\\'.$bundle), 339 | '', 340 | ); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Command/ResumeWorkflowCommand.php: -------------------------------------------------------------------------------- 1 | setName('yuriteixeira:workflow:resume') 27 | ->setAliases(array('workflow:resume')) 28 | ->setDescription('Resume all paused workflows') 29 | ; 30 | } 31 | 32 | /** 33 | * Initialize required services 34 | * 35 | * @param \Symfony\Component\Console\Input\InputInterface $input 36 | * @param \Symfony\Component\Console\Output\OutputInterface $output 37 | */ 38 | protected function initialize(InputInterface $input, OutputInterface $output) 39 | { 40 | $container = $this->getContainer(); 41 | $this->em = $container->get('doctrine.orm.entity_manager'); 42 | } 43 | 44 | /** 45 | * Query database for workflow executions and run them 46 | * 47 | * @param \Symfony\Component\Console\Input\InputInterface $input 48 | * @param \Symfony\Component\Console\Output\OutputInterface $output 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output) 51 | { 52 | $conn = $this->em->getConnection(); 53 | 54 | $res = $conn->executeQuery('SELECT * FROM execution e INNER JOIN workflow w ON e.workflow_id = w.workflow_id'); 55 | $executions = $res->fetchAll(); 56 | 57 | if (count($executions) > 0) { 58 | foreach ($executions as $execution) { 59 | $this->resumeWorkflows($execution); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Resume a workflow execution 66 | * 67 | * @param array $execution 68 | */ 69 | protected function resumeWorkflows(array $execution) 70 | { 71 | try { 72 | 73 | $workflowService = $this->getWorkflowService($execution['workflow_name']); 74 | $workflowFactory = $workflowService->getWorkflowExecutionFactory(); 75 | $workflowExecution = $workflowFactory->create($execution['workflow_id']); 76 | $workflowExecution->resume(); 77 | 78 | } catch (ServiceNotFoundException $e) { 79 | 80 | /** @todo log invalid workflow */ 81 | } 82 | } 83 | 84 | /** 85 | * Return the workflow service to the provided plugin name 86 | * 87 | * @param string $workflowName 88 | * 89 | * @return AbstractWorkflowService 90 | */ 91 | protected function getWorkflowService($workflowName) 92 | { 93 | return $this->getContainer()->get('workflow.' . $workflowName); 94 | } 95 | } -------------------------------------------------------------------------------- /Command/Validators.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 26 | $this->setSkeletonDirs($skeletonDirs); 27 | } 28 | 29 | /** 30 | * Generates workflow 31 | * 32 | * @param $namespace 33 | * @param $workflowName 34 | * @param $workflowDir 35 | */ 36 | public function generate($namespace, $workflowName, $workflowDir) 37 | { 38 | $this->checkFilesystemRequirements($workflowDir); 39 | $this->renderFiles($namespace, $workflowName, $workflowDir); 40 | } 41 | 42 | /** 43 | * Render files to target workflow directory 44 | * 45 | * @param $workflowNamespace 46 | * @param $workflowName 47 | * @param $targetDir 48 | * 49 | * @throws \Exception 50 | */ 51 | protected function renderFiles($workflowNamespace, $workflowName, $targetDir) 52 | { 53 | $serviceName = $workflowName . 'Service'; 54 | 55 | $parameters = $this->generateRenderParameters($workflowNamespace, $workflowName, $serviceName); 56 | 57 | $renderQueue = array( 58 | array( 59 | 'template_name' => 'Bundle.php.twig', 60 | 'target_path' => $targetDir . '/' . $workflowName . '.php', 61 | ), 62 | array( 63 | 'template_name' => 'WorkflowDefinition.php.twig', 64 | 'target_path' => $targetDir . '/WorkflowDefinition.php', 65 | ), 66 | array( 67 | 'template_name' => 'WorkflowExecution.php.twig', 68 | 'target_path' => $targetDir . '/WorkflowExecution.php', 69 | ), 70 | array( 71 | 'template_name' => 'WorkflowExecutionFactory.php.twig', 72 | 'target_path' => $targetDir . '/WorkflowExecutionFactory.php', 73 | ), 74 | array( 75 | 'template_name' => 'Action/WorkflowBeginAction.php.twig', 76 | 'target_path' => $targetDir . '/Action/WorkflowBeginAction.php', 77 | ), 78 | array( 79 | 'template_name' => 'Action/DoSomethingAction.php.twig', 80 | 'target_path' => $targetDir . '/Action/DoSomethingAction.php', 81 | ), 82 | array( 83 | 'template_name' => 'Action/CheckResultAction.php.twig', 84 | 'target_path' => $targetDir . '/Action/CheckResultAction.php', 85 | ), 86 | array( 87 | 'template_name' => 'Action/WorkflowEndAction.php.twig', 88 | 'target_path' => $targetDir . '/Action/WorkflowEndAction.php', 89 | ), 90 | array( 91 | 'template_name' => 'Service/WorkflowService.php.twig', 92 | 'target_path' => $targetDir . '/Service/' . $serviceName . '.php', 93 | ), 94 | array( 95 | 'template_name' => 'Tests/ServiceTest.php.twig', 96 | 'target_path' => $targetDir . '/Tests/Integration/Service/' . $serviceName . 'Test.php', 97 | ), 98 | array( 99 | 'template_name' => 'Resources/config/services.yml.twig', 100 | 'target_path' => $targetDir . '/Resources/config/services.yml', 101 | ), 102 | array( 103 | 'template_name' => 'DependencyInjection/Extension.php.twig', 104 | 'target_path' => $targetDir . '/DependencyInjection/' . $workflowName . 'Extension.php', 105 | ), 106 | array( 107 | 'template_name' => 'DependencyInjection/Configuration.php.twig', 108 | 'target_path' => $targetDir . '/DependencyInjection/Configuration.php', 109 | ), 110 | array( 111 | 'template_name' => 'Command/GetWorkflowImageCommand.php.twig', 112 | 'target_path' => $targetDir . '/Command/GetWorkflowImageCommand.php', 113 | ), 114 | ); 115 | 116 | foreach ($renderQueue as $renderItem) { 117 | try { 118 | 119 | $targetPath = $renderItem['target_path']; 120 | $templateName = $renderItem['template_name']; 121 | 122 | $this->renderFile( 123 | $templateName, 124 | $targetPath, 125 | $parameters 126 | ); 127 | 128 | } catch (\Exception $e) { 129 | 130 | throw new \Exception( 131 | sprintf( 132 | 'Failure rendering %s on path %s', 133 | $templateName, 134 | $targetPath 135 | ), 136 | 0, 137 | $e 138 | ); 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Generate parameters needed to render files 145 | * 146 | * @param $workflowNamespace 147 | * @param $workflowName 148 | * @param $workflowServiceName 149 | * 150 | * @return array 151 | */ 152 | protected function generateRenderParameters($workflowNamespace, $workflowName, $workflowServiceName) 153 | { 154 | $workflowNameCamelCase = lcfirst($workflowName); 155 | $workflowNameSnakeCase = strtolower(preg_replace('/([a-z])([A-Z])/e', "'\${1}_\${2}'", $workflowName)); 156 | $workflowNameSpinalCase = strtolower(str_replace('_', '-', $workflowNameSnakeCase)); 157 | 158 | $parameters = array( 159 | 'workflow_namespace' => $workflowNamespace, 160 | 'workflow_service_name' => $workflowServiceName, 161 | 'workflow_name' => $workflowName, 162 | 'workflow_name_camel_case' => $workflowNameCamelCase, 163 | 'workflow_name_snake_case' => $workflowNameSnakeCase, 164 | 'workflow_name_spinal_case' => $workflowNameSpinalCase, 165 | ); 166 | 167 | return $parameters; 168 | } 169 | 170 | /** 171 | * Verify if target directory has the requirements to be the workflow folder 172 | * 173 | * @param $dir Target directory 174 | * 175 | * @throws \RuntimeException 176 | */ 177 | protected function checkFilesystemRequirements($dir) 178 | { 179 | if (file_exists($dir)) { 180 | 181 | if (!is_dir($dir)) { 182 | throw new \RuntimeException(sprintf( 183 | 'Unable to generate the workflow: Target directory "%s" exists but is a file.', 184 | realpath($dir) 185 | )); 186 | } 187 | 188 | $files = scandir($dir); 189 | 190 | if ($files != array('.', '..')) { 191 | throw new \RuntimeException(sprintf( 192 | 'Unable to generate the workflow: Target directory "%s" is not empty.', 193 | realpath($dir) 194 | )); 195 | } 196 | 197 | if (!is_writable($dir)) { 198 | throw new \RuntimeException(sprintf( 199 | 'Unable to generate the workflow: Target directory "%s" is not writable.', 200 | realpath($dir) 201 | )); 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About Workflow Bundle 2 | 3 | If you ever needed to implement certain parts of your symfony2 application in a workflow fashion (like a payment state machine or a cms publishing flow), you came to the right place. 4 | 5 | This bundle was built upon Sebastian Bergman's [ezcWorkflow component](http://zetacomponents.org/documentation/trunk/Workflow/tutorial.html). Before start, we **strongly recommend** you to understand the concepts behind it (otherwise, your are gonna fell a little bit lost to define your own workflow). 6 | 7 | # Installation 8 | 9 | 1. Add the requirement `yuriteixeira/workflow-bundle` in your composer.json and run `composer update`: 10 | 2. Register the bundle on your AppKernel.php, by adding `new Yuriteixeira\WorkflowBundle\WorkflowBundle()` on `$bundles` array. 11 | 3. Run `php app/console`. You should see a section called `yuriteixeira` there with workflow related commands. 12 | 4. Run `php app/console doctrine:schema:update` to add the tables needed by our workflow in your database. 13 | 14 | All right! Let's get our hands dirty. 15 | 16 | # Usage 17 | 18 | ## Generating your own workflow 19 | 20 | This bundle intended to make your life easy. So, instead of give you complex instructions of how to create your workflow manually, we took this boring and error-prone job from your hands! 21 | 22 | All you have to do is run `yuriteixeira:workflow:generate`. This code generator will generate all the needed file structure, update your AppKernel and boom, your just need to customize the code to your needs. 23 | 24 | ## Understanding the generated files structure 25 | 26 | ``` 27 | ├── MyWorkflow 28 | │   ├── Action 29 | │   │   ├── CheckResultAction.php 30 | │   │   ├── DoSomethingAction.php 31 | │   │   ├── WorkflowBeginAction.php 32 | │   │   └── WorkflowEndAction.php 33 | │   ├── Command 34 | │   │   └── GetWorkflowImageCommand.php 35 | │   ├── DependencyInjection 36 | │   │   ├── Configuration.php 37 | │   │   └── MyWorkflowExtension.php 38 | │   ├── MyWorkflow.php 39 | │   ├── Resources 40 | │   │   └── config 41 | │   │   └── services.yml 42 | │   ├── Service 43 | │   │   └── MyWorkflowService.php 44 | │   ├── Tests 45 | │   │   └── Integration 46 | │   │   └── Service 47 | │   │   └── MyWorkflowServiceTest.php 48 | │   ├── WorkflowDefinition.php 49 | │   ├── WorkflowExecution.php 50 | │   └── WorkflowExecutionFactory.php 51 | ``` 52 | Important namespaces and classes: 53 | 54 | * **WorkflowDefinition.php** - Here you define your workflow in the ezcWorkflow way (Define a workflow through php code instead of a simple YAML or XML way is a little bit odd, but I promise to make it up soon). 55 | * **Service\MyWorkflowService.php** - Here you define your core functionality, that will be accessible by your **Action** classes. This class will be managed by Symfony's Dependency Injection Container, and it's id can be found on `Resources/config/services.yml` 56 | * **GetWorkflowImageCommand.php** - This CLI Command exposes, in this case, `workflow-my-workflow:generate-workflow-image`, that generates a graphical representation of your workflow definition, like this: 57 | 58 | ![image](http://f.cl.ly/items/3H3J2T0L36010Y1b450M/my-workflow_workflow_20130421_210946.png) 59 | 60 | 61 | ## Customizing the workflow definition 62 | 63 | Out of the box, after you generated your workflow, there is a workflow definition to exemplify how to do it in `WorkflowDefinition.php`. 64 | 65 | Your workflow business should reside on `MyWorkflow\Action` namespace. Just follow the examples and you will be good. 66 | 67 | Change the `WorkflowDefinition` class according to your needs (check this [tutorial](http://zetacomponents.org/documentation/trunk/Workflow/tutorial.html) to know how to link your workflow nodes). 68 | 69 | ## Starting 70 | 71 | Each workflow you generate has a service exposed on Symfony's Dependency Injection Container. Check it's name on your workflow's `services.yml` file. Supposing that it's name is `workflow.my_workflow`, to start it inside a Controller: 72 | 73 | ``` 74 | $this->get('workflow.my_workflow')->startNewWorkflowExecution(); 75 | ``` 76 | 77 | ## Resuming paused workflows 78 | 79 | Workflows can be paused and resumed arbitrary. 80 | 81 | To pause it, inside your `Action` class `run` method, return `false` instead of `true`. 82 | 83 | To resume it, run `yuriteixeira:workflow:resume`. 84 | 85 | # Techinal Debits (you may help here!) 86 | 87 | * Tests 88 | * Build tasks (phpcs, phpmd, code coverage, etc) 89 | * Add repo to Travis CI 90 | * Resume workflows through `resumeWorkflowExecution($executionId)` method of service 91 | * CLI argument to resume only one workflow 92 | 93 | # Future features 94 | 95 | * Define workflow through a YAML config 96 | * Be independent of workflow-database-tiein, that limits the options of database drivers (should use PDO/DBAL) 97 | * Be independent of Sebastian Bergman's ezcWorkflow (it is awesome, but I would like to modernize and simplify it a lil bit) 98 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Action/CheckResultAction.php.twig: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class CheckResultAction extends AbstractWorkflowAction 13 | { 14 | /** 15 | * @var \YuriTeixeira\WorkflowBundle\Service\AbstractWorkflowService 16 | */ 17 | protected $workflowService; 18 | 19 | /** 20 | * Action implementation (do your magic here!). 21 | * NOTE: Only an example, do your own business logic to continue flow or pause. 22 | * 23 | * @return bool TRUE to continue workflow, FALSE to break it's execution 24 | */ 25 | protected function run() 26 | { 27 | $this->execution->getWorkflowService()->getLogger()->debug($this->__toString()); 28 | 29 | $forcePause = $this->execution->getVariable(WorkflowDefinition::VAR_FORCE_PAUSE); 30 | 31 | // Forced pause! (for test porpouses, probably you should remove this) 32 | if ($forcePause) { 33 | $this->execution->setVariable(WorkflowDefinition::VAR_FORCE_PAUSE, false); 34 | return false; 35 | } 36 | 37 | $this->execution->setVariable(WorkflowDefinition::VAR_TRIES_LEFT, 0); 38 | 39 | $this->execution->getWorkflowService()->getLogger()->debug($this->__toString()); 40 | 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Action/DoSomethingAction.php.twig: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DoSomethingAction extends AbstractWorkflowAction 15 | { 16 | /** 17 | * Action implementation (do your magic here!). 18 | * NOTE: Only an example, do your own business logic to continue flow or pause. 19 | * 20 | * @return bool TRUE to continue workflow, FALSE to break it's execution 21 | */ 22 | protected function run() 23 | { 24 | $this->execution->getWorkflowService()->doSomething(); 25 | $this->execution->getWorkflowService()->getLogger()->debug($this->__toString()); 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Action/WorkflowBeginAction.php.twig: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class WorkflowBeginAction extends AbstractWorkflowAction 15 | { 16 | /** 17 | * Implement here any business logic needed before the other Actions execution 18 | * 19 | * @return bool TRUE to continue workflow, FALSE to break it's execution 20 | */ 21 | protected function run() 22 | { 23 | $this->execution->setVariable(WorkflowDefinition::VAR_TRIES_LEFT, 1); 24 | 25 | if (!$this->execution->hasVariable(WorkflowDefinition::VAR_FORCE_PAUSE)) { 26 | $this->execution->setVariable( 27 | WorkflowDefinition::VAR_FORCE_PAUSE, 28 | false 29 | ); 30 | } 31 | 32 | $this->execution->getWorkflowService()->getLogger()->debug($this->__toString()); 33 | 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Action/WorkflowEndAction.php.twig: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class WorkflowEndAction extends AbstractWorkflowAction 15 | { 16 | /** 17 | * Implement here any business logic needed after all the other Actions execution 18 | * 19 | * @return bool TRUE to continue workflow, FALSE to break it's execution 20 | */ 21 | protected function run() 22 | { 23 | $this->execution->getWorkflowService()->getLogger()->debug($this->__toString()); 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Bundle.php.twig: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class GetWorkflowImageCommand extends ContainerAwareCommand 16 | { 17 | protected function configure() 18 | { 19 | $this 20 | ->setName('workflow-{{ workflow_name_spinal_case }}:generate-workflow-image') 21 | ->setDescription('Generates an image representing workflow definition') 22 | ; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output) 26 | { 27 | $output->writeln("Generating image..."); 28 | 29 | $workflowDefinition = new WorkflowDefinition(); 30 | $visitor = new \ezcWorkflowVisitorVisualization(); 31 | $workflowDefinition->accept($visitor); 32 | 33 | try { 34 | 35 | $dir = __DIR__ . '/../Resources/doc/'; 36 | mkdir($dir); 37 | 38 | $path = $dir . '{{ workflow_name_spinal_case }}_workflow_' . date('Ymd_His'); 39 | file_put_contents($path, $visitor); 40 | exec("dot -Tpng {$path} > {$path}.png"); 41 | unlink($path); 42 | 43 | $output->writeln('Image generated successfully!'); 44 | $output->writeln("Path: {$path}.png!"); 45 | 46 | } catch (\Exception $e) { 47 | 48 | $output->writeln("An error ocurred. Make sure that directory \"{$dir}\" exists and you have graphviz (with its \"dot\" executable in your \$PATH) installed."); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/DependencyInjection/Configuration.php.twig: -------------------------------------------------------------------------------- 1 | root('workflow_{{ workflow_name_snake_case }}'); 22 | 23 | // Here you should define the parameters that are allowed to 24 | // configure your bundle. See the documentation linked above for 25 | // more information on that topic. 26 | 27 | return $treeBuilder; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/DependencyInjection/Extension.php.twig: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.yml'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Resources/config/services.yml.twig: -------------------------------------------------------------------------------- 1 | #parameters: 2 | # workflow_{{ workflow_name_snake_case }}_key: value 3 | 4 | services: 5 | workflow.{{ workflow_name_snake_case }}: 6 | class: {{ workflow_namespace }}\Service\{{ workflow_service_name }} 7 | arguments: 8 | - %database_host% 9 | - %database_name% 10 | - %database_user% 11 | - %database_password% 12 | - @logger 13 | - @doctrine.orm.entity_manager 14 | - @twig 15 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Service/WorkflowService.php.twig: -------------------------------------------------------------------------------- 1 | em = $em; 64 | $this->logger = $logger; 65 | $this->twig = $twig; 66 | } 67 | 68 | /** 69 | * @return \Doctrine\ORM\EntityManager 70 | */ 71 | public function getEm() 72 | { 73 | return $this->em; 74 | } 75 | 76 | /** 77 | * @return \Psr\Log\LoggerInterface 78 | */ 79 | public function getLogger() 80 | { 81 | return $this->logger; 82 | } 83 | 84 | /** 85 | * @return \Twig_Environment 86 | */ 87 | public function getTwig() 88 | { 89 | return $this->twig; 90 | } 91 | 92 | /** 93 | * Get Workflow Name 94 | * 95 | * @return string 96 | */ 97 | public function getWorkflowName() 98 | { 99 | return $this->workflowName; 100 | } 101 | 102 | /** 103 | * Something interesting starts here 104 | */ 105 | public function doSomething() 106 | { 107 | $this->logger->debug('Done Something!!!'); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/Tests/ServiceTest.php.twig: -------------------------------------------------------------------------------- 1 | getContainer()->get('workflow.{{ workflow_name_snake_case }}'); 18 | 19 | $execution = $service->getWorkflowExecutionFactory()->create(); 20 | $execution->setVariable(WorkflowDefinition::VAR_CAN_PAUSE, true); 21 | $execution->setVariable(WorkflowDefinition::VAR_FORCE_PAUSE, true); 22 | $execution->start(); 23 | 24 | $logContents = $this->getLogContents(); 25 | 26 | $this->assertContains('WorkflowBeginAction', $logContents); 27 | $this->assertContains('DoSomethingAction', $logContents); 28 | $this->assertContains('Done Something!!!', $logContents); 29 | $this->assertContains('CheckResultAction', $logContents); 30 | $this->assertNotContains('WorkflowEndAction', $logContents); 31 | 32 | $app = new Application($this->getContainer()->get('kernel')); 33 | $app->setAutoExit(false); 34 | 35 | $input = new ArrayInput(array('command' => 'yuriteixeira:workflow:resume')); 36 | $app->run($input); 37 | 38 | $logContents = $this->getLogContents(); 39 | $this->assertContains('WorkflowEndAction', $logContents); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/WorkflowDefinition.php.twig: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class WorkflowDefinition extends AbstractWorkflowDefinition 13 | { 14 | const VAR_TRIES_LEFT = 'tries_left'; 15 | const VAR_MAX_TRIES = 'max_tries'; 16 | const VAR_FORCE_PAUSE = 'force_pause'; 17 | 18 | /** 19 | * Returns the workflow name 20 | * 21 | * @return string 22 | */ 23 | public function getWorkflowName() 24 | { 25 | return '{{ workflow_name_snake_case }}'; 26 | } 27 | 28 | /** 29 | * Defines the entire workflow 30 | * 31 | * @return \ezcWorkflowNode 32 | */ 33 | protected function define() 34 | { 35 | $actionsNamespace = __NAMESPACE__ . '\\Action'; 36 | 37 | // Action Nodes (Maps a workflow action node to a Action class) 38 | $workflowBeginNode = new \ezcWorkflowNodeAction(array('class' => "{$actionsNamespace}\\WorkflowBeginAction")); 39 | $doSomethingNode = new \ezcWorkflowNodeAction(array('class' => "{$actionsNamespace}\\DoSomethingAction")); 40 | $checkResultNode = new \ezcWorkflowNodeAction(array('class' => "{$actionsNamespace}\\CheckResultAction")); 41 | $workflowEndNode = new \ezcWorkflowNodeAction(array('class' => "{$actionsNamespace}\\WorkflowEndAction")); 42 | 43 | // Merge Node 44 | $mergeNode = new \ezcWorkflowNodeSimpleMerge(); 45 | 46 | // Choice Node 47 | $choiceNode = new \ezcWorkflowNodeExclusiveChoice(); 48 | 49 | // Flow 50 | $this->startNode->addOutNode($workflowBeginNode); 51 | $workflowBeginNode->addOutNode($doSomethingNode); 52 | 53 | // NODE: This structure is needed to loop. So, we will run the CheckResultAction until we have tries left 54 | // or until we finally get a status that allow workflow to go on 55 | $mergeNode->addInNode($doSomethingNode); 56 | $mergeNode->addInNode($checkResultNode); 57 | $mergeNode->addOutNode($choiceNode); 58 | 59 | $choiceNode->addConditionalOutNode( 60 | new \ezcWorkflowConditionVariable( 61 | static::VAR_TRIES_LEFT, 62 | new \ezcWorkflowConditionIsGreaterThan(0) 63 | ), 64 | $checkResultNode 65 | ); 66 | 67 | $choiceNode->addConditionalOutNode( 68 | new \ezcWorkflowConditionVariable( 69 | static::VAR_TRIES_LEFT, 70 | new \ezcWorkflowConditionIsEqual(0) 71 | ), 72 | $workflowEndNode 73 | ); 74 | 75 | return $workflowEndNode; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/WorkflowExecution.php.twig: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class WorkflowExecution extends AbstractWorkflowExecution 13 | { 14 | /** 15 | * @return \{{ workflow_namespace }}\WorkflowDefinition 16 | */ 17 | protected function getWorkflowDefinitionInstance() 18 | { 19 | return new WorkflowDefinition(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/skeleton/workflow/WorkflowExecutionFactory.php.twig: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class WorkflowExecutionFactory extends AbstractWorkflowExecutionFactory 13 | { 14 | /** 15 | * @param int $executionId 16 | * 17 | * @return {{ workflow_namespace }}\WorkflowExecution 18 | */ 19 | protected function getWorkflowExecution($executionId = null) 20 | { 21 | return new WorkflowExecution($this->ezcDbHandler, $this->workflowService, $executionId); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Service/AbstractWorkflowService.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class AbstractWorkflowService 11 | { 12 | /** 13 | * @var string 14 | */ 15 | protected $host; 16 | 17 | /** 18 | * @var string 19 | */ 20 | protected $database; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $username; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $password; 31 | 32 | /** 33 | * @var \YuriTeixeira\WorkflowBundle\AbstractWorkflowExecutionFactory 34 | */ 35 | protected $workflowExecutionFactory; 36 | 37 | /** 38 | * Constructor 39 | * 40 | * @param string $host 41 | * @param string $database 42 | * @param string $username 43 | * @param string $password 44 | */ 45 | function __construct($host, $database, $username, $password) 46 | { 47 | $this->host = $host; 48 | $this->database = $database; 49 | $this->username = $username; 50 | $this->password = $password; 51 | } 52 | 53 | /** 54 | * Returns the full path of plugin's workflow execution factory class 55 | * 56 | * @return string 57 | */ 58 | protected function getWorkflowExecutionFactoryClass() 59 | { 60 | $pattern = '/((\\\\[a-zA-Z0-9_]+){2})$/'; 61 | $serviceClassPath = get_class($this); 62 | $factoryClassPath = preg_replace($pattern, '\\WorkflowExecutionFactory', $serviceClassPath); 63 | return $factoryClassPath; 64 | } 65 | 66 | /** 67 | * Returns a workflow excution factory instance 68 | * 69 | * @return \YuriTeixeira\WorkflowBundle\AbstractWorkflowExecutionFactory 70 | */ 71 | public function getWorkflowExecutionFactory() 72 | { 73 | if (!$this->workflowExecutionFactory) { 74 | $factoryClassName = $this->getWorkflowExecutionFactoryClass(); 75 | 76 | $factory = new $factoryClassName( 77 | $this->host, 78 | $this->database, 79 | $this->username, 80 | $this->password, 81 | $this 82 | ); 83 | 84 | $this->workflowExecutionFactory = $factory; 85 | } 86 | 87 | return $this->workflowExecutionFactory ; 88 | } 89 | 90 | /** 91 | * Start a workflow 92 | */ 93 | public function startNewWorkflowExecution() 94 | { 95 | $workflowExecution = $this->getWorkflowExecutionFactory()->create(); 96 | $workflowExecution->start(); 97 | } 98 | 99 | /** 100 | * Cancel a workflow execution 101 | */ 102 | public function cancelWorkflowExecution($executionId) 103 | { 104 | $workflowExecution = $this->getWorkflowExecutionFactory()->create($executionId); 105 | $workflowExecution->cancel(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/AbstractFunctionalTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/AbstractIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class AbstractIntegrationTest extends AbstractUnitTest 13 | { 14 | protected function setUp() 15 | { 16 | parent::setUp(); 17 | 18 | $this->resetDatabase(); 19 | $this->resetLogs(); 20 | } 21 | 22 | /** 23 | * Returns an entity manager 24 | * 25 | * @return \Doctrine\ORM\EntityManager 26 | */ 27 | protected function getEntityManager() 28 | { 29 | return $this->getContainer()->get('doctrine.orm.entity_manager'); 30 | } 31 | 32 | /** 33 | * Resets database 34 | */ 35 | protected function resetDatabase() 36 | { 37 | $connection = $this->getEntityManager()->getConnection(); 38 | 39 | $connection->executeQuery("SET foreign_key_checks = 0;"); 40 | $connection->executeUpdate('TRUNCATE TABLE execution;'); 41 | $connection->executeUpdate('TRUNCATE TABLE execution_state;'); 42 | $connection->executeUpdate('TRUNCATE TABLE node;'); 43 | $connection->executeUpdate('TRUNCATE TABLE node_connection;'); 44 | $connection->executeUpdate('TRUNCATE TABLE variable_handler;'); 45 | $connection->executeUpdate('TRUNCATE TABLE workflow;'); 46 | $connection->executeQuery("SET foreign_key_checks = 1;"); 47 | } 48 | 49 | /** 50 | * Resets logs 51 | */ 52 | protected function resetLogs($environment = null) 53 | { 54 | $kernel = $this->getContainer()->get('kernel'); 55 | $environment = $environment ? : $kernel->getEnvironment(); 56 | $logFile = $kernel->getLogDir() . '/' . $environment . '.log'; 57 | file_put_contents($logFile, ""); 58 | } 59 | 60 | /** 61 | * Gets log file content 62 | * 63 | * @param string $logFilename 64 | * 65 | * @return string 66 | */ 67 | protected function getLogContents($logFilename = null) 68 | { 69 | $kernel = $this->getContainer()->get('kernel'); 70 | $logFilename = $logFilename ? : $kernel->getEnvironment(); 71 | $logPath = "{$kernel->getLogDir()}/{$logFilename}.log"; 72 | $logContents = file_get_contents($logPath); 73 | 74 | return $logContents; 75 | } 76 | 77 | /** 78 | * Get the last line from log file 79 | * 80 | * @param string $logFilename 81 | * 82 | * @return mixed 83 | */ 84 | protected function getLastLineFromLog($logFilename = null) 85 | { 86 | $contents = $this->getLogContents($logFilename); 87 | $contentArray = explode("\n", $contents); 88 | for ($i = (count($contentArray) - 1); $i--; $i == 0) { 89 | if ($contentArray[$i]) { 90 | return $contentArray[$i]; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/AbstractUnitTest.php: -------------------------------------------------------------------------------- 1 | bootKernel(); 16 | } 17 | 18 | protected function bootKernel() 19 | { 20 | static::$kernel = $this->createKernel(); 21 | static::$kernel->boot(); 22 | } 23 | 24 | protected function getContainer() 25 | { 26 | return static::$kernel->getContainer(); 27 | } 28 | 29 | protected function getKernelRootDir() 30 | { 31 | return static::$kernel->getContainer()->get('kernel')->getRootDir(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WorkflowBundle.php: -------------------------------------------------------------------------------- 1 | =5.3.0", 18 | "zetacomponents/base": "*", 19 | "zetacomponents/workflow": "*", 20 | "zetacomponents/workflow-database-tiein": "*", 21 | "zetacomponents/database": "*", 22 | "symfony/framework-bundle": "~2.1", 23 | "doctrine/orm": "~2.2,>=2.2.3", 24 | "doctrine/doctrine-bundle": "1.2.*" 25 | }, 26 | "autoload": { 27 | "psr-0": { "YuriTeixeira\\WorkflowBundle": "" } 28 | }, 29 | "target-dir": "YuriTeixeira/WorkflowBundle" 30 | } 31 | --------------------------------------------------------------------------------