├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer-require-checker.json
├── composer.json
├── config
├── di.php
└── params.php
├── phpbench.json
├── rector.php
├── src
├── Adapter
│ ├── AdapterInterface.php
│ └── SynchronousAdapter.php
├── ChannelNormalizer.php
├── Cli
│ ├── LoopInterface.php
│ ├── SignalLoop.php
│ ├── SimpleLoop.php
│ └── SoftLimitTrait.php
├── Command
│ ├── ListenAllCommand.php
│ ├── ListenCommand.php
│ └── RunCommand.php
├── Debug
│ ├── QueueCollector.php
│ ├── QueueDecorator.php
│ ├── QueueProviderInterfaceProxy.php
│ └── QueueWorkerInterfaceProxy.php
├── Exception
│ ├── AdapterConfiguration
│ │ └── AdapterNotConfiguredException.php
│ └── JobFailureException.php
├── JobStatus.php
├── Message
│ ├── Envelope.php
│ ├── EnvelopeInterface.php
│ ├── IdEnvelope.php
│ ├── JsonMessageSerializer.php
│ ├── Message.php
│ ├── MessageHandlerInterface.php
│ ├── MessageInterface.php
│ └── MessageSerializerInterface.php
├── Middleware
│ ├── CallableFactory.php
│ ├── Consume
│ │ ├── ConsumeFinalHandler.php
│ │ ├── ConsumeMiddlewareDispatcher.php
│ │ ├── ConsumeRequest.php
│ │ ├── MessageHandlerConsumeInterface.php
│ │ ├── MiddlewareConsumeInterface.php
│ │ ├── MiddlewareConsumeStack.php
│ │ ├── MiddlewareFactoryConsume.php
│ │ └── MiddlewareFactoryConsumeInterface.php
│ ├── FailureHandling
│ │ ├── FailureEnvelope.php
│ │ ├── FailureFinalHandler.php
│ │ ├── FailureHandlingRequest.php
│ │ ├── FailureMiddlewareDispatcher.php
│ │ ├── Implementation
│ │ │ ├── ExponentialDelayMiddleware.php
│ │ │ └── SendAgainMiddleware.php
│ │ ├── MessageFailureHandlerInterface.php
│ │ ├── MiddlewareFactoryFailure.php
│ │ ├── MiddlewareFactoryFailureInterface.php
│ │ ├── MiddlewareFailureInterface.php
│ │ └── MiddlewareFailureStack.php
│ ├── InvalidCallableConfigurationException.php
│ ├── InvalidMiddlewareDefinitionException.php
│ └── Push
│ │ ├── AdapterPushHandler.php
│ │ ├── Implementation
│ │ ├── DelayMiddlewareInterface.php
│ │ └── IdMiddleware.php
│ │ ├── MessageHandlerPushInterface.php
│ │ ├── MiddlewareFactoryPush.php
│ │ ├── MiddlewareFactoryPushInterface.php
│ │ ├── MiddlewarePushInterface.php
│ │ ├── MiddlewarePushStack.php
│ │ ├── PushMiddlewareDispatcher.php
│ │ └── PushRequest.php
├── Provider
│ ├── AdapterFactoryQueueProvider.php
│ ├── ChannelNotFoundException.php
│ ├── CompositeQueueProvider.php
│ ├── InvalidQueueConfigException.php
│ ├── PrototypeQueueProvider.php
│ ├── QueueProviderException.php
│ └── QueueProviderInterface.php
├── Queue.php
├── QueueInterface.php
└── Worker
│ ├── Worker.php
│ └── WorkerInterface.php
└── stubs
├── StubAdapter.php
├── StubLoop.php
├── StubQueue.php
└── StubWorker.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Queue Change Log
2 |
3 | ## 1.0.0 under development
4 |
5 | - Initial release.
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Queue
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/queue)
10 | [](https://packagist.org/packages/yiisoft/queue)
11 | [](https://github.com/yiisoft/queue/actions)
12 | [](https://codecov.io/gh/yiisoft/queue)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/queue/master)
14 | [](https://github.com/yiisoft/queue/actions?query=workflow%3A%22static+analysis%22)
15 | [](https://shepherd.dev/github/yiisoft/queue)
16 |
17 | An extension for running tasks asynchronously via queues.
18 |
19 | ## Requirements
20 |
21 | - PHP 8.1 or higher.
22 |
23 | ## Installation
24 |
25 | The package could be installed with [Composer](https://getcomposer.org):
26 |
27 | ```shell
28 | composer require yiisoft/queue
29 | ```
30 |
31 | ## Ready for Yii Config
32 |
33 | If you are using [yiisoft/config](https://github.com/yiisoft/config), you'll find out this package has some defaults
34 | in the [`common`](config/di.php) and [`params`](config/params.php) configurations saving your time. Things you should
35 | change to start working with the queue:
36 |
37 | - Optionally: define default `\Yiisoft\Queue\Adapter\AdapterInterface` implementation.
38 | - And/or define channel-specific `AdapterInterface` implementations in the `channel` params key to be used
39 | with the [queue provider](#different-queue-channels).
40 | - Define [message handlers](docs/guide/worker.md#handler-format) in the `handlers` params key to be used with the `QueueWorker`.
41 | - Resolve other `\Yiisoft\Queue\Queue` dependencies (psr-compliant event dispatcher).
42 |
43 | ## Differences to yii2-queue
44 |
45 | If you have experience with `yiisoft/yii2-queue`, you will find out that this package is similar.
46 | Though, there are some key differences that are described in the "[migrating from yii2-queue](docs/guide/migrating-from-yii2-queue.md)"
47 | article.
48 |
49 | ## General usage
50 |
51 | Each queue task consists of two parts:
52 |
53 | 1. A message is a class implementing `MessageInterface`. For simple cases you can use the default implementation,
54 | `Yiisoft\Queue\Message\Message`. For more complex cases, you should implement the interface by your own.
55 | 2. A message handler is a callable called by a `Yiisoft\Queue\Worker\Worker`. The handler handles each queue message.
56 |
57 | For example, if you need to download and save a file, your message creation may look like the following:
58 | - Message handler as the first parameter
59 | - Message data as the second parameter
60 |
61 | ```php
62 | $data = [
63 | 'url' => $url,
64 | 'destinationFile' => $filename,
65 | ];
66 | $message = new \Yiisoft\Queue\Message\Message(FileDownloader::class, $data);
67 | ```
68 |
69 | Then you should push it to the queue:
70 |
71 | ```php
72 | $queue->push($message);
73 | ```
74 |
75 | Its handler may look like the following:
76 |
77 | ```php
78 | class FileDownloader
79 | {
80 | private string $absolutePath;
81 |
82 | public function __construct(string $absolutePath)
83 | {
84 | $this->absolutePath = $absolutePath;
85 | }
86 |
87 | public function handle(\Yiisoft\Queue\Message\MessageInterface $downloadMessage): void
88 | {
89 | $fileName = $downloadMessage->getData()['destinationFile'];
90 | $path = "$this->absolutePath/$fileName";
91 | file_put_contents($path, file_get_contents($downloadMessage->getData()['url']));
92 | }
93 | }
94 | ```
95 |
96 | The last thing we should do is to create a configuration for the `Yiisoft\Queue\Worker\Worker`:
97 |
98 | ```php
99 | $worker = new \Yiisoft\Queue\Worker\Worker(
100 | [],
101 | $logger,
102 | $injector,
103 | $container
104 | );
105 | ```
106 |
107 | There is a way to run all the messages that are already in the queue, and then exit:
108 |
109 | ```php
110 | $queue->run(); // this will execute all the existing messages
111 | $queue->run(10); // while this will execute only 10 messages as a maximum before exit
112 | ```
113 |
114 | If you don't want your script to exit immediately, you can use the `listen` method:
115 |
116 | ```php
117 | $queue->listen();
118 | ```
119 |
120 | You can also check the status of a pushed message (the queue adapter you are using must support this feature):
121 |
122 | ```php
123 | $queue->push($message);
124 | $id = $message->getId();
125 |
126 | // Get status of the job
127 | $status = $queue->status($id);
128 |
129 | // Check whether the job is waiting for execution.
130 | $status->isWaiting();
131 |
132 | // Check whether a worker got the job from the queue and executes it.
133 | $status->isReserved();
134 |
135 | // Check whether a worker has executed the job.
136 | $status->isDone();
137 | ```
138 |
139 | ## Custom handler names
140 | ### Custom handler names
141 |
142 | By default, when you push a message to the queue, the message handler name is the fully qualified class name of the handler.
143 | This can be useful for most cases, but sometimes you may want to use a shorter name or arbitrary string as the handler name.
144 | This can be useful when you want to reduce the amount of data being passed or when you communicate with external systems.
145 |
146 | To use a custom handler name before message push, you can pass it as the first argument `Message` when creating it:
147 | ```php
148 | new Message('handler-name', $data);
149 | ```
150 |
151 | To use a custom handler name on message consumption, you should configure handler mapping for the `Worker` class:
152 | ```php
153 | $worker = new \Yiisoft\Queue\Worker\Worker(
154 | ['handler-name' => FooHandler::class],
155 | $logger,
156 | $injector,
157 | $container
158 | );
159 | ```
160 |
161 | ## Different queue channels
162 |
163 | Often we need to push to different queue channels with an only application. There is the `QueueProviderInterface`
164 | interface that provides different `Queue` objects creation for different channels. With implementation of this interface
165 | channel-specific `Queue` creation is as simple as
166 |
167 | ```php
168 | $queue = $provider->get('channel-name');
169 | ```
170 |
171 | Out of the box, there are four implementations of the `QueueProviderInterface`:
172 |
173 | - `AdapterFactoryQueueProvider`
174 | - `PrototypeQueueProvider`
175 | - `CompositeQueueProvider`
176 |
177 | ### `AdapterFactoryQueueProvider`
178 |
179 | Provider based on the definition of channel-specific adapters. Definitions are passed in
180 | the `$definitions` constructor parameter of the factory, where keys are channel names and values are definitions
181 | for the [`Yiisoft\Factory\Factory`](https://github.com/yiisoft/factory). Below are some examples:
182 |
183 | ```php
184 | use Yiisoft\Queue\Adapter\SynchronousAdapter;
185 |
186 | [
187 | 'channel1' => new SynchronousAdapter(),
188 | 'channel2' => static fn(SynchronousAdapter $adapter) => $adapter->withChannel('channel2'),
189 | 'channel3' => [
190 | 'class' => SynchronousAdapter::class,
191 | '__constructor' => ['channel' => 'channel3'],
192 | ],
193 | ]
194 | ```
195 |
196 | For more information about the definition formats available, see the [factory](https://github.com/yiisoft/factory) documentation.
197 |
198 | ### `PrototypeQueueProvider`
199 |
200 | Queue provider that only changes the channel name of the base queue. It can be useful when your queues used the same
201 | adapter.
202 |
203 | > Warning: This strategy is not recommended as it does not give you any protection against typos and mistakes
204 | > in channel names.
205 |
206 | ### `CompositeQueueProvider`
207 |
208 | This provider allows you to combine multiple providers into one. It will try to get a queue from each provider in the
209 | order they are passed to the constructor. The first queue found will be returned.
210 |
211 | ## Console execution
212 |
213 | The exact way of task execution depends on the adapter used. Most adapters can be run using
214 | console commands, which the component automatically registers in your application.
215 |
216 | The following command obtains and executes tasks in a loop until the queue is empty:
217 |
218 | ```sh
219 | yii queue:run
220 | ```
221 |
222 | The following command launches a daemon which infinitely queries the queue:
223 |
224 | ```sh
225 | yii queue:listen
226 | ```
227 |
228 | See the documentation for more details about adapter specific console commands and their options.
229 |
230 | The component can also track the status of a job which was pushed into queue.
231 |
232 | For more details, see [the guide](docs/guide/en/README.md).
233 |
234 | ## Middleware pipelines
235 |
236 | Any message pushed to a queue or consumed from it passes through two different middleware pipelines: one pipeline
237 | on message push and another - on a message consume. The process is the same as for the HTTP request, but it is executed
238 | twice for a queue message. That means you can add extra functionality on message pushing and consuming with configuration
239 | of the two classes: `PushMiddlewareDispatcher` and `ConsumeMiddlewareDispatcher` respectively.
240 |
241 | You can use any of these formats to define a middleware:
242 |
243 | - A ready-to-use middleware object: `new FooMiddleware()`. It must implement `MiddlewarePushInterface`,
244 | `MiddlewareConsumeInterface` or `MiddlewareFailureInterface` depending on the place you use it.
245 | - An array in the format of [yiisoft/definitions](https://github.com/yiisoft/definitions).
246 | **Only if you use yiisoft/definitions and yiisoft/di**.
247 | - A `callable`: `fn() => // do stuff`, `$object->foo(...)`, etc. It will be executed through the
248 | [yiisoft/injector](https://github.com/yiisoft/injector), so all the dependencies of your callable will be resolved.
249 | - A string for your DI container to resolve the middleware, e.g. `FooMiddleware::class`
250 |
251 | Middleware will be executed forwards in the same order they are defined. If you define it like the following:
252 | `[$middleware1, $midleware2]`, the execution will look like this:
253 |
254 | ```mermaid
255 | graph LR
256 | StartPush((Start)) --> PushMiddleware1[$middleware1] --> PushMiddleware2[$middleware2] --> Push(Push to a queue)
257 | -.-> PushMiddleware2[$middleware2] -.-> PushMiddleware1[$middleware1]
258 | PushMiddleware1[$middleware1] -.-> EndPush((End))
259 |
260 |
261 | StartConsume((Start)) --> ConsumeMiddleware1[$middleware1] --> ConsumeMiddleware2[$middleware2] --> Consume(Consume / handle)
262 | -.-> ConsumeMiddleware2[$middleware2] -.-> ConsumeMiddleware1[$middleware1]
263 | ConsumeMiddleware1[$middleware1] -.-> EndConsume((End))
264 | ```
265 |
266 | ### Push a pipeline
267 |
268 | When you push a message, you can use middlewares to modify both message and queue adapter.
269 | With message modification you can add extra data, obfuscate data, collect metrics, etc.
270 | With queue adapter modification you can redirect the message to another queue, delay message consuming, and so on.
271 |
272 | To use this feature, you have to create a middleware class, which implements `MiddlewarePushInterface`, and
273 | return a modified `PushRequest` object from the `processPush` method:
274 |
275 | ```php
276 | return $pushRequest->withMessage($newMessage)->withAdapter($newAdapter);
277 | ```
278 |
279 | With push middlewares you can define an adapter object at the runtime, not in the `Queue` constructor.
280 | There is a restriction: by the time all middlewares are executed in the forward order, the adapter must be specified
281 | in the `PushRequest` object. You will get a `AdapterNotConfiguredException`, if it isn't.
282 |
283 | You have three places to define push middlewares:
284 |
285 | 1. `PushMiddlewareDispatcher`. You can pass it either to the constructor, or to the `withMiddlewares()` method, which
286 | creates a completely new dispatcher object with only those middlewares, which are passed as arguments.
287 | If you use [yiisoft/config](yiisoft/config), you can add middleware to the `middlewares-push` key of the
288 | `yiisoft/queue` array in the `params`.
289 | 2. Pass middlewares to either `Queue::withMiddlewares()` or `Queue::withMiddlewaresAdded()` methods. The difference is
290 | that the former will completely replace an existing middleware stack, while the latter will add passed middlewares to
291 | the end of the existing stack. These middlewares will be executed after the common ones, passed directly to the
292 | `PushMiddlewareDispatcher`. It's useful when defining a queue channel. Both methods return a new instance of the `Queue`
293 | class.
294 | 3. Put middlewares into the `Queue::push()` method like this: `$queue->push($message, ...$middlewares)`. These
295 | middlewares have the lowest priority and will be executed after those which are in the `PushMiddlewareDispatcher` and
296 | the ones passed to the `Queue::withMiddlewares()` and `Queue::withMiddlewaresAdded()` and only for the message passed
297 | along with them.
298 |
299 | ### Consume pipeline
300 |
301 | You can set a middleware pipeline for a message when it will be consumed from a queue server. This is useful to collect metrics, modify message data, etc. In a pair with a Push middleware you can deduplicate messages in the queue, calculate time from push to consume, handle errors (push to a queue again, redirect failed message to another queue, send a notification, etc.). Except push pipeline, you have only one place to define the middleware stack: in the `ConsumeMiddlewareDispatcher`, either in the constructor, or in the `withMiddlewares()` method. If you use [yiisoft/config](yiisoft/config), you can add middleware to the `middlewares-consume` key of the `yiisoft/queue` array in the `params`.
302 |
303 | ### Error handling pipeline
304 |
305 | Often when some job is failing, we want to retry its execution a couple more times or redirect it to another queue channel. This can be done in `yiisoft/queue` with a Failure middleware pipeline. They are triggered each time message processing via the Consume middleware pipeline is interrupted with any `Throwable`. The key differences from the previous two pipelines:
306 |
307 | - You should set up the middleware pipeline separately for each queue channel. That means, the format should be `['channel-name' => [FooMiddleware::class]]` instead of `[FooMiddleware::class]`, like for the other two pipelines. There is also a default key, which will be used for those channels without their own one: `FailureMiddlewareDispatcher::DEFAULT_PIPELINE`.
308 | - The last middleware will throw the exception, which will come with the `FailureHandlingRequest` object. If you don't want the exception to be thrown, your middlewares should `return` a request without calling `$handler->handleFailure()`.
309 |
310 | You can declare error handling a middleware pipeline in the `FailureMiddlewareDispatcher`, either in the constructor, or in the `withMiddlewares()` method. If you use [yiisoft/config](yiisoft/config), you can add middleware to the `middlewares-fail` key of the `yiisoft/queue` array in the `params`.
311 |
312 | See [error handling docs](docs/guide/error-handling.md) for details.
313 |
314 | ## Documentation
315 |
316 | - [Guide](docs/guide/en/README.md)
317 | - [Internals](docs/internals.md)
318 |
319 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that.
320 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
321 |
322 | ## License
323 |
324 | The Yii Queue is free software. It is released under the terms of the BSD License.
325 | Please see [`LICENSE`](./LICENSE.md) for more information.
326 |
327 | Maintained by [Yii Software](https://www.yiiframework.com/).
328 |
329 | ### Support the project
330 |
331 | [](https://opencollective.com/yiisoft)
332 |
333 | ### Follow updates
334 |
335 | [](https://www.yiiframework.com/)
336 | [](https://twitter.com/yiiframework)
337 | [](https://t.me/yii3en)
338 | [](https://www.facebook.com/groups/yiitalk)
339 | [](https://yiiframework.com/go/slack)
340 |
--------------------------------------------------------------------------------
/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist": [
3 | "Yiisoft\\Yii\\Debug\\Collector\\CollectorTrait",
4 | "Yiisoft\\Yii\\Debug\\Collector\\SummaryCollectorInterface",
5 | "pcntl_signal",
6 | "pcntl_signal_dispatch",
7 | "SIGCONT",
8 | "SIGHUP",
9 | "SIGINT",
10 | "SIGTERM",
11 | "SIGTSTP"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/queue",
3 | "description": "Queue Extension which supported DB, Redis, RabbitMQ, Beanstalk, SQS and Gearman",
4 | "type": "library",
5 | "keywords": [
6 | "yii",
7 | "queue",
8 | "async",
9 | "gii",
10 | "db",
11 | "redis",
12 | "rabbitmq",
13 | "beanstalk",
14 | "gearman",
15 | "sqs"
16 | ],
17 | "license": "BSD-3-Clause",
18 | "support": {
19 | "issues": "https://github.com/yiisoft/queue/issues?state=open",
20 | "source": "https://github.com/yiisoft/queue",
21 | "forum": "https://www.yiiframework.com/forum/",
22 | "wiki": "https://www.yiiframework.com/wiki/",
23 | "irc": "ircs://irc.libera.chat:6697/yii",
24 | "chat": "https://t.me/yii3en"
25 | },
26 | "funding": [
27 | {
28 | "type": "opencollective",
29 | "url": "https://opencollective.com/yiisoft"
30 | },
31 | {
32 | "type": "github",
33 | "url": "https://github.com/sponsors/yiisoft"
34 | }
35 | ],
36 | "minimum-stability": "dev",
37 | "prefer-stable": true,
38 | "require": {
39 | "php": "8.1 - 8.4",
40 | "psr/container": "^1.0 || ^2.0",
41 | "psr/log": "^2.0 || ^3.0",
42 | "symfony/console": "^5.4 || ^6.0 || ^7.0",
43 | "yiisoft/arrays": "^3.1",
44 | "yiisoft/definitions": "^3.3.1",
45 | "yiisoft/factory": "^1.3",
46 | "yiisoft/friendly-exception": "^1.0",
47 | "yiisoft/injector": "^1.0"
48 | },
49 | "require-dev": {
50 | "maglnet/composer-require-checker": "^4.7.1",
51 | "phpbench/phpbench": "^1.4.1",
52 | "phpunit/phpunit": "^10.5.45",
53 | "rector/rector": "^2.0.11",
54 | "roave/infection-static-analysis-plugin": "^1.35",
55 | "spatie/phpunit-watcher": "^1.24",
56 | "vimeo/psalm": "^5.26.1 || ^6.10",
57 | "yiisoft/test-support": "^3.0.2",
58 | "yiisoft/yii-debug": "dev-master"
59 | },
60 | "suggest": {
61 | "ext-pcntl": "Need for process signals"
62 | },
63 | "autoload": {
64 | "psr-4": {
65 | "Yiisoft\\Queue\\": "src",
66 | "Yiisoft\\Queue\\Stubs\\": "stubs"
67 | }
68 | },
69 | "autoload-dev": {
70 | "psr-4": {
71 | "Yiisoft\\Queue\\Tests\\": "tests"
72 | }
73 | },
74 | "extra": {
75 | "branch-alias": {
76 | "dev-master": "3.0.x-dev"
77 | },
78 | "config-plugin-options": {
79 | "source-directory": "config"
80 | },
81 | "config-plugin": {
82 | "di": "di.php",
83 | "params": "params.php"
84 | }
85 | },
86 | "config": {
87 | "sort-packages": true,
88 | "allow-plugins": {
89 | "infection/extension-installer": true,
90 | "composer/package-versions-deprecated": true,
91 | "yiisoft/config": false
92 | }
93 | },
94 | "scripts": {
95 | "test": "phpunit --testdox --no-interaction",
96 | "test-watch": "phpunit-watcher watch"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/config/di.php:
--------------------------------------------------------------------------------
1 | [
33 | '__construct()' => [
34 | 'definitions' => $params['yiisoft/queue']['channels'],
35 | ],
36 | ],
37 | QueueProviderInterface::class => AdapterFactoryQueueProvider::class,
38 | QueueWorker::class => [
39 | 'class' => QueueWorker::class,
40 | '__construct()' => [$params['yiisoft/queue']['handlers']],
41 | ],
42 | WorkerInterface::class => QueueWorker::class,
43 | LoopInterface::class => static function (ContainerInterface $container): LoopInterface {
44 | return extension_loaded('pcntl')
45 | ? $container->get(SignalLoop::class)
46 | : $container->get(SimpleLoop::class);
47 | },
48 | QueueInterface::class => Queue::class,
49 | MiddlewareFactoryPushInterface::class => MiddlewareFactoryPush::class,
50 | MiddlewareFactoryConsumeInterface::class => MiddlewareFactoryConsume::class,
51 | MiddlewareFactoryFailureInterface::class => MiddlewareFactoryFailure::class,
52 | PushMiddlewareDispatcher::class => [
53 | '__construct()' => ['middlewareDefinitions' => $params['yiisoft/queue']['middlewares-push']],
54 | ],
55 | ConsumeMiddlewareDispatcher::class => [
56 | '__construct()' => ['middlewareDefinitions' => $params['yiisoft/queue']['middlewares-consume']],
57 | ],
58 | FailureMiddlewareDispatcher::class => [
59 | '__construct()' => ['middlewareDefinitions' => $params['yiisoft/queue']['middlewares-fail']],
60 | ],
61 | MessageSerializerInterface::class => JsonMessageSerializer::class,
62 | RunCommand::class => [
63 | '__construct()' => [
64 | 'channels' => array_keys($params['yiisoft/queue']['channels']),
65 | ],
66 | ],
67 | ListenAllCommand::class => [
68 | '__construct()' => [
69 | 'channels' => array_keys($params['yiisoft/queue']['channels']),
70 | ],
71 | ],
72 | ];
73 |
--------------------------------------------------------------------------------
/config/params.php:
--------------------------------------------------------------------------------
1 | [
18 | 'commands' => [
19 | 'queue:run' => RunCommand::class,
20 | 'queue:listen' => ListenCommand::class,
21 | 'queue:listen:all' => ListenAllCommand::class,
22 | ],
23 | ],
24 | 'yiisoft/queue' => [
25 | 'handlers' => [],
26 | 'channels' => [
27 | QueueInterface::DEFAULT_CHANNEL => AdapterInterface::class,
28 | ],
29 | 'middlewares-push' => [],
30 | 'middlewares-consume' => [],
31 | 'middlewares-fail' => [],
32 | ],
33 | 'yiisoft/yii-debug' => [
34 | 'collectors' => [
35 | QueueCollector::class,
36 | ],
37 | 'trackedServices' => [
38 | QueueProviderInterface::class => [QueueProviderInterfaceProxy::class, QueueCollector::class],
39 | WorkerInterface::class => [QueueWorkerInterfaceProxy::class, QueueCollector::class],
40 | ],
41 | ],
42 | ];
43 |
--------------------------------------------------------------------------------
/phpbench.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json",
3 | "runner.bootstrap": "vendor/autoload.php",
4 | "runner.path": "tests/Benchmark",
5 | "runner.revs": 100000,
6 | "runner.iterations": 5,
7 | "runner.warmup": 5
8 | }
9 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
13 | __DIR__ . '/src',
14 | __DIR__ . '/tests',
15 | ]);
16 |
17 | // register a single rule
18 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
19 |
20 | // define sets of rules
21 | $rectorConfig->sets([
22 | LevelSetList::UP_TO_PHP_81,
23 | ]);
24 |
25 | $rectorConfig->skip([
26 | ClosureToArrowFunctionRector::class,
27 | ReadOnlyPropertyRector::class,
28 | ]);
29 | };
30 |
--------------------------------------------------------------------------------
/src/Adapter/AdapterInterface.php:
--------------------------------------------------------------------------------
1 | channel = ChannelNormalizer::normalize($channel);
28 | }
29 |
30 | public function __destruct()
31 | {
32 | $this->runExisting(function (MessageInterface $message): bool {
33 | $this->worker->process($message, $this->queue);
34 |
35 | return true;
36 | });
37 | }
38 |
39 | public function runExisting(callable $handlerCallback): void
40 | {
41 | $result = true;
42 | while (isset($this->messages[$this->current]) && $result === true) {
43 | $result = $handlerCallback($this->messages[$this->current]);
44 | unset($this->messages[$this->current]);
45 | $this->current++;
46 | }
47 | }
48 |
49 | public function status(string|int $id): JobStatus
50 | {
51 | $id = (int) $id;
52 |
53 | if ($id < 0) {
54 | throw new InvalidArgumentException('This adapter IDs start with 0.');
55 | }
56 |
57 | if ($id < $this->current) {
58 | return JobStatus::DONE;
59 | }
60 |
61 | if (isset($this->messages[$id])) {
62 | return JobStatus::WAITING;
63 | }
64 |
65 | throw new InvalidArgumentException('There is no message with the given ID.');
66 | }
67 |
68 | public function push(MessageInterface $message): MessageInterface
69 | {
70 | $key = count($this->messages) + $this->current;
71 | $this->messages[] = $message;
72 |
73 | return new IdEnvelope($message, $key);
74 | }
75 |
76 | public function subscribe(callable $handlerCallback): void
77 | {
78 | $this->runExisting($handlerCallback);
79 | }
80 |
81 | public function withChannel(string|BackedEnum $channel): self
82 | {
83 | $channel = ChannelNormalizer::normalize($channel);
84 |
85 | if ($channel === $this->channel) {
86 | return $this;
87 | }
88 |
89 | $new = clone $this;
90 | $new->channel = $channel;
91 | $new->messages = [];
92 |
93 | return $new;
94 | }
95 |
96 | public function getChannel(): string
97 | {
98 | return $this->channel;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/ChannelNormalizer.php:
--------------------------------------------------------------------------------
1 | value : $channel;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Cli/LoopInterface.php:
--------------------------------------------------------------------------------
1 | $this->exit = true);
37 | }
38 | foreach (self::SIGNALS_SUSPEND as $signal) {
39 | pcntl_signal($signal, fn () => $this->pause = true);
40 | }
41 | foreach (self::SIGNALS_RESUME as $signal) {
42 | pcntl_signal($signal, fn () => $this->pause = false);
43 | }
44 | }
45 |
46 | /**
47 | * Checks signals state.
48 | *
49 | * {@inheritdoc}
50 | */
51 | public function canContinue(): bool
52 | {
53 | if ($this->memoryLimitReached()) {
54 | return false;
55 | }
56 |
57 | return $this->dispatchSignals();
58 | }
59 |
60 | protected function dispatchSignals(): bool
61 | {
62 | pcntl_signal_dispatch();
63 |
64 | // Wait for resume signal until the loop is suspended
65 | while ($this->pause && !$this->exit) {
66 | usleep(10000);
67 | pcntl_signal_dispatch();
68 | }
69 |
70 | return !$this->exit;
71 | }
72 |
73 | protected function getMemoryLimit(): int
74 | {
75 | return $this->memorySoftLimit;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Cli/SimpleLoop.php:
--------------------------------------------------------------------------------
1 | memoryLimitReached();
22 | }
23 |
24 | protected function getMemoryLimit(): int
25 | {
26 | return $this->memorySoftLimit;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Cli/SoftLimitTrait.php:
--------------------------------------------------------------------------------
1 | getMemoryLimit();
14 |
15 | if ($limit !== 0) {
16 | $usage = memory_get_usage(true);
17 |
18 | if ($usage >= $limit) {
19 | return true;
20 | }
21 | }
22 |
23 | return false;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Command/ListenAllCommand.php:
--------------------------------------------------------------------------------
1 | addArgument(
39 | 'channel',
40 | InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
41 | 'Queue channel name list to connect to',
42 | $this->channels,
43 | )
44 | ->addOption(
45 | 'pause',
46 | 'p',
47 | InputOption::VALUE_REQUIRED,
48 | 'Pause between queue channel iterations in seconds. May save some CPU. Default: 1',
49 | 1,
50 | )
51 | ->addOption(
52 | 'maximum',
53 | 'm',
54 | InputOption::VALUE_REQUIRED,
55 | 'Maximum number of messages to process in each channel before switching to another channel. ' .
56 | 'Default is 0 (no limits).',
57 | 0,
58 | );
59 |
60 | $this->addUsage('[channel1 [channel2 [...]]] [--timeout=] [--maximum=]');
61 | }
62 |
63 | protected function execute(InputInterface $input, OutputInterface $output): int
64 | {
65 | $queues = [];
66 | /** @var string $channel */
67 | foreach ($input->getArgument('channel') as $channel) {
68 | $queues[] = $this->queueProvider->get($channel);
69 | }
70 |
71 | $pauseSeconds = (int) $input->getOption('pause');
72 | if ($pauseSeconds < 0) {
73 | $pauseSeconds = 1;
74 | }
75 |
76 | while ($this->loop->canContinue()) {
77 | $hasMessages = false;
78 | foreach ($queues as $queue) {
79 | $hasMessages = $queue->run((int) $input->getOption('maximum')) > 0 || $hasMessages;
80 | }
81 |
82 | if (!$hasMessages) {
83 | /** @psalm-var 0|positive-int $pauseSeconds */
84 | sleep($pauseSeconds);
85 | }
86 | }
87 |
88 | return 0;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Command/ListenCommand.php:
--------------------------------------------------------------------------------
1 | addArgument(
30 | 'channel',
31 | InputArgument::OPTIONAL,
32 | 'Queue channel name to connect to',
33 | QueueInterface::DEFAULT_CHANNEL,
34 | );
35 | }
36 |
37 | protected function execute(InputInterface $input, OutputInterface $output): int
38 | {
39 | $this->queueProvider
40 | ->get($input->getArgument('channel'))
41 | ->listen();
42 |
43 | return 0;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Command/RunCommand.php:
--------------------------------------------------------------------------------
1 | addArgument(
31 | 'channel',
32 | InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
33 | 'Queue channel name list to connect to.',
34 | $this->channels,
35 | )
36 | ->addOption(
37 | 'maximum',
38 | 'm',
39 | InputOption::VALUE_REQUIRED,
40 | 'Maximum number of messages to process in each channel. Default is 0 (no limits).',
41 | 0,
42 | )
43 | ->addUsage('[channel1 [channel2 [...]]] --maximum 100');
44 | }
45 |
46 | protected function execute(InputInterface $input, OutputInterface $output): int
47 | {
48 | /** @var string $channel */
49 | foreach ($input->getArgument('channel') as $channel) {
50 | $output->write("Processing channel $channel... ");
51 | $count = $this->queueProvider
52 | ->get($channel)
53 | ->run((int)$input->getOption('maximum'));
54 |
55 | $output->writeln("Messages processed: $count.");
56 | }
57 |
58 | return 0;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Debug/QueueCollector.php:
--------------------------------------------------------------------------------
1 | isActive()) {
25 | return [];
26 | }
27 |
28 | return [
29 | 'pushes' => $this->pushes,
30 | 'statuses' => $this->statuses,
31 | 'processingMessages' => $this->processingMessages,
32 | ];
33 | }
34 |
35 | public function collectStatus(string $id, JobStatus $status): void
36 | {
37 | if (!$this->isActive()) {
38 | return;
39 | }
40 |
41 | $this->statuses[] = [
42 | 'id' => $id,
43 | 'status' => $status->key(),
44 | ];
45 | }
46 |
47 | public function collectPush(
48 | ?string $channel,
49 | MessageInterface $message,
50 | string|array|callable|MiddlewarePushInterface ...$middlewareDefinitions,
51 | ): void {
52 | if (!$this->isActive()) {
53 | return;
54 | }
55 | if ($channel === null) {
56 | $channel = 'null';
57 | }
58 |
59 | $this->pushes[$channel][] = [
60 | 'message' => $message,
61 | 'middlewares' => $middlewareDefinitions,
62 | ];
63 | }
64 |
65 | public function collectWorkerProcessing(MessageInterface $message, QueueInterface $queue): void
66 | {
67 | if (!$this->isActive()) {
68 | return;
69 | }
70 | $this->processingMessages[$queue->getChannel()][] = $message;
71 | }
72 |
73 | private function reset(): void
74 | {
75 | $this->pushes = [];
76 | $this->statuses = [];
77 | $this->processingMessages = [];
78 | }
79 |
80 | public function getSummary(): array
81 | {
82 | if (!$this->isActive()) {
83 | return [];
84 | }
85 |
86 | $countPushes = array_sum(array_map(static fn ($messages) => is_countable($messages) ? count($messages) : 0, $this->pushes));
87 | $countStatuses = count($this->statuses);
88 | $countProcessingMessages = array_sum(array_map(static fn ($messages) => is_countable($messages) ? count($messages) : 0, $this->processingMessages));
89 |
90 | return [
91 | 'countPushes' => $countPushes,
92 | 'countStatuses' => $countStatuses,
93 | 'countProcessingMessages' => $countProcessingMessages,
94 | ];
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Debug/QueueDecorator.php:
--------------------------------------------------------------------------------
1 | queue->status($id);
24 | $this->collector->collectStatus((string) $id, $result);
25 |
26 | return $result;
27 | }
28 |
29 | public function push(
30 | MessageInterface $message,
31 | string|array|callable|MiddlewarePushInterface ...$middlewareDefinitions
32 | ): MessageInterface {
33 | $message = $this->queue->push($message, ...$middlewareDefinitions);
34 | $this->collector->collectPush($this->queue->getChannel(), $message, ...$middlewareDefinitions);
35 | return $message;
36 | }
37 |
38 | public function run(int $max = 0): int
39 | {
40 | return $this->queue->run($max);
41 | }
42 |
43 | public function listen(): void
44 | {
45 | $this->queue->listen();
46 | }
47 |
48 | public function withAdapter(AdapterInterface $adapter): QueueInterface
49 | {
50 | return new self($this->queue->withAdapter($adapter), $this->collector);
51 | }
52 |
53 | public function getChannel(): string
54 | {
55 | return $this->queue->getChannel();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Debug/QueueProviderInterfaceProxy.php:
--------------------------------------------------------------------------------
1 | queueProvider->get($channel);
22 | return new QueueDecorator($queue, $this->collector);
23 | }
24 |
25 | public function has(string|BackedEnum $channel): bool
26 | {
27 | return $this->queueProvider->has($channel);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Debug/QueueWorkerInterfaceProxy.php:
--------------------------------------------------------------------------------
1 | collector->collectWorkerProcessing($message, $queue);
22 | return $this->worker->process($message, $queue);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Exception/AdapterConfiguration/AdapterNotConfiguredException.php:
--------------------------------------------------------------------------------
1 | getMessage();
19 | $messageId = $queueMessage->getMetadata()[IdEnvelope::MESSAGE_ID_KEY] ?? 'null';
20 | $messageText = "Processing of message #$messageId is stopped because of an exception:\n$error.";
21 |
22 | parent::__construct($messageText, 0, $previous);
23 | }
24 |
25 | public function getQueueMessage(): MessageInterface
26 | {
27 | return $this->queueMessage;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/JobStatus.php:
--------------------------------------------------------------------------------
1 | 'waiting',
17 | self::RESERVED => 'reserved',
18 | self::DONE => 'done',
19 | };
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Message/Envelope.php:
--------------------------------------------------------------------------------
1 | message;
25 | }
26 |
27 | public function getHandlerName(): string
28 | {
29 | return $this->message->getHandlerName();
30 | }
31 |
32 | public function getData(): mixed
33 | {
34 | return $this->message->getData();
35 | }
36 |
37 | public function getMetadata(): array
38 | {
39 | if ($this->metadata === null) {
40 | $messageMeta = $this->message->getMetadata();
41 |
42 | $stack = $messageMeta[EnvelopeInterface::ENVELOPE_STACK_KEY] ?? [];
43 | if (!is_array($stack)) {
44 | $stack = [];
45 | }
46 |
47 | $this->metadata = array_merge(
48 | $messageMeta,
49 | [
50 | EnvelopeInterface::ENVELOPE_STACK_KEY => array_merge(
51 | $stack,
52 | [static::class],
53 | ),
54 | ],
55 | $this->getEnvelopeMetadata(),
56 | );
57 | }
58 |
59 | return $this->metadata;
60 | }
61 |
62 | abstract protected function getEnvelopeMetadata(): array;
63 | }
64 |
--------------------------------------------------------------------------------
/src/Message/EnvelopeInterface.php:
--------------------------------------------------------------------------------
1 | getMetadata()[self::MESSAGE_ID_KEY] ?? null;
24 |
25 | /** @var int|string|null $id */
26 | $id = match (true) {
27 | $rawId === null => null, // don't remove this branch: it's important for compute speed
28 | is_string($rawId) => $rawId,
29 | is_int($rawId) => $rawId,
30 | is_object($rawId) && method_exists($rawId, '__toString') => (string)$rawId,
31 | default => null,
32 | };
33 |
34 | return new self($message, $id);
35 | }
36 |
37 | public function getId(): string|int|null
38 | {
39 | return $this->id;
40 | }
41 |
42 | protected function getEnvelopeMetadata(): array
43 | {
44 | return [self::MESSAGE_ID_KEY => $this->getId()];
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Message/JsonMessageSerializer.php:
--------------------------------------------------------------------------------
1 | $message->getHandlerName(),
19 | 'data' => $message->getData(),
20 | 'meta' => $message->getMetadata(),
21 | ];
22 | if (!isset($payload['meta']['message-class'])) {
23 | $payload['meta']['message-class'] = $message instanceof EnvelopeInterface
24 | ? $message->getMessage()::class
25 | : $message::class;
26 | }
27 |
28 | return json_encode($payload, JSON_THROW_ON_ERROR);
29 | }
30 |
31 | /**
32 | * @throws JsonException
33 | * @throws InvalidArgumentException
34 | */
35 | public function unserialize(string $value): MessageInterface
36 | {
37 | $payload = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
38 | if (!is_array($payload)) {
39 | throw new InvalidArgumentException('Payload must be array. Got ' . get_debug_type($payload) . '.');
40 | }
41 |
42 | $name = $payload['name'] ?? null;
43 | if (!isset($name) || !is_string($name)) {
44 | throw new InvalidArgumentException('Handler name must be a string. Got ' . get_debug_type($name) . '.');
45 | }
46 |
47 | $meta = $payload['meta'] ?? [];
48 | if (!is_array($meta)) {
49 | throw new InvalidArgumentException('Metadata must be an array. Got ' . get_debug_type($meta) . '.');
50 | }
51 |
52 | $envelopes = [];
53 | if (isset($meta[EnvelopeInterface::ENVELOPE_STACK_KEY]) && is_array($meta[EnvelopeInterface::ENVELOPE_STACK_KEY])) {
54 | $envelopes = $meta[EnvelopeInterface::ENVELOPE_STACK_KEY];
55 | }
56 | $meta[EnvelopeInterface::ENVELOPE_STACK_KEY] = [];
57 |
58 | $class = $payload['meta']['message-class'] ?? Message::class;
59 | // Don't check subclasses when it's a default class: that's faster
60 | if ($class !== Message::class && !is_subclass_of($class, MessageInterface::class)) {
61 | $class = Message::class;
62 | }
63 |
64 | /**
65 | * @var class-string $class
66 | */
67 | $message = $class::fromData($name, $payload['data'] ?? null, $meta);
68 |
69 | foreach ($envelopes as $envelope) {
70 | if (is_string($envelope) && class_exists($envelope) && is_subclass_of($envelope, EnvelopeInterface::class)) {
71 | $message = $envelope::fromMessage($message);
72 | }
73 | }
74 |
75 | return $message;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Message/Message.php:
--------------------------------------------------------------------------------
1 | handlerName;
29 | }
30 |
31 | public function getData(): mixed
32 | {
33 | return $this->data;
34 | }
35 |
36 | public function getMetadata(): array
37 | {
38 | return $this->metadata;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Message/MessageHandlerInterface.php:
--------------------------------------------------------------------------------
1 | container->has($definition)) {
40 | // Object with an __invoke() method
41 | $callable = $this->container->get($definition);
42 | }
43 |
44 | if (is_array($definition)
45 | && array_keys($definition) === [0, 1]
46 | && is_string($definition[0])
47 | && is_string($definition[1])
48 | ) {
49 | [$className, $methodName] = $definition;
50 | $callable = $this->fromDefinition($className, $methodName);
51 | }
52 |
53 | if ($callable === null) {
54 | $callable = $definition;
55 | }
56 |
57 | if (is_callable($callable)) {
58 | return $callable;
59 | }
60 |
61 | throw new InvalidCallableConfigurationException();
62 | }
63 |
64 | /**
65 | * @throws ContainerExceptionInterface Error while retrieving the entry from container.
66 | * @throws NotFoundExceptionInterface
67 | */
68 | private function fromDefinition(string $className, string $methodName): ?callable
69 | {
70 | $result = null;
71 |
72 | if (class_exists($className)) {
73 | try {
74 | $reflection = new ReflectionMethod($className, $methodName);
75 | if ($reflection->isStatic()) {
76 | $result = [$className, $methodName];
77 | }
78 | } catch (ReflectionException) {
79 | }
80 | }
81 |
82 | if ($result === null && $this->container->has($className)) {
83 | $result = [
84 | $this->container->get($className),
85 | $methodName,
86 | ];
87 | }
88 |
89 | return is_callable($result) ? $result : null;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Middleware/Consume/ConsumeFinalHandler.php:
--------------------------------------------------------------------------------
1 | handler;
22 | $handler($request->getMessage());
23 |
24 | return $request;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Middleware/Consume/ConsumeMiddlewareDispatcher.php:
--------------------------------------------------------------------------------
1 | middlewareDefinitions = array_reverse($middlewareDefinitions);
28 | }
29 |
30 | /**
31 | * Dispatch request through middleware to get response.
32 | *
33 | * @param ConsumeRequest $request Request to pass to middleware.
34 | * @param MessageHandlerConsumeInterface $finishHandler Handler to use in case no middleware produced a response.
35 | */
36 | public function dispatch(
37 | ConsumeRequest $request,
38 | MessageHandlerConsumeInterface $finishHandler
39 | ): ConsumeRequest {
40 | $handlerName = $request->getMessage()->getHandlerName();
41 | if (!array_key_exists($handlerName, $this->stack)) {
42 | $this->stack[$handlerName] = new MiddlewareConsumeStack($this->buildMiddlewares(), $finishHandler);
43 | }
44 |
45 | return $this->stack[$handlerName]->handleConsume($request);
46 | }
47 |
48 | /**
49 | * Returns new instance with middleware handlers replaced with the ones provided.
50 | * The last specified handler will be executed first.
51 | *
52 | * @param array[]|callable[]|MiddlewareConsumeInterface[]|string[] $middlewareDefinitions Each array element is:
53 | *
54 | * - A name of a middleware class. The middleware instance will be obtained from container executed.
55 | * - A callable with `function(ServerRequestInterface $request, RequestHandlerInterface $handler):
56 | * ResponseInterface` signature.
57 | * - A "callable-like" array in format `[FooMiddleware::class, 'index']`. `FooMiddleware` instance will
58 | * be created and `index()` method will be executed.
59 | * - A function returning middleware. The middleware returned will be executed.
60 | *
61 | * For callables typed parameters are automatically injected using dependency injection container.
62 | *
63 | * @return self New instance of the {@see ConsumeMiddlewareDispatcher}
64 | */
65 | public function withMiddlewares(array $middlewareDefinitions): self
66 | {
67 | $instance = clone $this;
68 | $instance->middlewareDefinitions = array_reverse($middlewareDefinitions);
69 |
70 | // Fixes a memory leak.
71 | unset($instance->stack);
72 | $instance->stack = [];
73 |
74 | return $instance;
75 | }
76 |
77 | /**
78 | * @return bool Whether there are middleware defined in the dispatcher.
79 | */
80 | public function hasMiddlewares(): bool
81 | {
82 | return $this->middlewareDefinitions !== [];
83 | }
84 |
85 | /**
86 | * @return Closure[]
87 | */
88 | private function buildMiddlewares(): array
89 | {
90 | $middlewares = [];
91 | $factory = $this->middlewareFactory;
92 |
93 | foreach ($this->middlewareDefinitions as $middlewareDefinition) {
94 | $middlewares[] = static fn (): MiddlewareConsumeInterface => $factory->createConsumeMiddleware(
95 | $middlewareDefinition
96 | );
97 | }
98 |
99 | return $middlewares;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Middleware/Consume/ConsumeRequest.php:
--------------------------------------------------------------------------------
1 | message;
19 | }
20 |
21 | public function getQueue(): QueueInterface
22 | {
23 | return $this->queue;
24 | }
25 |
26 | public function withMessage(MessageInterface $message): self
27 | {
28 | $instance = clone $this;
29 | $instance->message = $message;
30 |
31 | return $instance;
32 | }
33 |
34 | public function withQueue(QueueInterface $queue): self
35 | {
36 | $instance = clone $this;
37 | $instance->queue = $queue;
38 |
39 | return $instance;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Middleware/Consume/MessageHandlerConsumeInterface.php:
--------------------------------------------------------------------------------
1 | stack === null) {
33 | $this->build();
34 | }
35 |
36 | /** @psalm-suppress PossiblyNullReference */
37 | return $this->stack->handleConsume($request);
38 | }
39 |
40 | private function build(): void
41 | {
42 | $handler = $this->finishHandler;
43 |
44 | foreach ($this->middlewares as $middleware) {
45 | $handler = $this->wrap($middleware, $handler);
46 | }
47 |
48 | $this->stack = $handler;
49 | }
50 |
51 | /**
52 | * Wrap handler by middlewares.
53 | */
54 | private function wrap(Closure $middlewareFactory, MessageHandlerConsumeInterface $handler): MessageHandlerConsumeInterface
55 | {
56 | return new class ($middlewareFactory, $handler) implements MessageHandlerConsumeInterface {
57 | private ?MiddlewareConsumeInterface $middleware = null;
58 |
59 | public function __construct(
60 | private readonly Closure $middlewareFactory,
61 | private readonly MessageHandlerConsumeInterface $handler,
62 | ) {
63 | }
64 |
65 | public function handleConsume(ConsumeRequest $request): ConsumeRequest
66 | {
67 | if ($this->middleware === null) {
68 | $this->middleware = ($this->middlewareFactory)();
69 | }
70 |
71 | return $this->middleware->processConsume($request, $this->handler);
72 | }
73 | };
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Middleware/Consume/MiddlewareFactoryConsume.php:
--------------------------------------------------------------------------------
1 | getFromContainer($middlewareDefinition);
63 | }
64 |
65 | return $this->tryGetFromCallable($middlewareDefinition)
66 | ?? $this->tryGetFromArrayDefinition($middlewareDefinition)
67 | ?? throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
68 | }
69 |
70 | private function getFromContainer(string $middlewareDefinition): MiddlewareConsumeInterface
71 | {
72 | if (class_exists($middlewareDefinition)) {
73 | if (is_subclass_of($middlewareDefinition, MiddlewareConsumeInterface::class)) {
74 | /** @var MiddlewareConsumeInterface */
75 | return $this->container->get($middlewareDefinition);
76 | }
77 | } elseif ($this->container->has($middlewareDefinition)) {
78 | $middleware = $this->container->get($middlewareDefinition);
79 | if ($middleware instanceof MiddlewareConsumeInterface) {
80 | return $middleware;
81 | }
82 | }
83 |
84 | throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
85 | }
86 |
87 | private function wrapCallable(callable $callback): MiddlewareConsumeInterface
88 | {
89 | return new class ($callback, $this->container) implements MiddlewareConsumeInterface {
90 | private $callback;
91 |
92 | public function __construct(
93 | callable $callback,
94 | private readonly ContainerInterface $container
95 | ) {
96 | $this->callback = $callback;
97 | }
98 |
99 | public function processConsume(ConsumeRequest $request, MessageHandlerConsumeInterface $handler): ConsumeRequest
100 | {
101 | $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
102 | if ($response instanceof ConsumeRequest) {
103 | return $response;
104 | }
105 |
106 | if ($response instanceof MiddlewareConsumeInterface) {
107 | return $response->processConsume($request, $handler);
108 | }
109 |
110 | throw new InvalidMiddlewareDefinitionException($this->callback);
111 | }
112 | };
113 | }
114 |
115 | private function tryGetFromCallable(
116 | callable|MiddlewareConsumeInterface|array|string $definition
117 | ): ?MiddlewareConsumeInterface {
118 | if ($definition instanceof Closure) {
119 | return $this->wrapCallable($definition);
120 | }
121 |
122 | if (
123 | is_array($definition)
124 | && array_keys($definition) === [0, 1]
125 | ) {
126 | try {
127 | return $this->wrapCallable($this->callableFactory->create($definition));
128 | } catch (InvalidCallableConfigurationException $exception) {
129 | throw new InvalidMiddlewareDefinitionException($definition, previous: $exception);
130 | }
131 | } else {
132 | return null;
133 | }
134 | }
135 |
136 | private function tryGetFromArrayDefinition(
137 | callable|MiddlewareConsumeInterface|array|string $definition
138 | ): ?MiddlewareConsumeInterface {
139 | if (!is_array($definition)) {
140 | return null;
141 | }
142 |
143 | try {
144 | DefinitionValidator::validateArrayDefinition($definition);
145 |
146 | $middleware = ArrayDefinition::fromConfig($definition)->resolve($this->container);
147 | if ($middleware instanceof MiddlewareConsumeInterface) {
148 | return $middleware;
149 | }
150 |
151 | throw new InvalidMiddlewareDefinitionException($definition);
152 | } catch (InvalidConfigException) {
153 | }
154 |
155 | throw new InvalidMiddlewareDefinitionException($definition);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/Middleware/Consume/MiddlewareFactoryConsumeInterface.php:
--------------------------------------------------------------------------------
1 | getMetadata()[self::FAILURE_META_KEY] ?? [];
25 |
26 | return new self($message, $metadata);
27 | }
28 |
29 | protected function getEnvelopeMetadata(): array
30 | {
31 | /** @var array $metadata */
32 | $metadata = $this->message->getMetadata()[self::FAILURE_META_KEY] ?? [];
33 |
34 | return [self::FAILURE_META_KEY => ArrayHelper::merge($metadata, $this->metadata)];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/FailureFinalHandler.php:
--------------------------------------------------------------------------------
1 | getException();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/FailureHandlingRequest.php:
--------------------------------------------------------------------------------
1 | message;
23 | }
24 |
25 | public function getException(): Throwable
26 | {
27 | return $this->exception;
28 | }
29 |
30 | public function getQueue(): QueueInterface
31 | {
32 | return $this->queue;
33 | }
34 |
35 | public function withMessage(MessageInterface $message): self
36 | {
37 | $instance = clone $this;
38 | $instance->message = $message;
39 |
40 | return $instance;
41 | }
42 |
43 | public function withException(Throwable $exception): self
44 | {
45 | $instance = clone $this;
46 | $instance->exception = $exception;
47 |
48 | return $instance;
49 | }
50 |
51 | public function withQueue(QueueInterface $queue): self
52 | {
53 | $instance = clone $this;
54 | $instance->queue = $queue;
55 |
56 | return $instance;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/FailureMiddlewareDispatcher.php:
--------------------------------------------------------------------------------
1 | init();
28 | }
29 |
30 | /**
31 | * Dispatch request through middleware to get response.
32 | *
33 | * @param FailureHandlingRequest $request Request to pass to middleware.
34 | * @param MessageFailureHandlerInterface $finishHandler Handler to use in case no middleware produced a response.
35 | */
36 | public function dispatch(
37 | FailureHandlingRequest $request,
38 | MessageFailureHandlerInterface $finishHandler
39 | ): FailureHandlingRequest {
40 | /** @var string $channel It is always string in this context */
41 | $channel = $request->getQueue()->getChannel();
42 | if (!isset($this->middlewareDefinitions[$channel]) || $this->middlewareDefinitions[$channel] === []) {
43 | $channel = self::DEFAULT_PIPELINE;
44 | }
45 | $definitions = array_reverse($this->middlewareDefinitions[$channel]);
46 |
47 | if (!isset($this->stack[$channel])) {
48 | $this->stack[$channel] = new MiddlewareFailureStack($this->buildMiddlewares(...$definitions), $finishHandler);
49 | }
50 |
51 | return $this->stack[$channel]->handleFailure($request);
52 | }
53 |
54 | /**
55 | * Returns new instance with middleware handlers replaced with the ones provided.
56 | * The last specified handler will be executed first.
57 | *
58 | * @param array[][]|callable[][]|MiddlewareFailureInterface[][]|string[][] $middlewareDefinitions Each array element is:
59 | *
60 | * - A name of a middleware class. The middleware instance will be obtained from container executed.
61 | * - A callable with `function(ServerRequestInterface $request, RequestHandlerInterface $handler):
62 | * ResponseInterface` signature.
63 | * - A "callable-like" array in format `[FooMiddleware::class, 'index']`. `FooMiddleware` instance will
64 | * be created and `index()` method will be executed.
65 | * - A function returning a middleware. The middleware returned will be executed.
66 | *
67 | * For callables typed parameters are automatically injected using dependency injection container.
68 | *
69 | * @return self New instance of the {@see FailureMiddlewareDispatcher}
70 | */
71 | public function withMiddlewares(array $middlewareDefinitions): self
72 | {
73 | $instance = clone $this;
74 | $instance->middlewareDefinitions = $middlewareDefinitions;
75 |
76 | // Fixes a memory leak.
77 | unset($instance->stack);
78 | $instance->stack = [];
79 |
80 | $instance->init();
81 |
82 | return $instance;
83 | }
84 |
85 | private function init(): void
86 | {
87 | if (!isset($this->middlewareDefinitions[self::DEFAULT_PIPELINE])) {
88 | $this->middlewareDefinitions[self::DEFAULT_PIPELINE] = [];
89 | }
90 | }
91 |
92 | /**
93 | * @return Closure[]
94 | */
95 | private function buildMiddlewares(array|callable|string|MiddlewareFailureInterface ...$definitions): array
96 | {
97 | $middlewares = [];
98 | $factory = $this->middlewareFactory;
99 |
100 | foreach ($definitions as $middlewareDefinition) {
101 | $middlewares[] = static fn (): MiddlewareFailureInterface =>
102 | $factory->createFailureMiddleware($middlewareDefinition);
103 | }
104 |
105 | return $middlewares;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php:
--------------------------------------------------------------------------------
1 | maxAttempts given.");
45 | }
46 |
47 | if ($delayInitial <= 0) {
48 | throw new InvalidArgumentException("delayInitial parameter must be a positive float, $this->delayInitial given.");
49 | }
50 |
51 | if ($delayMaximum < $delayInitial) {
52 | throw new InvalidArgumentException("delayMaximum parameter must not be less then delayInitial, , $this->delayMaximum given.");
53 | }
54 |
55 | if ($exponent <= 0) {
56 | throw new InvalidArgumentException("exponent parameter must not be zero or less, $this->exponent given.");
57 | }
58 | }
59 |
60 | public function processFailure(
61 | FailureHandlingRequest $request,
62 | MessageFailureHandlerInterface $handler
63 | ): FailureHandlingRequest {
64 | $message = $request->getMessage();
65 | if ($this->suites($message)) {
66 | $envelope = new FailureEnvelope($message, $this->createNewMeta($message));
67 | $queue = $this->queue ?? $request->getQueue();
68 | $middlewareDefinitions = $this->delayMiddleware->withDelay($this->getDelay($envelope));
69 | $messageNew = $queue->push(
70 | $envelope,
71 | $middlewareDefinitions
72 | );
73 |
74 | return $request->withMessage($messageNew);
75 | }
76 |
77 | return $handler->handleFailure($request);
78 | }
79 |
80 | private function suites(MessageInterface $message): bool
81 | {
82 | return $this->maxAttempts > $this->getAttempts($message);
83 | }
84 |
85 | private function createNewMeta(MessageInterface $message): array
86 | {
87 | return [
88 | self::META_KEY_DELAY . "-$this->id" => $this->getDelay($message),
89 | self::META_KEY_ATTEMPTS . "-$this->id" => $this->getAttempts($message) + 1,
90 | ];
91 | }
92 |
93 | private function getAttempts(MessageInterface $message): int
94 | {
95 | return $message->getMetadata()[FailureEnvelope::FAILURE_META_KEY][self::META_KEY_ATTEMPTS . "-$this->id"] ?? 0;
96 | }
97 |
98 | private function getDelay(MessageInterface $message): float
99 | {
100 | $meta = $message->getMetadata()[FailureEnvelope::FAILURE_META_KEY] ?? [];
101 | $key = self::META_KEY_DELAY . "-$this->id";
102 |
103 | $delayOriginal = (float) ($meta[$key] ?? 0);
104 | if ($delayOriginal <= 0) {
105 | $delayOriginal = $this->delayInitial;
106 | }
107 |
108 | $result = $delayOriginal * $this->exponent;
109 |
110 | return min($result, $this->delayMaximum);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php:
--------------------------------------------------------------------------------
1 | maxAttempts given.");
35 | }
36 | }
37 |
38 | public function processFailure(
39 | FailureHandlingRequest $request,
40 | MessageFailureHandlerInterface $handler
41 | ): FailureHandlingRequest {
42 | $message = $request->getMessage();
43 | if ($this->suites($message)) {
44 | $envelope = new FailureEnvelope($message, $this->createMeta($message));
45 | $envelope = ($this->targetQueue ?? $request->getQueue())->push($envelope);
46 |
47 | return $request->withMessage($envelope)
48 | ->withQueue($this->targetQueue ?? $request->getQueue());
49 | }
50 |
51 | return $handler->handleFailure($request);
52 | }
53 |
54 | private function suites(MessageInterface $message): bool
55 | {
56 | return $this->getAttempts($message) < $this->maxAttempts;
57 | }
58 |
59 | private function createMeta(MessageInterface $message): array
60 | {
61 | return [$this->getMetaKey() => $this->getAttempts($message) + 1];
62 | }
63 |
64 | private function getAttempts(MessageInterface $message): int
65 | {
66 | $result = $message->getMetadata()[FailureEnvelope::FAILURE_META_KEY][$this->getMetaKey()] ?? 0;
67 | if ($result < 0) {
68 | $result = 0;
69 | }
70 |
71 | return (int) $result;
72 | }
73 |
74 | private function getMetaKey(): string
75 | {
76 | return self::META_KEY_RESEND . "-$this->id";
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/MessageFailureHandlerInterface.php:
--------------------------------------------------------------------------------
1 | getFromContainer($middlewareDefinition);
66 | }
67 |
68 | return $this->tryGetFromCallable($middlewareDefinition)
69 | ?? $this->tryGetFromArrayDefinition($middlewareDefinition)
70 | ?? throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
71 | }
72 |
73 | private function getFromContainer(string $middlewareDefinition): MiddlewareFailureInterface
74 | {
75 | if (class_exists($middlewareDefinition)) {
76 | if (is_subclass_of($middlewareDefinition, MiddlewareFailureInterface::class)) {
77 | /** @var MiddlewareFailureInterface */
78 | return $this->container->get($middlewareDefinition);
79 | }
80 | } elseif ($this->container->has($middlewareDefinition)) {
81 | $middleware = $this->container->get($middlewareDefinition);
82 | if ($middleware instanceof MiddlewareFailureInterface) {
83 | return $middleware;
84 | }
85 | }
86 |
87 | throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
88 | }
89 |
90 | private function wrapCallable(callable $callback): MiddlewareFailureInterface
91 | {
92 | return new class ($callback, $this->container) implements MiddlewareFailureInterface {
93 | private $callback;
94 |
95 | public function __construct(
96 | callable $callback,
97 | private readonly ContainerInterface $container
98 | ) {
99 | $this->callback = $callback;
100 | }
101 |
102 | public function processFailure(FailureHandlingRequest $request, MessageFailureHandlerInterface $handler): FailureHandlingRequest
103 | {
104 | $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
105 | if ($response instanceof FailureHandlingRequest) {
106 | return $response;
107 | }
108 |
109 | if ($response instanceof MiddlewareFailureInterface) {
110 | return $response->processFailure($request, $handler);
111 | }
112 |
113 | throw new InvalidMiddlewareDefinitionException($this->callback);
114 | }
115 | };
116 | }
117 |
118 | private function tryGetFromCallable(
119 | callable|MiddlewareFailureInterface|array|string $definition
120 | ): ?MiddlewareFailureInterface {
121 | if ($definition instanceof Closure) {
122 | return $this->wrapCallable($definition);
123 | }
124 |
125 | if (
126 | is_array($definition)
127 | && array_keys($definition) === [0, 1]
128 | ) {
129 | try {
130 | return $this->wrapCallable($this->callableFactory->create($definition));
131 | } catch (InvalidCallableConfigurationException $exception) {
132 | throw new InvalidMiddlewareDefinitionException($definition, previous: $exception);
133 | }
134 | } else {
135 | return null;
136 | }
137 | }
138 |
139 | private function tryGetFromArrayDefinition(
140 | callable|MiddlewareFailureInterface|array|string $definition
141 | ): ?MiddlewareFailureInterface {
142 | if (!is_array($definition)) {
143 | return null;
144 | }
145 |
146 | try {
147 | DefinitionValidator::validateArrayDefinition($definition);
148 |
149 | $middleware = ArrayDefinition::fromConfig($definition)->resolve($this->container);
150 | if ($middleware instanceof MiddlewareFailureInterface) {
151 | return $middleware;
152 | }
153 |
154 | throw new InvalidMiddlewareDefinitionException($definition);
155 | } catch (InvalidConfigException) {
156 | }
157 |
158 | throw new InvalidMiddlewareDefinitionException($definition);
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Middleware/FailureHandling/MiddlewareFactoryFailureInterface.php:
--------------------------------------------------------------------------------
1 | stack === null) {
33 | $this->build();
34 | }
35 |
36 | /** @psalm-suppress PossiblyNullReference */
37 | return $this->stack->handleFailure($request);
38 | }
39 |
40 | private function build(): void
41 | {
42 | $handler = $this->finishHandler;
43 |
44 | foreach ($this->middlewares as $middleware) {
45 | $handler = $this->wrap($middleware, $handler);
46 | }
47 |
48 | $this->stack = $handler;
49 | }
50 |
51 | /**
52 | * Wrap handler by middlewares.
53 | */
54 | private function wrap(Closure $middlewareFactory, MessageFailureHandlerInterface $handler): MessageFailureHandlerInterface
55 | {
56 | return new class ($middlewareFactory, $handler) implements MessageFailureHandlerInterface {
57 | private ?MiddlewareFailureInterface $middleware = null;
58 |
59 | public function __construct(
60 | private readonly Closure $middlewareFactory,
61 | private readonly MessageFailureHandlerInterface $handler,
62 | ) {
63 | }
64 |
65 | public function handleFailure(FailureHandlingRequest $request): FailureHandlingRequest
66 | {
67 | if ($this->middleware === null) {
68 | $this->middleware = ($this->middlewareFactory)();
69 | }
70 |
71 | return $this->middleware->processFailure($request, $this->handler);
72 | }
73 | };
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Middleware/InvalidCallableConfigurationException.php:
--------------------------------------------------------------------------------
1 | convertDefinitionToString($middlewareDefinition);
24 | if ($definitionString !== null) {
25 | $message .= ' Got ' . $definitionString . '.';
26 | }
27 |
28 | parent::__construct($message, $code, $previous);
29 | }
30 |
31 | /**
32 | * @param mixed $middlewareDefinition Middleware definition.
33 | * @return string|null
34 | */
35 | private function convertDefinitionToString(mixed $middlewareDefinition): ?string
36 | {
37 | if (is_object($middlewareDefinition)) {
38 | return 'an instance of "' . $middlewareDefinition::class . '"';
39 | }
40 |
41 | if (is_string($middlewareDefinition)) {
42 | return '"' . $middlewareDefinition . '"';
43 | }
44 |
45 | if (is_array($middlewareDefinition)) {
46 | $items = $middlewareDefinition;
47 | foreach ($middlewareDefinition as $item) {
48 | if (!is_string($item)) {
49 | return null;
50 | }
51 | }
52 | array_walk(
53 | $items,
54 | static function (mixed &$item, int|string $key) {
55 | $item = (string) $item;
56 | $item = '"' . $item . '"';
57 | if (is_string($key)) {
58 | $item = '"' . $key . '" => ' . $item;
59 | }
60 | }
61 | );
62 |
63 | /** @var string[] $items */
64 | return '[' . implode(', ', $items) . ']';
65 | }
66 |
67 | return null;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Middleware/Push/AdapterPushHandler.php:
--------------------------------------------------------------------------------
1 | getAdapter()) === null) {
17 | throw new AdapterNotConfiguredException();
18 | }
19 | return $request->withMessage($adapter->push($request->getMessage()));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Middleware/Push/Implementation/DelayMiddlewareInterface.php:
--------------------------------------------------------------------------------
1 | getMessage()->getMetadata();
20 | if (empty($meta[IdEnvelope::MESSAGE_ID_KEY])) {
21 | $request = $request->withMessage(new IdEnvelope($request->getMessage(), uniqid('yii3-message-', true)));
22 | }
23 |
24 | return $handler->handlePush($request);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Middleware/Push/MessageHandlerPushInterface.php:
--------------------------------------------------------------------------------
1 | getFromContainer($middlewareDefinition);
63 | }
64 |
65 | return $this->tryGetFromCallable($middlewareDefinition)
66 | ?? $this->tryGetFromArrayDefinition($middlewareDefinition)
67 | ?? throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
68 | }
69 |
70 | private function getFromContainer(string $middlewareDefinition): MiddlewarePushInterface
71 | {
72 | if (class_exists($middlewareDefinition)) {
73 | if (is_subclass_of($middlewareDefinition, MiddlewarePushInterface::class)) {
74 | /** @var MiddlewarePushInterface */
75 | return $this->container->get($middlewareDefinition);
76 | }
77 | } elseif ($this->container->has($middlewareDefinition)) {
78 | $middleware = $this->container->get($middlewareDefinition);
79 | if ($middleware instanceof MiddlewarePushInterface) {
80 | return $middleware;
81 | }
82 | }
83 |
84 | throw new InvalidMiddlewareDefinitionException($middlewareDefinition);
85 | }
86 |
87 | private function wrapCallable(callable $callback): MiddlewarePushInterface
88 | {
89 | return new class ($callback, $this->container) implements MiddlewarePushInterface {
90 | private $callback;
91 |
92 | public function __construct(
93 | callable $callback,
94 | private readonly ContainerInterface $container
95 | ) {
96 | $this->callback = $callback;
97 | }
98 |
99 | public function processPush(PushRequest $request, MessageHandlerPushInterface $handler): PushRequest
100 | {
101 | $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
102 | if ($response instanceof PushRequest) {
103 | return $response;
104 | }
105 |
106 | if ($response instanceof MiddlewarePushInterface) {
107 | return $response->processPush($request, $handler);
108 | }
109 |
110 | throw new InvalidMiddlewareDefinitionException($this->callback);
111 | }
112 | };
113 | }
114 |
115 | private function tryGetFromCallable(
116 | callable|MiddlewarePushInterface|array|string $definition
117 | ): ?MiddlewarePushInterface {
118 | if ($definition instanceof Closure) {
119 | return $this->wrapCallable($definition);
120 | }
121 |
122 | if (
123 | is_array($definition)
124 | && array_keys($definition) === [0, 1]
125 | ) {
126 | try {
127 | return $this->wrapCallable($this->callableFactory->create($definition));
128 | } catch (InvalidCallableConfigurationException $exception) {
129 | throw new InvalidMiddlewareDefinitionException($definition, previous: $exception);
130 | }
131 | } else {
132 | return null;
133 | }
134 | }
135 |
136 | private function tryGetFromArrayDefinition(
137 | callable|MiddlewarePushInterface|array|string $definition
138 | ): ?MiddlewarePushInterface {
139 | if (!is_array($definition)) {
140 | return null;
141 | }
142 |
143 | try {
144 | DefinitionValidator::validateArrayDefinition($definition);
145 |
146 | $middleware = ArrayDefinition::fromConfig($definition)->resolve($this->container);
147 | if ($middleware instanceof MiddlewarePushInterface) {
148 | return $middleware;
149 | }
150 |
151 | throw new InvalidMiddlewareDefinitionException($definition);
152 | } catch (InvalidConfigException) {
153 | }
154 |
155 | throw new InvalidMiddlewareDefinitionException($definition);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/Middleware/Push/MiddlewareFactoryPushInterface.php:
--------------------------------------------------------------------------------
1 | stack === null) {
33 | $this->build();
34 | }
35 |
36 | /** @psalm-suppress PossiblyNullReference */
37 | return $this->stack->handlePush($request);
38 | }
39 |
40 | private function build(): void
41 | {
42 | $handler = $this->finishHandler;
43 |
44 | foreach ($this->middlewares as $middleware) {
45 | $handler = $this->wrap($middleware, $handler);
46 | }
47 |
48 | $this->stack = $handler;
49 | }
50 |
51 | /**
52 | * Wrap handler by middlewares.
53 | */
54 | private function wrap(Closure $middlewareFactory, MessageHandlerPushInterface $handler): MessageHandlerPushInterface
55 | {
56 | return new class ($middlewareFactory, $handler) implements MessageHandlerPushInterface {
57 | private ?MiddlewarePushInterface $middleware = null;
58 |
59 | public function __construct(
60 | private readonly Closure $middlewareFactory,
61 | private readonly MessageHandlerPushInterface $handler,
62 | ) {
63 | }
64 |
65 | public function handlePush(PushRequest $request): PushRequest
66 | {
67 | if ($this->middleware === null) {
68 | $this->middleware = ($this->middlewareFactory)();
69 | }
70 |
71 | return $this->middleware->processPush($request, $this->handler);
72 | }
73 | };
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Middleware/Push/PushMiddlewareDispatcher.php:
--------------------------------------------------------------------------------
1 | middlewareDefinitions = array_reverse($middlewareDefinitions);
27 | }
28 |
29 | /**
30 | * Dispatch request through middleware to get response.
31 | *
32 | * @param PushRequest $request Request to pass to middleware.
33 | * @param MessageHandlerPushInterface $finishHandler Handler to use in case no middleware produced a response.
34 | */
35 | public function dispatch(
36 | PushRequest $request,
37 | MessageHandlerPushInterface $finishHandler
38 | ): PushRequest {
39 | if ($this->stack === null) {
40 | $this->stack = new MiddlewarePushStack($this->buildMiddlewares(), $finishHandler);
41 | }
42 |
43 | return $this->stack->handlePush($request);
44 | }
45 |
46 | /**
47 | * Returns new instance with middleware handlers replaced with the ones provided.
48 | * The last specified handler will be executed first.
49 | *
50 | * @param array[]|callable[]|MiddlewarePushInterface[]|string[] $middlewareDefinitions Each array element is:
51 | *
52 | * - A name of a middleware class. The middleware instance will be obtained from container executed.
53 | * - A callable with `function(ServerRequestInterface $request, RequestHandlerInterface $handler):
54 | * ResponseInterface` signature.
55 | * - A "callable-like" array in format `[FooMiddleware::class, 'index']`. `FooMiddleware` instance will
56 | * be created and `index()` method will be executed.
57 | * - A function returning a middleware. The middleware returned will be executed.
58 | *
59 | * For callables typed parameters are automatically injected using dependency injection container.
60 | *
61 | * @return self New instance of the {@see PushMiddlewareDispatcher}
62 | */
63 | public function withMiddlewares(array $middlewareDefinitions): self
64 | {
65 | $instance = clone $this;
66 | $instance->middlewareDefinitions = array_reverse($middlewareDefinitions);
67 |
68 | // Fixes a memory leak.
69 | unset($instance->stack);
70 | $instance->stack = null;
71 |
72 | return $instance;
73 | }
74 |
75 | /**
76 | * @return bool Whether there are middleware defined in the dispatcher.
77 | */
78 | public function hasMiddlewares(): bool
79 | {
80 | return $this->middlewareDefinitions !== [];
81 | }
82 |
83 | /**
84 | * @return Closure[]
85 | */
86 | private function buildMiddlewares(): array
87 | {
88 | $middlewares = [];
89 | $factory = $this->middlewareFactory;
90 |
91 | foreach ($this->middlewareDefinitions as $middlewareDefinition) {
92 | $middlewares[] = static fn (): MiddlewarePushInterface => $factory->createPushMiddleware(
93 | $middlewareDefinition
94 | );
95 | }
96 |
97 | return $middlewares;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Middleware/Push/PushRequest.php:
--------------------------------------------------------------------------------
1 | message;
19 | }
20 |
21 | public function getAdapter(): ?AdapterInterface
22 | {
23 | return $this->adapter;
24 | }
25 |
26 | public function withMessage(MessageInterface $message): self
27 | {
28 | $instance = clone $this;
29 | $instance->message = $message;
30 |
31 | return $instance;
32 | }
33 |
34 | public function withAdapter(AdapterInterface $adapter): self
35 | {
36 | $instance = clone $this;
37 | $instance->adapter = $adapter;
38 |
39 | return $instance;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Provider/AdapterFactoryQueueProvider.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | private array $queues = [];
30 |
31 | private readonly StrictFactory $factory;
32 |
33 | /**
34 | * @param QueueInterface $baseQueue Base queue for queues creation.
35 | * @param array $definitions Adapter definitions indexed by channel names.
36 | * @param ContainerInterface|null $container Container to use for dependencies resolving.
37 | * @param bool $validate If definitions should be validated when set.
38 | *
39 | * @psalm-param array $definitions
40 | * @throws InvalidQueueConfigException
41 | */
42 | public function __construct(
43 | private readonly QueueInterface $baseQueue,
44 | array $definitions,
45 | ?ContainerInterface $container = null,
46 | bool $validate = true,
47 | ) {
48 | try {
49 | $this->factory = new StrictFactory($definitions, $container, $validate);
50 | } catch (InvalidConfigException $exception) {
51 | throw new InvalidQueueConfigException($exception->getMessage(), previous: $exception);
52 | }
53 | }
54 |
55 | public function get(string|BackedEnum $channel): QueueInterface
56 | {
57 | $channel = ChannelNormalizer::normalize($channel);
58 |
59 | $queue = $this->getOrTryToCreate($channel);
60 | if ($queue === null) {
61 | throw new ChannelNotFoundException($channel);
62 | }
63 |
64 | return $queue;
65 | }
66 |
67 | public function has(string|BackedEnum $channel): bool
68 | {
69 | $channel = ChannelNormalizer::normalize($channel);
70 | return $this->factory->has($channel);
71 | }
72 |
73 | /**
74 | * @throws InvalidQueueConfigException
75 | */
76 | private function getOrTryToCreate(string $channel): QueueInterface|null
77 | {
78 | if (array_key_exists($channel, $this->queues)) {
79 | return $this->queues[$channel];
80 | }
81 |
82 | if ($this->factory->has($channel)) {
83 | $adapter = $this->factory->create($channel);
84 | if (!$adapter instanceof AdapterInterface) {
85 | throw new InvalidQueueConfigException(
86 | sprintf(
87 | 'Adapter must implement "%s". For channel "%s" got "%s" instead.',
88 | AdapterInterface::class,
89 | $channel,
90 | get_debug_type($adapter),
91 | ),
92 | );
93 | }
94 | $this->queues[$channel] = $this->baseQueue->withAdapter($adapter->withChannel($channel));
95 | } else {
96 | $this->queues[$channel] = null;
97 | }
98 |
99 | return $this->queues[$channel];
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Provider/ChannelNotFoundException.php:
--------------------------------------------------------------------------------
1 | providers = $providers;
27 | }
28 |
29 | public function get(string|BackedEnum $channel): QueueInterface
30 | {
31 | foreach ($this->providers as $provider) {
32 | if ($provider->has($channel)) {
33 | return $provider->get($channel);
34 | }
35 | }
36 | throw new ChannelNotFoundException($channel);
37 | }
38 |
39 | public function has(string|BackedEnum $channel): bool
40 | {
41 | foreach ($this->providers as $provider) {
42 | if ($provider->has($channel)) {
43 | return true;
44 | }
45 | }
46 | return false;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Provider/InvalidQueueConfigException.php:
--------------------------------------------------------------------------------
1 | baseQueue->withAdapter($this->baseAdapter->withChannel($channel));
29 | }
30 |
31 | public function has(string|BackedEnum $channel): bool
32 | {
33 | return true;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Provider/QueueProviderException.php:
--------------------------------------------------------------------------------
1 | middlewareDefinitions = $middlewareDefinitions;
37 | $this->adapterPushHandler = new AdapterPushHandler();
38 | }
39 |
40 | public function getChannel(): string
41 | {
42 | $this->checkAdapter();
43 | return $this->adapter->getChannel();
44 | }
45 |
46 | public function push(
47 | MessageInterface $message,
48 | MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions
49 | ): MessageInterface {
50 | $this->checkAdapter();
51 | $this->logger->debug(
52 | 'Preparing to push message with handler name "{handlerName}".',
53 | ['handlerName' => $message->getHandlerName()]
54 | );
55 |
56 | $request = new PushRequest($message, $this->adapter);
57 | $message = $this->pushMiddlewareDispatcher
58 | ->dispatch($request, $this->createPushHandler(...$middlewareDefinitions))
59 | ->getMessage();
60 |
61 | /** @var string $messageId */
62 | $messageId = $message->getMetadata()[IdEnvelope::MESSAGE_ID_KEY] ?? 'null';
63 | $this->logger->info(
64 | 'Pushed message with handler name "{handlerName}" to the queue. Assigned ID #{id}.',
65 | ['handlerName' => $message->getHandlerName(), 'id' => $messageId]
66 | );
67 |
68 | return $message;
69 | }
70 |
71 | public function run(int $max = 0): int
72 | {
73 | $this->checkAdapter();
74 |
75 | $this->logger->debug('Start processing queue messages.');
76 | $count = 0;
77 |
78 | $handlerCallback = function (MessageInterface $message) use (&$max, &$count): bool {
79 | if (($max > 0 && $max <= $count) || !$this->handle($message)) {
80 | return false;
81 | }
82 | $count++;
83 |
84 | return true;
85 | };
86 |
87 | $this->adapter->runExisting($handlerCallback);
88 |
89 | $this->logger->info(
90 | 'Processed {count} queue messages.',
91 | ['count' => $count]
92 | );
93 |
94 | return $count;
95 | }
96 |
97 | public function listen(): void
98 | {
99 | $this->checkAdapter();
100 |
101 | $this->logger->info('Start listening to the queue.');
102 | $this->adapter->subscribe(fn (MessageInterface $message) => $this->handle($message));
103 | $this->logger->info('Finish listening to the queue.');
104 | }
105 |
106 | public function status(string|int $id): JobStatus
107 | {
108 | $this->checkAdapter();
109 | return $this->adapter->status($id);
110 | }
111 |
112 | public function withAdapter(AdapterInterface $adapter): self
113 | {
114 | $new = clone $this;
115 | $new->adapter = $adapter;
116 |
117 | return $new;
118 | }
119 |
120 | public function withMiddlewares(MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions): self
121 | {
122 | $instance = clone $this;
123 | $instance->middlewareDefinitions = $middlewareDefinitions;
124 |
125 | return $instance;
126 | }
127 |
128 | public function withMiddlewaresAdded(MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions): self
129 | {
130 | $instance = clone $this;
131 | $instance->middlewareDefinitions = [...array_values($instance->middlewareDefinitions), ...array_values($middlewareDefinitions)];
132 |
133 | return $instance;
134 | }
135 |
136 | private function handle(MessageInterface $message): bool
137 | {
138 | $this->worker->process($message, $this);
139 |
140 | return $this->loop->canContinue();
141 | }
142 |
143 | /**
144 | * @psalm-assert AdapterInterface $this->adapter
145 | */
146 | private function checkAdapter(): void
147 | {
148 | if ($this->adapter === null) {
149 | throw new AdapterNotConfiguredException();
150 | }
151 | }
152 |
153 | private function createPushHandler(MiddlewarePushInterface|callable|array|string ...$middlewares): MessageHandlerPushInterface
154 | {
155 | return new class (
156 | $this->adapterPushHandler,
157 | $this->pushMiddlewareDispatcher,
158 | array_merge($this->middlewareDefinitions, $middlewares)
159 | ) implements MessageHandlerPushInterface {
160 | public function __construct(
161 | private readonly AdapterPushHandler $adapterPushHandler,
162 | private readonly PushMiddlewareDispatcher $dispatcher,
163 | /**
164 | * @var array|array[]|callable[]|MiddlewarePushInterface[]|string[]
165 | */
166 | private readonly array $middlewares,
167 | ) {
168 | }
169 |
170 | public function handlePush(PushRequest $request): PushRequest
171 | {
172 | return $this->dispatcher
173 | ->withMiddlewares($this->middlewares)
174 | ->dispatch($request, $this->adapterPushHandler);
175 | }
176 | };
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/QueueInterface.php:
--------------------------------------------------------------------------------
1 | logger->info('Processing message #{message}.', ['message' => $message->getMetadata()[IdEnvelope::MESSAGE_ID_KEY] ?? 'null']);
51 |
52 | $name = $message->getHandlerName();
53 | $handler = $this->getHandler($name);
54 | if ($handler === null) {
55 | throw new RuntimeException(sprintf('Queue handler with name "%s" does not exist', $name));
56 | }
57 |
58 | $request = new ConsumeRequest($message, $queue);
59 | $closure = fn (MessageInterface $message): mixed => $this->injector->invoke($handler, [$message]);
60 | try {
61 | return $this->consumeMiddlewareDispatcher->dispatch($request, $this->createConsumeHandler($closure))->getMessage();
62 | } catch (Throwable $exception) {
63 | $request = new FailureHandlingRequest($request->getMessage(), $exception, $request->getQueue());
64 |
65 | try {
66 | $result = $this->failureMiddlewareDispatcher->dispatch($request, $this->createFailureHandler());
67 | $this->logger->info($exception->getMessage());
68 |
69 | return $result->getMessage();
70 | } catch (Throwable $exception) {
71 | $exception = new JobFailureException($message, $exception);
72 | $this->logger->error($exception->getMessage());
73 | throw $exception;
74 | }
75 | }
76 | }
77 |
78 | private function getHandler(string $name): ?callable
79 | {
80 | if (!array_key_exists($name, $this->handlersCached)) {
81 | $definition = $this->handlers[$name] ?? null;
82 | if ($definition === null && $this->container->has($name)) {
83 | $handler = $this->container->get($name);
84 | if ($handler instanceof MessageHandlerInterface) {
85 | $this->handlersCached[$name] = $handler->handle(...);
86 |
87 | return $this->handlersCached[$name];
88 | }
89 |
90 | return null;
91 | }
92 |
93 | $this->handlersCached[$name] = $this->prepare($this->handlers[$name] ?? null);
94 | }
95 |
96 | return $this->handlersCached[$name];
97 | }
98 |
99 | /**
100 | * Checks if the handler is a DI container alias
101 | *
102 | * @param array|callable|object|string|null $definition
103 | *
104 | * @throws ContainerExceptionInterface
105 | * @throws NotFoundExceptionInterface
106 | * @return callable|null
107 | */
108 | private function prepare(callable|object|array|string|null $definition): callable|null
109 | {
110 | if (is_string($definition) && $this->container->has($definition)) {
111 | return $this->container->get($definition);
112 | }
113 |
114 | if (
115 | is_array($definition)
116 | && array_keys($definition) === [0, 1]
117 | && is_string($definition[0])
118 | && is_string($definition[1])
119 | ) {
120 | [$className, $methodName] = $definition;
121 |
122 | if (!class_exists($className) && $this->container->has($className)) {
123 | return [
124 | $this->container->get($className),
125 | $methodName,
126 | ];
127 | }
128 |
129 | if (!class_exists($className)) {
130 | $this->logger->error("$className doesn't exist.");
131 |
132 | return null;
133 | }
134 |
135 | try {
136 | $reflection = new ReflectionMethod($className, $methodName);
137 | } catch (ReflectionException $e) {
138 | $this->logger->error($e->getMessage());
139 |
140 | return null;
141 | }
142 | if ($reflection->isStatic()) {
143 | return [$className, $methodName];
144 | }
145 | if ($this->container->has($className)) {
146 | return [
147 | $this->container->get($className),
148 | $methodName,
149 | ];
150 | }
151 |
152 | return null;
153 | }
154 |
155 | return $definition;
156 | }
157 |
158 | private function createConsumeHandler(Closure $handler): MessageHandlerConsumeInterface
159 | {
160 | return new ConsumeFinalHandler($handler);
161 | }
162 |
163 | private function createFailureHandler(): MessageFailureHandlerInterface
164 | {
165 | return new FailureFinalHandler();
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/Worker/WorkerInterface.php:
--------------------------------------------------------------------------------
1 | channel = ChannelNormalizer::normalize($channel);
25 | }
26 |
27 | public function runExisting(callable $handlerCallback): void
28 | {
29 | }
30 |
31 | public function status(int|string $id): JobStatus
32 | {
33 | return JobStatus::DONE;
34 | }
35 |
36 | public function push(MessageInterface $message): MessageInterface
37 | {
38 | return $message;
39 | }
40 |
41 | public function subscribe(callable $handlerCallback): void
42 | {
43 | }
44 |
45 | public function withChannel(string|BackedEnum $channel): AdapterInterface
46 | {
47 | $new = clone $this;
48 | $new->channel = ChannelNormalizer::normalize($channel);
49 | return $new;
50 | }
51 |
52 | public function getChannel(): string
53 | {
54 | return $this->channel;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/stubs/StubLoop.php:
--------------------------------------------------------------------------------
1 | canContinue;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/stubs/StubQueue.php:
--------------------------------------------------------------------------------
1 | adapter;
47 | }
48 |
49 | public function withAdapter(AdapterInterface $adapter): QueueInterface
50 | {
51 | $new = clone $this;
52 | $new->adapter = $adapter;
53 |
54 | return $new;
55 | }
56 |
57 | public function getChannel(): string
58 | {
59 | if ($this->adapter === null) {
60 | throw new LogicException('Adapter is not set.');
61 | }
62 |
63 | return $this->adapter->getChannel();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/stubs/StubWorker.php:
--------------------------------------------------------------------------------
1 |