├── .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 | 
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 |
--------------------------------------------------------------------------------