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 -[![Unit Test](https://github.com/RingierIMU/state-workflow/actions/workflows/main.yml/badge.svg)](https://github.com/RingierIMU/state-workflow/actions/workflows/main.yml) +[![Tests](https://github.com/RingierIMU/state-workflow/actions/workflows/main.yml/badge.svg)](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'); +});