RabbitMQController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <?php declare(strict_types=1);
  2. declare(ticks=1);
  3. namespace mikemadisonweb\rabbitmq\controllers;
  4. use BadFunctionCallException;
  5. use InvalidArgumentException;
  6. use mikemadisonweb\rabbitmq\components\Consumer;
  7. use mikemadisonweb\rabbitmq\components\Routing;
  8. use mikemadisonweb\rabbitmq\Configuration;
  9. use Yii;
  10. use yii\base\Action;
  11. use yii\console\Controller;
  12. use yii\console\ExitCode;
  13. use yii\helpers\Console;
  14. /**
  15. * RabbitMQ extension functionality
  16. *
  17. * @package mikemadisonweb\rabbitmq\controllers
  18. */
  19. class RabbitMQController extends Controller
  20. {
  21. public $memoryLimit = 0;
  22. public $messagesLimit = 0;
  23. public $debug = false;
  24. public $withoutSignals = false;
  25. /**
  26. * @var Configuration
  27. */
  28. private $rabbitmq;
  29. public function init()
  30. {
  31. $this->rabbitmq = Yii::$app->rabbitmq;
  32. }
  33. protected $options = [
  34. 'm' => 'messagesLimit',
  35. 'l' => 'memoryLimit',
  36. 'd' => 'debug',
  37. 'w' => 'withoutSignals',
  38. ];
  39. /**
  40. * @param string $actionID
  41. *
  42. * @return array
  43. */
  44. public function options($actionID): array
  45. {
  46. return array_merge(parent::options($actionID), array_values($this->options));
  47. }
  48. /**
  49. * @return array
  50. */
  51. public function optionAliases(): array
  52. {
  53. return array_merge(parent::optionAliases(), $this->options);
  54. }
  55. /**
  56. * @param Action $event
  57. *
  58. * @return bool
  59. */
  60. public function beforeAction($event): bool
  61. {
  62. if (defined('AMQP_WITHOUT_SIGNALS') === false)
  63. {
  64. define('AMQP_WITHOUT_SIGNALS', $this->withoutSignals);
  65. }
  66. if (defined('AMQP_DEBUG') === false)
  67. {
  68. if ($this->debug === 'false')
  69. {
  70. $this->debug = false;
  71. }
  72. define('AMQP_DEBUG', (bool)$this->debug);
  73. }
  74. return parent::beforeAction($event);
  75. }
  76. /**
  77. * Run a consumer
  78. *
  79. * @param string $name Consumer name
  80. *
  81. * @return int
  82. * @throws \Throwable
  83. */
  84. public function actionConsume(string $name): int
  85. {
  86. $consumer = $this->rabbitmq->getConsumer($name);
  87. $this->validateConsumerOptions($consumer);
  88. if ((null !== $this->memoryLimit) && ctype_digit((string)$this->memoryLimit) && ($this->memoryLimit > 0))
  89. {
  90. $consumer->setMemoryLimit($this->memoryLimit);
  91. }
  92. $consumer->consume($this->messagesLimit);
  93. return ExitCode::OK;
  94. }
  95. /**
  96. * Restart consumer by name
  97. *
  98. * @param string $name
  99. * @return int
  100. * @throws \Throwable
  101. * @throws \yii\base\InvalidConfigException
  102. * @throws \yii\di\NotInstantiableException
  103. */
  104. public function actionRestartConsume(string $name): int
  105. {
  106. $consumer = $this->rabbitmq->getConsumer($name);
  107. $consumer->restartDaemon();
  108. return $this->actionConsume($name);
  109. }
  110. /**
  111. * Publish a message from STDIN to the queue
  112. *
  113. * @param string $producerName
  114. * @param string $exchangeName
  115. * @param string $routingKey
  116. *
  117. * @return int
  118. * @throws \yii\base\InvalidConfigException
  119. * @throws \yii\di\NotInstantiableException
  120. */
  121. public function actionPublish(string $producerName, string $exchangeName, string $routingKey = ''): int
  122. {
  123. $producer = $this->rabbitmq->getProducer($producerName);
  124. $data = '';
  125. if (posix_isatty(STDIN))
  126. {
  127. $this->stderr(Console::ansiFormat("Please pipe in some data in order to send it.\n", [Console::FG_RED]));
  128. return ExitCode::UNSPECIFIED_ERROR;
  129. }
  130. while (!feof(STDIN))
  131. {
  132. $data .= fread(STDIN, 8192);
  133. }
  134. $producer->publish($data, $exchangeName, $routingKey);
  135. $this->stdout("Message was successfully published.\n", Console::FG_GREEN);
  136. return ExitCode::OK;
  137. }
  138. /**
  139. * Create RabbitMQ exchanges, queues and bindings based on configuration
  140. *
  141. * @param string $connectionName
  142. *
  143. * @return int
  144. * @throws \RuntimeException
  145. */
  146. public function actionDeclareAll(string $connectionName = Configuration::DEFAULT_CONNECTION_NAME): int
  147. {
  148. $routing = $this->getRouting($connectionName);
  149. $result = $routing->declareAll();
  150. if ($result)
  151. {
  152. $this->stdout(
  153. Console::ansiFormat("All configured entries was successfully declared.\n", [Console::FG_GREEN])
  154. );
  155. return ExitCode::OK;
  156. }
  157. $this->stderr(Console::ansiFormat("No queues, exchanges or bindings configured.\n", [Console::FG_RED]));
  158. return ExitCode::UNSPECIFIED_ERROR;
  159. }
  160. /**
  161. * Create the exchange listed in configuration
  162. *
  163. * @param $exchangeName
  164. * @param string $connectionName
  165. *
  166. * @return int
  167. * @throws \RuntimeException
  168. */
  169. public function actionDeclareExchange(
  170. string $exchangeName,
  171. string $connectionName = Configuration::DEFAULT_CONNECTION_NAME
  172. ): int {
  173. $routing = $this->getRouting($connectionName);
  174. if ($routing->isExchangeExists($exchangeName))
  175. {
  176. $this->stderr(Console::ansiFormat("Exchange `{$exchangeName}` is already exists.\n", [Console::FG_RED]));
  177. return ExitCode::UNSPECIFIED_ERROR;
  178. }
  179. $routing->declareExchange($exchangeName);
  180. $this->stdout(Console::ansiFormat("Exchange `{$exchangeName}` was declared.\n", [Console::FG_GREEN]));
  181. return ExitCode::OK;
  182. }
  183. /**
  184. * Create the queue listed in configuration
  185. *
  186. * @param $queueName
  187. * @param string $connectionName
  188. *
  189. * @return int
  190. * @throws \RuntimeException
  191. */
  192. public function actionDeclareQueue(
  193. string $queueName,
  194. string $connectionName = Configuration::DEFAULT_CONNECTION_NAME
  195. ): int {
  196. $routing = $this->getRouting($connectionName);
  197. if ($routing->isQueueExists($queueName))
  198. {
  199. $this->stderr(Console::ansiFormat("Queue `{$queueName}` is already exists.\n", [Console::FG_RED]));
  200. return ExitCode::UNSPECIFIED_ERROR;
  201. }
  202. $routing->declareQueue($queueName);
  203. $this->stdout(Console::ansiFormat("Queue `{$queueName}` was declared.\n", [Console::FG_GREEN]));
  204. return ExitCode::OK;
  205. }
  206. /**
  207. * Delete all RabbitMQ exchanges and queues that is defined in configuration
  208. *
  209. * @param string $connection
  210. *
  211. * @return int
  212. * @throws \RuntimeException
  213. */
  214. public function actionDeleteAll(string $connection = Configuration::DEFAULT_CONNECTION_NAME): int
  215. {
  216. if ($this->interactive)
  217. {
  218. $input = Console::prompt('Are you sure you want to delete all queues and exchanges?', ['default' => 'yes']);
  219. if ($input !== 'yes' && $input !== 'y')
  220. {
  221. $this->stderr(Console::ansiFormat("Aborted.\n", [Console::FG_RED]));
  222. return ExitCode::UNSPECIFIED_ERROR;
  223. }
  224. }
  225. $routing = $this->getRouting($connection);
  226. $routing->deleteAll();
  227. $this->stdout(Console::ansiFormat("All configured entries was deleted.\n", [Console::FG_GREEN]));
  228. return ExitCode::OK;
  229. }
  230. /**
  231. * Delete an exchange
  232. *
  233. * @param $exchangeName
  234. * @param string $connectionName
  235. *
  236. * @return int
  237. * @throws \RuntimeException
  238. */
  239. public function actionDeleteExchange(
  240. string $exchangeName,
  241. string $connectionName = Configuration::DEFAULT_CONNECTION_NAME
  242. ): int {
  243. if ($this->interactive)
  244. {
  245. $input = Console::prompt('Are you sure you want to delete that exchange?', ['default' => 'yes']);
  246. if ($input !== 'yes')
  247. {
  248. $this->stderr(Console::ansiFormat("Aborted.\n", [Console::FG_RED]));
  249. return ExitCode::UNSPECIFIED_ERROR;
  250. }
  251. }
  252. $routing = $this->getRouting($connectionName);
  253. $routing->deleteExchange($exchangeName);
  254. $this->stdout(Console::ansiFormat("Exchange `{$exchangeName}` was deleted.\n", [Console::FG_GREEN]));
  255. return ExitCode::OK;
  256. }
  257. /**
  258. * Delete a queue
  259. *
  260. * @param $queueName
  261. * @param string $connectionName
  262. *
  263. * @return int
  264. * @throws \RuntimeException
  265. */
  266. public function actionDeleteQueue(
  267. string $queueName,
  268. string $connectionName = Configuration::DEFAULT_CONNECTION_NAME
  269. ): int {
  270. if ($this->interactive)
  271. {
  272. $input = Console::prompt('Are you sure you want to delete that queue?', ['default' => 'yes']);
  273. if ($input !== 'yes')
  274. {
  275. $this->stderr(Console::ansiFormat("Aborted.\n", [Console::FG_RED]));
  276. return ExitCode::UNSPECIFIED_ERROR;
  277. }
  278. }
  279. $routing = $this->getRouting($connectionName);
  280. $routing->deleteQueue($queueName);
  281. $this->stdout(Console::ansiFormat("Queue `{$queueName}` was deleted.\n", [Console::FG_GREEN]));
  282. return ExitCode::OK;
  283. }
  284. /**
  285. * Delete all messages from the queue
  286. *
  287. * @param $queueName
  288. * @param string $connectionName
  289. *
  290. * @return int
  291. * @throws \RuntimeException
  292. */
  293. public function actionPurgeQueue(
  294. string $queueName,
  295. string $connectionName = Configuration::DEFAULT_CONNECTION_NAME
  296. ): int {
  297. if ($this->interactive)
  298. {
  299. $input = Console::prompt(
  300. 'Are you sure you want to delete all messages inside that queue?',
  301. ['default' => 'yes']
  302. );
  303. if ($input !== 'yes')
  304. {
  305. $this->stderr(Console::ansiFormat("Aborted.\n", [Console::FG_RED]));
  306. return ExitCode::UNSPECIFIED_ERROR;
  307. }
  308. }
  309. $routing = $this->getRouting($connectionName);
  310. $routing->purgeQueue($queueName);
  311. $this->stdout(Console::ansiFormat("Queue `{$queueName}` was purged.\n", [Console::FG_GREEN]));
  312. return ExitCode::OK;
  313. }
  314. /**
  315. * @param string $connectionName
  316. * @return Routing|object|string
  317. * @throws \yii\base\InvalidConfigException
  318. * @throws \yii\di\NotInstantiableException
  319. */
  320. private function getRouting(string $connectionName)
  321. {
  322. $conn = $this->rabbitmq->getConnection($connectionName);
  323. return $this->rabbitmq->getRouting($conn);
  324. }
  325. /**
  326. * Validate options passed by user
  327. *
  328. * @param Consumer $consumer
  329. */
  330. private function validateConsumerOptions(Consumer $consumer)
  331. {
  332. if (!AMQP_WITHOUT_SIGNALS && extension_loaded('pcntl'))
  333. {
  334. if (!function_exists('pcntl_signal'))
  335. {
  336. throw new BadFunctionCallException(
  337. "Function 'pcntl_signal' is referenced in the php.ini 'disable_functions' and can't be called."
  338. );
  339. }
  340. pcntl_signal(SIGTERM, [$consumer, 'stopDaemon']);
  341. pcntl_signal(SIGINT, [$consumer, 'stopDaemon']);
  342. pcntl_signal(SIGHUP, [$consumer, 'restartDaemon']);
  343. }
  344. $this->messagesLimit = (int)$this->messagesLimit;
  345. $this->memoryLimit = (int)$this->memoryLimit;
  346. if (!is_numeric($this->messagesLimit) || 0 > $this->messagesLimit)
  347. {
  348. throw new InvalidArgumentException('The -m option should be null or greater than 0');
  349. }
  350. if (!is_numeric($this->memoryLimit) || 0 > $this->memoryLimit)
  351. {
  352. throw new InvalidArgumentException('The -l option should be null or greater than 0');
  353. }
  354. }
  355. }