From a9c68424e0fcc933dd13816aaa7715e617fc36e2 Mon Sep 17 00:00:00 2001 From: adrianmiu Date: Sun, 7 Jun 2026 18:12:08 +0300 Subject: [PATCH 1/2] Replace processors with self-contained Callable* runnables and an Invokator registry Split the dual responsibility of the *Processor classes (name-keyed registry + executor) into two pieces: - Self-contained runnable callable stacks in Sirius\Invokator\Callables (CallablePipeline, CallableMiddleware, CallableFilter, CallableAction) that own one CallableCollection plus the Invoker and expose a fluent add()/run(). The old SimpleCallablesProcessor folds into CallableAction (argumentsLimit: null). - A framework-agnostic Sirius\Invokator\Invokator registry/facade, built from a single Invoker, exposing pipeline/middleware/filter/action/event/command with bulk registration plus dispatch()/handle() convenience. Events keep the PSR-14 Dispatcher/ListenerProvider; the command bus is reworked off the removed MiddlewareProcessor onto CallableMiddleware. CallableEvent and CallableCommand wrap them so events and commands share the same add()/run() shape. The Laravel layer collapses to thin wiring: InvokatorManager and Laravel\Registrar are removed; the service provider registers Invokator as the 'invokator' singleton and exposes Dispatcher/CommandBus from it. The facade is the explicit core API (extra args register callables; run via ->run()), while the do_* helpers keep WordPress-style run-on-args so the Blade directive and WP usage stay idiomatic. BREAKING CHANGE: removes Sirius\Invokator\Processors\*, InvokatorInterface, CallablesRegistryInterface, and the get()/process()/processCollection() API. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/1_callable_collection.md | 20 ++- docs/2_1_simple_collection.md | 31 +++- docs/2_2_pipelines.md | 20 +-- docs/2_3_command_bus.md | 20 ++- docs/2_3_event_dispatcher.md | 15 ++ docs/2_3_middlewares.md | 12 +- docs/2_4_wordpress_actions.md | 25 +-- docs/2_5_wordpress_filters.md | 26 +-- docs/2_6_custom_processors.md | 29 +++- docs/2_callable_processors.md | 40 +++-- docs/3_callable_modifiers.md | 50 +++--- docs/6_automatic_middleware.md | 51 +++--- docs/7_laravel.md | 62 +++++-- docs/9_upgrading_to_3.md | 112 +++++++++++++ docs/index.md | 15 ++ src/Callables/AbstractCallableStack.php | 51 ++++++ src/Callables/CallableAction.php | 40 +++++ src/Callables/CallableCommand.php | 37 +++++ src/Callables/CallableEvent.php | 39 +++++ src/Callables/CallableFilter.php | 43 +++++ src/Callables/CallableMiddleware.php | 41 +++++ .../CallablePipeline.php} | 39 ++--- src/Callables/CommandBus.php | 79 +++++++++ src/CallablesRegistryInterface.php | 10 -- src/Invokator.php | 154 +++++++++++++++++ src/InvokatorInterface.php | 18 -- src/Laravel/Facades/Invokator.php | 15 +- src/Laravel/InvokatorManager.php | 84 ---------- src/Laravel/Registrar.php | 31 ---- .../SiriusInvokatorServiceProvider.php | 32 +--- src/Laravel/helpers.php | 34 ++-- src/Processors/ActionsProcessor.php | 73 -------- src/Processors/CommandBus.php | 78 --------- src/Processors/FiltersProcessor.php | 75 --------- src/Processors/MiddlewareProcessor.php | 44 ----- src/Processors/SimpleCallablesProcessor.php | 70 -------- src/RunnableCallables.php | 17 ++ tests/Laravel/ContainerWiringTest.php | 4 +- tests/Laravel/FacadeTest.php | 25 ++- tests/Laravel/HelpersTest.php | 6 +- tests/src/Callables/CallableActionTest.php | 67 ++++++++ tests/src/Callables/CallableFilterTest.php | 20 +++ .../src/Callables/CallableMiddlewareTest.php | 20 +++ tests/src/Callables/CallablePipelineTest.php | 29 ++++ .../CommandBusTest.php | 19 ++- tests/src/CommandBus/SampleCommand.php | 3 - tests/src/CommandBus/SampleHandler.php | 3 - tests/src/InvokatorTest.php | 156 ++++++++++++++++++ tests/src/Modifiers/OnceTest.php | 16 +- tests/src/Modifiers/ResolveArgumentsTest.php | 10 +- tests/src/Modifiers/WithArgumentsTest.php | 18 +- tests/src/Modifiers/WrapTest.php | 10 +- tests/src/Processors/ActionsProcessorTest.php | 28 ---- tests/src/Processors/FiltersProcessorTest.php | 22 --- .../Processors/MiddlewareProcessorTest.php | 22 --- .../src/Processors/PipelineProcessorTest.php | 22 --- .../Processors/SimpleStackProcessorTest.php | 47 ------ 57 files changed, 1279 insertions(+), 900 deletions(-) create mode 100644 docs/9_upgrading_to_3.md create mode 100644 src/Callables/AbstractCallableStack.php create mode 100644 src/Callables/CallableAction.php create mode 100644 src/Callables/CallableCommand.php create mode 100644 src/Callables/CallableEvent.php create mode 100644 src/Callables/CallableFilter.php create mode 100644 src/Callables/CallableMiddleware.php rename src/{Processors/PipelineProcessor.php => Callables/CallablePipeline.php} (56%) create mode 100644 src/Callables/CommandBus.php delete mode 100644 src/CallablesRegistryInterface.php create mode 100644 src/Invokator.php delete mode 100644 src/InvokatorInterface.php delete mode 100644 src/Laravel/InvokatorManager.php delete mode 100644 src/Laravel/Registrar.php delete mode 100644 src/Processors/ActionsProcessor.php delete mode 100644 src/Processors/CommandBus.php delete mode 100644 src/Processors/FiltersProcessor.php delete mode 100644 src/Processors/MiddlewareProcessor.php delete mode 100644 src/Processors/SimpleCallablesProcessor.php create mode 100644 src/RunnableCallables.php create mode 100644 tests/src/Callables/CallableActionTest.php create mode 100644 tests/src/Callables/CallableFilterTest.php create mode 100644 tests/src/Callables/CallableMiddlewareTest.php create mode 100644 tests/src/Callables/CallablePipelineTest.php rename tests/src/{Processors => Callables}/CommandBusTest.php (50%) create mode 100644 tests/src/InvokatorTest.php delete mode 100644 tests/src/Processors/ActionsProcessorTest.php delete mode 100644 tests/src/Processors/FiltersProcessorTest.php delete mode 100644 tests/src/Processors/MiddlewareProcessorTest.php delete mode 100644 tests/src/Processors/PipelineProcessorTest.php delete mode 100644 tests/src/Processors/SimpleStackProcessorTest.php diff --git a/docs/1_callable_collection.md b/docs/1_callable_collection.md index 1f03f9c..82700cc 100644 --- a/docs/1_callable_collection.md +++ b/docs/1_callable_collection.md @@ -48,28 +48,32 @@ A callable with a higher priority will be executed before a callable with a lowe ## Executing a collection of callables -The `Sirius\Invokator` library comes with a few **collection processors** which are act as collection registries/repositories and collection executors. +A `CallableCollection` is the underlying queue. To actually execute the callables you build one of the **runnable callable stacks** that come with `Sirius\Invokator` (here a `CallablePipeline`) and add the callables to it. Each runner owns its own collection and is executed with a single `run(...)` call. ```php -use Sirius\Invokator\Processors\PipelineProcessor; +use Sirius\Invokator\Callables\CallablePipeline; use Sirius\Invokator\Invoker; // this is required for callables like "SomeClass@someMethod" // and by callables that have dependencies $invoker = new Invoker($yourChoiceOfDependencyInjectionContainer); -$processor = new PipelineProcessor($invoker); -// execute the collection created above as a pipeline with one parameter -$processor->processCollection($callables, ' world '); +$pipeline = new CallablePipeline($invoker); +$pipeline->add('trim') + ->add(fn ($s) => 'hello ' . $s) + ->add('Str::toUpper') + ->add('SlackChannel@send', -100); + +// execute the pipeline with one parameter +$pipeline->run(' world '); // this will // 1. trim the parameter => `world` // 2. concatenate with "hello " => `hello world` // 3. make the string uppercase => `HELLO WORLD`, -// 4. write the string as an info message to the logger -// 5. send the string to a SlackChannel +// 4. send the string to a SlackChannel ``` -Each type of callables processor has its own quirks that you can learn on the next page. +Each type of callable runner has its own quirks that you can learn on the next page. [Next: The callable_processors](2_callable_processors.md) diff --git a/docs/2_1_simple_collection.md b/docs/2_1_simple_collection.md index 5e0698e..106f470 100644 --- a/docs/2_1_simple_collection.md +++ b/docs/2_1_simple_collection.md @@ -2,26 +2,41 @@ title: Simple callable collections --- -# The simple processor +# The simple collection behaviour -This processor has the following characteristics: +The "simple" behaviour has the following characteristics: 1. All the parameters are passed down to each of the callables. This means all the callables should have the same signature (although this restriction can be by-passed with **modifiers**) 2. The values returned by the callables are ignored +There is no longer a dedicated `SimpleCallablesProcessor`. This behaviour is now provided by `CallableAction` with `argumentsLimit: null`, which passes **every** argument unchanged to each callable. + #### Use case: Reporting/logging +Using the `Invokator` registry: + +```php +$invokator->action('log') + ->add('FileLogger@log', 0, null) // priority, then argumentsLimit: null + ->add('SlackNotification@send', 0, null) + ->add('TextNotification@send', 0, null); + +$invokator->action('log')->run($severity, $message, $context); +``` + +or standalone: + ```php use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\SimpleCallablesProcessor; +use Sirius\Invokator\Callables\CallableAction; $invoker = new Invoker($psr11Container); -$processor = new SimpleCallablesProcessor($invoker); -$processor->add('log', 'FileLogger@log') // this returns the Stack - ->add('SlackNotification@send') - ->add('TextNotification@send'); +$action = new CallableAction($invoker); +$action->add('FileLogger@log', 0, null) + ->add('SlackNotification@send', 0, null) + ->add('TextNotification@send', 0, null); -$processor->process('log', $severity, $message, $context); +$action->run($severity, $message, $context); ``` [Next: Pipelines](2_2_pipelines.md) diff --git a/docs/2_2_pipelines.md b/docs/2_2_pipelines.md index 95dc1de..0dcc4a3 100644 --- a/docs/2_2_pipelines.md +++ b/docs/2_2_pipelines.md @@ -4,7 +4,7 @@ title: Pipelines # Pipelines -This processor has the following characteristics: +A pipeline has the following characteristics: 1. The parameters are passed the first callable 2. The value returned by each callable is the first and only parameter passed to the next callable 3. All the callables are called in sequence @@ -12,18 +12,12 @@ This processor has the following characteristics: #### Use case ```php -use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\PipelineProcessor; - -$invoker = new Invoker($psr11Container); -$processor = new PipelineProcessor($invoker); - -$processor->get('tax_report') +$invokator->pipeline('tax_report') ->add('ImportCsv@taxReport') // this receives a DTO with a file and a user ID, imports it into a table and returns a DTO with the table name and user ID ->add('GenerateTaxReport@compileExcelFile') // this receives the DTO returned by the previous callable, returns a DTO with the name of the XLS file and user ID ->add('NotifyReportReady@notifyTaxReport'); // this receives the DTO from the previous callable and sends an email -$processor->process('tax_report', new TaxReportDTO('path_to_csv_file', 'user_id') ); +$invokator->pipeline('tax_report')->run(new TaxReportDTO('path_to_csv_file', 'user_id')); ``` The example above uses DTOs to pass messages from one step to the next because each callable in the pipeline receives only one argument and most likely there is some "global" data that each step should know about (eg: the client ID for that report) @@ -33,13 +27,13 @@ In some situations pipelines can be interrupted. There are 2 scenarios for this ###### 1. The callable that is executed knows it can be retried in the future -In this case it can return an instance of `SuggestedRetry` which instructs the pipeline processor to not execute the rest of the callables and return a `PipelinePromise()` +In this case it can return an instance of `SuggestedRetry` which instructs the pipeline to not execute the rest of the callables and return a `PipelinePromise()` ```php -$processor->get('sales_report') +$invokator->pipeline('sales_report') ->add('SalesReport@prepareData') ->add('SalesReport@generateReport') - ->add('Notification@sendSalesReport') + ->add('Notification@sendSalesReport'); ``` In this case the `SalesReport@generateReport` talks to a 3rd-party API which returns `429 Too Many Requests`. @@ -48,7 +42,7 @@ Sure, the same can be achieved by throwing an exception and re-trying the entire ###### 2. The callable that is executed knows the next step should be delayed -In this case it can return an instance of `SuggestedResume` which instructs the pipeline processor to not execute the rest of the callables and return a `PipelinePromise()` +In this case it can return an instance of `SuggestedResume` which instructs the pipeline to not execute the rest of the callables and return a `PipelinePromise()` The `PipelinePromise` object has a few properties: - the remaining callables in the pipeline diff --git a/docs/2_3_command_bus.md b/docs/2_3_command_bus.md index b9179b0..82e9e59 100644 --- a/docs/2_3_command_bus.md +++ b/docs/2_3_command_bus.md @@ -1,5 +1,5 @@ --- -title: PSR-14 Command bus implementation +title: Command bus implementation --- # Command bus implementation @@ -14,11 +14,11 @@ The implementation of this pattern in the `Sirius\Invokator` library has the fol 1. The `PurchaseProductCommand` command class is automatically linked to the `PurchaseProductHandler` in the same namespace. This happens unless you specify a handler via the `register()` method 2. The handler class has to implement the method `handle($command)` or `__invoke($command)` -3. The processing of the command can be extended via middlewares (the command bus is a special type of [middleware processor](2_3_middlewares.md)) +3. The processing of the command can be extended via middlewares — under the hood the bus routes the command through a [`CallableMiddleware`](2_3_middlewares.md) stack to the handler ```php use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\CommandBus; +use Sirius\Invokator\Callables\CommandBus; $invoker = new Invoker($psr11Container); $bus = new CommandBus($invoker); @@ -40,6 +40,20 @@ You can add middlewares at any point in time, before or after registering the co ```php $bus->addMiddleware(CreateProductCommand::class, 'CommandMiddleware@execute', 100 /* priority (optional) */); + +$bus->handle(new CreateProductCommand(/* ... */)); +``` + +#### Through the Invokator registry + +The same can be expressed with the `Sirius\Invokator\Invokator` class. `command()` returns a `CallableCommand` on which you add middleware, optionally set the handler, and run the command: + +```php +$invokator->command(CreateProductCommand::class) + ->add('CommandMiddleware@execute', 100) + ->handledBy(CreateProductHandler::class); + +$invokator->handle(new CreateProductCommand(/* ... */)); ``` [Next: Actions a la Wordpress](2_4_wordpress_actions.md) diff --git a/docs/2_3_event_dispatcher.md b/docs/2_3_event_dispatcher.md index a434f41..6b26bd8 100644 --- a/docs/2_3_event_dispatcher.md +++ b/docs/2_3_event_dispatcher.md @@ -28,6 +28,21 @@ $dispatcher->subscribeOnceTo(Event::class, 'some_callable', 0); $dispatcher->dispatch(new Event()); ``` +### Through the Invokator registry + +If you use the `Sirius\Invokator\Invokator` class, `event()` returns a `CallableEvent` that wraps the dispatcher and is the unified way to subscribe and dispatch: + +```php +// subscribe a listener (optionally a priority) +$invokator->event(Event::class)->add('some_callable'); +// subscribe a listener that runs only once +$invokator->event(Event::class)->once('some_callable'); + +// dispatch the event (these are equivalent) +$invokator->dispatch(new Event()); +$invokator->event(Event::class)->run(new Event()); +``` + ### Named events If you want to identify the events by something other than the class name you can make the event classes implement the `HasEventname` interface diff --git a/docs/2_3_middlewares.md b/docs/2_3_middlewares.md index 2316946..847d6dc 100644 --- a/docs/2_3_middlewares.md +++ b/docs/2_3_middlewares.md @@ -4,7 +4,7 @@ title: Middlewares # Middlewares -This processor has the following characteristics: +A middleware stack has the following characteristics: 1. All the parameters are passed down to each of the callables as which means all the callables should have the same signature (although this restriction can be by-passed with **modifiers**) 2. The second to the last callables receive a `$next` as their last parameter which is a callable that continues the calls from the collection 3. Each callable may call `$next` or not @@ -12,20 +12,14 @@ This processor has the following characteristics: #### Use case ```php -use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\MiddlewareProcessor; - -$invoker = new Invoker($psr11Container); -$processor = new MiddlewareProcessor($invoker); - -$processor->get('http_handler') +$invokator->middleware('http_handler') ->add('CsrfCheckMiddleware') ->add('TrimStringsMiddleware') ->add('AuthMiddleware') ->add('CacheMiddleware') ->add('RouterMiddleware'); -$processor->process('http_handler', new HttpRequest); +$invokator->middleware('http_handler')->run(new HttpRequest); ``` While this example is for HTTP middleware, it does not implement the [PSR-15 middleware specifications](https://www.php-fig.org/psr/psr-15/) as it does not enforce their respective signatures. It would be up to your app to enforce those restrictions diff --git a/docs/2_4_wordpress_actions.md b/docs/2_4_wordpress_actions.md index f3ba867..3a14371 100644 --- a/docs/2_4_wordpress_actions.md +++ b/docs/2_4_wordpress_actions.md @@ -4,30 +4,21 @@ title: Actions a la Wordpress # Actions (a la Wordpress) -This processor is similar to the "Simple callables processor" with the difference that you also have to specify a limit for the arguments passed to each callable. +A `CallableAction` is similar to the simple collection behaviour with the difference that you also specify, per callable, a limit for the arguments passed to it (the default being `1`, the Wordpress action convention). -This means that the callables do not have to have the same signature as for the SimpleCallables processor. This processor is just a convenience as the same result could be been achieved using the ['limit_arguments' modifier](3_callable_modifiers.md) +This means that the callables do not have to have the same signature. It is just a convenience as the same result could be been achieved using the ['limit_arguments' modifier](3_callable_modifiers.md). Passing `argumentsLimit: null` passes every argument unchanged (the old "simple collection" behaviour). #### Use case ```php -use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\ActionsProcessor; +$invokator->action('save_post') + ->add('validate_taxonomies', 0, 2) // callback, priority, number of arguments passed + ->add('validate_acf_fields', 1, 2) + ->add('check_permissions', 10, 1); -$invoker = new Invoker($psr11Container); -$processor = new ActionsProcessor($invoker); - -$processor->add('save_post', 'validate_taxonomies', 0, 2); // callback, priority, number of arguments passed -$processor->add('save_post', 'validate_acf_fields', 1, 2); -$processor->add('save_post', 'check_permissions', 10, 1); - -$processor->process('save_post', $postID, $wpPost, $update); +$invokator->action('save_post')->run($postID, $wpPost, $update); ``` -**Attention!** The processor's `get()` and `add()` method return the callables collection, so you can't chain callables with arguments limit. For example the code below doesn't work as you might expect -```php -$processor->add('save_post', 'validate_taxonomies', 0, 2) // this returns the callables collection - ->add('validate_acf_fields', 0, 2); // this won't place a limit on the arguments for the 'validate_acf_fields' function since the callables is returned by the first add() call -``` +Chaining works as expected: `add()` returns the action itself and each call keeps its own argument limit, so the example above limits `validate_taxonomies` and `validate_acf_fields` to 2 arguments and `check_permissions` to 1. [Next: Filters a la Wordpress](2_5_wordpress_filters.md) diff --git a/docs/2_5_wordpress_filters.md b/docs/2_5_wordpress_filters.md index 4ba6fe4..8bb0b49 100644 --- a/docs/2_5_wordpress_filters.md +++ b/docs/2_5_wordpress_filters.md @@ -4,30 +4,20 @@ title: Filters a la Wordpress # Filters (a la Wordpress) -This processor is similar to the "Pipeline processor" with the difference that the additional parameters passed to the `process()` method are also passed to the other callbacks. +A `CallableFilter` is similar to a pipeline with the difference that only the **first** argument is threaded through the callables while the additional parameters passed to `run()` are also passed (as context) to each callback. -Just like the "actions processor" you have to specify the number of arguments passed to the callbacks +Just like the action runner you specify, per callable, the number of arguments passed to it (the default being `1`). #### Use case ```php -use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\FiltersProcessor; +$invokator->filter('the_title') + ->add('add_category_name', 0, 2) // callback, priority, no of arguments passed + ->add('add_site_name', 0, 2); -$invoker = new Invoker($psr11Container); -$processor = new FiltersProcessor($invoker); - -$processor->add('the_title', 'add_category_name', 0, 2); // callback, priority, no of arguments passed -$processor->add('the_title', 'add_site_name', 0, 2); - -$processor->process('the_title', $postTitle, $postID); +$invokator->filter('the_title')->run($postTitle, $postID); ``` -**Attention!** The processor's `get()` and `add()` method return the Stack object, so you can't chain callables with arguments limit. For example the code below doesn't work as you might expect - -```php -$processor->add('the_title', 'add_category_name', 0, 2) // this returns the collection - ->add('add_site_name', 0, 2) // this won't place a limit on the arguments for the 'add_site_name' function since the callables is returned by the first add() call -``` +Chaining works as expected: `add()` returns the filter itself and each call keeps its own argument limit, so both `add_category_name` and `add_site_name` above receive 2 arguments. -[Next: Custom callable processors](2_6_custom_processors.md) +[Next: Custom callable stacks](2_6_custom_processors.md) diff --git a/docs/2_6_custom_processors.md b/docs/2_6_custom_processors.md index 4643554..7d1fd5a 100644 --- a/docs/2_6_custom_processors.md +++ b/docs/2_6_custom_processors.md @@ -1,15 +1,32 @@ --- -title: Custom callable processors +title: Custom callable stacks --- -# Custom callables processor +# Custom callable stacks -You can easily build your own custom processor by extending the `SimpleCallablesProcessor` or starting from scratch as the API for a callables processor is very simple. +You can easily build your own runner by extending `Sirius\Invokator\Callables\AbstractCallableStack` and implementing the `run(mixed ...$args): mixed` method. The base class gives you the `add()` method, the `$this->invoker` used to execute callables, and a protected `freshStack()` helper that returns a disposable clone of the registered callables (a `CallableCollection` you can drain with `extract()` without altering the runner). -If you extend the `SimpleCallablesProcessor` you only need to implement the `processCollection()` method. +```php +use Sirius\Invokator\Callables\AbstractCallableStack; + +class MyRunner extends AbstractCallableStack +{ + public function run(mixed ...$args): mixed + { + $stack = $this->freshStack(); + $result = null; + while (! $stack->isEmpty()) { + $callable = $stack->extract(); + $result = $this->invoker->invoke($callable, ...$args); + } + + return $result; + } +} +``` Here are some ideas: -1. pipelines where all the callbacks receive the same arguments and where the result of a callback becomes the first argument in the list. It would be similar to the "Filters processor" but without having to specify the limit for the arguments. -2. HTTP middleware implementation of the PSR-15 standard. It would be similar to the "Middlewares processor" but with the restriction that all the callables should have the same signature. +1. A pipeline where all the callbacks receive the same arguments and where the result of a callback becomes the first argument in the list. It would be similar to the `CallableFilter` but without having to specify the argument limit. +2. An HTTP middleware implementation of the PSR-15 standard. It would be similar to the `CallableMiddleware` but with the restriction that all the callables share the same signature. [Next: callable modifiers](3_callable_modifiers.md) diff --git a/docs/2_callable_processors.md b/docs/2_callable_processors.md index 61d71f9..a4ad834 100644 --- a/docs/2_callable_processors.md +++ b/docs/2_callable_processors.md @@ -1,21 +1,41 @@ --- -title: What are callable processors in Sirius\Invokator? +title: What are callable runners in Sirius\Invokator? --- -# The callable processors +# The callable runners (stacks) -Callable processors are objects that have 3 objectives: -1. To act as a registry for collections via `$processor->get('callables_identifier')` -2. To simplify adding callbacks to collections via `$processor->add('callables_identifier', $callable, $priority)` which is syntactic sugar for `$processor->get('callables_identifier')->add($callable, $priority)` -3. To process collections stored in the registry via `$processor->process('callables_identifier', $param_1, $param_2)` -4. To process callable collections constructed separately via `$processor->processCollection($previouslyConstructedCollection, $param_1, $param_2)` +A **runnable callable stack** owns a single collection of callables and knows how to execute it. Each runner has the same simple API: -The processors depend on the [invoker](4_the_invoker.md) to actually execute the callbacks. +1. `add($callable, $priority)` — register a callable; this returns the runner itself, so calls can be chained +2. `run(...$args)` — execute all the registered callables, the way that particular runner dictates + +The runners depend on the [invoker](4_the_invoker.md) to actually execute the callbacks. + +You can use a runner on its own: + +```php +use Sirius\Invokator\Callables\CallablePipeline; + +$pipeline = new CallablePipeline($invoker); +$pipeline->add('trim')->add('ucwords'); +$pipeline->run(' hello '); +``` + +or you can let the `Sirius\Invokator\Invokator` registry hold them by identifier, so the same runner can be defined in one place and executed somewhere else: + +```php +$invokator->pipeline('slug')->add(fn ($t) => trim($t))->add('strtolower'); +// later on, anywhere +$invokator->pipeline('slug')->run(' Hello '); +``` + +The `Sirius\Invokator` library comes with the following runners -The `Sirius\Invokator` library comes with 5 callable processors/runners 1. [simple collection](2_1_simple_collection.md) 2. [pipelines](2_2_pipelines.md) 3. [middlewares](2_3_middlewares.md) 4. [actions a la Wordpress](2_4_wordpress_actions.md) 5. [filters a la Wordpress](2_5_wordpress_filters.md) -6. [custom processors](2_6_custom_processors.md) +6. [custom runners](2_6_custom_processors.md) + +The old "simple collection" processor no longer exists as a separate class; its behaviour (every callable receives the same arguments, results ignored) is now provided by `CallableAction` with `argumentsLimit: null`. diff --git a/docs/3_callable_modifiers.md b/docs/3_callable_modifiers.md index 665886e..ca3c80a 100644 --- a/docs/3_callable_modifiers.md +++ b/docs/3_callable_modifiers.md @@ -19,23 +19,23 @@ This modifier will limit the number of arguments passed to the callables. If you ```php use function Sirius\Invokator\limit_arguments; use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\SimpleStackProcessor; +use Sirius\Invokator\Callables\CallableAction; $invoker = new Invoker($psr11Container); -$processor = new SimpleStackProcessor($invoker); +$action = new CallableAction($invoker); -$processor->get('callables_collection') - ->add(limit_arguments(function($param_1, $param2) { +// argumentsLimit: null so each modifier receives all the arguments +$action->add(limit_arguments(function($param_1, $param2) { return 'something'; - }, 2)) - ->add(limit_arguments('Service@method', 1)); + }, 2), 0, null) + ->add(limit_arguments('Service@method', 1), 0, null); -$processor->process('callables_collection', $param_1, $param_2, $param_3, $param_4); +$action->run($param_1, $param_2, $param_3, $param_4); ``` -Even though this processor will receive 4 arguments, the callables will only receive 2 and 1 arguments respectively. +Even though the runner will receive 4 arguments, the callables will only receive 2 and 1 arguments respectively. -This modifier is used by the [actions processor](2_4_wordpress_actions.md) and the [filters processor](2_5_wordpress_filters.md) +This modifier is used by the [actions runner](2_4_wordpress_actions.md) and the [filters runner](2_5_wordpress_filters.md) ## The "once" modifier @@ -47,13 +47,13 @@ It is useful for an events system where you want a particular listener to be exe ```php use function Sirius\Invokator\once; -$processor->get('callables_collection') - ->add(once(function($param_1, $param2) { - return $param_1 + $param2 - })); +$action = new CallableAction($invoker); +$action->add(once(function($param_1, $param2) { + return $param_1 + $param2; + }), 0, null); -$processor->process('callables_collection', 2, 3); // this returns 5 -$processor->process('callables_collection', 8, 7); // this STILL returns 5 +$action->run(2, 3); // this returns 5 +$action->run(8, 7); // this STILL returns 5 ``` ## The "wrap" modifier @@ -64,12 +64,12 @@ This can be used to override how the callable is actually being executed by pass ```php use function Sirius\Invokator\wrap; -$processor->get('callables_collection') - ->add(wrap('Service@method', function(callable $callable) use ($param_3, $param_4) { +$action = new CallableAction($invoker); +$action->add(wrap('Service@method', function(callable $callable) use ($param_3, $param_4) { return $callable($param_3, $param_4); - }, 2)); + }, 2), 0, null); -$processor->process('callables_collection', $param_1, $param_2); +$action->run($param_1, $param_2); ``` The `Service@method` function will actually receive $param_3 and $param_4 as arguments instead of $param_1 and $param_2 @@ -82,10 +82,10 @@ This modifier can be used when you have a callable that has a specific signature use function Sirius\Invokator\with_arguments; use function Sirius\Invokator\ref; use function Sirius\Invokator\arg; -$processor->get('callables_collection') - ->add(with_arguments('Service@method', [arg(0), 'value', ref('SomeClass'), arg(1)]); +$action = new CallableAction($invoker); +$action->add(with_arguments('Service@method', [arg(0), 'value', ref('SomeClass'), arg(1)]), 0, null); -$processor->process('callables_collection', $param_1, $param_2); +$action->run($param_1, $param_2); // ``` @@ -102,10 +102,10 @@ use function Sirius\Invokator\resolve; use function Sirius\Invokator\arg; // Service@method($param_1, SomeClass $param_2, $param_3) -$processor->get('collection') - ->add(resolve('Service@method', ['param_1' => arg(0), 'param_3' => 20]); +$action = new CallableAction($invoker); +$action->add(resolve('Service@method', ['param_1' => arg(0), 'param_3' => 20]), 0, null); -$processor->process('collection', 10); +$action->run(10); ``` This will call `Service@method(10, $container->get('SomeClass'), 20)` diff --git a/docs/6_automatic_middleware.md b/docs/6_automatic_middleware.md index 1203456..0c88ea7 100644 --- a/docs/6_automatic_middleware.md +++ b/docs/6_automatic_middleware.md @@ -22,40 +22,41 @@ declare(strict_types=1); namespace App\Services; -use Psr\Container\ContainerInterface;use Sirius\Invokator\InvalidCallableException; -use Sirius\Invokator\CallableCollection; +use Psr\Container\ContainerInterface; use Sirius\Invokator\Invoker as BaseInvoker; -use Sirius\Invokator\Processors\MiddlewareProcessor; +use Sirius\Invokator\Callables\CallableMiddleware; class Invoker extends BaseInvoker { - private MiddlewareProcessor $middlewareProcessor; - - public function __construct(ContainerInterface $container) { - parent::__construct($container); - $this->middlewareProcessor = new MiddlewareProcessor($this); - } - - public function addMiddleware(string $name, mixed $callable, int $priority = 0) { - $collection = $this->middlewareProcessor->get($name); - if ($collection->isEmpty()) { - // this adds the original callable as the last item in the callables - $collection->add($name, $name, PHP_INT_MIN); + /** @var array */ + private array $middlewares = []; + + public function addMiddleware(string $name, mixed $callable, int $priority = 0): void { + // lazily create a middleware stack for this name + if ( ! isset($this->middlewares[$name])) { + $this->middlewares[$name] = new CallableMiddleware($this); } - $this->middlewareProcessor->add($name, $callable, $priority); + $this->middlewares[$name]->add($callable, $priority); } - - public function invoke(mixed $callable,...$params) : mixed{ - $callables = $this->middlewareProcessor->get($name); - if ( ! $callables->isEmpty()) { - return $this->middlewareProcessor->processCollection($callables, ...$params); + + public function invoke(mixed $callable, ...$params): mixed { + // $callable is the name used to register the middleware (eg: "ListProducts@execute") + if (is_string($callable) && isset($this->middlewares[$callable])) { + // run a clone with the original callable appended last (the bus-style pattern): + // the registered middlewares run first and the last one calls into $callable + $stack = clone $this->middlewares[$callable]; + $stack->add($callable, PHP_INT_MIN); + + return $stack->run(...$params); } - - return parent::invoke($callable,$params); + + return parent::invoke($callable, ...$params); } } ``` +The execution method on a `CallableMiddleware` is `run()`; cloning the stack before appending the original callable keeps the registered middlewares reusable across calls. + The 3rd-party module or somewhere in your service providers you can do ```php @@ -63,8 +64,8 @@ use function Sirius\Invokator\with_arguments; class SomeServiceProvider { public function boot() { - $this->invoker->add('ListProducts@execute', with_arguments('CacheMiddleware::cache', arg(0), 10 * 60)); // cache for 10 minutes - $this->invoker->add('ListProducts@create', with_arguments('CacheMiddleware::forget', arg(0)); // cache for 10 minutes + $this->invoker->addMiddleware('ListProducts@execute', with_arguments('CacheMiddleware::cache', arg(0), 10 * 60)); // cache for 10 minutes + $this->invoker->addMiddleware('ListProducts@create', with_arguments('CacheMiddleware::forget', arg(0))); // forget on create } } ``` diff --git a/docs/7_laravel.md b/docs/7_laravel.md index 26c495f..7a18cdb 100644 --- a/docs/7_laravel.md +++ b/docs/7_laravel.md @@ -22,14 +22,16 @@ The package is auto-discovered, so there is nothing to register manually. Larave - the `Invokator` facade alias. The service provider injects Laravel's container into the `Invoker` as its PSR-11 container -(Laravel's container implements `Psr\Container\ContainerInterface`), and registers the -processors and the event dispatcher as **singletons** so that registrations made during -boot persist for the whole request. +(Laravel's container implements `Psr\Container\ContainerInterface`). It registers the +`Sirius\Invokator\Invokator` class as a **singleton** (aliased `invokator`) so that +registrations made during boot persist for the whole request, and binds the `Dispatcher` +and the `CommandBus` from it so they remain injectable on their own. ## Defining vs. running -Every pattern uses the same overloaded call: pass **only the identifier** to get a builder -you can `->add()` callables to, or pass **extra arguments** to run it. +The facade is the explicit core API: every pattern method returns the runnable. You +register callables on it with `->add()` (or in bulk by passing them to the method) and +execute it with `->run()`. ```php use Sirius\Invokator\Laravel\Facades\Invokator; @@ -39,17 +41,21 @@ Invokator::pipeline('process-order') ->add(fn ($order) => /* ... */ $order) ->add(OrderNormalizer::class . '@handle'); +// or define in bulk +Invokator::pipeline('process-order', fn ($order) => $order, OrderNormalizer::class . '@handle'); + // run -$result = Invokator::pipeline('process-order', $order); +$result = Invokator::pipeline('process-order')->run($order); ``` +> **Important:** extra arguments passed to `pipeline()`/`filter()`/`action()`/`middleware()` +> now **register** callables — they do **not** auto-run (this is a change from the old +> facade). Always execute with `->run(...)`. Running with no arguments is simply +> `Invokator::pipeline('id')->run()`. + `add()` accepts an optional priority (higher runs first) and, for actions and filters, an optional argument limit: `->add($callable, $priority = 0, $argumentsLimit = 1)`. -> Running a pattern with **no** arguments is not expressible through this overload, because -> `Invokator::pipeline('id')` returns the builder. For that rare case resolve the processor -> directly, e.g. `app(\Sirius\Invokator\Processors\PipelineProcessor::class)->process('id')`. - ## The `Invokator` facade ### Pipelines, actions, filters, middlewares @@ -59,21 +65,21 @@ optional argument limit: `->add($callable, $priority = 0, $argumentsLimit = 1)`. Invokator::pipeline('slugify') ->add(fn ($title) => trim($title)) ->add(fn ($title) => strtolower($title)); -Invokator::pipeline('slugify', ' Hello World '); // "hello world" +Invokator::pipeline('slugify')->run(' Hello World '); // "hello world" // Filter — transform a value (extra arguments are kept for every callable) Invokator::filter('price')->add(fn ($amount) => $amount * 1.2); -Invokator::filter('price', 100); // 120 +Invokator::filter('price')->run(100); // 120 // Action — run callables for their side effects; returns null Invokator::action('analytics')->add(fn ($user) => Analytics::track($user)); -Invokator::action('analytics', $user); +Invokator::action('analytics')->run($user); // Middleware — each callable receives the arguments plus a $next callback Invokator::middleware('http') ->add(fn ($request, $next) => $next($request)) ->add(fn ($request, $next) => /* terminal */ $request); -Invokator::middleware('http', $request); +Invokator::middleware('http')->run($request); ``` ### Events (PSR-14) @@ -94,23 +100,43 @@ Invokator::dispatch(new OrderPlaced($order)); Events that implement `Psr\EventDispatcher\StoppableEventInterface` (for instance via the `Sirius\Invokator\Event\Stoppable` trait) stop propagation as usual. +### Commands + +```php +Invokator::command(CreateProductCommand::class) + ->add('CommandMiddleware@execute', 100) + ->handledBy(CreateProductHandler::class); + +Invokator::handle(new CreateProductCommand(/* ... */)); +``` + ## Helper functions The same operations are available as global helpers, prefixed with `do_` to avoid clashing with Laravel's built-in `event()` and `action()` helpers. They are loaded by the service provider, so they only exist inside a Laravel application. +Unlike the facade, the `do_*` helpers keep the **WordPress-style "run on extra arguments"** +convention: called with **only the identifier** they return the runnable so you can define +callables on it; called **with arguments** they run it. + ```php +// define do_pipeline('process-order')->add(/* ... */); +// run do_pipeline('process-order', $order); -do_filter('price', 100); -do_action('analytics', $user); -do_middleware('http', $request); +do_filter('price', 100); // returns the filtered value +do_action('analytics', $user); // runs the side effects +do_middleware('http', $request); // runs the stack -do_event(new OrderPlaced($order)); +do_event(new OrderPlaced($order)); // dispatches the event ``` +> This asymmetry is intentional: the facade/core API is explicit (`->run()`), while the +> `do_*` helpers run on extra arguments (the WordPress convention) so the Blade directive +> and WP-style usage work naturally. + ## Blade Use the `@do_action` directive to run an action from a template (it emits nothing), and the diff --git a/docs/9_upgrading_to_3.md b/docs/9_upgrading_to_3.md new file mode 100644 index 0000000..a844f69 --- /dev/null +++ b/docs/9_upgrading_to_3.md @@ -0,0 +1,112 @@ +--- +title: Upgrading Sirius\Invokator from 2.x to 3.0 +--- + +# Upgrading to 3.0 + +Version 3.0 splits the old "processor" classes — which were both a registry of +name-keyed collections **and** the executor — into two clear responsibilities: +self-contained **runnable callable stacks** and a unified **registry**. + +## 1. The `Sirius\Invokator\Processors\*` classes are removed + +The whole `Sirius\Invokator\Processors` namespace is gone. Replace each processor +with the matching runnable from `Sirius\Invokator\Callables`: + +| Removed | Replacement | +| --- | --- | +| `PipelineProcessor` | `CallablePipeline` | +| `MiddlewareProcessor` | `CallableMiddleware` | +| `FiltersProcessor` | `CallableFilter` | +| `ActionsProcessor` | `CallableAction` | +| `SimpleCallablesProcessor` | `CallableAction` with `argumentsLimit: null` | +| `Processors\CommandBus` | `Callables\CommandBus` | + +A runnable **owns its own callables** and is executed with `run(...)`: + +```php +// before (2.x) +$processor = new PipelineProcessor($invoker); +$processor->add('slug', fn ($t) => trim($t)); +$processor->add('slug', 'strtolower'); +$processor->process('slug', ' Hello '); + +// after (3.0) +$pipeline = new CallablePipeline($invoker); +$pipeline->add(fn ($t) => trim($t))->add('strtolower'); +$pipeline->run(' Hello '); +``` + +## 2. `get()`, `process()` and `processCollection()` are gone + +There is no longer a name-keyed `process('id', ...)` / `get('id')->add(...)` API on +the runners. A runnable holds a single collection and you `add()` to it and `run()` +it directly. The interfaces `InvokatorInterface` and `CallablesRegistryInterface` +have been removed. + +## 3. The new `Sirius\Invokator\Invokator` registry + +If you still want to register stacks by identifier, use the new framework-agnostic +`Invokator` class, constructed from a single `Invoker`. Its `pipeline()`, +`middleware()`, `filter()`, `action()`, `event()` and `command()` methods return the +(cached-per-id) runnable, and trailing callables passed to them are registered in +bulk at the default priority. + +```php +$invokator = new Invokator(new Invoker($psr11Container)); + +// register in bulk +$invokator->pipeline('slug', fn ($t) => trim($t), 'strtolower'); +// register with a per-callable priority +$invokator->pipeline('slug')->add('SlugService@finish', 10); +// run +$invokator->pipeline('slug')->run(' Hello '); +``` + +It also exposes `dispatch()`/`handle()` convenience methods and `dispatcher()`/ +`commandBus()` accessors. + +## 4. The simple collection is now `CallableAction` + +`SimpleCallablesProcessor` (every callable gets the same arguments, results ignored) +is replaced by `CallableAction` with `argumentsLimit: null`: + +```php +// before (2.x) +$processor = new SimpleCallablesProcessor($invoker); +$processor->add('log', 'FileLogger@log'); +$processor->process('log', $severity, $message, $context); + +// after (3.0) +$action = new CallableAction($invoker); +$action->add('FileLogger@log', 0, null); // argumentsLimit: null => every argument is passed +$action->run($severity, $message, $context); +``` + +The default `argumentsLimit` of `1` gives the Wordpress action behaviour. + +## 5. Laravel + +The facade methods now **return the runnable**; you execute it with `->run(...)`. +Extra arguments passed to `pipeline()`/`filter()`/`action()`/`middleware()` now +**register** callables — they no longer auto-run. + +```php +// before (2.x): extra arguments ran the pattern +Invokator::filter('price', 100); // ran the filter + +// after (3.0): extra arguments register callables; you run explicitly +Invokator::filter('price')->add(fn ($a) => $a * 1.2); +Invokator::filter('price')->run(100); // 120 +``` + +The `do_*` helpers keep the WordPress-style "run on extra arguments" convention +(`do_filter('price', 100)` returns the filtered value, `do_action('tag', ...$args)` +runs the side effects, etc.), so the Blade directive and WP-style usage still work. + +`InvokatorManager` and `Laravel\Registrar` are removed in favour of the core +`Invokator`, which the service provider registers as a singleton. + +```bash +composer require siriusphp/invokator:^3.0 +``` diff --git a/docs/index.md b/docs/index.md index 81b3810..39c56e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,21 @@ In the case of **events**, an `event` object is passed through each callable in The Wordpress' **action hooks** are similar to events as the callables are not influenced by each other +Each of these patterns is a **self-contained runnable**: a `CallablePipeline`, `CallableMiddleware`, `CallableFilter` or `CallableAction` owns its own list of callables and is executed with a single `run(...)` call. The `Sirius\Invokator\Invokator` class is the unified entry point — a framework-agnostic registry that holds these runnables by identifier (and also exposes events and a command bus). + +```php +use Sirius\Invokator\Invokator; +use Sirius\Invokator\Invoker; + +$invokator = new Invokator(new Invoker($psr11Container)); + +// register a pipeline once (callables can be passed in bulk)... +$invokator->pipeline('slug', fn ($t) => trim($t), 'strtolower'); + +// ...and run it whenever you need it +$slug = $invokator->pipeline('slug')->run(' Hello '); // "hello" +``` + ### Install using Composer Sirius\Invokator is available on [Packagist](https://packagist.org/packages/siriusphp/invokator). To install it run diff --git a/src/Callables/AbstractCallableStack.php b/src/Callables/AbstractCallableStack.php new file mode 100644 index 0000000..aa36b13 --- /dev/null +++ b/src/Callables/AbstractCallableStack.php @@ -0,0 +1,51 @@ +callables = new CallableCollection(); + } + + public function add(mixed $callable, int $priority = 0): static + { + $this->callables->add($callable, $priority); + + return $this; + } + + /** + * Cloning a stack must not share its underlying queue with the original + * (the {@see CommandBus} relies on this). + */ + public function __clone(): void + { + $this->callables = clone $this->callables; + } + + /** + * A disposable copy of the registered callables, safe to consume via extract(). + */ + protected function freshStack(): CallableCollection + { + return clone $this->callables; + } +} diff --git a/src/Callables/CallableAction.php b/src/Callables/CallableAction.php new file mode 100644 index 0000000..e88daf0 --- /dev/null +++ b/src/Callables/CallableAction.php @@ -0,0 +1,40 @@ +freshStack(); + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); + + while ($nextCallable !== null) { + $this->invoker->invoke($nextCallable, ...$params); + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); + } + + return null; + } +} diff --git a/src/Callables/CallableCommand.php b/src/Callables/CallableCommand.php new file mode 100644 index 0000000..7e9ba7c --- /dev/null +++ b/src/Callables/CallableCommand.php @@ -0,0 +1,37 @@ +bus->addMiddleware($this->commandClass, $middleware, $priority); + + return $this; + } + + public function handledBy(mixed $handler): static + { + $this->bus->register($this->commandClass, $handler); + + return $this; + } + + public function run(object $command): mixed + { + return $this->bus->handle($command); + } +} diff --git a/src/Callables/CallableEvent.php b/src/Callables/CallableEvent.php new file mode 100644 index 0000000..d923dfe --- /dev/null +++ b/src/Callables/CallableEvent.php @@ -0,0 +1,39 @@ +dispatcher->subscribeTo($this->eventName, $listener, $priority); + + return $this; + } + + public function once(mixed $listener, int $priority = 0): static + { + $this->dispatcher->subscribeOnceTo($this->eventName, $listener, $priority); + + return $this; + } + + public function run(object $event): object + { + return $this->dispatcher->dispatch($event); + } +} diff --git a/src/Callables/CallableFilter.php b/src/Callables/CallableFilter.php new file mode 100644 index 0000000..92ef835 --- /dev/null +++ b/src/Callables/CallableFilter.php @@ -0,0 +1,43 @@ +freshStack(); + $result = null; + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); + + while ($nextCallable !== null) { + $result = $this->invoker->invoke($nextCallable, ...$params); + $params[0] = $result; + + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); + } + + return $result; + } +} diff --git a/src/Callables/CallableMiddleware.php b/src/Callables/CallableMiddleware.php new file mode 100644 index 0000000..7c5935b --- /dev/null +++ b/src/Callables/CallableMiddleware.php @@ -0,0 +1,41 @@ +runStack($this->freshStack(), ...$params); + } + + private function runStack(CallableCollection $stack, mixed ...$params): mixed + { + $result = null; + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); + + while ($nextCallable !== null) { + if ($stack->isEmpty()) { + $response = $this->invoker->invoke($nextCallable, ...$params); + } else { + $next = fn ($result = null): mixed => $this->runStack($stack, ...$params); + $paramsForNext = [...$params, $next]; + $response = $this->invoker->invoke($nextCallable, ...$paramsForNext); + } + + $result = $response; + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); + } + + return $result; + } +} diff --git a/src/Processors/PipelineProcessor.php b/src/Callables/CallablePipeline.php similarity index 56% rename from src/Processors/PipelineProcessor.php rename to src/Callables/CallablePipeline.php index 23b3081..d6e80c2 100644 --- a/src/Processors/PipelineProcessor.php +++ b/src/Callables/CallablePipeline.php @@ -2,54 +2,47 @@ declare(strict_types=1); -namespace Sirius\Invokator\Processors; +namespace Sirius\Invokator\Callables; -use Sirius\Invokator\CallableCollection; use Sirius\Invokator\PipelinePromise; use Sirius\Invokator\SuggestedResume; use Sirius\Invokator\SuggestedRetry; -class PipelineProcessor extends SimpleCallablesProcessor +/** + * Runs callables in sequence, passing the result of one as the (single) argument of the next. + * The value returned by the last callable is the result of the pipeline. + * + * A callable may return a {@see SuggestedRetry} (retry itself later) or a {@see SuggestedResume} + * (continue the remaining callables later), in which case run() returns a {@see PipelinePromise} + * describing the deferred continuation. + */ +class CallablePipeline extends AbstractCallableStack { - /** - * @param array $params - */ - - #[\Override] - public function processCollection(CallableCollection $stack, ...$params): mixed + public function run(mixed ...$params): mixed { + $stack = $this->freshStack(); $result = null; - $nextCallable = $stack->extract(); + $nextCallable = $stack->isEmpty() ? null : $stack->extract(); while ($nextCallable !== null) { $result = $this->invoker->invoke($nextCallable, ...$params); // SuggestedRetry is returned when $nextCallable fails during processing - // but it knows how that it might work in the future + // but it knows that it might work in the future. if ($result instanceof SuggestedRetry) { $stack->add($nextCallable, PHP_INT_MIN); return new PipelinePromise($params[0], $stack, $params, $result->retryAfter); } - // SuggestedResume is returned when $nextCallable was succesful but + // SuggestedResume is returned when $nextCallable was successful but // knows the continuation of the pipeline should happen with a delay. if ($result instanceof SuggestedResume) { return new PipelinePromise($result, $stack, $params, $result->delay); } - $params = [$result]; - + $params = [$result]; $nextCallable = $stack->isEmpty() ? null : $stack->extract(); } return $result; } - - /** - * @param array $params - */ - public function resumeStack(CallableCollection $remainingStack, mixed $previousValue, ...$params): mixed - { - return null; - } - } diff --git a/src/Callables/CommandBus.php b/src/Callables/CommandBus.php new file mode 100644 index 0000000..7ea7873 --- /dev/null +++ b/src/Callables/CommandBus.php @@ -0,0 +1,79 @@ + `FooHandler`, + * using its `handle`/`__invoke` method). + */ +class CommandBus +{ + /** + * @var array + */ + protected array $middlewares = []; + + /** + * Handlers are stored separately so they don't have to be defined before the middleware; + * the relevant one is appended to the stack right before the command is handled. + * + * @var array + */ + protected array $handlers = []; + + public function __construct(public Invoker $invoker) + { + } + + public function register(string $commandClass, mixed $commandHandler): void + { + $this->handlers[$commandClass] = $commandHandler; + } + + public function addMiddleware(string $name, mixed $callable, int $priority = 0): void + { + ($this->middlewares[$name] ??= new CallableMiddleware($this->invoker))->add($callable, $priority); + } + + public function handle(object $command): mixed + { + // Work on a copy so the registered middleware stack is not mutated by the appended handler. + $stack = isset($this->middlewares[$command::class]) + ? clone $this->middlewares[$command::class] + : new CallableMiddleware($this->invoker); + + $stack->add($this->getCallableForCommand($command), PHP_INT_MIN); // executed last + + return $stack->run($command); + } + + protected function getCallableForCommand(object $commandInstance): mixed + { + $commandClass = $commandInstance::class; + $handler = $this->handlers[$commandClass] ?? preg_replace('/(.+)Command$/', '$1Handler', $commandClass); + + if (is_string($handler)) { + if (! class_exists($handler)) { + // maybe the handler is something like `SomeClass::method` or `SomeClass@method` + return $handler; + } + if (method_exists($handler, 'handle')) { + return $handler . '@handle'; + } + if (method_exists($handler, '__invoke')) { + return $handler . '@__invoke'; + } + throw new \RuntimeException('Unable to determine the command handler'); + } + + return $handler; + } +} diff --git a/src/CallablesRegistryInterface.php b/src/CallablesRegistryInterface.php deleted file mode 100644 index 12c4ce8..0000000 --- a/src/CallablesRegistryInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -pipeline('slug', 'trim', 'strtolower'); // bulk register + * $invokator->pipeline('slug')->add(fn ($t) => "$t!", 10); // per-callable priority + * $invokator->pipeline('slug')->run('Hello'); // run + * + * Stack patterns (pipeline/middleware/filter/action) are cached per identifier because their + * state lives in the returned object. Event and command handles are thin wrappers over the + * shared {@see Dispatcher} / {@see CommandBus}, so a fresh one is handed out on each call. + */ +final class Invokator +{ + /** + * @var array + */ + private array $pipelines = []; + + /** + * @var array + */ + private array $middlewares = []; + + /** + * @var array + */ + private array $filters = []; + + /** + * @var array + */ + private array $actions = []; + + private readonly Dispatcher $dispatcher; + + private readonly CommandBus $commandBus; + + public function __construct( + public readonly Invoker $invoker, + ?Dispatcher $dispatcher = null, + ?CommandBus $commandBus = null, + ) { + $this->dispatcher = $dispatcher ?? new Dispatcher(new ListenerProvider(), $invoker); + $this->commandBus = $commandBus ?? new CommandBus($invoker); + } + + public function pipeline(string $id, mixed ...$callables): CallablePipeline + { + $pipeline = $this->pipelines[$id] ??= new CallablePipeline($this->invoker); + foreach ($callables as $callable) { + $pipeline->add($callable); + } + + return $pipeline; + } + + public function middleware(string $id, mixed ...$callables): CallableMiddleware + { + $middleware = $this->middlewares[$id] ??= new CallableMiddleware($this->invoker); + foreach ($callables as $callable) { + $middleware->add($callable); + } + + return $middleware; + } + + public function filter(string $id, mixed ...$callables): CallableFilter + { + $filter = $this->filters[$id] ??= new CallableFilter($this->invoker); + foreach ($callables as $callable) { + $filter->add($callable); + } + + return $filter; + } + + public function action(string $id, mixed ...$callables): CallableAction + { + $action = $this->actions[$id] ??= new CallableAction($this->invoker); + foreach ($callables as $callable) { + $action->add($callable); + } + + return $action; + } + + /** + * Subscribe listeners to an event. The event name is the event's class name (or the value + * returned by HasEventName::getEventName()), so reference it as `Event::class`. + */ + public function event(string $eventName, mixed ...$listeners): CallableEvent + { + $event = new CallableEvent($this->dispatcher, $eventName); + foreach ($listeners as $listener) { + $event->add($listener); + } + + return $event; + } + + public function command(string $commandClass, mixed ...$middleware): CallableCommand + { + $command = new CallableCommand($this->commandBus, $commandClass); + foreach ($middleware as $callable) { + $command->add($callable); + } + + return $command; + } + + /** + * Dispatch a PSR-14 event object to its subscribed listeners. + */ + public function dispatch(object $event): object + { + return $this->dispatcher->dispatch($event); + } + + /** + * Send a command object through its middleware to its handler. + */ + public function handle(object $command): mixed + { + return $this->commandBus->handle($command); + } + + public function dispatcher(): Dispatcher + { + return $this->dispatcher; + } + + public function commandBus(): CommandBus + { + return $this->commandBus; + } +} diff --git a/src/InvokatorInterface.php b/src/InvokatorInterface.php deleted file mode 100644 index 63d8f34..0000000 --- a/src/InvokatorInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - $params - */ - public function process(string $name, ...$params): mixed; - - /** - * @param array $params - */ - public function processCollection(CallableCollection $stack, ...$params): mixed; -} diff --git a/src/Laravel/Facades/Invokator.php b/src/Laravel/Facades/Invokator.php index 7579bd1..a916d8c 100644 --- a/src/Laravel/Facades/Invokator.php +++ b/src/Laravel/Facades/Invokator.php @@ -5,17 +5,18 @@ namespace Sirius\Invokator\Laravel\Facades; use Illuminate\Support\Facades\Facade; -use Sirius\Invokator\Laravel\InvokatorManager; /** - * @method static mixed pipeline(string $id, mixed ...$args) - * @method static mixed action(string $id, mixed ...$args) - * @method static mixed filter(string $id, mixed ...$args) - * @method static mixed middleware(string $id, mixed ...$args) - * @method static \Sirius\Invokator\Laravel\Registrar event(string $eventName) + * @method static \Sirius\Invokator\Callables\CallablePipeline pipeline(string $id, mixed ...$callables) + * @method static \Sirius\Invokator\Callables\CallableAction action(string $id, mixed ...$callables) + * @method static \Sirius\Invokator\Callables\CallableFilter filter(string $id, mixed ...$callables) + * @method static \Sirius\Invokator\Callables\CallableMiddleware middleware(string $id, mixed ...$callables) + * @method static \Sirius\Invokator\Callables\CallableEvent event(string $eventName, mixed ...$listeners) + * @method static \Sirius\Invokator\Callables\CallableCommand command(string $commandClass, mixed ...$middleware) * @method static object dispatch(object $event) + * @method static mixed handle(object $command) * - * @see InvokatorManager + * @see \Sirius\Invokator\Invokator */ class Invokator extends Facade { diff --git a/src/Laravel/InvokatorManager.php b/src/Laravel/InvokatorManager.php deleted file mode 100644 index 377d490..0000000 --- a/src/Laravel/InvokatorManager.php +++ /dev/null @@ -1,84 +0,0 @@ - $this->pipelines->add($id, $cb, $priority)); - } - - return $this->pipelines->process($id, ...$args); - } - - public function action(string $id, mixed ...$args): mixed - { - if ($args === []) { - return new Registrar(fn (mixed $cb, int $priority, int $limit): CallableCollection => $this->actions->add($id, $cb, $priority, $limit)); - } - - return $this->actions->process($id, ...$args); - } - - public function filter(string $id, mixed ...$args): mixed - { - if ($args === []) { - return new Registrar(fn (mixed $cb, int $priority, int $limit): CallableCollection => $this->filters->add($id, $cb, $priority, $limit)); - } - - return $this->filters->process($id, ...$args); - } - - public function middleware(string $id, mixed ...$args): mixed - { - if ($args === []) { - return new Registrar(fn (mixed $cb, int $priority, int $limit): CallableCollection => $this->middlewares->add($id, $cb, $priority)); - } - - return $this->middlewares->process($id, ...$args); - } - - /** - * Subscribe listeners to an event. The event name is the event's class name (or the - * value returned by HasEventName::getEventName()), so subscribe to `Event::class`. - */ - public function event(string $eventName): Registrar - { - return new Registrar(fn (mixed $cb, int $priority, int $limit) => $this->dispatcher->subscribeTo($eventName, $cb, $priority)); - } - - /** - * Dispatch a PSR-14 event object to its subscribed listeners. - */ - public function dispatch(object $event): object - { - return $this->dispatcher->dispatch($event); - } -} diff --git a/src/Laravel/Registrar.php b/src/Laravel/Registrar.php deleted file mode 100644 index 7e00db8..0000000 --- a/src/Laravel/Registrar.php +++ /dev/null @@ -1,31 +0,0 @@ -add(...)`). - * - * Every `add()` routes through the closure provided by the manager, which calls the - * underlying processor's own `add()` method. This matters for actions and filters whose - * `add()` wraps the callable with `limit_arguments(...)` — a behaviour that would be lost - * if we returned and mutated a raw CallableCollection instead. - */ -final class Registrar -{ - /** - * @param \Closure(mixed, int, int): mixed $adder - */ - public function __construct(private \Closure $adder) - { - } - - public function add(mixed $callable, int $priority = 0, int $argumentsLimit = 1): self - { - ($this->adder)($callable, $priority, $argumentsLimit); - - return $this; - } -} diff --git a/src/Laravel/SiriusInvokatorServiceProvider.php b/src/Laravel/SiriusInvokatorServiceProvider.php index cce3e67..4574e06 100644 --- a/src/Laravel/SiriusInvokatorServiceProvider.php +++ b/src/Laravel/SiriusInvokatorServiceProvider.php @@ -6,13 +6,10 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; +use Sirius\Invokator\Callables\CommandBus; use Sirius\Invokator\Event\Dispatcher; -use Sirius\Invokator\Event\ListenerProvider; +use Sirius\Invokator\Invokator; use Sirius\Invokator\Invoker; -use Sirius\Invokator\Processors\ActionsProcessor; -use Sirius\Invokator\Processors\FiltersProcessor; -use Sirius\Invokator\Processors\MiddlewareProcessor; -use Sirius\Invokator\Processors\PipelineProcessor; class SiriusInvokatorServiceProvider extends ServiceProvider { @@ -23,24 +20,13 @@ public function register(): void // injected straight into the Invoker as its PSR container. $this->app->singleton(Invoker::class, fn ($app): Invoker => new Invoker($app)); - foreach ([PipelineProcessor::class, ActionsProcessor::class, FiltersProcessor::class, MiddlewareProcessor::class] as $processor) { - $this->app->singleton($processor, fn ($app): PipelineProcessor|\Sirius\Invokator\Processors\ActionsProcessor|\Sirius\Invokator\Processors\FiltersProcessor|\Sirius\Invokator\Processors\MiddlewareProcessor => new $processor($app->make(Invoker::class))); - } - - $this->app->singleton(ListenerProvider::class); - $this->app->singleton(Dispatcher::class, fn ($app): Dispatcher => new Dispatcher( - $app->make(ListenerProvider::class), - $app->make(Invoker::class), - )); - - $this->app->singleton(InvokatorManager::class, fn ($app): InvokatorManager => new InvokatorManager( - $app->make(PipelineProcessor::class), - $app->make(ActionsProcessor::class), - $app->make(FiltersProcessor::class), - $app->make(MiddlewareProcessor::class), - $app->make(Dispatcher::class), - )); - $this->app->alias(InvokatorManager::class, 'invokator'); + $this->app->singleton(Invokator::class, fn ($app): Invokator => new Invokator($app->make(Invoker::class))); + $this->app->alias(Invokator::class, 'invokator'); + + // Expose the PSR-14 dispatcher and the command bus owned by the Invokator so they + // remain injectable on their own. + $this->app->singleton(Dispatcher::class, fn ($app): Dispatcher => $app->make(Invokator::class)->dispatcher()); + $this->app->singleton(CommandBus::class, fn ($app): CommandBus => $app->make(Invokator::class)->commandBus()); require_once __DIR__ . '/helpers.php'; } diff --git a/src/Laravel/helpers.php b/src/Laravel/helpers.php index 176572a..e87535f 100644 --- a/src/Laravel/helpers.php +++ b/src/Laravel/helpers.php @@ -2,47 +2,57 @@ declare(strict_types=1); -use Sirius\Invokator\Laravel\InvokatorManager; -use Sirius\Invokator\Laravel\Registrar; +use Sirius\Invokator\Invokator; if (! function_exists('do_pipeline')) { /** - * Define a pipeline (identifier only, returns a {@see Registrar}) or run it (with args). + * Reference a pipeline (identifier only, returns the pipeline to define callables on) or + * run it (with args), returning the pipeline's result. */ function do_pipeline(string $id, mixed ...$args): mixed { - return app(InvokatorManager::class)->pipeline($id, ...$args); + $pipeline = app(Invokator::class)->pipeline($id); + + return $args === [] ? $pipeline : $pipeline->run(...$args); } } if (! function_exists('do_action')) { /** - * Define an action (identifier only, returns a {@see Registrar}) or run it (with args). + * Reference an action (identifier only, returns the action to define callables on) or + * run it (with args) for its side effects. */ function do_action(string $id, mixed ...$args): mixed { - return app(InvokatorManager::class)->action($id, ...$args); + $action = app(Invokator::class)->action($id); + + return $args === [] ? $action : $action->run(...$args); } } if (! function_exists('do_filter')) { /** - * Define a filter (identifier only, returns a {@see Registrar}) or run it (with args), - * returning the filtered value. + * Reference a filter (identifier only, returns the filter to define callables on) or + * run it (with args), returning the filtered value. */ function do_filter(string $id, mixed ...$args): mixed { - return app(InvokatorManager::class)->filter($id, ...$args); + $filter = app(Invokator::class)->filter($id); + + return $args === [] ? $filter : $filter->run(...$args); } } if (! function_exists('do_middleware')) { /** - * Define a middleware stack (identifier only, returns a {@see Registrar}) or run it (with args). + * Reference a middleware stack (identifier only, returns the stack to define callables on) + * or run it (with args). */ function do_middleware(string $id, mixed ...$args): mixed { - return app(InvokatorManager::class)->middleware($id, ...$args); + $middleware = app(Invokator::class)->middleware($id); + + return $args === [] ? $middleware : $middleware->run(...$args); } } @@ -52,6 +62,6 @@ function do_middleware(string $id, mixed ...$args): mixed */ function do_event(object $event): object { - return app(InvokatorManager::class)->dispatch($event); + return app(Invokator::class)->dispatch($event); } } diff --git a/src/Processors/ActionsProcessor.php b/src/Processors/ActionsProcessor.php deleted file mode 100644 index ee82467..0000000 --- a/src/Processors/ActionsProcessor.php +++ /dev/null @@ -1,73 +0,0 @@ - - */ - protected array $registry = []; - - public function __construct(public Invoker $invoker) - { - } - - public function get(string $name): CallableCollection - { - if (! isset($this->registry[$name])) { - $this->registry[$name] = $this->newStack(); - } - - return $this->registry[$name]; - } - - public function add(string $name, mixed $callable, int $priority = 0, int $argumentsLimit = 1): CallableCollection - { - return $this->get($name)->add(limit_arguments($callable, $argumentsLimit), $priority); - } - - protected function newStack(): CallableCollection - { - return new CallableCollection(); - } - - protected function getCopy(string $name): CallableCollection - { - return clone($this->get($name)); - } - - /** - * @param array $params - */ - public function process(string $name, ...$params): mixed - { - return $this->processCollection($this->getCopy($name), ...$params); - } - - /** - * @param array $params - */ - public function processCollection(CallableCollection $stack, ...$params): mixed - { - $nextCallable = $stack->extract(); - - while ($nextCallable !== null) { - $this->invoker->invoke($nextCallable, ...$params); - $nextCallable = $stack->isEmpty() ? null : $stack->extract(); - } - - return null; - } - - -} diff --git a/src/Processors/CommandBus.php b/src/Processors/CommandBus.php deleted file mode 100644 index 7de4989..0000000 --- a/src/Processors/CommandBus.php +++ /dev/null @@ -1,78 +0,0 @@ - $handlers - */ - protected array $handlers = []; - - public function register(string $commandClass, mixed $commandHandler): void - { - $this->handlers[$commandClass] = $commandHandler; - } - - public function addMiddleware(string $name, mixed $callable, int $priority = 0): void - { - parent::add($name, $callable, $priority); - } - - public function handle(object $command): mixed - { - $callableCollection = $this->getCopy($command::class); - $callableCollection->add($this->getCallableForCommand($command), PHP_INT_MIN); // to be executed at the end - - $result = null; - $nextCallable = $callableCollection->extract(); - while ($nextCallable !== null) { - if ($callableCollection->isEmpty()) { - $response = $this->invoker->invoke($nextCallable, $command); - } else { - $next = fn ($result): mixed => $this->processCollection($callableCollection, $command); - $response = $this->invoker->invoke($nextCallable, $command, $next); - } - - $result = $response; - - $nextCallable = $callableCollection->isEmpty() ? null : $callableCollection->extract(); - } - - return $result; - } - - #[\Override] - public function process(string $name, ...$params): mixed - { - throw new \BadMethodCallException('You should not call the process() method on the command bus. Use handle() instead!'); - } - - protected function getCallableForCommand(object $commandInstance): mixed - { - $commandClass = $commandInstance::class; - $handler = $this->handlers[$commandClass] ?? preg_replace('/(.+)Command$/', '$1Handler', $commandClass); - - if (is_string($handler)) { - if (! class_exists($handler)) { - // maybe the handler is something like `SomeClass::method` or `SomeClass@method` - return $handler; - } - if (method_exists($handler, 'handle')) { - return $handler . '@handle'; - } - if (method_exists($handler, '__invoke')) { - return $handler . '@__invoke'; - } - throw new \RuntimeException('Unable to determine the command handler'); - } - - return $handler; - } -} diff --git a/src/Processors/FiltersProcessor.php b/src/Processors/FiltersProcessor.php deleted file mode 100644 index d836325..0000000 --- a/src/Processors/FiltersProcessor.php +++ /dev/null @@ -1,75 +0,0 @@ - - */ - protected array $registry = []; - - public function __construct(public Invoker $invoker) - { - } - - public function get(string $name): CallableCollection - { - if (! isset($this->registry[$name])) { - $this->registry[$name] = $this->newStack(); - } - - return $this->registry[$name]; - } - - public function add(string $name, mixed $callable, int $priority = 0, int $argumentsLimit = 1): CallableCollection - { - return $this->get($name)->add(limit_arguments($callable, $argumentsLimit), $priority); - } - - protected function newStack(): CallableCollection - { - return new CallableCollection(); - } - - protected function getCopy(string $name): CallableCollection - { - return clone($this->get($name)); - } - - /** - * @param array $params - */ - public function process(string $name, ...$params): mixed - { - return $this->processCollection($this->getCopy($name), ...$params); - } - - /** - * @param array $params - */ - public function processCollection(CallableCollection $stack, ...$params): mixed - { - $result = null; - $nextCallable = $stack->extract(); - - while ($nextCallable !== null) { - $result = $this->invoker->invoke($nextCallable, ...$params); - - $params[0] = $result; - - $nextCallable = $stack->isEmpty() ? null : $stack->extract(); - } - - return $result; - } -} diff --git a/src/Processors/MiddlewareProcessor.php b/src/Processors/MiddlewareProcessor.php deleted file mode 100644 index f4193ba..0000000 --- a/src/Processors/MiddlewareProcessor.php +++ /dev/null @@ -1,44 +0,0 @@ - $params - * - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - * @throws InvalidCallableException - */ - #[\Override] - public function processCollection(CallableCollection $stack, ...$params): mixed - { - $result = null; - $nextCallable = $stack->extract(); - - while ($nextCallable !== null) { - if ($stack->isEmpty()) { - $response = $this->invoker->invoke($nextCallable, ...$params); - } else { - $next = fn ($result): mixed => $this->processCollection($stack, ...$params); - $paramsForNext = [...$params, $next]; - $response = $this->invoker->invoke($nextCallable, ...$paramsForNext); - } - - $result = $response; - - $nextCallable = $stack->isEmpty() ? null : $stack->extract(); - } - - return $result; - } - -} diff --git a/src/Processors/SimpleCallablesProcessor.php b/src/Processors/SimpleCallablesProcessor.php deleted file mode 100644 index fd5372c..0000000 --- a/src/Processors/SimpleCallablesProcessor.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ - protected array $registry = []; - - public function __construct(public Invoker $invoker) - { - } - - public function get(string $name): CallableCollection - { - if (! isset($this->registry[$name])) { - $this->registry[$name] = $this->newStack(); - } - - return $this->registry[$name]; - } - - public function add(string $name, mixed $callable, int $priority = 0): CallableCollection - { - return $this->get($name)->add($callable, $priority); - } - - protected function newStack(): CallableCollection - { - return new CallableCollection(); - } - - protected function getCopy(string $name): CallableCollection - { - return clone($this->get($name)); - } - - /** - * @param array $params - */ - public function process(string $name, ...$params): mixed - { - return $this->processCollection($this->getCopy($name), ...$params); - } - - /** - * @param array $params - */ - public function processCollection(CallableCollection $stack, ...$params): mixed - { - $nextCallable = $stack->extract(); - while ($nextCallable !== null) { - $this->invoker->invoke($nextCallable, ...$params); - $nextCallable = $stack->isEmpty() ? null : $stack->extract(); - } - - return null; - } - - -} diff --git a/src/RunnableCallables.php b/src/RunnableCallables.php new file mode 100644 index 0000000..d5ad327 --- /dev/null +++ b/src/RunnableCallables.php @@ -0,0 +1,17 @@ +add(Greeter::class . '@greet'); - $this->assertSame('Hello, Sam', Invokator::filter('greet', 'Sam')); + $this->assertSame('Hello, Sam', Invokator::filter('greet')->run('Sam')); } public function test_plain_function_name_string_callable_works_under_the_laravel_container(): void @@ -25,6 +25,6 @@ public function test_plain_function_name_string_callable_works_under_the_laravel // 'trim' directly rather than asking the container to resolve it. Invokator::filter('clean')->add('trim'); - $this->assertSame('hi', Invokator::filter('clean', ' hi ')); + $this->assertSame('hi', Invokator::filter('clean')->run(' hi ')); } } diff --git a/tests/Laravel/FacadeTest.php b/tests/Laravel/FacadeTest.php index 31f6d2d..b0ab055 100644 --- a/tests/Laravel/FacadeTest.php +++ b/tests/Laravel/FacadeTest.php @@ -4,26 +4,33 @@ namespace Sirius\Invokator\Tests\Laravel; +use Sirius\Invokator\Callables\CallablePipeline; use Sirius\Invokator\Laravel\Facades\Invokator; -use Sirius\Invokator\Laravel\Registrar; class FacadeTest extends TestCase { - public function test_pipeline_define_returns_registrar_and_run_chains_results(): void + public function test_pipeline_define_returns_the_pipeline_and_run_chains_results(): void { - $registrar = Invokator::pipeline('p') + $pipeline = Invokator::pipeline('p') ->add(fn ($x): string => $x . 'a') ->add(fn ($x): string => $x . 'b'); - $this->assertInstanceOf(Registrar::class, $registrar); - $this->assertSame('xab', Invokator::pipeline('p', 'x')); + $this->assertInstanceOf(CallablePipeline::class, $pipeline); + $this->assertSame('xab', Invokator::pipeline('p')->run('x')); + } + + public function test_bulk_registration_through_the_facade(): void + { + Invokator::pipeline('slug', fn ($t): string => trim((string) $t), 'strtolower'); + + $this->assertSame('hello', Invokator::pipeline('slug')->run(' HELLO ')); } public function test_filter_transforms_the_value(): void { Invokator::filter('up')->add(fn ($v) => strtoupper((string) $v)); - $this->assertSame('HELLO', Invokator::filter('up', 'hello')); + $this->assertSame('HELLO', Invokator::filter('up')->run('hello')); } public function test_action_runs_for_side_effects_and_returns_null(): void @@ -33,7 +40,7 @@ public function test_action_runs_for_side_effects_and_returns_null(): void $log[] = "got:$x"; }); - $result = Invokator::action('log', 'hi'); + $result = Invokator::action('log')->run('hi'); $this->assertNull($result); $this->assertSame(['got:hi'], $log); @@ -46,7 +53,7 @@ public function test_middleware_wraps_with_next(): void ->add(fn ($name, $next = null): string => 'Hello ' . $next($name)) ->add(fn ($name, $next = null) => $name); - $this->assertSame('HELLO WORLD', Invokator::middleware('m', 'world')); + $this->assertSame('HELLO WORLD', Invokator::middleware('m')->run('world')); } public function test_priority_controls_execution_order(): void @@ -56,6 +63,6 @@ public function test_priority_controls_execution_order(): void ->add(fn ($x): string => $x . '-high', 10); // higher priority runs first - $this->assertSame('start-high-low', Invokator::pipeline('ordered', 'start')); + $this->assertSame('start-high-low', Invokator::pipeline('ordered')->run('start')); } } diff --git a/tests/Laravel/HelpersTest.php b/tests/Laravel/HelpersTest.php index a6d9d13..049b8f0 100644 --- a/tests/Laravel/HelpersTest.php +++ b/tests/Laravel/HelpersTest.php @@ -4,17 +4,17 @@ namespace Sirius\Invokator\Tests\Laravel; -use Sirius\Invokator\Laravel\Registrar; +use Sirius\Invokator\Callables\CallablePipeline; class HelpersTest extends TestCase { public function test_do_pipeline_defines_and_runs(): void { - $registrar = do_pipeline('p') + $pipeline = do_pipeline('p') ->add(fn ($x): string => $x . 'a') ->add(fn ($x): string => $x . 'b'); - $this->assertInstanceOf(Registrar::class, $registrar); + $this->assertInstanceOf(CallablePipeline::class, $pipeline); $this->assertSame('xab', do_pipeline('p', 'x')); } diff --git a/tests/src/Callables/CallableActionTest.php b/tests/src/Callables/CallableActionTest.php new file mode 100644 index 0000000..b7174ce --- /dev/null +++ b/tests/src/Callables/CallableActionTest.php @@ -0,0 +1,67 @@ +getContainer()->register(SimpleCallables::class, new SimpleCallables); + $action = new CallableAction($this->getInvoker()); + $action->add(function (string $param_1): void { + static::$results[] = sprintf("anonymous function(%s)", $param_1); + }, 0, 1); + $action->add(SimpleCallables::class . '::staticMethod', 0, 1); + $action->add(SimpleCallables::class . '@method', 0, 2); + + $result = $action->run('A', 'B'); + + $this->assertNull($result); + $this->assertSame([ + "anonymous function(A)", + SimpleCallables::class . "::staticMethod(A)", + SimpleCallables::class . "@method(A, B)", + ], static::$results); + } + + public function test_a_null_arguments_limit_passes_every_argument(): void + { + $this->getContainer()->register(SimpleCallables::class, new SimpleCallables); + $action = new CallableAction($this->getInvoker()); + $action->add(function (string $param_1, $param_2): void { + static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2); + }, 0, null); + $action->add(SimpleCallables::class . '::staticMethod', 0, null); + $action->add(SimpleCallables::class . '@method', 0, null); + + $action->run('A', 'B'); + + $this->assertSame([ + "anonymous function(A, B)", + SimpleCallables::class . "::staticMethod(A, B)", + SimpleCallables::class . "@method(A, B)", + ], static::$results); + } + + public function test_execution_priority(): void + { + $this->getContainer()->register(SimpleCallables::class, new SimpleCallables); + $action = new CallableAction($this->getInvoker()); + $action->add(function (string $param_1, $param_2): void { + static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2); + }, 0, null); + $action->add(SimpleCallables::class . '@method', 100, null); + $action->add(SimpleCallables::class . '::staticMethod', 0, null); + + $action->run('A', 'B'); + + $this->assertSame([ + SimpleCallables::class . "@method(A, B)", + "anonymous function(A, B)", + SimpleCallables::class . "::staticMethod(A, B)", + ], static::$results); + } +} diff --git a/tests/src/Callables/CallableFilterTest.php b/tests/src/Callables/CallableFilterTest.php new file mode 100644 index 0000000..1c64496 --- /dev/null +++ b/tests/src/Callables/CallableFilterTest.php @@ -0,0 +1,20 @@ +getInvoker()); + $filter->add(fn ($name): string => ' hello ' . $name, 0, 1); + $filter->add('trim', 0, 1); + $filter->add('ucwords', 0, 2); + + $result = $filter->run('world'); + + $this->assertEquals('Hello World', $result); + } +} diff --git a/tests/src/Callables/CallableMiddlewareTest.php b/tests/src/Callables/CallableMiddlewareTest.php new file mode 100644 index 0000000..dd69fbd --- /dev/null +++ b/tests/src/Callables/CallableMiddlewareTest.php @@ -0,0 +1,20 @@ +getInvoker()); + $middleware->add(fn ($name, $next = null): string => ucwords((string) $next($name))); + $middleware->add(fn ($name, $next = null): string => 'Hello ' . $next($name)); + $middleware->add(fn ($name, $next = null) => $name); + + $result = $middleware->run('world'); + + $this->assertEquals('Hello World', $result); + } +} diff --git a/tests/src/Callables/CallablePipelineTest.php b/tests/src/Callables/CallablePipelineTest.php new file mode 100644 index 0000000..764f501 --- /dev/null +++ b/tests/src/Callables/CallablePipelineTest.php @@ -0,0 +1,29 @@ +getInvoker()); + $pipeline->add(fn ($name): string => ' hello ' . $name, 1); + $pipeline->add('trim', 1); + $pipeline->add('ucwords', 1); + + $result = $pipeline->run('world'); + + $this->assertEquals('Hello World', $result); + } + + public function test_it_can_be_run_more_than_once(): void + { + $pipeline = new CallablePipeline($this->getInvoker()); + $pipeline->add(fn ($name): string => 'hello ' . $name); + + $this->assertEquals('hello a', $pipeline->run('a')); + $this->assertEquals('hello b', $pipeline->run('b')); + } +} diff --git a/tests/src/Processors/CommandBusTest.php b/tests/src/Callables/CommandBusTest.php similarity index 50% rename from tests/src/Processors/CommandBusTest.php rename to tests/src/Callables/CommandBusTest.php index 0a2dceb..5b06cb5 100644 --- a/tests/src/Processors/CommandBusTest.php +++ b/tests/src/Callables/CommandBusTest.php @@ -1,6 +1,6 @@ getContainer()->register(SampleHandler::class, new SampleHandler()); $bus = new CommandBus($this->getInvoker()); - $bus->addMiddleware(SampleCommand::class, fn($name, $next = null): int|float => 2 * $next($name)); + $bus->addMiddleware(SampleCommand::class, fn ($name, $next = null): int|float => 2 * $next($name)); $result = $bus->handle(new SampleCommand(2, 5)); @@ -22,8 +22,8 @@ public function test_automatic_handler(): void public function test_custom_handler(): void { $bus = new CommandBus($this->getInvoker()); - $bus->addMiddleware(SampleCommand::class, fn($name, $next = null): int|float => 2 * $next($name)); - $bus->register(SampleCommand::class, fn(SampleCommand $command): int => $command->first * $command->second); + $bus->addMiddleware(SampleCommand::class, fn ($name, $next = null): int|float => 2 * $next($name)); + $bus->register(SampleCommand::class, fn (SampleCommand $command): int => $command->first * $command->second); $result = $bus->handle(new SampleCommand(2, 5)); @@ -39,4 +39,15 @@ public function test_no_middleware(): void $this->assertEquals(2 + 5, $result); } + + public function test_registered_middleware_survives_repeated_handling(): void + { + $this->getContainer()->register(SampleHandler::class, new SampleHandler()); + $bus = new CommandBus($this->getInvoker()); + $bus->addMiddleware(SampleCommand::class, fn ($name, $next = null): int|float => 2 * $next($name)); + + // The handler is appended on each handle(); the registered middleware must not be consumed. + $this->assertEquals(2 * (2 + 5), $bus->handle(new SampleCommand(2, 5))); + $this->assertEquals(2 * (3 + 4), $bus->handle(new SampleCommand(3, 4))); + } } diff --git a/tests/src/CommandBus/SampleCommand.php b/tests/src/CommandBus/SampleCommand.php index 41367b5..b893a5c 100644 --- a/tests/src/CommandBus/SampleCommand.php +++ b/tests/src/CommandBus/SampleCommand.php @@ -4,9 +4,6 @@ namespace Sirius\Invokator\CommandBus; -use Sirius\Invokator\SimpleStackProcessorTest; -use Sirius\Invokator\TestCase; - class SampleCommand { public function __construct(public int $first, public int $second) diff --git a/tests/src/CommandBus/SampleHandler.php b/tests/src/CommandBus/SampleHandler.php index 4590d95..6f9d56a 100644 --- a/tests/src/CommandBus/SampleHandler.php +++ b/tests/src/CommandBus/SampleHandler.php @@ -4,9 +4,6 @@ namespace Sirius\Invokator\CommandBus; -use Sirius\Invokator\SimpleStackProcessorTest; -use Sirius\Invokator\TestCase; - class SampleHandler { public function handle(SampleCommand $command): int diff --git a/tests/src/InvokatorTest.php b/tests/src/InvokatorTest.php new file mode 100644 index 0000000..c50da99 --- /dev/null +++ b/tests/src/InvokatorTest.php @@ -0,0 +1,156 @@ +getInvoker()); + } + + public function test_pipeline_bulk_register_and_run(): void + { + $invokator = $this->getInvokator(); + $invokator->pipeline('slug', fn ($t): string => trim((string) $t), 'strtolower'); + + $this->assertSame('hello', $invokator->pipeline('slug')->run(' HELLO ')); + } + + public function test_pipeline_is_cached_per_identifier(): void + { + $invokator = $this->getInvokator(); + + $first = $invokator->pipeline('p'); + $second = $invokator->pipeline('p'); + + $this->assertInstanceOf(CallablePipeline::class, $first); + $this->assertSame($first, $second); + } + + public function test_registrations_accumulate_across_calls(): void + { + $invokator = $this->getInvokator(); + $invokator->pipeline('p')->add(fn ($x): string => $x . 'a'); + $invokator->pipeline('p')->add(fn ($x): string => $x . 'b'); + + $this->assertSame('xab', $invokator->pipeline('p')->run('x')); + } + + public function test_per_callable_priority(): void + { + $invokator = $this->getInvokator(); + $invokator->pipeline('p') + ->add(fn ($x): string => $x . '-low', 0) + ->add(fn ($x): string => $x . '-high', 10); + + $this->assertSame('start-high-low', $invokator->pipeline('p')->run('start')); + } + + public function test_filter_threads_the_value(): void + { + $invokator = $this->getInvokator(); + $invokator->filter('up')->add(fn ($v): string => strtoupper((string) $v)); + + $this->assertSame('HELLO', $invokator->filter('up')->run('hello')); + } + + public function test_action_runs_for_side_effects_and_returns_null(): void + { + $invokator = $this->getInvokator(); + $log = []; + $invokator->action('log')->add(function ($x) use (&$log): void { + $log[] = $x; + }); + + $this->assertNull($invokator->action('log')->run('hi')); + $this->assertSame(['hi'], $log); + } + + public function test_middleware_wraps_with_next(): void + { + $invokator = $this->getInvokator(); + $invokator->middleware('m') + ->add(fn ($name, $next = null): string => strtoupper((string) $next($name))) + ->add(fn ($name, $next = null): string => 'Hello ' . $next($name)) + ->add(fn ($name, $next = null) => $name); + + $this->assertSame('HELLO WORLD', $invokator->middleware('m')->run('world')); + } + + public function test_event_subscribe_and_dispatch(): void + { + $invokator = $this->getInvokator(); + $seen = []; + $invokator->event(EventWithoutName::class)->add(function (object $event) use (&$seen): void { + $seen[] = $event::class; + }); + + $returned = $invokator->dispatch(new EventWithoutName()); + + $this->assertInstanceOf(EventWithoutName::class, $returned); + $this->assertSame([EventWithoutName::class], $seen); + } + + public function test_event_run_is_an_alias_for_dispatch(): void + { + $invokator = $this->getInvokator(); + $seen = []; + $invokator->event(EventWithoutName::class, function (object $event) use (&$seen): void { + $seen[] = 'seen'; + }); + + $invokator->event(EventWithoutName::class)->run(new EventWithoutName()); + + $this->assertSame(['seen'], $seen); + } + + public function test_stoppable_event_halts_propagation(): void + { + $invokator = $this->getInvokator(); + $order = []; + $invokator->event(StoppableEvent::class)->add(function () use (&$order): void { + $order[] = 1; + }); + $invokator->event(StoppableEvent::class)->add(function (StoppableEvent $event) use (&$order): void { + $order[] = 2; + $event->stopPropagation(); + }); + $invokator->event(StoppableEvent::class)->add(function () use (&$order): void { + $order[] = 3; + }); + + $invokator->dispatch(new StoppableEvent()); + + $this->assertSame([1, 2], $order); + } + + public function test_command_middleware_and_auto_discovered_handler(): void + { + $this->getContainer()->register(SampleHandler::class, new SampleHandler()); + $invokator = $this->getInvokator(); + $invokator->command(SampleCommand::class) + ->add(fn ($command, $next = null): int|float => 2 * $next($command)); + + $this->assertEquals(2 * (2 + 5), $invokator->handle(new SampleCommand(2, 5))); + } + + public function test_command_run_with_explicit_handler(): void + { + $invokator = $this->getInvokator(); + $result = $invokator->command(SampleCommand::class) + ->handledBy(fn (SampleCommand $command): int => $command->first * $command->second) + ->run(new SampleCommand(2, 5)); + + $this->assertEquals(10, $result); + } +} diff --git a/tests/src/Modifiers/OnceTest.php b/tests/src/Modifiers/OnceTest.php index ca50985..291f3f5 100644 --- a/tests/src/Modifiers/OnceTest.php +++ b/tests/src/Modifiers/OnceTest.php @@ -2,7 +2,7 @@ namespace Sirius\Invokator\Modifiers; -use Sirius\Invokator\Processors\SimpleCallablesProcessor; +use Sirius\Invokator\Callables\CallableAction; use Sirius\Invokator\TestCase; use function Sirius\Invokator\once; @@ -18,15 +18,15 @@ protected function setUp(): void public function test_modifier(): void { - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', once(function (string $param_1, $param_2): void { + $action = new CallableAction($this->getInvoker()); + $action->add(once(function (string $param_1, $param_2): void { static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2); - })); + }), 0, null); - $processor->process('test', 'A', 'B'); - $processor->process('test', 'A', 'B'); - $processor->process('test', 'A', 'B'); - $processor->process('test', 'A', 'B'); + $action->run('A', 'B'); + $action->run('A', 'B'); + $action->run('A', 'B'); + $action->run('A', 'B'); $this->assertSame([ "anonymous function(A, B)", diff --git a/tests/src/Modifiers/ResolveArgumentsTest.php b/tests/src/Modifiers/ResolveArgumentsTest.php index 94da202..6c135ef 100644 --- a/tests/src/Modifiers/ResolveArgumentsTest.php +++ b/tests/src/Modifiers/ResolveArgumentsTest.php @@ -2,7 +2,7 @@ namespace Sirius\Invokator\Modifiers; -use Sirius\Invokator\Processors\SimpleCallablesProcessor; +use Sirius\Invokator\Callables\CallableAction; use Sirius\Invokator\TestCase; use Sirius\Invokator\Utilities\DependencyClass; use Sirius\Invokator\Utilities\DependentClass; @@ -29,12 +29,12 @@ protected function setUp(): void public function test_resolve_arguments(): void { $this->getContainer()->register('test_param', 'C'); - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', wrap(resolve(DependentClass::class . '@multiply', ['firstNumber' => 5, 'secondNumber' => arg(0)]), function($next): void{ + $action = new CallableAction($this->getInvoker()); + $action->add(wrap(resolve(DependentClass::class . '@multiply', ['firstNumber' => 5, 'secondNumber' => arg(0)]), function($next): void{ static::$results[] = $next(); - })); + }), 0, null); - $processor->process('test', 1); + $action->run(1); $this->assertSame([ 5 * (1 + 5), diff --git a/tests/src/Modifiers/WithArgumentsTest.php b/tests/src/Modifiers/WithArgumentsTest.php index cf0dc3f..2308650 100644 --- a/tests/src/Modifiers/WithArgumentsTest.php +++ b/tests/src/Modifiers/WithArgumentsTest.php @@ -2,7 +2,7 @@ namespace Sirius\Invokator\Modifiers; -use Sirius\Invokator\Processors\SimpleCallablesProcessor; +use Sirius\Invokator\Callables\CallableAction; use Sirius\Invokator\TestCase; use function Sirius\Invokator\ref; use function Sirius\Invokator\arg; @@ -22,12 +22,12 @@ protected function setUp(): void public function test_modifier_with_refs(): void { $this->getContainer()->register('test_param', 'C'); - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', with_arguments(function (string $param_1, $param_2, $param_3, $param_4): void { + $action = new CallableAction($this->getInvoker()); + $action->add(with_arguments(function (string $param_1, $param_2, $param_3, $param_4): void { static::$results[] = sprintf("anonymous function(%s, %s, %s, %s)", $param_1, $param_2, $param_3, $param_4); - }, [arg(1), arg(0), ref('test_param'), 'D'])); + }, [arg(1), arg(0), ref('test_param'), 'D']), 0, null); - $processor->process('test', 'A', 'B'); + $action->run('A', 'B'); $this->assertSame([ "anonymous function(B, A, C, D)", @@ -37,12 +37,12 @@ public function test_modifier_with_refs(): void public function test_modifier_with_invoker_result(): void { $this->getContainer()->register('test_param', 'C'); - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', with_arguments(function (string $param_1, $param_2, $param_3): void { + $action = new CallableAction($this->getInvoker()); + $action->add(with_arguments(function (string $param_1, $param_2, $param_3): void { static::$results[] = sprintf("anonymous function(%s, %s, %s)", $param_1, $param_2, $param_3); - }, [result_of('trim', [' C ']), arg(1), arg(0)])); + }, [result_of('trim', [' C ']), arg(1), arg(0)]), 0, null); - $processor->process('test', 'A', 'B'); + $action->run('A', 'B'); $this->assertSame([ "anonymous function(C, B, A)", diff --git a/tests/src/Modifiers/WrapTest.php b/tests/src/Modifiers/WrapTest.php index bf18c13..6b20d80 100644 --- a/tests/src/Modifiers/WrapTest.php +++ b/tests/src/Modifiers/WrapTest.php @@ -2,7 +2,7 @@ namespace Sirius\Invokator; -use Sirius\Invokator\Processors\SimpleCallablesProcessor; +use Sirius\Invokator\Callables\CallableAction; use Sirius\Invokator\Utilities\SimpleCallables; class WrapTest extends TestCase @@ -18,16 +18,16 @@ protected function setUp(): void public function test_modifier(): void { $this->getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', wrap(function (string $param_1, $param_2): void { + $action = new CallableAction($this->getInvoker()); + $action->add(wrap(function (string $param_1, $param_2): void { static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2); }, function ($next) { static::$results[] = 'From wrapper function'; return $next(); - })); + }), 0, null); - $processor->process('test', 'A', 'B'); + $action->run('A', 'B'); $this->assertSame([ 'From wrapper function', diff --git a/tests/src/Processors/ActionsProcessorTest.php b/tests/src/Processors/ActionsProcessorTest.php deleted file mode 100644 index 047c9d3..0000000 --- a/tests/src/Processors/ActionsProcessorTest.php +++ /dev/null @@ -1,28 +0,0 @@ -getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new ActionsProcessor($this->getInvoker()); - $processor->add('test', function (string $param_1): void { - static::$results[] = sprintf("anonymous function(%s)", $param_1, 1); - }, 0, 1); - $processor->add('test', SimpleCallables::class . '::staticMethod', 0, 1); - $processor->add('test', SimpleCallables::class . '@method', 0, 2); - - $processor->process('test', 'A', 'B'); - - $this->assertSame([ - "anonymous function(A)", - SimpleCallables::class . "::staticMethod(A)", - SimpleCallables::class . "@method(A, B)", - ], static::$results); - } -} diff --git a/tests/src/Processors/FiltersProcessorTest.php b/tests/src/Processors/FiltersProcessorTest.php deleted file mode 100644 index bb36fd0..0000000 --- a/tests/src/Processors/FiltersProcessorTest.php +++ /dev/null @@ -1,22 +0,0 @@ -getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new FiltersProcessor($this->getInvoker()); - $processor->add('test', fn($name): string => ' hello ' . $name, 0, 1); - $processor->add('test', 'trim', 0, 1); - $processor->add('test', 'ucwords', 0, 2); - - $result = $processor->process('test', 'world'); - - $this->assertEquals('Hello World', $result); - } -} diff --git a/tests/src/Processors/MiddlewareProcessorTest.php b/tests/src/Processors/MiddlewareProcessorTest.php deleted file mode 100644 index 3eeb1c3..0000000 --- a/tests/src/Processors/MiddlewareProcessorTest.php +++ /dev/null @@ -1,22 +0,0 @@ -getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new MiddlewareProcessor($this->getInvoker()); - $processor->add('test', fn($name, $next = null): string => ucwords((string) $next($name))); - $processor->add('test', fn($name, $next = null): string => 'Hello ' . $next($name)); - $processor->add('test', fn($name, $next = null) => $name); - - $result = $processor->process('test', 'world'); - - $this->assertEquals('Hello World', $result); - } -} diff --git a/tests/src/Processors/PipelineProcessorTest.php b/tests/src/Processors/PipelineProcessorTest.php deleted file mode 100644 index 5071f2b..0000000 --- a/tests/src/Processors/PipelineProcessorTest.php +++ /dev/null @@ -1,22 +0,0 @@ -getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new PipelineProcessor($this->getInvoker()); - $processor->add('test', fn($name): string => ' hello ' . $name, 1); - $processor->add('test', 'trim', 1); - $processor->add('test', 'ucwords', 1); - - $result = $processor->process('test', 'world'); - - $this->assertEquals('Hello World', $result); - } -} diff --git a/tests/src/Processors/SimpleStackProcessorTest.php b/tests/src/Processors/SimpleStackProcessorTest.php deleted file mode 100644 index bcf31a1..0000000 --- a/tests/src/Processors/SimpleStackProcessorTest.php +++ /dev/null @@ -1,47 +0,0 @@ -getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', function (string $param_1, $param_2): void { - static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2); - }); - $processor->add('test', SimpleCallables::class . '::staticMethod'); - $processor->add('test', SimpleCallables::class . '@method'); - - $processor->process('test', 'A', 'B'); - - $this->assertSame([ - "anonymous function(A, B)", - SimpleCallables::class . "::staticMethod(A, B)", - SimpleCallables::class . "@method(A, B)", - ], static::$results); - } - - public function test_execution_priority(): void - { - $this->getContainer()->register(SimpleCallables::class, new SimpleCallables); - $processor = new SimpleCallablesProcessor($this->getInvoker()); - $processor->add('test', function (string $param_1, $param_2): void { - static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2); - }); - $processor->add('test', SimpleCallables::class . '@method', 100); - $processor->add('test', SimpleCallables::class . '::staticMethod'); - - $processor->process('test', 'A', 'B'); - - $this->assertSame([ - SimpleCallables::class . "@method(A, B)", - "anonymous function(A, B)", - SimpleCallables::class . "::staticMethod(A, B)", - ], static::$results); - } -} From e119b34a0d0b6e33dd2b9e9cabd1be7848b6033d Mon Sep 17 00:00:00 2001 From: adrianmiu Date: Sun, 7 Jun 2026 18:34:00 +0300 Subject: [PATCH 2/2] improved types --- src/Callables/CallableAction.php | 1 + src/Callables/CallableCommand.php | 4 ++-- src/Callables/CallableEvent.php | 4 ++-- src/Callables/CallableFilter.php | 1 + tests/src/Callables/CallableFilterTest.php | 2 ++ tests/src/Callables/CallablePipelineTest.php | 2 ++ 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Callables/CallableAction.php b/src/Callables/CallableAction.php index e88daf0..c355b8c 100644 --- a/src/Callables/CallableAction.php +++ b/src/Callables/CallableAction.php @@ -16,6 +16,7 @@ */ class CallableAction extends AbstractCallableStack { + #[\Override] public function add(mixed $callable, int $priority = 0, ?int $argumentsLimit = 1): static { if ($argumentsLimit !== null) { diff --git a/src/Callables/CallableCommand.php b/src/Callables/CallableCommand.php index 7e9ba7c..78483e0 100644 --- a/src/Callables/CallableCommand.php +++ b/src/Callables/CallableCommand.php @@ -10,9 +10,9 @@ * `add()` registers middleware, `handledBy()` binds an explicit handler, and `run()` dispatches * a command object through the bus. */ -final class CallableCommand +final readonly class CallableCommand { - public function __construct(private readonly CommandBus $bus, private readonly string $commandClass) + public function __construct(private CommandBus $bus, private string $commandClass) { } diff --git a/src/Callables/CallableEvent.php b/src/Callables/CallableEvent.php index d923dfe..62c9a38 100644 --- a/src/Callables/CallableEvent.php +++ b/src/Callables/CallableEvent.php @@ -12,9 +12,9 @@ * `add()` subscribes a listener, `once()` subscribes a one-shot listener, and `run()` dispatches * an event object (routed by the object's own class, like any PSR-14 dispatch). */ -final class CallableEvent +final readonly class CallableEvent { - public function __construct(private readonly Dispatcher $dispatcher, private readonly string $eventName) + public function __construct(private Dispatcher $dispatcher, private string $eventName) { } diff --git a/src/Callables/CallableFilter.php b/src/Callables/CallableFilter.php index 92ef835..ed7d8b7 100644 --- a/src/Callables/CallableFilter.php +++ b/src/Callables/CallableFilter.php @@ -16,6 +16,7 @@ */ class CallableFilter extends AbstractCallableStack { + #[\Override] public function add(mixed $callable, int $priority = 0, ?int $argumentsLimit = 1): static { if ($argumentsLimit !== null) { diff --git a/tests/src/Callables/CallableFilterTest.php b/tests/src/Callables/CallableFilterTest.php index 1c64496..425c1f3 100644 --- a/tests/src/Callables/CallableFilterTest.php +++ b/tests/src/Callables/CallableFilterTest.php @@ -1,5 +1,7 @@