diff --git a/README.md b/README.md index 6490de86..1c1cedee 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ -# PHP2021 \ No newline at end of file +# PHP2021 + +## Домашняя работа № 16. Очереди + +### Запуск приложения + +Отправка сообщений в очередь через форму по адресу `http://otus.local/index.php` + +Запуск обработчика сообщений через кнопку по адресу `http://otus.local/consume.php` + +Для обработчика необходимо опеределить почтовые переменные в env файле: +* EMAIL_HOST +* EMAIL_USER +* EMAIL_PASS diff --git a/code/app.php b/code/app.php new file mode 100644 index 00000000..4e4c0576 --- /dev/null +++ b/code/app.php @@ -0,0 +1,10 @@ +run(); +} catch (Exception $e) { + App\Infrastructure\Response::generateBadRequestResponse($e->getMessage()); +} \ No newline at end of file diff --git a/code/app_src/Application.php b/code/app_src/Application.php new file mode 100644 index 00000000..37cadb93 --- /dev/null +++ b/code/app_src/Application.php @@ -0,0 +1,27 @@ +handler = new RequestHandler($request); + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + } + + public function run(): void + { + $this->handler->execute(); + } +} diff --git a/code/app_src/Application/Adapters/RabbitAdapter.php b/code/app_src/Application/Adapters/RabbitAdapter.php new file mode 100644 index 00000000..bf1fab13 --- /dev/null +++ b/code/app_src/Application/Adapters/RabbitAdapter.php @@ -0,0 +1,21 @@ +connection = new AMQPStreamConnection( + getenv('RABBITMQ_DEFAULT_NAME'), + getenv('RABBITMQ_DEFAULT_PORT'), + getenv('RABBITMQ_DEFAULT_USER'), + getenv('RABBITMQ_DEFAULT_PASS') + ); + } +} diff --git a/code/app_src/Application/Interfaces/BankServiceInterface.php b/code/app_src/Application/Interfaces/BankServiceInterface.php new file mode 100644 index 00000000..e23c5dc1 --- /dev/null +++ b/code/app_src/Application/Interfaces/BankServiceInterface.php @@ -0,0 +1,9 @@ +dateFrom = $dateFrom; + $this->dateTo = $dateto; + $this->clientId = $clientId; + $this->clientMail = $clientMail; + } + + public function getDateFrom(): string + { + return $this->dateFrom; + } + + public function getDateTo(): string + { + return $this->dateTo; + } + + public function getClientId(): string + { + return $this->clientId; + } + + public function getClientMail(): string + { + return $this->clientMail; + } +} \ No newline at end of file diff --git a/code/app_src/Helpers/AppHelper.php b/code/app_src/Helpers/AppHelper.php new file mode 100644 index 00000000..b4ea570a --- /dev/null +++ b/code/app_src/Helpers/AppHelper.php @@ -0,0 +1,27 @@ +connection); + } + + public static function createConsumer(QueueInterface $adapter): Consumer + { + return new Consumer( + $adapter->connection, + new BankService(), + new MailAgent(new PHPMailer()) + ); + } +} diff --git a/code/app_src/Infrastructure/BankService.php b/code/app_src/Infrastructure/BankService.php new file mode 100644 index 00000000..c0f4c671 --- /dev/null +++ b/code/app_src/Infrastructure/BankService.php @@ -0,0 +1,64 @@ +validateStatementData($statement)) { + throw new Exception('Not enough data for request'); + } + + $this->bankStatement = new BankStatement( + $statement['date_from'], + $statement['date_to'], + $statement['client_id'], + $statement['client_email'] + ); + } + + public function validateStatementData(array $data): bool + { + if (is_null($data)) { + return false; + } + + foreach ($this->requiredStatementFields as $field){ + if (!in_array($field, $data)) { + return false; + } + } + + return true; + } + + public function getUserData(): array + { + if (isset($this->bankStatement)) { + //некоторая выборка данных за указанный пользователем период + $someClientInfo = range(0,100); + shuffle($someClientInfo); + return [ + 'client_mail' => $this->bankStatement->getClientMail(), + 'client_info' => json_encode($someClientInfo) + ]; + } else { + throw new Exception('Empty statement'); + } + } +} diff --git a/code/app_src/Infrastructure/Consumer.php b/code/app_src/Infrastructure/Consumer.php new file mode 100644 index 00000000..76c19e6e --- /dev/null +++ b/code/app_src/Infrastructure/Consumer.php @@ -0,0 +1,88 @@ +connection = $connection; + $this->channel = $connection->channel(); + $this->service = $service; + $this->mailAgent = $mailAgent; + } + + public function runFromQueue(string $queueName): void + { + $this->channel->queue_declare( + $queueName, + false, + true, + false, + false + ); + + $this->channel->basic_qos(null, 1, null); + $this->channel->basic_consume( + $queueName, + '', + false, + false, + false, + false, + $this->onConsume() + ); + + while(count($this->channel->callbacks)) { + $this->channel->wait(); + } + + $this->closeConnection(); + } + + public function closeConnection(): void + { + $this->channel->close(); + $this->connection->close(); + } + + private function onConsume(): Closure + { + return function (AMQPMessage $request): void { + $request->ack(); + $this->service->setBankStatement($request->getBody()); + + try { + $userData = $this->service->getUserData(); + + if (!empty($userData)) { + $this->mailAgent->send( + $userData['client_mail'], + 'Bank statement', + $userData['client_info'] + ); + } + } catch (Exception $e) { + throw new Exception('Error while consuming message from queue'); + } + }; + } +} \ No newline at end of file diff --git a/code/app_src/Infrastructure/MailAgent.php b/code/app_src/Infrastructure/MailAgent.php new file mode 100644 index 00000000..1417476a --- /dev/null +++ b/code/app_src/Infrastructure/MailAgent.php @@ -0,0 +1,35 @@ +mailer = $mailer; + $this->mailer->isSMTP(); + $this->mailer->Host = getenv('EMAIL_HOST'); + $this->mailer->SMTPAuth = true; + $this->mailer->Username = getenv('EMAIL_USER'); + $this->mailer->Password = getenv('EMAIL_PASS'); + $this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + $this->mailer->Port = 465; + } + + public function send(string $to, string $subject, string $body): bool + { + $this->mailer->setFrom(getenv('EMAIL_USER'), 'Mailer'); + $this->mailer->addAddress($to); + $this->mailer->isHTML(true); + $this->mailer->Subject = $subject; + $this->mailer->Body = $body; + + return $this->mailer->send(); + } +} \ No newline at end of file diff --git a/code/app_src/Infrastructure/Publisher.php b/code/app_src/Infrastructure/Publisher.php new file mode 100644 index 00000000..37afc490 --- /dev/null +++ b/code/app_src/Infrastructure/Publisher.php @@ -0,0 +1,79 @@ +connection = $connection; + $this->channel = $connection->channel(); + $this->channel->confirm_select(); + $this->channel->set_ack_handler($this->onAck()); + $this->channel->set_nack_handler($this->onNAck()); + } + + public function addToQueue(array $request, string $queueName = null): string + { + if (isset($queueName)) { + $this->channel->queue_declare( + $queueName, + false, + true, + false, + false + ); + } + + $this->responseMsg = null; + $this->correlationId = uniqid(); + + $msg = new AMQPMessage(json_encode($request), [ + 'correlation_id' => $this->correlationId, + 'delivery_mode' => 2, + ]); + + $this->channel->basic_publish($msg, '', $queueName); + while (!isset($this->responseMsg)) { + $this->channel->wait(); + } + + return $this->responseMsg; + } + + public function closeConnection(): void + { + $this->channel->close(); + $this->connection->close(); + } + + private function onAck(): Closure + { + return function (AMQPMessage $response): void { + if ($response->get('correlation_id') === $this->correlationId) { + $this->responseMsg = 'Request accepted'; + } + }; + } + + private function onNAck(): Closure + { + return function (AMQPMessage $response): void { + if ($response->get('correlation_id') === $this->correlationId) { + $this->responseMsg = 'Request not accepted'; + } + }; + } +} \ No newline at end of file diff --git a/code/app_src/Infrastructure/RequestHandler.php b/code/app_src/Infrastructure/RequestHandler.php new file mode 100644 index 00000000..651c43b2 --- /dev/null +++ b/code/app_src/Infrastructure/RequestHandler.php @@ -0,0 +1,49 @@ +request = $request; + $rabbitAdapter = new RabbitAdapter(); + $this->publisher = AppHelper::createPublisher($rabbitAdapter); + $this->consumer = AppHelper::createConsumer($rabbitAdapter); + + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + } + + public function execute(): void + { + switch($this->request['request_type']) { + case 'request': + Response::generateOkResponse($this->publisher->addToQueue($this->request, 'bank_queue')); + $this->publisher->closeConnection(); + break; + + case 'consume': + $this->consumer->runFromQueue('bank_queue'); + break; + + default: + throw new \Exception('Something went wrong, please reload and try again'); + } + } +} \ No newline at end of file diff --git a/code/app_src/Infrastructure/RequestValidator.php b/code/app_src/Infrastructure/RequestValidator.php new file mode 100644 index 00000000..c4bf8a4f --- /dev/null +++ b/code/app_src/Infrastructure/RequestValidator.php @@ -0,0 +1,30 @@ +=7.4.3", + "ext-json": "*", + "php-amqplib/php-amqplib": ">=3.0", + "phpmailer/phpmailer": "^6.5" + }, + "autoload": { + "psr-4": { + "App\\": "app_src" + } + } +} \ No newline at end of file diff --git a/code/composer.lock b/code/composer.lock new file mode 100644 index 00000000..66086d6c --- /dev/null +++ b/code/composer.lock @@ -0,0 +1,407 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6a902a9999828ea7fb2138c92e6d65b3", + "packages": [ + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.2", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "c1b1d82d109846ba58a4664dc5480c69ad2fc097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/c1b1d82d109846ba58a4664dc5480c69ad2fc097", + "reference": "c1b1d82d109846ba58a4664dc5480c69ad2fc097", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-13T05:29:16+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "php-amqplib/php-amqplib", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-amqplib/php-amqplib.git", + "reference": "0bec5b392428e0ac3b3f34fbc4e02d706995833e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/0bec5b392428e0ac3b3f34fbc4e02d706995833e", + "reference": "0bec5b392428e0ac3b3f34fbc4e02d706995833e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-sockets": "*", + "php": "^7.1||^8.0", + "phpseclib/phpseclib": "^2.0|^3.0" + }, + "conflict": { + "php": "7.4.0 - 7.4.1" + }, + "replace": { + "videlalvaro/php-amqplib": "self.version" + }, + "require-dev": { + "ext-curl": "*", + "nategood/httpful": "^0.2.20", + "phpunit/phpunit": "^7.5|^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpAmqpLib\\": "PhpAmqpLib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Alvaro Videla", + "role": "Original Maintainer" + }, + { + "name": "Raúl Araya", + "email": "nubeiro@gmail.com", + "role": "Maintainer" + }, + { + "name": "Luke Bakken", + "email": "luke@bakken.io", + "role": "Maintainer" + }, + { + "name": "Ramūnas Dronga", + "email": "github@ramuno.lt", + "role": "Maintainer" + } + ], + "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", + "homepage": "https://github.com/php-amqplib/php-amqplib/", + "keywords": [ + "message", + "queue", + "rabbitmq" + ], + "support": { + "issues": "https://github.com/php-amqplib/php-amqplib/issues", + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.2.0" + }, + "time": "2022-03-10T19:16:00+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.6.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "e43bac82edc26ca04b36143a48bde1c051cfd5b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e43bac82edc26ca04b36143a48bde1c051cfd5b1", + "reference": "e43bac82edc26ca04b36143a48bde1c051cfd5b1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.2", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6.2", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2022-02-28T15:31:21+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.14", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2f0b7af658cbea265cbb4a791d6c29a6613f98ef", + "reference": "2f0b7af658cbea265cbb4a791d6c29a6613f98ef", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.14" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2022-04-04T05:15:45+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4.3", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/code/consume.php b/code/consume.php new file mode 100644 index 00000000..9f29992e --- /dev/null +++ b/code/consume.php @@ -0,0 +1,30 @@ + +
+ + + +