├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon ├── publish └── queue.php └── src ├── AsyncCallback.php ├── Common └── XMLParser.php ├── Config.php ├── ConfigProvider.php ├── Constants.php ├── Exception ├── AckMessageException.php ├── InvalidArgumentException.php ├── MQException.php ├── MalformedXMLException.php ├── MessageNotExistException.php ├── ReceiptHandleErrorException.php └── TopicNotExistException.php ├── Http └── HttpClient.php ├── MQClient.php ├── MQConsumer.php ├── MQProducer.php ├── MQTransProducer.php ├── Model ├── AckMessageErrorItem.php ├── Message.php └── TopicMessage.php ├── Requests ├── AckMessageRequest.php ├── BaseRequest.php ├── ConsumeMessageRequest.php └── PublishMessageRequest.php ├── Responses ├── AckMessageResponse.php ├── BaseResponse.php ├── ConsumeMessageResponse.php ├── MQPromise.php └── PublishMessageResponse.php ├── Signature └── Signature.php └── Traits ├── MessagePropertiesForConsume.php └── MessagePropertiesForPublish.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.{vue,js,scss}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #忽略电脑中的文件 2 | .DS_Store 3 | .idea/* 4 | ide/* 5 | Ide/* 6 | vendor 7 | composer.lock 8 | .php_cs.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mingzhil 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 使用说明 2 | 3 | 主要代码来自于阿里云官方 sdk:`https://github.com/aliyunmq/mq-http-php-sdk` 4 | 5 | hyperf rocketmq 目前只支持 http/https 6 | 7 | ## 伪代码 demo 8 | 9 | ### 配置文件 10 | ```php 11 | [ 19 | 'host' => env('ROCKETMQ_HOST'), 20 | 'app_id' => env('ROCKETMQ_APP_ID'), 21 | 'app_key' => env('ROCKETMQ_APP_KEY'), 22 | 'driver' => 'rocketmq', 23 | 'instance_id' => 'MQ_INST_199174xx', 24 | 'topic' => [ 25 | 'user_topic' => env('ROCKETMQ_USER_TOPIC', 'user_staging'), 26 | ], 27 | 'http_guzzle' => [ 28 | 'handler' => PoolHandler::class, 29 | 'option' => [ 30 | 'min_connections' => 10, 31 | 'max_connections' => 100, 32 | 'connect_timeout' => 3.0, 33 | 'wait_timeout' => 30.0, 34 | 'heartbeat' => -1, 35 | 'max_idle_time' => 60.0, 36 | ], 37 | ], 38 | ], 39 | ]; 40 | ``` 41 | ### 配置类 42 | 43 | ```php 44 | class Config 45 | { 46 | private $params = [ 47 | 'host', 48 | 'app_id', 49 | 'app_key', 50 | 'driver', 51 | 'instance_id', 52 | 'topic', 53 | 'group_id', 54 | 'config', 55 | ]; 56 | 57 | /** 58 | * @var Collection 59 | */ 60 | public $config; 61 | 62 | public function __construct(string $clientId) 63 | { 64 | $config = config("queue.$clientId"); 65 | if (!$config) { 66 | throw new ServiceException(sprintf('%s config info error ', $clientId)); 67 | } 68 | $this->init($config); 69 | } 70 | 71 | /** 72 | * 初始化项目信息 73 | * 74 | * @param $config 75 | */ 76 | private function init($config) 77 | { 78 | $this->config = collect($config); 79 | $this->host = $config['host'] ?? ''; 80 | $this->app_id = $config['app_id'] ?? ''; 81 | $this->app_key = $config['app_key'] ?? ''; 82 | $this->driver = $config['driver'] ?? ''; 83 | $this->instance_id = $config['instance_id'] ?? ''; 84 | $this->topic = $config['topic'] ?? []; 85 | $this->group_id = $config['group_id'] ?? []; 86 | } 87 | 88 | /** 89 | * 获取config 90 | * 91 | * @param string $clientId 92 | * 93 | * @return Config 94 | */ 95 | public function getConfig(string $clientId): Config 96 | { 97 | $config = $this->config->get('queue.' . $clientId); 98 | 99 | if (!$config) { 100 | throw new ServiceException(sprintf('%s queue config info error ', $clientId)); 101 | } 102 | $this->config = collect($config); 103 | 104 | $this->host = $config['host'] ?? ''; 105 | $this->app_id = $config['app_id'] ?? ''; 106 | $this->app_key = $config['app_key'] ?? ''; 107 | $this->driver = $config['driver'] ?? ''; 108 | $this->instance_id = $config['instance_id'] ?? ''; 109 | $this->topic = $config['topic'] ?? []; 110 | $this->group_id = $config['group_id'] ?? []; 111 | 112 | return $this; 113 | } 114 | 115 | public function __get($name) 116 | { 117 | if (!in_array($name, $this->params)) { 118 | throw new ServiceException('error param', ['name' => $name]); 119 | } 120 | return Context::get(__CLASS__ . ':' . $name); 121 | } 122 | 123 | public function __set($name, $value) 124 | { 125 | if (!in_array($name, $this->params)) { 126 | throw new ServiceException('error param', [$name => $value]); 127 | } 128 | return Context::set(__CLASS__ . ':' . $name, $value); 129 | } 130 | 131 | /** 132 | * Get the value of host 133 | */ 134 | public function getHost(): string 135 | { 136 | return $this->host; 137 | } 138 | 139 | /** 140 | * Get the value of app_id 141 | */ 142 | public function getAppId(): string 143 | { 144 | return $this->app_id; 145 | } 146 | 147 | /** 148 | * Get the value of app_key 149 | */ 150 | public function getAppKey(): string 151 | { 152 | return $this->app_key; 153 | } 154 | 155 | /** 156 | * Get the value of driver 157 | */ 158 | public function getDriver(): string 159 | { 160 | return $this->driver; 161 | } 162 | 163 | /** 164 | * Get the value of instance_id 165 | */ 166 | public function getInstanceId(): string 167 | { 168 | return $this->instance_id; 169 | } 170 | 171 | public function getTopic(string $topicName) 172 | { 173 | return $this->topic[$topicName] ?? ''; 174 | } 175 | 176 | public function getGroupId(string $groupId) 177 | { 178 | return $this->group_id[$groupId] ?? ''; 179 | } 180 | } 181 | ``` 182 | 183 | ```php 184 | end_point = $config->getHost(); 240 | $this->access_id = $config->getAppId(); 241 | $this->access_key = $config->getAppKey(); 242 | $this->instance_id = $config->getInstanceId(); 243 | $this->config = $config; 244 | $this->client = new MQClient($this->end_point, $this->access_id, $this->access_key, null, $this->getMqConfig($config)); 245 | } 246 | 247 | /** 248 | * 配置文件转换 249 | * 250 | * @param Config $config 251 | * @return \TheFairLib\RocketMQ\Config 252 | */ 253 | protected function getMqConfig(Config $config): ?\TheFairLib\RocketMQ\Config 254 | { 255 | $mqConfig = new \TheFairLib\RocketMQ\Config(); 256 | $config = $config->config->toArray(); 257 | if (arrayGet($config, 'http_guzzle.handler') == PoolHandler::class) { 258 | $option = arrayGet($config, 'http_guzzle.option'); 259 | $mqConfig->setConnectTimeout($option['connect_timeout'] ?? 3.0); 260 | $mqConfig->setRequestTimeout($option['wait_timeout'] ?? 30.0); 261 | // $mqConfig->setHandler(HandlerStack::create(new CoroutineHandler())); 262 | $mqConfig->setHandler(make(PoolHandler::class, [ 263 | 'option' => [ 264 | 'min_connections' => $option['min_connections'] ?? 10, 265 | 'max_connections' => $option['max_connections'] ?? 100, 266 | 'connect_timeout' => $option['connect_timeout'] ?? 3.0, 267 | 'wait_timeout' => $option['wait_timeout'] ?? 30.0, 268 | 'heartbeat' => $option['heartbeat'] ?? -1, 269 | 'max_idle_time' => $option['max_idle_time'] ?? 60.0, 270 | ], 271 | ])); 272 | } 273 | return $mqConfig; 274 | } 275 | 276 | public function __get($name) 277 | { 278 | if (!in_array($name, $this->params)) { 279 | throw new ServiceException('error param', ['name' => $name]); 280 | } 281 | return Context::get(__CLASS__ . ':' . $name); 282 | } 283 | 284 | public function __set($name, $value) 285 | { 286 | if (!in_array($name, $this->params)) { 287 | throw new ServiceException('error param', [$name => $value]); 288 | } 289 | return Context::set(__CLASS__ . ':' . $name, $value); 290 | } 291 | 292 | /** 293 | * Get the value of producer 294 | * 295 | * @param string $topic 296 | * 297 | * @return MQProducer 298 | */ 299 | public function getProducer(string $topic): MQProducer 300 | { 301 | return $this->client->getProducer($this->instance_id, $topic); 302 | } 303 | 304 | /** 305 | * 推入队列 306 | * 307 | * @param string $topic 308 | * @param array $message 309 | * 310 | * @return TopicMessage 311 | */ 312 | public function publishMessage(string $topic, array $message): TopicMessage 313 | { 314 | $producer = $this->getProducer($topic); 315 | 316 | $publishMessage = new TopicMessage("将 $message 转为 string"); 317 | $publishMessage->setMessageKey($message->getMessageType()); 318 | 319 | if ($message->getStartDeliverTime()) { 320 | $publishMessage->setStartDeliverTime($message->getStartDeliverTime()); 321 | } 322 | 323 | if ($message->getMessageTag()) { 324 | $publishMessage->setMessageTag($message->getMessageTag()); 325 | } 326 | 327 | return $producer->publishMessage($publishMessage); 328 | } 329 | 330 | /** 331 | * Get the value of consumer 332 | * 333 | * @param string $topic 334 | * @param string $groupId 335 | * @param string|null $messageTag 336 | * 337 | * @return MQConsumer 338 | */ 339 | public function getConsumer(string $topic, string $groupId, string $messageTag = null): MQConsumer 340 | { 341 | return $this->client->getConsumer($this->instance_id, $topic, $groupId, $messageTag); 342 | } 343 | 344 | /** 345 | * 消费队列 346 | * 347 | * @param string $topic 348 | * @param string $groupId 349 | * @param callable $func 350 | * @param string|null $messageTag 351 | * @param int $numOfMessages 1~16 352 | * @param int $waitSeconds 353 | * @param bool $coroutine 是否开启协程并发消费 354 | * 355 | * @return void 356 | * @throws Throwable 357 | */ 358 | public function consumeMessage(string $topic, string $groupId, callable $func, string $messageTag = null, int $numOfMessages = 1, int $waitSeconds = 5, bool $coroutine = false) 359 | { 360 | $consumer = $this->getConsumer($topic, $groupId, $messageTag); 361 | 362 | while (ProcessManager::isRunning()) { 363 | try { 364 | // 长轮询消费消息 365 | // 长轮询表示如果topic没有消息则请求会在服务端挂住3s,3s内如果有消息可以消费则立即返回 366 | $messages = $consumer->consumeMessage( 367 | $numOfMessages, // 一次最多消费3条(最多可设置为16条) 368 | $waitSeconds // 长轮询时间(最多可设置为30秒) 369 | ); 370 | } catch (Throwable $e) { 371 | if ($e instanceof MessageNotExistException) { 372 | // 队列为空,结束消费,重新轮询 373 | continue; 374 | } 375 | 376 | throw $e; 377 | } 378 | $receiptHandles = []; 379 | if ($coroutine) { 380 | $callback = []; 381 | foreach ($messages as $key => $message) { 382 | $callback[$key] = function () use ($message, $func) { 383 | $func($message); 384 | return $message->getReceiptHandle(); 385 | }; 386 | } 387 | $receiptHandles = parallel($callback); 388 | } else { 389 | foreach ($messages as $message) { 390 | $func($message); 391 | $receiptHandles[] = $message->getReceiptHandle(); 392 | } 393 | } 394 | try { 395 | $consumer->ackMessage($receiptHandles); 396 | } catch (Throwable $e) { 397 | if ($e instanceof AckMessageException) { 398 | // 某些消息的句柄可能超时了会导致确认不成功 399 | Logger::get()->error("ack_error", ['RequestId' => $e->getRequestId()]); 400 | foreach ($e->getAckMessageErrorItems() as $errorItem) { 401 | Logger::get()->error('ack_error:receipt_handle', [ 402 | $errorItem->getReceiptHandle(), $errorItem->getErrorCode(), $errorItem->getErrorCode(), 403 | ]); 404 | } 405 | return; 406 | } 407 | throw $e; 408 | } 409 | } 410 | } 411 | } 412 | ``` 413 | 414 | 415 | ### 使用 416 | 417 | ```php 418 | publishMessage('user_test',[ 422 | 'uid' => 'xx1x' 423 | ]); 424 | 425 | //消费 426 | $client->consumeMessage('topic_name', 'groupId', function (Message $message) { 427 | try { 428 | var_dump($message); 429 | } catch (Throwable $th) { 430 | Logger::get()->error('error#topic:push#id:' . $message->getMessageId(), [ 431 | 'code' => $th->getCode(), 432 | 'message' => $th->getMessage(), 433 | 'trace' => $th->getTraceAsString(), 434 | ]); 435 | } 436 | }); 437 | 438 | 439 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thefair/hyperf-rocketmq", 3 | "type": "library", 4 | "description": "The PHP library for hyperf rocketmq", 5 | "keywords": [ 6 | "rocketmq" 7 | ], 8 | "homepage": "https://github.com/thefair-net/hyperf_rocketmq", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Zhang Han", 13 | "email": "zhanghan@thefair.net.cn" 14 | }, 15 | { 16 | "name": "Liu mingzhi", 17 | "email": "liumingzhi@thefair.net.cn" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.0", 22 | "hyperf/guzzle": "3.0.*", 23 | "ext-xmlreader": "*", 24 | "ext-xmlwriter": "*", 25 | "ext-json": "*" 26 | }, 27 | "require-dev": { 28 | "friendsofphp/php-cs-fixer": "^3.9", 29 | "phpstan/phpstan": "^1.8" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "TheFairLib\\RocketMQ\\": "src/" 34 | } 35 | }, 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "3.0-dev" 39 | }, 40 | "hyperf": { 41 | "config": "TheFairLib\\RocketMQ\\ConfigProvider" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | # Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :) 2 | # Fortunately, You can ingore it by the following config. 3 | # 4 | # vendor/bin/phpstan analyse app --memory-limit 200M -l 0 5 | # 6 | parameters: 7 | excludes_analyse: 8 | # - config/routes.php 9 | # - publish/bin/dev_start.php 10 | # - publish/Constants/InfoCode.php 11 | # - publish/Constants/ServerCode.php 12 | # - publish/test/Cases/ExampleTest.php 13 | # - publish/test/Cases/DocTest.php 14 | # - src/Exception/BusinessException.php 15 | # ignoreErrors: 16 | # - '#Class [a-zA-Z0-9\\_]+ was not found while trying to analyse it - autoloading is probably not configured properly.#' 17 | # 18 | # message: '#[.*]?autoloading is probably not configured properly.#' 19 | # paths: 20 | # - publish/test/* 21 | # - '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#' -------------------------------------------------------------------------------- /publish/queue.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'host' => env('QUEUE_HOST'), 9 | 'app_id' => env('QUEUE_APP_ID'), 10 | 'app_key' => env('QUEUE_APP_KEY'), 11 | 'driver' => 'rocketmq', 12 | 'instance_id' => env('QUEUE_INSTANCE_ID'), 13 | 'topic' => [ 14 | 'push_topic' => 'push' 15 | ], 16 | 'group_id' => [ 17 | 'push_group_id' => 'GID_push', 18 | ], 19 | ], 20 | 21 | ]; 22 | -------------------------------------------------------------------------------- /src/AsyncCallback.php: -------------------------------------------------------------------------------- 1 | succeedCallback = $succeedCallback; 14 | $this->failedCallback = $failedCallback; 15 | } 16 | 17 | public function onSucceed($result) 18 | { 19 | return call_user_func($this->succeedCallback, $result); 20 | } 21 | 22 | public function onFailed(MQException $e) 23 | { 24 | return call_user_func($this->failedCallback, $e); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Common/XMLParser.php: -------------------------------------------------------------------------------- 1 | null, 'Message' => null, 'RequestId' => null, 'HostId' => null); 12 | while ($xmlReader->Read()) { 13 | if ($xmlReader->nodeType == \XMLReader::ELEMENT) { 14 | switch ($xmlReader->name) { 15 | case 'Code': 16 | $xmlReader->read(); 17 | if ($xmlReader->nodeType == \XMLReader::TEXT) { 18 | $result['Code'] = $xmlReader->value; 19 | } 20 | break; 21 | case 'Message': 22 | $xmlReader->read(); 23 | if ($xmlReader->nodeType == \XMLReader::TEXT) { 24 | $result['Message'] = $xmlReader->value; 25 | } 26 | break; 27 | case 'RequestId': 28 | $xmlReader->read(); 29 | if ($xmlReader->nodeType == \XMLReader::TEXT) { 30 | $result['RequestId'] = $xmlReader->value; 31 | } 32 | break; 33 | case 'HostId': 34 | $xmlReader->read(); 35 | if ($xmlReader->nodeType == \XMLReader::TEXT) { 36 | $result['HostId'] = $xmlReader->value; 37 | } 38 | break; 39 | } 40 | } 41 | } 42 | return $result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | proxy = null; 20 | $this->requestTimeout = 30; // 30 seconds 21 | $this->connectTimeout = 3; // 3 seconds 22 | $this->expectContinue = false; 23 | $this->handler = HandlerStack::create(new CoroutineHandler()); 24 | } 25 | 26 | 27 | public function getProxy() 28 | { 29 | return $this->proxy; 30 | } 31 | 32 | public function setProxy($proxy) 33 | { 34 | $this->proxy = $proxy; 35 | } 36 | 37 | public function getRequestTimeout(): int 38 | { 39 | return $this->requestTimeout; 40 | } 41 | 42 | public function setRequestTimeout($requestTimeout) 43 | { 44 | $this->requestTimeout = $requestTimeout; 45 | } 46 | 47 | public function setConnectTimeout($connectTimeout) 48 | { 49 | $this->connectTimeout = $connectTimeout; 50 | } 51 | 52 | public function getConnectTimeout(): int 53 | { 54 | return $this->connectTimeout; 55 | } 56 | 57 | public function getExpectContinue(): bool 58 | { 59 | return $this->expectContinue; 60 | } 61 | 62 | public function setExpectContinue($expectContinue) 63 | { 64 | $this->expectContinue = $expectContinue; 65 | } 66 | 67 | /** 68 | * @return HandlerStack|null|PoolHandler 69 | */ 70 | public function getHandler() 71 | { 72 | return $this->handler; 73 | } 74 | 75 | /** 76 | * @param PoolHandler|HandlerStack $handler 77 | */ 78 | public function setHandler($handler) 79 | { 80 | $this->handler = $handler; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 13 | 14 | ], 15 | 'listeners' => [ 16 | 17 | ], 18 | 'annotations' => [ 19 | 'scan' => [ 20 | 'paths' => [ 21 | __DIR__, 22 | ], 23 | ], 24 | ], 25 | 'publish' => [ 26 | [ 27 | 'id' => 'queue', 28 | 'description' => 'The message queue.php', 29 | 'source' => __DIR__ . '/../publish/queue.php', 30 | 'destination' => BASE_PATH . '/config/autoload/queue.php', 31 | ], 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Constants.php: -------------------------------------------------------------------------------- 1 | ackMessageErrorItems = array(); 21 | } 22 | 23 | public function addAckMessageErrorItem(AckMessageErrorItem $item) 24 | { 25 | $this->ackMessageErrorItems[] = $item; 26 | } 27 | 28 | public function getAckMessageErrorItems() 29 | { 30 | return $this->ackMessageErrorItems; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | = 500) { 16 | $onsErrorCode = "ServerError"; 17 | } else { 18 | $onsErrorCode = "ClientError"; 19 | } 20 | } 21 | $this->onsErrorCode = $onsErrorCode; 22 | 23 | $this->requestId = $requestId; 24 | $this->hostId = $hostId; 25 | } 26 | 27 | public function __toString() 28 | { 29 | $str = "Code: " . $this->getCode() . " Message: " . $this->getMessage(); 30 | if ($this->onsErrorCode != null) { 31 | $str .= " ErrorCode: " . $this->onsErrorCode; 32 | } 33 | if ($this->requestId != null) { 34 | $str .= " RequestId: " . $this->requestId; 35 | } 36 | if ($this->hostId != null) { 37 | $str .= " HostId: " . $this->hostId; 38 | } 39 | return $str; 40 | } 41 | 42 | public function getOnsErrorCode() 43 | { 44 | return $this->onsErrorCode; 45 | } 46 | 47 | public function getRequestId() 48 | { 49 | return $this->requestId; 50 | } 51 | 52 | public function getHostId() 53 | { 54 | return $this->hostId; 55 | } 56 | 57 | public function setRequestId($requestId) 58 | { 59 | $this->requestId = $requestId; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Exception/MalformedXMLException.php: -------------------------------------------------------------------------------- 1 | accessId = $accessId; 42 | $this->accessKey = $accessKey; 43 | $this->client = new Client([ 44 | 'base_uri' => $endPoint, 45 | 'handler' => $config->getHandler(), 46 | 'timeout' => $config->getRequestTimeout(), 47 | 'defaults' => [ 48 | 'headers' => [ 49 | 'Host' => $endPoint, 50 | ], 51 | 'proxy' => $config->getProxy(), 52 | 'expect' => $config->getExpectContinue(), 53 | ], 54 | ]); 55 | $this->requestTimeout = $config->getRequestTimeout(); 56 | $this->connectTimeout = $config->getConnectTimeout(); 57 | $this->securityToken = $securityToken; 58 | $this->endpoint = $endPoint; 59 | // $this->agent = "mq-php-sdk/1.0.1(GuzzleHttp/" . \GuzzleHttp\Client::MAJOR_VERSION . " PHP/" . PHP_VERSION . ")"; 60 | $guzzleVersion = Client::MAJOR_VERSION; 61 | $this->agent = Constants::CLIENT_VERSION . $guzzleVersion . " PHP/" . PHP_VERSION . ")"; 62 | } 63 | 64 | private function addRequiredHeaders(BaseRequest &$request) 65 | { 66 | $body = $request->generateBody(); 67 | $queryString = $request->generateQueryString(); 68 | 69 | $request->setBody($body); 70 | $request->setQueryString($queryString); 71 | 72 | $request->setHeader(Constants::USER_AGENT, $this->agent); 73 | if ($body != null) { 74 | $request->setHeader(Constants::CONTENT_LENGTH, strlen($body)); 75 | } 76 | $request->setHeader('Date', gmdate(Constants::GMT_DATE_FORMAT)); 77 | if (!$request->isHeaderSet(Constants::CONTENT_TYPE)) { 78 | $request->setHeader(Constants::CONTENT_TYPE, 'text/xml'); 79 | } 80 | $request->setHeader(Constants::VERSION_HEADER, Constants::VERSION_VALUE); 81 | 82 | if ($this->securityToken != null) { 83 | $request->setHeader(Constants::SECURITY_TOKEN, $this->securityToken); 84 | } 85 | 86 | $sign = Signature::SignRequest($this->accessKey, $request); 87 | $request->setHeader( 88 | Constants::AUTHORIZATION, 89 | Constants::AUTH_PREFIX . " " . $this->accessId . ":" . $sign 90 | ); 91 | } 92 | 93 | public function sendRequestAsync( 94 | BaseRequest $request, 95 | BaseResponse &$response, 96 | AsyncCallback $callback = null 97 | ): MQPromise 98 | { 99 | $promise = $this->sendRequestAsyncInternal($request, $response, $callback); 100 | return new MQPromise($promise, $response); 101 | } 102 | 103 | public function sendRequest(BaseRequest $request, BaseResponse &$response) 104 | { 105 | $promise = $this->sendRequestAsync($request, $response); 106 | return $promise->wait(); 107 | } 108 | 109 | /** 110 | * @param BaseRequest $request 111 | * @param BaseResponse $response 112 | * @param AsyncCallback|null $callback 113 | * @return PromiseInterface 114 | */ 115 | private function sendRequestAsyncInternal(BaseRequest &$request, BaseResponse &$response, AsyncCallback $callback = null): PromiseInterface 116 | { 117 | $this->addRequiredHeaders($request); 118 | 119 | $parameters = ['exceptions' => false, 'http_errors' => false]; 120 | $queryString = $request->getQueryString(); 121 | $body = $request->getBody(); 122 | if ($queryString != null) { 123 | $parameters['query'] = $queryString; 124 | } 125 | if ($body != null) { 126 | $parameters['body'] = $body; 127 | } 128 | 129 | $parameters['timeout'] = $this->requestTimeout; 130 | $parameters['connect_timeout'] = $this->connectTimeout; 131 | 132 | $request = new Request( 133 | strtoupper($request->getMethod()), 134 | $request->getResourcePath(), 135 | $request->getHeaders() 136 | ); 137 | try { 138 | if ($callback != null) { 139 | return $this->client->sendAsync($request, $parameters)->then( 140 | function ($res) use (&$response, $callback) { 141 | try { 142 | $response->setRequestId($res->getHeaderLine("x-mq-request-id")); 143 | $callback->onSucceed($response->parseResponse($res->getStatusCode(), $res->getBody())); 144 | } catch (MQException $e) { 145 | $callback->onFailed($e); 146 | } 147 | } 148 | ); 149 | } else { 150 | return $this->client->sendAsync($request, $parameters); 151 | } 152 | } catch (TransferException $e) { 153 | $message = $e->getMessage(); 154 | if (method_exists($e, 'hasResponse') && $e->hasResponse()) { 155 | $message = $e->getResponse()->getBody(); 156 | } 157 | throw new MQException($e->getCode(), $message, $e); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/MQClient.php: -------------------------------------------------------------------------------- 1 | client = new HttpClient( 37 | $endPoint, 38 | $accessId, 39 | $accessKey, 40 | $securityToken, 41 | $config 42 | ); 43 | } 44 | 45 | 46 | /** 47 | * Returns a Producer reference for publish message to topic 48 | * 49 | * @param string $instanceId : instance id 50 | * @param string $topicName : the topic name 51 | * 52 | * @return MQProducer: the Producer instance 53 | */ 54 | public function getProducer(string $instanceId, string $topicName): MQProducer 55 | { 56 | if ($topicName == null || $topicName == "") { 57 | throw new InvalidArgumentException(400, "TopicName is null or empty"); 58 | } 59 | return new MQProducer($this->client, $instanceId, $topicName); 60 | } 61 | 62 | /** 63 | * Returns a Transaction Producer reference for publish message to topic 64 | * 65 | * @param string $instanceId : instance id 66 | * @param string $topicName : the topic name 67 | * @param string $groupId : the group id 68 | * 69 | * @return MQTransProducer: the Transaction Producer instance 70 | */ 71 | public function getTransProducer(string $instanceId, string $topicName, string $groupId): MQTransProducer 72 | { 73 | if ($topicName == null || $topicName == "") { 74 | throw new InvalidArgumentException(400, "TopicName is null or empty"); 75 | } 76 | return new MQTransProducer($this->client, $instanceId, $topicName, $groupId); 77 | } 78 | 79 | /** 80 | * Returns a Consumer reference for consume and ack message to topic 81 | * 82 | * @param string $instanceId : instance id 83 | * @param string $topicName : the topic name 84 | * @param string $consumer : the consumer name / ons cid 85 | * @param string|null $messageTag : filter tag for consumer. If not empty, only consume the message which's messageTag is equal to it. 86 | * 87 | * @return MQConsumer: the Consumer instance 88 | */ 89 | public function getConsumer(string $instanceId, string $topicName, string $consumer, string $messageTag = null): MQConsumer 90 | { 91 | if ($topicName == null || $topicName == "") { 92 | throw new InvalidArgumentException(400, "TopicName is null or empty"); 93 | } 94 | if ($consumer == null || $consumer == "") { 95 | throw new InvalidArgumentException(400, "Consumer is null or empty"); 96 | } 97 | return new MQConsumer($this->client, $instanceId, $topicName, $consumer, $messageTag); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/MQConsumer.php: -------------------------------------------------------------------------------- 1 | instanceId = $instanceId; 44 | $this->topicName = $topicName; 45 | $this->consumer = $consumer; 46 | $this->messageTag = $messageTag; 47 | $this->client = $client; 48 | } 49 | 50 | public function getInstanceId() 51 | { 52 | return $this->instanceId; 53 | } 54 | 55 | public function getTopicName() 56 | { 57 | return $this->topicName; 58 | } 59 | 60 | public function getConsumer() 61 | { 62 | return $this->consumer; 63 | } 64 | 65 | public function getMessageTag() 66 | { 67 | return $this->messageTag; 68 | } 69 | 70 | 71 | /** 72 | * consume message 73 | * 74 | * @param $numOfMessages: consume how many messages once, 1~16 75 | * @param int $waitSeconds: if > 0, means the time(second) the request holden at server if there is no message to consume. 76 | * If <= 0, means the server will response back if there is no message to consume. 77 | * It's value should be 1~30 78 | * 79 | * @return array 80 | * 81 | * @throws TopicNotExistException if queue does not exist 82 | * @throws MessageNotExistException if no message exists 83 | * @throws InvalidArgumentException if the argument is invalid 84 | * @throws MQException if any other exception happends 85 | */ 86 | public function consumeMessage($numOfMessages, int $waitSeconds = -1): array 87 | { 88 | if ($numOfMessages < 0 || $numOfMessages > 16) { 89 | throw new InvalidArgumentException(400, "numOfMessages should be 1~16"); 90 | } 91 | if ($waitSeconds > 30) { 92 | throw new InvalidArgumentException(400, "numOfMessages should less then 30"); 93 | } 94 | $request = new ConsumeMessageRequest($this->instanceId, $this->topicName, $this->consumer, $numOfMessages, $this->messageTag, $waitSeconds); 95 | $response = new ConsumeMessageResponse(); 96 | return $this->client->sendRequest($request, $response); 97 | } 98 | 99 | /** 100 | * consume message orderly 101 | * 102 | * Next messages will be consumed if all of same shard are acked. Otherwise, same messages will be consumed again after NextConsumeTime. 103 | * 104 | * Attention: the topic should be order topic created at console, if not, mq could not keep the order feature. 105 | * 106 | * This interface is suitable for globally order and partitionally order messages, and could be used in multi-thread scenes. 107 | * 108 | * @param $numOfMessages: consume how many messages once, 1~16 109 | * @param int $waitSeconds: if > 0, means the time(second) the request holden at server if there is no message to consume. 110 | * If <= 0, means the server will response back if there is no message to consume. 111 | * It's value should be 1~30 112 | * 113 | * @return Message may contains several shard's messages, the messages of one shard are ordered. 114 | * 115 | * @throws TopicNotExistException if queue does not exist 116 | * @throws MessageNotExistException if no message exists 117 | * @throws InvalidArgumentException if the argument is invalid 118 | * @throws MQException if any other exception happends 119 | */ 120 | public function consumeMessageOrderly($numOfMessages, int $waitSeconds = -1): Message 121 | { 122 | if ($numOfMessages < 0 || $numOfMessages > 16) { 123 | throw new InvalidArgumentException(400, "numOfMessages should be 1~16"); 124 | } 125 | if ($waitSeconds > 30) { 126 | throw new InvalidArgumentException(400, "numOfMessages should less then 30"); 127 | } 128 | $request = new ConsumeMessageRequest($this->instanceId, $this->topicName, $this->consumer, $numOfMessages, $this->messageTag, $waitSeconds); 129 | $request->setTrans(Constants::TRANSACTION_ORDER); 130 | $response = new ConsumeMessageResponse(); 131 | return $this->client->sendRequest($request, $response); 132 | } 133 | 134 | /** 135 | * ack message 136 | * 137 | * @param $receiptHandles: 138 | * array of $receiptHandle, which is got from consumeMessage 139 | * 140 | * @return AckMessageResponse 141 | * 142 | * @throws TopicNotExistException if queue does not exist 143 | * @throws ReceiptHandleErrorException if the receiptHandle is invalid 144 | * @throws InvalidArgumentException if the argument is invalid 145 | * @throws AckMessageException if any message not deleted 146 | * @throws MQException if any other exception happends 147 | */ 148 | public function ackMessage($receiptHandles): ?AckMessageResponse 149 | { 150 | $request = new AckMessageRequest($this->instanceId, $this->topicName, $this->consumer, $receiptHandles); 151 | $response = new AckMessageResponse(); 152 | return $this->client->sendRequest($request, $response); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/MQProducer.php: -------------------------------------------------------------------------------- 1 | instanceId = $instanceId; 22 | $this->client = $client; 23 | $this->topicName = $topicName; 24 | } 25 | 26 | public function getInstanceId() 27 | { 28 | return $this->instanceId; 29 | } 30 | 31 | public function getTopicName() 32 | { 33 | return $this->topicName; 34 | } 35 | 36 | public function publishMessage(TopicMessage $topicMessage) 37 | { 38 | $request = new PublishMessageRequest( 39 | $this->instanceId, 40 | $this->topicName, 41 | $topicMessage->getMessageBody(), 42 | $topicMessage->getProperties(), 43 | $topicMessage->getMessageTag() 44 | ); 45 | $response = new PublishMessageResponse(); 46 | return $this->client->sendRequest($request, $response); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/MQTransProducer.php: -------------------------------------------------------------------------------- 1 | groupId = $groupId; 30 | } 31 | 32 | /** 33 | * consume transaction half message 34 | * 35 | * @param $numOfMessages : consume how many messages once, 1~16 36 | * @param int $waitSeconds : if > 0, means the time(second) the request holden at server if there is no message to consume. 37 | * If <= 0, means the server will response back if there is no message to consume. 38 | * It's value should be 1~30 39 | * 40 | * @throws TopicNotExistException if queue does not exist 41 | * @throws MessageNotExistException if no message exists 42 | * @throws InvalidArgumentException if the argument is invalid 43 | * @throws MQException if any other exception happends 44 | */ 45 | public function consumeHalfMessage($numOfMessages, int $waitSeconds = -1) 46 | { 47 | if ($numOfMessages < 0 || $numOfMessages > 16) { 48 | throw new InvalidArgumentException(400, "numOfMessages should be 1~16"); 49 | } 50 | if ($waitSeconds > 30) { 51 | throw new InvalidArgumentException(400, "numOfMessages should less then 30"); 52 | } 53 | $request = new ConsumeMessageRequest($this->instanceId, $this->topicName, $this->groupId, $numOfMessages, $this->messageTag, $waitSeconds); 54 | $request->setTrans(Constants::TRANSACTION_POP); 55 | $response = new ConsumeMessageResponse(); 56 | return $this->client->sendRequest($request, $response); 57 | } 58 | 59 | /** 60 | * commit transaction message 61 | * 62 | * @param $receiptHandle : 63 | * $receiptHandle, which is got from consumeHalfMessage or publishMessage 64 | * 65 | * @return AckMessageResponse 66 | * 67 | * @throws TopicNotExistException if queue does not exist 68 | * @throws ReceiptHandleErrorException if the receiptHandle is invalid 69 | * @throws InvalidArgumentException if the argument is invalid 70 | * @throws AckMessageException if any message not deleted 71 | * @throws MQException if any other exception happends 72 | */ 73 | public function commit($receiptHandle): AckMessageResponse 74 | { 75 | $request = new AckMessageRequest($this->instanceId, $this->topicName, $this->groupId, [$receiptHandle]); 76 | $request->setTrans(Constants::TRANSACTION_COMMIT); 77 | $response = new AckMessageResponse(); 78 | return $this->client->sendRequest($request, $response); 79 | } 80 | 81 | 82 | /** 83 | * rollback transaction message 84 | * 85 | * @param $receiptHandle : 86 | * $receiptHandle, which is got from consumeHalfMessage or publishMessage 87 | * 88 | * @return AckMessageResponse 89 | * 90 | * @throws TopicNotExistException if queue does not exist 91 | * @throws ReceiptHandleErrorException if the receiptHandle is invalid 92 | * @throws InvalidArgumentException if the argument is invalid 93 | * @throws AckMessageException if any message not deleted 94 | * @throws MQException if any other exception happends 95 | */ 96 | public function rollback($receiptHandle): AckMessageResponse 97 | { 98 | $request = new AckMessageRequest($this->instanceId, $this->topicName, $this->groupId, [$receiptHandle]); 99 | $request->setTrans(Constants::TRANSACTION_ROLLBACK); 100 | $response = new AckMessageResponse(); 101 | return $this->client->sendRequest($request, $response); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Model/AckMessageErrorItem.php: -------------------------------------------------------------------------------- 1 | errorCode = $errorCode; 16 | $this->errorMessage = $errorMessage; 17 | $this->receiptHandle = $receiptHandle; 18 | } 19 | 20 | public function getErrorCode() 21 | { 22 | return $this->errorCode; 23 | } 24 | 25 | public function getErrorMessage() 26 | { 27 | return $this->errorMessage; 28 | } 29 | 30 | public function getReceiptHandle() 31 | { 32 | return $this->receiptHandle; 33 | } 34 | 35 | public static function fromXML($xmlReader): AckMessageErrorItem 36 | { 37 | $errorCode = null; 38 | $errorMessage = null; 39 | $receiptHandle = null; 40 | 41 | while ($xmlReader->read()) { 42 | switch ($xmlReader->nodeType) { 43 | case XMLReader::ELEMENT: 44 | switch ($xmlReader->name) { 45 | case Constants::ERROR_CODE: 46 | $xmlReader->read(); 47 | if ($xmlReader->nodeType == XMLReader::TEXT) { 48 | $errorCode = $xmlReader->value; 49 | } 50 | break; 51 | case Constants::ERROR_MESSAGE: 52 | $xmlReader->read(); 53 | if ($xmlReader->nodeType == XMLReader::TEXT) { 54 | $errorMessage = $xmlReader->value; 55 | } 56 | break; 57 | case Constants::RECEIPT_HANDLE: 58 | $xmlReader->read(); 59 | if ($xmlReader->nodeType == XMLReader::TEXT) { 60 | $receiptHandle = $xmlReader->value; 61 | } 62 | break; 63 | } 64 | break; 65 | case XMLReader::END_ELEMENT: 66 | if ($xmlReader->name == Constants::ERROR) { 67 | return new AckMessageErrorItem($errorCode, $errorMessage, $receiptHandle); 68 | } 69 | break; 70 | } 71 | } 72 | 73 | return new AckMessageErrorItem($errorCode, $errorMessage, $receiptHandle); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Model/Message.php: -------------------------------------------------------------------------------- 1 | messageId = $messageId; 25 | $this->messageBodyMD5 = $messageBodyMD5; 26 | $this->messageBody = $messageBody; 27 | $this->publishTime = $publishTime; 28 | $this->nextConsumeTime = $nextConsumeTime; 29 | $this->firstConsumeTime = $firstConsumeTime; 30 | $this->consumedTimes = $consumedTimes; 31 | $this->receiptHandle = $receiptHandle; 32 | $this->messageTag = $messageTag; 33 | $this->properties = $properties; 34 | } 35 | 36 | public static function fromXML(XMLReader $xmlReader): Message 37 | { 38 | $messageId = null; 39 | $messageBodyMD5 = null; 40 | $messageBody = null; 41 | $publishTime = null; 42 | $nextConsumeTime = null; 43 | $firstConsumeTime = null; 44 | $consumedTimes = null; 45 | $receiptHandle = null; 46 | $messageTag = null; 47 | $properties = null; 48 | 49 | while ($xmlReader->read()) { 50 | switch ($xmlReader->nodeType) { 51 | case XMLReader::ELEMENT: 52 | switch ($xmlReader->name) { 53 | case Constants::MESSAGE_ID: 54 | $xmlReader->read(); 55 | if ($xmlReader->nodeType == XMLReader::TEXT) { 56 | $messageId = $xmlReader->value; 57 | } 58 | break; 59 | case Constants::MESSAGE_BODY_MD5: 60 | $xmlReader->read(); 61 | if ($xmlReader->nodeType == XMLReader::TEXT) { 62 | $messageBodyMD5 = $xmlReader->value; 63 | } 64 | break; 65 | case Constants::MESSAGE_BODY: 66 | $xmlReader->read(); 67 | if ($xmlReader->nodeType == XMLReader::TEXT) { 68 | $messageBody = $xmlReader->value; 69 | } 70 | break; 71 | case Constants::PUBLISH_TIME: 72 | $xmlReader->read(); 73 | if ($xmlReader->nodeType == XMLReader::TEXT) { 74 | $publishTime = $xmlReader->value; 75 | } 76 | break; 77 | case Constants::NEXT_CONSUME_TIME: 78 | $xmlReader->read(); 79 | if ($xmlReader->nodeType == XMLReader::TEXT) { 80 | $nextConsumeTime = $xmlReader->value; 81 | } 82 | break; 83 | case Constants::FIRST_CONSUME_TIME: 84 | $xmlReader->read(); 85 | if ($xmlReader->nodeType == XMLReader::TEXT) { 86 | $firstConsumeTime = $xmlReader->value; 87 | } 88 | break; 89 | case Constants::CONSUMED_TIMES: 90 | $xmlReader->read(); 91 | if ($xmlReader->nodeType == XMLReader::TEXT) { 92 | $consumedTimes = $xmlReader->value; 93 | } 94 | break; 95 | case Constants::RECEIPT_HANDLE: 96 | $xmlReader->read(); 97 | if ($xmlReader->nodeType == XMLReader::TEXT) { 98 | $receiptHandle = $xmlReader->value; 99 | } 100 | break; 101 | case Constants::MESSAGE_TAG: 102 | $xmlReader->read(); 103 | if ($xmlReader->nodeType == XMLReader::TEXT) { 104 | $messageTag = $xmlReader->value; 105 | } 106 | break; 107 | case Constants::MESSAGE_PROPERTIES: 108 | $xmlReader->read(); 109 | if ($xmlReader->nodeType == XMLReader::TEXT) { 110 | $propertiesString = $xmlReader->value; 111 | if ($propertiesString != null) { 112 | $kvArray = explode("|", $propertiesString); 113 | foreach ($kvArray as $kv) { 114 | $kAndV = explode(":", $kv); 115 | if (sizeof($kAndV) == 2) { 116 | $properties[$kAndV[0]] = $kAndV[1]; 117 | } 118 | } 119 | } 120 | } 121 | break; 122 | } 123 | break; 124 | case XMLReader::END_ELEMENT: 125 | if ($xmlReader->name == 'Message') { 126 | return new Message( 127 | $messageId, 128 | $messageBodyMD5, 129 | $messageBody, 130 | $publishTime, 131 | $nextConsumeTime, 132 | $firstConsumeTime, 133 | $consumedTimes, 134 | $receiptHandle, 135 | $messageTag, 136 | $properties 137 | ); 138 | } 139 | break; 140 | } 141 | } 142 | 143 | return new Message( 144 | $messageId, 145 | $messageBodyMD5, 146 | $messageBody, 147 | $publishTime, 148 | $nextConsumeTime, 149 | $firstConsumeTime, 150 | $consumedTimes, 151 | $receiptHandle, 152 | $messageTag, 153 | $properties 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Model/TopicMessage.php: -------------------------------------------------------------------------------- 1 | messageBody = $messageBody; 15 | } 16 | 17 | public function putProperty($key, $value) 18 | { 19 | if ($key === null || $value === null || $key === "" || $value === "") { 20 | return; 21 | } 22 | $this->properties[$key . ""] = $value . ""; 23 | } 24 | 25 | /** 26 | * 设置消息KEY,如果没有设置,则消息的KEY为RequestId 27 | * 28 | * @param $key 29 | */ 30 | public function setMessageKey($key) 31 | { 32 | $this->putProperty(Constants::MESSAGE_PROPERTIES_MSG_KEY, $key); 33 | } 34 | 35 | /** 36 | * 定时消息,单位毫秒(ms),在指定时间戳(当前时间之后)进行投递。 37 | * 如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者 38 | * 39 | * @param $timeInMillis 40 | */ 41 | public function setStartDeliverTime($timeInMillis) 42 | { 43 | $this->putProperty(Constants::MESSAGE_PROPERTIES_TIMER_KEY, $timeInMillis); 44 | } 45 | 46 | /** 47 | * 在消息属性中添加第一次消息回查的最快时间,单位秒,并且表征这是一条事务消息 48 | * 范围: 10~300 49 | * @param $timeInSeconds 50 | */ 51 | public function setTransCheckImmunityTime($timeInSeconds) 52 | { 53 | $this->putProperty(Constants::MESSAGE_PROPERTIES_TRANS_CHECK_KEY, $timeInSeconds); 54 | } 55 | 56 | /** 57 | * 分区顺序消息中区分不同分区的关键字段,sharding key 于普通消息的 key 是完全不同的概念。 58 | * 全局顺序消息,该字段可以设置为任意非空字符串。 59 | * 60 | * @param $shardingKey 61 | */ 62 | public function setShardingKey($shardingKey) 63 | { 64 | $this->putProperty(Constants::MESSAGE_PROPERTIES_SHARDING, $shardingKey); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Requests/AckMessageRequest.php: -------------------------------------------------------------------------------- 1 | topicName = $topicName; 18 | $this->receiptHandles = $receiptHandles; 19 | $this->consumer = $consumer; 20 | } 21 | 22 | public function getTopicName() 23 | { 24 | return $this->topicName; 25 | } 26 | 27 | public function getReceiptHandles() 28 | { 29 | return $this->receiptHandles; 30 | } 31 | 32 | public function getConsumer() 33 | { 34 | return $this->consumer; 35 | } 36 | 37 | public function setTrans($trans) 38 | { 39 | $this->trans = $trans; 40 | } 41 | 42 | public function generateBody() 43 | { 44 | $xmlWriter = new \XMLWriter; 45 | $xmlWriter->openMemory(); 46 | $xmlWriter->startDocument("1.0", "UTF-8"); 47 | $xmlWriter->startElementNS(null, Constants::RECEIPT_HANDLES, Constants::XML_NAMESPACE); 48 | foreach ($this->receiptHandles as $receiptHandle) { 49 | $xmlWriter->writeElement(Constants::RECEIPT_HANDLE, $receiptHandle); 50 | } 51 | $xmlWriter->endElement(); 52 | $xmlWriter->endDocument(); 53 | return $xmlWriter->outputMemory(); 54 | } 55 | 56 | public function generateQueryString() 57 | { 58 | $params = array("consumer" => $this->consumer); 59 | if ($this->instanceId != null && $this->instanceId != "") { 60 | $params["ns"] = $this->instanceId; 61 | } 62 | if ($this->trans != null) { 63 | $params["trans"] = $this->trans; 64 | } 65 | return http_build_query($params); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Requests/BaseRequest.php: -------------------------------------------------------------------------------- 1 | instanceId = $instanceId; 17 | $this->method = $method; 18 | $this->resourcePath = $resourcePath; 19 | } 20 | 21 | abstract public function generateBody(); 22 | abstract public function generateQueryString(); 23 | 24 | public function setBody($body) 25 | { 26 | $this->body = $body; 27 | } 28 | 29 | public function getBody() 30 | { 31 | return $this->body; 32 | } 33 | 34 | public function setQueryString($queryString) 35 | { 36 | $this->queryString = $queryString; 37 | } 38 | 39 | public function getQueryString() 40 | { 41 | return $this->queryString; 42 | } 43 | 44 | public function isHeaderSet($header) 45 | { 46 | return isset($this->headers[$header]); 47 | } 48 | 49 | public function getHeaders() 50 | { 51 | return $this->headers; 52 | } 53 | 54 | public function removeHeader($header) 55 | { 56 | if (isset($this->headers[$header])) { 57 | unset($this->headers[$header]); 58 | } 59 | } 60 | 61 | public function setHeader($header, $value) 62 | { 63 | $this->headers[$header] = $value; 64 | } 65 | 66 | public function getResourcePath() 67 | { 68 | return $this->resourcePath; 69 | } 70 | 71 | public function getMethod() 72 | { 73 | return $this->method; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Requests/ConsumeMessageRequest.php: -------------------------------------------------------------------------------- 1 | topicName = $topicName; 18 | $this->consumer = $consumer; 19 | $this->messageTag = $messageTag; 20 | $this->numOfMessages = $numOfMessages; 21 | $this->waitSeconds = $waitSeconds; 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function getTopicName() 28 | { 29 | return $this->topicName; 30 | } 31 | 32 | /** 33 | * @return mixed 34 | */ 35 | public function getConsumer() 36 | { 37 | return $this->consumer; 38 | } 39 | 40 | /** 41 | * @return null 42 | */ 43 | public function getMessageTag() 44 | { 45 | return $this->messageTag; 46 | } 47 | 48 | /** 49 | * @return mixed 50 | */ 51 | public function getNumOfMessages() 52 | { 53 | return $this->numOfMessages; 54 | } 55 | 56 | /** 57 | * @return null 58 | */ 59 | public function getWaitSeconds() 60 | { 61 | return $this->waitSeconds; 62 | } 63 | 64 | 65 | public function generateBody() 66 | { 67 | return null; 68 | } 69 | 70 | public function setTrans($trans) 71 | { 72 | $this->trans = $trans; 73 | } 74 | 75 | public function generateQueryString() 76 | { 77 | $params = array("numOfMessages" => $this->numOfMessages); 78 | $params["consumer"] = $this->consumer; 79 | if ($this->instanceId != null && $this->instanceId != "") { 80 | $params["ns"] = $this->instanceId; 81 | } 82 | if ($this->waitSeconds != null) { 83 | $params["waitseconds"] = $this->waitSeconds; 84 | } 85 | if ($this->messageTag != null) { 86 | $params["tag"] = $this->messageTag; 87 | } 88 | if ($this->trans != null) { 89 | $params["trans"] = $this->trans; 90 | } 91 | return http_build_query($params); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Requests/PublishMessageRequest.php: -------------------------------------------------------------------------------- 1 | topicName = $topicName; 19 | $this->messageBody = $messageBody; 20 | $this->messageTag = $messageTag; 21 | $this->properties = $properties; 22 | } 23 | 24 | public function getTopicName() 25 | { 26 | return $this->topicName; 27 | } 28 | 29 | public function generateBody(): string 30 | { 31 | $xmlWriter = new XMLWriter; 32 | $xmlWriter->openMemory(); 33 | $xmlWriter->startDocument("1.0", "UTF-8"); 34 | $xmlWriter->startElementNS(null, "Message", Constants::XML_NAMESPACE); 35 | $this->writeMessagePropertiesForPublishXML($xmlWriter); 36 | $xmlWriter->endElement(); 37 | $xmlWriter->endDocument(); 38 | return $xmlWriter->outputMemory(); 39 | } 40 | 41 | public function generateQueryString(): ?string 42 | { 43 | if ($this->instanceId != null && $this->instanceId != "") { 44 | return http_build_query(array("ns" => $this->instanceId)); 45 | } 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Responses/AckMessageResponse.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 24 | if ($statusCode == 204) { 25 | $this->succeed = true; 26 | } else { 27 | $this->parseErrorResponse($statusCode, $content); 28 | } 29 | } 30 | 31 | public function parseErrorResponse($statusCode, $content, MQException $exception = null) 32 | { 33 | $this->succeed = false; 34 | $xmlReader = $this->loadXmlContent($content); 35 | 36 | try { 37 | while ($xmlReader->read()) { 38 | if ($xmlReader->nodeType == XMLReader::ELEMENT) { 39 | switch ($xmlReader->name) { 40 | case Constants::ERROR: 41 | $this->parseNormalErrorResponse($xmlReader); 42 | break; 43 | default: // case Constants::Messages 44 | $this->parseAckMessageErrorResponse($xmlReader); 45 | break; 46 | } 47 | } 48 | } 49 | } catch (Throwable $e) { 50 | if ($exception != null) { 51 | throw $exception; 52 | } elseif ($e instanceof MQException) { 53 | throw $e; 54 | } else { 55 | throw new MQException($statusCode, $e->getMessage()); 56 | } 57 | } 58 | } 59 | 60 | private function parseAckMessageErrorResponse($xmlReader) 61 | { 62 | $ex = new AckMessageException($this->statusCode, "AckMessage Failed For Some ReceiptHandles"); 63 | $ex->setRequestId($this->getRequestId()); 64 | while ($xmlReader->read()) { 65 | if ($xmlReader->nodeType == XMLReader::ELEMENT && $xmlReader->name == Constants::ERROR) { 66 | $ex->addAckMessageErrorItem(AckMessageErrorItem::fromXML($xmlReader)); 67 | } 68 | } 69 | throw $ex; 70 | } 71 | 72 | private function parseNormalErrorResponse($xmlReader) 73 | { 74 | $result = XMLParser::parseNormalError($xmlReader); 75 | 76 | if ($result['Code'] == Constants::INVALID_ARGUMENT) { 77 | throw new InvalidArgumentException($this->getStatusCode(), $result['Message'], null, $result['Code'], $result['RequestId'], $result['HostId']); 78 | } 79 | if ($result['Code'] == Constants::TOPIC_NOT_EXIST) { 80 | throw new TopicNotExistException($this->getStatusCode(), $result['Message'], null, $result['Code'], $result['RequestId'], $result['HostId']); 81 | } 82 | if ($result['Code'] == Constants::RECEIPT_HANDLE_ERROR) { 83 | throw new ReceiptHandleErrorException($this->getStatusCode(), $result['Message'], null, $result['Code'], $result['RequestId'], $result['HostId']); 84 | } 85 | 86 | throw new MQException($this->getStatusCode(), $result['Message'], null, $result['Code'], $result['RequestId'], $result['HostId']); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Responses/BaseResponse.php: -------------------------------------------------------------------------------- 1 | succeed; 23 | } 24 | 25 | public function getStatusCode() 26 | { 27 | return $this->statusCode; 28 | } 29 | 30 | public function setRequestId($requestId) 31 | { 32 | $this->requestId = $requestId; 33 | } 34 | 35 | public function getRequestId() 36 | { 37 | return $this->requestId; 38 | } 39 | 40 | protected function loadXmlContent($content): XMLReader 41 | { 42 | $xmlReader = new XMLReader(); 43 | $isXml = $xmlReader->XML($content); 44 | if ($isXml === false) { 45 | throw new MQException($this->statusCode, $content); 46 | } 47 | try { 48 | $i = 0; 49 | while ($xmlReader->read()) { 50 | $i++; 51 | } 52 | } catch (Exception $e) { 53 | throw new MQException($this->statusCode, $content); 54 | } 55 | $xmlReader->XML($content); 56 | return $xmlReader; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Responses/ConsumeMessageResponse.php: -------------------------------------------------------------------------------- 1 | messages = array(); 21 | } 22 | 23 | public function getMessages(): array 24 | { 25 | return $this->messages; 26 | } 27 | 28 | public function parseResponse($statusCode, $content): array 29 | { 30 | $this->statusCode = $statusCode; 31 | if ($statusCode == 200) { 32 | $this->succeed = true; 33 | } else { 34 | $this->parseErrorResponse($statusCode, $content); 35 | } 36 | 37 | $xmlReader = $this->loadXmlContent($content); 38 | 39 | try { 40 | while ($xmlReader->read()) { 41 | if ($xmlReader->nodeType == XMLReader::ELEMENT 42 | && $xmlReader->name == 'Message') { 43 | $this->messages[] = Message::fromXML($xmlReader); 44 | } 45 | } 46 | return $this->messages; 47 | } catch (Exception $e) { 48 | throw new MQException($statusCode, $e->getMessage(), $e); 49 | } catch (Throwable $t) { 50 | throw new MQException($statusCode, $t->getMessage(), $t); 51 | } 52 | } 53 | 54 | public function parseErrorResponse($statusCode, $content, MQException $exception = null) 55 | { 56 | $this->succeed = false; 57 | $xmlReader = $this->loadXmlContent($content); 58 | 59 | try { 60 | $result = XMLParser::parseNormalError($xmlReader); 61 | if ($result['Code'] == Constants::TOPIC_NOT_EXIST) { 62 | throw new TopicNotExistException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 63 | } 64 | if ($result['Code'] == Constants::MESSAGE_NOT_EXIST) { 65 | throw new MessageNotExistException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 66 | } 67 | throw new MQException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 68 | } catch (Exception $e) { 69 | if ($exception != null) { 70 | throw $exception; 71 | } elseif ($e instanceof MQException) { 72 | throw $e; 73 | } else { 74 | throw new MQException($statusCode, $e->getMessage()); 75 | } 76 | } catch (Throwable $t) { 77 | throw new MQException($statusCode, $t->getMessage()); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Responses/MQPromise.php: -------------------------------------------------------------------------------- 1 | promise = $promise; 17 | $this->response = $response; 18 | } 19 | 20 | public function isCompleted(): bool 21 | { 22 | return $this->promise->getState() != 'pending'; 23 | } 24 | 25 | public function getResponse(): BaseResponse 26 | { 27 | return $this->response; 28 | } 29 | 30 | public function getState(): string 31 | { 32 | return $this->promise->getState(); 33 | } 34 | 35 | /** 36 | * @return mixed 37 | */ 38 | public function wait() 39 | { 40 | try { 41 | $res = $this->promise->wait(); 42 | if ($res instanceof ResponseInterface) { 43 | $this->response->setRequestId($res->getHeaderLine("x-mq-request-id")); 44 | return $this->response->parseResponse($res->getStatusCode(), $res->getBody()); 45 | } 46 | } catch (TransferException $e) { 47 | $message = $e->getMessage(); 48 | if (method_exists($e, 'hasResponse') && $e->hasResponse()) { 49 | $message = $e->getResponse()->getBody(); 50 | } 51 | $this->response->parseErrorResponse($e->getCode(), $message); 52 | } 53 | $this->response->parseErrorResponse("500", "Unknown"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Responses/PublishMessageResponse.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 25 | if ($statusCode == 201) { 26 | $this->succeed = true; 27 | } else { 28 | $this->parseErrorResponse($statusCode, $content); 29 | } 30 | 31 | $xmlReader = $this->loadXmlContent($content); 32 | try { 33 | return $this->readMessageIdAndMD5XML($xmlReader); 34 | } catch (Exception $e) { 35 | throw new MQException($statusCode, $e->getMessage(), $e); 36 | } catch (Throwable $t) { 37 | throw new MQException($statusCode, $t->getMessage()); 38 | } 39 | } 40 | 41 | public function readMessageIdAndMD5XML(XMLReader $xmlReader): TopicMessage 42 | { 43 | $message = Message::fromXML($xmlReader); 44 | $topicMessage = new TopicMessage(null); 45 | $topicMessage->setMessageId($message->getMessageId()); 46 | $topicMessage->setMessageBodyMD5($message->getMessageBodyMD5()); 47 | $topicMessage->setReceiptHandle($message->getReceiptHandle()); 48 | 49 | return $topicMessage; 50 | } 51 | 52 | public function parseErrorResponse($statusCode, $content, MQException $exception = null) 53 | { 54 | $this->succeed = false; 55 | $xmlReader = $this->loadXmlContent($content); 56 | try { 57 | $result = XMLParser::parseNormalError($xmlReader); 58 | if ($result['Code'] == Constants::TOPIC_NOT_EXIST) { 59 | throw new TopicNotExistException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 60 | } 61 | if ($result['Code'] == Constants::INVALID_ARGUMENT) { 62 | throw new InvalidArgumentException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 63 | } 64 | if ($result['Code'] == Constants::MALFORMED_XML) { 65 | throw new MalformedXMLException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 66 | } 67 | throw new MQException($statusCode, $result['Message'], $exception, $result['Code'], $result['RequestId'], $result['HostId']); 68 | } catch (Exception $e) { 69 | if ($exception != null) { 70 | throw $exception; 71 | } elseif ($e instanceof MQException) { 72 | throw $e; 73 | } else { 74 | throw new MQException($statusCode, $e->getMessage()); 75 | } 76 | } catch (Throwable $t) { 77 | throw new MQException($statusCode, $t->getMessage()); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Signature/Signature.php: -------------------------------------------------------------------------------- 1 | getHeaders(); 13 | $contentMd5 = ""; 14 | if (isset($headers['Content-MD5'])) { 15 | $contentMd5 = $headers['Content-MD5']; 16 | } 17 | $contentType = ""; 18 | if (isset($headers['Content-Type'])) { 19 | $contentType = $headers['Content-Type']; 20 | } 21 | $date = $headers['Date']; 22 | $queryString = $request->getQueryString(); 23 | $resource = $request->getResourcePath(); 24 | if ($queryString != null) { 25 | $resource .= "?" . $request->getQueryString(); 26 | } 27 | if (0 !== strpos($resource, "/")) { 28 | $resource = "/" . $resource; 29 | } 30 | 31 | $tmpHeaders = []; 32 | foreach ($headers as $key => $value) { 33 | if (0 === strpos($key, Constants::HEADER_PREFIX)) { 34 | $tmpHeaders[$key] = $value; 35 | } 36 | } 37 | ksort($tmpHeaders); 38 | 39 | $headers = implode("\n", array_map(function ($v, $k) { 40 | return $k . ":" . $v; 41 | }, $tmpHeaders, array_keys($tmpHeaders))); 42 | 43 | $stringToSign = strtoupper($request->getMethod()) . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $date . "\n" . $headers . "\n" . $resource; 44 | 45 | return base64_encode(hash_hmac("sha1", $stringToSign, $accessKey, true)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Traits/MessagePropertiesForConsume.php: -------------------------------------------------------------------------------- 1 | messageBody; 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function getPublishTime() 28 | { 29 | return $this->publishTime; 30 | } 31 | 32 | /** 33 | * @return mixed 34 | */ 35 | public function getNextConsumeTime() 36 | { 37 | return $this->nextConsumeTime; 38 | } 39 | 40 | /** 41 | * 对于顺序消费没有意义 42 | * 43 | * @return mixed 44 | */ 45 | public function getFirstConsumeTime() 46 | { 47 | return $this->firstConsumeTime; 48 | } 49 | 50 | /** 51 | * @return mixed 52 | */ 53 | public function getConsumedTimes() 54 | { 55 | return $this->consumedTimes; 56 | } 57 | 58 | public function getProperty($key) 59 | { 60 | if ($this->properties == null) { 61 | return null; 62 | } 63 | return $this->properties[$key] ?? null; 64 | } 65 | 66 | /** 67 | * 消息KEY 68 | * 69 | * @return mixed|null 70 | */ 71 | public function getMessageKey() 72 | { 73 | return $this->getProperty(Constants::MESSAGE_PROPERTIES_MSG_KEY); 74 | } 75 | 76 | /** 77 | * 定时消息时间戳,单位毫秒(ms 78 | * 79 | * @return int 80 | */ 81 | public function getStartDeliverTime(): int 82 | { 83 | $temp = $this->getProperty(Constants::MESSAGE_PROPERTIES_TIMER_KEY); 84 | if ($temp === null) { 85 | return 0; 86 | } 87 | return (int)$temp; 88 | } 89 | 90 | /** 91 | * 事务消息第一次消息回查的最快时间,单位秒 92 | */ 93 | public function getTransCheckImmunityTime(): int 94 | { 95 | $temp = $this->getProperty(Constants::MESSAGE_PROPERTIES_TRANS_CHECK_KEY); 96 | if ($temp === null) { 97 | return 0; 98 | } 99 | return (int)$temp; 100 | } 101 | 102 | /** 103 | * @return array|mixed 104 | */ 105 | public function toArray() 106 | { 107 | return $this->getMessageBody() ? json_decode($this->getMessageBody(), true, 512, JSON_BIGINT_AS_STRING) : []; 108 | } 109 | 110 | 111 | /** 112 | * 顺序消息分区KEY 113 | */ 114 | public function getShardingKey() 115 | { 116 | return $this->getProperty(Constants::MESSAGE_PROPERTIES_SHARDING); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Traits/MessagePropertiesForPublish.php: -------------------------------------------------------------------------------- 1 | messageBody; 21 | } 22 | 23 | public function setMessageBody($messageBody) 24 | { 25 | $this->messageBody = $messageBody; 26 | } 27 | 28 | public function getMessageTag() 29 | { 30 | return $this->messageTag; 31 | } 32 | 33 | public function setMessageTag($messageTag) 34 | { 35 | $this->messageTag = $messageTag; 36 | } 37 | 38 | public function getMessageId() 39 | { 40 | return $this->messageId; 41 | } 42 | 43 | public function setMessageId($messageId) 44 | { 45 | $this->messageId = $messageId; 46 | } 47 | 48 | public function getMessageBodyMD5() 49 | { 50 | return $this->messageBodyMD5; 51 | } 52 | 53 | public function setMessageBodyMD5($messageBodyMD5) 54 | { 55 | $this->messageBodyMD5 = $messageBodyMD5; 56 | } 57 | 58 | public function getReceiptHandle() 59 | { 60 | return $this->receiptHandle; 61 | } 62 | 63 | public function setReceiptHandle($receiptHandle) 64 | { 65 | return $this->receiptHandle = $receiptHandle; 66 | } 67 | 68 | public function getProperties() 69 | { 70 | return $this->properties; 71 | } 72 | 73 | public function writeMessagePropertiesForPublishXML(XMLWriter $xmlWriter) 74 | { 75 | if ($this->messageBody != null) { 76 | $xmlWriter->writeElement(Constants::MESSAGE_BODY, $this->messageBody); 77 | } 78 | if ($this->messageTag !== null) { 79 | $xmlWriter->writeElement(Constants::MESSAGE_TAG, $this->messageTag); 80 | } 81 | if ($this->properties !== null && sizeof($this->properties) > 0) { 82 | $this->checkPropValid(); 83 | $xmlWriter->writeElement( 84 | Constants::MESSAGE_PROPERTIES, 85 | implode("|", array_map(function ($v, $k) { 86 | return $k . ":" . $v; 87 | }, $this->properties, array_keys($this->properties))) 88 | ); 89 | } 90 | } 91 | 92 | private function checkPropValid() 93 | { 94 | foreach ($this->properties as $key => $value) { 95 | if ($key === null || $key == "" || $value === null || $value == "") { 96 | throw new MQException(400, "Message Properties is null or empty"); 97 | } 98 | 99 | if ($this->isContainSpecialChar($key) || $this->isContainSpecialChar($value)) { 100 | throw new MQException(400, "Message's property can't contains: & \" ' < > : |"); 101 | } 102 | } 103 | } 104 | 105 | private function isContainSpecialChar($str): bool 106 | { 107 | return strpos($str, "&") !== false 108 | || strpos($str, "\"") !== false || strpos($str, "'") !== false 109 | || strpos($str, "<") !== false || strpos($str, ">") !== false 110 | || strpos($str, ":") !== false || strpos($str, "|") !== false; 111 | } 112 | } 113 | --------------------------------------------------------------------------------