├── .editorconfig
├── .github
└── workflows
│ └── coverage.yml
├── .gitignore
├── ADAPTERS.md
├── LICENSE
├── Makefile
├── README.md
├── composer.json
├── examples
├── Handlers
│ └── ExampleHandler.php
├── consumer.php
├── publisher.php
└── schemas
│ └── fruits.json
├── phpunit.xml
├── psalm.xml
├── src
├── Adapter
│ ├── Azure.php
│ ├── Beanstalk.php
│ ├── ConsumerInterface.php
│ ├── Gearman.php
│ ├── Google.php
│ ├── Iron.php
│ ├── Mercure.php
│ ├── MockQueue.php
│ ├── MockSubscriber.php
│ ├── Mqtt.php
│ ├── NullPublisher.php
│ ├── Outbox.php
│ ├── PublisherInterface.php
│ ├── RabbitMQ.php
│ ├── Redis.php
│ ├── RedisPubsub.php
│ ├── Segment.php
│ ├── Sns.php
│ ├── Sqs.php
│ ├── SubscriberInterface.php
│ └── Webhook.php
├── Application.php
├── Exception
│ ├── ConnectionException.php
│ ├── ConsumeException.php
│ ├── MessageValidationException.php
│ ├── PublishException.php
│ ├── RoutingException.php
│ └── SubscriptionException.php
├── Filter
│ ├── RedirectFilter.php
│ └── ValidatorFilter.php
├── Message.php
├── Middleware
│ ├── DeadletterMessage.php
│ ├── MiddlewareInterface.php
│ ├── ParseJsonMessage.php
│ └── ValidateMessage.php
├── Response.php
├── Router
│ ├── Consume.php
│ ├── Router.php
│ └── RouterInterface.php
└── Validator
│ ├── JsonSchemaValidator.php
│ └── ValidatorInterface.php
└── tests
├── Adapter
├── AzureTest.php
├── GearmanTest.php
├── GoogleTest.php
├── IronMQTest.php
├── MercureTest.php
├── MockQueueTest.php
├── MockSubscriberTest.php
├── MqttTest.php
├── NullPublisherTest.php
├── OutboxTest.php
├── RabbitMQTest.php
├── RedisPubsubTest.php
├── RedisTest.php
├── SegmentTest.php
├── SnsTest.php
├── SqsTest.php
└── WebhookTest.php
├── ApplicationTest.php
├── Exception
└── MessageValidationExceptionTest.php
├── Filter
├── RedirectFilterTest.php
└── ValidatorFilterTest.php
├── Fixtures
├── TestHandler.php
├── TestMiddleware.php
└── schema.json
├── MessageTest.php
├── Middleware
├── DeadletterMessageTest.php
├── ParseJsonMessageTest.php
└── ValidateMessageTest.php
├── Router
├── ConsumeAttributeTest.php
└── RouterTest.php
└── Validator
└── JsonSchemaValidatorTest.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root=true
2 |
3 | [*]
4 | end_of_line=lf
5 | indent_style=tab
6 | indent_size=4
7 | charset=utf-8
8 | trim_trailing_whitespace=true
9 |
10 | [*.yml]
11 | indent_style=space
12 | indent_size=2
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: Test suite
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 | pull_request:
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | build:
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | php-version: [8.2, 8.3, 8.4]
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Setup PHP
24 | uses: shivammathur/setup-php@v2
25 | with:
26 | php-version: ${{ matrix.php-version }}
27 | extensions: gearman
28 |
29 | - name: Validate composer.json and composer.lock
30 | run: composer validate --strict
31 |
32 | - name: Install dependencies
33 | run: composer install --prefer-dist --no-progress
34 |
35 | - name: Check dependencies
36 | run: composer audit
37 |
38 | - name: Run static analysis
39 | run: make analyze
40 |
41 | - name: Run test suite
42 | run: make coverage
43 |
44 | - uses: codecov/codecov-action@v4
45 | env:
46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
47 | with:
48 | files: ./build/logs/clover.xml
49 | #flags: unittests # optional
50 | #name: codecov-umbrella # optional
51 | #fail_ci_if_error: true # optional (default = false)
52 | #verbose: true # optional (default = false)
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | scratch.php
2 | .phpunit.cache/
3 | .vscode/
4 | .idea/
5 | vendor/
6 | build/
7 | composer.lock
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 nimbly.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | vendor/bin/phpunit
3 |
4 | coverage:
5 | vendor/bin/phpunit --coverage-clover=build/logs/clover.xml
6 |
7 | analyze:
8 | vendor/bin/psalm --no-cache
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nimbly/syndicate",
3 | "description": "A powerful queue and pubsub message publisher and consumer framework.",
4 | "keywords": ["php", "event", "queue", "pubsub", "framework"],
5 | "type": "library",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Brent Scheffler",
10 | "email": "brent@brentscheffler.com"
11 | }
12 | ],
13 | "require": {
14 | "php": "^8.2",
15 | "psr/container": "^1.0|^2.0",
16 | "psr/http-client": "^1.0",
17 | "psr/log": "^2.0|^3.0",
18 | "nimbly/resolve": "^2.0",
19 | "nimbly/capsule": "^3.0",
20 | "nimbly/shuttle": "^1.1",
21 | "softcreatr/jsonpath": "^0.9.1",
22 | "opis/json-schema": "^2.4"
23 | },
24 | "require-dev": {
25 | "vimeo/psalm": "^6.0",
26 | "phpunit/phpunit": "^10.0",
27 | "symfony/var-dumper": "^6.0",
28 | "mockery/mockery": "^1.6",
29 | "nimbly/carton": "^2.0",
30 | "monolog/monolog": "^3.8",
31 | "ext-pdo": "*",
32 | "ext-gearman": "*",
33 | "pda/pheanstalk": "^5.0",
34 | "predis/predis": "^2.3",
35 | "google/cloud-pubsub": "^2.8",
36 | "microsoft/azure-storage-queue": "^1.3",
37 | "iron-io/iron_mq": "^4.0",
38 | "php-mqtt/client": "^2.2",
39 | "php-amqplib/php-amqplib": "^3.7",
40 | "aws/aws-sdk-php": "^3.336",
41 | "segmentio/analytics-php": "^3.8"
42 | },
43 | "suggest": {
44 | "ext-pcntl": "Enables graceful shutdown of consumers.",
45 | "ext-pdo": "Required for DB based adapters.",
46 | "ext-gearman": "Required for Gearman support.",
47 | "pda/pheanstalk": "Required for Beanstalkd support.",
48 | "predis/predis": "Required for Redis support.",
49 | "google/cloud-pubsub": "Required for Google PubSub support.",
50 | "microsoft/azure-storage-queue": "Required for Azure support.",
51 | "iron-io/iron_mq": "Required for IronMQ support.",
52 | "php-mqtt/client": "Required for MQTT support.",
53 | "php-amqplib/php-amqplib": "Required for RabbitMQ support.",
54 | "aws/aws-sdk-php": "Required for AWS SNS and SQS support."
55 | },
56 | "autoload": {
57 | "psr-4": {
58 | "Nimbly\\Syndicate\\": "src/",
59 | "Nimbly\\Syndicate\\Examples\\": "examples/"
60 | }
61 | },
62 | "autoload-dev": {
63 | "psr-4": {
64 | "Nimbly\\Syndicate\\Tests\\": "tests/"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/examples/Handlers/ExampleHandler.php:
--------------------------------------------------------------------------------
1 | "bananas"]
20 | )]
21 | public function onBananas(Message $message): Response
22 | {
23 | echo \sprintf(
24 | "[HANDLER] Received message: %s\n",
25 | $message->getPayload()
26 | );
27 |
28 | if( \mt_rand(1, 10) > 9 ) {
29 | return Response::nack;
30 | }
31 |
32 | return Response::ack;
33 | }
34 |
35 | /**
36 | * Handle all messages about kiwis.
37 | *
38 | * @param Message $message
39 | * @return Response
40 | */
41 | #[Consume(
42 | topic: "fruits",
43 | payload: ["$.name" => "kiwis"]
44 | )]
45 | public function onKiwis(Message $message): Response
46 | {
47 | echo \sprintf(
48 | "[HANDLER] Received message: %s\n",
49 | $message->getPayload()
50 | );
51 |
52 | if( \mt_rand(1, 10) > 9 ) {
53 | return Response::nack;
54 | }
55 |
56 | return Response::ack;
57 | }
58 |
59 | /**
60 | * Handle all messages about oranges.
61 | *
62 | * @param Message $message
63 | * @return Response
64 | */
65 | #[Consume(
66 | topic: "fruits",
67 | payload: ["$.name" => "oranges"]
68 | )]
69 | public function onOranges(Message $message): Response
70 | {
71 | echo \sprintf(
72 | "[HANDLER] Received message: %s\n",
73 | $message->getPayload()
74 | );
75 |
76 | if( \mt_rand(1, 10) > 9 ) {
77 | return Response::nack;
78 | }
79 |
80 | return Response::ack;
81 | }
82 |
83 | /**
84 | * Handle all messages about mangoes or mangos.
85 | *
86 | * @param Message $message
87 | * @return Response
88 | */
89 | #[Consume(
90 | topic: "fruits",
91 | payload: ["$.name" => ["mangoes", "mangos"]]
92 | )]
93 | public function onMangoes(Message $message): Response
94 | {
95 | echo \sprintf(
96 | "[HANDLER] Received message: %s\n",
97 | $message->getPayload()
98 | );
99 |
100 | if( \mt_rand(1, 10) > 9 ) {
101 | return Response::nack;
102 | }
103 |
104 | return Response::ack;
105 | }
106 | }
--------------------------------------------------------------------------------
/examples/consumer.php:
--------------------------------------------------------------------------------
1 | 0]));
37 |
38 | /**
39 | * Validate messages against the "fruits" topic JSON schema.
40 | */
41 | $validator = new JsonSchemaValidator(
42 | ["fruits" => \file_get_contents(__DIR__ . "/schemas/fruits.json")]
43 | );
44 |
45 | $application = new Application(
46 | consumer: $client,
47 |
48 | /**
49 | * Create a Router instance with our single ExampleHandler class.
50 | */
51 | router: new Router(
52 | handlers: [
53 | ExampleHandler::class,
54 | ],
55 | ),
56 |
57 | /**
58 | * Redirect deadletter messages back to the same Redis queue
59 | * except publish them to the "deadletter" queue.
60 | */
61 | deadletter: new RedirectFilter($client, "deadletter"),
62 |
63 | /**
64 | * Add a simple logger to show what's going on behind the scenes.
65 | */
66 | logger: new Logger("EXAMPLE", [new ErrorLogHandler]),
67 |
68 | middleware: [
69 |
70 | /**
71 | * Parse all incoming messages as JSON.
72 | */
73 | new ParseJsonMessage,
74 |
75 | /**
76 | * Validate all incoming messages against our JSON schema.
77 | */
78 | new ValidateMessage($validator)
79 | ]
80 | );
81 |
82 | $application->listen(location: "fruits");
--------------------------------------------------------------------------------
/examples/publisher.php:
--------------------------------------------------------------------------------
1 | __DIR__ . "/schemas/fruits.json"
38 | ]),
39 | publisher: new Redis(new Client)
40 | );
41 |
42 | for( $i = 0; $i < ($argv[1] ?? 100); $i++ ){
43 |
44 | $c = \mt_rand(1, 100);
45 |
46 | if( $c <= 5 ){
47 | // There is no handler defined for this and should end up in the deadletter.
48 | $fruit = "apples";
49 | }
50 | elseif( $c <= 30 ){
51 | $fruit = "bananas";
52 | }
53 | elseif( $c <= 55 ){
54 | $fruit = "kiwis";
55 | }
56 | elseif( $c <= 80 ){
57 | $fruit = "oranges";
58 | }
59 | elseif( $c <= 90) {
60 | $fruit = "mangoes";
61 | }
62 | else {
63 | $fruit = "mangos";
64 | }
65 |
66 | $payload = [
67 | "name" => $fruit,
68 | "published_at" => \date("c"),
69 | ];
70 |
71 | $publisher->publish(
72 | new Message("fruits", \json_encode($payload)),
73 | );
74 | }
--------------------------------------------------------------------------------
/examples/schemas/fruits.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "name": {
5 | "type": "string",
6 | "enum": ["apples", "bananas", "kiwis", "oranges", "mangoes", "mangos"]
7 | },
8 |
9 | "published_at": {
10 | "type": "string",
11 | "format": "date-time"
12 | }
13 | },
14 | "required": ["name", "published_at"],
15 | "additionalProperties": false
16 | }
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 | src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Adapter/Azure.php:
--------------------------------------------------------------------------------
1 | client->createMessage(
33 | $message->getTopic(),
34 | $message->getPayload(),
35 | );
36 | }
37 | catch( ServiceException $exception ){
38 | throw new ConnectionException(
39 | message: "Connection to Azure failed.",
40 | previous: $exception
41 | );
42 | }
43 | catch( Throwable $exception ){
44 | throw new PublishException(
45 | message: "Failed to publish message.",
46 | previous: $exception
47 | );
48 | }
49 |
50 | return $result->getQueueMessage()->getMessageId();
51 | }
52 |
53 | /**
54 | * @inheritDoc
55 | *
56 | * Options:
57 | * * `delay` (integer) Visibility timeout in seconds.
58 | * * `timeout` (integer) Polling timeout in seconds.
59 | */
60 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
61 | {
62 | try {
63 |
64 | $listMessageResult = $this->client->listMessages(
65 | $topic,
66 | $this->buildListMessageOptions($max_messages, $options)
67 | );
68 | }
69 | catch( ServiceException $exception ){
70 | throw new ConnectionException(
71 | message: "Connection to Azure failed.",
72 | previous: $exception
73 | );
74 | }
75 | catch( Throwable $exception ){
76 | throw new ConsumeException(
77 | message: "Failed to consume message.",
78 | previous: $exception
79 | );
80 | }
81 |
82 | $messages = \array_map(
83 | function(QueueMessage $queueMessage) use ($topic): Message {
84 | return new Message(
85 | topic: $topic,
86 | payload: $queueMessage->getMessageText(),
87 | reference: [$queueMessage->getMessageId(), $queueMessage->getPopReceipt()],
88 | );
89 | },
90 | $listMessageResult->getQueueMessages()
91 | );
92 |
93 | return $messages;
94 | }
95 |
96 | /**
97 | * Build the Azure ListMessageOptions object for consuming messages.
98 | *
99 | * @param integer $max_messages
100 | * @param array $options
101 | * @return ListMessagesOptions
102 | */
103 | protected function buildListMessageOptions(int $max_messages, array $options): ListMessagesOptions
104 | {
105 | $listMessageOptions = new ListMessagesOptions;
106 | $listMessageOptions->setNumberOfMessages($max_messages);
107 |
108 | if( \array_key_exists("delay", $options) ){
109 | $listMessageOptions->setVisibilityTimeoutInSeconds((int) $options["delay"]);
110 | }
111 |
112 | if( \array_key_exists("timeout", $options) ){
113 | $listMessageOptions->setTimeout($options["timeout"]);
114 | }
115 |
116 | return $listMessageOptions;
117 | }
118 |
119 | /**
120 | * @inheritDoc
121 | */
122 | public function ack(Message $message): void
123 | {
124 | [$message_id, $pop_receipt] = $message->getReference();
125 |
126 | try {
127 |
128 | $this->client->deleteMessage(
129 | $message->getTopic(),
130 | $message_id,
131 | $pop_receipt
132 | );
133 | }
134 | catch( ServiceException $exception ){
135 | throw new ConnectionException(
136 | message: "Connection to Azure failed.",
137 | previous: $exception
138 | );
139 | }
140 | catch( Throwable $exception ){
141 | throw new ConsumeException(
142 | message: "Failed to ack message.",
143 | previous: $exception
144 | );
145 | }
146 | }
147 |
148 | /**
149 | * @inheritDoc
150 | */
151 | public function nack(Message $message, int $timeout = 0): void
152 | {
153 | [$message_id, $pop_receipt] = $message->getReference();
154 |
155 | try {
156 |
157 | $this->client->updateMessage(
158 | $message->getTopic(),
159 | $message_id,
160 | $pop_receipt,
161 | $message->getPayload(),
162 | $timeout
163 | );
164 | }
165 | catch( ServiceException $exception ){
166 | throw new ConnectionException(
167 | message: "Connection to Azure failed.",
168 | previous: $exception
169 | );
170 | }
171 | catch( Throwable $exception ){
172 | throw new ConsumeException(
173 | message: "Failed to nack message.",
174 | previous: $exception
175 | );
176 | }
177 | }
178 | }
--------------------------------------------------------------------------------
/src/Adapter/Beanstalk.php:
--------------------------------------------------------------------------------
1 | client->useTube(new TubeName($message->getTopic()));
39 |
40 | try {
41 |
42 | $job = $this->client->put(
43 | data: $message->getPayload(),
44 | priority: $message->getAttributes()["priority"] ?? Pheanstalk::DEFAULT_PRIORITY,
45 | delay: $options["delay"] ?? Pheanstalk::DEFAULT_DELAY,
46 | timeToRelease: $options["time_to_release"] ?? Pheanstalk::DEFAULT_TTR
47 | );
48 | }
49 | catch( PheanstalkConnectionException $exception ){
50 | throw new ConnectionException(
51 | message: "Connection to Beanstalkd failed.",
52 | previous: $exception
53 | );
54 | }
55 | catch( Throwable $exception ){
56 | throw new PublishException(
57 | message: "Failed to publish message.",
58 | previous: $exception
59 | );
60 | }
61 |
62 | return $job->getId();
63 | }
64 |
65 | /**
66 | * @inheritDoc
67 | *
68 | * Beanstalk does not allow any more than a single message to be reserved at a time. Setting
69 | * the `max_messages` argument will always result in a maximum of one message to be reserved.
70 | *
71 | * Options:
72 | * * `timeout` (integer) Polling timeout in seconds. Defaults to 10.
73 | */
74 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
75 | {
76 | if( empty($this->consumer_tube) ){
77 | $this->client->watch(new TubeName($topic));
78 | }
79 |
80 | try {
81 |
82 | $job = $this->client->reserveWithTimeout(
83 | timeout: $options["timeout"] ?? 10
84 | );
85 | }
86 | catch( PheanstalkConnectionException $exception ){
87 | throw new ConnectionException(
88 | message: "Connection to Beanstalkd failed.",
89 | previous: $exception
90 | );
91 | }
92 | catch( Throwable $exception ){
93 | throw new ConsumeException(
94 | message: "Failed to consume message.",
95 | previous: $exception
96 | );
97 | }
98 |
99 | if( empty($job) ){
100 | return [];
101 | }
102 |
103 | return [
104 | new Message(
105 | topic: $topic,
106 | payload: $job->getData(),
107 | reference: $job
108 | )
109 | ];
110 | }
111 |
112 | /**
113 | * @inheritDoc
114 | */
115 | public function ack(Message $message): void
116 | {
117 | try {
118 |
119 | $this->client->delete(
120 | job: $message->getReference()
121 | );
122 | }
123 | catch( PheanstalkConnectionException $exception ){
124 | throw new ConnectionException(
125 | message: "Connection to Beanstalkd failed.",
126 | previous: $exception
127 | );
128 | }
129 | catch( Throwable $exception ){
130 | throw new ConsumeException(
131 | message: "Failed to ack message.",
132 | previous: $exception
133 | );
134 | }
135 | }
136 |
137 | /**
138 | * @inheritDoc
139 | */
140 | public function nack(Message $message, int $timeout = 0): void
141 | {
142 | try {
143 |
144 | $this->client->release(
145 | job: $message->getReference(),
146 | priority: Pheanstalk::DEFAULT_PRIORITY,
147 | delay: $timeout
148 | );
149 | }
150 | catch( PheanstalkConnectionException $exception ){
151 | throw new ConnectionException(
152 | message: "Connection to Beanstalkd failed.",
153 | previous: $exception
154 | );
155 | }
156 | catch( Throwable $exception ){
157 | throw new ConsumeException(
158 | message: "Failed to nack message.",
159 | previous: $exception
160 | );
161 | }
162 | }
163 | }
--------------------------------------------------------------------------------
/src/Adapter/ConsumerInterface.php:
--------------------------------------------------------------------------------
1 | Implementation specific options.
21 | * @throws ConnectionException
22 | * @throws ConsumeException
23 | * @return array
24 | */
25 | public function consume(string $topic, int $max_messages = 1, array $options = []): array;
26 |
27 | /**
28 | * Acknowledge message.
29 | *
30 | * @param Message $message
31 | * @throws ConnectionException
32 | * @throws ConsumeException
33 | * @return void
34 | */
35 | public function ack(Message $message): void;
36 |
37 | /**
38 | * Disavow or release message.
39 | *
40 | * @param Message $message
41 | * @param integer $timeout
42 | * @throws ConnectionException
43 | * @throws ConsumeException
44 | * @return void
45 | */
46 | public function nack(Message $message, int $timeout = 0): void;
47 | }
--------------------------------------------------------------------------------
/src/Adapter/Gearman.php:
--------------------------------------------------------------------------------
1 | client === null ){
49 | throw new PublishException(
50 | "No GearmanClient instance was given. ".
51 | "In order to publish new jobs, you must pass a GearmanClient instance ".
52 | "into the constructor."
53 | );
54 | }
55 |
56 | $job_id = match( $message->getAttributes()["priority"] ?? "normal" ){
57 | "low" => $this->client->doLowBackground(
58 | $message->getTopic(),
59 | $message->getPayload()
60 | ),
61 |
62 | "high" => $this->client->doHighBackground(
63 | $message->getTopic(),
64 | $message->getPayload()
65 | ),
66 |
67 | default => $this->client->doBackground(
68 | $message->getTopic(),
69 | $message->getPayload()
70 | ),
71 | };
72 |
73 | return $job_id;
74 | }
75 |
76 | /**
77 | * @inheritDoc
78 | */
79 | public function subscribe(string|array $topics, callable $callback, array $options = []): void
80 | {
81 | if( $this->worker === null ){
82 | throw new SubscriptionException(
83 | "No GearmanWorker instance was given. ".
84 | "In order to process jobs, you must pass a GearmanWorker instance ".
85 | "into the constructor."
86 | );
87 | }
88 |
89 | if( !\is_array($topics) ){
90 | $topics = \array_map(
91 | fn(string $topic) => \trim($topic),
92 | \explode(",", $topics)
93 | );
94 | }
95 |
96 | foreach( $topics as $topic ){
97 | $result = $this->worker->addFunction(
98 | function_name: $topic,
99 | function: function(GearmanJob $job) use ($callback): void {
100 | $message = new Message(
101 | topic: $job->functionName(),
102 | payload: $job->workload(),
103 | reference: $job->unique(),
104 | attributes: [
105 | "handle" => $job->handle(),
106 | ]
107 | );
108 |
109 | \call_user_func($callback, $message);
110 | },
111 | timeout: $options["timeout"] ?? 0
112 | );
113 |
114 | if( $result === false ){
115 | throw new SubscriptionException(
116 | \sprintf(
117 | "Failed to subscribe to %s topic.",
118 | $topic
119 | )
120 | );
121 | }
122 | }
123 | }
124 |
125 | /**
126 | * @inheritDoc
127 | */
128 | public function loop(array $options = []): void
129 | {
130 | if( $this->worker === null ){
131 | throw new ConsumeException(
132 | "No GearmanWorker instance was given. ".
133 | "In order to process jobs, you must pass a GearmanWorker instance ".
134 | "into the constructor."
135 | );
136 | }
137 |
138 | $this->running = true;
139 |
140 | while( $this->running && $this->worker->work() ){
141 | if( $this->worker->returnCode() !== 0 ){
142 | $this->running = false;
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * @inheritDoc
149 | */
150 | public function shutdown(): void
151 | {
152 | $this->running = false;
153 | }
154 | }
--------------------------------------------------------------------------------
/src/Adapter/Google.php:
--------------------------------------------------------------------------------
1 | client->topic($message->getTopic(), $options);
28 |
29 | $options["headers"] = $message->getHeaders();
30 |
31 | try {
32 |
33 | $result = $topic->publish(
34 | [
35 | "data" => $message->getPayload(),
36 | "attributes" => $message->getAttributes(),
37 | ],
38 | $options
39 | );
40 | }
41 | catch( Throwable $exception ){
42 | throw new PublishException(
43 | message: "Failed to publish message.",
44 | previous: $exception
45 | );
46 | }
47 |
48 | return $result[0];
49 | }
50 |
51 | /**
52 | * The `topic` for Google PubSub is actually the subscription name. Therefore you must
53 | * create the subscription first before using this method.
54 | *
55 | * @inheritDoc
56 | */
57 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
58 | {
59 | $subscription = $this->client->subscription($topic);
60 |
61 | try {
62 |
63 | $response = $subscription->pull([
64 | "maxMessages" => $max_messages,
65 | ...$options
66 | ]);
67 | }
68 | catch( Throwable $exception ) {
69 | throw new ConsumeException(
70 | message: "Failed to consume message.",
71 | previous: $exception
72 | );
73 | }
74 |
75 | $messages = \array_map(
76 | function(GoogleMessage $message): Message {
77 | return new Message(
78 | topic: $message->subscription()->name(),
79 | payload: $message->data(),
80 | attributes: $message->attributes(),
81 | reference: $message,
82 | );
83 | },
84 | $response
85 | );
86 |
87 | return $messages;
88 | }
89 |
90 | /**
91 | * @inheritDoc
92 | */
93 | public function ack(Message $message): void
94 | {
95 | $subscription = $this->client->subscription($message->getTopic());
96 |
97 | try {
98 |
99 | $subscription->acknowledge($message->getReference());
100 | }
101 | catch( Throwable $exception ){
102 | throw new ConsumeException(
103 | message: "Failed to ack message.",
104 | previous: $exception
105 | );
106 | }
107 | }
108 |
109 | /**
110 | * @inheritDoc
111 | */
112 | public function nack(Message $message, int $timeout = 0): void
113 | {
114 | }
115 | }
--------------------------------------------------------------------------------
/src/Adapter/Iron.php:
--------------------------------------------------------------------------------
1 | $options["delay"] ?? null,
37 | "timeout" => $options["timeout"] ?? null,
38 | "expires_in" => $message->getAttributes()["expires_in"] ?? null
39 | ]);
40 |
41 | try {
42 |
43 | $result = $this->client->postMessage(
44 | $message->getTopic(),
45 | $message->getPayload(),
46 | $properties
47 | );
48 | }
49 | catch( HttpException $exception ){
50 | throw new ConnectionException(
51 | message: "Connection to IronMQ failed.",
52 | previous: $exception
53 | );
54 | }
55 | catch( Throwable $exception ){
56 | throw new PublishException(
57 | message: "Failed to publish message.",
58 | previous: $exception
59 | );
60 | }
61 |
62 | return (string) $result->id;
63 | }
64 |
65 | /**
66 | * @inheritDoc
67 | *
68 | * Options:
69 | * `timeout` (integer)
70 | * `wait` (integer)
71 | */
72 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
73 | {
74 | try {
75 |
76 | $reservedMessages = $this->client->reserveMessages(
77 | queue_name: $topic,
78 | count: $max_messages,
79 | timeout: $options["timeout"] ?? IronMQ::GET_MESSAGE_TIMEOUT,
80 | wait: $options["wait"] ?? IronMQ::GET_MESSAGE_WAIT
81 | );
82 | }
83 | catch( HttpException $exception ){
84 | throw new ConnectionException(
85 | message: "Connection to IronMQ failed.",
86 | previous: $exception
87 | );
88 | }
89 | catch( Throwable $exception ){
90 | throw new ConsumeException(
91 | message: "Failed to consume message.",
92 | previous: $exception
93 | );
94 | }
95 |
96 | return \array_map(
97 | function(object $reservedMessage) use ($topic): Message {
98 | return new Message(
99 | topic: $topic,
100 | payload: $reservedMessage->body,
101 | reference: [$reservedMessage->id, $reservedMessage->reservation_id]
102 | );
103 | },
104 | $reservedMessages ?? []
105 | );
106 | }
107 |
108 | /**
109 | * @inheritDoc
110 | */
111 | public function ack(Message $message): void
112 | {
113 | [$message_id, $reservation_id] = $message->getReference();
114 |
115 | try {
116 |
117 | $this->client->deleteMessage(
118 | $message->getTopic(),
119 | $message_id,
120 | $reservation_id,
121 | );
122 | }
123 | catch( HttpException $exception ){
124 | throw new ConnectionException(
125 | message: "Connection to IronMQ failed.",
126 | previous: $exception
127 | );
128 | }
129 | catch( Throwable $exception ){
130 | throw new ConsumeException(
131 | message: "Failed to ack message.",
132 | previous: $exception
133 | );
134 | }
135 | }
136 |
137 | /**
138 | * @inheritDoc
139 | */
140 | public function nack(Message $message, int $timeout = 0): void
141 | {
142 | [$message_id, $reservation_id] = $message->getReference();
143 |
144 | try {
145 |
146 | $this->client->releaseMessage(
147 | $message->getTopic(),
148 | $message_id,
149 | $reservation_id,
150 | $timeout
151 | );
152 | }
153 | catch( HttpException $exception ){
154 | throw new ConnectionException(
155 | message: "Connection to IronMQ failed.",
156 | previous: $exception
157 | );
158 | }
159 | catch( Throwable $exception ){
160 | throw new ConsumeException(
161 | message: "Failed to nack message.",
162 | previous: $exception
163 | );
164 | }
165 | }
166 | }
--------------------------------------------------------------------------------
/src/Adapter/Mercure.php:
--------------------------------------------------------------------------------
1 | $message->getTopic(),
44 | "data" => $message->getPayload(),
45 | "id" => $message->getAttributes()["id"] ?? null,
46 | "private" => (bool) ($message->getAttributes()["private"] ?? false),
47 | "type" => $message->getAttributes()["type"] ?? null,
48 | "retry" => $options["retry"] ?? null,
49 | ]);
50 |
51 | $request = new Request(
52 | method: HttpMethod::POST,
53 | uri: $this->hub,
54 | body: \http_build_query($body),
55 | headers: [
56 | "Content-Type" => "application/x-www-form-urlencoded",
57 | "Authorization" => "Bearer " . $this->token,
58 | ],
59 | );
60 |
61 | try {
62 |
63 | $response = $this->httpClient->sendRequest($request);
64 | }
65 | catch( RequestExceptionInterface $exception ){
66 | throw new ConnectionException(
67 | message: "Failed to connect to Mercure hub.",
68 | previous: $exception
69 | );
70 | }
71 |
72 | if( $response->getStatusCode() !== 200 ){
73 | throw new PublishException(
74 | message: \sprintf(
75 | "Failed to publish message: %s %s.",
76 | $response->getStatusCode(),
77 | $response->getReasonPhrase()
78 | )
79 | );
80 | }
81 |
82 | return $response->getBody()->getContents();
83 | }
84 | }
--------------------------------------------------------------------------------
/src/Adapter/MockQueue.php:
--------------------------------------------------------------------------------
1 | > $messages Array of preloaded messages in queue, indexed by topic.
18 | */
19 | public function __construct(
20 | protected array $messages = [])
21 | {
22 | }
23 |
24 | /**
25 | * @inheritDoc
26 | */
27 | public function publish(Message $message, array $options = []): ?string
28 | {
29 | if( isset($options["exception"]) ){
30 | throw new PublishException("Failed to publish message.");
31 | }
32 |
33 | $this->messages[$message->getTopic()][] = $message;
34 | return \bin2hex(\random_bytes(12));
35 | }
36 |
37 | /**
38 | * @inheritDoc
39 | */
40 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
41 | {
42 | if( isset($options["exception"]) ){
43 | throw new ConsumeException("Failed to consume messages.");
44 | }
45 |
46 | if( !\array_key_exists($topic, $this->messages) ){
47 | return [];
48 | }
49 |
50 | $messages = \array_splice($this->messages[$topic], 0, $max_messages);
51 |
52 | return $messages;
53 | }
54 |
55 | /**
56 | * @inheritDoc
57 | */
58 | public function ack(Message $message): void
59 | {
60 | return;
61 | }
62 |
63 | /**
64 | * @inheritDoc
65 | */
66 | public function nack(Message $message, int $timeout = 0): void
67 | {
68 | $this->publish($message);
69 | }
70 |
71 | /**
72 | * Get all the messages in a topic.
73 | *
74 | * @param string $topic
75 | * @return array
76 | */
77 | public function getMessages(string $topic): array
78 | {
79 | return $this->messages[$topic] ?? [];
80 | }
81 |
82 | /**
83 | * Flush messages for a given topic or all topics.
84 | *
85 | * @param string|null $topic The topic to flush messages for. If `null` flush all topics.
86 | * @return void
87 | */
88 | public function flushMessages(?string $topic = null): void
89 | {
90 | if( $topic === null ){
91 | $this->messages = [];
92 | }
93 | else {
94 | $this->messages[$topic] = [];
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/src/Adapter/MockSubscriber.php:
--------------------------------------------------------------------------------
1 | > $messages Array of preloaded messages in queue, indexed by topic.
21 | * @param array $subscriptions Array of topic names mapped to a callable.
22 | */
23 | public function __construct(
24 | protected array $messages = [],
25 | protected array $subscriptions = [])
26 | {
27 | }
28 |
29 | /**
30 | * @inheritDoc
31 | */
32 | public function publish(Message $message, array $options = []): ?string
33 | {
34 | if( isset($options["exception"]) ){
35 | throw new PublishException("Failed to publish message.");
36 | }
37 |
38 | $this->messages[$message->getTopic()][] = $message;
39 | return \bin2hex(\random_bytes(12));
40 | }
41 |
42 | /**
43 | * @inheritDoc
44 | */
45 | public function subscribe(string|array $topics, callable $callback, array $options = []): void
46 | {
47 | if( isset($options["exception"]) ){
48 | throw new SubscriptionException("Failed to subscribe to topic.");
49 | }
50 |
51 | if( \is_string($topics) ){
52 | $topics = \array_map(
53 | fn(string $topic) => \trim($topic),
54 | \explode(",", $topics)
55 | );
56 | }
57 |
58 | foreach( $topics as $topic ){
59 | $this->subscriptions[$topic] = $callback;
60 | }
61 | }
62 |
63 | /**
64 | * @inheritDoc
65 | */
66 | public function loop(array $options = []): void
67 | {
68 | if( isset($options["exception"]) ){
69 | throw new ConsumeException("Failed to consume message.");
70 | }
71 |
72 | $this->running = true;
73 |
74 | foreach( $this->subscriptions as $topic => $callback ){
75 | if( !isset($this->messages[$topic]) ){
76 | continue;
77 | }
78 |
79 | while( count($this->messages[$topic]) ){
80 | $messages = \array_splice($this->messages[$topic], 0, 1);
81 | \call_user_func($callback, $messages[0]);
82 |
83 | /**
84 | * @psalm-suppress TypeDoesNotContainType
85 | */
86 | if( $this->running === false ){
87 | return;
88 | }
89 | }
90 | }
91 | }
92 |
93 | /**
94 | * @inheritDoc
95 | */
96 | public function shutdown(): void
97 | {
98 | $this->running = false;
99 | }
100 |
101 | /**
102 | * Get all the messages in a topic.
103 | *
104 | * @param string $topic The topic name.
105 | * @return array Returns all pending messages in the topic.
106 | */
107 | public function getMessages(string $topic): array
108 | {
109 | return $this->messages[$topic] ?? [];
110 | }
111 |
112 | /**
113 | * Flush messages for a given topic or all topics.
114 | *
115 | * @param string|null $topic The topic to flush messages for. If `null` flush all topics.
116 | * @return void
117 | */
118 | public function flushMessages(?string $topic = null): void
119 | {
120 | if( $topic === null ){
121 | $this->messages = [];
122 | }
123 | else {
124 | $this->messages[$topic] = [];
125 | }
126 | }
127 |
128 | /**
129 | * Get the subscription (callback) for a topic.
130 | *
131 | * @param string $topic The topic name.
132 | * @return callable|null Returns `null` if no subscriptions exist for given topic.
133 | */
134 | public function getSubscription(string $topic): ?callable
135 | {
136 | return $this->subscriptions[$topic] ?? null;
137 | }
138 |
139 | /**
140 | * Get the running value.
141 | *
142 | * @return boolean
143 | */
144 | public function getRunning(): bool
145 | {
146 | return $this->running;
147 | }
148 | }
--------------------------------------------------------------------------------
/src/Adapter/Mqtt.php:
--------------------------------------------------------------------------------
1 | connect();
36 |
37 | try {
38 |
39 | $this->client->publish(
40 | topic: $message->getTopic(),
41 | message: $message->getPayload(),
42 | qualityOfService: (int) ($message->getAttributes()["qos"] ?? MqttClient::QOS_AT_MOST_ONCE),
43 | retain: (bool) ($message->getAttributes()["retain"] ?? false)
44 | );
45 | }
46 | catch( ConnectingToBrokerFailedException $exception ){
47 | throw new ConnectionException(
48 | message: "Connection to MQTT broker failed.",
49 | previous: $exception
50 | );
51 | }
52 | catch( Throwable $exception ) {
53 | throw new PublishException(
54 | message: "Failed to publish message.",
55 | previous: $exception
56 | );
57 | }
58 |
59 | return null;
60 | }
61 |
62 | /**
63 | * @inheritDoc
64 | *
65 | * Options:
66 | * * `qos` One of `MqttClient::QOS_AT_MOST_ONCE`, `MqttClient::QOS_AT_LEAST_ONCE`, or `MqttClient::QOS_EXACTLY_ONCE`. Defaults to `MqttClient::QOS_AT_MOST_ONCE`.
67 | */
68 | public function subscribe(string|array $topics, callable $callback, array $options = []): void
69 | {
70 | if( !\is_array($topics) ){
71 | $topics = \array_map(
72 | fn(string $topic) => \trim($topic),
73 | \explode(",", $topics)
74 | );
75 | }
76 |
77 | $this->connect();
78 |
79 | foreach( $topics as $topic ){
80 | try {
81 |
82 | $this->client->subscribe(
83 | topicFilter: $topic,
84 | callback: function(string $topic, string $body, bool $retained, array $matched) use ($callback): void {
85 | $message = new Message(
86 | topic: $topic,
87 | payload: $body,
88 | attributes: [
89 | "retained" => $retained,
90 | "matched" => $matched,
91 | ]
92 | );
93 |
94 | \call_user_func($callback, $message);
95 | },
96 | qualityOfService: $options["qos"] ?? MqttClient::QOS_AT_MOST_ONCE
97 | );
98 | }
99 | catch( ConnectingToBrokerFailedException $exception ){
100 | throw new ConnectionException(
101 | message: "Connection to MQTT broker failed.",
102 | previous: $exception
103 | );
104 | }
105 | catch( Throwable $exception ){
106 | throw new SubscriptionException(
107 | message: "Failed to subscribe to topic.",
108 | previous: $exception
109 | );
110 | }
111 | }
112 | }
113 |
114 | /**
115 | * @inheritDoc
116 | *
117 | * Options:
118 | * * `allow_sleep` (boolean) Defaults to true.
119 | * * `exit_when_empty` (boolea) Defaults to false.
120 | * * `timeout` (integer|null) Defaults to null.
121 | */
122 | public function loop(array $options = []): void
123 | {
124 | $this->connect();
125 |
126 | try {
127 |
128 | $this->client->loop(
129 | allowSleep: (bool) ($options["allow_sleep"] ?? true),
130 | exitWhenQueuesEmpty: (bool) ($options["exit_when_empty"] ?? false),
131 | queueWaitLimit: $options["timeout"] ?? null,
132 | );
133 | }
134 | catch( ConnectingToBrokerFailedException $exception ){
135 | throw new ConnectionException(
136 | message: "Connection to MQTT broker failed.",
137 | previous: $exception
138 | );
139 | }
140 | catch( Throwable $exception ){
141 | throw new ConsumeException(
142 | message: "Failed to consume message.",
143 | previous: $exception
144 | );
145 | }
146 |
147 | $this->disconnect();
148 | }
149 |
150 | /**
151 | * @inheritDoc
152 | */
153 | public function shutdown(): void
154 | {
155 | try {
156 |
157 | $this->client->interrupt();
158 | }
159 | catch( Throwable $exception ){
160 | throw new ConnectionException(
161 | message: "Connection to MQTT broker failed.",
162 | previous: $exception
163 | );
164 | }
165 | }
166 |
167 | /**
168 | * Connect to Mqtt.
169 | *
170 | * @throws ConnectionException
171 | * @return void
172 | */
173 | private function connect(): void
174 | {
175 | if( !$this->client->isConnected() ){
176 |
177 | try {
178 |
179 | $this->client->connect();
180 | }
181 | catch( Throwable $exception ){
182 | throw new ConnectionException(
183 | message: "Connection to MQTT broker failed.",
184 | previous: $exception
185 | );
186 | }
187 | }
188 | }
189 |
190 | /**
191 | * Disconnect from Mqtt.
192 | *
193 | * @throws ConnectionException
194 | * @return void
195 | */
196 | private function disconnect(): void
197 | {
198 | if( $this->client->isConnected() ){
199 |
200 | try {
201 |
202 | $this->client->disconnect();
203 | }
204 | catch( Throwable $exception ){
205 | throw new ConnectionException(
206 | message: "Connection to MQTT broker failed.",
207 | previous: $exception
208 | );
209 | }
210 | }
211 | }
212 |
213 | /**
214 | * Disconnect when tearing down.
215 | */
216 | public function __destruct()
217 | {
218 | $this->disconnect();
219 | }
220 | }
--------------------------------------------------------------------------------
/src/Adapter/NullPublisher.php:
--------------------------------------------------------------------------------
1 | receipt) ){
30 | return \call_user_func($this->receipt, $message);
31 | }
32 |
33 | return \bin2hex(\random_bytes(12));
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Adapter/Outbox.php:
--------------------------------------------------------------------------------
1 | buildValues($message);
54 |
55 | $statement = $this->getStatement(
56 | $this->buildQuery($values)
57 | );
58 |
59 | try {
60 |
61 | $result = $statement->execute($values);
62 | }
63 | catch( Throwable $exception ){
64 | throw new PublishException(
65 | message: "Failed to publish message.",
66 | previous: $exception
67 | );
68 | }
69 |
70 | if( $result === false ){
71 | throw new PublishException(
72 | "Failed to publish message."
73 | );
74 | }
75 |
76 | return $values["id"] ?? $this->pdo->lastInsertId();
77 | }
78 |
79 | /**
80 | * Build the query to insert a record.
81 | *
82 | * @param array $values
83 | * @return string
84 | */
85 | protected function buildQuery(array $values): string
86 | {
87 | return \sprintf(
88 | "insert into %s (%s) values (%s)",
89 | $this->table,
90 | \implode(", ", \array_keys($values)),
91 | \implode(", ", \array_map(fn(string $key) => ":{$key}", \array_keys($values)))
92 | );
93 | }
94 |
95 | /**
96 | * Build the value array to be used in the query.
97 | *
98 | * @param Message $message
99 | * @return array
100 | */
101 | protected function buildValues(Message $message): array
102 | {
103 | $values = [
104 | "topic" => $message->getTopic(),
105 | "payload" => $message->getPayload(),
106 | "headers" => $message->getHeaders() ? \json_encode($message->getHeaders()) : null,
107 | "attributes" => $message->getAttributes() ? \json_encode($message->getAttributes(), JSON_FORCE_OBJECT) : null,
108 | "created_at" => \date("c"),
109 | ];
110 |
111 | if( \is_callable($this->identity_generator) ){
112 | $values["id"] = \call_user_func($this->identity_generator, $message);
113 | }
114 |
115 | return $values;
116 | }
117 |
118 | /**
119 | * Get the publish PDOStatement needed to insert new messages.
120 | *
121 | * @param string $query
122 | * @return PDOStatement
123 | */
124 | protected function getStatement(string $query): PDOStatement
125 | {
126 | if( $this->statement === null ){
127 | try {
128 |
129 | $this->statement = $this->pdo->prepare($query);
130 | }
131 | catch( Throwable $exception ){
132 | throw new PublishException(
133 | message: "Failed to publish message.",
134 | previous: $exception
135 | );
136 | }
137 |
138 | if( $this->statement === false ){
139 | throw new PublishException(
140 | message: "Failed to publish message."
141 | );
142 | }
143 | }
144 |
145 | return $this->statement;
146 | }
147 | }
--------------------------------------------------------------------------------
/src/Adapter/PublisherInterface.php:
--------------------------------------------------------------------------------
1 | $options A key/value pair of implementation specific options when publishing.
20 | * @throws ConnectionException
21 | * @throws PublishException
22 | * @return string|null Some publishers return a receipt or confirmation identifier.
23 | */
24 | public function publish(Message $message, array $options = []): ?string;
25 | }
--------------------------------------------------------------------------------
/src/Adapter/RabbitMQ.php:
--------------------------------------------------------------------------------
1 | channel->basic_publish(
44 | msg: new AMQPMessage($message->getPayload(), $message->getAttributes()),
45 | exchange: $options["exchange"] ?? $this->exchange,
46 | routing_key: $message->getTopic(),
47 | mandatory: $message->getAttributes()["mandatory"] ?? false,
48 | immediate: $message->getAttributes()["immediate"] ?? false,
49 | ticket: $message->getAttributes()["ticket"] ?? null,
50 | );
51 | }
52 | catch( AMQPConnectionClosedException|AMQPConnectionBlockedException $exception ){
53 | throw new ConnectionException(
54 | message: "Connection to RabbitMQ failed.",
55 | previous: $exception
56 | );
57 | }
58 | catch( Throwable $exception ){
59 | throw new PublishException(
60 | message: "Failed to publish message.",
61 | previous: $exception
62 | );
63 | }
64 |
65 | return null;
66 | }
67 |
68 | /**
69 | * @inheritDoc
70 | *
71 | * Options:
72 | * * `no_ack` (boolean) Do not automatically ACK messages as they are pulled off the queue. Defaults to true.
73 | * * `ticket` (?string) Defaults to null.
74 | */
75 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
76 | {
77 | try {
78 |
79 | $message = $this->channel->basic_get(
80 | queue: $topic,
81 | no_ack: $options["no_ack"] ?? true,
82 | ticket: $options["ticket"] ?? null,
83 | );
84 | }
85 | catch( AMQPConnectionClosedException|AMQPConnectionBlockedException $exception ){
86 | throw new ConnectionException(
87 | message: "Connection to RabbitMQ failed.",
88 | previous: $exception
89 | );
90 | }
91 | catch( Throwable $exception ){
92 | throw new ConsumeException(
93 | message: "Failed to consume message.",
94 | previous: $exception
95 | );
96 | }
97 |
98 | if( $message === null ){
99 | return [];
100 | }
101 |
102 | return [
103 | new Message(
104 | topic: $topic,
105 | payload: $message->getBody(),
106 | reference: $message
107 | )
108 | ];
109 | }
110 |
111 | /**
112 | * @inheritDoc
113 | */
114 | public function ack(Message $message): void
115 | {
116 | /**
117 | * @var AMQPMessage $rabbitMessage
118 | */
119 | $rabbitMessage = $message->getReference();
120 |
121 | try {
122 |
123 | $rabbitMessage->ack();
124 | }
125 | catch( AMQPConnectionClosedException|AMQPConnectionBlockedException $exception ){
126 | throw new ConnectionException(
127 | message: "Connection to RabbitMQ failed.",
128 | previous: $exception
129 | );
130 | }
131 | catch( Throwable $exception ){
132 | throw new ConsumeException(
133 | message: "Failed to ack message.",
134 | previous: $exception
135 | );
136 | }
137 | }
138 |
139 | /**
140 | * @inheritDoc
141 | */
142 | public function nack(Message $message, int $timeout = 0): void
143 | {
144 | /**
145 | * @var AMQPMessage $rabbitMessage
146 | */
147 | $rabbitMessage = $message->getReference();
148 |
149 | try {
150 |
151 | $rabbitMessage->reject(true);
152 | }
153 | catch( AMQPConnectionClosedException|AMQPConnectionBlockedException $exception ){
154 | throw new ConnectionException(
155 | message: "Connection to RabbitMQ failed.",
156 | previous: $exception
157 | );
158 | }
159 | catch( Throwable $exception ){
160 | throw new ConsumeException(
161 | message: "Failed to nack message.",
162 | previous: $exception
163 | );
164 | }
165 | }
166 | }
--------------------------------------------------------------------------------
/src/Adapter/Redis.php:
--------------------------------------------------------------------------------
1 | client->rpush(
36 | $message->getTopic(),
37 | [$message->getPayload()]
38 | );
39 | }
40 | catch( RedisConnectionException $exception ){
41 | throw new ConnectionException(
42 | message: "Connection to Redis failed.",
43 | previous: $exception
44 | );
45 | }
46 | catch( Throwable $exception ){
47 | throw new PublishException(
48 | message: "Failed to publish message.",
49 | previous: $exception
50 | );
51 | }
52 |
53 | return (string) $result;
54 | }
55 |
56 | /**
57 | * @inheritDoc
58 | *
59 | * Options:
60 | * None
61 | */
62 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
63 | {
64 | try {
65 |
66 | /**
67 | * @var array $messages
68 | */
69 | $messages = $this->client->lpop($topic, $max_messages);
70 | }
71 | catch( RedisConnectionException $exception ){
72 | throw new ConnectionException(
73 | message: "Connection to Redis failed.",
74 | previous: $exception
75 | );
76 | }
77 | catch( Throwable $exception ){
78 | throw new ConsumeException(
79 | message: "Failed to consume message.",
80 | previous: $exception
81 | );
82 | }
83 |
84 | if( empty($messages) ){
85 | return [];
86 | }
87 |
88 | return \array_map(
89 | function(string $message) use ($topic): Message {
90 | return new Message(
91 | topic: $topic,
92 | payload: $message
93 | );
94 | },
95 | $messages
96 | );
97 | }
98 |
99 | /**
100 | * @inheritDoc
101 | */
102 | public function ack(Message $message): void
103 | {
104 | return;
105 | }
106 |
107 | /**
108 | * @inheritDoc
109 | */
110 | public function nack(Message $message, int $timeout = 0): void
111 | {
112 | try {
113 |
114 | $this->publish($message);
115 | }
116 | catch( PublishException $exception ){
117 | throw new ConsumeException(
118 | message: "Failed to nack message.",
119 | previous: $exception
120 | );
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/src/Adapter/RedisPubsub.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | protected array $subscriptions = [];
26 |
27 | /**
28 | * @param Client $client Predis Client instance.
29 | */
30 | public function __construct(
31 | protected Client $client,
32 | )
33 | {
34 | }
35 |
36 | /**
37 | * @inheritDoc
38 | */
39 | public function publish(Message $message, array $options = []): ?string
40 | {
41 | try {
42 |
43 | $result = $this->client->publish(
44 | $message->getTopic(),
45 | $message->getPayload()
46 | );
47 | }
48 | catch( RedisConnectionException $exception ){
49 | throw new ConnectionException(
50 | message: "Connection to Redis failed.",
51 | previous: $exception
52 | );
53 | }
54 | catch( Throwable $exception ) {
55 | throw new PublishException(
56 | message: "Failed to publish message.",
57 | previous: $exception
58 | );
59 | }
60 |
61 | return (string) $result;
62 | }
63 |
64 | /**
65 | * @inheritDoc
66 | */
67 | public function subscribe(string|array $topics, callable $callback, array $options = []): void
68 | {
69 | if( !\is_array($topics) ){
70 | $topics = \array_map(
71 | fn(string $topic) => \trim($topic),
72 | \explode(",", $topics)
73 | );
74 | }
75 |
76 | foreach( $topics as $channel ){
77 | $this->subscriptions[$channel] = $callback;
78 | }
79 |
80 | try {
81 |
82 | $this->getLoop()->subscribe(...$topics);
83 | }
84 | catch( RedisConnectionException $exception ){
85 | throw new ConnectionException(
86 | message: "Connection to Redis failed.",
87 | previous: $exception
88 | );
89 | }
90 | catch( Throwable $exception ){
91 | throw new SubscriptionException(
92 | message: "Failed to subscribe to topic.",
93 | previous: $exception
94 | );
95 | }
96 | }
97 |
98 | /**
99 | * @inheritDoc
100 | * @throws ConsumeException
101 | */
102 | public function loop(array $options = []): void
103 | {
104 | /**
105 | * Because Predis uses fgets() to read from a socket,
106 | * it is a hard blocking call. We disable async signals
107 | * and manually call pcntl_signal_dispatch() with each
108 | * loop. This requires data to be read first from the socket,
109 | * so if there is no data, you will still block and wait
110 | * until there is data.
111 | */
112 | \pcntl_async_signals(false);
113 |
114 | $loop = $this->getLoop();
115 |
116 | while( $loop->valid() ) {
117 |
118 | try {
119 | /**
120 | * @var object{kind:string,channel:string,payload:string} $msg
121 | */
122 | $msg = $loop->current();
123 | }
124 | catch( RedisConnectionException $exception ){
125 | throw new ConnectionException(
126 | message: "Connection to Redis failed.",
127 | previous: $exception
128 | );
129 | }
130 | catch( Throwable $exception ){
131 | throw new ConsumeException(
132 | message: "Failed to consume message.",
133 | previous: $exception
134 | );
135 | }
136 |
137 | if( $msg->kind === "message" ){
138 | $callback = $this->subscriptions[$msg->channel] ?? null;
139 |
140 | if( $callback === null ){
141 | throw new ConsumeException(
142 | \sprintf(
143 | "Message received from channel \"%s\", but no callback defined for it.",
144 | $msg->channel
145 | )
146 | );
147 | }
148 |
149 | $message = new Message(
150 | topic: $msg->channel,
151 | payload: $msg->payload,
152 | reference: $msg
153 | );
154 |
155 | \call_user_func($callback, $message);
156 | }
157 |
158 | \pcntl_signal_dispatch();
159 | }
160 | }
161 |
162 | /**
163 | * @inheritDoc
164 | */
165 | public function shutdown(): void
166 | {
167 | try {
168 |
169 | $this->getLoop()->stop(true);
170 | }
171 | catch( Throwable $exception ){
172 | throw new ConnectionException(
173 | message: "Failed to shutdown subscriber.",
174 | previous: $exception
175 | );
176 | }
177 | }
178 |
179 | /**
180 | * Get the Redis consumer loop.
181 | *
182 | * @return Consumer
183 | * @throws ConsumeException
184 | */
185 | protected function getLoop(): Consumer
186 | {
187 | if( empty($this->loop) ){
188 | $this->loop = $this->client->pubSubLoop();
189 |
190 | if( empty($this->loop) ){
191 | throw new ConsumeException("Could not initialize Redis pubsub loop.");
192 | }
193 | }
194 |
195 | return $this->loop;
196 | }
197 | }
--------------------------------------------------------------------------------
/src/Adapter/Segment.php:
--------------------------------------------------------------------------------
1 | getTopic() ){
36 | "track" => $this->client->track(
37 | $this->buildTrackRequest($message)
38 | ),
39 |
40 | "identify" => $this->client->identify(
41 | $this->buildIdentifyRequest($message)
42 | ),
43 |
44 | "group" => $this->client->group(
45 | $this->buildGroupRequest($message)
46 | ),
47 |
48 | default => throw new PublishException(
49 | message: \sprintf(
50 | "Unknown or unsupported Segment call %s.",
51 | $message->getTopic()
52 | )
53 | )
54 | };
55 |
56 | if( $result === false ){
57 | throw new PublishException(
58 | message: "Failed to publish message."
59 | );
60 | }
61 |
62 | if( $this->autoflush ){
63 | $this->client->flush();
64 | }
65 |
66 | return null;
67 | }
68 |
69 | /**
70 | * Build the base/common request elements for all Segment actions.
71 | *
72 | * @param Message $message
73 | * @return array
74 | */
75 | protected function buildCommonRequest(Message $message): array
76 | {
77 | $request = \array_filter([
78 | "anonymousId" => $message->getAttributes()["anonymousId"] ?? null,
79 | "userId" => $message->getAttributes()["userId"] ?? null,
80 | "integrations" => $message->getAttributes()["integrations"] ?? [],
81 | "timestamp" => $message->getAttributes()["timestamp"] ?? null,
82 | "context" => $message->getAttributes()["context"] ?? null,
83 | ]);
84 |
85 | if( !isset($request["anonymousId"]) && !isset($request["userId"]) ){
86 | throw new PublishException(
87 | message: "Segment requires an anonymous ID or a user ID. Please add either an \"anonymousId\" or \"userId\" to the message attributes."
88 | );
89 | }
90 |
91 | return $request;
92 | }
93 |
94 | /**
95 | * Build the request needed to make a track call.
96 | *
97 | * @param Message $message
98 | * @return array
99 | */
100 | protected function buildTrackRequest(Message $message): array
101 | {
102 | $request = \array_merge(
103 | $this->buildCommonRequest($message),
104 | [
105 | "event" => $message->getAttributes()["event"] ?? null,
106 | "properties" => \json_decode($message->getPayload(), true),
107 | ]
108 | );
109 |
110 | if( !isset($request["event"]) ){
111 | throw new PublishException(
112 | message: "Segment track call requires an event name. Please add an \"event\" attribute to the message."
113 | );
114 | }
115 |
116 | return $request;
117 | }
118 |
119 | /**
120 | * Build the request needed to make an Identify call.
121 | *
122 | * @param Message $message
123 | * @return array
124 | */
125 | protected function buildIdentifyRequest(Message $message): array
126 | {
127 | $request = \array_merge(
128 | $this->buildCommonRequest($message),
129 | [
130 | "traits" => \json_decode($message->getPayload(), true),
131 | ]
132 | );
133 |
134 | return $request;
135 | }
136 |
137 | /**
138 | * Build the request needed to make a Group call.
139 | *
140 | * @param Message $message
141 | * @return array
142 | */
143 | protected function buildGroupRequest(Message $message): array
144 | {
145 | if( !isset($message->getAttributes()["groupId"]) ){
146 | throw new PublishException(
147 | message: "Segment group call requires a groupId. Please add a \"groupId\" attribute to the message."
148 | );
149 | }
150 |
151 | $request = \array_merge(
152 | $this->buildCommonRequest($message),
153 | [
154 | "groupId" => $message->getAttributes()["groupId"],
155 | "traits" => \json_decode($message->getPayload(), true),
156 | ]
157 | );
158 |
159 | return $request;
160 | }
161 | }
--------------------------------------------------------------------------------
/src/Adapter/Sns.php:
--------------------------------------------------------------------------------
1 | buildArguments($message, $options);
35 |
36 | try {
37 |
38 | $result = $this->client->publish($args);
39 | }
40 | catch( CredentialsException $exception ){
41 | throw new ConnectionException(
42 | message: "Connection to SNS failed.",
43 | previous: $exception
44 | );
45 | }
46 | catch( Throwable $exception ){
47 | throw new PublishException(
48 | message: "Failed to publish message.",
49 | previous: $exception
50 | );
51 | }
52 |
53 | return $result->get("MessageId");
54 | }
55 |
56 | /**
57 | * Build the arguments array needed to call SNS.
58 | *
59 | * @param Message $message
60 | * @param array $options
61 | * @return array
62 | */
63 | private function buildArguments(Message $message, array $options = []): array
64 | {
65 | $attributes = \array_filter(
66 | $message->getAttributes(),
67 | fn(string $key) => !\in_array($key, ["MessageGroupId", "MessageDeduplicationId"]),
68 | ARRAY_FILTER_USE_KEY
69 | );
70 |
71 | $args = \array_filter([
72 | "TopicArn" => $this->base_arn ?? "" . $message->getTopic(),
73 | "Message" => $message->getPayload(),
74 | "MessageGroupId" => $message->getAttributes()["MessageGroupId"] ?? null,
75 | "MessageDeduplicationId" => $message->getAttributes()["MessageDeduplicationId"] ?? null,
76 | "MessageAttributes" => $attributes,
77 | ...$options,
78 | ]);
79 |
80 | return $args;
81 | }
82 | }
--------------------------------------------------------------------------------
/src/Adapter/Sqs.php:
--------------------------------------------------------------------------------
1 | buildPublishArguments($message, $options);
40 |
41 | try {
42 |
43 | $result = $this->client->sendMessage($args);
44 | }
45 | catch( CredentialsException $exception ){
46 | throw new ConnectionException(
47 | message: "Connection to SQS failed.",
48 | previous: $exception
49 | );
50 | }
51 | catch( Throwable $exception ){
52 | throw new PublishException(
53 | message: "Failed to publish message.",
54 | previous: $exception
55 | );
56 | }
57 |
58 | return (string) $result->get("MessageId");
59 | }
60 |
61 | /**
62 | * Build the arguments array needed to call SQS when publishing a message.
63 | *
64 | * @param Message $message
65 | * @param array $options
66 | * @return array
67 | */
68 | private function buildPublishArguments(Message $message, array $options = []): array
69 | {
70 | $attributes = \array_filter(
71 | $message->getAttributes(),
72 | fn(string $key) => !\in_array($key, ["MessageGroupId", "MessageDeduplicationId"]),
73 | ARRAY_FILTER_USE_KEY
74 | );
75 |
76 | $args = \array_filter([
77 | "QueueUrl" => $this->base_url ?? "" . $message->getTopic(),
78 | "MessageBody" => $message->getPayload(),
79 | "MessageGroupId" => $message->getAttributes()["MessageGroupId"] ?? null,
80 | "MessageDeduplicationId" => $message->getAttributes()["MessageDeduplicationId"] ?? null,
81 | "MessageAttributes" => $attributes,
82 | ...$options,
83 | ]);
84 |
85 | return $args;
86 | }
87 |
88 | /**
89 | * @inheritDoc
90 | */
91 | public function consume(string $topic, int $max_messages = 1, array $options = []): array
92 | {
93 | try {
94 |
95 | $result = $this->client->receiveMessage([
96 | "QueueUrl" => $this->base_url ?? "" . $topic,
97 | "MaxNumberOfMessages" => $max_messages,
98 | ...$options
99 | ]);
100 | }
101 | catch( CredentialsException $exception ){
102 | throw new ConnectionException(
103 | message: "Connection to SQS failed.",
104 | previous: $exception
105 | );
106 | }
107 | catch( Throwable $exception ){
108 | throw new ConsumeException(
109 | message: "Failed to consume message.",
110 | previous: $exception
111 | );
112 | }
113 |
114 | $messages = \array_map(
115 | function(array $message) use ($topic): Message {
116 | return new Message(
117 | topic: $topic,
118 | payload: $message["Body"],
119 | attributes: $message["Attributes"],
120 | reference: $message["ReceiptHandle"]
121 | );
122 | },
123 | $result->get("Messages") ?? []
124 | );
125 |
126 | return $messages;
127 | }
128 |
129 | /**
130 | * @inheritDoc
131 | */
132 | public function ack(Message $message): void
133 | {
134 | $request = [
135 | "QueueUrl" => $message->getTopic(),
136 | "ReceiptHandle" => $message->getReference(),
137 | ];
138 |
139 | try {
140 |
141 | $this->client->deleteMessage($request);
142 | }
143 | catch( CredentialsException $exception ){
144 | throw new ConnectionException(
145 | message: "Connection to SQS failed.",
146 | previous: $exception
147 | );
148 | }
149 | catch( Throwable $exception ){
150 | throw new ConsumeException(
151 | message: "Failed to ack message.",
152 | previous: $exception
153 | );
154 | }
155 | }
156 |
157 | /**
158 | * @inheritDoc
159 | */
160 | public function nack(Message $message, int $timeout = 0): void
161 | {
162 | $request = [
163 | "QueueUrl" => $message->getTopic(),
164 | "ReceiptHandle" => $message->getReference(),
165 | "VisibilityTimeout" => $timeout,
166 | ];
167 |
168 | try {
169 |
170 | $this->client->changeMessageVisibility($request);
171 | }
172 | catch( CredentialsException $exception ){
173 | throw new ConnectionException(
174 | message: "Connection to SQS failed.",
175 | previous: $exception
176 | );
177 | }
178 | catch( Throwable $exception ){
179 | throw new ConsumeException(
180 | message: "Failed to nack message.",
181 | previous: $exception
182 | );
183 | }
184 | }
185 | }
--------------------------------------------------------------------------------
/src/Adapter/SubscriberInterface.php:
--------------------------------------------------------------------------------
1 | $topics A topic name or an array of topic names to subscribe to.
19 | * @param callable $callback The callback function to trigger when a message from topic is received.
20 | * @param array $options Key/value pairs of options. This is dependent on the implementation being used.
21 | * @throws ConnectionException
22 | * @throws SubscriptionException
23 | * @return void
24 | */
25 | public function subscribe(string|array $topics, callable $callback, array $options = []): void;
26 |
27 | /**
28 | * Begin consumer loop.
29 | *
30 | * @param array $options Key/value pairs of options. This is dependent on the implementation being used.
31 | * @throws ConnectionException
32 | * @throws ConsumeException
33 | * @return void
34 | */
35 | public function loop(array $options = []): void;
36 |
37 | /**
38 | * Shutdown the consumer loop.
39 | *
40 | * @throws ConnectionException
41 | * @return void
42 | */
43 | public function shutdown(): void;
44 | }
--------------------------------------------------------------------------------
/src/Adapter/Webhook.php:
--------------------------------------------------------------------------------
1 | $headers Default headers to send with each request.
32 | * @param HttpMethod $method Default HTTP method to use. Defaults to `HttpMethod::POST`.
33 | */
34 | public function __construct(
35 | protected ClientInterface $httpClient = new Shuttle,
36 | protected ?string $hostname = null,
37 | protected array $headers = [],
38 | protected HttpMethod $method = HttpMethod::POST
39 | )
40 | {
41 | }
42 |
43 | /**
44 | * @inheritDoc
45 | *
46 | * Options:
47 | * * `method` (string) Override the default HTTP method to use.
48 | */
49 | public function publish(Message $message, array $options = []): ?string
50 | {
51 | try {
52 |
53 | $response = $this->httpClient->sendRequest(
54 | $this->buildRequest($message, $options)
55 | );
56 | }
57 | catch( Throwable $exception ){
58 | throw new ConnectionException(
59 | message: "Failed to connect to webhook remote host.",
60 | previous: $exception
61 | );
62 | }
63 |
64 | if( $response->getStatusCode() >= ResponseStatus::BAD_REQUEST->value ){
65 | throw new PublishException(
66 | message: "Failed to publish message.",
67 | code: $response->getStatusCode()
68 | );
69 | }
70 |
71 | return $response->getBody()->getContents();
72 | }
73 |
74 | /**
75 | * Build the Request instance to send as webhook.
76 | *
77 | * @param Message $message
78 | * @param array $options
79 | * @return Request
80 | */
81 | protected function buildRequest(Message $message, array $options = []): Request
82 | {
83 | if( \preg_match("/^https?\:\/\//i", $message->getTopic()) ){
84 | $uri = $message->getTopic();
85 | }
86 | else {
87 | $uri = ($this->hostname ?? "") . $message->getTopic();
88 | }
89 |
90 | return new Request(
91 | method: $options["method"] ?? $this->method,
92 | uri: $uri,
93 | body: $message->getPayload(),
94 | headers: \array_merge(
95 | $this->headers,
96 | $message->getHeaders()
97 | )
98 | );
99 | }
100 | }
--------------------------------------------------------------------------------
/src/Exception/ConnectionException.php:
--------------------------------------------------------------------------------
1 | failedMessage;
30 | }
31 |
32 | /**
33 | * Get addtional context on the validation error that
34 | * includes a more specific error message, the offending
35 | * data, and full path to that data.
36 | *
37 | * @return array{message:string,path:string,data:mixed}
38 | */
39 | public function getContext(): array
40 | {
41 | return $this->context;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Exception/PublishException.php:
--------------------------------------------------------------------------------
1 | publisher->publish(
35 | message: new Message(
36 | topic: $this->topic,
37 | payload: $message->getPayload(),
38 | attributes: $message->getAttributes(),
39 | headers: $message->getHeaders()
40 | ),
41 | options: $options
42 | );
43 |
44 | return $receipt;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/Filter/ValidatorFilter.php:
--------------------------------------------------------------------------------
1 | validator->validate($message) === false ){
39 | throw new MessageValidationException(
40 | "Message failed validation.",
41 | $message
42 | );
43 | }
44 |
45 | return $this->publisher->publish($message, $options);
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Message.php:
--------------------------------------------------------------------------------
1 | $attributes A key/value pair of attributes to be sent with message. Most implementations do not support attributes.
16 | * @param array $headers A key/value pair of headers to be sent with message. Most implementations do not support headers.
17 | * @param mixed $reference A reference to the original source message. This is populated when pulling messages off source.
18 | * @param mixed $parsed_payload The parsed payload.
19 | */
20 | public function __construct(
21 | protected string $topic,
22 | protected string $payload,
23 | protected array $attributes = [],
24 | protected array $headers = [],
25 | protected mixed $reference = null,
26 | protected mixed $parsed_payload = null,
27 | )
28 | {
29 | }
30 |
31 | /**
32 | * The topic, queue name, or queue URL this Message is intended for or came from.
33 | *
34 | * @return string
35 | */
36 | public function getTopic(): string
37 | {
38 | return $this->topic;
39 | }
40 |
41 | /**
42 | * The raw payload/body of the message.
43 | *
44 | * @return string
45 | */
46 | public function getPayload(): string
47 | {
48 | return $this->payload;
49 | }
50 |
51 | /**
52 | * Message attributes.
53 | *
54 | * @return array
55 | */
56 | public function getAttributes(): array
57 | {
58 | return $this->attributes;
59 | }
60 |
61 | /**
62 | * Message headers.
63 | *
64 | * @return array
65 | */
66 | public function getHeaders(): array
67 | {
68 | return $this->headers;
69 | }
70 |
71 | /**
72 | * Get the reference to the original message.
73 | *
74 | * @return mixed Depending on the implementation, this could be a string, an array of values, or the full original message object.
75 | */
76 | public function getReference(): mixed
77 | {
78 | return $this->reference;
79 | }
80 |
81 | /**
82 | * Sets the parsed payload of the message.
83 | *
84 | * @param mixed $parsed_payload
85 | * @return void
86 | */
87 | public function setParsedPayload(mixed $parsed_payload): void
88 | {
89 | $this->parsed_payload = $parsed_payload;
90 | }
91 |
92 | /**
93 | * Get the parsed payload of the message.
94 | *
95 | * @return mixed
96 | */
97 | public function getParsedPayload(): mixed
98 | {
99 | return $this->parsed_payload;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Middleware/DeadletterMessage.php:
--------------------------------------------------------------------------------
1 | deadletter->publish($message);
34 | return Response::ack;
35 | }
36 |
37 | return $response;
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Middleware/MiddlewareInterface.php:
--------------------------------------------------------------------------------
1 | getPayload(), $this->associative);
32 |
33 | if( \json_last_error() !== JSON_ERROR_NONE ){
34 | return Response::deadletter;
35 | }
36 |
37 | $message->setParsedPayload($parsed_payload);
38 |
39 | return $next($message);
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Middleware/ValidateMessage.php:
--------------------------------------------------------------------------------
1 | validator->validate($message);
33 | }
34 | catch( MessageValidationException ){
35 | return Response::deadletter;
36 | }
37 |
38 | return $next($message);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Response.php:
--------------------------------------------------------------------------------
1 | $topic The topic or topics to match. You can provide multiple topics and are ORed.
12 | * @param array> $payload The JSON path statements to match. You can provide multiple JSON paths and values, matches are ANDed.
13 | * @param array> $attributes The attributes to match. If providing multiple values for an attribute, matches are ORed.
14 | * @param array> $headers The headers to match. If providing multiple values for a header, matches are ORed.
15 | */
16 | public function __construct(
17 | protected string|array $topic = [],
18 | protected array $payload = [],
19 | protected array $attributes = [],
20 | protected array $headers = [])
21 | {
22 | }
23 |
24 | /**
25 | * Get the topic(s) to match on the Message.
26 | *
27 | * @return string|array
28 | */
29 | public function getTopic(): string|array
30 | {
31 | return $this->topic;
32 | }
33 |
34 | /**
35 | * Get the payload JSON paths to match on the Message.
36 | *
37 | * @return array>
38 | */
39 | public function getPayload(): array
40 | {
41 | return $this->payload;
42 | }
43 |
44 | /**
45 | * Get the attributes to match on the Message.
46 | *
47 | * @return array>
48 | */
49 | public function getAttributes(): array
50 | {
51 | return $this->attributes;
52 | }
53 |
54 | /**
55 | * Get the headers to match on the Message.
56 | *
57 | * @return array>
58 | */
59 | public function getHeaders(): array
60 | {
61 | return $this->headers;
62 | }
63 | }
--------------------------------------------------------------------------------
/src/Router/Router.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | protected array $routes;
19 |
20 | /**
21 | * @param array