├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yaml ├── examples ├── consumeBatch.php ├── consumeBatchIterator.php ├── consumeIterator.php ├── explicitReturn.php ├── publishBatch.php ├── publishConfirm.php ├── rpc.php └── transactional.php ├── rector.php └── src ├── AmqpException.php ├── Channel.php ├── Client.php ├── Config.php ├── ConsumeBatch.php ├── DeliveryMessage.php ├── DeliveryMode.php ├── Exception ├── AuthenticationMechanismIsNotSupported.php ├── ChannelIsNotTransactional.php ├── ChannelModeIsImpossible.php ├── ChannelWasClosed.php ├── ConnectionIsClosed.php ├── ConnectionNotAvailable.php ├── ConnectionWasClosed.php ├── FrameIsBroken.php ├── MessageCannotBeRouted.php ├── NoAvailableChannel.php ├── NotImplemented.php ├── UnexpectedFrameReceived.php ├── UnknownValueType.php ├── UnsupportedClassMethod.php └── UriIsInvalid.php ├── Internal ├── AtomicGet.php ├── Batch │ ├── BatchConsumer.php │ └── ConsumeBatchOptions.php ├── Cancellation │ ├── CancellationStorage.php │ └── Canceller.php ├── ChannelMode.php ├── ConfirmationListener.php ├── Delivery │ ├── Consumer.php │ ├── ConsumerTagGenerator.php │ ├── DeliverySupervisor.php │ └── Receiver.php ├── Hooks.php ├── Io │ ├── AmqpConnection.php │ ├── AmqpConnectionFactory.php │ ├── Buffer.php │ ├── ChannelFactory.php │ ├── ReadBytes.php │ ├── WriteBytes.php │ └── WriterTo.php ├── MessageProperties.php ├── Properties.php ├── Protocol │ ├── Auth │ │ ├── AMQPlain.php │ │ ├── Mechanism.php │ │ └── Plain.php │ ├── Body.php │ ├── ClassMethod.php │ ├── ClassType.php │ ├── Frame.php │ ├── Frame │ │ ├── BasicAck.php │ │ ├── BasicCancel.php │ │ ├── BasicCancelOk.php │ │ ├── BasicConsume.php │ │ ├── BasicConsumeOk.php │ │ ├── BasicDeliver.php │ │ ├── BasicGet.php │ │ ├── BasicGetEmpty.php │ │ ├── BasicGetOk.php │ │ ├── BasicNack.php │ │ ├── BasicPublish.php │ │ ├── BasicQos.php │ │ ├── BasicQosOk.php │ │ ├── BasicRecover.php │ │ ├── BasicRecoverOk.php │ │ ├── BasicReject.php │ │ ├── BasicReturn.php │ │ ├── ChannelClose.php │ │ ├── ChannelCloseOk.php │ │ ├── ChannelFlow.php │ │ ├── ChannelFlowOk.php │ │ ├── ChannelOpen.php │ │ ├── ChannelOpenOkFrame.php │ │ ├── ConfirmSelect.php │ │ ├── ConfirmSelectOk.php │ │ ├── ConnectionClose.php │ │ ├── ConnectionCloseOk.php │ │ ├── ConnectionOpen.php │ │ ├── ConnectionOpenOk.php │ │ ├── ConnectionStart.php │ │ ├── ConnectionStartOk.php │ │ ├── ConnectionTune.php │ │ ├── ConnectionTuneOk.php │ │ ├── ContentBody.php │ │ ├── ContentHeader.php │ │ ├── ExchangeBind.php │ │ ├── ExchangeBindOk.php │ │ ├── ExchangeDeclare.php │ │ ├── ExchangeDeclareOk.php │ │ ├── ExchangeDelete.php │ │ ├── ExchangeDeleteOk.php │ │ ├── ExchangeUnbind.php │ │ ├── ExchangeUnbindOk.php │ │ ├── ProtocolHeader.php │ │ ├── QueueBind.php │ │ ├── QueueBindOk.php │ │ ├── QueueDeclare.php │ │ ├── QueueDeclareOk.php │ │ ├── QueueDelete.php │ │ ├── QueueDeleteOk.php │ │ ├── QueuePurge.php │ │ ├── QueuePurgeOk.php │ │ ├── QueueUnbind.php │ │ ├── QueueUnbindOk.php │ │ ├── TxCommit.php │ │ ├── TxCommitOk.php │ │ ├── TxRollback.php │ │ ├── TxRollbackOk.php │ │ ├── TxSelect.php │ │ └── TxSelectOk.php │ ├── FrameType.php │ ├── Header.php │ ├── Heartbeat.php │ ├── Method.php │ ├── Protocol.php │ ├── Reader.php │ ├── Request.php │ ├── Status.php │ └── Type.php ├── QueueIterator.php ├── Returns │ ├── FutureBoundedReturnListener.php │ ├── ReturnFuture.php │ └── ReturnListener.php ├── VersionProvider.php └── functions.php ├── Iterator.php ├── Message.php ├── PublishBatchConfirmation.php ├── PublishBatchConfirmationResult.php ├── PublishConfirmation.php ├── PublishMessage.php ├── PublishResult.php ├── Queue.php ├── Rpc.php ├── RpcConfig.php └── Scheme.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.0] 2025-05-06 9 | 10 | ## Changed 11 | 12 | - Improve title and description in README. 13 | - Reorder `Channel` properties. 14 | - Sync codebase with the latest Thesis template and update dev dependencies. 15 | - Apply Rector fixes. 16 | - Make `Client::$config` property public. 17 | 18 | ## Added 19 | 20 | - Experimental batch consume implemented. 21 | - Returns should be handled as callbacks and explicitly in confirmation mode enabled. 22 | - Improve `PublishBatchConfirmationResult::ok()`. 23 | - Use `TimeoutCancellation` instead of `EventLoop::delay` in `BatchConsumer`. 24 | 25 | ## [0.3.1] 2025-04-14 26 | 27 | ### Changed 28 | 29 | - Implicit connection on `Client::channel()`. 30 | - Allow concurrent and exclusive call to `DeliveryMessage::ack()`, `DeliveryMessage::nack()` or `DeliveryMessage::reject()`. 31 | 32 | ## [0.3.0] 2025-04-12 33 | 34 | ### Added 35 | 36 | - Support cluster config. 37 | - Added batch api for publish. 38 | 39 | ### Changed 40 | 41 | - Enable `tcp_nodelay` by default. 42 | - Add `Thesis\Amqp\Channel::isClosed()`. 43 | - Use PascalCase for `ChannelMode` cases. 44 | - Use `Thesis\Amqp\Message` in `Thesis\Amqp\Delivery`. 45 | - Rename `Thesis\Amqp\Confirmation` to `Thesis\Amqp\PublishConfirmation`. 46 | 47 | ## [0.2.0] 2025-03-23 48 | 49 | ### Changed 50 | 51 | - Use `DateTimeImmutable` instead of `DateTimeInterface` in all signatures. 52 | - Disconnect client in destructor if PHP >= 8.4. 53 | - Do not throw in `Client::disconnect()` if client is not connected. 54 | 55 | ### Fixed 56 | 57 | - Allow to use `Confirmation::awaitAll()` without iteration. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present Valentin Udaltsov 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thesis/amqp", 3 | "description": "Async (fiber based) client for AMQP 0.9.1", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Valentin Udaltsov", 9 | "email": "udaltsov.valentin@gmail.com" 10 | }, 11 | { 12 | "name": "kafkiansky", 13 | "email": "vadimzanfir@gmail.com" 14 | }, 15 | { 16 | "name": "Thesis Team", 17 | "homepage": "https://github.com/orgs/thesis-php/people" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.3", 22 | "ext-filter": "*", 23 | "amphp/amp": "^3.0", 24 | "amphp/byte-stream": "^2.1", 25 | "amphp/pipeline": "^1.2", 26 | "amphp/socket": "^2.3", 27 | "revolt/event-loop": "^1.0", 28 | "thesis/amp-bridge": "^0.1.0", 29 | "thesis/byte-buffer": "^0.1.0", 30 | "thesis/byte-order": "^0.2.0", 31 | "thesis/byte-reader": "^0.3.1", 32 | "thesis/byte-reader-writer": "^0.1.0", 33 | "thesis/byte-writer": "^0.2.1", 34 | "thesis/endian": "^0.1.0", 35 | "thesis/sync-once": "^0.1.1", 36 | "thesis/time-span": "^0.2.0" 37 | }, 38 | "require-dev": { 39 | "bamarni/composer-bin-plugin": "^1.8.2", 40 | "phpunit/phpunit": "^12.1.4", 41 | "symfony/var-dumper": "^6.4.15 || ^7.2.6" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Thesis\\Amqp\\": "src/" 46 | }, 47 | "files": [ 48 | "src/Internal/functions.php" 49 | ] 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "Thesis\\Amqp\\": "tests/" 54 | } 55 | }, 56 | "config": { 57 | "allow-plugins": { 58 | "bamarni/composer-bin-plugin": true 59 | }, 60 | "bump-after-update": "dev", 61 | "platform": { 62 | "php": "8.3.17" 63 | }, 64 | "sort-packages": true 65 | }, 66 | "extra": { 67 | "bamarni-bin": { 68 | "bin-links": false, 69 | "forward-command": true, 70 | "target-directory": "tools" 71 | } 72 | }, 73 | "scripts": { 74 | "analyse-deps": "tools/composer-dependency-analyser/vendor/bin/composer-dependency-analyser", 75 | "check": [ 76 | "@composer fixcs -- --dry-run", 77 | "@composer rector -- --dry-run", 78 | "@phpstan", 79 | "@composer validate", 80 | "@composer normalize --dry-run", 81 | "@analyse-deps", 82 | "@test" 83 | ], 84 | "fixcs": "tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff --verbose", 85 | "infection": "tools/infection/vendor/bin/infection --show-mutations", 86 | "normalize": "@composer bin composer-normalize normalize --diff ../../composer.json", 87 | "phpstan": "tools/phpstan/vendor/bin/phpstan analyze", 88 | "pre-command-run": "mkdir -p var", 89 | "psalm": "tools/psalm/vendor/bin/psalm --show-info --no-diff --no-cache", 90 | "rector": "tools/rector/vendor/bin/rector process", 91 | "test": "phpunit" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | services: 4 | rabbitmq: 5 | image: rabbitmq:4.0-management-alpine 6 | restart: always 7 | ports: 8 | - "127.0.0.1:5672:5672" 9 | - "127.0.0.1:15672:15672" 10 | -------------------------------------------------------------------------------- /examples/consumeBatch.php: -------------------------------------------------------------------------------- 1 | channel(); 19 | 20 | $queue = $channel->queueDeclare(autoDelete: true); 21 | 22 | $channel->confirmSelect(); 23 | 24 | $channel 25 | ->publishBatch(array_map( 26 | static fn(int $number): PublishMessage => new PublishMessage(new Message("{$number}"), routingKey: $queue->name), 27 | range(1, 8), 28 | )) 29 | ->await() 30 | ->ensureAllPublished(); 31 | 32 | $consumerTag = $channel->consumeBatch( 33 | static function (ConsumeBatch $batch): void { 34 | dump(array_map( 35 | static fn(DeliveryMessage $delivery): string => $delivery->message->body, 36 | $batch->deliveries, 37 | )); 38 | 39 | $batch->ack(); 40 | }, 41 | count: 5, 42 | timeout: 1, 43 | queue: $queue->name, 44 | ); 45 | 46 | /** @var Future $future */ 47 | $future = async(static function () use ($consumerTag, $channel): int { 48 | $signal = trapSignal([\SIGINT, \SIGTERM]); 49 | $channel->cancel($consumerTag); 50 | 51 | return $signal; 52 | }); 53 | 54 | dump("signal received: {$future->await()}"); 55 | 56 | $channel->close(); 57 | -------------------------------------------------------------------------------- /examples/consumeBatchIterator.php: -------------------------------------------------------------------------------- 1 | channel(); 18 | 19 | $queue = $channel->queueDeclare(autoDelete: true); 20 | 21 | $channel->confirmSelect(); 22 | 23 | $channel 24 | ->publishBatch(array_map( 25 | static fn(int $number): PublishMessage => new PublishMessage(new Message("{$number}"), routingKey: $queue->name), 26 | range(1, 8), 27 | )) 28 | ->await() 29 | ->ensureAllPublished(); 30 | 31 | $iterator = $channel->consumeBatchIterator(count: 5, timeout: 1, queue: $queue->name); 32 | 33 | /** @var Future $future */ 34 | $future = async(static function () use ($iterator): int { 35 | $signal = trapSignal([\SIGINT, \SIGTERM]); 36 | $iterator->complete(); 37 | 38 | return $signal; 39 | }); 40 | 41 | foreach ($iterator as $batch) { 42 | dump(array_map( 43 | static fn(DeliveryMessage $delivery): string => $delivery->message->body, 44 | $batch->deliveries, 45 | )); 46 | 47 | $batch->ack(); 48 | } 49 | 50 | dump("signal received: {$future->await()}"); 51 | 52 | $channel->close(); 53 | -------------------------------------------------------------------------------- /examples/consumeIterator.php: -------------------------------------------------------------------------------- 1 | channel(); 15 | 16 | $queue = $channel->queueDeclare(autoDelete: true); 17 | 18 | $messageId = 0; 19 | 20 | EventLoop::unreference( 21 | EventLoop::repeat(0.5, static function () use ($queue, $channel, &$messageId): void { 22 | ++$messageId; 23 | 24 | $channel->publish(new Message("Message#{$messageId}"), routingKey: $queue->name); 25 | }), 26 | ); 27 | 28 | $channel->qos(prefetchCount: 1); 29 | $deliveries = $channel->consumeIterator($queue->name, size: 1); 30 | 31 | Amp\async(static function () use ($deliveries): void { 32 | Amp\trapSignal([\SIGINT, \SIGTERM]); 33 | $deliveries->complete(); 34 | }); 35 | 36 | foreach ($deliveries as $delivery) { 37 | dump($delivery->message->body); 38 | $delivery->ack(); 39 | } 40 | 41 | dump('Consumer cancelled by signal.'); 42 | 43 | $client->disconnect(); 44 | -------------------------------------------------------------------------------- /examples/explicitReturn.php: -------------------------------------------------------------------------------- 1 | channel(); 14 | 15 | $channel->queueDelete('xxx'); 16 | 17 | $channel->confirmSelect(); 18 | 19 | $msg = new Message('abz'); 20 | 21 | $confirmation = $channel->publish($msg, routingKey: 'xxx', mandatory: true); 22 | $result = $confirmation?->await(); 23 | 24 | if ($result === PublishResult::Unrouted) { 25 | dump('Message cannot be routed. Creating queue explicitly...'); 26 | 27 | $channel->queueDeclare('xxx', durable: true); 28 | } 29 | 30 | $channel->publish($msg, routingKey: 'xxx', mandatory: true)?->await(); 31 | 32 | $msg = $channel->get('xxx'); 33 | dump("Now message '{$msg?->message->body}' was published."); 34 | 35 | $client->disconnect(); 36 | -------------------------------------------------------------------------------- /examples/publishBatch.php: -------------------------------------------------------------------------------- 1 | channel(); 14 | 15 | $queue = $channel->queueDeclare(autoDelete: true); 16 | 17 | $channel->publishBatch(array_map( 18 | static fn(int $number): PublishMessage => new PublishMessage(new Message("{$number}"), routingKey: $queue->name), 19 | range(1, 8), 20 | )); 21 | 22 | dump('Messages published successfully.'); 23 | -------------------------------------------------------------------------------- /examples/publishConfirm.php: -------------------------------------------------------------------------------- 1 | channel(); 14 | 15 | $queue = $channel->queueDeclare(autoDelete: true); 16 | 17 | $channel->confirmSelect(); 18 | 19 | $channel->publish(new Message('xxx'))?->await(); 20 | 21 | $channel 22 | ->publishBatch(array_map( 23 | static fn(int $number): PublishMessage => new PublishMessage(new Message("{$number}"), routingKey: $queue->name), 24 | range(1, 8), 25 | )) 26 | ->await() 27 | ->ensureAllPublished(); 28 | 29 | dump('Messages published successfully.'); 30 | -------------------------------------------------------------------------------- /examples/rpc.php: -------------------------------------------------------------------------------- 1 | channel(); 17 | $queue = $channel->queueDeclare(autoDelete: true); 18 | 19 | $channel->consume( 20 | callback: static function (DeliveryMessage $delivery): void { 21 | $delivery->reply(new Message("Request '{$delivery->message->body}' handled.")); 22 | }, 23 | queue: $queue->name, 24 | noAck: true, 25 | ); 26 | 27 | $rpc = new Rpc($client); 28 | 29 | for ($i = 0; $i < 100; ++$i) { 30 | dump($rpc->request(new Message("Request#{$i}"), routingKey: $queue->name, cancellation: new TimeoutCancellation(2))->body); 31 | } 32 | 33 | trapSignal([\SIGINT, \SIGTERM]); 34 | 35 | $client->disconnect(); 36 | -------------------------------------------------------------------------------- /examples/transactional.php: -------------------------------------------------------------------------------- 1 | channel(); 15 | 16 | $queue = $channel->queueDeclare(autoDelete: true); 17 | 18 | $channel->transactional(static function (Channel $channel) use ($queue): void { 19 | $channel->publish(new Message('1'), routingKey: $queue->name); 20 | $channel->publish(new Message('2'), routingKey: $queue->name); 21 | $channel->publish(new Message('3'), routingKey: $queue->name); 22 | }); 23 | 24 | try { 25 | $channel->transactional(static function (Channel $channel) use ($queue): void { 26 | $channel->publish(new Message('4'), routingKey: $queue->name); 27 | $channel->publish(new Message('5'), routingKey: $queue->name); 28 | 29 | throw new DomainException('Ops.'); 30 | }); 31 | } catch (Throwable $e) { 32 | dump($e->getMessage()); // Ops. 33 | } 34 | 35 | dump('Count of messages in queue: ' . $channel->queueDelete($queue->name)); 36 | 37 | $client->disconnect(); 38 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 11 | __DIR__ . '/examples', 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]) 15 | ->withParallel() 16 | ->withCache(__DIR__ . '/var/rector') 17 | ->withPhpSets() 18 | ->withSkip([ 19 | StringableForToStringRector::class, 20 | AddOverrideAttributeToOverriddenMethodsRector::class, 21 | ]); 22 | -------------------------------------------------------------------------------- /src/AmqpException.php: -------------------------------------------------------------------------------- 1 | */ 25 | private ?Sync\Once $connection = null; 26 | 27 | /** @var ?Sync\Once */ 28 | private ?Sync\Once $disconnection = null; 29 | 30 | private readonly Properties $properties; 31 | 32 | private readonly Hooks $hooks; 33 | 34 | public function __construct( 35 | public readonly Config $config, 36 | ) { 37 | $this->properties = Properties::createDefault(); 38 | $this->hooks = new Hooks(); 39 | 40 | $this->connectionFactory = new AmqpConnectionFactory( 41 | $this->config, 42 | $this->properties, 43 | $this->hooks, 44 | ); 45 | 46 | $this->channelFactory = new ChannelFactory( 47 | $this->properties, 48 | $this->hooks, 49 | ); 50 | } 51 | 52 | public function connect(?Cancellation $cancellation = null): void 53 | { 54 | $this->connection($cancellation); 55 | } 56 | 57 | /** 58 | * @param non-negative-int $replyCode 59 | */ 60 | public function disconnect( 61 | int $replyCode = 200, 62 | string $replyText = '', 63 | ?Cancellation $cancellation = null, 64 | ): void { 65 | $connection = $this->connection?->await($cancellation); 66 | 67 | if ($connection === null) { 68 | return; 69 | } 70 | 71 | $this->disconnection ??= new Sync\Once(function () use ($connection, $replyCode, $replyText, $cancellation): void { 72 | $this->channelFactory->close($replyCode, $replyText); 73 | $this->connectionFactory->close($connection, $replyCode, $replyText, $cancellation); 74 | $this->connection = null; 75 | }); 76 | 77 | try { 78 | $this->disconnection->await($cancellation); 79 | } finally { 80 | $this->disconnection = null; 81 | $this->hooks->complete(); 82 | } 83 | } 84 | 85 | public function channel(?Cancellation $cancellation = null): Channel 86 | { 87 | return $this->channelFactory->open( 88 | $this->connection($cancellation), 89 | $cancellation, 90 | ); 91 | } 92 | 93 | private function connection(?Cancellation $cancellation = null): AmqpConnection 94 | { 95 | return ($this->connection ??= new Sync\Once($this->connectionFactory->connect(...)))->await($cancellation); 96 | } 97 | 98 | public function __destruct() 99 | { 100 | if (\PHP_VERSION_ID >= 80400) { 101 | $this->disconnect(); 102 | } 103 | 104 | $this->hooks->complete(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | */ 28 | private array $sasl; 29 | 30 | /** 31 | * @param non-empty-list $urls 32 | * @param non-empty-string $vhost 33 | * @param list $authMechanisms 34 | * @param non-negative-int $heartbeat 35 | * @param float $connectionTimeout in seconds 36 | * @param int<0, 65535> $channelMax 37 | * @param int<0, 65535> $frameMax 38 | */ 39 | public function __construct( 40 | public Scheme $scheme = Scheme::amqp, 41 | public array $urls = [self::DEFAULT_URL], 42 | public string $user = self::DEFAULT_USERNAME, 43 | public string $password = self::DEFAULT_PASSWORD, 44 | public string $vhost = self::DEFAULT_VHOST, 45 | public ?string $certFile = null, 46 | public ?string $keyFile = null, 47 | public ?string $cacertFile = null, 48 | public ?string $serverName = null, 49 | public array $authMechanisms = [], 50 | public int $heartbeat = self::DEFAULT_HEARTBEAT_INTERVAL, 51 | public float $connectionTimeout = self::DEFAULT_CONNECTION_TIMEOUT, 52 | public int $channelMax = self::MAX_CHANNEL, 53 | public int $frameMax = self::MAX_FRAME, 54 | public bool $tcpNoDelay = true, 55 | ) { 56 | $authMechanisms = $this->authMechanisms; 57 | if (\count($authMechanisms) === 0) { 58 | $authMechanisms[] = Mechanism::PLAIN; 59 | } 60 | 61 | $this->sasl = array_map(fn(string $mechanism): Mechanism => Mechanism::create($mechanism, $this->user, $this->password), $authMechanisms); 62 | } 63 | 64 | public static function default(): self 65 | { 66 | return new self(); 67 | } 68 | 69 | /** 70 | * @see https://www.rabbitmq.com/docs/uri-spec 71 | * 72 | * @param non-empty-string $uri 73 | * @throws UriIsInvalid 74 | */ 75 | public static function fromURI(string $uri): self 76 | { 77 | $components = parse_url($uri); 78 | 79 | if ($components === false) { 80 | throw new UriIsInvalid(); 81 | } 82 | 83 | $query = []; 84 | if (isset($components['query']) && $components['query'] !== '') { 85 | $query = Internal\parseQuery($components['query']); 86 | } 87 | 88 | $certFile = null; 89 | if (isset($query['certfile'])) { 90 | $certFile = \is_array($query['certfile']) ? $query['certfile'][0] : $query['certfile']; 91 | } 92 | 93 | $keyFile = null; 94 | if (isset($query['keyfile'])) { 95 | $keyFile = \is_array($query['keyfile']) ? $query['keyfile'][0] : $query['keyfile']; 96 | } 97 | 98 | $cacertfile = null; 99 | if (isset($query['cacertfile'])) { 100 | $cacertfile = \is_array($query['cacertfile']) ? $query['cacertfile'][0] : $query['cacertfile']; 101 | } 102 | 103 | $serverName = null; 104 | if (isset($query['server_name_indication'])) { 105 | $serverName = \is_array($query['server_name_indication']) ? $query['server_name_indication'][0] : $query['server_name_indication']; 106 | } 107 | 108 | $authMechanisms = []; 109 | if (isset($query['auth_mechanism'])) { 110 | $authMechanisms = \is_string($query['auth_mechanism']) ? [$query['auth_mechanism']] : $query['auth_mechanism']; 111 | } 112 | 113 | $heartbeat = self::DEFAULT_HEARTBEAT_INTERVAL; 114 | if (isset($query['heartbeat']) && is_numeric($query['heartbeat']) && (int) $query['heartbeat'] >= 0) { 115 | /** @var non-negative-int $heartbeat */ 116 | $heartbeat = (int) $query['heartbeat']; 117 | } 118 | 119 | $connectionTimeout = self::DEFAULT_CONNECTION_TIMEOUT; 120 | if (isset($query['connection_timeout']) && is_numeric($query['connection_timeout']) && (int) $query['connection_timeout'] > 0) { 121 | /** @var positive-int $connectionTimeout */ 122 | $connectionTimeout = (int) $query['connection_timeout']; 123 | } 124 | 125 | $channelMax = self::MAX_CHANNEL; 126 | if (isset($query['channel_max']) && is_numeric($query['channel_max']) && (int) $query['channel_max'] > 0) { 127 | /** @var int<0, 65535> $channelMax */ 128 | $channelMax = min($channelMax, (int) $query['channel_max']); 129 | } 130 | 131 | $frameMax = self::MAX_FRAME; 132 | if (isset($query['frame_max']) && is_numeric($query['frame_max']) && (int) $query['frame_max'] > 0) { 133 | /** @var int<0, 65535> $frameMax */ 134 | $frameMax = min($frameMax, (int) $query['frame_max']); 135 | } 136 | 137 | $tcpNoDelay = true; 138 | if (isset($query['tcp_nodelay'])) { 139 | $tcpNoDelay = filter_var($query['tcp_nodelay'], FILTER_VALIDATE_BOOL); 140 | } 141 | 142 | $port = self::DEFAULT_PORT; 143 | if (isset($components['port']) && $components['port'] > 0) { 144 | $port = $components['port']; 145 | } 146 | 147 | $urls = []; 148 | foreach (explode(',', $components['host'] ?? '') as $host) { 149 | $hostport = explode(':', $host); 150 | $urls[] = \sprintf('%s:%d', $hostport[0] ?: self::DEFAULT_HOST, (int) ($hostport[1] ?? $port)); 151 | } 152 | 153 | $vhost = self::DEFAULT_VHOST; 154 | if (isset($components['path']) && $components['path'] !== '') { 155 | $vhost = $components['path']; 156 | } 157 | 158 | $user = self::DEFAULT_USERNAME; 159 | if (isset($components['user']) && $components['user'] !== '') { 160 | $user = $components['user']; 161 | } 162 | 163 | $password = self::DEFAULT_PASSWORD; 164 | if (isset($components['pass']) && $components['pass'] !== '') { 165 | $password = $components['pass']; 166 | } 167 | 168 | return new self( 169 | scheme: Scheme::tryFrom($components['scheme'] ?? Scheme::amqp->value) ?: throw UriIsInvalid::invalidScheme($components['scheme'] ?? ''), 170 | urls: $urls, 171 | user: $user, 172 | password: $password, 173 | vhost: $vhost, 174 | certFile: $certFile, 175 | keyFile: $keyFile, 176 | cacertFile: $cacertfile, 177 | serverName: $serverName, 178 | authMechanisms: $authMechanisms, 179 | heartbeat: $heartbeat, 180 | connectionTimeout: $connectionTimeout, 181 | channelMax: $channelMax, 182 | frameMax: $frameMax, 183 | tcpNoDelay: $tcpNoDelay, 184 | ); 185 | } 186 | 187 | /** 188 | * @param array{ 189 | * scheme?: AmqpScheme, 190 | * urls?: non-empty-list, 191 | * user?: non-empty-string, 192 | * password?: non-empty-string, 193 | * vhost?: non-empty-string, 194 | * certfile?: non-empty-string, 195 | * keyfile?: non-empty-string, 196 | * cacertfile?: non-empty-string, 197 | * server_name?: ?non-empty-string, 198 | * auth_mechanisms?: list, 199 | * heartbeat?: non-negative-int, 200 | * connection_timeout?: positive-int, 201 | * channel_max?: int<0, 65535>, 202 | * frame_max?: int<0, 65535>, 203 | * tcp_nodelay?: bool, 204 | * } $options 205 | */ 206 | public static function fromArray(array $options): self 207 | { 208 | return new self( 209 | scheme: isset($options['scheme']) ? Scheme::parse($options['scheme']) : Scheme::amqp, 210 | urls: $options['urls'] ?? [self::DEFAULT_URL], 211 | user: $options['user'] ?? self::DEFAULT_USERNAME, 212 | password: $options['password'] ?? self::DEFAULT_PASSWORD, 213 | vhost: $options['vhost'] ?? self::DEFAULT_VHOST, 214 | certFile: $options['certfile'] ?? null, 215 | keyFile: $options['keyfile'] ?? null, 216 | cacertFile: $options['cacertfile'] ?? null, 217 | serverName: $options['server_name'] ?? null, 218 | authMechanisms: $options['auth_mechanisms'] ?? [], 219 | heartbeat: $options['heartbeat'] ?? self::DEFAULT_HEARTBEAT_INTERVAL, 220 | connectionTimeout: $options['connection_timeout'] ?? self::DEFAULT_CONNECTION_TIMEOUT, 221 | channelMax: $options['channel_max'] ?? self::MAX_CHANNEL, 222 | frameMax: $options['frame_max'] ?? self::MAX_FRAME, 223 | tcpNoDelay: $options['tcp_nodelay'] ?? true, 224 | ); 225 | } 226 | 227 | /** 228 | * @return non-negative-int 229 | */ 230 | public function heartbeat(int $suggestHeartbeat): int 231 | { 232 | $heartbeat = min($this->heartbeat, $suggestHeartbeat); 233 | \assert($heartbeat >= 0, 'heartbeat must not be negative.'); 234 | 235 | return $heartbeat; 236 | } 237 | 238 | /** 239 | * @return non-negative-int 240 | */ 241 | public function channelMax(int $suggestChannelMax): int 242 | { 243 | $channelMax = min($this->channelMax, $suggestChannelMax); 244 | \assert($channelMax >= 0, 'channel max must not be negative.'); 245 | 246 | return $channelMax; 247 | } 248 | 249 | /** 250 | * @return positive-int 251 | */ 252 | public function frameMax(int $suggestFrameMax): int 253 | { 254 | $frameMax = min($this->frameMax, $suggestFrameMax); 255 | \assert($frameMax > 0, 'frame max must be positive.'); 256 | 257 | return $frameMax; 258 | } 259 | 260 | /** 261 | * @return iterable 262 | */ 263 | public function connectionUrls(): iterable 264 | { 265 | foreach ($this->urls as $url) { 266 | if (!str_starts_with($url, 'tcp://')) { 267 | $url = "tcp://{$url}"; 268 | } 269 | 270 | yield $url; 271 | } 272 | } 273 | 274 | /** 275 | * @return non-empty-list 276 | */ 277 | public function sasl(): array 278 | { 279 | return $this->sasl; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/ConsumeBatch.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ConsumeBatch implements 13 | \IteratorAggregate, 14 | \Countable 15 | { 16 | private bool $processed = false; 17 | 18 | /** 19 | * @param non-empty-list $deliveries 20 | */ 21 | public function __construct( 22 | public readonly array $deliveries, 23 | ) {} 24 | 25 | public function ack(): void 26 | { 27 | if (!$this->processed) { 28 | $this->watermark()->ack(multiple: true); 29 | $this->processed = true; 30 | } 31 | } 32 | 33 | public function nack(bool $requeue = true): void 34 | { 35 | if (!$this->processed) { 36 | $this->watermark()->nack(multiple: true, requeue: $requeue); 37 | $this->processed = true; 38 | } 39 | } 40 | 41 | public function getIterator(): \Traversable 42 | { 43 | yield from $this->deliveries; 44 | } 45 | 46 | public function count(): int 47 | { 48 | return \count($this->deliveries); 49 | } 50 | 51 | private function watermark(): DeliveryMessage 52 | { 53 | return $this->deliveries[\count($this->deliveries) - 1]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DeliveryMessage.php: -------------------------------------------------------------------------------- 1 | */ 20 | private ?Sync\Once $processed = null; 21 | 22 | /** 23 | * @param Ack $ack 24 | * @param Nack $nack 25 | * @param Reject $reject 26 | * @param Reply $reply 27 | * @param non-negative-int $deliveryTag 28 | */ 29 | public function __construct( 30 | private readonly \Closure $ack, 31 | private readonly \Closure $nack, 32 | private readonly \Closure $reject, 33 | private readonly \Closure $reply, 34 | public readonly Message $message, 35 | public readonly string $exchange = '', 36 | public readonly string $routingKey = '', 37 | public readonly int $deliveryTag = 0, 38 | public readonly string $consumerTag = '', 39 | public readonly bool $redelivered = false, 40 | public readonly bool $returned = false, 41 | ) {} 42 | 43 | public function ack(bool $multiple = false, ?Cancellation $cancellation = null): void 44 | { 45 | $this->process(fn() => ($this->ack)($this, $multiple), $cancellation); 46 | } 47 | 48 | public function nack(bool $multiple = false, bool $requeue = true, ?Cancellation $cancellation = null): void 49 | { 50 | $this->process(fn() => ($this->nack)($this, $multiple, $requeue), $cancellation); 51 | } 52 | 53 | public function reject(bool $requeue = true, ?Cancellation $cancellation = null): void 54 | { 55 | $this->process(fn() => ($this->reject)($this, $requeue), $cancellation); 56 | } 57 | 58 | public function reply(Message $message, ?Cancellation $cancellation = null): void 59 | { 60 | $this->process(fn() => ($this->reply)($message), $cancellation); 61 | } 62 | 63 | /** 64 | * @param \Closure(): void $hook 65 | */ 66 | private function process(\Closure $hook, ?Cancellation $cancellation = null): void 67 | { 68 | ($this->processed ??= new Sync\Once($hook))->await($cancellation); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/DeliveryMode.php: -------------------------------------------------------------------------------- 1 | $mechanisms 16 | */ 17 | public static function forServerMechanisms(array $mechanisms): self 18 | { 19 | return new self(\sprintf('Authentication server mechanisms "%s" is not supported.', implode(', ', $mechanisms))); 20 | } 21 | 22 | public static function forClientMechanism(string $mechanism): self 23 | { 24 | return new self(\sprintf('Authentication client mechanism "%s" is not supported.', $mechanism)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/ChannelIsNotTransactional.php: -------------------------------------------------------------------------------- 1 | replyText}.", $this->replyCode, $previous); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/ConnectionIsClosed.php: -------------------------------------------------------------------------------- 1 | */ 20 | private ?Future $future = null; 21 | 22 | /** 23 | * @param non-negative-int $channelId 24 | */ 25 | public function __construct( 26 | private readonly Receiver $receiver, 27 | private readonly AmqpConnection $connection, 28 | private readonly int $channelId, 29 | ) {} 30 | 31 | public function receive(string $queue = '', bool $noAck = false, ?Cancellation $cancellation = null): ?DeliveryMessage 32 | { 33 | while ($this->future !== null) { 34 | $this->future->await($cancellation); 35 | } 36 | 37 | try { 38 | return ($this->future = async(function () use ($queue, $noAck): ?DeliveryMessage { 39 | $this->connection->writeFrame(Protocol\Method::basicGet( 40 | channelId: $this->channelId, 41 | queue: $queue, 42 | noAck: $noAck, 43 | )); 44 | 45 | return $this->receiver->receive(); 46 | }))->await($cancellation); 47 | } finally { 48 | $this->future = null; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Internal/Batch/BatchConsumer.php: -------------------------------------------------------------------------------- 1 | $iterator 29 | * @param callable(ConsumeBatch, Channel): void $callback 30 | * @throws \Throwable 31 | */ 32 | public function consume( 33 | Iterator $iterator, 34 | callable $callback, 35 | ): void { 36 | while (!$this->cancellation->isRequested()) { 37 | $deliveries = $this->awaitDeliveries($iterator, $this->cancellation); 38 | 39 | if (\count($deliveries) > 0) { 40 | $callback(new ConsumeBatch($deliveries), $this->channel); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * @param Iterator $iterator 47 | * @return list 48 | */ 49 | private function awaitDeliveries(Iterator $iterator, Cancellation $consumerCancellation): array 50 | { 51 | $cancellations = [$consumerCancellation]; 52 | 53 | if ($this->options->timeout !== null) { 54 | $cancellations[] = new TimeoutCancellation($this->options->timeout); 55 | } 56 | 57 | $deliveryCancellation = new CompositeCancellation(...$cancellations); 58 | 59 | /** @var list $deliveries */ 60 | $deliveries = []; 61 | 62 | try { 63 | while ($iterator->continue($deliveryCancellation)) { 64 | $deliveries[] = $iterator->value(); 65 | if (\count($deliveries) === $this->options->count) { 66 | break; 67 | } 68 | } 69 | } catch (CancelledException) { 70 | } 71 | 72 | return $deliveries; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Internal/Batch/ConsumeBatchOptions.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $cancellers = []; 14 | 15 | /** 16 | * @param non-empty-string $consumerTag 17 | */ 18 | public function add(string $consumerTag, Canceller $canceller): void 19 | { 20 | $this->cancellers[$consumerTag] = $canceller; 21 | } 22 | 23 | /** 24 | * @param non-empty-string $consumerTag 25 | */ 26 | public function cancel(string $consumerTag, bool $noWait = false, ?\Throwable $error = null): void 27 | { 28 | if (isset($this->cancellers[$consumerTag])) { 29 | $canceller = $this->cancellers[$consumerTag]; 30 | unset($this->cancellers[$consumerTag]); 31 | 32 | $canceller->cancel($noWait, $error); 33 | } 34 | } 35 | 36 | public function cancelAll(bool $noWait = false, ?\Throwable $error = null): void 37 | { 38 | try { 39 | foreach ($this->cancellers as $canceller) { 40 | $canceller->cancel($noWait, $error); 41 | } 42 | } finally { 43 | $this->cancellers = []; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Internal/Cancellation/Canceller.php: -------------------------------------------------------------------------------- 1 | deferred = new DeferredCancellation(); 28 | } 29 | 30 | public function cancel(bool $noWait = false, ?\Throwable $e = null): void 31 | { 32 | if ($this->cancelled) { 33 | return; 34 | } 35 | 36 | try { 37 | if ($e !== null) { 38 | ($this->cancel)($e, $noWait); 39 | } else { 40 | ($this->complete)($noWait); 41 | } 42 | 43 | $this->deferred->cancel(); 44 | } finally { 45 | $this->cancelled = true; 46 | } 47 | } 48 | 49 | public function cancellation(): Cancellation 50 | { 51 | return $this->deferred->getCancellation(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Internal/ChannelMode.php: -------------------------------------------------------------------------------- 1 | > */ 26 | private array $confirms = []; 27 | 28 | /** 29 | * @param non-negative-int $channelId 30 | */ 31 | public function __construct( 32 | private readonly Hooks $hooks, 33 | private readonly int $channelId, 34 | ) {} 35 | 36 | public function listen(): void 37 | { 38 | [$this->deliveryTag, $this->confirmed, $this->confirms] = [0, 0, []]; 39 | 40 | $this->hooks->subscribe($this->channelId, BasicAck::class, $this->confirm(PublishResult::Acked)); 41 | $this->hooks->subscribe($this->channelId, BasicNack::class, $this->confirm(PublishResult::Nacked)); 42 | } 43 | 44 | public function newConfirmation(?ReturnFuture $returnFuture = null): PublishConfirmation 45 | { 46 | $deliveryTag = ++$this->deliveryTag; 47 | 48 | /** @var DeferredFuture $deferred */ 49 | $deferred = new DeferredFuture(); 50 | $this->confirms[$deliveryTag] = $deferred; 51 | 52 | return new PublishConfirmation($deliveryTag, $deferred->getFuture(), function () use ($deliveryTag, $deferred): void { 53 | unset($this->confirms[$deliveryTag]); 54 | $deferred->complete(PublishResult::Canceled); 55 | }, $returnFuture); 56 | } 57 | 58 | public function count(): int 59 | { 60 | return \count($this->confirms); 61 | } 62 | 63 | /** 64 | * @return callable(BasicAck|BasicNack): void 65 | */ 66 | private function confirm(PublishResult $result): callable 67 | { 68 | return function (BasicAck|BasicNack $frame) use ($result): void { 69 | if ($frame->multiple) { 70 | for ($i = $this->confirmed + 1; $i < $frame->deliveryTag; ++$i) { 71 | $this->complete($i, $result); 72 | } 73 | } 74 | 75 | $this->complete($frame->deliveryTag, $result); 76 | }; 77 | } 78 | 79 | /** 80 | * @param non-negative-int $deliveryTag 81 | */ 82 | private function complete(int $deliveryTag, PublishResult $result): void 83 | { 84 | $confirmation = $this->confirms[$deliveryTag] ?? null; 85 | if ($confirmation !== null) { 86 | $confirmation->complete($result); 87 | unset($this->confirms[$deliveryTag]); 88 | 89 | $this->confirmed = $deliveryTag; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Internal/Delivery/Consumer.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $consumers = []; 18 | 19 | public function __construct(DeliverySupervisor $supervisor) 20 | { 21 | $consumers = &$this->consumers; 22 | 23 | $supervisor->addConsumeListener(static function (DeliveryMessage $delivery, Channel $channel) use (&$consumers): void { 24 | $consumer = $consumers[$delivery->consumerTag] ?? null; 25 | if ($consumer !== null) { 26 | $consumer($delivery, $channel); 27 | } 28 | }); 29 | 30 | $supervisor->addShutdownListener(static function () use (&$consumers): void { 31 | $consumers = []; 32 | }); 33 | } 34 | 35 | /** 36 | * @param non-empty-string $consumerTag 37 | * @param Listener $consumer 38 | */ 39 | public function register(string $consumerTag, callable $consumer): void 40 | { 41 | $this->consumers[$consumerTag] = $consumer; 42 | } 43 | 44 | /** 45 | * @param non-empty-string $consumerTag 46 | */ 47 | public function unregister(string $consumerTag): void 48 | { 49 | unset($this->consumers[$consumerTag]); 50 | } 51 | 52 | /** 53 | * @param callable(non-empty-string): void $cancel 54 | */ 55 | public function cancelAll(callable $cancel): void 56 | { 57 | foreach (array_keys($this->consumers) as $consumerTag) { 58 | $cancel($consumerTag); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Internal/Delivery/ConsumerTagGenerator.php: -------------------------------------------------------------------------------- 1 | self::PACKAGE_NAME]; 25 | 26 | $this->commandName = ($cli[0] ?? self::PACKAGE_NAME) ?: self::PACKAGE_NAME; 27 | } 28 | 29 | /** 30 | * @return non-empty-string 31 | */ 32 | public function next(): string 33 | { 34 | $prefix = 'ctag-'; 35 | $infix = $this->commandName; 36 | $suffix = \sprintf('-%d', ++$this->consumerId); 37 | 38 | if (\strlen($prefix) + \strlen($infix) + \strlen($suffix) > self::TAG_LENGTH_MAX) { 39 | $infix = self::PACKAGE_NAME; 40 | } 41 | 42 | return "{$prefix}{$infix}{$suffix}"; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function select(string $consumerTag): string 49 | { 50 | if ($consumerTag === '') { 51 | $consumerTag = $this->next(); 52 | } 53 | 54 | return $consumerTag; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Internal/Delivery/DeliverySupervisor.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | private readonly \WeakReference $weakChannel; 30 | 31 | /** @var self::* */ 32 | private int $step = self::WAIT; 33 | 34 | private ?Frame\BasicDeliver $delivery = null; 35 | 36 | private ?Frame\BasicGetOk $get = null; 37 | 38 | private ?Frame\BasicReturn $return = null; 39 | 40 | private ?Frame\ContentHeader $header = null; 41 | 42 | /** @var non-negative-int */ 43 | private int $messageSize = 0; 44 | 45 | private string $message = ''; 46 | 47 | /** @var list */ 48 | private array $consumeListeners = []; 49 | 50 | /** @var list */ 51 | private array $returnListeners = []; 52 | 53 | /** @var list */ 54 | private array $getListeners = []; 55 | 56 | /** @var list */ 57 | private array $shutdownListeners = []; 58 | 59 | /** 60 | * @param non-negative-int $channelId 61 | */ 62 | public function __construct( 63 | Channel $channel, 64 | private readonly Hooks $hooks, 65 | private readonly int $channelId, 66 | ) { 67 | $this->weakChannel = \WeakReference::create($channel); 68 | } 69 | 70 | public function run(): void 71 | { 72 | $this->subscribe(Frame\BasicGetEmpty::class, $this->onBasicGetEmpty(...)); 73 | $this->subscribe(Frame\BasicGetOk::class, $this->onBasicGetOk(...)); 74 | $this->subscribe(Frame\BasicDeliver::class, $this->onBasicDeliver(...)); 75 | $this->subscribe(Frame\BasicReturn::class, $this->onBasicReturn(...)); 76 | $this->subscribe(Frame\ContentHeader::class, $this->onContentHeader(...)); 77 | $this->subscribe(Frame\ContentBody::class, $this->onContentBody(...)); 78 | } 79 | 80 | /** 81 | * @param ConsumeListener $listener 82 | */ 83 | public function addConsumeListener(callable $listener): void 84 | { 85 | $this->consumeListeners[] = $listener; 86 | } 87 | 88 | /** 89 | * @param ReturnListener $listener 90 | */ 91 | public function addReturnListener(callable $listener): void 92 | { 93 | $this->returnListeners[] = $listener; 94 | } 95 | 96 | /** 97 | * @param GetListener $listener 98 | */ 99 | public function addGetListener(callable $listener): void 100 | { 101 | $this->getListeners[] = $listener; 102 | } 103 | 104 | /** 105 | * @param callable(): void $listener 106 | */ 107 | public function addShutdownListener(callable $listener): void 108 | { 109 | $this->shutdownListeners[] = $listener; 110 | } 111 | 112 | public function stop(): void 113 | { 114 | foreach ($this->shutdownListeners as $shutdownListener) { 115 | $shutdownListener(); 116 | } 117 | 118 | [ 119 | $this->consumeListeners, 120 | $this->returnListeners, 121 | $this->getListeners, 122 | $this->shutdownListeners, 123 | ] = [[], [], [], []]; 124 | } 125 | 126 | private function onBasicGetEmpty(): void 127 | { 128 | foreach ($this->getListeners as $listener) { 129 | $listener(null, $this->channel()); 130 | } 131 | } 132 | 133 | private function onBasicGetOk(Frame\BasicGetOk $get): void 134 | { 135 | if ($this->step === self::WAIT) { 136 | [$this->get, $this->step] = [$get, self::HEADER]; 137 | } 138 | } 139 | 140 | private function onBasicDeliver(Frame\BasicDeliver $delivery): void 141 | { 142 | if ($this->step === self::WAIT) { 143 | [$this->delivery, $this->step] = [$delivery, self::HEADER]; 144 | } 145 | } 146 | 147 | private function onBasicReturn(Frame\BasicReturn $return): void 148 | { 149 | if ($this->step === self::WAIT) { 150 | [$this->return, $this->step] = [$return, self::HEADER]; 151 | } 152 | } 153 | 154 | private function onContentHeader(Frame\ContentHeader $header): void 155 | { 156 | if ($this->step === self::HEADER) { 157 | $this->header = $header; 158 | $this->step = self::BODY; 159 | $this->messageSize = $this->header->bodySize; 160 | 161 | $this->runListeners(); 162 | } 163 | } 164 | 165 | private function onContentBody(Frame\ContentBody $body): void 166 | { 167 | if ($this->step === self::BODY) { 168 | $this->message .= $body->body; 169 | $this->messageSize = max($this->messageSize - \strlen($body->body), 0); 170 | 171 | $this->runListeners(); 172 | } 173 | } 174 | 175 | private function runListeners(): void 176 | { 177 | if ($this->messageSize !== 0) { 178 | return; 179 | } 180 | 181 | \assert($this->delivery !== null || $this->return !== null || $this->get !== null, 'delivery, return or get must not be empty.'); 182 | \assert($this->header !== null, 'header must not be empty.'); 183 | 184 | // We cannot call ack/nack/reject on a returned message. 185 | $noAction = static function (): void {}; 186 | 187 | $channel = $this->channel(); 188 | 189 | $delivery = new DeliveryMessage( 190 | ack: $this->return !== null ? $noAction : $channel->ack(...), 191 | nack: $this->return !== null ? $noAction : $channel->nack(...), 192 | reject: $this->return !== null ? $noAction : $channel->reject(...), 193 | reply: $this->replier($this->header->properties, $channel) ?: $noAction, 194 | message: new Message( 195 | body: $this->message, 196 | headers: $this->header->properties->headers, 197 | contentType: $this->header->properties->contentType, 198 | contentEncoding: $this->header->properties->contentEncoding, 199 | deliveryMode: $this->header->properties->deliveryMode, 200 | priority: $this->header->properties->priority, 201 | correlationId: $this->header->properties->correlationId, 202 | replyTo: $this->header->properties->replyTo, 203 | expiration: $this->header->properties->expiration, 204 | messageId: $this->header->properties->messageId, 205 | timestamp: $this->header->properties->timestamp, 206 | type: $this->header->properties->type, 207 | userId: $this->header->properties->userId, 208 | appId: $this->header->properties->appId, 209 | ), 210 | exchange: $this->delivery->exchange ?? $this->get->exchange ?? $this->return->exchange ?? '', 211 | routingKey: $this->delivery->routingKey ?? $this->get->routingKey ?? $this->return->routingKey ?? '', 212 | deliveryTag: $this->delivery->deliveryTag ?? $this->get->deliveryTag ?? 0, 213 | consumerTag: $this->delivery->consumerTag ?? '', 214 | redelivered: $this->delivery->redelivered ?? $this->get->redelivered ?? false, 215 | returned: $this->return !== null, 216 | ); 217 | 218 | $listeners = match (true) { 219 | $this->delivery !== null => $this->consumeListeners, 220 | $this->get !== null => $this->getListeners, 221 | $this->return !== null => $this->returnListeners, 222 | default => [], 223 | }; 224 | 225 | foreach ($listeners as $listener) { 226 | $listener($delivery, $channel); 227 | } 228 | 229 | $this->get = null; 230 | $this->delivery = null; 231 | $this->return = null; 232 | $this->header = null; 233 | $this->message = ''; 234 | $this->step = self::WAIT; 235 | } 236 | 237 | /** 238 | * @template T of Frame 239 | * @param class-string $frameType 240 | * @param \Closure(T): void $callback 241 | */ 242 | private function subscribe(string $frameType, \Closure $callback): void 243 | { 244 | $this->hooks->subscribe($this->channelId, $frameType, $callback); 245 | } 246 | 247 | /** 248 | * @return ?\Closure(Message): void 249 | */ 250 | private function replier(MessageProperties $properties, Channel $channel): ?\Closure 251 | { 252 | $replyTo = $properties->replyTo ?? null; 253 | if ($replyTo === null) { 254 | return null; 255 | } 256 | 257 | $correlationId = $properties->correlationId ?? null; 258 | if ($correlationId === null) { 259 | return null; 260 | } 261 | 262 | return static function (Message $message) use ($channel, $replyTo, $correlationId): void { 263 | $channel->publish( 264 | message: new Message( 265 | body: $message->body, 266 | headers: $message->headers, 267 | contentType: $message->contentType, 268 | contentEncoding: $message->contentEncoding, 269 | deliveryMode: $message->deliveryMode, 270 | priority: $message->priority, 271 | correlationId: $correlationId, 272 | replyTo: $replyTo, 273 | expiration: $message->expiration, 274 | messageId: $message->messageId, 275 | timestamp: $message->timestamp, 276 | type: $message->type, 277 | userId: $message->userId, 278 | appId: $message->appId, 279 | ), 280 | routingKey: $replyTo, 281 | ); 282 | }; 283 | } 284 | 285 | private function channel(): Channel 286 | { 287 | return $this->weakChannel->get() ?? throw new \LogicException('Channel has been garbage collected.'); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/Internal/Delivery/Receiver.php: -------------------------------------------------------------------------------- 1 | */ 16 | private Pipeline\ConcurrentIterator $iterator; 17 | 18 | public function __construct(DeliverySupervisor $supervisor) 19 | { 20 | /** @var Pipeline\Queue $queue */ 21 | $queue = new Pipeline\Queue(bufferSize: 1); 22 | $this->iterator = $queue->iterate(); 23 | 24 | $supervisor->addGetListener($queue->push(...)); 25 | $supervisor->addShutdownListener($queue->complete(...)); 26 | } 27 | 28 | public function receive(): ?DeliveryMessage 29 | { 30 | return $this->iterator->continue() 31 | ? $this->iterator->getValue() 32 | : null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Internal/Hooks.php: -------------------------------------------------------------------------------- 1 | >> 17 | */ 18 | private array $defers = []; 19 | 20 | /** 21 | * @var array>> 22 | */ 23 | private array $queue = []; 24 | 25 | /** 26 | * @template T of Protocol\Frame 27 | * @param non-negative-int $channelId 28 | * @param non-empty-list>|class-string $frameTypes 29 | * @param callable(T): void $subscriber 30 | */ 31 | public function anyOf(int $channelId, array|string $frameTypes, callable $subscriber): void 32 | { 33 | if (!\is_array($frameTypes)) { 34 | $frameTypes = [$frameTypes]; 35 | } 36 | 37 | foreach ($frameTypes as $frameType) { 38 | $idx = \count($this->defers[$channelId][$frameType] ?? []); 39 | 40 | $this->defers[$channelId][$frameType][] 41 | = function (Protocol\Frame $frame) use ($channelId, $frameType, $idx, $subscriber): void { 42 | /** @var T $frame */ 43 | $subscriber($frame); 44 | unset($this->defers[$channelId][$frameType][$idx]); 45 | }; 46 | } 47 | } 48 | 49 | /** 50 | * @template T of Protocol\Frame 51 | * @param non-negative-int $channelId 52 | * @param class-string $frameType 53 | * @return Future 54 | */ 55 | public function oneshot(int $channelId, string $frameType): Future 56 | { 57 | /** @var DeferredFuture $deferred */ 58 | $deferred = new DeferredFuture(); 59 | 60 | $idx = \count($this->defers[$channelId][$frameType] ?? []); 61 | $this->defers[$channelId][$frameType][] 62 | = function (Protocol\Frame $frame) use ($deferred, $channelId, $frameType, $idx): void { 63 | if (!$deferred->isComplete()) { 64 | $deferred->complete($frame); 65 | } 66 | unset( 67 | $this->defers[$channelId][$frameType][$idx], 68 | $this->queue[$channelId][spl_object_id($deferred)], 69 | ); 70 | }; 71 | $this->queue[$channelId][spl_object_id($deferred)] = $deferred; 72 | 73 | return $deferred->getFuture(); 74 | } 75 | 76 | /** 77 | * @template T of Protocol\Frame 78 | * @param non-negative-int $channelId 79 | * @param class-string $frameType 80 | * @param callable(T): void $subscriber 81 | */ 82 | public function subscribe(int $channelId, string $frameType, callable $subscriber): void 83 | { 84 | /** @phpstan-ignore assign.propertyType */ 85 | $this->defers[$channelId][$frameType][] = $subscriber; 86 | } 87 | 88 | public function reject(int $channelId, \Throwable $e): void 89 | { 90 | $defers = $this->queue[$channelId] ?? []; 91 | unset($this->queue[$channelId], $this->defers[$channelId]); 92 | 93 | foreach ($defers as $f) { 94 | $f->error($e); 95 | } 96 | } 97 | 98 | /** 99 | * @param non-negative-int $channelId 100 | */ 101 | public function unsubscribe(int $channelId): void 102 | { 103 | unset($this->defers[$channelId], $this->queue[$channelId]); 104 | } 105 | 106 | public function emit(Protocol\Request $request): void 107 | { 108 | foreach ($this->defers[$request->channelId][$request->frame::class] ?? [] as $f) { 109 | $f($request->frame); 110 | } 111 | } 112 | 113 | public function error(\Throwable $e): void 114 | { 115 | try { 116 | foreach ($this->queue as $deferred) { 117 | foreach ($deferred as $future) { 118 | $future->error($e); 119 | } 120 | } 121 | } finally { 122 | $this->complete(); 123 | } 124 | } 125 | 126 | public function complete(): void 127 | { 128 | $this->defers = []; 129 | $this->queue = []; 130 | } 131 | 132 | public function count(): int 133 | { 134 | return \count($this->queue); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Internal/Io/AmqpConnection.php: -------------------------------------------------------------------------------- 1 | buffer = Buffer::empty(); 38 | $this->reader = new Protocol\Reader( 39 | new ReaderWriter( 40 | new BufferedReaderWriter( 41 | new AmpReaderWriter($this->socket), 42 | ), 43 | ), 44 | ); 45 | } 46 | 47 | /** 48 | * @template T of Protocol\Frame 49 | * @param ?class-string $expects 50 | * @return ($expects is null ? null : T) 51 | * @throws \Throwable 52 | */ 53 | public function rpc(Protocol\Frame $frame, ?string $expects = null): ?Protocol\Frame 54 | { 55 | $frame->write($this->buffer); 56 | $this->buffer->writeTo($this); 57 | 58 | if ($expects === null) { 59 | return null; 60 | } 61 | 62 | $response = $this->reader->read(); 63 | 64 | if ($response->frame instanceof $expects) { 65 | return $response->frame; 66 | } 67 | 68 | throw UnexpectedFrameReceived::forFrame($expects, $response->frame::class); 69 | } 70 | 71 | /** 72 | * @param iterable|Protocol\Frame $frames 73 | * @throws \Throwable 74 | */ 75 | public function writeFrame(iterable|Protocol\Frame $frames): void 76 | { 77 | if ($frames instanceof Protocol\Frame) { 78 | $frames = [$frames]; 79 | } 80 | 81 | foreach ($frames as $frame) { 82 | $frame->write($this->buffer); 83 | } 84 | 85 | $this->buffer->writeTo($this); 86 | } 87 | 88 | public function write(string $bytes): void 89 | { 90 | $this->socket->write($bytes); 91 | $this->lastWrite = Amp\now(); 92 | } 93 | 94 | public function ioLoop(Hooks $hooks): void 95 | { 96 | $reader = $this->reader; 97 | $isClosed = &$this->closed; 98 | 99 | EventLoop::queue(static function () use ($reader, &$isClosed, $hooks): void { 100 | try { 101 | while (!$isClosed) { 102 | $hooks->emit($reader->read()); 103 | } 104 | } catch (\Throwable $e) { 105 | if (!$e instanceof UnexpectedEof) { 106 | $hooks->error($e); 107 | } 108 | } finally { 109 | $isClosed = true; 110 | } 111 | 112 | $hooks->complete(); 113 | }); 114 | } 115 | 116 | /** 117 | * @param non-negative-int $interval 118 | */ 119 | public function heartbeat(int $interval): void 120 | { 121 | $interval = (int) ($interval / 2); 122 | 123 | $this->heartbeatId = EventLoop::repeat((int) ($interval / 3), function () use ($interval): void { 124 | if (Amp\now() >= ($this->lastWrite + $interval)) { 125 | $this->writeFrame(Protocol\Heartbeat::frame); 126 | } 127 | }); 128 | } 129 | 130 | public function close(): void 131 | { 132 | if (!$this->socket->isClosed()) { 133 | $this->socket->close(); 134 | } 135 | 136 | if ($this->heartbeatId !== null) { 137 | EventLoop::cancel($this->heartbeatId); 138 | } 139 | 140 | $this->closed = true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Internal/Io/AmqpConnectionFactory.php: -------------------------------------------------------------------------------- 1 | createConnection(); 32 | 33 | $start = $connection->rpc(Frame\ProtocolHeader::frame, Frame\ConnectionStart::class); 34 | 35 | $tune = $connection->rpc( 36 | Protocol\Method::connectionStartOk($this->properties->toArray(), Auth\Mechanism::select( 37 | $this->config->sasl(), 38 | $start->mechanisms, 39 | )), 40 | Frame\ConnectionTune::class, 41 | ); 42 | 43 | [$heartbeat, $channelMax, $frameMax] = [ 44 | $this->config->heartbeat($tune->heartbeat), 45 | $this->config->channelMax($tune->channelMax), 46 | $this->config->frameMax($tune->frameMax), 47 | ]; 48 | 49 | $connection->rpc( 50 | Protocol\Method::connectionTuneOk($channelMax, $frameMax, $heartbeat), 51 | ); 52 | 53 | $this->properties->tune($channelMax, $frameMax); 54 | 55 | if ($heartbeat > 0) { 56 | $connection->heartbeat($heartbeat); 57 | } 58 | 59 | $connection->rpc( 60 | Protocol\Method::connectionOpen($this->config->vhost), 61 | Frame\ConnectionOpenOk::class, 62 | ); 63 | 64 | $connection->ioLoop($this->hooks); 65 | 66 | $this->hooks->anyOf(0, Frame\ConnectionClose::class, static function () use ($connection): void { 67 | $connection->writeFrame(Protocol\Method::connectionCloseOk()); 68 | $connection->close(); 69 | }); 70 | 71 | return $connection; 72 | } 73 | 74 | /** 75 | * @param non-negative-int $replyCode 76 | */ 77 | public function close( 78 | AmqpConnection $connection, 79 | int $replyCode, 80 | string $replyText, 81 | ?Cancellation $cancellation = null, 82 | ): void { 83 | $connection->writeFrame(Protocol\Method::connectionClose($replyCode, $replyText)); 84 | 85 | $this->hooks->oneshot(0, Frame\ConnectionCloseOk::class)->await($cancellation); 86 | 87 | $connection->close(); 88 | } 89 | 90 | private function createConnection(): AmqpConnection 91 | { 92 | $exceptions = []; 93 | 94 | foreach ($this->config->connectionUrls() as $url) { 95 | try { 96 | return new AmqpConnection($this->createSocket($url)); 97 | } catch (\Throwable $e) { 98 | $exceptions[] = "{$url}: {$e->getMessage()}"; 99 | } 100 | } 101 | 102 | throw new Exception\ConnectionNotAvailable( 103 | \sprintf('No available amqp host: %s.', implode('; ', $exceptions)), 104 | ); 105 | } 106 | 107 | /** 108 | * @param non-empty-string $url 109 | */ 110 | private function createSocket(string $url): Socket\Socket 111 | { 112 | $context = (new Socket\ConnectContext()) 113 | ->withConnectTimeout($this->config->connectionTimeout); 114 | 115 | if ($this->config->tcpNoDelay) { 116 | $context = $context->withTcpNoDelay(); 117 | } 118 | 119 | $socket = Socket\connect($url, $context); 120 | 121 | if ($this->config->scheme === Scheme::amqps) { 122 | $socket->setupTls(); 123 | } 124 | 125 | return $socket; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Internal/Io/Buffer.php: -------------------------------------------------------------------------------- 1 | append($this->endian->packUint8($v)); 36 | } 37 | 38 | public function writeInt16(int $v): self 39 | { 40 | return $this->append($this->endian->packInt16($v)); 41 | } 42 | 43 | public function writeUint16(int $v): self 44 | { 45 | return $this->append($this->endian->packUint16($v)); 46 | } 47 | 48 | public function writeInt32(int $v): self 49 | { 50 | return $this->append($this->endian->packInt32($v)); 51 | } 52 | 53 | public function writeUint32(int $v): self 54 | { 55 | return $this->append($this->endian->packUint32($v)); 56 | } 57 | 58 | public function writeUint64(int $v): self 59 | { 60 | return $this->append($this->endian->packUint64($v)); 61 | } 62 | 63 | public function writeDouble(float $v): self 64 | { 65 | return $this->append($this->endian->packDouble($v)); 66 | } 67 | 68 | public function writeString(string $v): self 69 | { 70 | $this 71 | ->writeUint8(\strlen($v)) 72 | ->write($v); 73 | 74 | return $this; 75 | } 76 | 77 | public function writeText(string $v): self 78 | { 79 | $this 80 | ->writeUint32(\strlen($v)) 81 | ->write($v); 82 | 83 | return $this; 84 | } 85 | 86 | public function writeTimestamp(\DateTimeImmutable $date): self 87 | { 88 | $timestamp = $date->getTimestamp(); 89 | \assert($timestamp >= 0); 90 | 91 | return $this->writeUint64($timestamp); 92 | } 93 | 94 | public function writeTable(array $values): self 95 | { 96 | return $this->reserve($this->endian->packUint32(...), static function (WriteBytes $buffer) use ($values): void { 97 | foreach ($values as $key => $value) { 98 | $buffer = $buffer 99 | ->writeString((string) $key) 100 | ->writeValue($value); 101 | } 102 | }); 103 | } 104 | 105 | public function writeArray(array $values): self 106 | { 107 | return $this->reserve($this->endian->packUint32(...), static function (WriteBytes $buffer) use ($values): void { 108 | foreach ($values as $value) { 109 | $buffer = $buffer->writeValue($value); 110 | } 111 | }); 112 | } 113 | 114 | public function writeValue(mixed $value): self 115 | { 116 | return match (true) { 117 | \is_string($value) => $this 118 | ->writeUint8(Type::text->value) 119 | ->writeText($value), 120 | \is_int($value) => $this 121 | ->writeUint8(Type::int32->value) 122 | ->writeInt32($value), 123 | \is_float($value) => $this 124 | ->writeUint8(Type::float->value) 125 | ->writeDouble($value), 126 | \is_bool($value) => $this 127 | ->writeUint8(Type::boolean->value) 128 | ->writeUint8((int) $value), 129 | $value instanceof \DateTimeImmutable => $this 130 | ->writeUint8(Type::timestamp->value) 131 | ->writeTimestamp($value), 132 | $value === null => $this->writeUint8(Type::null->value), 133 | \is_array($value) && array_is_list($value) => $this 134 | ->writeUint8(Type::array->value) 135 | ->writeArray($value), 136 | \is_array($value) => $this 137 | ->writeUint8(Type::table->value) 138 | ->writeTable($value), 139 | default => throw UnknownValueType::forValue($value), 140 | }; 141 | } 142 | 143 | public function writeBits(bool ...$bits): self 144 | { 145 | $value = 0; 146 | 147 | foreach ($bits as $i => $bit) { 148 | $value |= (int) $bit << (int) $i; 149 | } 150 | 151 | /** @var non-negative-int $value */ 152 | return $this->writeUint8($value); 153 | } 154 | 155 | public function write(string $v): self 156 | { 157 | return $this->append($v); 158 | } 159 | 160 | public function writeTo(Writer $writer): void 161 | { 162 | try { 163 | if (($v = $this->reset()) !== '') { 164 | $writer->write($v); 165 | } 166 | } catch (ClosedException) { 167 | throw new ConnectionIsClosed(); 168 | } 169 | } 170 | 171 | public function readInt8(): int 172 | { 173 | return $this->endian->unpackInt8($this->consume(1)); 174 | } 175 | 176 | public function readUint8(): int 177 | { 178 | return $this->endian->unpackUint8($this->consume(1)); 179 | } 180 | 181 | public function readInt16(): int 182 | { 183 | return $this->endian->unpackInt16($this->consume(2)); 184 | } 185 | 186 | public function readUint16(): int 187 | { 188 | return $this->endian->unpackUint16($this->consume(2)); 189 | } 190 | 191 | public function readInt32(): int 192 | { 193 | return $this->endian->unpackInt32($this->consume(4)); 194 | } 195 | 196 | public function readUint32(): int 197 | { 198 | return $this->endian->unpackUint32($this->consume(4)); 199 | } 200 | 201 | public function readInt64(): int 202 | { 203 | return $this->endian->unpackInt64($this->consume(8)); 204 | } 205 | 206 | public function readUint64(): int 207 | { 208 | return $this->endian->unpackUint64($this->consume(8)); 209 | } 210 | 211 | public function readFloat(): float 212 | { 213 | return $this->endian->unpackFloat($this->consume(4)); 214 | } 215 | 216 | public function readDouble(): float 217 | { 218 | return $this->endian->unpackDouble($this->consume(8)); 219 | } 220 | 221 | public function readTimestamp(): \DateTimeImmutable 222 | { 223 | return new \DateTimeImmutable(\sprintf('@%s', $this->readUint64())); 224 | } 225 | 226 | public function readDecimal(): int 227 | { 228 | $scale = $this->readUint8(); 229 | $value = $this->readUint32(); 230 | 231 | return (int) ($value * (10 ** $scale)); 232 | } 233 | 234 | public function readText(): string 235 | { 236 | $v = ''; 237 | if (($size = $this->readUint32()) > 0) { 238 | $v = $this->read($size); 239 | } 240 | 241 | return $v; 242 | } 243 | 244 | public function readString(): string 245 | { 246 | $v = ''; 247 | if (($size = $this->readUint8()) > 0) { 248 | $v = $this->read($size); 249 | } 250 | 251 | return $v; 252 | } 253 | 254 | public function readArray(): array 255 | { 256 | $expects = \strlen($this->buffer) - $this->readUint32(); 257 | $values = []; 258 | 259 | while ($expects < \strlen($this->buffer)) { 260 | $values[] = $this->readValue(); 261 | } 262 | 263 | return $values; 264 | } 265 | 266 | public function readTable(): array 267 | { 268 | $expects = \strlen($this->buffer) - $this->readUint32(); 269 | $table = []; 270 | 271 | while ($expects < \strlen($this->buffer)) { 272 | $table[$this->readString()] = $this->readValue(); 273 | } 274 | 275 | return $table; 276 | } 277 | 278 | public function read(int $n): string 279 | { 280 | return $this->consume($n); 281 | } 282 | 283 | public function readValue(): mixed 284 | { 285 | return match (Type::from($this->readUint8())) { 286 | Type::boolean => $this->readUint8() > 0, 287 | Type::int8 => $this->readInt8(), 288 | Type::uint8 => $this->readUint8(), 289 | Type::int16 => $this->readInt16(), 290 | Type::uint16 => $this->readUint16(), 291 | Type::int32 => $this->readInt32(), 292 | Type::uint32 => $this->readUint32(), 293 | Type::int64 => $this->readInt64(), 294 | Type::uint64 => $this->readUint64(), 295 | Type::float => $this->readFloat(), 296 | Type::double => $this->readDouble(), 297 | Type::decimal => $this->readDecimal(), 298 | Type::string => $this->readString(), 299 | Type::text => $this->readText(), 300 | Type::timestamp => $this->readTimestamp(), 301 | Type::array => $this->readArray(), 302 | Type::table => $this->readTable(), 303 | Type::null => null, 304 | }; 305 | } 306 | 307 | public function readBits(int $n): array 308 | { 309 | /** @var non-empty-list $bits */ 310 | $bits = []; 311 | $value = $this->readUint8(); 312 | 313 | for ($i = 0; $i < $n; ++$i) { 314 | $bits[] = ($value & (1 << $i)) > 0; 315 | } 316 | 317 | return $bits; 318 | } 319 | 320 | public function reset(): string 321 | { 322 | [$v, $this->buffer] = [$this->buffer, '']; 323 | 324 | return $v; 325 | } 326 | 327 | public function reserve(callable $reserve, callable $write): self 328 | { 329 | $pos = \strlen($this->buffer); 330 | $this->append($idle = $reserve(0)); 331 | $write($this); 332 | 333 | $len = \strlen($this->buffer) - $pos - \strlen($idle); 334 | \assert($len >= 0); 335 | $v = $reserve($len); 336 | 337 | for ($i = 0, $cursor = $pos; $i < \strlen($v); ++$i, ++$cursor) { 338 | $this->buffer[$cursor] = $v[$i]; 339 | } 340 | 341 | return $this; 342 | } 343 | 344 | public function count(): int 345 | { 346 | return \strlen($this->buffer); 347 | } 348 | 349 | private function append(string $v): self 350 | { 351 | $this->buffer .= $v; 352 | 353 | return $this; 354 | } 355 | 356 | /** 357 | * @param positive-int $n 358 | * @return non-empty-string 359 | */ 360 | private function consume(int $n): string 361 | { 362 | if (\strlen($this->buffer) < $n) { 363 | throw new \RuntimeException('Buffer is empty.'); 364 | } 365 | 366 | /** @var non-empty-string $v */ 367 | $v = substr($this->buffer, 0, $n); 368 | $this->buffer = substr($this->buffer, $n); 369 | 370 | return $v; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/Internal/Io/ChannelFactory.php: -------------------------------------------------------------------------------- 1 | */ 24 | private array $channels = []; 25 | 26 | public function __construct( 27 | private readonly Properties $properties, 28 | private readonly Hooks $hooks, 29 | ) { 30 | $this->hooks->anyOf(0, Frame\ConnectionClose::class, function (Frame\ConnectionClose $close): void { 31 | $error = Exception\ConnectionWasClosed::byServer($close->replyCode, $close->replyText); 32 | 33 | foreach ($this->channels as $channel) { 34 | $channel->abandon($error); 35 | } 36 | 37 | $this->channels = []; 38 | }); 39 | } 40 | 41 | public function open(AmqpConnection $connection, ?Cancellation $cancellation = null): Channel 42 | { 43 | $channelId = $this->allocateChannelId(); 44 | $this->openChannel($connection, $channelId, $cancellation); 45 | 46 | return $this->channels[$channelId] = new Channel( 47 | $channelId, 48 | $connection, 49 | $this->properties, 50 | $this->hooks, 51 | ); 52 | } 53 | 54 | /** 55 | * @param non-negative-int $replyCode 56 | */ 57 | public function close( 58 | int $replyCode, 59 | string $replyText, 60 | ): void { 61 | try { 62 | foreach ($this->channels as $channel) { 63 | $channel->close($replyCode, $replyText); 64 | } 65 | } finally { 66 | $this->channels = []; 67 | $this->channelId = 1; 68 | } 69 | } 70 | 71 | /** 72 | * @param non-negative-int $channelId 73 | */ 74 | private function openChannel(AmqpConnection $connection, int $channelId, ?Cancellation $cancellation = null): void 75 | { 76 | $connection->writeFrame(Protocol\Method::channelOpen($channelId)); 77 | 78 | $this->hooks->oneshot($channelId, Frame\ChannelOpenOkFrame::class)->await($cancellation); 79 | 80 | $this->hooks->anyOf( 81 | $channelId, 82 | [Frame\ChannelCloseOk::class, Frame\ChannelClose::class], 83 | function (Frame\ChannelCloseOk|Frame\ChannelClose $frame) use ($channelId, $connection): void { 84 | $channel = $this->channels[$channelId] ?? null; 85 | 86 | if ($channel !== null) { 87 | unset($this->channels[$channelId]); 88 | 89 | if ($frame instanceof Frame\ChannelClose) { 90 | $connection->writeFrame(Protocol\Method::channelCloseOk($channelId)); 91 | $channel->abandon(new Exception\ChannelWasClosed($frame->replyCode, $frame->replyText)); 92 | } 93 | 94 | $this->hooks->unsubscribe($channelId); 95 | } 96 | }, 97 | ); 98 | } 99 | 100 | /** 101 | * @return non-negative-int 102 | */ 103 | private function allocateChannelId(): int 104 | { 105 | for ($id = $this->channelId; $id <= $this->properties->maxChannel(); ++$id) { 106 | if (!isset($this->channels[$id])) { 107 | $this->channelId = $id + 1; 108 | 109 | return $id; 110 | } 111 | } 112 | 113 | for ($id = 1; $id < $this->channelId; ++$id) { 114 | if (!isset($this->channels[$id])) { 115 | $this->channelId = $id + 1; 116 | 117 | return $id; 118 | } 119 | } 120 | 121 | throw Exception\NoAvailableChannel::forMaxChannel($this->properties->maxChannel()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Internal/Io/ReadBytes.php: -------------------------------------------------------------------------------- 1 | 57 | */ 58 | public function readArray(): array; 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function readTable(): array; 64 | 65 | /** 66 | * @param positive-int $n 67 | * @return non-empty-string 68 | */ 69 | public function read(int $n): string; 70 | 71 | public function readValue(): mixed; 72 | 73 | /** 74 | * @param non-negative-int $n 75 | * @return non-empty-list 76 | */ 77 | public function readBits(int $n): array; 78 | 79 | public function reset(): string; 80 | } 81 | -------------------------------------------------------------------------------- /src/Internal/Io/WriteBytes.php: -------------------------------------------------------------------------------- 1 | $values 48 | */ 49 | public function writeTable(array $values): self; 50 | 51 | /** 52 | * @param list $values 53 | */ 54 | public function writeArray(array $values): self; 55 | 56 | public function writeValue(mixed $value): self; 57 | 58 | public function writeBits(bool ...$bits): self; 59 | 60 | public function write(string $v): self; 61 | 62 | /** 63 | * @param callable(non-negative-int): non-empty-string $reserve 64 | * @param callable(self): void $write 65 | */ 66 | public function reserve(callable $reserve, callable $write): self; 67 | 68 | /** 69 | * @throws \Throwable 70 | */ 71 | public function writeTo(Writer $writer): void; 72 | } 73 | -------------------------------------------------------------------------------- /src/Internal/Io/WriterTo.php: -------------------------------------------------------------------------------- 1 | */ 21 | private array $capabilities = [ 22 | 'connection.blocked' => true, 23 | 'basic.nack' => true, 24 | 'publisher_confirms' => true, 25 | ]; 26 | 27 | /** @var non-empty-string */ 28 | private string $product = 'AMQP 0.9.1 Client'; 29 | 30 | /** @var non-empty-string */ 31 | private readonly string $version; 32 | 33 | /** @var non-empty-string */ 34 | private readonly string $platform; 35 | 36 | public static function createDefault(): self 37 | { 38 | return new self(); 39 | } 40 | 41 | /** 42 | * @param non-negative-int $maxChannel 43 | * @param positive-int $maxFrame 44 | */ 45 | public function tune( 46 | int $maxChannel, 47 | int $maxFrame, 48 | ): void { 49 | $this->maxChannel = $maxChannel; 50 | $this->maxFrame = $maxFrame; 51 | } 52 | 53 | /** 54 | * @param non-empty-string $capability 55 | */ 56 | public function capable(string $capability): bool 57 | { 58 | return $this->capabilities[$capability] ?? false; 59 | } 60 | 61 | /** 62 | * @return non-negative-int 63 | */ 64 | public function maxChannel(): int 65 | { 66 | return $this->maxChannel; 67 | } 68 | 69 | /** 70 | * @return positive-int 71 | */ 72 | public function maxFrame(): int 73 | { 74 | return $this->maxFrame; 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public function toArray(): array 81 | { 82 | return [ 83 | 'product' => $this->product, 84 | 'version' => $this->version, 85 | 'platform' => $this->platform, 86 | 'capabilities' => $this->capabilities, 87 | ]; 88 | } 89 | 90 | private function __construct() 91 | { 92 | $this->version = VersionProvider::provide(); 93 | $this->platform = self::DEFAULT_PLATFORM; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Auth/AMQPlain.php: -------------------------------------------------------------------------------- 1 | writeString('LOGIN') 29 | ->writeValue($this->username) 30 | ->writeString('PASSWORD') 31 | ->writeValue($this->password); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Auth/Mechanism.php: -------------------------------------------------------------------------------- 1 | new Plain($username, $password), 25 | self::AMQPLAIN => new AMQPlain($username, $password), 26 | default => throw AuthenticationMechanismIsNotSupported::forClientMechanism($mechanism), 27 | }; 28 | } 29 | 30 | /** 31 | * @param non-empty-list $selected 32 | * @param list $available 33 | * @throws AuthenticationMechanismIsNotSupported 34 | */ 35 | final public static function select(array $selected, array $available): self 36 | { 37 | foreach ($selected as $selectedMechanism) { 38 | if (\in_array($selectedMechanism->name(), $available, true)) { 39 | return $selectedMechanism; 40 | } 41 | } 42 | 43 | throw AuthenticationMechanismIsNotSupported::forServerMechanisms($available); 44 | } 45 | 46 | /** 47 | * @return self::* 48 | */ 49 | abstract public function name(): string; 50 | 51 | abstract public function write(Io\WriteBytes $writer): void; 52 | } 53 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Auth/Plain.php: -------------------------------------------------------------------------------- 1 | write("\000{$this->username}\000{$this->password}"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Body.php: -------------------------------------------------------------------------------- 1 | writeUint8(FrameType::body->value) 32 | ->writeUint16($this->channelId) 33 | ->writeUint32(\strlen($this->body)) 34 | ->write($this->body) 35 | ->writeUint8(Protocol::FRAME_END); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Internal/Protocol/ClassMethod.php: -------------------------------------------------------------------------------- 1 | readUint64(), 27 | $reader->readBits(1)[0], 28 | ); 29 | } 30 | 31 | public function write(Io\WriteBytes $writer): void 32 | { 33 | $writer 34 | ->writeUint64($this->deliveryTag) 35 | ->writeBits($this->multiple); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicCancel.php: -------------------------------------------------------------------------------- 1 | readString(), 24 | $reader->readBits(1)[0], 25 | ); 26 | } 27 | 28 | public function write(Io\WriteBytes $writer): void 29 | { 30 | $writer 31 | ->writeString($this->consumerTag) 32 | ->writeBits($this->noWait); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicCancelOk.php: -------------------------------------------------------------------------------- 1 | readString()); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeString($this->consumerTag); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicConsume.php: -------------------------------------------------------------------------------- 1 | $arguments 17 | * @param non-negative-int $reserved1 18 | */ 19 | public function __construct( 20 | public string $queue, 21 | public string $consumerTag, 22 | public bool $noLocal, 23 | public bool $noAck, 24 | public bool $exclusive, 25 | public bool $noWait, 26 | public array $arguments, 27 | public int $reserved1 = 0, 28 | ) {} 29 | 30 | public static function read(Io\ReadBytes $reader): self 31 | { 32 | $reserved1 = $reader->readUint16(); 33 | $queue = $reader->readString(); 34 | $consumerTag = $reader->readString(); 35 | [$noLocal, $noAck, $exclusive, $noWait] = $reader->readBits(4); 36 | $arguments = $reader->readTable(); 37 | 38 | return new self( 39 | $queue, 40 | $consumerTag, 41 | $noLocal, 42 | $noAck, 43 | $exclusive, 44 | $noWait, 45 | $arguments, 46 | $reserved1, 47 | ); 48 | } 49 | 50 | public function write(Io\WriteBytes $writer): void 51 | { 52 | $writer 53 | ->writeUint16($this->reserved1) 54 | ->writeString($this->queue) 55 | ->writeString($this->consumerTag) 56 | ->writeBits($this->noLocal, $this->noAck, $this->exclusive, $this->noWait) 57 | ->writeTable($this->arguments); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicConsumeOk.php: -------------------------------------------------------------------------------- 1 | readString()); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeString($this->consumerTag); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicDeliver.php: -------------------------------------------------------------------------------- 1 | readString(), 30 | $reader->readUint64(), 31 | $reader->readBits(1)[0], 32 | $reader->readString(), 33 | $reader->readString(), 34 | ); 35 | } 36 | 37 | public function write(Io\WriteBytes $writer): void 38 | { 39 | $writer 40 | ->writeString($this->consumerTag) 41 | ->writeUint64($this->deliveryTag) 42 | ->writeBits($this->redelivered) 43 | ->writeString($this->exchange) 44 | ->writeString($this->routingKey); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicGet.php: -------------------------------------------------------------------------------- 1 | readUint16(); 27 | $queue = $reader->readString(); 28 | $noAck = $reader->readBits(1)[0]; 29 | 30 | return new self( 31 | $queue, 32 | $noAck, 33 | $reserved1, 34 | ); 35 | } 36 | 37 | public function write(Io\WriteBytes $writer): void 38 | { 39 | $writer 40 | ->writeUint16($this->reserved1) 41 | ->writeString($this->queue) 42 | ->writeBits($this->noAck); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicGetEmpty.php: -------------------------------------------------------------------------------- 1 | readString()); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeString($this->reserved1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicGetOk.php: -------------------------------------------------------------------------------- 1 | readUint64(), 31 | $reader->readBits(1)[0], 32 | $reader->readString(), 33 | $reader->readString(), 34 | $reader->readUint32(), 35 | ); 36 | } 37 | 38 | public function write(Io\WriteBytes $writer): void 39 | { 40 | $writer 41 | ->writeUint64($this->deliveryTag) 42 | ->writeBits($this->redelivered) 43 | ->writeString($this->exchange) 44 | ->writeString($this->routingKey) 45 | ->writeUint32($this->messageCount); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicNack.php: -------------------------------------------------------------------------------- 1 | readUint64(); 27 | [$multiple, $requeue] = $reader->readBits(2); 28 | 29 | return new self( 30 | $deliveryTag, 31 | $multiple, 32 | $requeue, 33 | ); 34 | } 35 | 36 | public function write(Io\WriteBytes $writer): void 37 | { 38 | $writer 39 | ->writeUint64($this->deliveryTag) 40 | ->writeBits($this->multiple, $this->requeue); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicPublish.php: -------------------------------------------------------------------------------- 1 | readUint16(); 29 | $exchange = $reader->readString(); 30 | $routingKey = $reader->readString(); 31 | [$mandatory, $immediate] = $reader->readBits(2); 32 | 33 | return new self( 34 | $exchange, 35 | $routingKey, 36 | $mandatory, 37 | $immediate, 38 | $reserved1, 39 | ); 40 | } 41 | 42 | public function write(Io\WriteBytes $writer): void 43 | { 44 | $writer 45 | ->writeUint16($this->reserved1) 46 | ->writeString($this->exchange) 47 | ->writeString($this->routingKey) 48 | ->writeBits($this->mandatory, $this->immediate); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicQos.php: -------------------------------------------------------------------------------- 1 | readUint32(), 29 | $reader->readUint16(), 30 | $reader->readBits(1)[0], 31 | ); 32 | } 33 | 34 | public function write(Io\WriteBytes $writer): void 35 | { 36 | $writer 37 | ->writeUint32($this->prefetchSize) 38 | ->writeUint16($this->prefetchCount) 39 | ->writeBits($this->global); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicQosOk.php: -------------------------------------------------------------------------------- 1 | readBits(1)[0]); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeBits($this->requeue); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicRecoverOk.php: -------------------------------------------------------------------------------- 1 | readUint64(), 27 | $reader->readBits(1)[0], 28 | ); 29 | } 30 | 31 | public function write(Io\WriteBytes $writer): void 32 | { 33 | $writer 34 | ->writeUint64($this->deliveryTag) 35 | ->writeBits($this->requeue); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/BasicReturn.php: -------------------------------------------------------------------------------- 1 | readUint16(), 29 | $reader->readString(), 30 | $reader->readString(), 31 | $reader->readString(), 32 | ); 33 | } 34 | 35 | public function write(Io\WriteBytes $writer): void 36 | { 37 | $writer 38 | ->writeUint16($this->replyCode) 39 | ->writeString($this->replyText) 40 | ->writeString($this->exchange) 41 | ->writeString($this->routingKey); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ChannelClose.php: -------------------------------------------------------------------------------- 1 | readUint16(), 33 | $reader->readString(), 34 | $reader->readUint16(), 35 | $reader->readUint16(), 36 | ); 37 | } 38 | 39 | public function write(Io\WriteBytes $writer): void 40 | { 41 | $writer 42 | ->writeUint16($this->replyCode) 43 | ->writeString($this->replyText) 44 | ->writeUint16($this->classId) 45 | ->writeUint16($this->methodId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ChannelCloseOk.php: -------------------------------------------------------------------------------- 1 | readBits(1)[0]); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeBits($this->active); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ChannelFlowOk.php: -------------------------------------------------------------------------------- 1 | readBits(1)[0]); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeBits($this->active); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ChannelOpen.php: -------------------------------------------------------------------------------- 1 | readString()); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeString($this->reserved1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ChannelOpenOkFrame.php: -------------------------------------------------------------------------------- 1 | readText()); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeText($this->channelId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConfirmSelect.php: -------------------------------------------------------------------------------- 1 | readBits(1)[0]); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeBits($this->noWait); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConfirmSelectOk.php: -------------------------------------------------------------------------------- 1 | readUint16(), 33 | $reader->readString(), 34 | $reader->readUint16(), 35 | $reader->readUint16(), 36 | ); 37 | } 38 | 39 | public function write(Io\WriteBytes $writer): void 40 | { 41 | $writer 42 | ->writeUint16($this->replyCode) 43 | ->writeString($this->replyText) 44 | ->writeUint16($this->classId) 45 | ->writeUint16($this->methodId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConnectionCloseOk.php: -------------------------------------------------------------------------------- 1 | readString(); 27 | \assert($vhost !== '', 'vhost must not be empty.'); 28 | 29 | return new self( 30 | $vhost, 31 | $reader->readString(), 32 | $reader->readBits(1)[0], 33 | ); 34 | } 35 | 36 | public function write(Io\WriteBytes $writer): void 37 | { 38 | $writer 39 | ->writeString($this->vhost) 40 | ->writeString($this->reserved1) 41 | ->writeBits($this->reserved2); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConnectionOpenOk.php: -------------------------------------------------------------------------------- 1 | readString()); 22 | } 23 | 24 | public function write(Io\WriteBytes $writer): void 25 | { 26 | $writer->writeString($this->knownHosts); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConnectionStart.php: -------------------------------------------------------------------------------- 1 | $mechanisms 30 | */ 31 | public function __construct( 32 | public int $versionMajor, 33 | public int $versionMinor, 34 | public array $serverProperties, 35 | public array $mechanisms = [], 36 | public string $locales = '', 37 | ) {} 38 | 39 | public static function read(Io\ReadBytes $reader): self 40 | { 41 | [$versionMajor, $versionMinor] = [$reader->readUint8(), $reader->readUint8()]; 42 | 43 | /** @var ServerProperties $serverProperties */ 44 | $serverProperties = $reader->readTable(); 45 | 46 | return new self( 47 | $versionMajor, 48 | $versionMinor, 49 | $serverProperties, 50 | explode(' ', $reader->readText()), 51 | $reader->readText(), 52 | ); 53 | } 54 | 55 | public function write(Io\WriteBytes $writer): void 56 | { 57 | $writer 58 | ->writeUint8($this->versionMajor) 59 | ->writeUint8($this->versionMinor) 60 | ->writeTable($this->serverProperties) 61 | ->writeText(implode(' ', $this->mechanisms)) 62 | ->writeText($this->locales); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConnectionStartOk.php: -------------------------------------------------------------------------------- 1 | $clientProperties 19 | */ 20 | public function __construct( 21 | public array $clientProperties, 22 | public Mechanism $auth, 23 | public string $locale = 'en_US', 24 | ) {} 25 | 26 | public static function read(Io\ReadBytes $reader): self 27 | { 28 | throw new \BadMethodCallException('Not implemented yet.'); 29 | } 30 | 31 | public function write(Io\WriteBytes $writer): void 32 | { 33 | $writer 34 | ->writeTable($this->clientProperties) 35 | ->writeString($this->auth->name()) 36 | ->reserve(endian::network->packUint32(...), $this->auth->write(...)) 37 | ->writeString($this->locale); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConnectionTune.php: -------------------------------------------------------------------------------- 1 | readInt16(), 25 | $reader->readInt32(), 26 | $reader->readInt16(), 27 | ); 28 | } 29 | 30 | public function write(Io\WriteBytes $writer): void 31 | { 32 | $writer 33 | ->writeInt16($this->channelMax) 34 | ->writeInt32($this->frameMax) 35 | ->writeInt16($this->heartbeat); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ConnectionTuneOk.php: -------------------------------------------------------------------------------- 1 | readUint16(), 30 | $reader->readUint32(), 31 | $reader->readUint16(), 32 | ); 33 | } 34 | 35 | public function write(Io\WriteBytes $writer): void 36 | { 37 | $writer 38 | ->writeUint16($this->channelMax) 39 | ->writeUint32($this->frameMax) 40 | ->writeUint16($this->heartbeat); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ContentBody.php: -------------------------------------------------------------------------------- 1 | reset()); 23 | } 24 | 25 | public function write(Io\WriteBytes $writer): void 26 | { 27 | throw new NotImplemented(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ContentHeader.php: -------------------------------------------------------------------------------- 1 | readUint16(), 35 | $reader->readUint16(), 36 | $reader->readUint64(), 37 | $flags = $reader->readUint16(), 38 | MessageProperties::read($reader, $flags), 39 | ); 40 | } 41 | 42 | public function write(Io\WriteBytes $writer): void 43 | { 44 | throw new NotImplemented(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ExchangeBind.php: -------------------------------------------------------------------------------- 1 | $arguments 19 | * @param non-negative-int $reserved1 20 | */ 21 | public function __construct( 22 | public string $destination, 23 | public string $source, 24 | public string $routingKey = '', 25 | public array $arguments = [], 26 | public bool $noWait = false, 27 | public int $reserved1 = 0, 28 | ) {} 29 | 30 | public static function read(Io\ReadBytes $reader): self 31 | { 32 | $reserved1 = $reader->readUint16(); 33 | 34 | $destination = $reader->readString(); 35 | \assert($destination !== '', 'destination must not be empty.'); 36 | 37 | $source = $reader->readString(); 38 | \assert($source !== '', 'source must not be empty.'); 39 | 40 | $routingKey = $reader->readString(); 41 | $noWait = $reader->readBits(1)[0]; 42 | $arguments = $reader->readTable(); 43 | 44 | return new self( 45 | $destination, 46 | $source, 47 | $routingKey, 48 | $arguments, 49 | $noWait, 50 | $reserved1, 51 | ); 52 | } 53 | 54 | public function write(Io\WriteBytes $writer): void 55 | { 56 | $writer 57 | ->writeUint16($this->reserved1) 58 | ->writeString($this->destination) 59 | ->writeString($this->source) 60 | ->writeString($this->routingKey) 61 | ->writeBits($this->noWait) 62 | ->writeTable($this->arguments); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ExchangeBindOk.php: -------------------------------------------------------------------------------- 1 | $arguments 19 | * @param non-negative-int $reserved1 20 | */ 21 | public function __construct( 22 | public string $exchange, 23 | public string $exchangeType, 24 | public bool $passive = false, 25 | public bool $durable = false, 26 | public bool $autoDelete = false, 27 | public bool $internal = false, 28 | public bool $noWait = false, 29 | public array $arguments = [], 30 | public int $reserved1 = 0, 31 | ) {} 32 | 33 | public static function read(Io\ReadBytes $reader): Frame 34 | { 35 | $reserved1 = $reader->readUint16(); 36 | 37 | $exchange = $reader->readString(); 38 | \assert($exchange !== '', 'exchange must not be empty.'); 39 | 40 | $exchangeType = $reader->readString(); 41 | \assert($exchangeType !== '', 'exchange type must not be empty.'); 42 | 43 | [$passive, $durable, $autoDelete, $internal, $noWait] = $reader->readBits(5); 44 | $arguments = $reader->readTable(); 45 | 46 | return new self( 47 | $exchange, 48 | $exchangeType, 49 | $passive, 50 | $durable, 51 | $autoDelete, 52 | $internal, 53 | $noWait, 54 | $arguments, 55 | $reserved1, 56 | ); 57 | } 58 | 59 | public function write(Io\WriteBytes $writer): void 60 | { 61 | $writer 62 | ->writeUint16($this->reserved1) 63 | ->writeString($this->exchange) 64 | ->writeString($this->exchangeType) 65 | ->writeBits($this->passive, $this->durable, $this->autoDelete, $this->internal, $this->noWait) 66 | ->writeTable($this->arguments); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ExchangeDeclareOk.php: -------------------------------------------------------------------------------- 1 | readUint16(); 29 | 30 | $exchange = $reader->readString(); 31 | \assert($exchange !== '', 'exchange must not be empty.'); 32 | 33 | [$ifUnused, $noWait] = $reader->readBits(2); 34 | 35 | return new self( 36 | $exchange, 37 | $ifUnused, 38 | $noWait, 39 | $reserved1, 40 | ); 41 | } 42 | 43 | public function write(Io\WriteBytes $writer): void 44 | { 45 | $writer 46 | ->writeUint16($this->reserved1) 47 | ->writeString($this->exchange) 48 | ->writeBits($this->ifUnused, $this->noWait); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ExchangeDeleteOk.php: -------------------------------------------------------------------------------- 1 | $arguments 19 | * @param non-negative-int $reserved1 20 | */ 21 | public function __construct( 22 | public string $destination, 23 | public string $source, 24 | public string $routingKey = '', 25 | public array $arguments = [], 26 | public bool $noWait = false, 27 | public int $reserved1 = 0, 28 | ) {} 29 | 30 | public static function read(Io\ReadBytes $reader): self 31 | { 32 | $reserved1 = $reader->readUint16(); 33 | 34 | $destination = $reader->readString(); 35 | \assert($destination !== '', 'destination must not be empty.'); 36 | 37 | $source = $reader->readString(); 38 | \assert($source !== '', 'source must not be empty.'); 39 | 40 | $routingKey = $reader->readString(); 41 | $noWait = $reader->readBits(1)[0]; 42 | $arguments = $reader->readTable(); 43 | 44 | return new self( 45 | $destination, 46 | $source, 47 | $routingKey, 48 | $arguments, 49 | $noWait, 50 | $reserved1, 51 | ); 52 | } 53 | 54 | public function write(Io\WriteBytes $writer): void 55 | { 56 | $writer 57 | ->writeUint16($this->reserved1) 58 | ->writeString($this->destination) 59 | ->writeString($this->source) 60 | ->writeString($this->routingKey) 61 | ->writeBits($this->noWait) 62 | ->writeTable($this->arguments); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/ExchangeUnbindOk.php: -------------------------------------------------------------------------------- 1 | read(8); 21 | 22 | return self::frame; 23 | } 24 | 25 | public function write(Io\WriteBytes $writer): void 26 | { 27 | $writer 28 | ->write('AMQP') 29 | ->writeUint8(0) 30 | ->writeUint8(0) 31 | ->writeUint8(9) 32 | ->writeUint8(1); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueBind.php: -------------------------------------------------------------------------------- 1 | $arguments 17 | */ 18 | public function __construct( 19 | public string $queue, 20 | public string $exchange, 21 | public string $routingKey, 22 | public array $arguments = [], 23 | public bool $noWait = false, 24 | public int $reserved1 = 0, 25 | ) {} 26 | 27 | public static function read(Io\ReadBytes $reader): self 28 | { 29 | $reserved1 = $reader->readUint16(); 30 | $queue = $reader->readString(); 31 | $exchange = $reader->readString(); 32 | $routingKey = $reader->readString(); 33 | $noWait = $reader->readBits(1)[0]; 34 | $arguments = $reader->readTable(); 35 | 36 | return new self( 37 | $queue, 38 | $exchange, 39 | $routingKey, 40 | $arguments, 41 | $noWait, 42 | $reserved1, 43 | ); 44 | } 45 | 46 | public function write(Io\WriteBytes $writer): void 47 | { 48 | $writer 49 | ->writeInt16(0) 50 | ->writeString($this->queue) 51 | ->writeString($this->exchange) 52 | ->writeString($this->routingKey) 53 | ->writeBits($this->noWait) 54 | ->writeTable($this->arguments); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueBindOk.php: -------------------------------------------------------------------------------- 1 | $arguments 17 | */ 18 | public function __construct( 19 | public string $queue, 20 | public bool $passive = false, 21 | public bool $durable = false, 22 | public bool $exclusive = false, 23 | public bool $autoDelete = false, 24 | public bool $noWait = false, 25 | public array $arguments = [], 26 | public int $reserved1 = 0, 27 | ) {} 28 | 29 | public static function read(Io\ReadBytes $reader): self 30 | { 31 | $reserved1 = $reader->readUint16(); 32 | $queue = $reader->readString(); 33 | 34 | [$passive, $durable, $exclusive, $autoDelete, $noWait] = $reader->readBits(5); 35 | $arguments = $reader->readTable(); 36 | 37 | return new self( 38 | $queue, 39 | $passive, 40 | $durable, 41 | $exclusive, 42 | $autoDelete, 43 | $noWait, 44 | $arguments, 45 | $reserved1, 46 | ); 47 | } 48 | 49 | public function write(Io\WriteBytes $writer): void 50 | { 51 | $writer 52 | ->writeUint16(0) 53 | ->writeString($this->queue) 54 | ->writeBits($this->passive, $this->durable, $this->exclusive, $this->autoDelete, $this->noWait) 55 | ->writeTable($this->arguments); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueDeclareOk.php: -------------------------------------------------------------------------------- 1 | readString(); 30 | 31 | /** @var non-negative-int $messages */ 32 | $messages = $reader->readInt32(); 33 | 34 | /** @var non-negative-int $consumers */ 35 | $consumers = $reader->readInt32(); 36 | 37 | return new self( 38 | $queue, 39 | $messages, 40 | $consumers, 41 | ); 42 | } 43 | 44 | public function write(Io\WriteBytes $writer): void 45 | { 46 | $writer 47 | ->writeString($this->queue) 48 | ->writeInt32($this->messages) 49 | ->writeInt32($this->consumers); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueDelete.php: -------------------------------------------------------------------------------- 1 | readUint16(); 30 | $queue = $reader->readString(); 31 | \assert($queue !== '', 'queue must not be empty.'); 32 | 33 | [$ifUnused, $ifEmpty, $noWait] = $reader->readBits(3); 34 | 35 | return new self( 36 | $queue, 37 | $ifUnused, 38 | $ifEmpty, 39 | $noWait, 40 | $reserved1, 41 | ); 42 | } 43 | 44 | public function write(Io\WriteBytes $writer): void 45 | { 46 | $writer 47 | ->writeUint16($this->reserved1) 48 | ->writeString($this->queue) 49 | ->writeBits($this->ifUnused, $this->ifEmpty, $this->noWait); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueDeleteOk.php: -------------------------------------------------------------------------------- 1 | readUint32()); 25 | } 26 | 27 | public function write(Io\WriteBytes $writer): void 28 | { 29 | $writer->writeUint32($this->messages); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueuePurge.php: -------------------------------------------------------------------------------- 1 | readUint16(); 28 | $queue = $reader->readString(); 29 | \assert($queue !== '', 'queue must not be empty'); 30 | 31 | $noWait = $reader->readBits(1)[0]; 32 | 33 | return new self( 34 | $queue, 35 | $noWait, 36 | $reserved1, 37 | ); 38 | } 39 | 40 | public function write(Io\WriteBytes $writer): void 41 | { 42 | $writer 43 | ->writeUint16($this->reserved1) 44 | ->writeString($this->queue) 45 | ->writeBits($this->noWait); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueuePurgeOk.php: -------------------------------------------------------------------------------- 1 | readUint32()); 25 | } 26 | 27 | public function write(Io\WriteBytes $writer): void 28 | { 29 | $writer->writeUint32($this->messages); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueUnbind.php: -------------------------------------------------------------------------------- 1 | $arguments 18 | * @param non-negative-int $reserved1 19 | */ 20 | public function __construct( 21 | public string $queue, 22 | public string $exchange, 23 | public string $routingKey, 24 | public array $arguments = [], 25 | public int $reserved1 = 0, 26 | ) {} 27 | 28 | public static function read(Io\ReadBytes $reader): self 29 | { 30 | $reserved1 = $reader->readUint16(); 31 | $queue = $reader->readString(); 32 | \assert($queue !== '', 'queue must not be empty.'); 33 | 34 | $exchange = $reader->readString(); 35 | $routingKey = $reader->readString(); 36 | $arguments = $reader->readTable(); 37 | 38 | return new self( 39 | $queue, 40 | $exchange, 41 | $routingKey, 42 | $arguments, 43 | $reserved1, 44 | ); 45 | } 46 | 47 | public function write(Io\WriteBytes $writer): void 48 | { 49 | $writer 50 | ->writeUint16($this->reserved1) 51 | ->writeString($this->queue) 52 | ->writeString($this->exchange) 53 | ->writeString($this->routingKey) 54 | ->writeTable($this->arguments); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Frame/QueueUnbindOk.php: -------------------------------------------------------------------------------- 1 | writeUint8(FrameType::header->value) 37 | ->writeUint16($this->channelId) 38 | ->writeUint32(14 + $this->properties->size()) 39 | ->writeUint16($this->classId) 40 | ->writeUint16($this->weight) 41 | ->writeUint64($this->properties->bodyLen) 42 | ->writeUint16($mask = $this->properties->mask()); 43 | 44 | $this->properties 45 | ->write($writer, $mask) 46 | ->writeUint8(Protocol::FRAME_END); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Heartbeat.php: -------------------------------------------------------------------------------- 1 | writeUint8(FrameType::heartbeat->value) 25 | ->writeUint16(0) 26 | ->writeUint32(0) 27 | ->writeUint8(Protocol::FRAME_END); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Protocol.php: -------------------------------------------------------------------------------- 1 | >> */ 21 | private const METHODS = [ 22 | ClassType::CONNECTION => [ 23 | ClassMethod::CONNECTION_START => Frame\ConnectionStart::class, 24 | ClassMethod::CONNECTION_TUNE => Frame\ConnectionTune::class, 25 | ClassMethod::CONNECTION_OPEN_OK => Frame\ConnectionOpenOk::class, 26 | ClassMethod::CONNECTION_CLOSE => Frame\ConnectionClose::class, 27 | ClassMethod::CONNECTION_CLOSE_OK => Frame\ConnectionCloseOk::class, 28 | ], 29 | ClassType::CHANNEL => [ 30 | ClassMethod::CHANNEL_OPEN_OK => Frame\ChannelOpenOkFrame::class, 31 | ClassMethod::CHANNEL_CLOSE => Frame\ChannelClose::class, 32 | ClassMethod::CHANNEL_CLOSE_OK => Frame\ChannelCloseOk::class, 33 | ClassMethod::CHANNEL_FLOW_OK => Frame\ChannelFlowOk::class, 34 | ], 35 | ClassType::EXCHANGE => [ 36 | ClassMethod::EXCHANGE_DECLARE_OK => Frame\ExchangeDeclareOk::class, 37 | ClassMethod::EXCHANGE_BIND_OK => Frame\ExchangeBindOk::class, 38 | ClassMethod::EXCHANGE_UNBIND_OK => Frame\ExchangeUnbindOk::class, 39 | ClassMethod::EXCHANGE_DELETE_OK => Frame\ExchangeDeleteOk::class, 40 | ], 41 | ClassType::QUEUE => [ 42 | ClassMethod::QUEUE_DECLARE_OK => Frame\QueueDeclareOk::class, 43 | ClassMethod::QUEUE_BIND_OK => Frame\QueueBindOk::class, 44 | ClassMethod::QUEUE_UNBIND_OK => Frame\QueueUnbindOk::class, 45 | ClassMethod::QUEUE_PURGE_OK => Frame\QueuePurgeOk::class, 46 | ClassMethod::QUEUE_DELETE_OK => Frame\QueueDeleteOk::class, 47 | ], 48 | ClassType::TX => [ 49 | ClassMethod::TX_SELECT_OK => Frame\TxSelectOk::class, 50 | ClassMethod::TX_COMMIT_OK => Frame\TxCommitOk::class, 51 | ClassMethod::TX_ROLLBACK_OK => Frame\TxRollbackOk::class, 52 | ], 53 | ClassType::CONFIRM => [ 54 | ClassMethod::CONFIRM_SELECT_OK => Frame\ConfirmSelectOk::class, 55 | ], 56 | ClassType::BASIC => [ 57 | ClassMethod::BASIC_GET_EMPTY => Frame\BasicGetEmpty::class, 58 | ClassMethod::BASIC_GET_OK => Frame\BasicGetOk::class, 59 | ClassMethod::BASIC_RECOVER_OK => Frame\BasicRecoverOk::class, 60 | ClassMethod::BASIC_QOS_OK => Frame\BasicQosOk::class, 61 | ClassMethod::BASIC_CONSUME_OK => Frame\BasicConsumeOk::class, 62 | ClassMethod::BASIC_DELIVER => Frame\BasicDeliver::class, 63 | ClassMethod::BASIC_CANCEL_OK => Frame\BasicCancelOk::class, 64 | ClassMethod::BASIC_ACK => Frame\BasicAck::class, 65 | ClassMethod::BASIC_NACK => Frame\BasicNack::class, 66 | ClassMethod::BASIC_REJECT => Frame\BasicReject::class, 67 | ClassMethod::BASIC_RETURN => Frame\BasicReturn::class, 68 | ], 69 | ]; 70 | 71 | public function parseMethod(Io\ReadBytes $reader): Frame 72 | { 73 | $classId = $reader->readUint16(); 74 | $methodId = $reader->readUint16(); 75 | 76 | return (self::METHODS[$classId][$methodId] ?? throw UnsupportedClassMethod::forClassMethod($classId, $methodId))::read($reader); 77 | } 78 | 79 | public function parseHeader(Io\ReadBytes $reader): Frame 80 | { 81 | return Frame\ContentHeader::read($reader); 82 | } 83 | 84 | public function parseBody(Io\ReadBytes $reader): Frame 85 | { 86 | return Frame\ContentBody::read($reader); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Reader.php: -------------------------------------------------------------------------------- 1 | buffer = Io\Buffer::empty(); 25 | } 26 | 27 | /** 28 | * @throws \Throwable 29 | */ 30 | public function read(): Request 31 | { 32 | $this->buffer->write($this->reader->read(self::HEADER_SIZE)); 33 | 34 | $type = FrameType::from($this->buffer->readUint8()); 35 | $channelId = $this->buffer->readUint16(); 36 | 37 | if (($size = $this->buffer->readUint32()) > 0) { 38 | $this->buffer->write($this->reader->read($size)); 39 | } 40 | 41 | $frame = match ($type) { 42 | FrameType::method => Protocol::amqp091->parseMethod($this->buffer), 43 | FrameType::header => Protocol::amqp091->parseHeader($this->buffer), 44 | FrameType::body => Protocol::amqp091->parseBody($this->buffer), 45 | FrameType::heartbeat => Heartbeat::frame, 46 | }; 47 | 48 | if ($this->reader->readUint8() !== Protocol::FRAME_END) { 49 | throw new FrameIsBroken(); 50 | } 51 | 52 | return new Request($channelId, $frame); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Internal/Protocol/Request.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class QueueIterator implements Iterator 18 | { 19 | /** 20 | * @template E 21 | * @param non-empty-string $consumerTag 22 | * @param non-negative-int $size 23 | * @return self 24 | * @phpstan-ignore method.templateTypeNotInParameter 25 | */ 26 | public static function buffered( 27 | string $consumerTag, 28 | Channel $channel, 29 | int $size, 30 | ): self { 31 | /** @var Pipeline\Queue $queue */ 32 | $queue = new Pipeline\Queue(bufferSize: $size); 33 | 34 | return new self($queue, $channel, $consumerTag); 35 | } 36 | 37 | /** @var Pipeline\ConcurrentIterator */ 38 | private Pipeline\ConcurrentIterator $iterator; 39 | 40 | /** 41 | * @param Pipeline\Queue $queue 42 | * @param non-empty-string $consumerTag 43 | */ 44 | private function __construct( 45 | private Pipeline\Queue $queue, 46 | private Channel $channel, 47 | private string $consumerTag, 48 | ) { 49 | $this->iterator = $this->queue->iterate(); 50 | } 51 | 52 | public function push(mixed $delivery): void 53 | { 54 | $this->queue->push($delivery); 55 | } 56 | 57 | public function complete(bool $noWait = false): void 58 | { 59 | $this->channel->cancel($this->consumerTag, $noWait); 60 | $this->queue->complete(); 61 | } 62 | 63 | public function cancel(\Throwable $e, bool $noWait = false): void 64 | { 65 | $this->channel->cancel($this->consumerTag, $noWait); 66 | $this->queue->error($e); 67 | } 68 | 69 | public function continue(?Cancellation $cancellation = null): bool 70 | { 71 | return $this->iterator->continue($cancellation); 72 | } 73 | 74 | public function value(): mixed 75 | { 76 | return $this->iterator->getValue(); 77 | } 78 | 79 | public function getIterator(): \Traversable 80 | { 81 | yield from $this->iterator; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Internal/Returns/FutureBoundedReturnListener.php: -------------------------------------------------------------------------------- 1 | > */ 21 | private array $futures = []; 22 | 23 | /** @var \Closure(): non-empty-string */ 24 | private readonly \Closure $mandatoryIdGenerator; 25 | 26 | /** 27 | * @param ?\Closure(): non-empty-string $mandatoryIdGenerator 28 | */ 29 | public function __construct( 30 | private readonly DeliverySupervisor $supervisor, 31 | ?\Closure $mandatoryIdGenerator = null, 32 | ) { 33 | $this->mandatoryIdGenerator = $mandatoryIdGenerator ?: static fn(): string => bin2hex(random_bytes(10)); 34 | } 35 | 36 | public function listen(): void 37 | { 38 | $futures = &$this->futures; 39 | 40 | $this->supervisor->addReturnListener(static function (DeliveryMessage $delivery) use (&$futures): void { 41 | if (($correlationId = ($delivery->message->headers[self::TRACE_HEADER_KEY] ?? null)) !== null) { 42 | try { 43 | ($futures[$correlationId] ?? null)?->error(new MessageCannotBeRouted()); 44 | } finally { 45 | unset($futures[$correlationId]); 46 | } 47 | } 48 | }); 49 | 50 | $this->supervisor->addShutdownListener(static function () use (&$futures): void { 51 | $futures = []; 52 | }); 53 | } 54 | 55 | /** 56 | * @return array{Message, ReturnFuture} 57 | */ 58 | public function trace(Message $message): array 59 | { 60 | $uniqueId = ($this->mandatoryIdGenerator)(); 61 | 62 | /** @var DeferredFuture $deferred */ 63 | $deferred = new DeferredFuture(); 64 | $this->futures[$uniqueId] = $deferred; 65 | 66 | $message = new Message( 67 | body: $message->body, 68 | headers: array_merge($message->headers, [self::TRACE_HEADER_KEY => $uniqueId]), 69 | contentType: $message->contentType, 70 | contentEncoding: $message->contentEncoding, 71 | deliveryMode: $message->deliveryMode, 72 | priority: $message->priority, 73 | correlationId: $message->correlationId, 74 | replyTo: $message->replyTo, 75 | expiration: $message->expiration, 76 | messageId: $message->messageId, 77 | timestamp: $message->timestamp, 78 | type: $message->type, 79 | userId: $message->userId, 80 | appId: $message->appId, 81 | ); 82 | 83 | $returnFuture = new ReturnFuture( 84 | $deferred->getFuture(), 85 | function () use ($uniqueId): void { 86 | unset($this->futures[$uniqueId]); 87 | }, 88 | ); 89 | 90 | return [$message, $returnFuture]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Internal/Returns/ReturnFuture.php: -------------------------------------------------------------------------------- 1 | $future 16 | * @param \Closure(): void $complete 17 | */ 18 | public function __construct( 19 | public Future $future, 20 | private \Closure $complete, 21 | ) {} 22 | 23 | public function complete(): void 24 | { 25 | ($this->complete)(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Internal/Returns/ReturnListener.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $onReturnCallbacks = []; 18 | 19 | public function __construct(DeliverySupervisor $supervisor) 20 | { 21 | $callbacks = &$this->onReturnCallbacks; 22 | 23 | $supervisor->addReturnListener(static function (DeliveryMessage $delivery) use (&$callbacks): void { 24 | if (!isset($delivery->message->headers[FutureBoundedReturnListener::TRACE_HEADER_KEY])) { 25 | foreach ($callbacks as $callback) { 26 | $callback($delivery); 27 | } 28 | } 29 | }); 30 | 31 | $supervisor->addShutdownListener(static function () use (&$callbacks): void { 32 | $callbacks = []; 33 | }); 34 | } 35 | 36 | /** 37 | * @param ReturnCallback $callback 38 | */ 39 | public function addReturnCallback(callable $callback): void 40 | { 41 | $this->onReturnCallbacks[] = $callback; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/VersionProvider.php: -------------------------------------------------------------------------------- 1 | > 11 | */ 12 | function parseQuery(string $query): array 13 | { 14 | $items = []; 15 | 16 | foreach (queryIterator($query) as $name => $value) { 17 | if (!isset($items[$name])) { 18 | $items[$name] = $value; 19 | } else { 20 | /** @phpstan-ignore-next-line booleanNot.alwaysTrue */ 21 | if (!\is_array($items[$name])) { 22 | $items[$name] = [$items[$name]]; 23 | } 24 | 25 | $items[$name][] = $value; 26 | } 27 | } 28 | 29 | return $items; 30 | } 31 | 32 | /** 33 | * @internal 34 | * @param non-empty-string $query 35 | * @return \Generator 36 | */ 37 | function queryIterator(string $query): \Generator 38 | { 39 | $pairs = explode('&', $query); 40 | 41 | foreach ($pairs as $pair) { 42 | $it = explode('=', $pair, 2); 43 | 44 | if (\count($it) === 2) { 45 | [$name, $value] = [$it[0], urldecode($it[1])]; 46 | 47 | if ($name !== '' && $value !== '') { 48 | yield $name => $value; 49 | } 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * @internal 56 | * @param positive-int $length 57 | * @return iterable 58 | */ 59 | function chunks(string $v, int $length): iterable 60 | { 61 | foreach (str_split($v, $length) as $chunk) { 62 | if ($chunk !== '') { 63 | yield $chunk; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Iterator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface Iterator extends \IteratorAggregate 16 | { 17 | /** 18 | * @throws \Throwable 19 | */ 20 | public function complete(bool $noWait = false): void; 21 | 22 | /** 23 | * @throws \Throwable 24 | */ 25 | public function cancel(\Throwable $e, bool $noWait = false): void; 26 | 27 | /** 28 | * @throws CancelledException 29 | */ 30 | public function continue(?Cancellation $cancellation = null): bool; 31 | 32 | /** 33 | * @return T 34 | */ 35 | public function value(): mixed; 36 | } 37 | -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | $headers 16 | * @param ?int<0, 9> $priority 17 | * @param ?non-empty-string $correlationId 18 | */ 19 | public function __construct( 20 | public string $body = '', 21 | public array $headers = [], 22 | public ?string $contentType = null, 23 | public ?string $contentEncoding = null, 24 | public DeliveryMode $deliveryMode = DeliveryMode::Whatever, 25 | public ?int $priority = null, 26 | public ?string $correlationId = null, 27 | public ?string $replyTo = null, 28 | public ?TimeSpan $expiration = null, 29 | public ?string $messageId = null, 30 | public ?\DateTimeImmutable $timestamp = null, 31 | public ?string $type = null, 32 | public ?string $userId = null, 33 | public ?string $appId = null, 34 | ) {} 35 | } 36 | -------------------------------------------------------------------------------- /src/PublishBatchConfirmation.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final readonly class PublishBatchConfirmation implements \IteratorAggregate 12 | { 13 | /** 14 | * @param array $messages 15 | * @param list $confirmations 16 | */ 17 | public function __construct( 18 | public array $messages, 19 | public array $confirmations, 20 | ) {} 21 | 22 | public function await(): PublishBatchConfirmationResult 23 | { 24 | $unconfirmed = []; 25 | $unrouted = []; 26 | 27 | foreach (PublishConfirmation::awaitAll($this->confirmations) as $deliveryTag => $publishResult) { 28 | if ($publishResult === PublishResult::Unrouted) { 29 | $unrouted[] = $this->messages[$deliveryTag]; 30 | } elseif ($publishResult !== PublishResult::Acked) { 31 | $unconfirmed[] = $this->messages[$deliveryTag]; 32 | } 33 | } 34 | 35 | return new PublishBatchConfirmationResult($unconfirmed, $unrouted); 36 | } 37 | 38 | public function getIterator(): \Traversable 39 | { 40 | yield from $this->confirmations; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PublishBatchConfirmationResult.php: -------------------------------------------------------------------------------- 1 | $unconfirmed 14 | * @param list $unrouted 15 | */ 16 | public function __construct( 17 | public array $unconfirmed, 18 | public array $unrouted, 19 | ) {} 20 | 21 | public function ensureAllPublished(): void 22 | { 23 | $failedCount = \count($this->unconfirmed) + \count($this->unrouted); 24 | 25 | if ($failedCount > 0) { 26 | throw new \RuntimeException(\sprintf( 27 | 'Failed to publish %d message%s.', 28 | $failedCount, 29 | $failedCount === 1 ? '' : 's', 30 | )); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PublishConfirmation.php: -------------------------------------------------------------------------------- 1 | $confirmations 20 | * @return array 21 | */ 22 | public static function awaitAll(iterable $confirmations, ?Cancellation $cancellation = null): array 23 | { 24 | $publishResults = []; 25 | 26 | foreach (self::iterate($confirmations, $cancellation) as $deliveryTag => $future) { 27 | $publishResults[$deliveryTag] = $future->await($cancellation); 28 | } 29 | 30 | return $publishResults; 31 | } 32 | 33 | /** 34 | * @param iterable $confirmations 35 | * @return iterable> 36 | */ 37 | public static function iterate(iterable $confirmations, ?Cancellation $cancellation = null): iterable 38 | { 39 | $futures = []; 40 | foreach ($confirmations as $confirmation) { 41 | $futures[$confirmation->deliveryTag] = async($confirmation->await(...)); 42 | } 43 | 44 | return Future::iterate($futures, $cancellation); 45 | } 46 | 47 | private PublishResult $result = PublishResult::Waiting; 48 | 49 | /** 50 | * @param non-negative-int $deliveryTag 51 | * @param Future $future 52 | * @param \Closure(): void $cancel 53 | */ 54 | public function __construct( 55 | public readonly int $deliveryTag, 56 | private readonly Future $future, 57 | private readonly \Closure $cancel, 58 | private readonly ?ReturnFuture $returnFuture = null, 59 | ) {} 60 | 61 | public function await(?Cancellation $cancellation = null): PublishResult 62 | { 63 | if ($this->result !== PublishResult::Waiting) { 64 | return $this->result; 65 | } 66 | 67 | $futures = [$this->future]; 68 | 69 | if ($this->returnFuture !== null) { 70 | $futures[] = $this->returnFuture->future; 71 | } 72 | 73 | $cancellationId = $cancellation?->subscribe($this->cancel(...)); 74 | 75 | try { 76 | return $this->result = Future\awaitFirst($futures, $cancellation); 77 | } catch (MessageCannotBeRouted) { // @phpstan-ignore-line 78 | return $this->result = PublishResult::Unrouted; 79 | } finally { 80 | /** @phpstan-ignore argument.type */ 81 | $cancellation?->unsubscribe($cancellationId); 82 | $this->returnFuture?->complete(); 83 | } 84 | } 85 | 86 | public function result(): PublishResult 87 | { 88 | return $this->result; 89 | } 90 | 91 | public function cancel(): void 92 | { 93 | ($this->cancel)(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/PublishMessage.php: -------------------------------------------------------------------------------- 1 | */ 19 | private ?Sync\Once $channel = null; 20 | 21 | /** @var non-empty-string */ 22 | private string $replyTo; 23 | 24 | /** @var array> */ 25 | private array $futures = []; 26 | 27 | public function __construct( 28 | private readonly Client $client, 29 | private readonly RpcConfig $config = new RpcConfig(), 30 | ) {} 31 | 32 | public function request( 33 | Message $message, 34 | string $exchange = '', 35 | string $routingKey = '', 36 | bool $mandatory = false, 37 | bool $immediate = false, 38 | ?Cancellation $cancellation = null, 39 | ): Message { 40 | $channel = $this->channel($cancellation); 41 | 42 | $correlationId = $message->correlationId ?? self::generateId(); 43 | 44 | /** @var ?DeferredFuture $deferred */ 45 | $deferred = $this->futures[$correlationId] ?? null; 46 | 47 | if ($deferred === null) { 48 | /** @var DeferredFuture $deferred */ 49 | $deferred = new DeferredFuture(); 50 | $this->futures[$correlationId] = $deferred; 51 | 52 | $channel 53 | ->publish( 54 | message: $this->createMessage($message, $correlationId), 55 | exchange: $exchange, 56 | routingKey: $routingKey, 57 | mandatory: $mandatory, 58 | immediate: $immediate, 59 | ) 60 | ?->await() 61 | ->ensurePublished(); 62 | } 63 | 64 | $cancellation ??= new TimeoutCancellation($this->config->timeout->toSeconds()); 65 | 66 | try { 67 | return $deferred->getFuture()->await($cancellation); 68 | } finally { 69 | unset($this->futures[$correlationId]); 70 | } 71 | } 72 | 73 | public function close(?Cancellation $cancellation = null): void 74 | { 75 | [$channel, $this->channel] = [$this->channel, null]; 76 | $channel?->await($cancellation)->close(cancellation: $cancellation); 77 | } 78 | 79 | /** 80 | * @param non-empty-string $correlationId 81 | */ 82 | private function createMessage(Message $message, string $correlationId): Message 83 | { 84 | return new Message( 85 | body: $message->body, 86 | headers: $message->headers, 87 | contentType: $message->contentType, 88 | contentEncoding: $message->contentEncoding, 89 | deliveryMode: $message->deliveryMode, 90 | priority: $message->priority, 91 | correlationId: $correlationId, 92 | replyTo: $this->replyTo, 93 | expiration: $message->expiration, 94 | messageId: $message->messageId, 95 | timestamp: $message->timestamp, 96 | type: $message->type, 97 | userId: $message->userId, 98 | appId: $message->appId, 99 | ); 100 | } 101 | 102 | private function channel(?Cancellation $cancellation = null): Channel 103 | { 104 | return ($this->channel ??= new Sync\Once(weakClosure($this->setup(...)), static fn(Channel $channel): bool => !$channel->isClosed()))->await($cancellation); 105 | } 106 | 107 | private function setup(): Channel 108 | { 109 | $channel = $this->client->channel(); 110 | 111 | if ($this->config->confirms) { 112 | $channel->confirmSelect(); 113 | } 114 | 115 | $this->replyTo = $channel 116 | ->queueDeclare( 117 | queue: ($this->config->generateReplyTo ?? self::generateReplyTo(...))(), 118 | exclusive: true, 119 | autoDelete: true, 120 | ) 121 | ->name; 122 | 123 | $channel->consume( 124 | callback: function (DeliveryMessage $delivery): void { 125 | ($this->futures[$delivery->message->correlationId ?? ''] ?? null)?->complete($delivery->message); 126 | }, 127 | queue: $this->replyTo, 128 | noAck: true, 129 | ); 130 | 131 | return $channel; 132 | } 133 | 134 | /** 135 | * @return non-empty-string 136 | */ 137 | private static function generateReplyTo(): string 138 | { 139 | return 'thesis.rpc.' . self::generateId(); 140 | } 141 | 142 | /** 143 | * @return non-empty-string 144 | */ 145 | private static function generateId(): string 146 | { 147 | return bin2hex(random_bytes(length: 15)); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/RpcConfig.php: -------------------------------------------------------------------------------- 1 | timeout = $timeout ?? TimeSpan::fromSeconds(self::DEFAULT_TIMEOUT); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Scheme.php: -------------------------------------------------------------------------------- 1 |