diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 023ccbc..6176840 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,4 +1,4 @@
-name: Unit Test
+name: Unit Tests
on:
pull_request:
@@ -17,40 +17,42 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [8.1, 8.2, 8.3]
- laravel: [10.*, 11.*, 12.*]
+ php: [8.3, 8.4]
+ laravel: ['11.*', '12.*']
dependency-version: [prefer-lowest, prefer-stable]
- exclude:
- - laravel: 11.*
- php: 8.1
- - laravel: 12.*
- php: 8.1
+ include:
+ - laravel: '11.*'
+ testbench: '^9.0'
+ - laravel: '12.*'
+ testbench: '^10.0'
- name: PHP${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
+ name: PHP ${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: ${{matrix.php}}
+ php-version: ${{ matrix.php }}
+ coverage: none
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
- # Use composer.json for key, if composer.lock is not committed.
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
run: |
- composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- - name: Run PHPUnit tests
- run: |
- vendor/bin/phpunit
+ - name: Run tests
+ run: vendor/bin/pest
diff --git a/.gitignore b/.gitignore
index 86f5995..52a35ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
/*.cache
/vendor/
phpunit.xml.bak
+.planning/
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
new file mode 100644
index 0000000..5b9ecbe
--- /dev/null
+++ b/.php-cs-fixer.php
@@ -0,0 +1,211 @@
+ 'psr12',
+ 'rules' => [
+ '@PHP83Migration' => true,
+ '@PSR12' => true,
+ 'align_multiline_comment' => true,
+ 'array_indentation' => true,
+ 'array_push' => true,
+ 'array_syntax' => true,
+ 'backtick_to_shell_exec' => true,
+ 'binary_operator_spaces' => true,
+ 'blank_line_before_statement' => [
+ 'statements' => [
+ 'return',
+ 'throw',
+ ],
+ ],
+ 'cast_spaces' => true,
+ 'class_attributes_separation' => [
+ 'elements' => [
+ 'const' => 'one',
+ 'method' => 'one',
+ 'property' => 'one',
+ ],
+ ],
+ 'concat_space' => [
+ 'spacing' => 'one',
+ ],
+ 'ereg_to_preg' => true,
+ 'function_declaration' => true,
+ 'function_typehint_space' => true,
+ 'general_phpdoc_annotation_remove' => [
+ 'annotations' => [
+ 'author',
+ 'package',
+ 'subpackage',
+ ],
+ ],
+ 'general_phpdoc_tag_rename' => true,
+ 'include' => true,
+ 'linebreak_after_opening_tag' => true,
+ 'magic_constant_casing' => true,
+ 'magic_method_casing' => true,
+ 'method_argument_space' => [
+ 'after_heredoc' => false,
+ ],
+ 'method_chaining_indentation' => true,
+ 'modernize_strpos' => true,
+ 'modernize_types_casting' => true,
+ 'multiline_comment_opening_closing' => true,
+ 'multiline_whitespace_before_semicolons' => [
+ 'strategy' => 'no_multi_line',
+ ],
+ 'native_function_casing' => true,
+ 'new_with_parentheses' => true,
+ 'no_alias_functions' => true,
+ 'no_alias_language_construct_call' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_empty_comment' => true,
+ 'no_empty_phpdoc' => true,
+ 'no_empty_statement' => true,
+ 'no_extra_blank_lines' => true,
+ 'no_mixed_echo_print' => [
+ 'use' => 'echo',
+ ],
+ 'no_multiline_whitespace_around_double_arrow' => true,
+ 'no_php4_constructor' => true,
+ 'no_short_bool_cast' => true,
+ 'no_superfluous_phpdoc_tags' => [
+ 'allow_mixed' => false,
+ 'allow_unused_params' => true,
+ 'remove_inheritdoc' => true,
+ ],
+ 'no_trailing_comma_in_singleline_array' => true,
+ 'no_unused_imports' => true,
+ 'no_whitespace_before_comma_in_array' => true,
+ 'normalize_index_brace' => true,
+ 'nullable_type_declaration' => [
+ 'syntax' => 'union',
+ ],
+ 'object_operator_without_whitespace' => true,
+ 'operator_linebreak' => true,
+ 'ordered_imports' => [
+ 'imports_order' => [
+ 'const',
+ 'class',
+ 'function',
+ ],
+ 'sort_algorithm' => 'alpha',
+ ],
+ 'ordered_types' => [
+ 'null_adjustment' => 'always_last',
+ 'sort_algorithm' => 'alpha',
+ ],
+ 'phpdoc_add_missing_param_annotation' => [
+ 'only_untyped' => false,
+ ],
+ 'phpdoc_align' => [
+ 'align' => 'left',
+ ],
+ 'phpdoc_indent' => true,
+ 'phpdoc_no_alias_tag' => [
+ 'replacements' => [
+ 'link' => 'see',
+ 'type' => 'var',
+ ],
+ ],
+ 'phpdoc_no_empty_return' => true,
+ 'phpdoc_no_package' => true,
+ 'phpdoc_no_useless_inheritdoc' => true,
+ 'phpdoc_order' => true,
+ 'phpdoc_scalar' => true,
+ 'phpdoc_separation' => true,
+ 'phpdoc_summary' => false,
+ 'phpdoc_trim' => true,
+ 'phpdoc_types' => true,
+ 'phpdoc_var_without_name' => true,
+ 'pow_to_exponentiation' => true,
+ 'psr_autoloading' => true,
+ 'random_api_migration' => true,
+ 'set_type_to_cast' => true,
+ 'single_class_element_per_statement' => true,
+ 'single_line_comment_style' => [
+ 'comment_types' => [
+ 'asterisk',
+ 'hash',
+ ],
+ ],
+ 'single_line_empty_body' => false,
+ 'single_quote' => true,
+ 'single_space_around_construct' => [
+ 'constructs_followed_by_a_single_space' => [
+ 'abstract',
+ 'as',
+ 'case',
+ 'catch',
+ 'class',
+ 'const',
+ 'const_import',
+ 'do',
+ 'else',
+ 'elseif',
+ 'enum',
+ 'final',
+ 'finally',
+ 'for',
+ 'foreach',
+ 'function',
+ 'function_import',
+ 'if',
+ 'insteadof',
+ 'interface',
+ 'match',
+ 'named_argument',
+ 'namespace',
+ 'new',
+ 'private',
+ 'protected',
+ 'public',
+ 'readonly',
+ 'static',
+ 'switch',
+ 'trait',
+ 'try',
+ 'type_colon',
+ 'use',
+ 'use_lambda',
+ 'while',
+ ],
+ 'constructs_preceded_by_a_single_space' => [
+ 'as',
+ 'else',
+ 'elseif',
+ 'use_lambda',
+ ],
+ ],
+ 'space_after_semicolon' => true,
+ 'standardize_not_equals' => true,
+ 'ternary_to_null_coalescing' => true,
+ 'trailing_comma_in_multiline' => [
+ 'after_heredoc' => true,
+ 'elements' => [
+ 'arguments',
+ 'array_destructuring',
+ 'arrays',
+ 'match',
+ 'parameters',
+ ],
+ ],
+ 'trim_array_spaces' => true,
+ 'types_spaces' => true,
+ 'unary_operator_spaces' => true,
+ 'whitespace_after_comma_in_array' => [
+ 'ensure_single_space' => true,
+ ],
+ 'yield_from_array_to_yields' => true,
+ 'yoda_style' => false,
+ ],
+];
+
+return (new PhpCsFixer\Config())
+ ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
+ ->setRules($config['rules'])
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->exclude($config['exclude'] ?? [])
+ ->notPath($config['notPath'] ?? [])
+ ->in(__DIR__),
+ );
diff --git a/.styleci.yml b/.styleci.yml
index 5c27c16..0c6ec71 100644
--- a/.styleci.yml
+++ b/.styleci.yml
@@ -1,56 +1,76 @@
-preset: psr12
-version: 8
-enabled:
- - alpha_ordered_imports
- - blank_line_before_return
- - cast_spaces
- - hash_to_slash_comment
- - heredoc_to_nowdoc
- - include
- - linebreak_after_opening_tag
- - method_separation
- - native_function_casing
- - no_blank_lines_after_phpdoc
- - no_blank_lines_after_return
- - no_blank_lines_after_throw
- - no_blank_lines_between_traits
- - no_empty_comment
- - no_empty_phpdoc
- - no_empty_statement
- - no_extra_consecutive_blank_lines
- - no_short_bool_cast
- - no_singleline_whitespace_before_semicolons
- - no_spaces_inside_offset
- - no_spaces_outside_offset
- - no_trailing_comma_in_list_call
- - no_trailing_comma_in_singleline_array
- - no_unneeded_control_parentheses
- - no_unused_imports
- - no_useless_return
- - no_whitespace_before_comma_in_array
- - normalize_index_brace
- - object_operator_without_whitespace
- - phpdoc_add_missing_param_annotation
- - phpdoc_indent
- - phpdoc_inline_tag
- - phpdoc_link_to_see
- - phpdoc_no_access
- - phpdoc_no_empty_return
- - phpdoc_no_package
- - phpdoc_order
- - phpdoc_scalar
- - phpdoc_separation
- - phpdoc_single_line_var_spacing
- - phpdoc_trim
- - phpdoc_type_to_var
- - phpdoc_types
- - phpdoc_var_without_name
- - print_to_echo
- - short_array_syntax
- - space_after_semicolon
- - standardize_not_equals
- - trailing_comma_in_multiline_array
- - trim_array_spaces
- - unalign_double_arrow
- - unalign_equals
- - whitespace_after_comma_in_array
+php:
+ preset: psr12
+ risky: true
+ disabled:
+ enabled:
+ - alpha_ordered_imports
+ - array_push
+ - backtick_to_shell_exec
+ - blank_line_before_return
+ - cast_spaces
+ - ereg_to_preg
+ - hash_to_slash_comment
+ - heredoc_to_nowdoc
+ - include
+ - linebreak_after_opening_tag
+ - method_separation
+ - modernize_strpos
+ - native_function_casing
+ - no_alias_functions
+ - no_blank_lines_after_return
+ - no_blank_lines_after_throw
+ - no_blank_lines_between_traits
+ - no_empty_comment
+ - no_empty_phpdoc
+ - no_empty_statement
+ - no_extra_consecutive_blank_lines
+ - no_multiline_whitespace_around_double_arrow
+ - no_short_bool_cast
+ - no_singleline_whitespace_before_semicolons
+ - no_spaces_inside_offset
+ - no_spaces_outside_offset
+ - no_superfluous_phpdoc_tags
+ - no_trailing_comma_in_list_call
+ - no_trailing_comma_in_singleline_array
+ - no_unused_imports
+ - no_useless_return
+ - no_whitespace_before_comma_in_array
+ - normalize_index_brace
+ - object_operator_without_whitespace
+ # - operator_linebreak_beginning
+ - phpdoc_add_missing_param_annotation
+ - phpdoc_indent
+ - phpdoc_inline_tag
+ - phpdoc_link_to_see
+ - phpdoc_no_access
+ - phpdoc_no_empty_return
+ - phpdoc_no_package
+ - phpdoc_order
+ - phpdoc_scalar
+ - phpdoc_separation
+ - phpdoc_single_line_var_spacing
+ - phpdoc_trim
+ - phpdoc_type_to_var
+ - phpdoc_types
+ - phpdoc_var_without_name
+ - pow_to_exponentiation
+ - print_to_echo
+ - set_type_to_cast
+ - short_array_syntax
+ - space_after_semicolon
+ - standardize_not_equals
+ - trailing_comma_in_multiline_array
+ # - trailing_comma_in_multiline_call
+ # - trailing_comma_in_multiline_definition
+ - trim_array_spaces
+ - unalign_double_arrow
+ - unalign_equals
+ - whitespace_after_comma_in_array
+ finder:
+ exclude:
+ - "modules"
+ - "node_modules"
+ - "nova"
+ - "storage"
+ - "spark"
+ - "vendor"
diff --git a/README.md b/README.md
index 74a7710..e39230d 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,26 @@
# Laravel State workflow
-[](https://github.com/RingierIMU/state-workflow/actions/workflows/main.yml)
+[](https://github.com/RingierIMU/state-workflow/actions/workflows/main.yml)
**Laravel State workflow** provide tools for defining and managing workflows and activities with ease.
-It offers an object oriented way to define a process or a life cycle that your object goes through.
+It offers an object oriented way to define a process or a life cycle that your object goes through.
Each step or stage in the process is called a state. You do also define transitions that describe the action to get from one state to another.
A workflow consist of **state** and **actions** to get from one state to another.
These **actions** are called **transitions** which describes how to get from one state to another.
+
+## Requirements
+
+| Version | PHP | Laravel |
+|---------|-----|---------|
+| 5.x | 8.3+ | 11, 12 |
+| 4.x | 8.1+ | 10, 11 |
+
## Installation
+
+Requires PHP 8.3+ and Laravel 11+.
+
```bash
-composer require ringierimu/state-workflow
+composer require ringierimu/state-workflow
```
Publish `config/workflow.php` file
diff --git a/composer.json b/composer.json
index fd63def..c928b5c 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,8 @@
{
"name": "ringierimu/state-workflow",
- "type": "library",
"description": "Laravel State Workflow provide tools for defining and managing workflows and activities with ease.",
+ "license": "MIT",
+ "type": "library",
"keywords": [
"workflow",
"state management",
@@ -9,8 +10,6 @@
"state workflow",
"laravel"
],
- "homepage": "https://github.com/RingierIMU/state-workflow",
- "license": "MIT",
"authors": [
{
"name": "Norby Baruani",
@@ -18,30 +17,23 @@
"role": "Developer"
}
],
+ "homepage": "https://github.com/RingierIMU/state-workflow",
"require": {
- "php": "^8.1",
- "illuminate/events": "^10.0|^11.0|^12.0",
- "illuminate/support": "^10.0|^11.0|^12.0",
- "symfony/event-dispatcher": "^6.0|^7.0",
- "symfony/workflow": "^5.1",
- "symfony/property-access": "^5.1"
+ "php": "^8.3",
+ "illuminate/events": "^11.0 || ^12.0",
+ "illuminate/support": "^11.0 || ^12.0",
+ "symfony/event-dispatcher": "^7.0",
+ "symfony/property-access": "^7.0",
+ "symfony/workflow": "^7.0"
},
"require-dev": {
- "funkjedi/composer-include-files": "^1.0",
- "mockery/mockery": "^1.3|^1.4.2",
- "orchestra/testbench": "^8.0|^9.15|^10",
- "phpunit/phpunit": "^10.0|^11.0"
- },
- "extra": {
- "include_files": [
- "tests/Fixtures/Helpers.php"
- ],
- "laravel": {
- "providers": [
- "Ringierimu\\StateWorkflow\\StateWorkflowServiceProvider"
- ]
- }
+ "mockery/mockery": "^1.3",
+ "orchestra/testbench": "^9.15 || ^10.0",
+ "pestphp/pest": "^4.0",
+ "pestphp/pest-plugin-laravel": "^4.0"
},
+ "minimum-stability": "dev",
+ "prefer-stable": true,
"autoload": {
"psr-4": {
"Ringierimu\\StateWorkflow\\": "src/"
@@ -52,14 +44,19 @@
"Ringierimu\\StateWorkflow\\Tests\\": "tests/"
}
},
- "minimum-stability": "dev",
- "prefer-stable": true,
"config": {
"allow-plugins": {
- "funkjedi/composer-include-files": true
+ "pestphp/pest-plugin": true
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Ringierimu\\StateWorkflow\\StateWorkflowServiceProvider"
+ ]
}
},
"scripts": {
- "test": "phpunit"
+ "test": "vendor/bin/pest"
}
}
diff --git a/src/Console/Commands/StateWorkflowDumpCommand.php b/src/Console/Commands/StateWorkflowDumpCommand.php
index af3e9d0..21f4a50 100644
--- a/src/Console/Commands/StateWorkflowDumpCommand.php
+++ b/src/Console/Commands/StateWorkflowDumpCommand.php
@@ -5,6 +5,8 @@
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
+use ReflectionClass;
+use ReflectionException;
use Ringierimu\StateWorkflow\Workflow\StateWorkflow;
use Symfony\Component\Process\Process;
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
@@ -13,9 +15,6 @@
* Class StateWorkflowDumpCommand.
*
* Symfony dump workflow https://symfony.com/doc/current/workflow/dumping-workflows.html
- *
- *
- * @author Norby Baruani
*/
class StateWorkflowDumpCommand extends Command
{
@@ -36,10 +35,10 @@ class StateWorkflowDumpCommand extends Command
protected $description = 'Dumps a State Workflow as a graphviz file using GraphvizDumper.';
/**
- * @throws \ReflectionException
+ * @throws ReflectionException
* @throws Exception
*/
- public function handle()
+ public function handle(): void
{
$workflowName = $this->argument('workflow');
$format = $this->option('format');
@@ -47,7 +46,7 @@ public function handle()
if (!isset($config[$workflowName])) {
throw new Exception(
- "Workflow $workflowName is not configured. Make sure it is configured correctly on the config file."
+ "Workflow $workflowName is not configured. Make sure it is configured correctly on the config file.",
);
}
@@ -57,12 +56,12 @@ public function handle()
$class = $config[$workflowName]['class'];
- $ref = new \ReflectionClass($class);
+ $ref = new ReflectionClass($class);
$model = $ref->newInstance();
if (!method_exists($model, 'workflow')) {
throw new Exception(
- "Class $class does not support State Workflow. Make sure Class is configured correctly"
+ "Class $class does not support State Workflow. Make sure Class is configured correctly",
);
}
diff --git a/src/Events/BaseEvent.php b/src/Events/BaseEvent.php
index eaabf6d..dba53f6 100644
--- a/src/Events/BaseEvent.php
+++ b/src/Events/BaseEvent.php
@@ -9,19 +9,11 @@
*/
abstract class BaseEvent
{
- /**
- * @var Event
- */
- protected $originalEvent;
-
/**
* BaseEvent constructor.
- *
- * @param Event $event
*/
- public function __construct(Event $event)
+ public function __construct(protected Event $originalEvent)
{
- $this->originalEvent = $event;
}
/**
diff --git a/src/Events/GuardEvent.php b/src/Events/GuardEvent.php
index 2fa545b..41ec87c 100644
--- a/src/Events/GuardEvent.php
+++ b/src/Events/GuardEvent.php
@@ -2,6 +2,8 @@
namespace Ringierimu\StateWorkflow\Events;
+use Override;
+use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent as SymfonyGuardEvent;
/**
@@ -13,8 +15,6 @@ class GuardEvent extends BaseEvent
{
/**
* GuardEvent constructor.
- *
- * @param SymfonyGuardEvent $event
*/
public function __construct(SymfonyGuardEvent $event)
{
@@ -22,8 +22,9 @@ public function __construct(SymfonyGuardEvent $event)
}
/**
- * @return \Symfony\Component\Workflow\Event\Event|SymfonyGuardEvent
+ * @return Event|SymfonyGuardEvent
*/
+ #[Override]
public function getOriginalEvent()
{
return $this->originalEvent;
diff --git a/src/Interfaces/StateWorkflowInterface.php b/src/Interfaces/StateWorkflowInterface.php
index 7ff7449..31f666a 100644
--- a/src/Interfaces/StateWorkflowInterface.php
+++ b/src/Interfaces/StateWorkflowInterface.php
@@ -9,10 +9,6 @@ interface StateWorkflowInterface
{
/**
* Returns the current state.
- *
- * @param $object
- *
- * @return mixed
*/
public function getState($object);
}
diff --git a/src/Interfaces/WorkflowEventSubscriberInterface.php b/src/Interfaces/WorkflowEventSubscriberInterface.php
index e2a99e0..f239697 100644
--- a/src/Interfaces/WorkflowEventSubscriberInterface.php
+++ b/src/Interfaces/WorkflowEventSubscriberInterface.php
@@ -2,6 +2,8 @@
namespace Ringierimu\StateWorkflow\Interfaces;
+use Illuminate\Events\Dispatcher;
+
/**
* Interface WorkflowEventSubscriberInterface.
*/
@@ -20,7 +22,7 @@ interface WorkflowEventSubscriberInterface
* "App\Listeners\UserEventSubscriber@onGuardActivate"
* );
*
- * @param \Illuminate\Events\Dispatcher $event
+ * @param Dispatcher $event
*/
public function subscribe($event);
}
diff --git a/src/Interfaces/WorkflowRegistryInterface.php b/src/Interfaces/WorkflowRegistryInterface.php
index 2be68de..bc5c4ab 100644
--- a/src/Interfaces/WorkflowRegistryInterface.php
+++ b/src/Interfaces/WorkflowRegistryInterface.php
@@ -13,7 +13,7 @@ interface WorkflowRegistryInterface
* Returns SateWorkflow.
*
* @param object $object
- * @param null $workflowName
+ * @param null $workflowName
*
* @return WorkflowInterface
*/
@@ -21,9 +21,6 @@ public function get($object, $workflowName = null);
/**
* Register workflow subscribers.
- *
- * @param $class
- * @param $name
*/
public function addSubscriber($class, $name);
}
diff --git a/src/Models/StateWorkflowHistory.php b/src/Models/StateWorkflowHistory.php
index cb5fa6f..7cd2a32 100644
--- a/src/Models/StateWorkflowHistory.php
+++ b/src/Models/StateWorkflowHistory.php
@@ -3,14 +3,17 @@
namespace Ringierimu\StateWorkflow\Models;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
+use Override;
/**
* @property string $model_name
- * @property int $model_id
+ * @property int $model_id
* @property string $transition
* @property string $from
* @property string $to
- * @property int $user_id
+ * @property int $user_id
*
* Class StateWorkflowHistory
*/
@@ -30,14 +33,7 @@ class StateWorkflowHistory extends Model
];
/**
- * @var array
- */
- protected $casts = [
- 'context' => 'array',
- ];
-
- /**
- * @return \Illuminate\Database\Eloquent\Relations\MorphTo
+ * @return MorphTo
*/
public function model()
{
@@ -45,10 +41,18 @@ public function model()
}
/**
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ * @return BelongsTo
*/
public function user()
{
return $this->belongsTo(config('workflow.setup.user_class'));
}
+
+ #[Override]
+ protected function casts(): array
+ {
+ return [
+ 'context' => 'array',
+ ];
+ }
}
diff --git a/src/StateWorkflowServiceProvider.php b/src/StateWorkflowServiceProvider.php
index 028481a..0d8d7eb 100644
--- a/src/StateWorkflowServiceProvider.php
+++ b/src/StateWorkflowServiceProvider.php
@@ -3,6 +3,7 @@
namespace Ringierimu\StateWorkflow;
use Illuminate\Support\ServiceProvider;
+use Override;
use Ringierimu\StateWorkflow\Console\Commands\StateWorkflowDumpCommand;
use Ringierimu\StateWorkflow\Interfaces\WorkflowRegistryInterface;
@@ -14,7 +15,7 @@ class StateWorkflowServiceProvider extends ServiceProvider
/**
* Bootstrap the application services.
*/
- public function boot()
+ public function boot(): void
{
$this->publishConfig();
$this->publishDatabase();
@@ -24,37 +25,32 @@ public function boot()
/**
* Register the application services...
*/
- public function register()
+ #[Override]
+ public function register(): void
{
$this->mergeConfigFrom($this->configPath(), 'workflow');
- $this->app->singleton('stateWorkflow', function () {
- return new WorkflowRegistry(
- collect($this->app['config']->get('workflow'))
- ->except('setup')
- ->toArray()
- );
- });
+ $this->app->singleton('stateWorkflow', fn () => new WorkflowRegistry(
+ collect($this->app['config']->get('workflow'))
+ ->except('setup')
+ ->toArray(),
+ ));
$this->app->alias('stateWorkflow', WorkflowRegistryInterface::class);
}
/**
* Return config file.
- *
- * @return string
*/
- protected function configPath()
+ protected function configPath(): string
{
return __DIR__ . '/../config/workflow.php';
}
/**
* Return migrations path.
- *
- * @return string
*/
- private function migrationPath()
+ private function migrationPath(): string
{
return __DIR__ . '/../database/migrations';
}
@@ -96,6 +92,7 @@ protected function registerCommands()
*
* @return array
*/
+ #[Override]
public function provides()
{
return ['stateWorkflow'];
diff --git a/src/Subscribers/WorkflowSubscriber.php b/src/Subscribers/WorkflowSubscriber.php
index 1569c6c..e70f65a 100644
--- a/src/Subscribers/WorkflowSubscriber.php
+++ b/src/Subscribers/WorkflowSubscriber.php
@@ -14,17 +14,13 @@
/**
* Class WorkflowSubscriber.
- *
- * @author Norby Baruani
*/
class WorkflowSubscriber implements EventSubscriberInterface
{
/**
* Validate whether the transition is allowed at all.
- *
- * @param SymfonyGuardEvent $event
*/
- public function guardEvent(SymfonyGuardEvent $event)
+ public function guardEvent(SymfonyGuardEvent $event): void
{
$workflowName = $event->getWorkflowName();
$transitionName = $event->getTransition()->getName();
@@ -37,10 +33,8 @@ public function guardEvent(SymfonyGuardEvent $event)
/**
* The subject is about to leave a place.
- *
- * @param Event $event
*/
- public function leaveEvent(Event $event)
+ public function leaveEvent(Event $event): void
{
$places = $event->getTransition()->getFroms();
$workflowName = $event->getWorkflowName();
@@ -56,10 +50,8 @@ public function leaveEvent(Event $event)
/**
* The subject is going through this transition.
- *
- * @param Event $event
*/
- public function transitionEvent(Event $event)
+ public function transitionEvent(Event $event): void
{
$workflowName = $event->getWorkflowName();
$transitionName = $event->getTransition()->getName();
@@ -73,10 +65,8 @@ public function transitionEvent(Event $event)
/**
* The subject is about to enter a new place. This event is triggered just before the subject places are updated,
* which means that the marking of the subject is not yet updated with the new places.
- *
- * @param Event $event
*/
- public function enterEvent(Event $event)
+ public function enterEvent(Event $event): void
{
$places = $event->getTransition()->getTos();
$workflowName = $event->getWorkflowName();
@@ -92,10 +82,8 @@ public function enterEvent(Event $event)
/**
* The subject has entered in the places and the marking is updated (making it a good place to flush data in Doctrine).
- *
- * @param Event $event
*/
- public function enteredEvent(Event $event)
+ public function enteredEvent(Event $event): void
{
$places = $event->getTransition()->getTos();
$workflowName = $event->getWorkflowName();
@@ -125,10 +113,8 @@ public function enteredEvent(Event $event)
/**
* The object has completed this transition.
- *
- * @param Event $event
*/
- public function completedEvent(Event $event)
+ public function completedEvent(Event $event): void
{
$workflowName = $event->getWorkflowName();
$transitionName = $event->getTransition()->getName();
diff --git a/src/Subscribers/WorkflowSubscriberHandler.php b/src/Subscribers/WorkflowSubscriberHandler.php
index ca0fc34..2968982 100644
--- a/src/Subscribers/WorkflowSubscriberHandler.php
+++ b/src/Subscribers/WorkflowSubscriberHandler.php
@@ -2,24 +2,22 @@
namespace Ringierimu\StateWorkflow\Subscribers;
+use Illuminate\Events\Dispatcher;
use Ringierimu\StateWorkflow\Interfaces\WorkflowEventSubscriberInterface;
/**
* Class WorkflowSubscriberHandler.
*
* Dynamically register listener for workflow events
- *
- * @author Norby Baruani
*/
abstract class WorkflowSubscriberHandler implements WorkflowEventSubscriberInterface
{
- /** @var null */
protected $name;
/**
* WorkflowSubscriberHandler constructor.
*
- * @param $workflowName
+ * @param null|mixed $workflowName
*/
public function __construct($workflowName = null)
{
@@ -39,12 +37,12 @@ public function __construct($workflowName = null)
* "App\Listeners\UserEventSubscriber@onGuardActivate"
* );
*
- * @param \Illuminate\Events\Dispatcher $event
+ * @param Dispatcher $event
*/
- public function subscribe($event)
+ public function subscribe($event): void
{
// get name of instantiated concrete class
- $class = get_called_class();
+ $class = static::class;
// loop through each method of the class
foreach (get_class_methods($class) as $method) {
// if the method name starts with 'on'
@@ -94,20 +92,17 @@ public function subscribe($event)
* workflow.[workflow name].entered
* workflow.[workflow name].entered.[state name]
*
- * @param $name
*
* @return string
*/
protected function key($name)
{
// remove on from beginning. eg. onGuard => Guard
- $name = ltrim($name, 'on');
+ $name = ltrim((string) $name, 'on');
// prepend uppercase letters with . eg. EnteredDeleted => .Entered.Deleted
- $name = preg_replace_callback('/[A-Z]/', function ($m) {
- return ".{$m[0]}";
- }, $name);
+ $name = preg_replace_callback('/[A-Z]/', fn ($m) => ".{$m[0]}", $name);
// remove trailing . eg. .Entered.Deleted => Entered.Deleted
- $name = ltrim($name, '.');
+ $name = ltrim((string) $name, '.');
// now that we have the dots we can lowercase the name. eg. Entered.Deleted => entered.deleted
$name = strtolower($name);
diff --git a/src/Traits/HasWorkflowTrait.php b/src/Traits/HasWorkflowTrait.php
index 2215db7..23a270a 100644
--- a/src/Traits/HasWorkflowTrait.php
+++ b/src/Traits/HasWorkflowTrait.php
@@ -2,14 +2,18 @@
namespace Ringierimu\StateWorkflow\Traits;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use ReflectionClass;
+use ReflectionException;
use Ringierimu\StateWorkflow\Interfaces\WorkflowRegistryInterface;
use Ringierimu\StateWorkflow\Models\StateWorkflowHistory;
use Ringierimu\StateWorkflow\Workflow\StateWorkflow;
+use Symfony\Component\Workflow\Marking;
+use Symfony\Component\Workflow\Transition;
/**
* Trait HasWorkflowTrait.
- *
- * @author Norby Baruani
*/
trait HasWorkflowTrait
{
@@ -29,7 +33,7 @@ trait HasWorkflowTrait
private $stateHistoryModel = StateWorkflowHistory::class;
/**
- * @throws \ReflectionException
+ * @throws ReflectionException
*
* @return StateWorkflow
*/
@@ -43,9 +47,7 @@ public function workflow()
}
/**
- * @throws \ReflectionException
- *
- * @return mixed
+ * @throws ReflectionException
*/
public function state()
{
@@ -53,12 +55,11 @@ public function state()
}
/**
- * @param $transition
* @param array $context
*
- * @throws \ReflectionException
+ * @throws ReflectionException
*
- * @return \Symfony\Component\Workflow\Marking
+ * @return Marking
*/
public function applyTransition($transition, $context = [])
{
@@ -68,9 +69,7 @@ public function applyTransition($transition, $context = [])
}
/**
- * @param $transition
- *
- * @throws \ReflectionException
+ * @throws ReflectionException
*
* @return bool
*/
@@ -82,9 +81,9 @@ public function canTransition($transition)
/**
* Return object available transitions.
*
- * @throws \ReflectionException
+ * @throws ReflectionException
*
- * @return array|\Symfony\Component\Workflow\Transition[]
+ * @return array|Transition[]
*/
public function getEnabledTransition()
{
@@ -92,7 +91,7 @@ public function getEnabledTransition()
}
/**
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function stateHistory()
{
@@ -102,9 +101,8 @@ public function stateHistory()
/**
* Save Model changes and log changes to StateHistory table.
*
- * @param array $transitionData
*
- * @return \Illuminate\Database\Eloquent\Model
+ * @return Model
*/
public function saveStateHistory(array $transitionData)
{
@@ -126,27 +124,19 @@ public function authenticatedUserId()
/**
* Model configuration name on config/workflow.php.
*
- * @throws \ReflectionException
- *
- * @return string
+ * @throws ReflectionException
*/
- public function configName()
+ public function configName(): string
{
- return lcfirst((new \ReflectionClass($this))->getShortName());
+ return lcfirst((new ReflectionClass($this))->getShortName());
}
- /**
- * @return string
- */
- public function authUserForeignKey()
+ public function authUserForeignKey(): string
{
return 'user_id';
}
- /**
- * @return string
- */
- public function modelPrimaryKey()
+ public function modelPrimaryKey(): string
{
return 'id';
}
diff --git a/src/Workflow/MethodMarkingStore.php b/src/Workflow/MethodMarkingStore.php
index 02b097f..f827d29 100644
--- a/src/Workflow/MethodMarkingStore.php
+++ b/src/Workflow/MethodMarkingStore.php
@@ -3,6 +3,7 @@
namespace Ringierimu\StateWorkflow\Workflow;
use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
@@ -11,28 +12,20 @@
*/
class MethodMarkingStore implements MarkingStoreInterface
{
- private $property;
- private $propertyAccessor;
- private $singleState;
+ private readonly PropertyAccessor $propertyAccessor;
/**
* MethodMarkingStore constructor.
*
- * @param bool $singleState
- * @param string $property Used to determine methods to call
- * The `getMarking` method will use `$subject->getProperty()`
- * The `setMarking` method will use `$subject->setProperty(string|array $places, array $context = array())`
+ * @param string $property Used to determine methods to call
+ * The `getMarking` method will use `$subject->getProperty()`
+ * The `setMarking` method will use `$subject->setProperty(string|array $places, array $context = array())`
*/
- public function __construct(bool $singleState = false, string $property = 'marking')
+ public function __construct(private readonly bool $singleState = false, private readonly string $property = 'marking')
{
- $this->singleState = $singleState;
- $this->property = $property;
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
- /**
- * {@inheritdoc}
- */
public function getMarking(object $subject): Marking
{
$marking = $this->propertyAccessor->getValue($subject, $this->property);
@@ -48,10 +41,7 @@ public function getMarking(object $subject): Marking
return new Marking($marking);
}
- /**
- * {@inheritdoc}
- */
- public function setMarking(object $subject, Marking $marking, array $context = [])
+ public function setMarking(object $subject, Marking $marking, array $context = []): void
{
$this->propertyAccessor->setValue($subject, $this->property, key($marking->getPlaces()));
}
diff --git a/src/Workflow/StateWorkflow.php b/src/Workflow/StateWorkflow.php
index e5722d8..33a8e34 100644
--- a/src/Workflow/StateWorkflow.php
+++ b/src/Workflow/StateWorkflow.php
@@ -14,40 +14,26 @@
*/
class StateWorkflow extends Workflow implements StateWorkflowInterface
{
- /** @var array */
- protected $config;
-
/**
* StateWorkflow constructor.
- *
- * @param Definition $definition
- * @param MarkingStoreInterface|null $markingStore
- * @param EventDispatcherInterface|null $dispatcher
- * @param string $name
- * @param array $config
*/
public function __construct(
- array $config,
+ protected array $config,
Definition $definition,
MarkingStoreInterface $markingStore = null,
EventDispatcherInterface $dispatcher = null,
- string $name = 'unnamed'
+ string $name = 'unnamed',
) {
parent::__construct($definition, $markingStore, $dispatcher, $name);
- $this->config = $config;
}
/**
* Returns the current state.
- *
- * @param $object
- *
- * @return mixed
*/
- public function getState($object)
+ public function getState($object): mixed
{
$accessor = new PropertyAccessor();
- $propertyPath = isset($this->config['property_path']) ? $this->config['property_path'] : 'current_state';
+ $propertyPath = $this->config['property_path'] ?? 'current_state';
return $accessor->getValue($object, $propertyPath);
}
diff --git a/src/WorkflowRegistry.php b/src/WorkflowRegistry.php
index 88bf995..658d550 100644
--- a/src/WorkflowRegistry.php
+++ b/src/WorkflowRegistry.php
@@ -5,6 +5,7 @@
use Exception;
use Illuminate\Support\Facades\Event;
use ReflectionClass;
+use ReflectionException;
use Ringierimu\StateWorkflow\Interfaces\WorkflowEventSubscriberInterface;
use Ringierimu\StateWorkflow\Interfaces\WorkflowRegistryInterface;
use Ringierimu\StateWorkflow\Subscribers\WorkflowSubscriber;
@@ -16,42 +17,29 @@
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\StateMachine;
-use Symfony\Component\Workflow\SupportStrategy\ClassInstanceSupportStrategy;
use Symfony\Component\Workflow\SupportStrategy\InstanceOfSupportStrategy;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
+use Symfony\Component\Workflow\WorkflowInterface;
/**
* Class WorkflowRegistry.
*/
class WorkflowRegistry implements WorkflowRegistryInterface
{
- /**
- * @var Registry
- */
- protected $registry;
+ protected Registry $registry;
- /**
- * @var array
- */
- protected $config;
-
- /**
- * @var EventDispatcher
- */
- protected $dispatcher;
+ protected EventDispatcher $dispatcher;
/**
* WorkflowRegistry constructor.
*
- * @param array $config
*
- * @throws \ReflectionException
+ * @throws ReflectionException
*/
- public function __construct(array $config)
+ public function __construct(protected array $config)
{
$this->registry = new Registry();
- $this->config = $config;
$this->dispatcher = new EventDispatcher();
$subscriber = new WorkflowSubscriber();
@@ -65,40 +53,26 @@ public function __construct(array $config)
}
}
- /**
- * {@inheritdoc}
- */
- public function get($subject, $workflowName = null)
+ public function get($subject, $workflowName = null): WorkflowInterface
{
return $this->registry->get($subject, $workflowName);
}
/**
* Add a workflow to the subject.
- *
- * @param StateWorkflow $workflow
- * @param string $className
*/
- public function registerWorkflow(StateWorkflow $workflow, string $className)
+ public function registerWorkflow(StateWorkflow $workflow, string $className): void
{
- // Add method became addWorkflow method in Symfony Workflow Component v4.1
- // InstanceOfSupportStrategy class became ClassInstanceSupportStrategy in v4.1
- $method = method_exists($this->registry, 'addWorkflow') ? 'addWorkflow' : 'add';
- $strategyClass = class_exists(InstanceOfSupportStrategy::class)
- ? InstanceOfSupportStrategy::class
- : ClassInstanceSupportStrategy::class;
- $this->registry->$method($workflow, new $strategyClass($className));
+ $this->registry->addWorkflow($workflow, new InstanceOfSupportStrategy($className));
}
/**
* Add a workflow to the registry from array.
*
- * @param $name
- * @param array $workflowData
*
- * @throws \ReflectionException
+ * @throws ReflectionException
*/
- public function addWorkflows($name, array $workflowData)
+ public function addWorkflows($name, array $workflowData): void
{
$definitionBuilder = new DefinitionBuilder($workflowData['states']);
@@ -121,19 +95,12 @@ public function addWorkflows($name, array $workflowData)
/**
* Return the workflow instance.
- *
- * @param $name
- * @param array $workflowData
- * @param Definition $definition
- * @param MarkingStoreInterface $markingStore
- *
- * @return mixed
*/
protected function getWorkflowInstance(
$name,
array $workflowData,
Definition $definition,
- MarkingStoreInterface $markingStore
+ MarkingStoreInterface $markingStore,
) {
$className = $this->getWorkflowClass($workflowData);
@@ -141,12 +108,11 @@ protected function getWorkflowInstance(
}
/**
- * @param array $workflowData
- * @param bool $override
+ * @param bool $override
*
* @return mixed|string
*/
- private function getWorkflowClass(array $workflowData, $override = true)
+ private function getWorkflowClass(array $workflowData, $override = true): string
{
if ($override) {
$className = StateWorkflow::class;
@@ -162,16 +128,13 @@ private function getWorkflowClass(array $workflowData, $override = true)
/**
* Return the making store instance.
*
- * @param array $workflowData
- *
- * @throws \ReflectionException
*
- * @return object|MarkingStoreInterface
+ * @throws ReflectionException
*/
- protected function getMarkingStoreInstance(array $workflowData)
+ protected function getMarkingStoreInstance(array $workflowData): object
{
- $markingStoreData = isset($workflowData['marking_store']) ? $workflowData['marking_store'] : [];
- $propertyPath = isset($workflowData['property_path']) ? $workflowData['property_path'] : 'current_state';
+ $markingStoreData = $workflowData['marking_store'] ?? [];
+ $propertyPath = $workflowData['property_path'] ?? 'current_state';
$singleState = true;
@@ -194,13 +157,11 @@ protected function getMarkingStoreInstance(array $workflowData)
/**
* Register workflow subscribers.
*
- * @param $class
- * @param $name
*
- * @throws \ReflectionException
+ * @throws ReflectionException
* @throws Exception
*/
- public function addSubscriber($class, $name)
+ public function addSubscriber($class, $name): void
{
$reflection = new ReflectionClass($class);
diff --git a/tests/ErrorHandlingTest.php b/tests/ErrorHandlingTest.php
new file mode 100644
index 0000000..9a7291b
--- /dev/null
+++ b/tests/ErrorHandlingTest.php
@@ -0,0 +1,41 @@
+user->applyTransition('create');
+})->throws(\RuntimeException::class, 'Subscriber error');
+
+test('transition does not silently succeed when subscriber throws', function () {
+ Event::listen('workflow.transition', function () {
+ throw new \RuntimeException('Transition handler failed');
+ });
+
+ $caught = false;
+
+ try {
+ $this->user->applyTransition('create');
+ } catch (\RuntimeException $e) {
+ $caught = true;
+ expect($e->getMessage())->toBe('Transition handler failed');
+ }
+
+ expect($caught)->toBeTrue();
+
+ // Model state may have partially changed depending on where in lifecycle
+ // the exception fires, but the exception DID propagate
+ $this->user = $this->user->refresh();
+ expect($this->user)->not->toBeNull();
+});
+
+test('exception in entered event handler propagates', function () {
+ Event::listen('workflow.entered', function () {
+ throw new \RuntimeException('Entered handler error');
+ });
+
+ $this->user->applyTransition('create');
+})->throws(\RuntimeException::class, 'Entered handler error');
diff --git a/tests/EventOrderingTest.php b/tests/EventOrderingTest.php
new file mode 100644
index 0000000..0b9f119
--- /dev/null
+++ b/tests/EventOrderingTest.php
@@ -0,0 +1,74 @@
+user->applyTransition('create');
+
+ // Verify milestone ordering (not exact sequence of all events)
+ $guardIndex = array_search('guard', $eventOrder);
+ $leaveIndex = array_search('leave', $eventOrder);
+ $transitionIndex = array_search('transition', $eventOrder);
+ $enterIndex = array_search('enter', $eventOrder);
+ $enteredIndex = array_search('entered', $eventOrder);
+ $completedIndex = array_search('completed', $eventOrder);
+
+ expect($guardIndex)->toBeLessThan($leaveIndex);
+ expect($leaveIndex)->toBeLessThan($transitionIndex);
+ expect($transitionIndex)->toBeLessThan($enterIndex);
+ expect($enterIndex)->toBeLessThan($enteredIndex);
+ expect($enteredIndex)->toBeLessThan($completedIndex);
+});
+
+test('all six lifecycle phases fire during a transition', function () {
+ $phases = [];
+
+ Event::listen('workflow.guard', function () use (&$phases) {
+ $phases[] = 'guard';
+ });
+ Event::listen('workflow.leave', function () use (&$phases) {
+ $phases[] = 'leave';
+ });
+ Event::listen('workflow.transition', function () use (&$phases) {
+ $phases[] = 'transition';
+ });
+ Event::listen('workflow.enter', function () use (&$phases) {
+ $phases[] = 'enter';
+ });
+ Event::listen('workflow.entered', function () use (&$phases) {
+ $phases[] = 'entered';
+ });
+ Event::listen('workflow.completed', function () use (&$phases) {
+ $phases[] = 'completed';
+ });
+
+ $this->user->applyTransition('create');
+
+ expect($phases)->toContain('guard');
+ expect($phases)->toContain('leave');
+ expect($phases)->toContain('transition');
+ expect($phases)->toContain('enter');
+ expect($phases)->toContain('entered');
+ expect($phases)->toContain('completed');
+});
diff --git a/tests/Fixtures/Helpers.php b/tests/Fixtures/Helpers.php
deleted file mode 100644
index 059667b..0000000
--- a/tests/Fixtures/Helpers.php
+++ /dev/null
@@ -1,14 +0,0 @@
- propertyListing
@@ -58,4 +55,32 @@ public function getWorflowConfig()
],
];
}
+
+ /**
+ * Return config with both primary and subscription workflows for multi-workflow testing.
+ */
+ public function getMultiWorflowConfig(): array
+ {
+ return array_merge($this->getWorflowConfig(), [
+ 'user_subscription' => [
+ 'class' => User::class,
+ 'property_path' => 'subscription_state',
+ 'states' => [
+ 'inactive',
+ 'active',
+ 'cancelled',
+ ],
+ 'transitions' => [
+ 'subscribe' => [
+ 'from' => 'inactive',
+ 'to' => 'active',
+ ],
+ 'cancel' => [
+ 'from' => 'active',
+ 'to' => 'cancelled',
+ ],
+ ],
+ ],
+ ]);
+ }
}
diff --git a/tests/Fixtures/database/migrations/add_new_column_to_users_table.php b/tests/Fixtures/database/migrations/add_new_column_to_users_table.php
index 8879f41..645e37b 100644
--- a/tests/Fixtures/database/migrations/add_new_column_to_users_table.php
+++ b/tests/Fixtures/database/migrations/add_new_column_to_users_table.php
@@ -12,6 +12,7 @@ public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('user_state');
+ $table->string('subscription_state')->nullable();
});
}
@@ -21,7 +22,7 @@ public function up()
public function down()
{
Schema::table('users', function (Blueprint $table) {
- $table->dropColumn('user_state');
+ $table->dropColumn(['user_state', 'subscription_state']);
});
}
};
diff --git a/tests/MultiWorkflowTest.php b/tests/MultiWorkflowTest.php
new file mode 100644
index 0000000..8b2a884
--- /dev/null
+++ b/tests/MultiWorkflowTest.php
@@ -0,0 +1,83 @@
+getMultiWorflowConfig();
+ app()['config']->set('workflow', $config);
+
+ $registry = new WorkflowRegistry($config);
+
+ $primaryWorkflow = $registry->get($this->user, 'user');
+ $subscriptionUser = $this->user;
+ $subscriptionUser->subscription_state = 'inactive';
+ $subscriptionUser->save();
+ $subscriptionWorkflow = $registry->get($subscriptionUser, 'user_subscription');
+
+ expect($primaryWorkflow)->not->toBeNull();
+ expect($subscriptionWorkflow)->not->toBeNull();
+});
+
+test('primary workflow transitions independently of subscription workflow', function () {
+ $config = $this->getMultiWorflowConfig();
+ app()['config']->set('workflow', $config);
+
+ // Set initial subscription state
+ $this->user->subscription_state = 'inactive';
+ $this->user->save();
+
+ $registry = new WorkflowRegistry($config);
+
+ // Apply primary workflow transition
+ $primaryWorkflow = $registry->get($this->user, 'user');
+ $primaryWorkflow->apply($this->user, 'create');
+ $this->user = $this->user->refresh();
+
+ expect($this->user->state())->toBe('pending_activation');
+ expect($this->user->subscription_state)->toBe('inactive');
+});
+
+test('subscription workflow transitions independently of primary workflow', function () {
+ $config = $this->getMultiWorflowConfig();
+ app()['config']->set('workflow', $config);
+
+ // Set initial subscription state
+ $this->user->subscription_state = 'inactive';
+ $this->user->save();
+
+ $registry = new WorkflowRegistry($config);
+
+ // Apply subscription workflow transition
+ $subscriptionWorkflow = $registry->get($this->user, 'user_subscription');
+ $subscriptionWorkflow->apply($this->user, 'subscribe');
+ $this->user = $this->user->refresh();
+
+ // Primary state unchanged
+ expect($this->user->state())->toBe('new');
+ // Subscription state transitioned
+ expect($this->user->subscription_state)->toBe('active');
+});
+
+test('both workflows can transition in sequence on same model', function () {
+ $config = $this->getMultiWorflowConfig();
+ app()['config']->set('workflow', $config);
+
+ // Set initial subscription state
+ $this->user->subscription_state = 'inactive';
+ $this->user->save();
+
+ $registry = new WorkflowRegistry($config);
+
+ // Transition primary workflow
+ $primaryWorkflow = $registry->get($this->user, 'user');
+ $primaryWorkflow->apply($this->user, 'create');
+ $this->user = $this->user->refresh();
+
+ // Transition subscription workflow
+ $subscriptionWorkflow = $registry->get($this->user, 'user_subscription');
+ $subscriptionWorkflow->apply($this->user, 'subscribe');
+ $this->user = $this->user->refresh();
+
+ expect($this->user->state())->toBe('pending_activation');
+ expect($this->user->subscription_state)->toBe('active');
+});
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..9b2f77c
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,5 @@
+in(__DIR__);
diff --git a/tests/TestCase.php b/tests/TestCase.php
index bad1d17..a778d0a 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -19,7 +19,7 @@ abstract class TestCase extends OrchestraTestCase
/** @var User */
protected $user;
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
diff --git a/tests/Unit/UserUnitTest.php b/tests/Unit/UserUnitTest.php
index 6069e96..048ea00 100644
--- a/tests/Unit/UserUnitTest.php
+++ b/tests/Unit/UserUnitTest.php
@@ -1,63 +1,43 @@
assertInstanceOf(StateWorkflow::class, $this->user->workflow());
- }
-
- public function test_is_current_state_new()
- {
- $this->assertEquals('new', $this->user->state());
- }
-
- public function test_can_apply_transition()
- {
- $this->assertTrue($this->user->canTransition('create'));
- $this->assertFalse($this->user->canTransition('block'));
- }
-
- public function test_invalid_transition_throws_exception()
- {
- $expectedExceptionClass = class_exists(NotEnabledTransitionException::class)
- ? NotEnabledTransitionException::class
- : LogicException::class;
- $this->expectException($expectedExceptionClass);
-
- $this->user->applyTransition('block');
- }
-
- public function test_apply_transitions()
- {
- $this->user->applyTransition('create');
- $this->user = $this->user->refresh();
- $this->assertEquals('pending_activation', $this->user->state());
- $this->assertEquals(1, $this->user->stateHistory()->count());
-
- $this->user->applyTransition('activate');
- $this->user = $this->user->refresh();
- $this->assertEquals('activated', $this->user->state());
- $this->assertEquals(2, $this->user->stateHistory()->count());
-
- $this->user->applyTransition('block');
- $this->user = $this->user->refresh();
- $this->assertEquals('blocked', $this->user->state());
- $this->assertEquals(3, $this->user->stateHistory()->count());
-
- $this->user->applyTransition('delete');
- $this->user = $this->user->refresh();
- $this->assertEquals('deleted', $this->user->state());
- $this->assertEquals(4, $this->user->stateHistory()->count());
- }
-}
+test('it returns workflow instance', function () {
+ expect($this->user->workflow())->toBeInstanceOf(StateWorkflow::class);
+});
+
+test('current state is new', function () {
+ expect($this->user->state())->toBe('new');
+});
+
+test('can apply transition', function () {
+ expect($this->user->canTransition('create'))->toBeTrue();
+ expect($this->user->canTransition('block'))->toBeFalse();
+});
+
+test('invalid transition throws exception', function () {
+ $this->user->applyTransition('block');
+})->throws(NotEnabledTransitionException::class);
+
+test('apply transitions through full lifecycle', function () {
+ $this->user->applyTransition('create');
+ $this->user = $this->user->refresh();
+ expect($this->user->state())->toBe('pending_activation');
+ expect($this->user->stateHistory()->count())->toBe(1);
+
+ $this->user->applyTransition('activate');
+ $this->user = $this->user->refresh();
+ expect($this->user->state())->toBe('activated');
+ expect($this->user->stateHistory()->count())->toBe(2);
+
+ $this->user->applyTransition('block');
+ $this->user = $this->user->refresh();
+ expect($this->user->state())->toBe('blocked');
+ expect($this->user->stateHistory()->count())->toBe(3);
+
+ $this->user->applyTransition('delete');
+ $this->user = $this->user->refresh();
+ expect($this->user->state())->toBe('deleted');
+ expect($this->user->stateHistory()->count())->toBe(4);
+});
diff --git a/tests/WorkflowSubscriberTest.php b/tests/WorkflowSubscriberTest.php
index edbcd90..6887ede 100644
--- a/tests/WorkflowSubscriberTest.php
+++ b/tests/WorkflowSubscriberTest.php
@@ -1,68 +1,43 @@
getWorflowConfig());
- $workflow = $workflowRegistry->get($this->user);
-
- $workflow->apply($this->user, 'create');
- $this->assertCount(24, $events);
-
- $this->assertEquals('workflow.guard', $events[0]);
- $this->assertEquals('workflow.user.guard', $events[1]);
- $this->assertEquals('workflow.user.guard.create', $events[2]);
-
- $this->assertEquals('workflow.leave', $events[3]);
- $this->assertEquals('workflow.user.leave', $events[4]);
- $this->assertEquals('workflow.user.leave.new', $events[5]);
-
- $this->assertEquals('workflow.transition', $events[6]);
- $this->assertEquals('workflow.user.transition', $events[7]);
- $this->assertEquals('workflow.user.transition.create', $events[8]);
-
- $this->assertEquals('workflow.enter', $events[9]);
- $this->assertEquals('workflow.user.enter', $events[10]);
- $this->assertEquals('workflow.user.enter.pending_activation', $events[11]);
-
- $this->assertEquals('workflow.entered', $events[12]);
- $this->assertEquals('workflow.user.entered', $events[13]);
- $this->assertEquals('workflow.user.entered.pending_activation', $events[14]);
-
- $this->assertEquals('workflow.completed', $events[15]);
- $this->assertEquals('workflow.user.completed', $events[16]);
- $this->assertEquals('workflow.user.completed.create', $events[17]);
-
- //Announce model next available transitions events
- $this->assertEquals('workflow.guard', $events[18]);
- $this->assertEquals('workflow.user.guard', $events[19]);
- $this->assertEquals('workflow.user.guard.activate', $events[20]);
-
- $this->assertEquals('workflow.guard', $events[21]);
- $this->assertEquals('workflow.user.guard', $events[22]);
- $this->assertEquals('workflow.user.guard.block', $events[23]);
- }
- }
-}
-
-namespace {
- $events = null;
-
- if (!function_exists('event')) {
- function event($ev)
- {
- global $events;
- $events[] = $ev;
- }
- }
-}
+use Illuminate\Support\Facades\Event;
+
+test('workflow subscriber emits events on transition', function () {
+ Event::fake();
+
+ $this->user->applyTransition('create');
+
+ // Guard events (3: generic, workflow-specific, transition-specific)
+ Event::assertDispatched('workflow.guard');
+ Event::assertDispatched('workflow.user.guard');
+ Event::assertDispatched('workflow.user.guard.create');
+
+ // Leave events (3: generic, workflow-specific, state-specific)
+ Event::assertDispatched('workflow.leave');
+ Event::assertDispatched('workflow.user.leave');
+ Event::assertDispatched('workflow.user.leave.new');
+
+ // Transition events
+ Event::assertDispatched('workflow.transition');
+ Event::assertDispatched('workflow.user.transition');
+ Event::assertDispatched('workflow.user.transition.create');
+
+ // Enter events
+ Event::assertDispatched('workflow.enter');
+ Event::assertDispatched('workflow.user.enter');
+ Event::assertDispatched('workflow.user.enter.pending_activation');
+
+ // Entered events
+ Event::assertDispatched('workflow.entered');
+ Event::assertDispatched('workflow.user.entered');
+ Event::assertDispatched('workflow.user.entered.pending_activation');
+
+ // Completed events
+ Event::assertDispatched('workflow.completed');
+ Event::assertDispatched('workflow.user.completed');
+ Event::assertDispatched('workflow.user.completed.create');
+
+ // Guard checks for available transitions after state change
+ Event::assertDispatched('workflow.user.guard.activate');
+ Event::assertDispatched('workflow.user.guard.block');
+});