├── CHANGELOG.md ├── LICENSE.md ├── README-todo.md ├── README.md ├── assests └── img.png ├── composer.json ├── config └── arflow.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ ├── create_arflow_history_table.php │ └── create_arflow_table.php.stub ├── docker-compose.yml ├── resources └── views │ └── .gitkeep └── src ├── Abstracts └── AbstractTransitionSuccessJob.php ├── ArFlowService.php ├── ArFlowServiceProvider.php ├── Collections └── TransitionGuardResultCollection.php ├── Commands └── ArFlowCommand.php ├── Contacts ├── StateableModelContract.php ├── TransitionActionContract.php └── TransitionGuardContract.php ├── DTOs └── TransitionGuardResultDTO.php ├── Exceptions ├── InitialStateNotFoundException.php ├── NotImplementedException.php ├── StateMetadataNotFoundException.php ├── StateNotFoundException.php ├── TransitionActionException.php ├── TransitionNotFoundException.php ├── WorkflowNotAppliedException.php ├── WorkflowNotFoundException.php └── WorkflowNotSupportedException.php ├── Facades └── ArFlow.php ├── StateTransition.php ├── Traits └── HasState.php └── TransitionActions └── LogHistoryTransitionAction.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `ArFlow` will be documented in this file. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) AuroraWebSoftware 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-todo.md: -------------------------------------------------------------------------------- 1 | https://github.com/sebdesign/laravel-state-machine 2 | 3 | 4 | ## todo 5 | - without guards'ların imlemenet edilmesi 6 | - testlerin yazılması 7 | - config den alınan veriler için bir util class yazılmalı 8 | - history testleri 9 | - kanban boards 10 | - test coverage %90 11 | - geçişlerde veya fail’lerde event’lerin dispatch edilmesi 12 | - eğer hiç guard yoksa geçemiyor. geçebilmeli 13 | - drop column testleri 14 | - metadata ların kayıt edilip edilememesi test yazılması 15 | 16 | ## in progress 17 | - arflow facade'ı içine 18 | - workflow's states 19 | - workflowun supportded olduğu model ler 20 | - workflow u kullanan modeller 21 | 22 | 23 | ## done 24 | - histories tabloları ve actions 25 | - larastan 26 | - github unit test ayarları 27 | - docs 28 | - jobs için de bir interfcae tanımlanması 29 | 30 | 31 | --- 32 | composer analyse 33 | composer test-coverage 34 | composer format 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArFlow Workflow and State Machine Laravel Package Documentation 2 | 3 | ![img.png](assests%2Fimg.png) 4 | 5 | ## Introduction 6 | 7 | ArFlow is a Laravel package that allows you to implement workflow management for your Laravel Eloquent models. This documentation provides a comprehensive guide on how to use ArFlow effectively. 8 | 9 | ## Key Concepts 10 | 11 | ### 1. Workflows 12 | 13 | A workflow represents a series of states and transitions that a model can go through. Key points about workflows: 14 | 15 | - Each model can be associated with one or more workflows. 16 | - Workflows define the possible states and transitions for a model. 17 | 18 | ### 2. States 19 | 20 | States represent the different stages that a model can exist in within a workflow. Key points about states: 21 | 22 | - Each workflow has a set of predefined states. 23 | - Models can be in one of these states at any given time. 24 | 25 | ### 3. Transitions 26 | 27 | Transitions define the rules and conditions for moving a model from one state to another within a workflow. Key points about transitions: 28 | 29 | - Transitions specify which states a model can move from and to. 30 | - They can have **guards**, **actions**, and **success jobs** associated with them. 31 | 32 | ### 4. Guards 33 | 34 | Guards are conditions or checks that must be satisfied for a transition to occur. Key points about guards: 35 | 36 | - Guards prevent transitions if their conditions are not met. 37 | - They are defined as classes and can be customized to suit your application's logic. 38 | 39 | ### 5. Actions 40 | 41 | Actions are tasks or operations that are executed during a transition. Key points about actions: 42 | 43 | - Actions are executed when a transition occurs. 44 | - They are defined as classes and can be customized to perform specific tasks. 45 | 46 | ### 6. Success Jobs 47 | 48 | Success jobs are jobs or tasks that are dispatched after a successful transition. Key points about success jobs: 49 | 50 | - They allow you to perform background tasks after a transition. 51 | - Useful for logging, notifications, or other post-transition actions. 52 | 53 | ### 7. Initial State 54 | 55 | Each workflow has an initial state that a model enters when the workflow is applied. Key points about the initial state: 56 | 57 | - It's the starting point for models within a workflow. 58 | - Models are in the initial state when the workflow is first applied. 59 | 60 | 61 | ## Installation 62 | 63 | You can install the ArFlow package via Composer. Run the following command: 64 | 65 | ```bash 66 | composer require aurorawebsoftware/arflow 67 | ``` 68 | 69 | Next, you need to publish the package configuration and migration files: 70 | 71 | ```bash 72 | php artisan vendor:publish --tag=arflow-config 73 | ``` 74 | 75 | don't forget to run the migration: 76 | 77 | ```bash 78 | php artisan migrate 79 | ``` 80 | 81 | ### Model Setup 82 | 83 | To use ArFlow in your model, follow these steps: 84 | 85 | 1. Use the `HasState` trait in your model class. 86 | 87 | This trait provides functionality that allows a model to be a part of a workflow, fetch configurations, get initial states, and perform transitions. 88 | 89 | ```php 90 | use AuroraWebSoftware\ArFlow\Traits\HasState; 91 | 92 | class YourModel extends Model 93 | { 94 | use HasState; 95 | 96 | // Your model properties and methods 97 | } 98 | ``` 99 | 100 | 2. Implement the `StateableModelContract` interface in your model class. 101 | 102 | This interface ensures your model has the required methods to function as a stateable entity. This includes setting workflow attributes, state attributes, and metadata attributes. You can also determine supported workflows, apply workflows, and make transitions. Below are sample usages: 103 | 104 | ```php 105 | use AuroraWebSoftware\ArFlow\Contacts\StateableModelContract; 106 | 107 | class YourModel extends Model implements StateableModelContract 108 | { 109 | use HasState; 110 | 111 | public static function supportedWorkflows(): array 112 | { 113 | return ['workflow1', 'workflow3']; 114 | } 115 | 116 | // Your model properties and methods 117 | } 118 | ``` 119 | 120 | 3. (Optional) Define the workflow-related attributes for your model in the model class if you want to change default values or skip this step: 121 | 122 | ```php 123 | class YourModel extends Model implements StateableModelContract 124 | { 125 | use HasState; 126 | 127 | public static function workflowAttribute(): string 128 | { 129 | return 'workflow'; 130 | } 131 | 132 | public static function stateAttribute(): string 133 | { 134 | return 'state'; 135 | } 136 | 137 | public static function stateMetadataAttribute(): string 138 | { 139 | return 'state_metadata'; 140 | } 141 | 142 | // Your model properties and methods 143 | } 144 | ``` 145 | 146 | ### Usage 147 | 148 | Now that you've set up your model, you can apply workflows and perform transitions: 149 | 150 | #### Applying a Workflow 151 | 152 | To apply a workflow to a model instance, use the `applyWorkflow` method: 153 | 154 | ```php 155 | $model = YourModel::find($id); 156 | $model->applyWorkflow('workflow_name'); 157 | 158 | ``` 159 | #### To get the current workflow of a model: 160 | ```php 161 | $currentWorkflow = $instance->currentWorkflow(); 162 | ``` 163 | 164 | #### To get the current state of a model: 165 | ```php 166 | $currentState = $instance->currentState(); 167 | ```` 168 | 169 | 170 | ### Checking Transition States 171 | 172 | You can check if a transition to a specific state is allowed using the `canTransitionTo` method: 173 | 174 | ```php 175 | $model = YourModel::find($id); 176 | if ($model->canTransitionTo('new_state')) { 177 | // Transition is allowed 178 | } else { 179 | // Transition is not allowed 180 | } 181 | ``` 182 | 183 | #### Transitioning to a State 184 | 185 | To transition a model to a new state, use the `transitionTo` method: 186 | 187 | ```php 188 | $model = YourModel::find($id); 189 | $model->transitionTo('new_state'); 190 | ``` 191 | 192 | ### Getting Defined and Allowed Transition States 193 | 194 | You can retrieve the defined and allowed transition states for a model: 195 | 196 | ```php 197 | // Defined transition states 198 | $definedStates = $model->definedTransitionStates(); 199 | 200 | // Allowed transition states 201 | $allowedStates = $model->allowedTransitionStates(); 202 | ``` 203 | 204 | ### TransitionGuardResults 205 | 206 | You can also get transition guard results using the `transitionGuardResults` method: 207 | 208 | ```php 209 | $results = $model->transitionGuardResults('new_state'); 210 | ``` 211 | 212 | This method returns a collection of transition guard results, which can be used to check if guards allow the transition. 213 | 214 | ### Configuration 215 | 216 | You can configure your workflows in the `config/arflow.php` file. Define your workflows, states, transitions, guards, and actions there. 217 | 218 | ### Sample Configuration 219 | 220 | Here's a sample configuration for a workflow: 221 | 222 | ### Blueprint Macro 223 | To simplify adding state columns to your migrations, a Blueprint macro is provided: 224 | This macro `$table->arflow()` will create three columns: workflow, state, and state_metadata. 225 | 226 | ```php 227 | // your_migration.php 228 | Schema::create('your_model', function (Blueprint $table) { 229 | $table->id(); 230 | $table->string('name'); 231 | $table->arflow(); 232 | $table->timestamps(); 233 | }); 234 | ``` 235 | 236 | 237 | 238 | ```php 239 | // config/arflow.php 240 | return [ 241 | 'workflows' => [ 242 | 'workflow_name' => [ 243 | 'states' => ['state1', 'state2', 'state3'], 244 | 'initial_state' => 'state1', 245 | 'transitions' => [ 246 | 'transition_name' => [ 247 | 'from' => ['state1'], 248 | 'to' => 'state2', 249 | 'guards' => [ 250 | [GuardClass::class, ['permission' => 'approval']], 251 | ], 252 | 'actions' => [ 253 | [ActionClass::class, ['param1' => 'value1']], 254 | ], 255 | 'success_metadata' => ['key' => 'value'], 256 | 'success_jobs' => [JobClass::class], 257 | ], 258 | // Define more transitions as needed 259 | ], 260 | ], 261 | // Define additional workflows 262 | ] 263 | ]; 264 | ``` 265 | 266 | ### Creating Transition Guards 267 | 268 | Sample Transition Guard Implementation. 269 | 270 | ```php 271 | 272 | namespace App\ArFlow\Guards; 273 | 274 | use AuroraWebSoftware\ArFlow\Contacts\StateableModelContract; 275 | use AuroraWebSoftware\ArFlow\Contacts\TransitionGuardContract; 276 | use AuroraWebSoftware\ArFlow\DTOs\TransitionGuardResultDTO; 277 | 278 | class PermissionTransitionGuard implements TransitionGuardContract 279 | { 280 | private StateableModelContract $model; 281 | private string $from; 282 | private string $to; 283 | private array $parameters; 284 | 285 | public function __construct() {} 286 | 287 | public function boot(StateableModelContract &Model $model, string $from, string $to, array $parameters): void 288 | { 289 | $this->model = $model; 290 | $this->from = $from; 291 | $this->to = $to; 292 | $this->parameters = $parameters; 293 | 294 | // You can perform any initialization here. 295 | } 296 | 297 | public function handle(): TransitionGuardResultDTO 298 | { 299 | // Implement your logic to check permissions here. 300 | // For example, check if the user has the required role to make the transition. 301 | 302 | // If the permission check passes, allow the transition: 303 | return TransitionGuardResultDTO::build(TransitionGuardResultDTO::ALLOWED); 304 | 305 | // If the permission check fails, deny the transition: 306 | // return TransitionGuardResultDTO::build(TransitionGuardResultDTO::DENIED, 'Permission denied.'); 307 | } 308 | } 309 | 310 | ``` 311 | 312 | ### Creating Transition Action 313 | 314 | Sample Transition Action 315 | ```php 316 | use AuroraWebSoftware\ArFlow\Contacts\TransitionActionContract; 317 | 318 | class SendNotificationAction implements TransitionActionContract 319 | { 320 | public function boot(StateableModelContract&Model $model, string $from, string $to, array $parameters = []): void {} 321 | 322 | public function handle(): void 323 | { 324 | // Send a notification when the transition is successful. 325 | } 326 | 327 | public function failed(): void 328 | { 329 | // Handle any cleanup or error logging here if the action fails. 330 | } 331 | } 332 | 333 | ``` 334 | 335 | 336 | ### Creating Transition Success Job 337 | Sample Transition Job 338 | ```php 339 | namespace AuroraWebSoftware\ArFlow\Tests\Jobs; 340 | 341 | use AuroraWebSoftware\ArFlow\Abstracts\AbstractTransitionSuccessJob; 342 | use AuroraWebSoftware\ArFlow\Contacts\StateableModelContract; 343 | use Illuminate\Database\Eloquent\Model; 344 | 345 | class TestTransitionSuccessJob extends AbstractTransitionSuccessJob 346 | { 347 | /** 348 | * Execute the job. 349 | * @param StateableModelContract&Model $model 350 | * @param string $from 351 | * @param string $to 352 | * @param array $parameters 353 | */ 354 | public function handle(StateableModelContract & Model $model, string $from, string $to, array $parameters = []): void 355 | { 356 | // Process 357 | } 358 | } 359 | 360 | 361 | ``` 362 | 363 | ## Contribution 364 | 365 | - php 8.2+ 366 | 367 | ```shell 368 | docker-compose up -d 369 | ``` 370 | 371 | ```shell 372 | composer format 373 | composer analyse 374 | composer test 375 | composer test-coverage 376 | ``` 377 | 378 | 379 | 380 | --- 381 | This documentation should help you get started with the ArFlow package in your Laravel application. Feel free to explore more features and configurations based on your project's requirements. 382 | 383 | For more information, please refer to the package's GitHub repository or contact us for support. 384 | 385 | 386 | -------------------------------------------------------------------------------- /assests/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraWebSoftware/ArFlow/e6355b96cf96d1837a29a3594367b06f0e34354e/assests/img.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurorawebsoftware/arflow", 3 | "description": "ArFlow is a Laravel package that allows you to implement workflow management for your Laravel Eloquent models.", 4 | "keywords": [ 5 | "AuroraWebSoftware", 6 | "laravel", 7 | "arflow" 8 | ], 9 | "homepage": "https://github.com/aurorawebsoftware/arflow", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "AuroraWebSoftwareTeam", 14 | "email": "websoftwareteam@aurorabilisim.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2|^8.3|^8.4", 20 | "spatie/laravel-package-tools": "^1.14.0", 21 | "laravel/framework": "^12.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.0", 25 | "nunomaduro/collision": "^8.0", 26 | "larastan/larastan": "^v3", 27 | "orchestra/testbench": "^10", 28 | "pestphp/pest": "^3.0", 29 | "pestphp/pest-plugin-arch": "^3.0", 30 | "pestphp/pest-plugin-laravel": "^3.0", 31 | "phpstan/extension-installer": "^1.1", 32 | "phpstan/phpstan-deprecation-rules": "^2.0", 33 | "phpstan/phpstan-phpunit": "^2.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "AuroraWebSoftware\\ArFlow\\": "src/", 38 | "AuroraWebSoftware\\ArFlow\\Database\\Factories\\": "database/factories/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "AuroraWebSoftware\\ArFlow\\Tests\\": "tests/", 44 | "Workbench\\App\\": "workbench/app/" 45 | } 46 | }, 47 | "scripts": { 48 | "post-autoload-dump": "@composer run prepare", 49 | "clear": "@php vendor/bin/testbench package:purge-arflow --ansi", 50 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 51 | "build": [ 52 | "@composer run prepare", 53 | "@php vendor/bin/testbench workbench:build --ansi" 54 | ], 55 | "start": [ 56 | "Composer\\Config::disableProcessTimeout", 57 | "@composer run build", 58 | "@php vendor/bin/testbench serve" 59 | ], 60 | "analyse": "vendor/bin/phpstan analyse", 61 | "test": "vendor/bin/pest", 62 | "test-coverage": "vendor/bin/pest --coverage", 63 | "format": "vendor/bin/pint" 64 | }, 65 | "config": { 66 | "sort-packages": true, 67 | "allow-plugins": { 68 | "pestphp/pest-plugin": true, 69 | "phpstan/extension-installer": true 70 | } 71 | }, 72 | "extra": { 73 | "laravel": { 74 | "providers": [ 75 | "AuroraWebSoftware\\ArFlow\\ArFlowServiceProvider" 76 | ], 77 | "aliases": { 78 | "ArFlow": "AuroraWebSoftware\\ArFlow\\Facades\\ArFlow" 79 | } 80 | } 81 | }, 82 | "minimum-stability": "stable", 83 | "prefer-stable": true 84 | } 85 | -------------------------------------------------------------------------------- /config/arflow.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('workflow'); 14 | $table->string('model_type')->index(); 15 | $table->integer('model_id')->index(); 16 | $table->string('from')->nullable(); 17 | $table->string('to'); 18 | $table->string('actor_model_type')->nullable(); 19 | $table->integer('actor_model_id')->nullable(); 20 | $table->string('comment')->nullable(); 21 | $table->json('metadata')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | public function down(): void 27 | { 28 | Schema::drop('arflow_state_transitions'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/create_arflow_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('asd'); 14 | $table->int('aasd'); 15 | $table->arflow(); 16 | 17 | $table->string($workflow)->nullable(false)->index(); 18 | $table->string($state)->nullable(false)->index(); 19 | $table->json($stateMetadata)->nullable(false); 20 | 21 | // add fields 22 | 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | public function down() 28 | { 29 | Schema::drop('payment_plan'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mariadb: 5 | image: mariadb:10.8 6 | ports: 7 | - "33062:3306" 8 | volumes: 9 | - ~/apps/arflow/mariadb:/var/lib/mysql 10 | environment: 11 | - MYSQL_ROOT_PASSWORD=arflow 12 | - MYSQL_PASSWORD=arflow 13 | - MYSQL_USER=arflow 14 | - MYSQL_DATABASE=arflow 15 | networks: 16 | default: 17 | driver: bridge 18 | ipam: 19 | config: 20 | - subnet: 172.16.51.0/24 21 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraWebSoftware/ArFlow/e6355b96cf96d1837a29a3594367b06f0e34354e/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Abstracts/AbstractTransitionSuccessJob.php: -------------------------------------------------------------------------------- 1 | workflows = Config::get('arflow.workflows') ?? []; 18 | } 19 | 20 | /** 21 | * @return array 22 | * 23 | * @throws WorkflowNotFoundException 24 | * @throws StateNotFoundException 25 | */ 26 | public function getStates(string $workflow): array 27 | { 28 | foreach ($this->workflows as $workflowKey => $workflowValues) { 29 | if ($workflowKey == $workflow) { 30 | return $workflowValues['states'] ?? throw new StateNotFoundException; 31 | } 32 | } 33 | throw new WorkflowNotFoundException; 34 | } 35 | 36 | public function getSupportedModelTypes(string $workflow): array 37 | { 38 | return []; 39 | // workflowun supportded olduğu model ler 40 | // https://github.com/spatie/laravel-model-info 41 | // todo akif 42 | } 43 | 44 | /** 45 | * @param class-string $modelType 46 | * @return Collection|null 47 | */ 48 | public function getModelInstances(string $workflow, string $modelType): ?Collection 49 | { 50 | return null; 51 | // workflow u kullanan modeller 52 | // todo akif 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ArFlowServiceProvider.php: -------------------------------------------------------------------------------- 1 | string($workflow)->nullable()->index(); 19 | $this->string($state)->nullable()->index(); 20 | $this->json($stateMetadata)->nullable(); 21 | }); 22 | 23 | Blueprint::macro('arflowDown', function (string $workflow = 'workflow', string $state = 'state', string $stateMetadata = 'state_metadata') { 24 | /** 25 | * @var Blueprint $this 26 | */ 27 | $this->dropColumn($workflow); 28 | $this->dropColumn($state); 29 | $this->dropColumn($stateMetadata); 30 | }); 31 | 32 | // load packages migrations 33 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 34 | 35 | return parent::boot(); 36 | } 37 | 38 | public function configurePackage(Package $package): void 39 | { 40 | /* 41 | * This class is a Package Service Provider 42 | * 43 | * More info: https://github.com/spatie/laravel-package-tools 44 | */ 45 | $package 46 | ->name('arflow') 47 | ->hasConfigFile('arflow') 48 | ->hasViews() 49 | // ->hasMigration('create_arflow_history_table') 50 | ->hasCommand(ArFlowCommand::class); 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Collections/TransitionGuardResultCollection.php: -------------------------------------------------------------------------------- 1 | > 10 | */ 11 | class TransitionGuardResultCollection extends Collection 12 | { 13 | const ALLOWED = 1; 14 | 15 | const DISALLOWED = 2; 16 | 17 | public function allowed(): bool 18 | { 19 | foreach ($this as $collection) { 20 | 21 | /** 22 | * @var Collection $collection 23 | */ 24 | $allowed = true; 25 | foreach ($collection as $transitionGuardResultDTO) { 26 | 27 | /** 28 | * @var TransitionGuardResultDTO $transitionGuardResultDTO 29 | */ 30 | if ($transitionGuardResultDTO->status == TransitionGuardResultDTO::DISALLOWED) { 31 | $allowed = false; 32 | } 33 | } 34 | 35 | if ($allowed) { 36 | return true; 37 | } 38 | } 39 | 40 | return false; 41 | } 42 | 43 | /** 44 | * @return array> 45 | */ 46 | public function messages(): array 47 | { 48 | $allMessages = []; 49 | 50 | $this->each( 51 | function (Collection $collection, $key1) use (&$allMessages) { 52 | 53 | $collection->each( 54 | function (TransitionGuardResultDTO $transitionGuardResultDTO) use (&$allMessages, $key1) { 55 | $allMessages[$key1] = array_merge($allMessages[$key1] ?? [], $transitionGuardResultDTO->messages()); 56 | } 57 | ); 58 | } 59 | ); 60 | 61 | return $allMessages; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Commands/ArFlowCommand.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Contacts/StateableModelContract.php: -------------------------------------------------------------------------------- 1 | 41 | */ 42 | public static function supportedWorkflows(): array; 43 | 44 | public function getId(): int|string; 45 | 46 | /** 47 | * applies the workflow to the instance 48 | * 49 | * @throws WorkflowNotFoundException 50 | * @throws WorkflowNotSupportedException 51 | */ 52 | public function applyWorkflow(string $workflow): bool; 53 | 54 | /** 55 | * Current model instance's applied workflow 56 | */ 57 | public function currentWorkflow(): string; 58 | 59 | /** 60 | * Current model instance's current workflow 61 | */ 62 | public function currentState(): string; 63 | 64 | /** 65 | * Current metadata of the instance 66 | * 67 | * @return array 68 | */ 69 | public function currentStateMetadata(): array; 70 | 71 | /** 72 | * @return TransitionGuardResultCollection> 73 | * 74 | * @throws WorkflowNotFoundException 75 | */ 76 | public function transitionGuardResults(string $toState, ?array $withoutGuards = null): TransitionGuardResultCollection; 77 | 78 | public function canTransitionTo(string $toState, ?array $withoutGuards = null): bool; 79 | 80 | /** 81 | * @return array|null 82 | */ 83 | public function definedTransitionKeys(?array $withoutGuards = null): ?array; 84 | 85 | /** 86 | * @param array|null $withoutGuards 87 | * 88 | * @throws WorkflowNotFoundException 89 | * @throws Throwable 90 | */ 91 | public function allowedTransitionKeys(?array $withoutGuards = null): ?array; 92 | 93 | /** 94 | * @return array|null 95 | */ 96 | public function definedTransitionStates(?array $withoutGuards = null): ?array; 97 | 98 | /** 99 | * @param array|null $withoutGuards 100 | * 101 | * @throws WorkflowNotFoundException 102 | * @throws Throwable 103 | */ 104 | public function allowedTransitionStates(?array $withoutGuards = null): ?array; 105 | 106 | public function lastUpdatedTime(): ?DateTime; 107 | 108 | /** 109 | * @param class-string|null $actorModelType 110 | * @param array|null $withoutGuards 111 | * 112 | * @throws StateNotFoundException 113 | * @throws TransitionActionException 114 | * @throws TransitionNotFoundException 115 | * @throws WorkflowNotAppliedException 116 | * @throws WorkflowNotFoundException 117 | * @throws WorkflowNotSupportedException 118 | */ 119 | public function transitionTo( 120 | string $toState, ?string $comment = null, 121 | ?string $actorModelType = null, ?int $actorModelId = null, 122 | ?array $metadata = null, 123 | ?array $withoutGuards = null, 124 | ?string $transitionKey = null, 125 | bool $logHistoryTransitionAction = true 126 | ): bool; 127 | } 128 | -------------------------------------------------------------------------------- /src/Contacts/TransitionActionContract.php: -------------------------------------------------------------------------------- 1 | |null $messages 18 | */ 19 | public function __construct( 20 | public int $status, 21 | public ?array $messages = [], 22 | ) {} 23 | 24 | public function addMessage(string $message): TransitionGuardResultDTO 25 | { 26 | $this->messages[] = $message; 27 | 28 | return $this; 29 | } 30 | 31 | public function allowed(): bool 32 | { 33 | return $this->status == self::ALLOWED; 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function messages(): array 40 | { 41 | return $this->messages; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Exceptions/InitialStateNotFoundException.php: -------------------------------------------------------------------------------- 1 | getStates(string $workflow) 12 | */ 13 | class ArFlow extends Facade 14 | { 15 | protected static function getFacadeAccessor(): string 16 | { 17 | return ArFlowService::class; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/StateTransition.php: -------------------------------------------------------------------------------- 1 | getAttribute('id'); 32 | } 33 | 34 | /** 35 | * returns all workflows 36 | * 37 | * @return array 38 | */ 39 | private function getConfigWorkflows(): array 40 | { 41 | $workflows = []; 42 | 43 | foreach (Config::get('arflow.workflows') ?? [] as $key => $value) { 44 | $workflows[] = $key; 45 | } 46 | 47 | return $workflows; 48 | } 49 | 50 | /** 51 | * @throws InitialStateNotFoundException 52 | */ 53 | private function getInitialState(string $workflow): string 54 | { 55 | $workflows = []; 56 | 57 | foreach (Config::get('arflow.workflows') ?? [] as $key => $value) { 58 | if ($key == $workflow) { 59 | return (string) $value['initial_state']; 60 | } 61 | } 62 | 63 | throw new InitialStateNotFoundException; 64 | } 65 | 66 | public function getGuarded(): array 67 | { 68 | $self = static::class; 69 | 70 | return [$self::workflowAttribute(), $self::stateAttribute(), $self::stateMetadataAttribute()]; 71 | } 72 | 73 | /** 74 | * workflow attribute of the model on the db 75 | */ 76 | public static function workflowAttribute(): string 77 | { 78 | return 'workflow'; 79 | } 80 | 81 | /** 82 | * state attribute of the model on the db 83 | */ 84 | public static function stateAttribute(): string 85 | { 86 | return 'state'; 87 | } 88 | 89 | /** 90 | * state metadata attribute of the model on the db 91 | */ 92 | public static function stateMetadataAttribute(): string 93 | { 94 | return 'state_metadata'; 95 | } 96 | 97 | /** 98 | * applies workflow to the model instance 99 | * 100 | * @throws WorkflowNotFoundException 101 | * @throws WorkflowNotSupportedException 102 | * @throws TransitionActionException 103 | * @throws InitialStateNotFoundException 104 | */ 105 | public function applyWorkflow(string $workflow): bool 106 | { 107 | /** 108 | * @var Model&StateableModelContract $self 109 | * @var Model&StateableModelContract $this 110 | */ 111 | $self = static::class; 112 | 113 | if (! in_array($workflow, $this->getConfigWorkflows())) { 114 | throw new WorkflowNotFoundException("$workflow Not Found"); 115 | } 116 | 117 | if (! in_array($workflow, $self::supportedWorkflows())) { 118 | throw new WorkflowNotSupportedException("$workflow Not Supported by $self"); 119 | } 120 | 121 | $this->{$self::workflowAttribute()} = $workflow; 122 | $this->{$self::stateAttribute()} = $this->getInitialState($workflow); 123 | 124 | $this->save(); 125 | 126 | $historyAction = App::make(LogHistoryTransitionAction::class); 127 | $historyAction->boot($this, '', $this->getInitialState($workflow), 128 | [ 129 | 'actor_model_type' => null, 130 | 'actor_model_id' => null, 131 | ] 132 | ); 133 | $historyAction->handle(); 134 | 135 | return true; 136 | 137 | } 138 | 139 | /** 140 | * @throws WorkflowNotAppliedException 141 | */ 142 | public function currentWorkflow(): string 143 | { 144 | $self = static::class; 145 | 146 | /** 147 | * @var Model&StateableModelContract $self 148 | */ 149 | $attribute = $this->getAttribute($self::workflowAttribute()); 150 | if (! $attribute) { 151 | throw new WorkflowNotAppliedException; 152 | } 153 | 154 | return $attribute; 155 | } 156 | 157 | /** 158 | * returns current state of the model instance 159 | * 160 | * @throws StateNotFoundException 161 | */ 162 | public function currentState(): string 163 | { 164 | $self = static::class; 165 | 166 | /** 167 | * @var Model&StateableModelContract $self 168 | */ 169 | $attribute = $this->getAttribute($self::stateAttribute()); 170 | if (! $attribute) { 171 | throw new StateNotFoundException; 172 | } 173 | 174 | return $attribute; 175 | } 176 | 177 | /** 178 | * @return array 179 | * 180 | * @throws StateMetadataNotFoundException 181 | */ 182 | public function currentStateMetadata(): array 183 | { 184 | $self = static::class; 185 | 186 | /** 187 | * @var Model&StateableModelContract $self 188 | */ 189 | $attribute = $this->getAttribute($self::stateMetadataAttribute()); 190 | if (! $attribute) { 191 | throw new StateMetadataNotFoundException; 192 | } 193 | 194 | return $this->getAttribute($self::stateMetadataAttribute()); 195 | } 196 | 197 | /** 198 | * @return TransitionGuardResultCollection> 199 | * 200 | * @throws WorkflowNotFoundException 201 | * @throws TransitionNotFoundException 202 | * @throws StateNotFoundException 203 | * @throws WorkflowNotSupportedException 204 | * @throws WorkflowNotAppliedException 205 | */ 206 | public function transitionGuardResults(string $toState, ?array $withoutGuards = null): TransitionGuardResultCollection 207 | { 208 | $resultCollection = TransitionGuardResultCollection::make(); 209 | 210 | $workflowValues = Config::get('arflow.workflows')[$this->currentWorkflow()] ?? null; 211 | 212 | if (! $workflowValues) { 213 | throw new WorkflowNotFoundException($this->currentWorkflow().' Not Found'); 214 | } 215 | 216 | $transitionValues = $workflowValues['transitions'] ?? null; 217 | 218 | if (! $transitionValues) { 219 | throw new TransitionNotFoundException; 220 | } 221 | 222 | foreach ($transitionValues as $transitionKey => $transitionValue) { 223 | $fromStateValues = is_array($transitionValue['from']) ? $transitionValue['from'] : [$transitionValue['from']]; 224 | $toStateValues = is_array($transitionValue['to']) ? $transitionValue['to'] : [$transitionValue['to']]; 225 | $guardValues = $transitionValue['guards']; 226 | 227 | if (in_array($this->currentState(), $fromStateValues) and in_array($toState, $toStateValues)) { 228 | $handledGuardInstancesResults = collect(); 229 | foreach ($guardValues as $guardValue) { 230 | /** 231 | * @var TransitionGuardContract $guardInstance 232 | */ 233 | $guardInstance = App::make($guardValue[0]); 234 | $guardInstance->boot($this, $this->currentState(), $toState, $guardValue[1] ?? []); 235 | $handledGuardInstancesResults->push($guardInstance->handle()); 236 | } 237 | 238 | $resultCollection->put($transitionKey, $handledGuardInstancesResults); 239 | } 240 | } 241 | 242 | return $resultCollection; 243 | } 244 | 245 | /** 246 | * check if state can transition to a state 247 | * 248 | * 249 | * @throws StateNotFoundException 250 | * @throws TransitionNotFoundException 251 | * @throws WorkflowNotAppliedException 252 | * @throws WorkflowNotFoundException 253 | * @throws WorkflowNotSupportedException 254 | */ 255 | public function canTransitionTo(string $toState, ?array $withoutGuards = null): bool 256 | { 257 | return $this->transitionGuardResults($toState, $withoutGuards)->allowed(); 258 | } 259 | 260 | public function definedTransitionKeys(?array $withoutGuards = null): ?array 261 | { 262 | // TODO: Implement definedTransitionKeys() method. 263 | // testleri yazılacak 264 | } 265 | 266 | public function allowedTransitionKeys(?array $withoutGuards = null): ?array 267 | { 268 | // TODO: Implement allowedTransitionKeys() method. 269 | // testleri yazılacak 270 | } 271 | 272 | /** 273 | * @throws StateNotFoundException 274 | * @throws WorkflowNotAppliedException 275 | * @throws WorkflowNotFoundException 276 | * @throws TransitionNotFoundException 277 | */ 278 | public function definedTransitionStates(?array $withoutGuards = null): ?array 279 | { 280 | $definedTransitionStates = []; 281 | 282 | $workflowValues = Config::get('arflow.workflows')[$this->currentWorkflow()] ?? null; 283 | 284 | if (! $workflowValues) { 285 | throw new WorkflowNotFoundException($this->currentWorkflow().' Not Found'); 286 | } 287 | 288 | $transitionValues = $workflowValues['transitions'] ?? null; 289 | if (! $transitionValues) { 290 | throw new TransitionNotFoundException; 291 | } 292 | 293 | foreach ($transitionValues as $transitionKey => $transitionValue) { 294 | $fromStateValues = is_array($transitionValue['from']) ? $transitionValue['from'] : [$transitionValue['from']]; 295 | 296 | if (in_array($this->currentState(), $fromStateValues)) { 297 | $toStateValues = is_array($transitionValue['to']) ? $transitionValue['to'] : [$transitionValue['to']]; 298 | $definedTransitionStates = array_merge($definedTransitionStates, $toStateValues); 299 | } 300 | } 301 | 302 | return array_unique($definedTransitionStates); 303 | } 304 | 305 | /** 306 | * @throws StateNotFoundException 307 | * @throws TransitionNotFoundException 308 | * @throws WorkflowNotAppliedException 309 | * @throws WorkflowNotFoundException 310 | * @throws WorkflowNotSupportedException 311 | */ 312 | public function allowedTransitionStates(?array $withoutGuards = null): ?array 313 | { 314 | $allowedTransitionStates = []; 315 | $definedTransitionStates = $this->definedTransitionStates($withoutGuards); 316 | 317 | foreach ($definedTransitionStates as $definedTransitionState) { 318 | if ($this->canTransitionTo($definedTransitionState)) { 319 | $allowedTransitionStates[] = $definedTransitionState; 320 | } 321 | } 322 | 323 | return $allowedTransitionStates; 324 | } 325 | 326 | /** 327 | * @throws WorkflowNotAppliedException 328 | */ 329 | public function lastUpdatedTime(): ?DateTime 330 | { 331 | return StateTransition::where([ 332 | 'workflow' => $this->currentWorkflow(), 333 | 'model_type' => static::class, 334 | 'model_id' => $this->id, 335 | ])->orderBy('id', 'desc')?->first()?->updated_at; 336 | } 337 | 338 | /** 339 | * @param class-string|null $actorModelType 340 | * @param array|null $withoutGuards 341 | * 342 | * @throws StateNotFoundException 343 | * @throws TransitionActionException 344 | * @throws TransitionNotFoundException 345 | * @throws WorkflowNotAppliedException 346 | * @throws WorkflowNotFoundException 347 | * @throws WorkflowNotSupportedException 348 | */ 349 | public function transitionTo( 350 | string $toState, ?string $comment = null, 351 | ?string $actorModelType = null, ?int $actorModelId = null, 352 | ?array $metadata = null, 353 | ?array $withoutGuards = null, 354 | ?string $transitionKey = null, 355 | bool $logHistoryTransitionAction = true 356 | ): bool { 357 | 358 | if (! $this->canTransitionTo($toState, $withoutGuards)) { 359 | throw new TransitionActionException; 360 | } 361 | 362 | $workflowValues = Config::get('arflow.workflows')[$this->currentWorkflow()] ?? []; 363 | if (! $workflowValues) { 364 | throw new WorkflowNotFoundException($this->currentWorkflow().' Not Found'); 365 | } 366 | 367 | $transitionValues = $workflowValues['transitions'] ?? null; 368 | if (! $transitionValues) { 369 | throw new TransitionNotFoundException; 370 | } 371 | 372 | $transitionKeyItem = ''; 373 | $actions = ''; 374 | $transitionFound = false; 375 | foreach ($transitionValues as $transitionKeyItem => $transition) { 376 | 377 | if ($transitionKey != null and $transitionKeyItem != $transitionKey) { 378 | continue; 379 | } 380 | 381 | $from = is_array($transition['from']) ? $transition['from'] : [$transition['from']]; 382 | $to = is_array($transition['to']) ? $transition['to'] : [$transition['to']]; 383 | $actions = $transition['actions']; 384 | $successJobs = $transition['success_jobs']; 385 | 386 | $actionInstances = []; 387 | 388 | if ($logHistoryTransitionAction) { 389 | $actorModelId = $actorModelId ?: Auth::id(); 390 | $actorModelType = $actorModelType ?: get_class(Auth::user()); 391 | 392 | $actions[] = [ 393 | LogHistoryTransitionAction::class, 394 | [ 395 | 'actor_model_type' => $actorModelType, 396 | 'actor_model_id' => $actorModelId, 397 | 'comment' => $comment, 398 | 'metadata' => (is_array($metadata)) ? json_encode($metadata, JSON_UNESCAPED_UNICODE) : $metadata, 399 | ], 400 | ]; 401 | } 402 | 403 | if (in_array($this->currentState(), $from) and in_array($toState, $to)) { 404 | 405 | foreach ($actions as $action) { 406 | /** 407 | * @var array $actionInstances 408 | */ 409 | $actionInstances[$action[0]] = App::make($action[0]); 410 | $actionInstances[$action[0]]->boot($this, $this->currentState(), $toState, $action[1] ?? []); 411 | 412 | try { 413 | $actionInstances[$action[0]]->handle(); 414 | } catch (\Exception $e) { 415 | foreach ($actionInstances as $actionInstance) { 416 | $actionInstance->failed(); 417 | } 418 | throw new TransitionActionException('Transition Action Failed: '.$e->getMessage()); 419 | } 420 | } 421 | 422 | foreach ($successJobs as $successJob) { 423 | 424 | if (isset($successJob[1]) && is_array($successJob[1])) { 425 | $successJobParameter = array_merge($successJob[1], $metadata ?? []); 426 | } else { 427 | $successJobParameter = $metadata; 428 | } 429 | 430 | dispatch(new $successJob[0]($this, $this->currentState(), $toState, $successJobParameter ?? [])); 431 | } 432 | 433 | $transitionFound = true; 434 | break; 435 | } 436 | } 437 | 438 | if (! $transitionFound) { 439 | throw new TransitionActionException('Transition Not Found'); 440 | } 441 | 442 | $self = static::class; 443 | $this->{$self::stateAttribute()} = $toState; 444 | $this->{$self::stateMetadataAttribute()} = [ 445 | 'latest_from_state' => $this->currentState(), 446 | 'latest_transition_key' => $transitionKeyItem, 447 | 'latest_actions' => $actions, 448 | ]; 449 | 450 | return $this->save(); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/TransitionActions/LogHistoryTransitionAction.php: -------------------------------------------------------------------------------- 1 | model = $model; 23 | $this->from = $from; 24 | $this->to = $to; 25 | $this->parameters = $parameters; 26 | } 27 | 28 | public function handle(): void 29 | { 30 | StateTransition::create([ 31 | 'workflow' => $this->model->currentWorkflow(), 32 | 'model_type' => get_class($this->model), 33 | 'model_id' => $this->model->getId(), 34 | 'from' => $this->from, 35 | 'to' => $this->to, 36 | 'actor_model_type' => $this->parameters['actor_model_type'], 37 | 'actor_model_id' => $this->parameters['actor_model_id'], 38 | 'comment' => $this->parameters['comment'] ?? null, 39 | 'metadata' => $this->parameters['metadata'] ?? null, 40 | ]); 41 | } 42 | 43 | public function failed(): void 44 | { 45 | // TODO: Implement failed() method. 46 | } 47 | } 48 | --------------------------------------------------------------------------------