├── .phpcq.lock ├── .phpcq.yaml.dist ├── .phpcq └── local │ ├── phpspec-plugin.php │ └── repository.json ├── .phpcs.xml.dist ├── CHANGELOG.md ├── README.md ├── composer.json ├── example ├── README.md └── src │ ├── DependencyInjection │ └── NetzmachtContaoWorkflowExampleExtension.php │ ├── NetzmachtContaoWorkflowExampleBundle.php │ ├── Resources │ ├── config │ │ └── services.yml │ └── contao │ │ ├── config │ │ └── config.php │ │ ├── dca │ │ ├── tl_example.php │ │ ├── tl_workflow.php │ │ └── tl_workflow_action.php │ │ └── languages │ │ └── en │ │ ├── modules.php │ │ ├── tl_example.php │ │ └── tl_workflow_action.php │ └── Workflow │ ├── Action │ ├── AbstractExampleActionFactory.php │ ├── PublishAction.php │ ├── PublishActionFactory.php │ ├── SendEmailNotificationAction.php │ └── SendEmailNotificationFactory.php │ ├── Form │ └── SendEmailNotificationFormBuilder.php │ └── Type │ └── ExampleType.php ├── phpspec.yml ├── psalm.xml ├── publish_docs.sh └── src ├── ContaoManager └── Plugin.php ├── Controller └── Backend │ ├── AbstractController.php │ ├── StepController.php │ └── TransitionController.php ├── DependencyInjection ├── Configuration.php ├── NetzmachtContaoWorkflowExtension.php └── Pass │ ├── ExpressionLanguageFunctionsPass.php │ ├── TransitionFormBuilderPass.php │ └── ViewFactoryPass.php ├── EventListener ├── Backend │ ├── AssetListener.php │ └── UserNavigationListener.php ├── Dca │ ├── ActionCallbackListener.php │ ├── CommonListener.php │ ├── CopyWorkflowCallbackListener.php │ ├── EntityPropertiesTrait.php │ ├── ModuleCallbackListener.php │ ├── PermissionCallbackListener.php │ ├── SavePermissionsCallbackListener.php │ ├── TransitionCallbackListener.php │ └── WorkflowCallbackListener.php ├── Hook │ └── LoadDataContainerListener.php ├── Integration │ ├── DataContainerListener.php │ ├── OperationListener.php │ ├── OptionsListener.php │ └── SubmitButtonsListener.php └── Workflow │ ├── AddFormattedEntityNotificationTokensListener.php │ ├── CreateConditionalTransitionsListener.php │ ├── CreateMetaDataActionListener.php │ └── CreateWorkflowChangeTransitionListener.php ├── Exception ├── DataContainer │ └── FieldAlreadyExists.php ├── Exception.php ├── PropertyAccessFailed.php └── RuntimeException.php ├── ExpressionLanguage └── ExpressionFunctionProvider.php ├── Form ├── Builder │ ├── ActionFormBuilder.php │ ├── ConditionalTransitionFormBuilder.php │ ├── DataAwareActionFormBuilder.php │ ├── DelegatingTransitionFormBuilder.php │ ├── TransitionActionsFormBuilder.php │ ├── TransitionFormBuilder.php │ └── WorkflowChangeTransitionFormBuilder.php ├── Choice │ └── UserChoices.php └── TransitionFormType.php ├── FrontendModule └── TransitionFrontendModuleController.php ├── Migration └── TransitionActionsMigration.php ├── Model ├── Action │ ├── ActionModel.php │ └── ActionRepository.php ├── Permission │ └── PermissionModel.php ├── State │ ├── StateModel.php │ └── StateRepository.php ├── Step │ ├── StepModel.php │ └── StepRepository.php ├── Transition │ ├── TransitionModel.php │ └── TransitionRepository.php └── Workflow │ ├── WorkflowModel.php │ └── WorkflowRepository.php ├── NetzmachtContaoWorkflowBundle.php ├── PropertyAccess ├── ArrayPropertyAccessor.php ├── ArrayPropertyAccessorFactory.php ├── ContaoModelPropertyAccessor.php ├── ContaoModelPropertyAccessorFactory.php ├── PropertyAccessManager.php ├── PropertyAccessor.php ├── PropertyAccessorFactory.php └── ReadonlyPropertyAccessor.php ├── Request └── EntityIdParamConverter.php ├── Resources ├── config │ ├── actions.yml │ ├── controllers.yml │ ├── integration.yml │ ├── listeners.yml │ ├── renderer.yml │ ├── routing.yml │ ├── services.yml │ ├── templates.yml │ └── transitions.yml ├── contao │ ├── config │ │ └── config.php │ ├── dca │ │ ├── tl_member_group.php │ │ ├── tl_module.php │ │ ├── tl_user.php │ │ ├── tl_user_group.php │ │ ├── tl_workflow.php │ │ ├── tl_workflow_action.php │ │ ├── tl_workflow_permission.php │ │ ├── tl_workflow_state.php │ │ ├── tl_workflow_step.php │ │ ├── tl_workflow_transition.php │ │ └── tl_workflow_transition_conditional_transition.php │ ├── languages │ │ ├── de │ │ │ ├── default.php │ │ │ ├── modules.php │ │ │ ├── tl_member_group.php │ │ │ ├── tl_user.php │ │ │ ├── tl_user_group.php │ │ │ ├── tl_workflow.php │ │ │ ├── tl_workflow_action.php │ │ │ ├── tl_workflow_state.php │ │ │ ├── tl_workflow_step.php │ │ │ ├── tl_workflow_transition.php │ │ │ ├── workflow.php │ │ │ ├── workflow_messages.php │ │ │ └── workflow_permissions.php │ │ └── en │ │ │ ├── default.php │ │ │ ├── modules.php │ │ │ ├── tl_member_group.php │ │ │ ├── tl_module.php │ │ │ ├── tl_user.php │ │ │ ├── tl_user_group.php │ │ │ ├── tl_workflow.php │ │ │ ├── tl_workflow_action.php │ │ │ ├── tl_workflow_state.php │ │ │ ├── tl_workflow_step.php │ │ │ ├── tl_workflow_transition.php │ │ │ ├── workflow.php │ │ │ ├── workflow_messages.php │ │ │ └── workflow_permissions.php │ └── templates │ │ ├── be_workflow_state_row.html5 │ │ ├── be_workflow_transition_form.html5 │ │ ├── modules │ │ └── mod_workflow_transition.html5 │ │ └── workflow_errors.html5 ├── public │ ├── css │ │ └── backend.css │ ├── img │ │ ├── action.png │ │ ├── action_.png │ │ ├── role.png │ │ ├── step.png │ │ ├── transition.png │ │ ├── workflow-module.svg │ │ └── workflow.png │ └── js │ │ └── backend.js ├── translations │ ├── netzmacht_workflow.de.xliff │ └── netzmacht_workflow.en.xlf └── views │ ├── backend │ ├── backend.html.twig │ ├── default.html.twig │ ├── step.html.twig │ └── transition.html.twig │ ├── frontend │ └── transition.html.twig │ └── sections │ ├── buttons.html.twig │ ├── errors.html.twig │ ├── form.html.twig │ ├── headline.html.twig │ ├── history.html.twig │ └── teaser.html.twig ├── Security ├── AbstractPermissionVoter.php ├── StepPermissionVoter.php ├── StepVoter.php ├── TransitionPermissionVoter.php ├── TransitionVoter.php ├── User.php ├── WorkflowPermissions.php └── WorkflowUser.php └── Workflow ├── Definition ├── Database │ ├── ConditionBuilder.php │ └── WorkflowBuilder.php ├── Definition.php ├── Event │ ├── CreateStepEvent.php │ ├── CreateTransitionEvent.php │ └── CreateWorkflowEvent.php ├── Exception │ └── DefinitionException.php └── Loader │ ├── DatabaseDrivenWorkflowLoader.php │ └── WorkflowLoader.php ├── Entity ├── ContaoModel │ ├── ContaoModelEntityRepository.php │ ├── ContaoModelEntityRepositoryFactory.php │ ├── ContaoModelRelatedModelChangeTracker.php │ └── ContaoModelSpecificationAwareSpecification.php ├── Database │ ├── DatabaseEntityRepository.php │ └── DatabaseEntityRepositoryFactory.php ├── DelegatingRepositoryFactory.php ├── EntityManager.php └── RepositoryFactory.php ├── Exception ├── UnsupportedAction.php ├── UnsupportedEntity.php └── UnsupportedViewContentType.php ├── Flow ├── Action │ ├── AbstractAction.php │ ├── AbstractPropertyAccessAction.php │ ├── ActionFactory.php │ ├── ActionTypeFactory.php │ ├── AssignUser │ │ ├── AssignUserAction.php │ │ ├── AssignUserActionFactory.php │ │ └── AssignUserActionFormBuilder.php │ ├── ChangeLogging.php │ ├── ConditionalTransition │ │ └── ConditionalTransitionAction.php │ ├── Form │ │ ├── FormAction.php │ │ ├── FormActionFactory.php │ │ └── FormActionFormBuilder.php │ ├── Integration │ │ └── UpdateEntityAction.php │ ├── Metadata │ │ └── MetadataAction.php │ ├── Note │ │ ├── NoteAction.php │ │ ├── NoteActionFactory.php │ │ └── NoteActionFormBuilder.php │ ├── Notification │ │ ├── BuildNotificationTokensEvent.php │ │ ├── NotificationAction.php │ │ └── NotificationActionFactory.php │ ├── UpdateEntityAction │ │ ├── UpdateEntityAction.php │ │ ├── UpdateEntityActionFactory.php │ │ └── UpdateEntityActionFormBuilder.php │ ├── UpdatePropertyAction │ │ ├── UpdatePropertyAction.php │ │ └── UpdatePropertyActionFactory.php │ └── WorkflowChange │ │ └── WorkflowChangeAction.php └── Condition │ ├── Transition │ ├── AssignedUserCondition.php │ ├── ExpressionCondition.php │ ├── PropertyCondition.php │ ├── StepPermissionCondition.php │ └── TransitionPermissionCondition.php │ └── Workflow │ ├── PropertyCondition.php │ └── TypeCondition.php ├── Manager └── LazyLoadingWorkflowManager.php ├── Type ├── AbstractWorkflowType.php ├── DefaultWorkflowType.php ├── WorkflowType.php ├── WorkflowTypeNotFound.php └── WorkflowTypeRegistry.php └── View ├── Factory ├── DelegatingViewFactory.php └── HtmlViewFactory.php ├── HtmlView.php ├── Renderer.php ├── Renderer ├── AbstractRenderer.php ├── AbstractStepRenderer.php ├── AbstractTransitionRenderer.php ├── ButtonRenderer.php ├── DelegatingRenderer.php ├── ErrorRenderer.php ├── FormRenderer.php ├── HeadlineRenderer.php ├── StateHistoryRenderer.php └── TeaserRenderer.php ├── Sections.php ├── View.php └── ViewFactory.php /.phpcq/local/repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "phpspec": [ 4 | { 5 | "api-version": "1.0.0", 6 | "version": "1.0.0", 7 | "type": "php-file", 8 | "url": "phpspec-plugin.php", 9 | "checksum": { 10 | "type": "sha-512", 11 | "value": "0307cb00e814c3de436e2c0cc87e4cb949fdec8ce8b3f8fc5a25f7b3631f27dfe8e2b867f4e6419ad4668e252a29267d022c3f6fa20118e4016df66ccfce5605" 12 | } 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Workflow integration for Contao CMS 3 | =================================== 4 | 5 | 6 | [![Build Status](https://github.com/netzmacht/contao-workflow/actions/workflows/diagnostics.yml/badge.svg)](https://github.com/netzmacht/contao-workflow/actions) 7 | [![Version](http://img.shields.io/packagist/v/netzmacht/contao-workflow.svg?style=flat-square)](http://packagist.com/packages/netzmacht/contao-workflow) 8 | [![License](http://img.shields.io/packagist/l/netzmacht/contao-workflow.svg?style=flat-square)](http://packagist.org/packages/netzmacht/contao-workflow) 9 | [![Downloads](http://img.shields.io/packagist/dt/netzmacht/contao-workflow.svg?style=flat-square)](http://packagist.org/packages/netzmacht/contao-workflow) 10 | [![Contao Community Alliance coding standard](http://img.shields.io/badge/cca-coding_standard-red.svg?style=flat-square)](https://github.com/contao-community-alliance/coding-standard) 11 | 12 | This library is an integration of the [workflow library](http://github.com/netzmacht/workflow) for the Contao CMS. It 13 | provides a backend user interface to define workflows. 14 | 15 | This extension is intended for developers which have to integrate workflows for their entities. Though this extension 16 | provides a default workflow integration you probably need to create own actions to perform changes during the workflow. 17 | 18 | ## Requirements 19 | 20 | * Contao `^4.9` 21 | * PHP `^7.1 || ^8.0` 22 | * Symfony `^4.4 || ^5.1` 23 | 24 | ## Documentation 25 | 26 | A short [documentation] written in German exists. There is also an [example] for customized workflow development. 27 | 28 | ## Changelog 29 | 30 | See [changelog] 31 | 32 | ## License 33 | 34 | [LGPL-3.0-or-later] 35 | 36 | [documentation]: https://netzmacht.github.io/contao-workflow/ 37 | [example]: https://github.com/netzmacht/contao-workflow/tree/master/example 38 | [changelog]: https://github.com/netzmacht/contao-workflow/tree/master/CHANGELOG.md 39 | [LGPL-3.0-or-later]: https://github.com/netzmacht/contao-workflow/tree/master/LICENSE 40 | -------------------------------------------------------------------------------- /example/src/DependencyInjection/NetzmachtContaoWorkflowExampleExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Netzmacht\ContaoWorkflowExampleBundle\DependencyInjection; 17 | 18 | use Symfony\Component\Config\FileLocator; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\DependencyInjection\Extension\Extension; 21 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 22 | 23 | /** 24 | * Class NetzmachtContaoWorkflowExampleExtension 25 | */ 26 | final class NetzmachtContaoWorkflowExampleExtension extends Extension 27 | { 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function load(array $configs, ContainerBuilder $container): void 32 | { 33 | $loader = new YamlFileLoader( 34 | $container, 35 | new FileLocator(dirname(__DIR__) . '/Resources/config') 36 | ); 37 | 38 | $loader->load('services.yml'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/NetzmachtContaoWorkflowExampleBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Netzmacht\ContaoWorkflowExampleBundle; 17 | 18 | use Symfony\Component\HttpKernel\Bundle\Bundle; 19 | 20 | /** 21 | * Class NetzmachtContaoWorkflowExampleBundle 22 | */ 23 | final class NetzmachtContaoWorkflowExampleBundle extends Bundle 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /example/src/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | Netzmacht\ContaoWorkflowExampleBundle\Workflow\Type\ExampleType: 3 | tags: 4 | - { name: 'netzmacht.contao_workflow.type' } 5 | 6 | Netzmacht\ContaoWorkflowExampleBundle\Workflow\Action\PublishActionFactory: 7 | arguments: 8 | - '@netzmacht.contao_workflow.property_access_manager' 9 | tags: 10 | - { name: 'netzmacht.contao_workflow.action' } 11 | 12 | Netzmacht\ContaoWorkflowExampleBundle\Workflow\Action\SendEmailNotificationFactory: 13 | arguments: 14 | - '@mailer' 15 | tags: 16 | - { name: 'netzmacht.contao_workflow.action' } 17 | 18 | Netzmacht\ContaoWorkflowExampleBundle\Workflow\Form\SendEmailNotificationFormBuilder: 19 | tags: 20 | - { name: 'netzmacht.contao_workflow.action_form_builder'} 21 | 22 | Netzmacht\ContaoWorkflowExampleBundle\EventListener\ExampleDcaListener: 23 | public: true 24 | arguments: 25 | - '@netzmacht.contao_toolkit.dca.manager' 26 | - '@translator' 27 | -------------------------------------------------------------------------------- /example/src/Resources/contao/config/config.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | $GLOBALS['BE_MOD']['workflow']['example']['tables'] = ['tl_example']; 17 | -------------------------------------------------------------------------------- /example/src/Resources/contao/dca/tl_workflow.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | $GLOBALS['TL_DCA']['tl_workflow']['metapalettes']['example extends __base__'] = []; 17 | -------------------------------------------------------------------------------- /example/src/Resources/contao/dca/tl_workflow_action.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | $GLOBALS['TL_DCA']['tl_workflow_action']['metapalettes']['example_publish extends default'] = [ 17 | 'config' => ['publish_state'], 18 | ]; 19 | 20 | $GLOBALS['TL_DCA']['tl_workflow_action']['metapalettes']['example_notification extends default'] = [ 21 | 'config' => ['notification_recipient', 'notification_message'], 22 | ]; 23 | 24 | 25 | $GLOBALS['TL_DCA']['tl_workflow_action']['fields']['publish_state'] = [ 26 | 'label' => &$GLOBALS['TL_LANG']['tl_workflow_action']['publish_state'], 27 | 'inputType' => 'checkbox', 28 | 'exclude' => true, 29 | 'reference' => &$GLOBALS['TL_LANG']['tl_workflow_action']['publish_state'], 30 | 'eval' => [ 31 | 'tl_class' => 'w50', 32 | ], 33 | 'sql' => "char(1) NOT NULL default ''", 34 | ]; 35 | 36 | $GLOBALS['TL_DCA']['tl_workflow_action']['fields']['notification_recipient'] = [ 37 | 'label' => &$GLOBALS['TL_LANG']['tl_workflow_action']['notification_recipient'], 38 | 'inputType' => 'text', 39 | 'exclude' => true, 40 | 'eval' => [ 41 | 'tl_class' => 'w50', 42 | 'maxlength' => 255, 43 | 'rgxp' => 'email', 44 | 'mandatory' => true, 45 | ], 46 | 'sql' => "varchar(255) NOT NULL default ''", 47 | ]; 48 | 49 | $GLOBALS['TL_DCA']['tl_workflow_action']['fields']['notification_message'] = [ 50 | 'label' => &$GLOBALS['TL_LANG']['tl_workflow_action']['notification_message'], 51 | 'inputType' => 'textarea', 52 | 'exclude' => true, 53 | 'eval' => [ 54 | 'tl_class' => 'long clr', 55 | ], 56 | 'sql' => 'tinytext NULL', 57 | ]; 58 | -------------------------------------------------------------------------------- /example/src/Resources/contao/languages/en/modules.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | $GLOBALS['TL_LANG']['MOD']['example'][0] = 'Examples'; 17 | $GLOBALS['TL_LANG']['MOD']['example'][1] = 'Manage examples'; 18 | -------------------------------------------------------------------------------- /example/src/Resources/contao/languages/en/tl_example.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | $GLOBALS['TL_LANG']['tl_example']['new'][0] = 'New Example'; 17 | $GLOBALS['TL_LANG']['tl_example']['new'][1] = 'New Example'; 18 | $GLOBALS['TL_LANG']['tl_example']['edit'][0] = 'Edit Example'; 19 | $GLOBALS['TL_LANG']['tl_example']['edit'][1] = 'Edit Example'; 20 | $GLOBALS['TL_LANG']['tl_example']['copy'][0] = 'Copy Example'; 21 | $GLOBALS['TL_LANG']['tl_example']['copy'][1] = 'Copy Example'; 22 | $GLOBALS['TL_LANG']['tl_example']['delete'][0] = 'Delete Example'; 23 | $GLOBALS['TL_LANG']['tl_example']['delete'][1] = 'Delete Example'; 24 | $GLOBALS['TL_LANG']['tl_example']['show'][0] = 'Show details of Example'; 25 | $GLOBALS['TL_LANG']['tl_example']['show'][1] = 'Show details of Example'; 26 | $GLOBALS['TL_LANG']['tl_example']['workflowBT'][0] = 'Workflow overview'; 27 | $GLOBALS['TL_LANG']['tl_example']['workflowBT'][1] = 'Workflow overview'; 28 | $GLOBALS['TL_LANG']['tl_example']['workflow'][0] = 'Workflow'; 29 | $GLOBALS['TL_LANG']['tl_example']['workflow'][1] = 'Please select a workflow.'; 30 | $GLOBALS['TL_LANG']['tl_example']['workflowStep'][0] = 'Current step'; 31 | $GLOBALS['TL_LANG']['tl_example']['workflowStep'][1] = 'Current step of the workflow.'; 32 | 33 | $GLOBALS['TL_LANG']['tl_example']['title_legend'] = 'Example'; 34 | $GLOBALS['TL_LANG']['tl_example']['title'][0] = 'Title'; 35 | $GLOBALS['TL_LANG']['tl_example']['published'][0] = 'Published'; 36 | -------------------------------------------------------------------------------- /example/src/Resources/contao/languages/en/tl_workflow_action.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | $GLOBALS['TL_LANG']['tl_workflow_action']['types']['example'] = 'Example'; 17 | $GLOBALS['TL_LANG']['tl_workflow_action']['types']['example_publish'][0] = 'Change publish state'; 18 | $GLOBALS['TL_LANG']['tl_workflow_action']['types']['example_notification'][0] = 'Send notification'; 19 | 20 | $GLOBALS['TL_LANG']['tl_workflow_action']['publish_state'][0] = 'Published'; 21 | $GLOBALS['TL_LANG']['tl_workflow_action']['publish_state'][1] = 'Please define new published state.'; 22 | $GLOBALS['TL_LANG']['tl_workflow_action']['notification_recipient'][0] = 'Reciepient'; 23 | $GLOBALS['TL_LANG']['tl_workflow_action']['notification_recipient'][1] = 'Please insert e-mail address of recipient.'; 24 | $GLOBALS['TL_LANG']['tl_workflow_action']['notification_message'][0] = 'Message'; 25 | $GLOBALS['TL_LANG']['tl_workflow_action']['notification_message'][1] = 'E-Mail message.'; 26 | -------------------------------------------------------------------------------- /example/src/Workflow/Action/AbstractExampleActionFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Netzmacht\ContaoWorkflowExampleBundle\Workflow\Action; 17 | 18 | use Netzmacht\ContaoWorkflowBundle\Workflow\Flow\Action\ActionTypeFactory; 19 | use Netzmacht\ContaoWorkflowBundle\Workflow\Type\WorkflowType; 20 | use Netzmacht\ContaoWorkflowExampleBundle\Workflow\Type\ExampleType; 21 | use Netzmacht\Workflow\Flow\Workflow; 22 | 23 | /** 24 | * Class AbstractExampleActionFactory 25 | */ 26 | abstract class AbstractExampleActionFactory implements ActionTypeFactory 27 | { 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function getCategory(): string 32 | { 33 | return 'example'; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function supports(Workflow $workflow): bool 40 | { 41 | return $workflow->getConfigValue(WorkflowType::class) instanceof ExampleType; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/src/Workflow/Action/PublishActionFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Netzmacht\ContaoWorkflowExampleBundle\Workflow\Action; 17 | 18 | use Netzmacht\ContaoWorkflowBundle\PropertyAccess\PropertyAccessManager; 19 | use Netzmacht\Workflow\Flow\Action; 20 | use Netzmacht\Workflow\Flow\Transition; 21 | 22 | /** 23 | * Class PublishActionFactory 24 | */ 25 | final class PublishActionFactory extends AbstractExampleActionFactory 26 | { 27 | /** 28 | * Property access manager. 29 | * 30 | * @var PropertyAccessManager 31 | */ 32 | private $propertyAccessManager; 33 | 34 | /** 35 | * PublishActionFactory constructor. 36 | * 37 | * @param PropertyAccessManager $propertyAccessManager Property access manager. 38 | */ 39 | public function __construct(PropertyAccessManager $propertyAccessManager) 40 | { 41 | $this->propertyAccessManager = $propertyAccessManager; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function getName(): string 48 | { 49 | return 'example_publish'; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function isPostAction(): bool 56 | { 57 | return false; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | public function create(array $config, Transition $transition): Action 64 | { 65 | return new PublishAction( 66 | $this->propertyAccessManager, 67 | 'action_' . $config['id'], 68 | $config['label'], 69 | $config['publish_state'], 70 | $config 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/src/Workflow/Action/SendEmailNotificationFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Netzmacht\ContaoWorkflowExampleBundle\Workflow\Action; 17 | 18 | use Netzmacht\Workflow\Flow\Action; 19 | use Netzmacht\Workflow\Flow\Transition; 20 | use Swift_Mailer; 21 | 22 | /** 23 | * Class SendEmailNotificationFactory 24 | */ 25 | final class SendEmailNotificationFactory extends AbstractExampleActionFactory 26 | { 27 | /** 28 | * Mailer service. 29 | * 30 | * @var Swift_Mailer 31 | */ 32 | private $mailer; 33 | 34 | /** 35 | * SendEmailNotificationFactory constructor. 36 | * 37 | * @param Swift_Mailer $mailer Mailer service. 38 | */ 39 | public function __construct(Swift_Mailer $mailer) 40 | { 41 | $this->mailer = $mailer; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function getName(): string 48 | { 49 | return 'example_notification'; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function isPostAction(): bool 56 | { 57 | return false; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | public function create(array $config, Transition $transition): Action 64 | { 65 | return new SendEmailNotificationAction( 66 | $this->mailer, 67 | 'action_' . $config['id'], 68 | $config['label'], 69 | $config['notification_recipient'], 70 | $config['notification_message'] 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/src/Workflow/Type/ExampleType.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2014-2019 netzmacht David Molineus 10 | * @license LGPL 3.0-or-later 11 | * @filesource 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Netzmacht\ContaoWorkflowExampleBundle\Workflow\Type; 17 | 18 | use Netzmacht\ContaoWorkflowBundle\Workflow\Type\AbstractWorkflowType; 19 | 20 | /** 21 | * Class ExampleType 22 | */ 23 | final class ExampleType extends AbstractWorkflowType 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function __construct() 29 | { 30 | parent::__construct('example', ['tl_example']); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | formatter.name: pretty 2 | composer_suite_detection: true 3 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /publish_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd docs 4 | 5 | gitbook build 6 | 7 | cd .. 8 | 9 | git checkout gh-pages 10 | 11 | git pull origin gh-pages --rebase 12 | 13 | cp -R ./docs/_book/* . 14 | 15 | git add . 16 | 17 | git commit -a -m "Update docs." 18 | 19 | git push origin gh-pages 20 | 21 | git checkout master 22 | -------------------------------------------------------------------------------- /src/ContaoManager/Plugin.php: -------------------------------------------------------------------------------- 1 | setLoadAfter( 30 | [ 31 | ContaoCoreBundle::class, 32 | SensioFrameworkExtraBundle::class, 33 | NetzmachtContaoToolkitBundle::class, 34 | ] 35 | ) 36 | ->setReplace(['workflow']), 37 | ]; 38 | } 39 | 40 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 41 | public function getRouteCollection(LoaderResolverInterface $resolver, KernelInterface $kernel): ?RouteCollection 42 | { 43 | $loader = $resolver->resolve(__DIR__ . '/../Resources/config/routing.yml'); 44 | if ($loader === false) { 45 | return null; 46 | } 47 | 48 | return $loader->load(__DIR__ . '/../Resources/config/routing.yml'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Controller/Backend/AbstractController.php: -------------------------------------------------------------------------------- 1 | workflowManager = $workflowManager; 61 | $this->entityManager = $entityManager; 62 | $this->viewFactory = $viewFactory; 63 | $this->router = $router; 64 | } 65 | 66 | /** 67 | * Find the entity. 68 | * 69 | * @param EntityId $entityId The entity id. 70 | * 71 | * @throws NotFoundHttpException If entity is not found. 72 | */ 73 | protected function createItem(EntityId $entityId): Item 74 | { 75 | try { 76 | $repository = $this->entityManager->getRepository($entityId->getProviderName()); 77 | $entity = $repository->find($entityId->getIdentifier()); 78 | } catch (UnsupportedEntity $e) { 79 | throw new NotFoundHttpException(sprintf('Entity "%s" not found.', (string) $entityId), $e); 80 | } 81 | 82 | return $this->workflowManager->createItem($entityId, $entity); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/DependencyInjection/NetzmachtContaoWorkflowExtension.php: -------------------------------------------------------------------------------- 1 | load('templates.yml'); 30 | $loader->load('actions.yml'); 31 | $loader->load('controllers.yml'); 32 | $loader->load('listeners.yml'); 33 | $loader->load('renderer.yml'); 34 | $loader->load('integration.yml'); 35 | $loader->load('transitions.yml'); 36 | $loader->load('services.yml'); 37 | 38 | $configuration = new Configuration(); 39 | $config = $this->processConfiguration($configuration, $configs); 40 | $providers = $config['providers']; 41 | 42 | foreach (array_keys($config['default_type']) as $provider) { 43 | $providers[$provider] = [ 44 | 'workflow' => true, 45 | 'step' => true, 46 | 'step_permission' => $config['default_type'][$provider]['step_permission'], 47 | 'assign_users' => $config['default_type'][$provider]['assign_users'], 48 | ]; 49 | } 50 | 51 | $container->setParameter('netzmacht.contao_workflow.dca_providers', $providers); 52 | $container->setParameter('netzmacht.contao_workflow.type.default', $config['default_type']); 53 | /** @psalm-suppress PossiblyInvalidArgument */ 54 | $container->setParameter( 55 | 'netzmacht.contao_workflow.transition_types', 56 | array_merge($config['transitions'], $container->getParameter('netzmacht.contao_workflow.transition_types')) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/DependencyInjection/Pass/ExpressionLanguageFunctionsPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('netzmacht.contao_workflow.expression_language')) { 18 | return; 19 | } 20 | 21 | $definition = $container->getDefinition('netzmacht.contao_workflow.expression_language'); 22 | $taggedServices = $this->findAndSortTaggedServices( 23 | 'netzmacht.contao_workflow.expression_language.function_provider', 24 | $container 25 | ); 26 | 27 | $definition->setArgument(1, $taggedServices); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DependencyInjection/Pass/TransitionFormBuilderPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('netzmacht.contao_workflow.form.transition_form_builder')) { 18 | return; 19 | } 20 | 21 | $definition = $container->getDefinition('netzmacht.contao_workflow.form.transition_form_builder'); 22 | $formBuilders = $this->findAndSortTaggedServices( 23 | 'netzmacht.contao_workflow.transition_form_builder', 24 | $container 25 | ); 26 | 27 | foreach ($formBuilders as $formBuilder) { 28 | $definition->addMethodCall('register', [$formBuilder]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DependencyInjection/Pass/ViewFactoryPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('netzmacht.contao_workflow.view.factory')) { 24 | return; 25 | } 26 | 27 | $serviceIds = $container->findTaggedServiceIds('netzmacht.contao_workflow.view_factory'); 28 | $definition = $container->getDefinition('netzmacht.contao_workflow.view.factory'); 29 | $factories = (array) $definition->getArgument(0); 30 | 31 | foreach ($serviceIds as $serviceId => $tags) { 32 | foreach ($tags as $attributes) { 33 | if (! isset($attributes['content_type'])) { 34 | throw new InvalidConfigurationException( 35 | sprintf( 36 | 'Service "%s" is tagged as workflow view factory but content_type attribute is missing', 37 | $serviceId 38 | ) 39 | ); 40 | } 41 | 42 | $factories[$attributes['content_type']] = new Reference($serviceId); 43 | } 44 | } 45 | 46 | $definition->setArgument(0, $factories); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/EventListener/Backend/AssetListener.php: -------------------------------------------------------------------------------- 1 | assetsManager = $assetsManager; 22 | $this->scopeMatcher = $scopeMatcher; 23 | } 24 | 25 | /** 26 | * Adds extensions' stylesheet to back-end. 27 | */ 28 | public function addBackendAssets(RequestEvent $event): void 29 | { 30 | if (! $this->scopeMatcher->isBackendRequest($event->getRequest())) { 31 | return; 32 | } 33 | 34 | $this->assetsManager->addStylesheet('bundles/netzmachtcontaoworkflow/css/backend.css'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/EventListener/Backend/UserNavigationListener.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 24 | } 25 | 26 | /** 27 | * Handle the getUserNavigation hook to determine if workflow module is used. 28 | * 29 | * @param array>> $modules User navigation modules. 30 | * 31 | * @return array>> 32 | */ 33 | public function onGetUserNavigation(array $modules): array 34 | { 35 | $request = $this->requestStack->getCurrentRequest(); 36 | if (! $request) { 37 | return $modules; 38 | } 39 | 40 | $module = $request->attributes->get('module'); 41 | 42 | if ($request->attributes->get('_backend_module') === 'workflow') { 43 | foreach ($modules as $group => $groupModules) { 44 | if (! isset($groupModules['modules'][$module])) { 45 | continue; 46 | } 47 | 48 | $modules[$group]['modules'][$module]['isActive'] = true; 49 | } 50 | } 51 | 52 | return $modules; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EventListener/Dca/CommonListener.php: -------------------------------------------------------------------------------- 1 | $row Current data row. 18 | */ 19 | public function generateRow(array $row): string 20 | { 21 | return sprintf( 22 | '%s
%s', 23 | $row['label'], 24 | $row['description'] 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EventListener/Dca/ModuleCallbackListener.php: -------------------------------------------------------------------------------- 1 | workflowManager = $workflowManager; 30 | } 31 | 32 | /** 33 | * Get all providers with a workflow. 34 | * 35 | * @return list 36 | */ 37 | public function providerOptions(): array 38 | { 39 | $providers = []; 40 | 41 | foreach ($this->workflowManager->getWorkflows() as $workflow) { 42 | $providers[] = $workflow->getProviderName(); 43 | } 44 | 45 | $providers = array_unique($providers); 46 | sort($providers); 47 | 48 | return $providers; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/EventListener/Hook/LoadDataContainerListener.php: -------------------------------------------------------------------------------- 1 | */ 19 | private $input; 20 | 21 | public function __construct(DcaManager $dcaManager, Adapter $adapter) 22 | { 23 | $this->dcaManager = $dcaManager; 24 | $this->input = $adapter; 25 | } 26 | 27 | public function __invoke(string $name): void 28 | { 29 | if ($name !== 'tl_workflow_action') { 30 | return; 31 | } 32 | 33 | if ($this->input->get('ptable') === 'tl_workflow') { 34 | return; 35 | } 36 | 37 | $this->dcaManager->getDefinition('tl_workflow_action')->set(['config', 'ptable'], 'tl_workflow_transition'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/EventListener/Workflow/CreateConditionalTransitionsListener.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 32 | } 33 | 34 | /** 35 | * Handle the event. 36 | * 37 | * @param CreateTransitionEvent $event The event. 38 | */ 39 | public function onCreateTransition(CreateTransitionEvent $event): void 40 | { 41 | $transition = $event->getTransition(); 42 | if ($transition->getConfigValue('type') !== 'conditional') { 43 | return; 44 | } 45 | 46 | $workflow = $event->getTransition()->getWorkflow(); 47 | $transitions = $this->getConditionalTransitionNames((int) $transition->getConfigValue('id')); 48 | 49 | $action = new ConditionalTransitionAction( 50 | sprintf('%s_%s_conditional_action', $workflow->getName(), $transition->getName()), 51 | $workflow, 52 | $transitions 53 | ); 54 | 55 | $transition->addPostAction($action); 56 | } 57 | 58 | /** 59 | * Get conditional transition actions by transition id. 60 | * 61 | * @param int $transitionId The id of the parent transition. 62 | * 63 | * @return string[] 64 | */ 65 | private function getConditionalTransitionNames(int $transitionId): array 66 | { 67 | $sql = 'SELECT tid FROM tl_workflow_transition_conditional_transition WHERE pid=:pid ORDER BY sorting'; 68 | $statement = $this->connection->prepare($sql); 69 | $result = $statement->executeQuery(['pid' => $transitionId]); 70 | 71 | return array_map( 72 | // @codingStandardsIgnoreStart 73 | static function ($transitionId) : string { 74 | return 'transition_' . $transitionId; 75 | }, 76 | // @codingStandardsIgnoreEnd 77 | $result->fetchFirstColumn() 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/EventListener/Workflow/CreateMetaDataActionListener.php: -------------------------------------------------------------------------------- 1 | metadataAction = $action; 29 | } 30 | 31 | /** 32 | * Handle the event. 33 | * 34 | * @param CreateTransitionEvent $event The event. 35 | */ 36 | public function onCreateTransition(CreateTransitionEvent $event): void 37 | { 38 | $transition = $event->getTransition(); 39 | 40 | if ($transition->getConfigValue(Definition::SOURCE) !== Definition::SOURCE_DATABASE) { 41 | return; 42 | } 43 | 44 | $event->getTransition()->addAction($this->metadataAction); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/EventListener/Workflow/CreateWorkflowChangeTransitionListener.php: -------------------------------------------------------------------------------- 1 | workflowManager = $workflowManager; 29 | } 30 | 31 | /** 32 | * Handle the event. 33 | * 34 | * @param CreateTransitionEvent $event The event. 35 | * 36 | * @throws DefinitionException When an invalid workflow value is given. 37 | */ 38 | public function onCreateTransition(CreateTransitionEvent $event): void 39 | { 40 | $transition = $event->getTransition(); 41 | if ($transition->getConfigValue('type') !== 'workflow') { 42 | return; 43 | } 44 | 45 | if (! is_string($transition->getConfigValue('workflow'))) { 46 | throw new DefinitionException('Invalid target workflow defined.'); 47 | } 48 | 49 | $transition->addPostAction( 50 | new WorkflowChangeAction($this->workflowManager, $transition->getConfigValue('workflow')) 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exception/DataContainer/FieldAlreadyExists.php: -------------------------------------------------------------------------------- 1 | transitionFormBuilder = $transitionFormBuilder; 29 | } 30 | 31 | public function supports(Transition $transition): bool 32 | { 33 | return $transition->getConfigValue('type') === 'conditional'; 34 | } 35 | 36 | public function buildForm(Transition $transition, Item $item, Context $context, FormBuilder $formBuilder): void 37 | { 38 | foreach ($transition->getPostActions() as $action) { 39 | if (! $action instanceof ConditionalTransitionAction) { 40 | continue; 41 | } 42 | 43 | $conditionalTransition = $action->determineMatchingTransition($item, $context); 44 | if (! $conditionalTransition) { 45 | continue; 46 | } 47 | 48 | if (! $this->transitionFormBuilder->supports($conditionalTransition)) { 49 | continue; 50 | } 51 | 52 | $this->transitionFormBuilder->buildForm($conditionalTransition, $item, $context, $formBuilder); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Form/Builder/DataAwareActionFormBuilder.php: -------------------------------------------------------------------------------- 1 | $data Default form data. 27 | * 28 | * @return array 29 | */ 30 | public function buildFormData( 31 | Action $action, 32 | Transition $transition, 33 | Context $context, 34 | Item $item, 35 | array $data 36 | ): array; 37 | } 38 | -------------------------------------------------------------------------------- /src/Form/Builder/DelegatingTransitionFormBuilder.php: -------------------------------------------------------------------------------- 1 | register($formBuilder); 29 | } 30 | } 31 | 32 | /** 33 | * Register a transition form builder. 34 | * 35 | * @param TransitionFormBuilder $transitionFormBuilder The transition form builder. 36 | * 37 | * @reurn void 38 | */ 39 | public function register(TransitionFormBuilder $transitionFormBuilder): void 40 | { 41 | $this->formBuilders[] = $transitionFormBuilder; 42 | } 43 | 44 | public function supports(Transition $transition): bool 45 | { 46 | return true; 47 | } 48 | 49 | public function buildForm(Transition $transition, Item $item, Context $context, FormBuilder $formBuilder): void 50 | { 51 | foreach ($this->formBuilders as $transitionFormBuilder) { 52 | if (! $transitionFormBuilder->supports($transition)) { 53 | continue; 54 | } 55 | 56 | $transitionFormBuilder->buildForm($transition, $item, $context, $formBuilder); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Form/Builder/TransitionFormBuilder.php: -------------------------------------------------------------------------------- 1 | transitionFormBuilder = $transitionFormBuilder; 29 | } 30 | 31 | public function supports(Transition $transition): bool 32 | { 33 | return $transition->getConfigValue('type') === 'workflow'; 34 | } 35 | 36 | public function buildForm(Transition $transition, Item $item, Context $context, FormBuilder $formBuilder): void 37 | { 38 | foreach ($transition->getPostActions() as $action) { 39 | if (! $action instanceof WorkflowChangeAction) { 40 | continue; 41 | } 42 | 43 | $startTransition = $action->getStartTransition(); 44 | if (! $this->transitionFormBuilder->supports($startTransition)) { 45 | continue; 46 | } 47 | 48 | $this->transitionFormBuilder->buildForm($startTransition, $item, $context, $formBuilder); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Form/TransitionFormType.php: -------------------------------------------------------------------------------- 1 | formBuilder = $formBuilder; 32 | } 33 | 34 | public function configureOptions(OptionsResolver $resolver): void 35 | { 36 | $resolver 37 | ->setDefaults(['item' => null]) 38 | ->setRequired(['handler']) 39 | ->setAllowedTypes('handler', TransitionHandler::class) 40 | ->setAllowedTypes('item', Item::class); 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function buildForm(FormBuilder $builder, array $options): void 47 | { 48 | $transitionHandler = $options['handler']; 49 | assert($transitionHandler instanceof TransitionHandler); 50 | $transition = $transitionHandler->getTransition(); 51 | $item = $transitionHandler->getItem(); 52 | $context = $transitionHandler->getContext(); 53 | 54 | if ($this->formBuilder->supports($transition)) { 55 | $this->formBuilder->buildForm($transition, $item, $context, $builder); 56 | } 57 | 58 | $builder->add( 59 | 'submit', 60 | SubmitType::class, 61 | [ 62 | 'label' => $transition->getLabel(), 63 | ] 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Model/Action/ActionModel.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 32 | } 33 | 34 | /** 35 | * Find by transition. 36 | * 37 | * @param int $transitionId The transition id. 38 | * @param array $options Query options. 39 | * 40 | * @return ActionModel[]|Collection|null 41 | */ 42 | public function findByTransition(int $transitionId, array $options = ['order' => '.sorting']) 43 | { 44 | return $this->findBy(['.ptable=?', '.pid=?'], [TransitionModel::getTable(), $transitionId], $options); 45 | } 46 | 47 | /** 48 | * Find active by transition. 49 | * 50 | * @param int $transitionId The transition id. 51 | * @param string $orderField The order field. 52 | * @param string $direction The direction. 53 | * 54 | * @return ActionModel[]|Collection|null 55 | */ 56 | public function findActiveByTransition( 57 | int $transitionId, 58 | string $orderField = '.sorting', 59 | string $direction = 'ASC' 60 | ) { 61 | return $this->findBy( 62 | ['.ptable=?', '.pid=?', '.active=?'], 63 | [TransitionModel::getTable(), $transitionId, '1'], 64 | ['order' => $orderField . ' ' . $direction] 65 | ); 66 | } 67 | 68 | /** 69 | * Find global actions defined for a workflow. 70 | * 71 | * @param int $workflowId The workflow id. 72 | * @param array $options Query options. 73 | * 74 | * @return ActionModel[]|Collection|null 75 | */ 76 | public function findByWorkflow(int $workflowId, array $options = ['order' => '.sorting']) 77 | { 78 | return $this->findBy(['.ptable=?', '.pid=?'], [WorkflowModel::getTable(), $workflowId], $options); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Model/Permission/PermissionModel.php: -------------------------------------------------------------------------------- 1 | $options Query options. 25 | * 26 | * @return Collection|StepModel[]|null 27 | * @psalm-return Collection|null 28 | */ 29 | public function findByWorkflow(int $workflowId, array $options = ['order' => 'label']) 30 | { 31 | return $this->findBy( 32 | ['.pid=?'], 33 | [$workflowId], 34 | $options 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Model/Transition/TransitionModel.php: -------------------------------------------------------------------------------- 1 | self::$strTable . '.sorting'] 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Model/Transition/TransitionRepository.php: -------------------------------------------------------------------------------- 1 | $options Query options. 25 | * 26 | * @return Collection|TransitionModel[]|null 27 | */ 28 | public function findByWorkflow(int $workflowId, array $options = ['order' => '.sorting']) 29 | { 30 | return $this->findBy( 31 | ['.pid=?'], 32 | [$workflowId], 33 | $options 34 | ); 35 | } 36 | 37 | /** 38 | * Find active by workflow id. 39 | * 40 | * @param int $workflowId The workflow id. 41 | * @param array $options Query options. 42 | * 43 | * @return Collection|TransitionModel[]|null 44 | */ 45 | public function findActiveByTransition(int $workflowId, array $options = ['order' => '.sorting']) 46 | { 47 | return $this->findBy( 48 | ['.active=1', '.pid=?'], 49 | [$workflowId], 50 | $options 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Model/Workflow/WorkflowModel.php: -------------------------------------------------------------------------------- 1 | $options Query options. 26 | * 27 | * @return Collection|WorkflowModel[]|null 28 | */ 29 | public function findByProviderAndType(string $providerName, string $workflowType, array $options = []) 30 | { 31 | return $this->findBy( 32 | ['.providerName=?', '.type=?'], 33 | [$providerName, $workflowType], 34 | $options 35 | ); 36 | } 37 | 38 | /** 39 | * Find workflow definitions by the provider name. 40 | * 41 | * @param string $providerName The provider name. 42 | * @param array $options Query options. 43 | * 44 | * @return Collection|WorkflowModel[]|null 45 | */ 46 | public function findByProvider(string $providerName, array $options = []) 47 | { 48 | return $this->findBy(['.providerName=?'], [$providerName], $options); 49 | } 50 | 51 | /** 52 | * Find active workflow definitions. 53 | * 54 | * @param array $options Query options. 55 | * 56 | * @return Collection|WorkflowModel[]|null 57 | */ 58 | public function findActive(array $options = []) 59 | { 60 | return $this->findBy(['.active=?'], ['1'], $options); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/NetzmachtContaoWorkflowBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new ViewFactoryPass()); 18 | $container->addCompilerPass(new TransitionFormBuilderPass()); 19 | $container->addCompilerPass(new ExpressionLanguageFunctionsPass()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/PropertyAccess/ArrayPropertyAccessorFactory.php: -------------------------------------------------------------------------------- 1 | changesRegistry = $changesRegistry; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function supports($object): bool 36 | { 37 | return $object instanceof Model; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function create($object): PropertyAccessor 44 | { 45 | assert($object instanceof Model); 46 | 47 | return new ContaoModelPropertyAccessor($object, $this->changesRegistry); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/PropertyAccess/PropertyAccessManager.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | private $mapping = []; 29 | 30 | /** 31 | * @param iterable $factories Property access factories. 32 | */ 33 | public function __construct(iterable $factories) 34 | { 35 | Assert::thatAll($factories)->subclassOf(PropertyAccessorFactory::class); 36 | 37 | foreach ($factories as $factory) { 38 | $this->factories[] = $factory; 39 | } 40 | } 41 | 42 | /** 43 | * Determine if property access is supported for given entity. 44 | * 45 | * @param mixed $data Given data structure. 46 | */ 47 | public function supports($data): bool 48 | { 49 | foreach ($this->factories as $factory) { 50 | if ($factory->supports($data)) { 51 | return true; 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | 58 | /** 59 | * Provide access to a given data structure. 60 | * 61 | * @param mixed $data Data structure. 62 | * 63 | * @throws PropertyAccessFailed If no supported data structure is given. 64 | */ 65 | public function provideAccess($data): PropertyAccessor 66 | { 67 | $hash = is_object($data) ? spl_object_hash($data) : null; 68 | 69 | if ($hash && array_key_exists($hash, $this->mapping)) { 70 | return $this->mapping[$hash]; 71 | } 72 | 73 | foreach ($this->factories as $factory) { 74 | if (! $factory->supports($data)) { 75 | continue; 76 | } 77 | 78 | $accessor = $factory->create($data); 79 | 80 | if ($hash) { 81 | $this->mapping[$hash] = $accessor; 82 | } 83 | 84 | return $accessor; 85 | } 86 | 87 | throw new PropertyAccessFailed('Could not determine property accessor for given data.'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/PropertyAccess/PropertyAccessor.php: -------------------------------------------------------------------------------- 1 | propertyAccessor = $propertyAccessor; 27 | } 28 | 29 | /** 30 | * Check if property exists for the object. 31 | * 32 | * @param string $name Name of the property. 33 | */ 34 | public function has(string $name): bool 35 | { 36 | return $this->propertyAccessor->has($name); 37 | } 38 | 39 | /** 40 | * Get the deserialized value of the property. If property not exists null is returned. 41 | * 42 | * @param string $name Name of the property. 43 | * 44 | * @return mixed 45 | */ 46 | public function get(string $name) 47 | { 48 | return StringUtil::deserialize($this->propertyAccessor->get($name)); 49 | } 50 | 51 | /** 52 | * Get the raw value of the property. If property not exists null is returned. 53 | * 54 | * @param string $name Name of the property. 55 | * 56 | * @return mixed 57 | */ 58 | public function raw(string $name) 59 | { 60 | return $this->propertyAccessor->get($name); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Request/EntityIdParamConverter.php: -------------------------------------------------------------------------------- 1 | getName(); 20 | 21 | if (! $request->attributes->has($attribute)) { 22 | return false; 23 | } 24 | 25 | $entityId = EntityId::fromString($request->attributes->get($attribute)); 26 | $request->attributes->set($attribute, $entityId); 27 | 28 | return true; 29 | } 30 | 31 | public function supports(ParamConverter $configuration): bool 32 | { 33 | return $configuration->getClass() === EntityId::class; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Resources/config/controllers.yml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | autowire: false 5 | autoconfigure: false 6 | 7 | netzmacht.contao_workflow.backend.step_controller: 8 | class: Netzmacht\ContaoWorkflowBundle\Controller\Backend\StepController 9 | public: true 10 | arguments: 11 | - '@netzmacht.contao_workflow.manager' 12 | - '@netzmacht.contao_workflow.entity_manager' 13 | - '@netzmacht.contao_workflow.view.factory' 14 | - '@router' 15 | - '@security.authorization_checker' 16 | 17 | netzmacht.contao_workflow.backend.transition_controller: 18 | class: Netzmacht\ContaoWorkflowBundle\Controller\Backend\TransitionController 19 | public: true 20 | arguments: 21 | - '@netzmacht.contao_workflow.manager' 22 | - '@netzmacht.contao_workflow.entity_manager' 23 | - '@netzmacht.contao_workflow.view.factory' 24 | - '@router' 25 | - '@netzmacht.contao_workflow.form.factory' 26 | -------------------------------------------------------------------------------- /src/Resources/config/integration.yml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | autowire: false 5 | autoconfigure: false 6 | 7 | Netzmacht\ContaoWorkflowBundle\EventListener\Integration\DataContainerListener: 8 | public: true 9 | arguments: 10 | - '@netzmacht.contao_toolkit.dca.manager' 11 | - '@translator' 12 | - '%netzmacht.contao_workflow.dca_providers%' 13 | - '%netzmacht.contao_workflow.type.default%' 14 | tags: 15 | - { name: 'contao.hook', hook: 'loadDataContainer', priority: 100 } 16 | 17 | Netzmacht\ContaoWorkflowBundle\EventListener\Integration\OptionsListener: 18 | public: true 19 | arguments: 20 | - '@netzmacht.contao_workflow.manager' 21 | - '@database_connection' 22 | 23 | Netzmacht\ContaoWorkflowBundle\EventListener\Integration\OperationListener: 24 | public: true 25 | arguments: 26 | - '@netzmacht.contao_workflow.manager' 27 | - '@netzmacht.contao_workflow.entity_manager' 28 | - '@router' 29 | - '@security.authorization_checker' 30 | 31 | Netzmacht\ContaoWorkflowBundle\EventListener\Integration\SubmitButtonsListener: 32 | public: true 33 | arguments: 34 | - '@netzmacht.contao_workflow.manager' 35 | - '@netzmacht.contao_workflow.entity_manager' 36 | - '@router' 37 | - '@netzmacht.contao_toolkit.contao.input_adapter' 38 | -------------------------------------------------------------------------------- /src/Resources/config/renderer.yml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | autowire: false 5 | autoconfigure: false 6 | 7 | netzmacht.contao_workflow.view.renderer: 8 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\DelegatingRenderer 9 | arguments: 10 | - !tagged netzmacht.contao_workflow.view.renderer 11 | 12 | netzmacht.contao_workflow.view.headline_renderer: 13 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\HeadlineRenderer 14 | arguments: 15 | - '@translator' 16 | - '%netzmacht.contao_workflow.templates.headline%' 17 | tags: 18 | - { name: netzmacht.contao_workflow.view.renderer, priority: 10 } 19 | 20 | netzmacht.contao_workflow.view.button_renderer: 21 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\ButtonRenderer 22 | arguments: 23 | - '@translator' 24 | - '@netzmacht.contao_toolkit.routing.scope_matcher' 25 | - '@request_stack' 26 | - '%netzmacht.contao_workflow.templates.button%' 27 | tags: 28 | - { name: netzmacht.contao_workflow.view.renderer, priority: -10 } 29 | 30 | netzmacht.contao_workflow.view.history_renderer: 31 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\StateHistoryRenderer 32 | arguments: 33 | - '@netzmacht.contao_workflow.manager' 34 | - '@translator' 35 | - '@netzmacht.contao_toolkit.contao.config_adapter' 36 | - '@netzmacht.contao_workflow.entity_manager' 37 | - '%netzmacht.contao_workflow.templates.history%' 38 | tags: 39 | - { name: netzmacht.contao_workflow.view.renderer, priority: -50 } 40 | 41 | netzmacht.contao_workflow.view.teaser_renderer: 42 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\TeaserRenderer 43 | arguments: 44 | - '@translator' 45 | - '@netzmacht.contao_workflow.property_access_manager' 46 | - '%netzmacht.contao_workflow.templates.teaser%' 47 | tags: 48 | - { name: netzmacht.contao_workflow.view.renderer, priority: 10 } 49 | 50 | netzmacht.contao_workflow.view.form_renderer: 51 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\FormRenderer 52 | arguments: 53 | - '@translator' 54 | - '%netzmacht.contao_workflow.templates.form%' 55 | tags: 56 | - { name: netzmacht.contao_workflow.view.renderer } 57 | 58 | netzmacht.contao_workflow.view.error_renderer: 59 | class: Netzmacht\ContaoWorkflowBundle\Workflow\View\Renderer\ErrorRenderer 60 | arguments: 61 | - '@translator' 62 | - '%netzmacht.contao_workflow.templates.errors%' 63 | tags: 64 | - { name: netzmacht.contao_workflow.view.renderer, priority: 20 } 65 | -------------------------------------------------------------------------------- /src/Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | netzmacht.contao_workflow.backend.step: 2 | path: /contao/workflow/{module}/{entityId} 3 | defaults: 4 | _controller: netzmacht.contao_workflow.backend.step_controller 5 | _scope: "backend" 6 | _token_check: true 7 | _custom_backend_view: true 8 | _backend_module: workflow 9 | 10 | netzmacht.contao_workflow.backend.transition: 11 | path: /contao/workflow/{module}/{entityId}/{transition} 12 | methods: POST 13 | defaults: 14 | _controller: netzmacht.contao_workflow.backend.transition_controller 15 | _scope: "backend" 16 | _token_check: false 17 | _custom_backend_view: true 18 | _backend_module: workflow 19 | -------------------------------------------------------------------------------- /src/Resources/config/templates.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | netzmacht.contao_workflow.templates.headline: 3 | "text/html": '@@NetzmachtContaoWorkflow/sections/headline.html.twig' 4 | 5 | netzmacht.contao_workflow.templates.history: 6 | "text/html": '@@NetzmachtContaoWorkflow/sections/history.html.twig' 7 | 8 | netzmacht.contao_workflow.templates.teaser: 9 | "text/html": '@@NetzmachtContaoWorkflow/sections/teaser.html.twig' 10 | 11 | netzmacht.contao_workflow.templates.step_teaser: 12 | "text/html": '@@NetzmachtContaoWorkflow/sections/step_teaser.html.twig' 13 | 14 | netzmacht.contao_workflow.templates.form: 15 | "text/html": '@@NetzmachtContaoWorkflow/sections/form.html.twig' 16 | 17 | netzmacht.contao_workflow.templates.button: 18 | "text/html": '@@NetzmachtContaoWorkflow/sections/buttons.html.twig' 19 | 20 | netzmacht.contao_workflow.templates.errors: 21 | "text/html": '@@NetzmachtContaoWorkflow/sections/errors.html.twig' 22 | 23 | netzmacht.contao_workflow.templates.html_view: 24 | unknown: '@@NetzmachtContaoWorkflow/backend/default.html.twig' 25 | step: '@@NetzmachtContaoWorkflow/backend/step.html.twig' 26 | transition: 27 | frontend: '@@NetzmachtContaoWorkflow/frontend/transition.html.twig' 28 | backend: '@@NetzmachtContaoWorkflow/backend/transition.html.twig' 29 | -------------------------------------------------------------------------------- /src/Resources/config/transitions.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | netzmacht.contao_workflow.transition_types: 3 | actions: 4 | step: true 5 | actions: true 6 | conditional: 7 | step: false 8 | actions: false 9 | workflow: 10 | step: false 11 | actions: false 12 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_member_group.php: -------------------------------------------------------------------------------- 1 | ['workflow']] 17 | ); 18 | } 19 | 20 | $GLOBALS['TL_DCA']['tl_member_group']['fields']['workflow'] = [ 21 | 'label' => &$GLOBALS['TL_LANG']['tl_member_group']['workflow'], 22 | 'inputType' => 'checkbox', 23 | 'options_callback' => ['netzmacht.contao_workflow.listeners.dca.permissions', 'getAllPermissions'], 24 | 'save_callback' => [ 25 | ['netzmacht.contao_workflow.listeners.dca.save_permission_callback', 'onSaveCallback'], 26 | ], 27 | 'eval' => [ 28 | 'tl_class' => 'clr', 29 | 'multiple' => true, 30 | ], 31 | 'sql' => 'mediumblob NULL', 32 | ]; 33 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_module.php: -------------------------------------------------------------------------------- 1 | ['name', 'type', 'headline'], 7 | 'config' => ['workflow_providers', 'workflow_detach'], 8 | 'redirect' => ['jumpTo'], 9 | 'template' => [':hide', 'customTpl'], 10 | 'protected' => [':hide', 'protected'], 11 | 'expert' => [':hide', 'guests', 'cssID', 'space'], 12 | 'invisible' => [':hide', 'invisible', 'start', 'start'], 13 | ]; 14 | 15 | $GLOBALS['TL_DCA']['tl_module']['fields']['workflow_providers'] = [ 16 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['workflow_providers'], 17 | 'inputType' => 'checkbox', 18 | 'options_callback' => ['netzmacht.contao_workflow.listeners.dca.module', 'providerOptions'], 19 | 'reference' => &$GLOBALS['TL_LANG']['MOD'], 20 | 'eval' => [ 21 | 'tl_class' => 'clr w50', 22 | 'multiple' => true, 23 | ], 24 | 'sql' => 'blob NULL', 25 | ]; 26 | 27 | $GLOBALS['TL_DCA']['tl_module']['fields']['workflow_detach'] = [ 28 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['workflow_detach'], 29 | 'inputType' => 'checkbox', 30 | 'eval' => [ 31 | 'tl_class' => 'clr w50', 32 | 'submitOnChange' => true, 33 | ], 34 | 'sql' => "char(1) NOT NULL default ''", 35 | ]; 36 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_user.php: -------------------------------------------------------------------------------- 1 | ['workflow']]); 8 | MetaPalettes::appendBefore('tl_user', 'extend', 'account', ['workflow' => ['workflow']]); 9 | MetaPalettes::appendBefore('tl_user', 'admin', 'account', ['workflow' => ['workflow']]); 10 | 11 | 12 | $GLOBALS['TL_DCA']['tl_user']['fields']['workflow'] = [ 13 | 'label' => &$GLOBALS['TL_LANG']['tl_user']['workflow'], 14 | 'inputType' => 'checkbox', 15 | 'options_callback' => ['netzmacht.contao_workflow.listeners.dca.permissions', 'getAllPermissions'], 16 | 'save_callback' => [ 17 | ['netzmacht.contao_workflow.listeners.dca.save_permission_callback', 'onSaveCallback'], 18 | ], 19 | 'eval' => [ 20 | 'tl_class' => 'clr', 21 | 'multiple' => true, 22 | ], 23 | 'sql' => 'mediumblob NULL', 24 | ]; 25 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_user_group.php: -------------------------------------------------------------------------------- 1 | ['workflow']]); 13 | } 14 | 15 | $GLOBALS['TL_DCA']['tl_user_group']['fields']['workflow'] = [ 16 | 'label' => &$GLOBALS['TL_LANG']['tl_user_group']['workflow'], 17 | 'inputType' => 'checkbox', 18 | 'options_callback' => ['netzmacht.contao_workflow.listeners.dca.permissions', 'getAllPermissions'], 19 | 'save_callback' => [ 20 | ['netzmacht.contao_workflow.listeners.dca.save_permission_callback', 'onSaveCallback'], 21 | ], 22 | 'eval' => [ 23 | 'tl_class' => 'clr', 24 | 'multiple' => true, 25 | ], 26 | 'sql' => 'mediumblob NULL', 27 | ]; 28 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_workflow_permission.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'dataContainer' => 'Table', 8 | 'sql' => [ 9 | 'keys' => [ 10 | 'id' => 'primary', 11 | 'source,source_id,permission' => 'unique', 12 | ], 13 | ], 14 | ], 15 | 'fields' => [ 16 | 'id' => ['sql' => 'int(10) unsigned NOT NULL auto_increment'], 17 | 'tstamp' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 18 | 'source' => ['sql' => "varchar(16) NOT NULL default ''"], 19 | 'source_id' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 20 | 'permission' => ['sql' => "varchar(128) NOT NULL default ''"], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_workflow_state.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'dataContainer' => 'Table', 8 | 'closed' => true, 9 | 'sql' => [ 10 | 'keys' => [ 11 | 'id' => 'primary', 12 | 'entityId' => 'index', 13 | ], 14 | ], 15 | ], 16 | 17 | 'list' => [ 18 | 'sorting' => [ 19 | 'panelLayout' => 'filter;sort,limit', 20 | 'fields' => ['entityId', 'reachedAt DESC'], 21 | 'mode' => 2, 22 | ], 23 | 'label' => [ 24 | 'fields' => ['entityId', 'workflowName', 'transitionName', 'stepName', 'success', 'reachedAt'], 25 | 'label_callback' => ['netzmacht.contao_workflow.listeners.dca.state', 'generateRow'], 26 | 'group_callback' => ['netzmacht.contao_workflow.listeners.dca.state', 'generateGroupHeader'], 27 | ], 28 | 29 | 'operations' => [ 30 | 'show' => [ 31 | 'label' => &$GLOBALS['TL_LANG']['tl_workflow_state']['show'], 32 | 'href' => 'act=show', 33 | 'icon' => 'show.gif', 34 | ], 35 | ], 36 | ], 37 | 38 | 'fields' => [ 39 | 'id' => ['sql' => 'int(10) unsigned NOT NULL auto_increment'], 40 | 'tstamp' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 41 | 'workflowName' => ['sql' => "varchar(32) NOT NULL default ''"], 42 | 'targetWorkflowName' => ['sql' => 'varchar(32) NULL default NULL'], 43 | 'entityId' => [ 44 | 'sql' => "varchar(64) NOT NULL default ''", 45 | 'sorting' => true, 46 | 'filter' => true, 47 | ], 48 | 'transitionName' => [ 49 | 'filter' => true, 50 | 'sql' => "varchar(32) NOT NULL default ''", 51 | ], 52 | 'stepName' => [ 53 | 'filter' => true, 54 | 'sql' => "varchar(32) NOT NULL default ''", 55 | ], 56 | 'success' => [ 57 | 'filter' => true, 58 | 'sql' => "char(1) NOT NULL default ''", 59 | ], 60 | 'reachedAt' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 61 | 'data' => ['sql' => 'text NULL'], 62 | 'errors' => ['sql' => 'text NULL'], 63 | ], 64 | 65 | ]; 66 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_workflow_transition_conditional_transition.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'dataContainer' => 'Table', 8 | 'sql' => [ 9 | 'keys' => [ 10 | 'id' => 'primary', 11 | 'tid,pid' => 'unique', 12 | ], 13 | ], 14 | ], 15 | 16 | 'fields' => [ 17 | 'id' => ['sql' => 'int(10) unsigned NOT NULL auto_increment'], 18 | 'tstamp' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 19 | 'sorting' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 20 | 'pid' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 21 | 'tid' => ['sql' => "int(10) unsigned NOT NULL default '0'"], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/default.php: -------------------------------------------------------------------------------- 1 | 2 |
entityId; ?>
3 |
workflowName; ?>
4 |
transitionName; ?>
5 |
stepName; ?>
6 |
success; ?>
7 |
reachedAt; ?>
8 | 9 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/be_workflow_transition_form.html5: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | fieldsets as $fieldset) : ?> 7 |
8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/modules/mod_workflow_transition.html5: -------------------------------------------------------------------------------- 1 | extend('block_unsearchable') ?> 2 | 3 | block('content') ?> 4 | 5 | view ?> 6 | 7 | endblock() ?> 8 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/workflow_errors.html5: -------------------------------------------------------------------------------- 1 | ' . $message . ''; 13 | } 14 | 15 | return sprintf( 16 | '
    %s
', 17 | $level, 18 | $list 19 | ); 20 | } 21 | ?> 22 |
23 | errors); ?> 24 |
25 | -------------------------------------------------------------------------------- /src/Resources/public/css/backend.css: -------------------------------------------------------------------------------- 1 | #tl_navigation .group-workflow { 2 | background: url('../img/workflow-module.svg') 3px 0 no-repeat; 3 | background-size: 14px; 4 | } 5 | 6 | .icon_selector .selector_container img { 7 | max-width: 20px; 8 | height: auto; 9 | padding: 5px; 10 | } 11 | 12 | .icon_selector .selector_container ul { 13 | float: left; 14 | } 15 | 16 | .icon_selector .selector_container p { 17 | float: left; 18 | margin-top: 4px; 19 | } 20 | 21 | .icon_selector .tl_help { 22 | clear: both; 23 | } 24 | 25 | .workflow_state.row { 26 | clear: both; 27 | max-width: 600px; 28 | } 29 | 30 | .workflow_state.row div { 31 | float: left; 32 | padding: 4px; 33 | width: 16.5%; 34 | box-sizing: border-box; 35 | } 36 | 37 | .workflow-section + .workflow-section { 38 | margin-top: 2em; 39 | border-top: 1px solid #e6e6e8; 40 | } 41 | 42 | .workflow-content-box { 43 | margin: 20px 15px 16px; 44 | } 45 | 46 | .workflow-history { 47 | width: 100%; 48 | box-sizing: border-box; 49 | } 50 | 51 | .workflow-history th { 52 | padding: 6px; 53 | background: #f9f9fb; 54 | border-top: 1px solid #ddd; 55 | border-bottom: 1px solid #ddd; 56 | } 57 | 58 | .workflow-history-state td { 59 | padding: 6px; 60 | border-bottom: 1px solid #e9e9e9; 61 | } 62 | 63 | .workflow-history-state.transition-failed td { 64 | color: #bbb; 65 | } 66 | 67 | .workflow-submit-container { 68 | padding: 8px 15px; 69 | } 70 | 71 | .workflow-error-list { 72 | margin: 0 0 1px; 73 | padding: 12px 18px 6px 24px; 74 | line-height: 1.2; 75 | color: #c33; 76 | background: #faebeb; 77 | } 78 | 79 | .workflow-error-list .workflow-error-list { 80 | padding-top: 0px; 81 | } 82 | 83 | .workflow-error-list li { 84 | list-style-type: square; 85 | } 86 | 87 | .workflow-definition-error { 88 | color: #c33; 89 | background: url(../../../system/themes/flexible/icons/error.svg) no-repeat 5px 4px #faebeb; 90 | padding: 5px 5px 5px 25px; 91 | margin: 0; 92 | } 93 | 94 | .workflow-transition-form { 95 | display: inline-block; 96 | } 97 | 98 | .edit_conditional_transition_column { 99 | width: 16px; 100 | } 101 | 102 | .edit-all-transitions { 103 | margin-top: 10px; 104 | } 105 | 106 | #ctrl_conditionalTransitions thead th:nth-child(2) { 107 | display: none; 108 | } 109 | -------------------------------------------------------------------------------- /src/Resources/public/img/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netzmacht/contao-workflow/43c70801eb16fb96c81fc121291d46a4a8e0e121/src/Resources/public/img/action.png -------------------------------------------------------------------------------- /src/Resources/public/img/action_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netzmacht/contao-workflow/43c70801eb16fb96c81fc121291d46a4a8e0e121/src/Resources/public/img/action_.png -------------------------------------------------------------------------------- /src/Resources/public/img/role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netzmacht/contao-workflow/43c70801eb16fb96c81fc121291d46a4a8e0e121/src/Resources/public/img/role.png -------------------------------------------------------------------------------- /src/Resources/public/img/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netzmacht/contao-workflow/43c70801eb16fb96c81fc121291d46a4a8e0e121/src/Resources/public/img/step.png -------------------------------------------------------------------------------- /src/Resources/public/img/transition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netzmacht/contao-workflow/43c70801eb16fb96c81fc121291d46a4a8e0e121/src/Resources/public/img/transition.png -------------------------------------------------------------------------------- /src/Resources/public/img/workflow-module.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Resources/public/img/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netzmacht/contao-workflow/43c70801eb16fb96c81fc121291d46a4a8e0e121/src/Resources/public/img/workflow.png -------------------------------------------------------------------------------- /src/Resources/public/js/backend.js: -------------------------------------------------------------------------------- 1 | 2 | function workflowConditionalTransitionsUpdateEditButton(button) 3 | { 4 | if (button) { 5 | button = $(button); 6 | var image = button.getElement('img'); 7 | var select = button.getParent('tr').getElement('select'); 8 | var moduleID = select.value; 9 | var label = select.options[select.selectedIndex].innerHTML; 10 | 11 | if (/^\d+$/.exec(moduleID)) { 12 | image.src = image.src.replace('edit_.svg', 'edit.svg'); 13 | button.moduleID = moduleID; 14 | button.moduleTitle = label; 15 | button.setStyle('cursor', ''); 16 | } else { 17 | image.src = image.src.replace('edit.svg', 'edit_.svg'); 18 | button.moduleID = null; 19 | button.moduleTitle = null; 20 | button.setStyle('cursor', 'default'); 21 | } 22 | } 23 | } 24 | 25 | function workflowConditionalTransitionsButtonClick() 26 | { 27 | if (this.moduleID) { 28 | var rt = /[&\?](rt=[^&]+)/.exec(document.location.search); 29 | var href = 'contao?do=workflows&table=tl_workflow_transition&act=edit&id=' + this.moduleID 30 | + '&popup=1&nb=1' 31 | + (rt ? '&' + rt[1] : ''); 32 | 33 | Backend.openModalIframe( 34 | { 35 | 'title': this.moduleTitle, 36 | 'url': href 37 | } 38 | ); 39 | } 40 | } 41 | 42 | $(window).addEvent('domready', function () { 43 | MultiColumnWizard.addOperationUpdateCallback('new', function (el, row) { 44 | var button = $(row).getElement('a.edit_transition'); 45 | if (button === null) { 46 | return; 47 | } 48 | workflowConditionalTransitionsUpdateEditButton(button); 49 | button.addEvent('click', workflowConditionalTransitionsButtonClick); 50 | $(row).getElement('select').addEvent('change', function () { 51 | workflowConditionalTransitionsUpdateEditButton($(this).getParent('tr').getElement('a.edit_transition')); 52 | }); 53 | }); 54 | 55 | $$('#ctrl_conditionalTransitions select').addEvent('change', function () { 56 | var button = $(this).getParent('tr').getElement('a.edit_transition'); 57 | if (button !== null) { 58 | workflowConditionalTransitionsUpdateEditButton(button); 59 | } 60 | }); 61 | $$('#ctrl_conditionalTransitions a.edit_transition').each(function (button) { 62 | workflowConditionalTransitionsUpdateEditButton(button); 63 | button.addEvent('click', workflowConditionalTransitionsButtonClick); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/Resources/views/backend/backend.html.twig: -------------------------------------------------------------------------------- 1 | {{ render_contao_backend_template({ 2 | headline: block('headline'), 3 | main: block('main'), 4 | error: block('error') 5 | }) | raw }} 6 | -------------------------------------------------------------------------------- /src/Resources/views/backend/default.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@NetzmachtContaoWorkflow/backend/backend.html.twig" %} 2 | 3 | {% block headline %} 4 | {% include sections.template('headline') with sections.get('headline') only %} 5 | {% endblock %} 6 | 7 | {%- block error -%} 8 | {%- endblock -%} 9 | 10 | {% block main %} 11 |
12 | Zurück 13 |
14 | 15 | {% for name, section in sections %} 16 | {% set content = include(sections.template(name), section )|trim %} 17 | {% if content|length %} 18 |
19 | {{ content|raw }} 20 |
21 | {% endif %} 22 | {% endfor %} 23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /src/Resources/views/backend/step.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@NetzmachtContaoWorkflow/backend/backend.html.twig" %} 2 | 3 | {% block headline %} 4 | {% include sections.template('headline') with sections.get('headline') only %} 5 | {% endblock %} 6 | 7 | {%- block error -%} 8 | 9 | {%- endblock -%} 10 | 11 | {% block main %} 12 | 15 | 16 | {% for name, section in sections %} 17 | {% set content = include(sections.template(name), section)|trim %} 18 | {% if content|length %} 19 |
20 | {{ content|raw }} 21 |
22 | {% endif %} 23 | {% endfor %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /src/Resources/views/backend/transition.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@NetzmachtContaoWorkflow/backend/backend.html.twig" %} 2 | 3 | {% block headline %} 4 | {% include sections.template('headline') with sections.get('headline') only %} 5 | {% endblock %} 6 | 7 | {%- block error -%} 8 | {%- endblock -%} 9 | 10 | {% block main %} 11 |
12 | Zurück 13 |
14 | 15 | {% for name, section in sections %} 16 | {% set content = include(sections.template(name), section )|trim %} 17 | {% if content|length %} 18 |
19 | {{ content|raw }} 20 |
21 | {% endif %} 22 | {% endfor %} 23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /src/Resources/views/frontend/transition.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% include sections.template('headline') with sections.get('headline') only %} 3 | 4 | {% for name, section in sections %} 5 |
6 | {% include sections.template(name) with section only %} 7 |
8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /src/Resources/views/sections/buttons.html.twig: -------------------------------------------------------------------------------- 1 | {% if actions|length %} 2 |
3 | {% for action in actions %} 4 |
5 | 11 |
12 | {% endfor %} 13 |
14 | {% endif %} 15 | -------------------------------------------------------------------------------- /src/Resources/views/sections/errors.html.twig: -------------------------------------------------------------------------------- 1 | {% trans_default_domain 'netzmacht_workflow' %} 2 | 3 | {% macro errorList(errors) %} 4 | {% if errors is not null %} 5 |
    6 | {% for error in errors %} 7 |
  • {{ error.0|trans(error.1) }} 8 | {{ _self.errorList(error.2) }} 9 |
  • 10 | {% endfor %} 11 |
12 | {% endif %} 13 | {% endmacro %} 14 | 15 |
16 | {{ _self.errorList(errors) }} 17 |
18 | -------------------------------------------------------------------------------- /src/Resources/views/sections/form.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% form_theme form '@NetzmachtContaoForm/form/contao_backend.html.twig' %} 3 | {{ form(form) }} 4 | -------------------------------------------------------------------------------- /src/Resources/views/sections/headline.html.twig: -------------------------------------------------------------------------------- 1 | {{ headline|join(' › ') }} 2 | -------------------------------------------------------------------------------- /src/Resources/views/sections/history.html.twig: -------------------------------------------------------------------------------- 1 | {% trans_default_domain 'netzmacht_workflow' %} 2 | 3 |

{{ "history"|trans }}

4 | 5 |
6 | 7 | 8 | 9 | {% for column in columns %} 10 | 11 | {% endfor %} 12 | 13 | 14 | {% for state in history %} 15 | 16 | {% for name, value in state %} 17 | 24 | {% endfor %} 25 | 26 | {% endfor %} 27 |
{{ ('history.' ~ column)|trans }}
18 | {% if block(name) is defined %} 19 | {{ block(name) }} 20 | {% else %} 21 | {{ value }} 22 | {% endif %} 23 |
28 |
29 | 30 | {% block user %} 31 | {% if value is defined %} 32 | {% if value is iterable %} 33 | {{ value.name }} [{{ value.username }}] 34 | {% else %} 35 | {{ value }} 36 | {% endif %} 37 | {% endif %} 38 | {% endblock %} 39 | 40 | {% block target %} 41 | {% if value is defined %} 42 | {{ value.step }} [{{ value.workflow }}] 43 | {% endif %} 44 | {% endblock %} 45 | 46 | {% block successful %} 47 | {% if value is defined %}{{ value.label }}{% endif %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /src/Resources/views/sections/teaser.html.twig: -------------------------------------------------------------------------------- 1 | {% trans_default_domain 'netzmacht_workflow' %} 2 | 3 |

{{ headline }}

4 | 5 | {% if description %} 6 |
7 |

{{ description }}

8 |
9 | {% endif %} 10 | 11 |
12 |
13 | 14 | 15 | {% if entityLabel %} 16 | 17 | 18 | 19 | 20 | {% endif %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% if item.latestState %} 30 | 31 | 32 | 33 | 34 | {% endif %} 35 | 36 |
{{ 'dataRecord'|trans }}{{ entityLabel }}
{{ 'workflowName'|trans }}{{ workflow.label }}
{{ 'currentStep'|trans }}{{ currentStep != null ? currentStep.label : '-' }}
{{ 'reachedAt'|trans }}{{ item.latestState.reachedAt.format('d.m.Y H:i') }}
37 |
38 |
39 | -------------------------------------------------------------------------------- /src/Security/AbstractPermissionVoter.php: -------------------------------------------------------------------------------- 1 | user = $user; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | protected function supports($attribute, $subject): bool 36 | { 37 | $subjectClass = $this->getSubjectClass(); 38 | 39 | if (! $subject instanceof $subjectClass) { 40 | return false; 41 | } 42 | 43 | if ($attribute instanceof Permission) { 44 | $permission = $attribute; 45 | } else { 46 | try { 47 | $permission = Permission::fromString($attribute); 48 | } catch (Throwable $e) { 49 | return false; 50 | } 51 | } 52 | 53 | return $subject->hasPermission($permission); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | * 59 | * @psalm-suppress MissingParamType 60 | */ 61 | protected function voteOnAttribute($attribute, $subject, Token $token) 62 | { 63 | /** @psalm-suppress RedundantConditionGivenDocblockType - TODO: Do we need to fix the attribute type */ 64 | if (! $attribute instanceof Permission) { 65 | return false; 66 | } 67 | 68 | return $this->user->hasPermission($attribute); 69 | } 70 | 71 | /** 72 | * Get the expected subject class. 73 | */ 74 | abstract protected function getSubjectClass(): string; 75 | } 76 | -------------------------------------------------------------------------------- /src/Security/StepPermissionVoter.php: -------------------------------------------------------------------------------- 1 | user = $user; 37 | $this->workflowManager = $workflowManager; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | protected function supports(string $attribute, $subject): bool 44 | { 45 | if (! WorkflowPermissions::isTransitTransition($attribute)) { 46 | return false; 47 | } 48 | 49 | return $subject instanceof Item; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | * 55 | * @SuppressWarnings(PHPMD.UnusedLocalVariable) 56 | */ 57 | protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token) 58 | { 59 | assert($subject instanceof Item); 60 | 61 | $transition = WorkflowPermissions::extractTransitionName($attribute); 62 | 63 | try { 64 | $workflow = $this->workflowManager->getWorkflowByItem($subject); 65 | $transition = $workflow->getTransition($transition); 66 | } catch (WorkflowException | FlowException $exception) { 67 | return false; 68 | } 69 | 70 | $permission = $transition->getPermission(); 71 | if ($permission === null) { 72 | return true; 73 | } 74 | 75 | $user = $token->getUser(); 76 | if (! $user instanceof UserInterface) { 77 | $user = null; 78 | } 79 | 80 | return $this->user->hasPermission($permission, $user); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Security/User.php: -------------------------------------------------------------------------------- 1 | getName(); 23 | } 24 | 25 | public static function accessStep(Step $step): string 26 | { 27 | return self::ACCESS_STEP_PREFIX . $step->getName(); 28 | } 29 | 30 | public static function isTransitTransition(string $attribute): bool 31 | { 32 | return strpos($attribute, self::TRANSIT_TRANSITION_PREFIX) === 0; 33 | } 34 | 35 | public static function isAccessStep(string $attribute): bool 36 | { 37 | return strpos($attribute, self::ACCESS_STEP_PREFIX) === 0; 38 | } 39 | 40 | public static function extractStepName(string $attribute): string 41 | { 42 | if (! self::isAccessStep($attribute)) { 43 | throw new RuntimeException('Invalid step permission: ' . $attribute); 44 | } 45 | 46 | return substr($attribute, strlen(self::ACCESS_STEP_PREFIX)); 47 | } 48 | 49 | public static function extractTransitionName(string $attribute): string 50 | { 51 | if (! self::isTransitTransition($attribute)) { 52 | throw new RuntimeException('Invalid transition permission: ' . $attribute); 53 | } 54 | 55 | return substr($attribute, strlen(self::TRANSIT_TRANSITION_PREFIX)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Workflow/Definition/Definition.php: -------------------------------------------------------------------------------- 1 | workflow = $workflow; 42 | $this->step = $step; 43 | } 44 | 45 | /** 46 | * Get the step. 47 | */ 48 | public function getStep(): Step 49 | { 50 | return $this->step; 51 | } 52 | 53 | /** 54 | * Get the workflow. 55 | */ 56 | public function getWorkflow(): Workflow 57 | { 58 | return $this->workflow; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Workflow/Definition/Event/CreateTransitionEvent.php: -------------------------------------------------------------------------------- 1 | transition = $transition; 32 | } 33 | 34 | /** 35 | * Get the transition. 36 | */ 37 | public function getTransition(): Transition 38 | { 39 | return $this->transition; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Workflow/Definition/Event/CreateWorkflowEvent.php: -------------------------------------------------------------------------------- 1 | workflow = $workflow; 32 | } 33 | 34 | /** 35 | * Get the workflow. 36 | */ 37 | public function getWorkflow(): Workflow 38 | { 39 | return $this->workflow; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Workflow/Definition/Exception/DefinitionException.php: -------------------------------------------------------------------------------- 1 | repositoryManager = $repositoryManager; 51 | $this->modelAdapter = $modelAdapter; 52 | $this->changeTracker = $changeTracker; 53 | } 54 | 55 | public function supports(string $providerName): bool 56 | { 57 | $modelClass = $this->modelAdapter->getClassFromTable($providerName); 58 | 59 | return $modelClass && class_exists($modelClass) && is_a($modelClass, Model::class, true); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | * 65 | * @throws UnsupportedEntity When Entity type is not supported. 66 | */ 67 | public function create(string $providerName): EntityRepository 68 | { 69 | $modelClass = $this->modelAdapter->getClassFromTable($providerName); 70 | if (! $modelClass) { 71 | throw UnsupportedEntity::withProviderName($providerName); 72 | } 73 | 74 | $repository = $this->repositoryManager->getRepository($modelClass); 75 | 76 | return new ContaoModelEntityRepository($repository, $this->changeTracker, $this->repositoryManager); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Workflow/Entity/ContaoModel/ContaoModelRelatedModelChangeTracker.php: -------------------------------------------------------------------------------- 1 | >> 21 | */ 22 | private $changes = []; 23 | 24 | /** 25 | * Track a change of a related model. 26 | * 27 | * @param Model $baseModel The base model which is the base model being used then changing a related 28 | * model. 29 | * @param Model $changedRelatedModel The model being changed. 30 | */ 31 | public function track(Model $baseModel, Model $changedRelatedModel): void 32 | { 33 | $this->changes[$baseModel::getTable()][$baseModel->id][] = $changedRelatedModel; 34 | } 35 | 36 | /** 37 | * Release the tracked changes of a model. 38 | * 39 | * @param Model $model The base model. 40 | * 41 | * @return Model[] 42 | */ 43 | public function release(Model $model): array 44 | { 45 | $models = ($this->changes[$model::getTable()][$model->id] ?? []); 46 | unset($this->changes[$model::getTable()][$model->id]); 47 | 48 | return array_unique($models); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Workflow/Entity/ContaoModel/ContaoModelSpecificationAwareSpecification.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 27 | } 28 | 29 | public function supports(string $providerName): bool 30 | { 31 | return $this->connection->getSchemaManager()->tablesExist([$providerName]); 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | * 37 | * @throws UnsupportedEntity When Entity type is not supported. 38 | */ 39 | public function create(string $providerName): EntityRepository 40 | { 41 | if (! $this->supports($providerName)) { 42 | throw UnsupportedEntity::withProviderName($providerName); 43 | } 44 | 45 | return new DatabaseEntityRepository($this->connection, $providerName); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Workflow/Entity/DelegatingRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | factories = $factories; 25 | } 26 | 27 | public function supports(string $providerName): bool 28 | { 29 | foreach ($this->factories as $factory) { 30 | if ($factory->supports($providerName)) { 31 | return true; 32 | } 33 | } 34 | 35 | return false; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | * 41 | * @throws UnsupportedEntity When Entity type is not supported. 42 | */ 43 | public function create(string $providerName): EntityRepository 44 | { 45 | foreach ($this->factories as $factory) { 46 | if ($factory->supports($providerName)) { 47 | return $factory->create($providerName); 48 | } 49 | } 50 | 51 | throw UnsupportedEntity::withProviderName($providerName); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Workflow/Entity/EntityManager.php: -------------------------------------------------------------------------------- 1 | repositoryFactory = $repositoryFactory; 49 | $this->connection = $connection; 50 | } 51 | 52 | public function getRepository(string $providerName): EntityRepository 53 | { 54 | if (isset($this->repositories[$providerName])) { 55 | return $this->repositories[$providerName]; 56 | } 57 | 58 | $this->repositories[$providerName] = $this->repositoryFactory->create($providerName); 59 | 60 | return $this->repositories[$providerName]; 61 | } 62 | 63 | public function begin(): void 64 | { 65 | $this->connection->beginTransaction(); 66 | } 67 | 68 | public function commit(): void 69 | { 70 | $this->connection->commit(); 71 | } 72 | 73 | public function rollback(): void 74 | { 75 | $this->connection->rollBack(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Workflow/Entity/RepositoryFactory.php: -------------------------------------------------------------------------------- 1 | $config Configuration values. 28 | */ 29 | public function __construct( 30 | PropertyAccessManager $propertyAccessManager, 31 | string $name, 32 | string $label = '', 33 | array $config = [] 34 | ) { 35 | parent::__construct($name, $label, $config); 36 | 37 | $this->propertyAccessManager = $propertyAccessManager; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/ActionTypeFactory.php: -------------------------------------------------------------------------------- 1 | $config Action config. 39 | * @param Transition $transition Transition to which the action belongs. 40 | */ 41 | public function create(array $config, Transition $transition): Action; 42 | } 43 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/AssignUser/AssignUserActionFormBuilder.php: -------------------------------------------------------------------------------- 1 | userChoices = $userChoices; 36 | } 37 | 38 | public function supports(Action $action): bool 39 | { 40 | return $action instanceof AssignUserAction && ! $action->isCurrentUserAssigned(); 41 | } 42 | 43 | public function buildForm(Action $action, Transition $transition, FormBuilder $formBuilder): void 44 | { 45 | assert($action instanceof AssignUserAction); 46 | 47 | $permission = $action->getConfigValue('assign_user_permission'); 48 | if ($permission) { 49 | $choices = $this->userChoices->fetchByPermission(Permission::fromString($permission)); 50 | } else { 51 | $choices = $this->userChoices->findAll(); 52 | } 53 | 54 | $formBuilder->add( 55 | 'action_' . $action->getConfigValue('id'), 56 | FieldsetType::class, 57 | [ 58 | 'legend' => $action->getLabel(), 59 | 'fields' => [ 60 | [ 61 | 'name' => $action->getName() . '_user', 62 | 'type' => ChoiceType::class, 63 | 'attr' => [ 64 | 'label' => $action->getConfigValue('description'), 65 | 'choices' => $choices, 66 | ], 67 | ], 68 | ], 69 | ] 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/ChangeLogging.php: -------------------------------------------------------------------------------- 1 | logChanges; 28 | } 29 | 30 | /** 31 | * Enable logging. 32 | */ 33 | public function enableLogging(): void 34 | { 35 | $this->logChanges = true; 36 | } 37 | 38 | /** 39 | * Disable logging. 40 | */ 41 | public function disableLogging(): void 42 | { 43 | $this->logChanges = false; 44 | } 45 | 46 | /** 47 | * Log changes if enabled. 48 | * 49 | * @param string $property Property name. 50 | * @param mixed $value Property value. 51 | * @param Context $context Transition context. 52 | */ 53 | protected function propertyChanged(string $property, $value, Context $context): void 54 | { 55 | if (! ($this instanceof Base) || ! $this->isLoggingEnabled()) { 56 | return; 57 | } 58 | 59 | $context->getProperties()->set($this->getName() . '_' . $property, $value); 60 | } 61 | 62 | /** 63 | * Log multiple changes. 64 | * 65 | * @param array $values Changes properties as associated array['name' => 'val']. 66 | * @param Context $context Transition context. 67 | */ 68 | protected function propertiesChanged(array $values, Context $context): void 69 | { 70 | if (! ($this instanceof Base) || ! $this->isLoggingEnabled()) { 71 | return; 72 | } 73 | 74 | $properties = $context->getProperties(); 75 | 76 | foreach ($values as $name => $value) { 77 | $properties->set($this->getName() . '_' . $name, $value); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/Form/FormAction.php: -------------------------------------------------------------------------------- 1 | getName()]; 20 | } 21 | 22 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 23 | public function validate(Item $item, Context $context): bool 24 | { 25 | return true; 26 | } 27 | 28 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 29 | public function transit(Transition $transition, Item $item, Context $context): void 30 | { 31 | $context->getProperties()->set($this->getName(), $context->getPayload()->get($this->getName())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/Form/FormActionFactory.php: -------------------------------------------------------------------------------- 1 | getConfigValue('form_fieldset')) { 34 | $formBuilder->add( 35 | $action->getName(), 36 | FormGeneratorType::class, 37 | [ 38 | 'formId' => $action->getConfigValue('form_formId'), 39 | 'ignore' => ['submit'], 40 | ] 41 | ); 42 | 43 | return; 44 | } 45 | 46 | $formBuilder->add( 47 | 'action_' . $action->getConfigValue('id') . '_fieldset', 48 | FieldsetType::class, 49 | [ 50 | 'legend' => $action->getLabel(), 51 | 'fields' => [ 52 | [ 53 | 'name' => 'action_' . $action->getConfigValue('id'), 54 | 'type' => FormGeneratorType::class, 55 | 'attr' => [ 56 | 'formId' => $action->getConfigValue('form_formId'), 57 | 'ignore' => ['submit'], 58 | ], 59 | ], 60 | ], 61 | ] 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/Metadata/MetadataAction.php: -------------------------------------------------------------------------------- 1 | user = $user; 37 | $this->requestStack = $requestStack; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function getRequiredPayloadProperties(Item $item): array 44 | { 45 | return []; 46 | } 47 | 48 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 49 | public function validate(Item $item, Context $context): bool 50 | { 51 | return true; 52 | } 53 | 54 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 55 | public function transit(Transition $transition, Item $item, Context $context): void 56 | { 57 | $userId = $this->user->getUserId(); 58 | $metadata = [ 59 | 'scope' => null, 60 | 'userId' => $userId ? (string) $userId : null, 61 | ]; 62 | 63 | $request = $this->requestStack->getCurrentRequest(); 64 | if ($request) { 65 | $metadata['scope'] = $request->attributes->get('_scope'); 66 | } 67 | 68 | $context->getProperties()->set('metadata', $metadata); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/Note/NoteActionFactory.php: -------------------------------------------------------------------------------- 1 | required()) { 39 | $constraints[] = new NotBlank(); 40 | 41 | if ($action->minLength() > 0) { 42 | $constraints[] = new Length(['min' => $action->minLength()]); 43 | $attributes['minlength'] = $action->minLength(); 44 | } 45 | } 46 | 47 | $formBuilder->add( 48 | 'action_' . $action->getConfigValue('id'), 49 | FieldsetType::class, 50 | [ 51 | 'legend' => $action->getLabel(), 52 | 'fields' => [ 53 | [ 54 | 'name' => $action->payloadName(), 55 | 'type' => TextareaType::class, 56 | 'attr' => [ 57 | 'constraints' => $constraints, 58 | 'label' => $action->getConfigValue('description'), 59 | 'required' => $action->required(), 60 | 'attr' => $attributes, 61 | ], 62 | ], 63 | ], 64 | ] 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/UpdateEntityAction/UpdateEntityAction.php: -------------------------------------------------------------------------------- 1 | getName()]; 26 | } 27 | 28 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 29 | public function validate(Item $item, Context $context): bool 30 | { 31 | $payload = $context->getPayload(); 32 | if (! $payload->has($this->getName())) { 33 | return false; 34 | } 35 | 36 | return is_array($payload->get($this->getName())); 37 | } 38 | 39 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 40 | public function transit(Transition $transition, Item $item, Context $context): void 41 | { 42 | $payload = $context->getPayload(); 43 | $name = $this->getName(); 44 | assert($payload->has($name)); 45 | 46 | $data = $payload->get($name); 47 | assert(is_array($data)); 48 | 49 | $accessor = $this->propertyAccessManager->provideAccess($item->getEntity()); 50 | foreach ($data as $key => $value) { 51 | $accessor->set($key, $value); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/UpdateEntityAction/UpdateEntityActionFactory.php: -------------------------------------------------------------------------------- 1 | dcaManager = $dcaManager; 42 | $this->propertyAccessManager = $propertyAccessManager; 43 | } 44 | 45 | public function getCategory(): string 46 | { 47 | return 'default'; 48 | } 49 | 50 | public function getName(): string 51 | { 52 | return 'update_entity'; 53 | } 54 | 55 | public function isPostAction(): bool 56 | { 57 | return false; 58 | } 59 | 60 | public function supports(Workflow $workflow): bool 61 | { 62 | try { 63 | $definition = $this->dcaManager->getDefinition($workflow->getProviderName()); 64 | 65 | return (bool) $definition->get(['config', 'useRawRequestData']); 66 | } catch (AssertionFailed $exception) { 67 | return false; 68 | } 69 | } 70 | 71 | /** 72 | * {@inheritDoc} 73 | */ 74 | public function create(array $config, Transition $transition): Action 75 | { 76 | return new UpdateEntityAction( 77 | $this->propertyAccessManager, 78 | 'action_' . $config['id'], 79 | $config['label'], 80 | $config 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Action/WorkflowChange/WorkflowChangeAction.php: -------------------------------------------------------------------------------- 1 | workflowManager = $workflowManager; 39 | $this->workflowName = $workflowName; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getRequiredPayloadProperties(Item $item): array 46 | { 47 | return $this->getStartTransition()->getRequiredPayloadProperties($item); 48 | } 49 | 50 | public function validate(Item $item, Context $context): bool 51 | { 52 | $startTransition = $this->getStartTransition(); 53 | 54 | if (! $startTransition->isAllowed($item, $context)) { 55 | return false; 56 | } 57 | 58 | return $this->getStartTransition()->validate($item, $context); 59 | } 60 | 61 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 62 | public function transit(Transition $transition, Item $item, Context $context): void 63 | { 64 | $this->getStartTransition()->execute($item, $context); 65 | } 66 | 67 | /** 68 | * Get the start transition of the assigned workflow. 69 | */ 70 | public function getStartTransition(): Transition 71 | { 72 | return $this->workflowManager->getWorkflowByName($this->workflowName)->getStartTransition(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Condition/Transition/AssignedUserCondition.php: -------------------------------------------------------------------------------- 1 | propertyAccessManager = $propertyAccessManager; 47 | $this->user = $user; 48 | $this->property = $property; 49 | } 50 | 51 | /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ 52 | public function match(Transition $transition, Item $item, Context $context): bool 53 | { 54 | $entity = $item->getEntity(); 55 | if (! $this->propertyAccessManager->supports($entity)) { 56 | return false; 57 | } 58 | 59 | $accessor = $this->propertyAccessManager->provideAccess($entity); 60 | $assignedUser = $accessor->get($this->property); 61 | if ($assignedUser === null) { 62 | return false; 63 | } 64 | 65 | $userId = $this->user->getUserId(); 66 | if ($userId === null) { 67 | return false; 68 | } 69 | 70 | return EntityId::fromString($assignedUser)->equals($userId); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Condition/Transition/ExpressionCondition.php: -------------------------------------------------------------------------------- 1 | expressionLanguage = $expressionLanguage; 41 | $this->expression = $expression; 42 | } 43 | 44 | /** 45 | * Get expression. 46 | */ 47 | public function getExpression(): string 48 | { 49 | return $this->expression; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | * 55 | * @SuppressWarnings(PHPMD.UnusedLocalVariable) 56 | * @SuppressWarnings(PHPMD.EvalExpression) 57 | */ 58 | public function match(Transition $transition, Item $item, Context $context): bool 59 | { 60 | $values = [ 61 | 'transition' => $transition, 62 | 'item' => $item, 63 | 'context' => $context, 64 | 'entity' => $item->getEntity(), 65 | 'entityId' => $item->getEntityId(), 66 | ]; 67 | 68 | if ($this->expressionLanguage->evaluate($this->expression, $values)) { 69 | return true; 70 | } 71 | 72 | $context->addError('transition.condition.expression.failed', ['%expression%' => $this->expression]); 73 | 74 | return false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Condition/Transition/TransitionPermissionCondition.php: -------------------------------------------------------------------------------- 1 | authorizationChecker = $authorizationChecker; 40 | $this->grantAccessByDefault = $grantAccessByDefault; 41 | } 42 | 43 | public function match(Transition $transition, Item $item, Context $context): bool 44 | { 45 | $permission = $transition->getPermission(); 46 | 47 | if (! $this->grantAccessByDefault && $permission === null) { 48 | return false; 49 | } 50 | 51 | try { 52 | if ($this->authorizationChecker->isGranted(WorkflowPermissions::transitTransition($transition), $item)) { 53 | return true; 54 | } 55 | } catch (AuthenticationCredentialsNotFoundException $exception) { 56 | if ($this->grantAccessByDefault) { 57 | return true; 58 | } 59 | } 60 | 61 | $context->addError( 62 | 'transition.condition.transition_permission.failed', 63 | [ 64 | 'transition' => $transition->getLabel(), 65 | 'permission' => (string) $permission, 66 | ] 67 | ); 68 | 69 | return false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Condition/Workflow/PropertyCondition.php: -------------------------------------------------------------------------------- 1 | property = $property; 61 | $this->value = $value; 62 | $this->operator = $operator; 63 | $this->propertyAccessManager = $propertyAccessManager; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function match(Workflow $workflow, EntityId $entityId, $entity): bool 70 | { 71 | if (! $this->propertyAccessManager->supports($entity)) { 72 | return false; 73 | } 74 | 75 | $value = $this->propertyAccessManager->provideAccess($entity)->get($this->property); 76 | 77 | return Comparison::compare($value, $this->value, $this->operator); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Workflow/Flow/Condition/Workflow/TypeCondition.php: -------------------------------------------------------------------------------- 1 | type = $type; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function match(Workflow $workflow, EntityId $entityId, $entity): bool 36 | { 37 | return $this->type->match((string) $workflow->getConfigValue('type')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Workflow/Type/AbstractWorkflowType.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | $this->supportedProviders = $supportedProviders; 37 | } 38 | 39 | public function getName(): string 40 | { 41 | return $this->name; 42 | } 43 | 44 | public function match(string $typeName): bool 45 | { 46 | return $this->getName() === $typeName; 47 | } 48 | 49 | public function configure(Workflow $workflow, callable $next): void 50 | { 51 | $workflow->setConfigValue(WorkflowType::class, $this); 52 | $workflow->addCondition(new TypeCondition($this)); 53 | $next(); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function getProviderNames(): array 60 | { 61 | return $this->supportedProviders; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Workflow/Type/WorkflowType.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | public function getProviderNames(): array; 38 | } 39 | -------------------------------------------------------------------------------- /src/Workflow/Type/WorkflowTypeNotFound.php: -------------------------------------------------------------------------------- 1 | types = $types; 22 | } 23 | 24 | /** 25 | * Get all workflow type names. 26 | * 27 | * @return string[] 28 | */ 29 | public function getTypeNames(): array 30 | { 31 | $names = []; 32 | 33 | foreach ($this->types as $workflowType) { 34 | $names[] = $workflowType->getName(); 35 | } 36 | 37 | return $names; 38 | } 39 | 40 | /** 41 | * Get a workflow type by name. 42 | * 43 | * @param string $typeName Name of the workflow type. 44 | * 45 | * @throws WorkflowTypeNotFound When workflow type is not registered. 46 | */ 47 | public function getType(string $typeName): WorkflowType 48 | { 49 | foreach ($this->types as $workflowType) { 50 | if ($workflowType->getName() === $typeName) { 51 | return $workflowType; 52 | } 53 | } 54 | 55 | throw WorkflowTypeNotFound::withName($typeName); 56 | } 57 | 58 | /** 59 | * Check if a type is know. 60 | * 61 | * @param string $typeName Workflow type name. 62 | */ 63 | public function hasType(string $typeName): bool 64 | { 65 | foreach ($this->types as $workflowType) { 66 | if ($workflowType->getName() === $typeName) { 67 | return true; 68 | } 69 | } 70 | 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Workflow/View/Factory/DelegatingViewFactory.php: -------------------------------------------------------------------------------- 1 | factories = $factories; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | * 32 | * @throws UnsupportedViewContentType When no factory is registered for the content type. 33 | */ 34 | public function create( 35 | Item $item, 36 | $context, 37 | array $options = [], 38 | ?string $template = null, 39 | string $contentType = View::CONTENT_TYPE_HTML 40 | ): View { 41 | if (! isset($this->factories[$contentType])) { 42 | throw new UnsupportedViewContentType(); 43 | } 44 | 45 | return $this->factories[$contentType]->create($item, $context, $options, $template, $contentType); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Workflow/View/Renderer.php: -------------------------------------------------------------------------------- 1 | getContext() instanceof Step; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Workflow/View/Renderer/AbstractTransitionRenderer.php: -------------------------------------------------------------------------------- 1 | getContext() instanceof Transition; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Workflow/View/Renderer/DelegatingRenderer.php: -------------------------------------------------------------------------------- 1 | renderer[] = $rendererInstance; 28 | } 29 | } 30 | 31 | public function supports(View $view): bool 32 | { 33 | return count($this->renderer) > 0; 34 | } 35 | 36 | public function render(View $view): void 37 | { 38 | foreach ($this->renderer as $renderer) { 39 | if (! $renderer->supports($view)) { 40 | continue; 41 | } 42 | 43 | $renderer->render($view); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Workflow/View/Renderer/ErrorRenderer.php: -------------------------------------------------------------------------------- 1 | $view->getOption('errors'), 26 | ]; 27 | } 28 | 29 | public function supports(View $view): bool 30 | { 31 | $errors = $view->getOption('errors'); 32 | 33 | return $errors instanceof ErrorCollection && $errors->countErrors() > 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Workflow/View/Renderer/FormRenderer.php: -------------------------------------------------------------------------------- 1 | getContext() instanceof Transition && $view->getOption('form') instanceof Form; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | protected function renderParameters(View $view): array 31 | { 32 | $form = $view->getOption('form'); 33 | assert($form instanceof Form); 34 | 35 | return ['form' => $form->createView()]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Workflow/View/View.php: -------------------------------------------------------------------------------- 1 | $parameters The section parameters. 56 | * @param string|null $defaultTemplate The default template. 57 | */ 58 | public function addSection(string $name, array $parameters, ?string $defaultTemplate = null): View; 59 | 60 | /** 61 | * Check is section exists. 62 | * 63 | * @param string $name The section name. 64 | */ 65 | public function hasSection(string $name): bool; 66 | 67 | /** 68 | * Render the response. 69 | */ 70 | public function render(): Response; 71 | } 72 | -------------------------------------------------------------------------------- /src/Workflow/View/ViewFactory.php: -------------------------------------------------------------------------------- 1 | $options View options. 17 | * @param string|null $template The template. 18 | * @param string $contentType The content type. 19 | */ 20 | public function create( 21 | Item $item, 22 | $context, 23 | array $options = [], 24 | ?string $template = null, 25 | string $contentType = View::CONTENT_TYPE_HTML 26 | ): View; 27 | } 28 | --------------------------------------------------------------------------------