diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df25d92..8d6a1bd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,14 +10,12 @@ jobs:
build:
strategy:
matrix:
- php: ['8.0', '8.1', '8.2', '8.3']
- include:
- - php: '8.0'
+ php: ['8.3', '8.4', '8.5']
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Setup PHP with fail-fast
uses: shivammathur/setup-php@v2
with:
@@ -31,19 +29,20 @@ jobs:
- name: Cache Composer packages
id: composer-cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: vendor
- key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('composer.json') }}
restore-keys: |
- ${{ runner.os }}-php-
+ ${{ runner.os }}-php-${{ matrix.php }}-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
- run: composer install --prefer-dist --no-progress --no-suggest
+ run: composer install --prefer-dist --no-progress
- name: Run test suite
run: |
mkdir -p build/logs
+ composer run rector:dry
composer run stan
composer run test
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index dbd33b0..028b135 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -8,7 +8,12 @@ jobs:
docs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
- name: Build docs
run: |
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..a930921
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,16 @@
+in(__DIR__ . '/src');
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@PSR12' => true,
+ // Import fully-qualified class names and shorten the references to the short name.
+ 'fully_qualified_strict_types' => ['import_symbols' => true],
+ 'no_unused_imports' => true,
+ 'ordered_imports' => ['sort_algorithm' => 'alpha'],
+ ])
+ ->setFinder($finder);
diff --git a/Dockerfile b/Dockerfile
index 26c01c3..1a1d0be 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Use the official PHP 8 image as the base image
-FROM php:8.1-fpm
+FROM php:8.3-fpm
# Set the working directory
WORKDIR /var/www/html
@@ -21,15 +21,17 @@ RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug
+# Write xdebug settings to a separate ini file so the `zend_extension=` line
+# generated by docker-php-ext-enable above is preserved (not overwritten).
RUN { \
echo "xdebug.mode=coverage"; \
echo "xdebug.start_with_request=yes"; \
echo "xdebug.client_host=host.docker.internal"; \
echo "xdebug.client_port=9000"; \
- } > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \
+ } > /usr/local/etc/php/conf.d/xdebug-settings.ini
# Install Composer (a PHP package manager)
-RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
# Mount a local folder to /var/www/html
VOLUME ./:/var/www/html
diff --git a/composer.json b/composer.json
index 8294d8b..5c38237 100644
--- a/composer.json
+++ b/composer.json
@@ -17,13 +17,21 @@
}
],
"require": {
- "php": "^8.0|^8.1|^8.2",
+ "php": "^8.3",
"psr/container": "^2.0",
"psr/event-dispatcher": "^1.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.6",
- "phpstan/phpstan": "^1.10"
+ "phpunit/phpunit": "^12.0",
+ "phpstan/phpstan": "^2.0",
+ "rector/rector": "^2.0",
+ "friendsofphp/php-cs-fixer": "^3.34",
+ "illuminate/support": "^11.0 || ^12.0 || ^13.0",
+ "illuminate/contracts": "^11.0 || ^12.0 || ^13.0",
+ "orchestra/testbench": "^10.0 || ^11.0"
+ },
+ "suggest": {
+ "illuminate/support": "Required to use the Laravel integration (SiriusInvokatorServiceProvider, Invokator facade, do_* helpers, Blade directives)"
},
"autoload": {
"files": [
@@ -33,18 +41,39 @@
"Sirius\\Invokator\\": "src/"
}
},
+ "autoload-dev": {
+ "psr-4": {
+ "Sirius\\Invokator\\Tests\\Laravel\\": "tests/Laravel/"
+ }
+ },
"provide": {
"psr/event-dispatcher-implementation": "1.0"
},
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Sirius\\Invokator\\Laravel\\SiriusInvokatorServiceProvider"
+ ],
+ "aliases": {
+ "Invokator": "Sirius\\Invokator\\Laravel\\Facades\\Invokator"
+ }
+ }
+ },
"scripts": {
"stan": [
"php vendor/bin/phpstan analyse"
],
"csfix": [
- "tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --standard=PSR-2 src"
+ "php vendor/bin/php-cs-fixer fix"
],
"test": [
"php vendor/bin/phpunit -c tests/phpunit.xml"
+ ],
+ "rector": [
+ "php vendor/bin/rector process"
+ ],
+ "rector:dry": [
+ "php vendor/bin/rector process --dry-run"
]
}
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 9525954..a36db68 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,12 +1,10 @@
-version: '3'
-
services:
### PHP ###################################################
php:
build:
context: ./
- container_name: stack_runner_php
+ container_name: siriusphp_invokator
volumes:
- ./:/var/www/html:delegated
ports:
diff --git a/docs/7_laravel.md b/docs/7_laravel.md
new file mode 100644
index 0000000..26c495f
--- /dev/null
+++ b/docs/7_laravel.md
@@ -0,0 +1,139 @@
+---
+title: Using Sirius\Invokator with Laravel
+---
+
+# Laravel integration
+
+Sirius\Invokator ships an optional Laravel bridge that exposes the library through
+idiomatic Laravel surfaces: a service provider, an `Invokator` facade, a set of `do_*`
+helper functions and a Blade directive. The core library stays framework-agnostic — the
+bridge only loads inside a Laravel application.
+
+## Installation
+
+```bash
+composer require siriusphp/invokator
+```
+
+The package is auto-discovered, so there is nothing to register manually. Laravel reads the
+`extra.laravel` section of `composer.json` and registers:
+
+- the `Sirius\Invokator\Laravel\SiriusInvokatorServiceProvider` service provider, and
+- 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.
+
+## 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.
+
+```php
+use Sirius\Invokator\Laravel\Facades\Invokator;
+
+// define
+Invokator::pipeline('process-order')
+ ->add(fn ($order) => /* ... */ $order)
+ ->add(OrderNormalizer::class . '@handle');
+
+// run
+$result = Invokator::pipeline('process-order', $order);
+```
+
+`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
+
+```php
+// Pipeline — the result of each callable is the only argument to the next
+Invokator::pipeline('slugify')
+ ->add(fn ($title) => trim($title))
+ ->add(fn ($title) => strtolower($title));
+Invokator::pipeline('slugify', ' 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
+
+// Action — run callables for their side effects; returns null
+Invokator::action('analytics')->add(fn ($user) => Analytics::track($user));
+Invokator::action('analytics', $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);
+```
+
+### Events (PSR-14)
+
+Events keep PSR-14 semantics: you dispatch an event **object** and the listener key is taken
+from its class name (or `HasEventName::getEventName()`).
+
+```php
+// subscribe
+Invokator::event(OrderPlaced::class)->add(function (OrderPlaced $event) {
+ // ...
+});
+
+// dispatch
+Invokator::dispatch(new OrderPlaced($order));
+```
+
+Events that implement `Psr\EventDispatcher\StoppableEventInterface` (for instance via the
+`Sirius\Invokator\Event\Stoppable` trait) stop propagation as usual.
+
+## 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.
+
+```php
+do_pipeline('process-order')->add(/* ... */);
+do_pipeline('process-order', $order);
+
+do_filter('price', 100);
+do_action('analytics', $user);
+do_middleware('http', $request);
+
+do_event(new OrderPlaced($order));
+```
+
+## Blade
+
+Use the `@do_action` directive to run an action from a template (it emits nothing), and the
+`do_filter()` helper inside an echo to print a filtered value:
+
+```blade
+
+
+ @do_action('html-head', $page)
+ {{ do_filter('page-title', $title) }}
+
+...
+
+```
+
+## Resolving callables
+
+Because the `Invoker` is wired to Laravel's container, string callables are resolved through
+it. The following all work:
+
+- **Closures** — `fn ($x) => ...`.
+- **`Service@method`** — the `Service` is resolved from the container *with dependency
+ injection*, then `method` is called.
+- **Bound service ids / invokable services** registered in the container.
+- **Plain function names** (`'trim'`) and **`Class::method`** static strings — used directly
+ when they are not bound in the container.
diff --git a/docs/9_upgrading_to_2.md b/docs/9_upgrading_to_2.md
new file mode 100644
index 0000000..534690d
--- /dev/null
+++ b/docs/9_upgrading_to_2.md
@@ -0,0 +1,60 @@
+---
+title: Upgrading Sirius\Invokator from 1.x to 2.0
+---
+
+# Upgrading to 2.0
+
+Version 2.0 modernizes the library for current PHP versions and refreshes the
+development tooling. The public API is unchanged, so for most applications the
+upgrade is just a version bump. There are two things to be aware of.
+
+## 1. PHP 8.3 or newer is required
+
+The minimum supported PHP version moved from 8.0 to **8.3**. The library is tested
+against PHP 8.3, 8.4 and 8.5.
+
+If you are still on PHP 8.0–8.2, stay on the `1.x` releases until you can upgrade
+your runtime.
+
+```bash
+composer require siriusphp/invokator:^2.0
+```
+
+## 2. Value objects are now immutable (`readonly`)
+
+The small value objects used internally now declare their public properties as
+`readonly`:
+
+- `ArgumentReference` — created by `arg()`
+- `InvokerReference` — created by `ref()`
+- `InvokerResult` — created by `result_of()`
+- `PipelinePromise`
+- `SuggestedResume`
+- `SuggestedRetry`
+
+In practice you create these through the helper functions and read their
+properties, so nothing changes. The only breaking case is code that **reassigned**
+one of their public properties after construction:
+
+```php
+$ref = arg(0);
+$ref->reference = 1; // 2.0: Error — cannot modify readonly property
+```
+
+Build a new instance instead of mutating an existing one:
+
+```php
+$ref = arg(1);
+```
+
+## Tooling changes (development only)
+
+These do not affect applications consuming the library; they matter only if you
+work on the library itself or copy its setup:
+
+- PHPUnit `9` → `12` (test configuration migrated to the 12.x schema).
+- PHPStan `1` → `2` (still analysed at level 9).
+- [Rector](https://getrector.com/) `2` added; run `composer run rector`.
+- `friendsofphp/php-cs-fixer` moved from `tools/` into `require-dev`; `composer run
+ csfix` now applies the `@PSR12` ruleset.
+- The bundled Docker image is based on `php:8.3-fpm`.
diff --git a/docs/couscous.yml b/docs/couscous.yml
index 92091bc..b8de55d 100644
--- a/docs/couscous.yml
+++ b/docs/couscous.yml
@@ -74,9 +74,21 @@ menu:
invoker:
text: The callable invoker
relativeUrl: 4_the_invoker.html
+ integrations:
+ name: Integrations
+ items:
+ laravel:
+ text: Laravel
+ relativeUrl: 7_laravel.html
ideas:
name: Ideas
items:
auto_middleware:
text: Automatic middleware
relativeUrl: 6_automatic_middleware.html
+ upgrading:
+ name: Upgrading
+ items:
+ upgrade_2_0:
+ text: Upgrading to 2.0
+ relativeUrl: 9_upgrading_to_2.html
diff --git a/phpstan.neon b/phpstan.neon
index 783dcad..c158993 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,5 +1,4 @@
parameters:
level: 9
- checkGenericClassInNonGenericObjectType: false
paths:
- src
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..6aae3b7
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,33 @@
+withPaths([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ // Resolve the PHP feature level from composer.json ("php": "^8.3"),
+ // so emitted syntax always runs on the lowest supported version.
+ ->withPhpSets()
+ ->withPreparedSets(
+ deadCode: true,
+ codeQuality: true,
+ typeDeclarations: true,
+ earlyReturn: true,
+ )
+ // `readonly` on public value-object properties is a minor BC break, so it is
+ // applied deliberately by hand (see the value objects in src/) rather than
+ // swept in automatically.
+ ->withSkip([
+ ReadOnlyPropertyRector::class,
+ // $reversed must keep its class-level default for __unserialize() (which
+ // bypasses the constructor), so it cannot be a promoted property.
+ ClassPropertyAssignToConstructorPromotionRector::class => [
+ __DIR__ . '/src/CallableCollection.php',
+ ],
+ ]);
diff --git a/src/ArgumentReference.php b/src/ArgumentReference.php
index 1cf6b5e..c8c442b 100644
--- a/src/ArgumentReference.php
+++ b/src/ArgumentReference.php
@@ -6,7 +6,7 @@
class ArgumentReference
{
- public function __construct(public int $reference)
+ public function __construct(public readonly int $reference)
{
}
}
diff --git a/src/CallableCollection.php b/src/CallableCollection.php
index 95cd492..cebba45 100644
--- a/src/CallableCollection.php
+++ b/src/CallableCollection.php
@@ -4,10 +4,15 @@
namespace Sirius\Invokator;
+/**
+ * @extends \SplPriorityQueue, mixed>
+ */
class CallableCollection extends \SplPriorityQueue
{
protected int $index = 0;
+ // NOT promoted on purpose: __unserialize() bypasses the constructor, so the
+ // property needs a class-level default to stay initialized after unserializing.
protected bool $reversed = false;
public function __construct(bool $reversed = false)
@@ -39,6 +44,9 @@ public function compare($priority1, $priority2): int
return $sign * ($priority1[1] ?? 0) < $sign * ($priority2[1] ?? 0) ? 1 : -1;
}
+ /**
+ * @return array>
+ */
public function __serialize(): array
{
$data = [];
@@ -58,8 +66,6 @@ public function __serialize(): array
/**
* @param array $data
- *
- * @return void
*/
public function __unserialize(array $data): void
{
diff --git a/src/Event/Dispatcher.php b/src/Event/Dispatcher.php
index a9b20da..7588a1d 100644
--- a/src/Event/Dispatcher.php
+++ b/src/Event/Dispatcher.php
@@ -7,12 +7,11 @@
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
-use Sirius\Invokator\Invoker;
use Sirius\Invokator\CallableCollection;
+use Sirius\Invokator\Invoker;
class Dispatcher implements EventDispatcherInterface
{
-
public function __construct(public ListenerProviderInterface $registry, public Invoker $invoker)
{
}
@@ -36,9 +35,10 @@ public function dispatch(object $event): object
public function subscribeTo(string $eventName, mixed $callable, int $priority = 0): void
{
- if ( ! $this->registry instanceof ListenerSubscriber) {
- throw new \LogicException(sprintf('Unable to subscribe listener because %s is not instace of %s',
- get_class($this->registry),
+ if (! $this->registry instanceof ListenerSubscriber) {
+ throw new \LogicException(sprintf(
+ 'Unable to subscribe listener because %s is not instace of %s',
+ $this->registry::class,
ListenerSubscriber::class
));
}
@@ -48,9 +48,10 @@ public function subscribeTo(string $eventName, mixed $callable, int $priority =
public function subscribeOnceTo(string $eventName, mixed $callable, int $priority = 0): void
{
- if ( ! $this->registry instanceof ListenerSubscriber) {
- throw new \LogicException(sprintf('Unable to subscribe listener because %s is not instace of %s',
- get_class($this->registry),
+ if (! $this->registry instanceof ListenerSubscriber) {
+ throw new \LogicException(sprintf(
+ 'Unable to subscribe listener because %s is not instace of %s',
+ $this->registry::class,
ListenerSubscriber::class
));
}
diff --git a/src/Event/ListenerProvider.php b/src/Event/ListenerProvider.php
index 9cbce0e..129c660 100644
--- a/src/Event/ListenerProvider.php
+++ b/src/Event/ListenerProvider.php
@@ -6,6 +6,7 @@
use Psr\EventDispatcher\ListenerProviderInterface;
use Sirius\Invokator\CallableCollection;
+
use function Sirius\Invokator\once;
class ListenerProvider implements ListenerProviderInterface, ListenerSubscriber
@@ -14,13 +15,9 @@ class ListenerProvider implements ListenerProviderInterface, ListenerSubscriber
* @var array
*/
protected array $registry = []; // @phpstan-ignore-line
-
- /**
- * @return iterable|CallableCollection
- */
public function getListenersForEvent(object $event): iterable // @phpstan-ignore-line
{
- $eventName = get_class($event);
+ $eventName = $event::class;
if ($event instanceof HasEventName) {
$eventName = $event->getEventName();
}
diff --git a/src/Event/Stoppable.php b/src/Event/Stoppable.php
index d81d6c1..c16f107 100644
--- a/src/Event/Stoppable.php
+++ b/src/Event/Stoppable.php
@@ -4,6 +4,12 @@
namespace Sirius\Invokator\Event;
+/**
+ * Public-API convenience trait for consumers' stoppable events; it is used outside
+ * the analysed `src/` paths (e.g. by event classes in applications using this library).
+ *
+ * @phpstan-ignore trait.unused
+ */
trait Stoppable
{
protected bool $propagationStopped = false;
diff --git a/src/InvalidCallableException.php b/src/InvalidCallableException.php
index dd77bea..2bb4254 100644
--- a/src/InvalidCallableException.php
+++ b/src/InvalidCallableException.php
@@ -1,5 +1,7 @@
container = $container;
}
/**
@@ -109,7 +106,7 @@ protected function resolveArguments(mixed $callable, array $args): array
} else {
throw new InvalidArgumentException("Cannot resolve parameter: $paramName");
}
- } else if (isset($args[$paramName]) || $param->getDefaultValue()) {
+ } elseif (isset($args[$paramName]) || $param->getDefaultValue()) {
// Builtin types, such as int or string, do not need resolution.
$resolvedArgs[] = $args[$paramName] ?? $param->getDefaultValue();
} else {
@@ -121,8 +118,8 @@ protected function resolveArguments(mixed $callable, array $args): array
}
/**
- * @throws \Psr\Container\ContainerExceptionInterface
- * @throws \Psr\Container\NotFoundExceptionInterface
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
* @throws InvalidCallableException
*/
protected function getActualCallable(mixed $callable): callable
@@ -136,9 +133,13 @@ protected function getActualCallable(mixed $callable): callable
$callable = [$service, $method];
}
- // if the callable references an invokable class from the container
+ // A string callable may also reference an invokable service bound in the container.
+ // Guard the lookup with has() so a plain function name like 'trim' — for which a
+ // strict PSR-11 container's get() throws NotFoundException — falls through and is
+ // used as the callable directly instead.
if (is_string($callable) &&
is_callable($callable) &&
+ $this->container->has($callable) &&
($service = $this->container->get($callable)) &&
is_callable($service)
) {
diff --git a/src/InvokerReference.php b/src/InvokerReference.php
index 9bba466..3670ae7 100644
--- a/src/InvokerReference.php
+++ b/src/InvokerReference.php
@@ -6,7 +6,7 @@
class InvokerReference
{
- public function __construct(public string $reference)
+ public function __construct(public readonly string $reference)
{
}
}
diff --git a/src/InvokerResult.php b/src/InvokerResult.php
index b1c3194..23c6c6b 100644
--- a/src/InvokerResult.php
+++ b/src/InvokerResult.php
@@ -7,10 +7,9 @@
class InvokerResult
{
/**
- * @param mixed $callable
* @param array $params
*/
- public function __construct(public mixed $callable, public array $params = [])
+ public function __construct(public readonly mixed $callable, public readonly array $params = [])
{
}
}
diff --git a/src/Laravel/Facades/Invokator.php b/src/Laravel/Facades/Invokator.php
new file mode 100644
index 0000000..7579bd1
--- /dev/null
+++ b/src/Laravel/Facades/Invokator.php
@@ -0,0 +1,26 @@
+ $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
new file mode 100644
index 0000000..7e00db8
--- /dev/null
+++ b/src/Laravel/Registrar.php
@@ -0,0 +1,31 @@
+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
new file mode 100644
index 0000000..cce3e67
--- /dev/null
+++ b/src/Laravel/SiriusInvokatorServiceProvider.php
@@ -0,0 +1,52 @@
+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');
+
+ require_once __DIR__ . '/helpers.php';
+ }
+
+ public function boot(): void
+ {
+ Blade::directive('do_action', static fn (string $expression): string => "");
+ }
+}
diff --git a/src/Laravel/helpers.php b/src/Laravel/helpers.php
new file mode 100644
index 0000000..176572a
--- /dev/null
+++ b/src/Laravel/helpers.php
@@ -0,0 +1,57 @@
+pipeline($id, ...$args);
+ }
+}
+
+if (! function_exists('do_action')) {
+ /**
+ * Define an action (identifier only, returns a {@see Registrar}) or run it (with args).
+ */
+ function do_action(string $id, mixed ...$args): mixed
+ {
+ return app(InvokatorManager::class)->action($id, ...$args);
+ }
+}
+
+if (! function_exists('do_filter')) {
+ /**
+ * Define a filter (identifier only, returns a {@see Registrar}) or run it (with args),
+ * returning the filtered value.
+ */
+ function do_filter(string $id, mixed ...$args): mixed
+ {
+ return app(InvokatorManager::class)->filter($id, ...$args);
+ }
+}
+
+if (! function_exists('do_middleware')) {
+ /**
+ * Define a middleware stack (identifier only, returns a {@see Registrar}) or run it (with args).
+ */
+ function do_middleware(string $id, mixed ...$args): mixed
+ {
+ return app(InvokatorManager::class)->middleware($id, ...$args);
+ }
+}
+
+if (! function_exists('do_event')) {
+ /**
+ * Dispatch a PSR-14 event object to its subscribed listeners.
+ */
+ function do_event(object $event): object
+ {
+ return app(InvokatorManager::class)->dispatch($event);
+ }
+}
diff --git a/src/Modifiers/LimitArguments.php b/src/Modifiers/LimitArguments.php
index 8b71cfd..4c24e80 100644
--- a/src/Modifiers/LimitArguments.php
+++ b/src/Modifiers/LimitArguments.php
@@ -11,7 +11,7 @@ class LimitArguments implements InvokerAwareInterface
{
protected Invoker $invoker;
- public function __construct(public mixed $callable, public int $argumentsLimit)
+ public function __construct(public readonly mixed $callable, public readonly int $argumentsLimit)
{
}
diff --git a/src/Modifiers/Once.php b/src/Modifiers/Once.php
index 02282c5..37a3f6c 100644
--- a/src/Modifiers/Once.php
+++ b/src/Modifiers/Once.php
@@ -13,9 +13,9 @@ class Once implements InvokerAwareInterface
protected mixed $result = null;
- protected bool $has_run = false;
+ protected bool $hasRun = false;
- public function __construct(public mixed $callable)
+ public function __construct(public readonly mixed $callable)
{
}
@@ -29,12 +29,12 @@ public function setInvoker(Invoker $invoker): void
*/
public function __invoke(...$params): mixed
{
- if ($this->has_run) {
+ if ($this->hasRun) {
return $this->result;
}
$this->result = $this->invoker->invoke($this->callable, ...$params);
- $this->has_run = true;
+ $this->hasRun = true;
return $this->result;
}
diff --git a/src/Modifiers/ResolveArguments.php b/src/Modifiers/ResolveArguments.php
index cb96339..1cf76e6 100644
--- a/src/Modifiers/ResolveArguments.php
+++ b/src/Modifiers/ResolveArguments.php
@@ -4,10 +4,11 @@
namespace Sirius\Invokator\Modifiers;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Sirius\Invokator\ArgumentReference;
use Sirius\Invokator\Invoker;
use Sirius\Invokator\InvokerAwareInterface;
-use Sirius\Invokator\InvokerReference;
class ResolveArguments implements InvokerAwareInterface
{
@@ -16,7 +17,9 @@ class ResolveArguments implements InvokerAwareInterface
/**
* @param array $arguments
*/
- public function __construct(public mixed $callable, public array $arguments)
+ // $arguments is intentionally NOT readonly: __invoke() resolves ArgumentReferences
+ // into it in place.
+ public function __construct(public readonly mixed $callable, public array $arguments)
{
}
@@ -28,9 +31,8 @@ public function setInvoker(Invoker $invoker): void
/**
* @param array $params
*
- * @return mixed
- * @throws \Psr\Container\ContainerExceptionInterface
- * @throws \Psr\Container\NotFoundExceptionInterface
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
*/
public function __invoke(...$params): mixed
{
diff --git a/src/Modifiers/WithArguments.php b/src/Modifiers/WithArguments.php
index f7023bd..3a0dc80 100644
--- a/src/Modifiers/WithArguments.php
+++ b/src/Modifiers/WithArguments.php
@@ -7,7 +7,6 @@
use Sirius\Invokator\ArgumentReference;
use Sirius\Invokator\Invoker;
use Sirius\Invokator\InvokerAwareInterface;
-use Sirius\Invokator\InvokerReference;
class WithArguments implements InvokerAwareInterface
{
@@ -16,7 +15,7 @@ class WithArguments implements InvokerAwareInterface
/**
* @param array $arguments
*/
- public function __construct(public mixed $callable, public array $arguments)
+ public function __construct(public readonly mixed $callable, public readonly array $arguments)
{
}
diff --git a/src/Modifiers/Wrap.php b/src/Modifiers/Wrap.php
index 7aed24d..f25b0e9 100644
--- a/src/Modifiers/Wrap.php
+++ b/src/Modifiers/Wrap.php
@@ -12,10 +12,9 @@ class Wrap implements InvokerAwareInterface
protected Invoker $invoker;
/**
- * @param mixed $callable
* @param callable $wrapperCallback
*/
- public function __construct(public mixed $callable, public $wrapperCallback)
+ public function __construct(public readonly mixed $callable, public readonly mixed $wrapperCallback)
{
}
@@ -29,7 +28,7 @@ public function setInvoker(Invoker $invoker): void
*/
public function __invoke(...$params): mixed
{
- $next = fn () => $this->invoker->invoke($this->callable, ...$params);
+ $next = fn (): mixed => $this->invoker->invoke($this->callable, ...$params);
return call_user_func($this->wrapperCallback, $next);
}
diff --git a/src/PipelinePromise.php b/src/PipelinePromise.php
index 3b62e02..5c2a4d3 100644
--- a/src/PipelinePromise.php
+++ b/src/PipelinePromise.php
@@ -9,7 +9,7 @@ class PipelinePromise
/**
* @param array $params
*/
- public function __construct(public mixed $value, public CallableCollection $remainingCallables, public array $params, public int $retryAfter)
+ public function __construct(public readonly mixed $value, public readonly CallableCollection $remainingCallables, public readonly array $params, public readonly int $retryAfter)
{
}
}
diff --git a/src/Processors/ActionsProcessor.php b/src/Processors/ActionsProcessor.php
index fb976d8..ee82467 100644
--- a/src/Processors/ActionsProcessor.php
+++ b/src/Processors/ActionsProcessor.php
@@ -4,10 +4,10 @@
namespace Sirius\Invokator\Processors;
-use Sirius\Invokator\Invoker;
use Sirius\Invokator\CallableCollection;
use Sirius\Invokator\CallablesRegistryInterface;
use Sirius\Invokator\InvokatorInterface;
+use Sirius\Invokator\Invoker;
use function Sirius\Invokator\limit_arguments;
@@ -16,7 +16,7 @@ class ActionsProcessor implements CallablesRegistryInterface, InvokatorInterface
/**
* @var array
*/
- protected $registry = [];
+ protected array $registry = [];
public function __construct(public Invoker $invoker)
{
diff --git a/src/Processors/CommandBus.php b/src/Processors/CommandBus.php
index d79c332..7de4989 100644
--- a/src/Processors/CommandBus.php
+++ b/src/Processors/CommandBus.php
@@ -4,9 +4,6 @@
namespace Sirius\Invokator\Processors;
-use Sirius\Invokator\InvalidCallableException;
-use Sirius\Invokator\CallableCollection;
-
class CommandBus extends MiddlewareProcessor
{
/**
@@ -30,7 +27,7 @@ public function addMiddleware(string $name, mixed $callable, int $priority = 0):
public function handle(object $command): mixed
{
- $callableCollection = $this->getCopy(get_class($command));
+ $callableCollection = $this->getCopy($command::class);
$callableCollection->add($this->getCallableForCommand($command), PHP_INT_MIN); // to be executed at the end
$result = null;
@@ -39,7 +36,7 @@ public function handle(object $command): mixed
if ($callableCollection->isEmpty()) {
$response = $this->invoker->invoke($nextCallable, $command);
} else {
- $next = fn($result) => $this->processCollection($callableCollection, $command);
+ $next = fn ($result): mixed => $this->processCollection($callableCollection, $command);
$response = $this->invoker->invoke($nextCallable, $command, $next);
}
@@ -51,6 +48,7 @@ public function handle(object $command): mixed
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!');
@@ -58,11 +56,11 @@ public function process(string $name, ...$params): mixed
protected function getCallableForCommand(object $commandInstance): mixed
{
- $commandClass = get_class($commandInstance);
+ $commandClass = $commandInstance::class;
$handler = $this->handlers[$commandClass] ?? preg_replace('/(.+)Command$/', '$1Handler', $commandClass);
if (is_string($handler)) {
- if ( ! class_exists($handler)) {
+ if (! class_exists($handler)) {
// maybe the handler is something like `SomeClass::method` or `SomeClass@method`
return $handler;
}
diff --git a/src/Processors/FiltersProcessor.php b/src/Processors/FiltersProcessor.php
index 2968607..d836325 100644
--- a/src/Processors/FiltersProcessor.php
+++ b/src/Processors/FiltersProcessor.php
@@ -4,10 +4,10 @@
namespace Sirius\Invokator\Processors;
-use Sirius\Invokator\Invoker;
use Sirius\Invokator\CallableCollection;
use Sirius\Invokator\CallablesRegistryInterface;
use Sirius\Invokator\InvokatorInterface;
+use Sirius\Invokator\Invoker;
use function Sirius\Invokator\limit_arguments;
@@ -16,7 +16,7 @@ class FiltersProcessor implements CallablesRegistryInterface, InvokatorInterface
/**
* @var array
*/
- protected $registry = [];
+ protected array $registry = [];
public function __construct(public Invoker $invoker)
{
diff --git a/src/Processors/MiddlewareProcessor.php b/src/Processors/MiddlewareProcessor.php
index d1d4ab2..f4193ba 100644
--- a/src/Processors/MiddlewareProcessor.php
+++ b/src/Processors/MiddlewareProcessor.php
@@ -4,18 +4,21 @@
namespace Sirius\Invokator\Processors;
-use Sirius\Invokator\InvalidCallableException;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Sirius\Invokator\CallableCollection;
+use Sirius\Invokator\InvalidCallableException;
class MiddlewareProcessor extends SimpleCallablesProcessor
{
/**
* @param array $params
*
- * @throws \Psr\Container\ContainerExceptionInterface
- * @throws \Psr\Container\NotFoundExceptionInterface
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
* @throws InvalidCallableException
*/
+ #[\Override]
public function processCollection(CallableCollection $stack, ...$params): mixed
{
$result = null;
@@ -25,7 +28,7 @@ public function processCollection(CallableCollection $stack, ...$params): mixed
if ($stack->isEmpty()) {
$response = $this->invoker->invoke($nextCallable, ...$params);
} else {
- $next = fn ($result) => $this->processCollection($stack, ...$params);
+ $next = fn ($result): mixed => $this->processCollection($stack, ...$params);
$paramsForNext = [...$params, $next];
$response = $this->invoker->invoke($nextCallable, ...$paramsForNext);
}
diff --git a/src/Processors/PipelineProcessor.php b/src/Processors/PipelineProcessor.php
index 495621c..23b3081 100644
--- a/src/Processors/PipelineProcessor.php
+++ b/src/Processors/PipelineProcessor.php
@@ -4,10 +4,10 @@
namespace Sirius\Invokator\Processors;
+use Sirius\Invokator\CallableCollection;
+use Sirius\Invokator\PipelinePromise;
use Sirius\Invokator\SuggestedResume;
use Sirius\Invokator\SuggestedRetry;
-use Sirius\Invokator\PipelinePromise;
-use Sirius\Invokator\CallableCollection;
class PipelineProcessor extends SimpleCallablesProcessor
{
@@ -15,6 +15,7 @@ class PipelineProcessor extends SimpleCallablesProcessor
* @param array $params
*/
+ #[\Override]
public function processCollection(CallableCollection $stack, ...$params): mixed
{
$result = null;
diff --git a/src/Processors/SimpleCallablesProcessor.php b/src/Processors/SimpleCallablesProcessor.php
index 54929d8..fd5372c 100644
--- a/src/Processors/SimpleCallablesProcessor.php
+++ b/src/Processors/SimpleCallablesProcessor.php
@@ -4,17 +4,17 @@
namespace Sirius\Invokator\Processors;
-use Sirius\Invokator\Invoker;
use Sirius\Invokator\CallableCollection;
use Sirius\Invokator\CallablesRegistryInterface;
use Sirius\Invokator\InvokatorInterface;
+use Sirius\Invokator\Invoker;
class SimpleCallablesProcessor implements CallablesRegistryInterface, InvokatorInterface
{
/**
* @var array
*/
- protected $registry = [];
+ protected array $registry = [];
public function __construct(public Invoker $invoker)
{
diff --git a/src/SuggestedResume.php b/src/SuggestedResume.php
index a46e4a5..ffcbb03 100644
--- a/src/SuggestedResume.php
+++ b/src/SuggestedResume.php
@@ -6,7 +6,7 @@
class SuggestedResume
{
- public function __construct(public mixed $value, public int $delay = 0)
+ public function __construct(public readonly mixed $value, public readonly int $delay = 0)
{
}
}
diff --git a/src/SuggestedRetry.php b/src/SuggestedRetry.php
index 55402ad..034d6ba 100644
--- a/src/SuggestedRetry.php
+++ b/src/SuggestedRetry.php
@@ -6,7 +6,7 @@
class SuggestedRetry
{
- public function __construct(public int $retryAfter = 0)
+ public function __construct(public readonly int $retryAfter = 0)
{
}
}
diff --git a/tests/Laravel/BladeTest.php b/tests/Laravel/BladeTest.php
new file mode 100644
index 0000000..f3284fd
--- /dev/null
+++ b/tests/Laravel/BladeTest.php
@@ -0,0 +1,35 @@
+add(function ($user) use (&$log): void {
+ $log[] = $user;
+ });
+
+ // Blade only recognises @directive when it isn't glued to a preceding word char
+ // (here it follows `>`). The action returns null, so the directive emits nothing.
+ $html = Blade::render("@do_action('analytics', \$user)", ['user' => 'sam']);
+
+ $this->assertSame('', $html);
+ $this->assertSame(['sam'], $log);
+ }
+
+ public function test_do_filter_function_in_blade_echo(): void
+ {
+ Invokator::filter('shout')->add(fn ($v): string => strtoupper((string) $v) . '!');
+
+ $html = Blade::render("{{ do_filter('shout', \$title) }}", ['title' => 'hey']);
+
+ $this->assertSame('HEY!', $html);
+ }
+}
diff --git a/tests/Laravel/ContainerWiringTest.php b/tests/Laravel/ContainerWiringTest.php
new file mode 100644
index 0000000..b4d0e82
--- /dev/null
+++ b/tests/Laravel/ContainerWiringTest.php
@@ -0,0 +1,30 @@
+add(Greeter::class . '@greet');
+
+ $this->assertSame('Hello, Sam', Invokator::filter('greet', 'Sam'));
+ }
+
+ public function test_plain_function_name_string_callable_works_under_the_laravel_container(): void
+ {
+ // Laravel's container throws for unbound ids; the Invoker must fall through to using
+ // 'trim' directly rather than asking the container to resolve it.
+ Invokator::filter('clean')->add('trim');
+
+ $this->assertSame('hi', Invokator::filter('clean', ' hi '));
+ }
+}
diff --git a/tests/Laravel/EventTest.php b/tests/Laravel/EventTest.php
new file mode 100644
index 0000000..7feb48d
--- /dev/null
+++ b/tests/Laravel/EventTest.php
@@ -0,0 +1,57 @@
+add(function (SampleEvent $event) use (&$seen): void {
+ $seen[] = $event->payload;
+ });
+
+ $returned = Invokator::dispatch(new SampleEvent('hello'));
+
+ $this->assertInstanceOf(SampleEvent::class, $returned);
+ $this->assertSame('hello', $returned->payload);
+ $this->assertSame(['hello'], $seen);
+ }
+
+ public function test_do_event_helper_dispatches(): void
+ {
+ $seen = [];
+ Invokator::event(SampleEvent::class)->add(function (SampleEvent $event) use (&$seen): void {
+ $seen[] = $event->payload;
+ });
+
+ do_event(new SampleEvent('from-helper'));
+
+ $this->assertSame(['from-helper'], $seen);
+ }
+
+ public function test_stoppable_event_halts_propagation(): void
+ {
+ $order = [];
+ Invokator::event(StoppableSampleEvent::class)->add(function () use (&$order): void {
+ $order[] = 1;
+ });
+ Invokator::event(StoppableSampleEvent::class)->add(function (StoppableSampleEvent $event) use (&$order): void {
+ $order[] = 2;
+ $event->stopPropagation();
+ });
+ Invokator::event(StoppableSampleEvent::class)->add(function () use (&$order): void {
+ $order[] = 3;
+ });
+
+ Invokator::dispatch(new StoppableSampleEvent());
+
+ $this->assertSame([1, 2], $order);
+ }
+}
diff --git a/tests/Laravel/FacadeTest.php b/tests/Laravel/FacadeTest.php
new file mode 100644
index 0000000..31f6d2d
--- /dev/null
+++ b/tests/Laravel/FacadeTest.php
@@ -0,0 +1,61 @@
+add(fn ($x): string => $x . 'a')
+ ->add(fn ($x): string => $x . 'b');
+
+ $this->assertInstanceOf(Registrar::class, $registrar);
+ $this->assertSame('xab', Invokator::pipeline('p', 'x'));
+ }
+
+ public function test_filter_transforms_the_value(): void
+ {
+ Invokator::filter('up')->add(fn ($v) => strtoupper((string) $v));
+
+ $this->assertSame('HELLO', Invokator::filter('up', 'hello'));
+ }
+
+ public function test_action_runs_for_side_effects_and_returns_null(): void
+ {
+ $log = [];
+ Invokator::action('log')->add(function ($x) use (&$log): void {
+ $log[] = "got:$x";
+ });
+
+ $result = Invokator::action('log', 'hi');
+
+ $this->assertNull($result);
+ $this->assertSame(['got:hi'], $log);
+ }
+
+ public function test_middleware_wraps_with_next(): void
+ {
+ Invokator::middleware('m')
+ ->add(fn ($name, $next = null) => 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', 'world'));
+ }
+
+ public function test_priority_controls_execution_order(): void
+ {
+ Invokator::pipeline('ordered')
+ ->add(fn ($x): string => $x . '-low', 0)
+ ->add(fn ($x): string => $x . '-high', 10);
+
+ // higher priority runs first
+ $this->assertSame('start-high-low', Invokator::pipeline('ordered', 'start'));
+ }
+}
diff --git a/tests/Laravel/Fixtures/Dependency.php b/tests/Laravel/Fixtures/Dependency.php
new file mode 100644
index 0000000..30875d9
--- /dev/null
+++ b/tests/Laravel/Fixtures/Dependency.php
@@ -0,0 +1,13 @@
+dependency->prefix() . $name;
+ }
+}
diff --git a/tests/Laravel/Fixtures/SampleEvent.php b/tests/Laravel/Fixtures/SampleEvent.php
new file mode 100644
index 0000000..79ea1ef
--- /dev/null
+++ b/tests/Laravel/Fixtures/SampleEvent.php
@@ -0,0 +1,12 @@
+add(fn ($x): string => $x . 'a')
+ ->add(fn ($x): string => $x . 'b');
+
+ $this->assertInstanceOf(Registrar::class, $registrar);
+ $this->assertSame('xab', do_pipeline('p', 'x'));
+ }
+
+ public function test_do_filter_transforms_the_value(): void
+ {
+ do_filter('up')->add(fn ($v) => strtoupper((string) $v));
+
+ $this->assertSame('HELLO', do_filter('up', 'hello'));
+ }
+
+ public function test_do_action_runs_side_effects(): void
+ {
+ $log = [];
+ do_action('log')->add(function ($x) use (&$log): void {
+ $log[] = $x;
+ });
+
+ $this->assertNull(do_action('log', 'hi'));
+ $this->assertSame(['hi'], $log);
+ }
+
+ public function test_do_middleware_wraps_with_next(): void
+ {
+ do_middleware('m')
+ ->add(fn ($name, $next = null) => strtoupper((string) $next($name)))
+ ->add(fn ($name, $next = null): string => 'Hello ' . $next($name))
+ ->add(fn ($name, $next = null) => $name);
+
+ $this->assertSame('HELLO WORLD', do_middleware('m', 'world'));
+ }
+}
diff --git a/tests/Laravel/TestCase.php b/tests/Laravel/TestCase.php
new file mode 100644
index 0000000..3933ac1
--- /dev/null
+++ b/tests/Laravel/TestCase.php
@@ -0,0 +1,30 @@
+
+ */
+ protected function getPackageProviders($app): array
+ {
+ return [SiriusInvokatorServiceProvider::class];
+ }
+
+ /**
+ * @param \Illuminate\Foundation\Application $app
+ * @return array
+ */
+ protected function getPackageAliases($app): array
+ {
+ return ['Invokator' => Invokator::class];
+ }
+}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index ebdda95..1be4670 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -2,31 +2,30 @@
-
-
- ./../src
-
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.0/phpunit.xsd">
+
-
./src/
+
+ ./Laravel/
+
+
+
+ ./../src
+
+
diff --git a/tests/phpunit_bootstrap.php b/tests/phpunit_bootstrap.php
index 4280de8..74f71da 100644
--- a/tests/phpunit_bootstrap.php
+++ b/tests/phpunit_bootstrap.php
@@ -1,5 +1,7 @@
first + $command->second;
}
diff --git a/tests/src/Event/DispatcherTest.php b/tests/src/Event/DispatcherTest.php
index a5c326b..b0f2865 100644
--- a/tests/src/Event/DispatcherTest.php
+++ b/tests/src/Event/DispatcherTest.php
@@ -10,16 +10,16 @@
class DispatcherTest extends TestCase
{
- public function test_subscribers_are_executed_in_order()
+ public function test_subscribers_are_executed_in_order(): void
{
$dispatcher = new Dispatcher(new ListenerProvider(), $this->getInvoker());
- $dispatcher->subscribeTo('event_with_name', function (object $event) {
+ $dispatcher->subscribeTo('event_with_name', function (object $event): void {
static::$results[] = 'subscriber 1';
});
- $dispatcher->subscribeTo('event_with_name', function (object $event) {
+ $dispatcher->subscribeTo('event_with_name', function (object $event): void {
static::$results[] = 'subscriber 2';
});
- $dispatcher->subscribeTo('event_with_name', function (object $event) {
+ $dispatcher->subscribeTo('event_with_name', function (object $event): void {
static::$results[] = 'subscriber 3';
});
@@ -32,17 +32,17 @@ public function test_subscribers_are_executed_in_order()
], static::$results);
}
- public function test_stoppable_events()
+ public function test_stoppable_events(): void
{
$dispatcher = new Dispatcher(new ListenerProvider(), $this->getInvoker());
- $dispatcher->subscribeTo(StoppableEvent::class, function (object $event) {
+ $dispatcher->subscribeTo(StoppableEvent::class, function (object $event): void {
static::$results[] = 'subscriber 1';
});
- $dispatcher->subscribeTo(StoppableEvent::class, function (object $event) {
+ $dispatcher->subscribeTo(StoppableEvent::class, function (object $event): void {
static::$results[] = 'subscriber 2';
$event->stopPropagation();
});
- $dispatcher->subscribeTo(StoppableEvent::class, function (object $event) {
+ $dispatcher->subscribeTo(StoppableEvent::class, function (object $event): void {
static::$results[] = 'subscriber 3';
});
@@ -54,10 +54,10 @@ public function test_stoppable_events()
], static::$results);
}
- public function test_once_subscribers()
+ public function test_once_subscribers(): void
{
$dispatcher = new Dispatcher(new ListenerProvider(), $this->getInvoker());
- $dispatcher->subscribeOnceTo(EventWithoutName::class, function (object $event) {
+ $dispatcher->subscribeOnceTo(EventWithoutName::class, function (object $event): void {
static::$results[] = 'once subscriber';
});
diff --git a/tests/src/Event/EventWithName.php b/tests/src/Event/EventWithName.php
index e2f5dbe..85a7613 100644
--- a/tests/src/Event/EventWithName.php
+++ b/tests/src/Event/EventWithName.php
@@ -1,5 +1,7 @@
*/
+ private array $services = [];
+
+ public function set(string $id, mixed $value): void
+ {
+ $this->services[$id] = $value;
+ }
+
+ public function get(string $id): mixed
+ {
+ if (! $this->has($id)) {
+ throw new StrictNotFoundException("No entry found for: {$id}");
+ }
+
+ return $this->services[$id];
+ }
+
+ public function has(string $id): bool
+ {
+ return array_key_exists($id, $this->services);
+ }
+}
+
+class Shouter
+{
+ public static function shout(string $value): string
+ {
+ return strtoupper($value);
+ }
+}
+
+class InvokerStrictContainerTest extends PHPUnitTestCase
+{
+ public function test_plain_function_name_string_callable_is_used_directly(): void
+ {
+ $invoker = new Invoker(new StrictContainer());
+
+ // 'trim' is callable but not a bound service: get() would throw, so it must be used as-is.
+ $this->assertSame('hi', $invoker->invoke('trim', ' hi '));
+ }
+
+ public function test_static_method_string_callable_is_used_directly(): void
+ {
+ $invoker = new Invoker(new StrictContainer());
+
+ $this->assertSame('HELLO', $invoker->invoke(Shouter::class . '::shout', 'hello'));
+ }
+
+ public function test_bound_callable_name_still_resolves_from_the_container(): void
+ {
+ $container = new StrictContainer();
+ // A callable-looking string that is ALSO bound should resolve to the bound service
+ // (the has()+get() branch), proving the guard doesn't disable container overrides.
+ $container->set('strtoupper', fn (string $value): string => 'OVERRIDDEN:' . $value);
+ $invoker = new Invoker($container);
+
+ $this->assertSame('OVERRIDDEN:x', $invoker->invoke('strtoupper', 'x'));
+ }
+}
diff --git a/tests/src/Modifiers/OnceTest.php b/tests/src/Modifiers/OnceTest.php
index 9d90bdf..ca50985 100644
--- a/tests/src/Modifiers/OnceTest.php
+++ b/tests/src/Modifiers/OnceTest.php
@@ -16,10 +16,10 @@ protected function setUp(): void
static::$results = [];
}
- public function test_modifier()
+ public function test_modifier(): void
{
$processor = new SimpleCallablesProcessor($this->getInvoker());
- $processor->add('test', once(function ($param_1, $param_2) {
+ $processor->add('test', once(function (string $param_1, $param_2): void {
static::$results[] = sprintf("anonymous function(%s, %s)", $param_1, $param_2);
}));
diff --git a/tests/src/Modifiers/ResolveArgumentsTest.php b/tests/src/Modifiers/ResolveArgumentsTest.php
index 8d6f698..94da202 100644
--- a/tests/src/Modifiers/ResolveArgumentsTest.php
+++ b/tests/src/Modifiers/ResolveArgumentsTest.php
@@ -26,11 +26,11 @@ protected function setUp(): void
$this->getContainer()->register(DependencyClass::class, new DependencyClass());
}
- public function test_resolve_arguments()
+ 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){
+ $processor->add('test', wrap(resolve(DependentClass::class . '@multiply', ['firstNumber' => 5, 'secondNumber' => arg(0)]), function($next): void{
static::$results[] = $next();
}));
diff --git a/tests/src/Modifiers/WithArgumentsTest.php b/tests/src/Modifiers/WithArgumentsTest.php
index 8188cf7..cf0dc3f 100644
--- a/tests/src/Modifiers/WithArgumentsTest.php
+++ b/tests/src/Modifiers/WithArgumentsTest.php
@@ -19,11 +19,11 @@ protected function setUp(): void
static::$results = [];
}
- public function test_modifier_with_refs()
+ public function test_modifier_with_refs(): void
{
$this->getContainer()->register('test_param', 'C');
$processor = new SimpleCallablesProcessor($this->getInvoker());
- $processor->add('test', with_arguments(function ($param_1, $param_2, $param_3, $param_4) {
+ $processor->add('test', 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']));
@@ -34,11 +34,11 @@ public function test_modifier_with_refs()
], static::$results);
}
- public function test_modifier_with_invoker_result()
+ 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 ($param_1, $param_2, $param_3) {
+ $processor->add('test', 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)]));
diff --git a/tests/src/Modifiers/WrapTest.php b/tests/src/Modifiers/WrapTest.php
index 11e2de9..bf18c13 100644
--- a/tests/src/Modifiers/WrapTest.php
+++ b/tests/src/Modifiers/WrapTest.php
@@ -15,11 +15,11 @@ protected function setUp(): void
static::$results = [];
}
- public function test_modifier()
+ public function test_modifier(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new SimpleCallablesProcessor($this->getInvoker());
- $processor->add('test', wrap(function ($param_1, $param_2) {
+ $processor->add('test', 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';
diff --git a/tests/src/Processors/ActionsProcessorTest.php b/tests/src/Processors/ActionsProcessorTest.php
index 352b581..047c9d3 100644
--- a/tests/src/Processors/ActionsProcessorTest.php
+++ b/tests/src/Processors/ActionsProcessorTest.php
@@ -7,11 +7,11 @@
class ActionsProcessorTest extends TestCase
{
- public function test_actions_processor()
+ public function test_actions_processor(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new ActionsProcessor($this->getInvoker());
- $processor->add('test', function ($param_1) {
+ $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);
diff --git a/tests/src/Processors/CommandBusTest.php b/tests/src/Processors/CommandBusTest.php
index 68118b2..0a2dceb 100644
--- a/tests/src/Processors/CommandBusTest.php
+++ b/tests/src/Processors/CommandBusTest.php
@@ -8,35 +8,29 @@
class CommandBusTest extends TestCase
{
- public function test_automatic_handler()
+ public function test_automatic_handler(): void
{
$this->getContainer()->register(SampleHandler::class, new SampleHandler());
$bus = new CommandBus($this->getInvoker());
- $bus->addMiddleware(SampleCommand::class, function ($name, $next = null) {
- return 2 * $next($name);
- });
+ $bus->addMiddleware(SampleCommand::class, fn($name, $next = null): int|float => 2 * $next($name));
$result = $bus->handle(new SampleCommand(2, 5));
$this->assertEquals(2 * (2 + 5), $result);
}
- public function test_custom_handler()
+ public function test_custom_handler(): void
{
$bus = new CommandBus($this->getInvoker());
- $bus->addMiddleware(SampleCommand::class, function ($name, $next = null) {
- return 2 * $next($name);
- });
- $bus->register(SampleCommand::class, function (SampleCommand $command) {
- return $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));
$this->assertEquals(2 * 2 * 5, $result);
}
- public function test_no_middleware()
+ public function test_no_middleware(): void
{
$this->getContainer()->register(SampleHandler::class, new SampleHandler());
$bus = new CommandBus($this->getInvoker());
diff --git a/tests/src/Processors/FiltersProcessorTest.php b/tests/src/Processors/FiltersProcessorTest.php
index 7b88b94..bb36fd0 100644
--- a/tests/src/Processors/FiltersProcessorTest.php
+++ b/tests/src/Processors/FiltersProcessorTest.php
@@ -7,13 +7,11 @@
class FiltersProcessorTest extends TestCase
{
- public function test_filters_processor()
+ public function test_filters_processor(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new FiltersProcessor($this->getInvoker());
- $processor->add('test', function ($name) {
- return ' hello ' . $name;
- }, 0, 1);
+ $processor->add('test', fn($name): string => ' hello ' . $name, 0, 1);
$processor->add('test', 'trim', 0, 1);
$processor->add('test', 'ucwords', 0, 2);
diff --git a/tests/src/Processors/MiddlewareProcessorTest.php b/tests/src/Processors/MiddlewareProcessorTest.php
index 6ad4580..3eeb1c3 100644
--- a/tests/src/Processors/MiddlewareProcessorTest.php
+++ b/tests/src/Processors/MiddlewareProcessorTest.php
@@ -7,19 +7,13 @@
class MiddlewareProcessorTest extends TestCase
{
- public function test_middleware_processor()
+ public function test_middleware_processor(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new MiddlewareProcessor($this->getInvoker());
- $processor->add('test', function ($name, $next = null) {
- return ucwords($next($name));
- });
- $processor->add('test', function ($name, $next = null) {
- return 'Hello ' . $next($name);
- });
- $processor->add('test', function ($name, $next = null) {
- return $name;
- });
+ $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');
diff --git a/tests/src/Processors/PipelineProcessorTest.php b/tests/src/Processors/PipelineProcessorTest.php
index 6efb5c2..5071f2b 100644
--- a/tests/src/Processors/PipelineProcessorTest.php
+++ b/tests/src/Processors/PipelineProcessorTest.php
@@ -7,13 +7,11 @@
class PipelineProcessorTest extends TestCase
{
- public function test_pipeline_processor()
+ public function test_pipeline_processor(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new PipelineProcessor($this->getInvoker());
- $processor->add('test', function ($name) {
- return ' hello ' . $name;
- }, 1, 1);
+ $processor->add('test', fn($name): string => ' hello ' . $name, 1);
$processor->add('test', 'trim', 1);
$processor->add('test', 'ucwords', 1);
diff --git a/tests/src/Processors/SimpleStackProcessorTest.php b/tests/src/Processors/SimpleStackProcessorTest.php
index 1b9f6b3..bcf31a1 100644
--- a/tests/src/Processors/SimpleStackProcessorTest.php
+++ b/tests/src/Processors/SimpleStackProcessorTest.php
@@ -7,11 +7,11 @@
class SimpleStackProcessorTest extends TestCase
{
- public function test_simple_stack_processor()
+ public function test_simple_stack_processor(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new SimpleCallablesProcessor($this->getInvoker());
- $processor->add('test', function ($param_1, $param_2) {
+ $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');
@@ -26,11 +26,11 @@ public function test_simple_stack_processor()
], static::$results);
}
- public function test_execution_priority()
+ public function test_execution_priority(): void
{
$this->getContainer()->register(SimpleCallables::class, new SimpleCallables);
$processor = new SimpleCallablesProcessor($this->getInvoker());
- $processor->add('test', function ($param_1, $param_2) {
+ $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);
diff --git a/tests/src/StackTest.php b/tests/src/StackTest.php
index 39d72b1..fb3157b 100644
--- a/tests/src/StackTest.php
+++ b/tests/src/StackTest.php
@@ -4,7 +4,7 @@
class StackTest extends TestCase
{
- function test_priorities_are_respected()
+ function test_priorities_are_respected(): void
{
$stack = new CallableCollection();
$stack->add('callable_1', 10);
@@ -24,7 +24,7 @@ function test_priorities_are_respected()
], $this->getCallablesFromStack($stack));
}
- public function test_serialization_of_simple_stack()
+ public function test_serialization_of_simple_stack(): void
{
$stack = new CallableCollection();
$stack->add('callable_1', 10);
diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php
index 1257179..b9e297a 100644
--- a/tests/src/TestCase.php
+++ b/tests/src/TestCase.php
@@ -24,7 +24,7 @@ public function has(string $id): bool
return isset($this->services[$id]);
}
- public function register(string $id, $implementation)
+ public function register(string $id, $implementation): void
{
$this->services[$id] = $implementation;
}
@@ -50,12 +50,15 @@ protected function setUp(): void
static::$results = [];
}
- protected function getInvoker()
+ protected function getInvoker(): \Sirius\Invokator\Invoker
{
return new Invoker($this->getContainer());
}
- protected function getCallablesFromStack(CallableCollection $stack)
+ /**
+ * @return mixed[]
+ */
+ protected function getCallablesFromStack(CallableCollection $stack): array
{
$callables = [];
do {
@@ -68,7 +71,7 @@ protected function getCallablesFromStack(CallableCollection $stack)
return $callables;
}
- protected function getContainer()
+ protected function getContainer(): \Sirius\Invokator\Container
{
if ( ! isset($this->container)) {
$this->container = new Container();
diff --git a/tests/src/Utilities/DependencyClass.php b/tests/src/Utilities/DependencyClass.php
index 7031311..a1b0ec2 100644
--- a/tests/src/Utilities/DependencyClass.php
+++ b/tests/src/Utilities/DependencyClass.php
@@ -1,5 +1,7 @@
add5($secondNumber);
}
diff --git a/tests/src/Utilities/SimpleCallables.php b/tests/src/Utilities/SimpleCallables.php
index 48e69d4..880bb0a 100644
--- a/tests/src/Utilities/SimpleCallables.php
+++ b/tests/src/Utilities/SimpleCallables.php
@@ -1,5 +1,7 @@