Skip to content

Commit 23e7997

Browse files
committed
Add comprehensive documentation for callable definitions and update related guides
1 parent 4fe7321 commit 23e7997

14 files changed

Lines changed: 365 additions & 304 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Callable Definitions Extended
2+
3+
Сallable definitions in `yiisoft/queue` extend [native PHP callables](https://www.php.net/manual/en/language.types.callable.php). That means, there are two types of definitions. Nevertheless, each of them may define dependency list in their parameter lists, which will be resolved via [yiisoft/injector](https://github.com/yiisoft/injector) and a DI Container.
4+
It is used across the package to convert configuration definitions into real callables.
5+
6+
## Type 1: Native PHP callable
7+
8+
When you define a callable in a such manner, they are not modified in any way and are called as is. An only difference is that you can define dependency list in their parameter lists, which will be resolved via [yiisoft/injector](https://github.com/yiisoft/injector) and a DI Container.
9+
As you can see in the [PHP documentation](https://www.php.net/manual/en/language.types.callable.php), there are several ways to define a native callable:
10+
11+
- **Closure (lambda function)**. It may be static. Example:
12+
```php
13+
$callable = static function(Update $update) {
14+
// do stuff
15+
}
16+
```
17+
- **First class callable**. It's a Closure too, BTW ;) Example:
18+
```php
19+
$callable = trim(...);
20+
$callable2 = $this->foo(...);
21+
```
22+
- **A class static function**. When a class has a static function, an array syntax may be used:
23+
```php
24+
$callable = [Foo::class, 'bar']; // this will be called the same way as Foo::bar();
25+
```
26+
- **An object method**. The same as above, but with an object and a non-static method:
27+
```php
28+
$foo = new Foo();
29+
$callable = [$foo, 'bar']; // this will be called the same way as $foo->bar();
30+
```
31+
- **A class static function as a string**. I don't recommend you to use this ability, as it's non-obvious and
32+
hard to refactor, but it still exists:
33+
```php
34+
$callable = 'Foo::bar'; // this will be called the same way as Foo::bar();
35+
```
36+
- **A name of a named function**:
37+
```php
38+
function foo() {
39+
// do stuff
40+
}
41+
$callable = 'foo';
42+
$callable2 = 'array_map';
43+
```
44+
- **Callable objects**. An object with [the `__invoke` method](https://www.php.net/manual/en/language.oop5.magic.php#object.invoke) implemented:
45+
```php
46+
class Foo
47+
{
48+
public function __invoke()
49+
{
50+
// do stuff
51+
}
52+
}
53+
54+
$callable = new Foo();
55+
```
56+
57+
## Type 2: Callable definition extensions (via container)
58+
59+
Under the hood, this extension behaves exactly like the **Type 1** ones. But there is a major difference too:
60+
all the objects are instantiated automatically with a PSR-11 DI Container with all their dependencies
61+
and in a lazy way (only when they are really needed).
62+
Ways to define an extended callable:
63+
64+
- An object method through a class name or alias:
65+
```php
66+
final readonly class Foo
67+
{
68+
public function __construct(private MyHeavyDependency $dependency) {}
69+
70+
public function bar()
71+
{
72+
// do stuff
73+
}
74+
}
75+
76+
$callable = [Foo::class, 'bar'];
77+
```
78+
Here is a simplified example of how it works:
79+
```php
80+
if ($container->has($callable[0])) {
81+
$callable[0] = $container->get($callable[0])
82+
}
83+
84+
$callable();
85+
```
86+
- Class name of an object with [the `__invoke` method](https://www.php.net/manual/en/language.oop5.magic.php#object.invoke) implemented:
87+
```php
88+
$callable = Foo::class;
89+
```
90+
It works the same way as above: an object will be retrieved from a DI container and called as a function.
91+
92+
_Note: you can use an alias registered in your DI Container instead of a class name._ This will also work if you have a "class alias" definition in container:
93+
```php
94+
$callable = 'class alias'; // for a "callable object"
95+
$callable2 = ['class alias', 'foo']; // to call "foo" method of an object found by "class alias" in DI Container
96+
```
97+
98+
## Invalid definitions
99+
100+
The factory throws `Yiisoft\Queue\Middleware\InvalidCallableConfigurationException` when it cannot create a callable (for example: `null`, unsupported array format, missing method, container entry is not callable).

docs/guide/en/configuration-manual.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ To use the queue, you need to create instances of the following classes:
1616
use Yiisoft\Queue\Adapter\SynchronousAdapter;
1717
use Yiisoft\Queue\Queue;
1818
use Yiisoft\Queue\Worker\Worker;
19+
use Yiisoft\Queue\Middleware\CallableFactory;
1920
use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareDispatcher;
2021
use Yiisoft\Queue\Middleware\Consume\MiddlewareFactoryConsume;
2122
use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareDispatcher;
@@ -33,13 +34,15 @@ $handlers = [
3334
FileDownloader::class => [FileDownloader::class, 'handle'],
3435
];
3536

37+
$callableFactory = new CallableFactory($container);
38+
3639
// Create middleware dispatchers
3740
$consumeMiddlewareDispatcher = new ConsumeMiddlewareDispatcher(
38-
new MiddlewareFactoryConsume($container),
41+
new MiddlewareFactoryConsume($container, $callableFactory),
3942
);
4043

4144
$failureMiddlewareDispatcher = new FailureMiddlewareDispatcher(
42-
new MiddlewareFactoryFailure($container),
45+
new MiddlewareFactoryFailure($container, $callableFactory),
4346
[],
4447
);
4548

@@ -55,6 +58,7 @@ $worker = new Worker(
5558
$container,
5659
$consumeMiddlewareDispatcher,
5760
$failureMiddlewareDispatcher,
61+
$callableFactory,
5862
);
5963

6064
// Create queue with adapter

docs/guide/en/error-handling.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ Often when some message handling is failing, we want to retry its execution a co
44

55
## Configuration
66

7-
Here below is configuration via [yiisoft/config](https://github.com/yiisoft/config). If you don't use it, you should add a middleware definition list (in the `middlewares-fail` key here) to the `FailureMiddlewareDispatcher` by your own.
8-
9-
Configuration should be passed to the `yiisoft/queue.fail-strategy-pipelines` key of the `params` config to work with the [yiisoft/config](https://github.com/yiisoft/config). You can define different failure handling pipelines for each queue channel. Let's see and describe an example:
7+
Here below is configuration via [yiisoft/config](https://github.com/yiisoft/config). If you don't use it, you should add a middleware definition list (in the `middlewares-fail` key here) to the `FailureMiddlewareDispatcher` by your own. You can define different failure handling pipelines for each queue channel. The example below defines two different failure handling pipelines:
108

119
```php
1210
'yiisoft/queue' => [
@@ -39,11 +37,15 @@ Configuration should be passed to the `yiisoft/queue.fail-strategy-pipelines` ke
3937
]
4038
```
4139

42-
Keys here except `FailureMiddlewareDispatcher::DEFAULT_PIPELINE` are queue channel names, and values are lists of `FailureMiddlewareInterface` definitions. `FailureMiddlewareDispatcher::DEFAULT_PIPELINE` defines a default pipeline to apply to channels without an explicitly defined failure strategy pipeline. Each middleware definition must be one of:
40+
Here is the meaning of the keys:
41+
- The `failed-messages` key couples the defined pipeline with the `failed-messages` queue channel.
42+
- The `FailureMiddlewareDispatcher::DEFAULT_PIPELINE` key couples the defined pipeline with all queue channels without an explicitly defined failure strategy pipeline.
43+
44+
Each middleware definition must be one of:
4345
- A ready-to-use `MiddlewareFailureInterface` object like `new FooMiddleware()`.
4446
- A valid definition for the [yiisoft/definitions](https://github.com/yiisoft/definitions). It must describe an object, implementing the `MiddlewareFailureInterface`.
45-
- A callable: `fn() => // do stuff`, `$object->foo(...)`, etc. It will be executed through the [yiisoft/injector](https://github.com/yiisoft/injector), so all the dependencies of your callable will be resolved. You can also define a "callable-looking" array, where an object will be instantiated with a DI container: `[FooMiddleware::class, 'handle']`.
46-
- A string for your DI container to resolve the middleware, e.g. `FooMiddleware::class`.
47+
- An [extended callable definition](callable-definitions-extended.md).
48+
- An id string for your DI container to resolve a middleware, e.g. `FooMiddleware::class`.
4749

4850
In the example above failures will be handled this way (look the concrete middleware description below):
4951

@@ -58,15 +60,15 @@ Failures of messages, which are initially sent to the `failed-messages` channel,
5860

5961
Let's see the built-in defaults.
6062

61-
### SendAgainMiddleware
63+
### [SendAgainMiddleware](../../../src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php)
6264

6365
This strategy simply resends the given message to a queue. Let's see the constructor parameters through which it's configured:
6466

6567
- `id` - A unique string. Allows to use this strategy more than once for the same message, just like in example above.
6668
- `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up.
6769
- `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue channel it came from. When the `queue` parameter is set to `null`, a message will be sent to the same channel it came from.
6870

69-
### ExponentialDelayMiddleware
71+
### [ExponentialDelayMiddleware](../../../src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php)
7072

7173
This strategy does the same thing as the `SendAgainMiddleware` with a single difference: it resends a message with an exponentially increasing delay. The delay **must** be implemented by the used `AdapterInterface` implementation.
7274

@@ -82,9 +84,11 @@ It's configured via constructor parameters, too. Here they are:
8284
## How to create a custom Failure Middleware?
8385

8486
All you need is to implement the `MiddlewareFailureInterface` and add your implementation definition to the [configuration](#configuration).
85-
This interface has the only method `handle`. And the method has these parameters:
86-
- `ConsumeRequest $request` - a request for a message handling. It consists of a message and a queue the message came from.
87-
- `Throwable $exception` - an exception thrown on the `request` handling
87+
This interface has the only method `handle` with these parameters:
88+
- [`FailureHandlingRequest $request`](../../../src/Middleware/FailureHandling/FailureHandlingRequest.php) - a request for a message handling. It consists of
89+
- a [message](../../../src/Message/MessageInterface.php)
90+
- a `Throwable $exception` object thrown on the `request` handling
91+
- a queue the message came from
8892
- `MessageFailureHandlerInterface $handler` - failure strategy pipeline continuation. Your Middleware should call `$pipeline->handle()` when it shouldn't interrupt failure pipeline execution.
8993

90-
> Note: your strategy have to check by its own if it should be applied. Look into [`SendAgainMiddleware::suites()`](../../src/Middleware/Implementation/FailureMiddleware/Middleware/SendAgainMiddleware.php#L52) for an example.
94+
> Note: your strategy have to check by its own if it should be applied. Look into [`SendAgainMiddleware::suites()`](../../../src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php#L54) for an example.

docs/guide/en/message-handler.md

Lines changed: 8 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ Handler definitions are configured in:
99

1010
## Supported handler definition formats
1111

12-
`Worker` supports a limited set of formats. Below are the exact formats that are converted to a callable.
13-
1412
### 1. HandlerInterface implementation (without mapping)
1513

16-
If your handler is a dedicated class implementing `Yiisoft\Queue\Message\MessageHandlerInterface`, you can use the class name itself as the message handler name.
14+
If your handler is a dedicated class implementing `Yiisoft\Queue\Message\MessageHandlerInterface`, you can use the class name itself as the message handler name (FQCN) if your DI container can resolve the handler class.
15+
16+
> By default the [yiisoft/di](https://github.com/yiisoft/di) container resolves all FQCNs into corresponding class objects.
1717
1818
This is the default and most convenient option when the producer and the consumer are the same application.
1919

@@ -57,9 +57,9 @@ Not needed
5757
- Producer and consumer are the same application.
5858
- You control message creation code and can safely use FQCN as the handler name.
5959

60-
### 2. Closure
60+
### 2. Named handlers
6161

62-
In this and all the cases below, you should use a proper handler name when pushing a `Message` instead of a handler class name in the example above:
62+
In this case you should use a proper handler name when pushing a `Message` instead of a handler class name as in the example above:
6363

6464
```php
6565
new \Yiisoft\Queue\Message\Message('send-email', ['data' => '...']);
@@ -73,105 +73,13 @@ Map handler name to a closure in `$params`:
7373
return [
7474
'yiisoft/queue' => [
7575
'handlers' => [
76-
'send-email' => static fn (\Yiisoft\Queue\Message\MessageInterface $message, \App\Foo $foo) => $foo->bar($message->getData()),
77-
],
78-
],
79-
];
80-
```
81-
82-
**How it works**:
83-
84-
- A `Closure` is accepted as-is.
85-
- The worker executes it using `Injector`, so you may type-hint extra dependencies in the closure parameters.
86-
87-
**Pros**:
88-
89-
- Very simple for small tasks and quick prototypes.
90-
- Easy to inject extra services via `Injector`.
91-
92-
**Cons**:
93-
94-
- Less reusable and harder to unit-test than a dedicated class.
95-
- Easy to accidentally put non-trivial business logic into config.
96-
- Harder to maintain and refactor as the logic grows.
97-
98-
**Use when**:
99-
100-
- You're prototyping async workflows and going to refactor it later into a proper handler class.
101-
- You want a quick "glue" handler that delegates to services.
102-
103-
### 3. Container ID string
104-
105-
**Config**:
106-
107-
```php
108-
return [
109-
'yiisoft/queue' => [
110-
'handlers' => [
111-
'file-download' => FileDownloader::class,
112-
],
113-
],
114-
];
115-
```
116-
117-
**How it works**:
118-
119-
The handler object is retrieved from the DI container. In this case the handler class should either
120-
121-
- have the `__invoke()` method, which receives a message parameter,
122-
- implement `Yiisoft\Queue\Message\MessageHandlerInterface` (then the `$handler->handle(...)` method is called).
123-
124-
If the resolved service is neither callable nor a `MessageHandlerInterface`, the handler is treated as invalid.
125-
126-
**Pros**:
127-
128-
- Short and clean configuration.
129-
- Supports invokable handlers and `MessageHandlerInterface` handlers.
130-
131-
**Cons**:
132-
133-
—
134-
135-
**Use when**:
136-
137-
- You already registered handlers in DI (recommended for production).
138-
- You prefer invokable handlers (`__invoke`) or `MessageHandlerInterface`.
139-
140-
### 4. Two-element array of strings: `[classOrServiceId, method]`
141-
142-
**Config**:
143-
144-
```php
145-
return [
146-
'yiisoft/queue' => [
147-
'handlers' => [
148-
'file-download' => [FileDownloader::class, 'handle'],
149-
'file-download2' => [$handler, 'handle'],
76+
'send-email' => /** handler definition */,
15077
],
15178
],
15279
];
15380
```
15481

155-
**How it works**:
156-
157-
- If the class exists:
158-
- If the method is static, it is called statically: `[$className, $methodName]`. Dependencies may be passed *to the provided method* in case they are resolvable from the DI container.
159-
- If the first element is an object instance, it is called as `$firstElement->$methodName(...)` with dependency injection applied *to the $methodName*.
160-
- If the method is not static, the class must be resolvable from the DI container, and the worker calls `$container->get($className)->$methodName(...)`. DI container will also resolve dependencies declared in the *class constructor*.
161-
162-
**Pros**:
163-
164-
- Explicit method name, good for “classic” `handle()` methods.
165-
- Supports static methods for pure, dependency-free handlers.
166-
167-
**Cons**:
168-
169-
- Harder to maintain and refactor than regular class definitions with either `__invoke` method or `MessageHandlerInterface` implementation.
170-
171-
**Use when**:
172-
173-
- You want to use static handlers (rare, but can be useful for pure transforms).
174-
- You want to group different handlers in a single class for organizational purposes.
82+
Handler definition should be either an [extended callable definition](./callable-definitions-extended.md) or a string for your DI container to resolve a `MessageHandlerInterface` instance.
17583

17684
## When mapping by short names is a better idea
17785

@@ -199,7 +107,7 @@ This way external producers never need to know your internal PHP class names.
199107

200108
## Common pitfalls and unsupported formats
201109

202-
- A string definition is **not** treated as a function name. It is treated only as a DI container ID.
110+
- A string definition is treated as a DI container ID first. If the container doesn't have such entry, it is resolved as a callable only when it is a valid PHP callable.
203111
- A class-string that is not resolvable via `$container->has()` will not be auto-instantiated.
204112
- [yiisoft/definitions](https://github.com/yiisoft/definitions) array format (like `['class' => ..., '__construct()' => ...]`) is **not** supported for handlers.
205113

docs/guide/en/middleware-pipelines.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,9 @@ graph LR
5757
You can use any of these formats:
5858

5959
- A ready-to-use middleware object.
60-
- An array in the format of [yiisoft/definitions](https://github.com/yiisoft/definitions).
61-
- A `callable` (closure, invokable object, `[$object, 'method']`, etc.). It is executed through the
62-
[yiisoft/injector](https://github.com/yiisoft/injector), so its dependencies are resolved automatically.
60+
- An array in the format of [yiisoft/definitions](https://github.com/yiisoft/definitions), which defines a middleware implementation.
6361
- A string for your DI container to resolve the middleware, e.g. `FooMiddleware::class`.
62+
- An [extended callable definition](callable-definitions-extended.md). A callable should either be a middleware itself or return a configured middleware object.
6463

6564
The required interface depends on the pipeline:
6665

0 commit comments

Comments
 (0)