From f541d55e053819a19cb8bfa97233e2b394550f80 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 00:46:20 +0100 Subject: [PATCH 01/17] feat: add monolog --- composer.json | 1 + composer.lock | 262 ++++++++++++++++++++++++++++++++++- config/bundles.php | 1 + config/packages/monolog.yaml | 61 ++++++++ symfony.lock | 12 ++ 5 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 config/packages/monolog.yaml diff --git a/composer.json b/composer.json index 50c1c0b..2cee8c0 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "symfony/form": "7.0.*", "symfony/framework-bundle": "7.0.*", "symfony/http-client": "*", + "symfony/monolog-bundle": "^3.10", "symfony/options-resolver": "7.0.*", "symfony/runtime": "7.0.*", "symfony/translation": "7.0.*", diff --git a/composer.lock b/composer.lock index cb30729..f2b53bd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "001cdd38079665d928258b761b93729c", + "content-hash": "dda7db8ccac14db47a72f26e4769dac8", "packages": [ { "name": "composer/semver", @@ -258,6 +258,107 @@ ], "time": "2023-12-03T20:19:20+00:00" }, + { + "name": "monolog/monolog", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^10.1", + "predis/predis": "^1.1 || ^2", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.5.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2023-10-27T15:32:31+00:00" + }, { "name": "nyholm/psr7", "version": "1.8.1", @@ -3256,6 +3357,165 @@ ], "time": "2023-12-30T15:41:17+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "4ee9e0b3a4736d5598888444e2f1cd3bf206067c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/4ee9e0b3a4736d5598888444e2f1cd3bf206067c", + "reference": "4ee9e0b3a4736d5598888444e2f1cd3bf206067c", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-21T15:09:11+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, { "name": "symfony/options-resolver", "version": "v7.0.0", diff --git a/config/bundles.php b/config/bundles.php index cd8f9bb..1982a0d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -5,4 +5,5 @@ return [ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], ]; diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000..8c9efa9 --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,61 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr diff --git a/symfony.lock b/symfony.lock index 02e7380..b958bdd 100644 --- a/symfony.lock +++ b/symfony.lock @@ -107,6 +107,18 @@ "src/Kernel.php" ] }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/phpunit-bridge": { "version": "7.0", "recipe": { From bf65ce058dc75ba0259fa11afa9b49b5e161d35d Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 00:47:17 +0100 Subject: [PATCH 02/17] feat: add UsetrenoHttpClient !fixup bb244e40f5fa42294acc7cc7f925796325ada4c5 --- src/Entity/Remote/Usetreno/AuthRequest.php | 8 + .../Remote/Exception/AuthorizeException.php | 8 + src/Service/Remote/UsetrenoHttpClient.php | 145 ++++++++++++++++++ tests/Common/LoggerTrait.php | 18 +++ .../Service/Remote/UsetrenoHttpClientTest.php | 54 +++++++ 5 files changed, 233 insertions(+) create mode 100644 src/Entity/Remote/Usetreno/AuthRequest.php create mode 100644 src/Service/Remote/Exception/AuthorizeException.php create mode 100644 src/Service/Remote/UsetrenoHttpClient.php create mode 100644 tests/Common/LoggerTrait.php create mode 100644 tests/Service/Remote/UsetrenoHttpClientTest.php diff --git a/src/Entity/Remote/Usetreno/AuthRequest.php b/src/Entity/Remote/Usetreno/AuthRequest.php new file mode 100644 index 0000000..2d475d5 --- /dev/null +++ b/src/Entity/Remote/Usetreno/AuthRequest.php @@ -0,0 +1,8 @@ + $options + * @return ResponseInterface + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface + * @throws TransportExceptionInterface + */ + public function request(string $method, string $url, array $options = []): ResponseInterface { + if ($this->authorizationToken === null) { + $this->authorize(); + } + + return $this->innerClient->request($method, $url, $options); + } + + /** + * @param ResponseInterface|iterable $responses One or more responses created by the current HTTP client + * @param float|null $timeout + * @return ResponseStreamInterface + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface + * @throws TransportExceptionInterface + */ + public function stream(iterable|ResponseInterface $responses, float $timeout = null): ResponseStreamInterface + { + if ($this->authorizationToken === null) { + $this->authorize(); + } + + return $this->innerClient->stream($responses, $timeout); + } + + /** + * @param array $options + * @return static(UsetrenoHttpClient) + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface + * @throws TransportExceptionInterface + */ + public function withOptions(array $options): static + { + if ($this->authorizationToken === null) { + $this->authorize(); + } + + $this->innerClient = $this->innerClient->withOptions($options); + return $this; + } + + /** + * @throws TransportExceptionInterface + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface + */ + protected function authorize(): void { + $this->logger->debug("trying authorize request", [ + "AUTHORIZE_API" => static::AUTHORIZE_API + ]); + + $rq = $this->innerClient->request( + "POST", + static::AUTHORIZE_API, + [ + 'json' => new AuthRequest($this->username, $this->password), + ] + ); + + $statusCode = $rq->getStatusCode(); + $responseData = $rq->getContent(false); + + if ($statusCode != 200) { + $this->logger->error("authorization request status code is not ok", [ + "AUTHORIZE_API" => static::AUTHORIZE_API, + "statusCode" => $statusCode, + "content" => $responseData, + "username" => $this->username + ]); + + throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)"); + } + + $this->authorizationToken = $this->processAuthorizeResponse($responseData); + $this->innerClient = $this->innerClient->withOptions([ + 'headers' => [ + 'Authorization' => "Bearer " . $this->authorizationToken + ] + ]); + } + + /** + * @param string $responseData + * @return string Bearer token + * @throws AuthorizeException + */ + protected function processAuthorizeResponse(string $responseData): string { + $data = json_decode($responseData); + + if (!is_object($data)) { + $this->logger->error("authorize: received null response data", [ + "responseData" => $responseData + ]); + throw new AuthorizeException("Can't decode json response"); + } + + if (!isset($data->data->token)) { + $this->logger->error("authorize: empty token in response data", [ + "responseData" => $responseData + ]); + throw new AuthorizeException("Got invalid json response"); + } + + return $data->data->token; + } +} diff --git a/tests/Common/LoggerTrait.php b/tests/Common/LoggerTrait.php new file mode 100644 index 0000000..b625754 --- /dev/null +++ b/tests/Common/LoggerTrait.php @@ -0,0 +1,18 @@ +pushHandler(new TestHandler()); + return $logger; + } +} diff --git a/tests/Service/Remote/UsetrenoHttpClientTest.php b/tests/Service/Remote/UsetrenoHttpClientTest.php new file mode 100644 index 0000000..92d7ef8 --- /dev/null +++ b/tests/Service/Remote/UsetrenoHttpClientTest.php @@ -0,0 +1,54 @@ + [ + "expires_in" => 1000, + "token" => "foobarfoobar" + ], + "timestamp" => "2024-01-17T22:47:59+01:00", + "uri" => "/api/v1/token" + ]); + $authorizedRequestResponse = clone $authMockResponse; + $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]); + $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar"); + $client->request("POST", "https://www.root.cz/"); + $this->assertEquals("https://topapi.top-test.cz/chameleon/api/v1/token", $authMockResponse->getRequestUrl()); + $headers = $authorizedRequestResponse->getRequestOptions()['headers']; + $this->assertEquals("https://www.root.cz/", $authorizedRequestResponse->getRequestUrl()); + $this->assertTrue(in_array("Authorization: Bearer foobarfoobar", $headers), "missing bearer authorization header"); + } + + public function testRequestFailedAuthorization() { + $this->expectException(AuthorizeException::class); + $authMockResponse = new JsonMockResponse([ + "errors" => [ + [ + "message" => "Bad credentials, please verify that your username/password are correctly set.", + "specificType" => "bad_credentials", + "type" => "security_error" + ] + ], + "timestamp" => "2024-01-17T23:55:25+01:00", + "uri" => "/api/v1/token" + ]); + + $authorizedRequestResponse = clone $authMockResponse; + $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]); + $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar"); + $client->request("POST", "https://www.root.cz/"); + } +} From deacc6024b03d6d9d2d1b730b5c38f791988fd5c Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 13:50:08 +0100 Subject: [PATCH 03/17] fix(BankPaymentValidator): check for negative identification number --- src/Validator/BankPaymentIdentificationNumberValidator.php | 2 +- .../Validator/BankPaymentIdentificationNumberValidatorTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Validator/BankPaymentIdentificationNumberValidator.php b/src/Validator/BankPaymentIdentificationNumberValidator.php index 3459da2..122b065 100644 --- a/src/Validator/BankPaymentIdentificationNumberValidator.php +++ b/src/Validator/BankPaymentIdentificationNumberValidator.php @@ -24,7 +24,7 @@ class BankPaymentIdentificationNumberValidator extends ConstraintValidator throw new UnexpectedValueException($value, "string"); } - if (strlen($value) <= 10 && filter_var($value, FILTER_VALIDATE_INT) !== false) { + if (strlen($value) <= 10 && filter_var($value, FILTER_VALIDATE_INT) !== false && (int) $value > 0) { return ; } diff --git a/tests/Validator/BankPaymentIdentificationNumberValidatorTest.php b/tests/Validator/BankPaymentIdentificationNumberValidatorTest.php index d9e08e9..d33db73 100644 --- a/tests/Validator/BankPaymentIdentificationNumberValidatorTest.php +++ b/tests/Validator/BankPaymentIdentificationNumberValidatorTest.php @@ -35,6 +35,6 @@ class BankPaymentIdentificationNumberValidatorTest extends ValidatorTestCase } private function failureDp(): array { - return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a'], ['122.1'], ['12345678901']]; + return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a'], ['122.1'], ['12345678901'], ['-323']]; } } From 450187aa23eb98b969fcb9e9a05d9a543a6249c7 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 13:51:49 +0100 Subject: [PATCH 04/17] docs: UPDATE README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c68f907..fe8ffcd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ TODO: ----- - [ ] Chybí speciální slovník nebo vypnutí slovníku pro testy +- [ ] V reálný aplikaci bych použil Mockery, nicméně tady mě to přijde zbytečný From 264e0260cf1a1110081996c1ea9ec7fa674f7c5f Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 14:20:10 +0100 Subject: [PATCH 05/17] feat: implementation UsetrenoQRCodeProvider --- .../Remote/Usetreno/Edge/EdgeQRCode.php | 18 +++ .../Remote/Usetreno/Edge/EdgeQRCodeMoney.php | 9 ++ .../Edge/EdgeQRCodePaymentIdentification.php | 9 ++ .../Usetreno/Edge/EdgeQRCodeQROptions.php | 11 ++ .../Exceptions/MissingParameterException.php | 9 ++ .../Remote/Edge/QRCodeEntityConverter.php | 62 ++++++++++ .../Exception/UsetrenoQRCodeException.php | 11 ++ src/Service/Remote/UsetrenoQRCodeProvider.php | 89 ++++++++++++++ .../Remote/UsetrenoQRCodeProviderTest.php | 110 ++++++++++++++++++ 9 files changed, 328 insertions(+) create mode 100644 src/Entity/Remote/Usetreno/Edge/EdgeQRCode.php create mode 100644 src/Entity/Remote/Usetreno/Edge/EdgeQRCodeMoney.php create mode 100644 src/Entity/Remote/Usetreno/Edge/EdgeQRCodePaymentIdentification.php create mode 100644 src/Entity/Remote/Usetreno/Edge/EdgeQRCodeQROptions.php create mode 100644 src/Service/Remote/Edge/Exceptions/MissingParameterException.php create mode 100644 src/Service/Remote/Edge/QRCodeEntityConverter.php create mode 100644 src/Service/Remote/Exception/UsetrenoQRCodeException.php create mode 100644 src/Service/Remote/UsetrenoQRCodeProvider.php create mode 100644 tests/Service/Remote/UsetrenoQRCodeProviderTest.php diff --git a/src/Entity/Remote/Usetreno/Edge/EdgeQRCode.php b/src/Entity/Remote/Usetreno/Edge/EdgeQRCode.php new file mode 100644 index 0000000..2d7f767 --- /dev/null +++ b/src/Entity/Remote/Usetreno/Edge/EdgeQRCode.php @@ -0,0 +1,18 @@ +getIban() ?? throw new MissingParameterException("iban not set"), + $code->getDueDate() ? $code->getDueDate()->format('Y-m-d') : throw new MissingParameterException("due date not set"), + $code->getMessage() ?? throw new MissingParameterException("message not set"), + $this->convertMoney($code->getMoney() ?? throw new MissingParameterException("money not set")), + $this->convertCodeOptions($code->getCodeQROptions() ?? throw new MissingParameterException("codeQROptions not set")), + $this->convertIdentification($code->getPaymentIdentification()) + ); + } + + protected function convertMoney(QRCodeMoney $money): EdgeQRCodeMoney + { + return new EdgeQRCodeMoney( + $money->getAmount() ?? throw new MissingParameterException("amount not set"), + $money->getCurrency() ?? throw new MissingParameterException("currency not set") + ); + } + + protected function convertCodeOptions(QRCodeQROptions $options): EdgeQRCodeQROptions + { + return new EdgeQRCodeQROptions( + $options->getScale(), + $options->getMargin() + ); + } + + protected function convertIdentification(?QRCodePaymentIdentification $ident): EdgeQRCodePaymentIdentification { + // OMG: proc? symboly v ostatnich statech nejsou, prijde me lepsi posilat NULL, nebo empty string ;-) + return match ($ident) { + null => new EdgeQRCodePaymentIdentification( + "0", + "0", + "0" + ), + default => new EdgeQRCodePaymentIdentification( + $ident->getVariableSymbol() ?? "0", + $ident->getSpecificSymbol() ?? "0", + $ident->getConstantSymbol() ?? "0" + ) + }; + } +} diff --git a/src/Service/Remote/Exception/UsetrenoQRCodeException.php b/src/Service/Remote/Exception/UsetrenoQRCodeException.php new file mode 100644 index 0000000..fea0bd5 --- /dev/null +++ b/src/Service/Remote/Exception/UsetrenoQRCodeException.php @@ -0,0 +1,11 @@ +codeEntityConverter->convert($entity); + + $this->logger->debug("Sending request for QR code", [ + "entity" => $entity, + "edgeEntity" => $edgeEntity + ]); + + $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [ + 'json' => $edgeEntity, + ]); + + $statusCode = $response->getStatusCode(); + $responseData = $response->getContent(false); + + if ($statusCode != 200) { + $this->logger->error("qrcode request status code is not ok", [ + "AUTHORIZE_API" => static::QRCODE_API, + "statusCode" => $statusCode, + "content" => $responseData, + ]); + + throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)"); + } + + $this->logger->debug("QRCode generation success", [ + "responseContent" => $responseData, + ]); + + return $this->parseBase64String($this->processQRCodeResponseEntity($responseData)); + } + + protected function parseBase64String(string $content): string { + $data = explode(';base64,', $content); + // check if response is image (we support png only now) + if ($data[0] === "image/png") { + throw new UsetrenoQRCodeException("Unsupported image type $data[0]"); + } + + return $data[1]; + } + + /** + * @param string $responseData + * @return string Bearer token + * @throws UsetrenoQRCodeException + */ + protected function processQRCodeResponseEntity(string $responseData): string { + $data = json_decode($responseData); + + if (!is_object($data)) { + $this->logger->error("qrcode: received null response data", [ + "responseData" => $responseData + ]); + throw new UsetrenoQRCodeException("Can't decode json response"); + } + + if (!isset($data->data->base64Data)) { + $this->logger->error("qrcode: empty base64Data in response data", [ + "responseData" => $responseData + ]); + throw new UsetrenoQRCodeException("Got invalid json response"); + } + + return $data->data->base64Data; + } +} diff --git a/tests/Service/Remote/UsetrenoQRCodeProviderTest.php b/tests/Service/Remote/UsetrenoQRCodeProviderTest.php new file mode 100644 index 0000000..517f9aa --- /dev/null +++ b/tests/Service/Remote/UsetrenoQRCodeProviderTest.php @@ -0,0 +1,110 @@ + "/api/v1/qr-code/create-for-bank-account-payment", + "timestamp" => "2024-01-16T14:05:42+01:00", + "data" => [ + "base64Data" => "data:image/png;base64,$base64Image" + ] + ]; + + $edgeEntity = new EdgeQRCode( + "CZ0000", + (new \DateTime("now"))->format('Y-m-d'), + "foo", + new EdgeQRCodeMoney( + 100, + "CZK" + ), + new EdgeQRCodeQROptions( + 1, + 1 + ), + new EdgeQRCodePaymentIdentification( + "0", "0", "0" + ) + ); + + $entity = $this->createMock(QRCode::class); + + $qrCodeProvider = $this->createQRCodeProvider($successRequest, 200, $edgeEntity, $entity); + $data = $qrCodeProvider->generateQRCodeFromEntity($entity); + $this->assertEquals($base64Image, $data); + } + + public function testFailureRequest() { + $this->expectException(QRCodeGeneratorException::class); + $failureRequest = [ + "error" => "internal server error", + ]; + + $edgeEntity = new EdgeQRCode( + "CZ0000", + (new \DateTime("now"))->format('Y-m-d'), + "foo", + new EdgeQRCodeMoney( + 100, + "CZK" + ), + new EdgeQRCodeQROptions( + 1, + 1 + ), + new EdgeQRCodePaymentIdentification( + "0", "0", "0" + ) + ); + + $entity = $this->createMock(QRCode::class); + + $qrCodeProvider = $this->createQRCodeProvider($failureRequest, 500, $edgeEntity, $entity); + $data = $qrCodeProvider->generateQRCodeFromEntity($entity); + $this->assertEquals($failureRequest["data"]["base64Data"], $data); + } + + public function createQRCodeProvider(array $response, int $responseCode, EdgeQRCode $edgeEntity, QRCode $entity): UsetrenoQRCodeProvider + { + $responseMock = $this->createMock(ResponseInterface::class); + + $responseMock->expects($this->any()) + ->method('getContent') + ->will($this->returnValue(json_encode($response))); + $responseMock->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue($responseCode)); + + $mock = $this->createMock(UsetrenoHttpClient::class); + $mock->expects($this->once()) + ->method('request') + ->will($this->returnValue($responseMock)); + + $converterMock = $this->createMock(QRCodeEntityConverter::class); + $converterMock->expects($this->once()) + ->method('convert') + ->will($this->returnValue($edgeEntity)); + + return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock); + } +} From beb7b06bf45b87e48a608c23df7309d22c0921ff Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 14:22:27 +0100 Subject: [PATCH 06/17] fix: add QRCodeGeneratorException --- src/Service/Exception/QRCodeGeneratorException.php | 9 +++++++++ src/Service/QRCodeGeneratorInterface.php | 2 ++ 2 files changed, 11 insertions(+) create mode 100644 src/Service/Exception/QRCodeGeneratorException.php diff --git a/src/Service/Exception/QRCodeGeneratorException.php b/src/Service/Exception/QRCodeGeneratorException.php new file mode 100644 index 0000000..ab2fd68 --- /dev/null +++ b/src/Service/Exception/QRCodeGeneratorException.php @@ -0,0 +1,9 @@ + Date: Thu, 18 Jan 2024 14:23:30 +0100 Subject: [PATCH 07/17] fix: QRCodePaymentIdentification - default values --- src/Entity/QRCode/QRCodePaymentIdentification.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Entity/QRCode/QRCodePaymentIdentification.php b/src/Entity/QRCode/QRCodePaymentIdentification.php index cb777b1..5474120 100644 --- a/src/Entity/QRCode/QRCodePaymentIdentification.php +++ b/src/Entity/QRCode/QRCodePaymentIdentification.php @@ -13,17 +13,17 @@ class QRCodePaymentIdentification { #[BankPaymentIdentificationNumber( message: 'messages.not_variable_symbol', )] - private ?string $variableSymbol; + private ?string $variableSymbol = null; #[BankPaymentIdentificationNumber( message: 'messages.not_specific_symbol', )] - private ?string $specificSymbol; + private ?string $specificSymbol = null; #[BankPaymentIdentificationNumber( message: 'messages.not_constant_symbol', )] // https://www.hyponamiru.cz/en/glossary/constant-symbol/ - private ?string $constantSymbol; + private ?string $constantSymbol = null; public function getVariableSymbol(): ?string { From 2d6f917ecb5e911608ae058eaadc36a3785ee8f7 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 14:24:03 +0100 Subject: [PATCH 08/17] fix: QRCodeQROptionsDefaultProvider - use const values and tunning of scale --- src/Service/QRCodeQROptionsDefaultProvider.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Service/QRCodeQROptionsDefaultProvider.php b/src/Service/QRCodeQROptionsDefaultProvider.php index 7ae853d..b52756c 100644 --- a/src/Service/QRCodeQROptionsDefaultProvider.php +++ b/src/Service/QRCodeQROptionsDefaultProvider.php @@ -6,12 +6,15 @@ namespace App\Service; use App\Entity\QRCode\QRCodeQROptions; readonly final class QRCodeQROptionsDefaultProvider implements QRCodeQROptionsProviderInterface { + const DEFAULT_SCALE = 8; + const DEFAULT_MARGIN = 0; + private QRCodeQROptions $qrCodeDefaultOptions; public function __construct() { $this->qrCodeDefaultOptions = new QRCodeQROptions( - 1, - 0 + self::DEFAULT_SCALE, + self::DEFAULT_MARGIN ); } From 172cf058fe48d1b978ce170766013ddff545001b Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 14:24:28 +0100 Subject: [PATCH 09/17] feat: use UsetrenoQRCodeProvider --- config/services.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config/services.yaml b/config/services.yaml index c2bc64f..06e08fd 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -5,7 +5,11 @@ # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: env(SYMFONY_EXAMPLE_APP_CURRENCIES): '["CZK", "EUR", "USD", "GBP"]' + env(SYMFONY_EXAMPLE_APP_USETRENO_USERNAME): 'developer' + env(SYMFONY_EXAMPLE_APP_USETRENO_PASSWORD): 'developer123' app.currencies: '%env(json:SYMFONY_EXAMPLE_APP_CURRENCIES)%' + app.usetreno.username: '%env(string:SYMFONY_EXAMPLE_APP_USETRENO_USERNAME)%' + app.usetreno.password: '%env(string:SYMFONY_EXAMPLE_APP_USETRENO_PASSWORD)%' services: # default configuration for services in *this* file @@ -31,9 +35,14 @@ services: arguments: $imagePath: '%kernel.project_dir%/assets/images/wip.png' + App\Service\Remote\UsetrenoHttpClient: + arguments: + $username: '%app.usetreno.username%' + $password: '%app.usetreno.password%' + App\Service\CurrencyListerInterface: '@App\Service\StaticCurrencyLister' App\Service\QRCodeQROptionsProviderInterface: '@App\Service\QRCodeQROptionsDefaultProvider' - App\Service\QRCodeGeneratorInterface: '@App\Service\StubQRCodeGenerator' + App\Service\QRCodeGeneratorInterface: '@App\Service\Remote\UsetrenoQRCodeProvider' # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones From 574dba85ada9e40d1b0dd8f27c9de0912382273b Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 14:24:45 +0100 Subject: [PATCH 10/17] fix: better MoneyValidatorTest --- tests/Validator/MoneyValidatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Validator/MoneyValidatorTest.php b/tests/Validator/MoneyValidatorTest.php index a234620..8a869cd 100644 --- a/tests/Validator/MoneyValidatorTest.php +++ b/tests/Validator/MoneyValidatorTest.php @@ -35,6 +35,6 @@ class MoneyValidatorTest extends ValidatorTestCase } private function failureDp(): array { - return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a']]; + return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a'], ['-223']]; } } From 9bd3b5efffd9e16c9a556726caf5d79821c25af4 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 15:21:41 +0100 Subject: [PATCH 11/17] feat: add CachedQRCodeProvider --- README.md | 1 + composer.json | 1 + composer.lock | 2 +- config/packages/cache.yaml | 2 +- config/services.yaml | 7 +++- .../CacheableQRCodeGeneratorInterface.php | 12 ++++++ src/Service/CachedQRCodeGenerator.php | 39 +++++++++++++++++++ src/Service/Remote/UsetrenoQRCodeProvider.php | 15 +++++-- 8 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 src/Service/CacheableQRCodeGeneratorInterface.php create mode 100644 src/Service/CachedQRCodeGenerator.php diff --git a/README.md b/README.md index fe8ffcd..dfb5e0d 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,4 @@ TODO: ----- - [ ] Chybí speciální slovník nebo vypnutí slovníku pro testy - [ ] V reálný aplikaci bych použil Mockery, nicméně tady mě to přijde zbytečný +- [ ] Nastavení cache ideálně v memcached/redis etc. diff --git a/composer.json b/composer.json index 2cee8c0..5ee109a 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "php-http/httplug": "*", "symfony/asset": "7.0.*", "symfony/asset-mapper": "7.0.*", + "symfony/cache": "7.0.*", "symfony/console": "7.0.*", "symfony/dotenv": "7.0.*", "symfony/flex": "^2", diff --git a/composer.lock b/composer.lock index f2b53bd..140508e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dda7db8ccac14db47a72f26e4769dac8", + "content-hash": "15e05e23293f0f945b50f450fe8213c3", "packages": [ { "name": "composer/semver", diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 6899b72..23dcfb1 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -1,7 +1,7 @@ framework: cache: # Unique name of your app: used to compute stable namespaces for cache keys. - #prefix_seed: your_vendor_name/app_name + prefix_seed: ovlach/symfony_example_app # The "app" cache stores to the filesystem by default. # The data in this cache should persist between deploys. diff --git a/config/services.yaml b/config/services.yaml index 06e08fd..f96070f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -40,9 +40,14 @@ services: $username: '%app.usetreno.username%' $password: '%app.usetreno.password%' + App\Service\CachedQRCodeGenerator: + arguments: + $innerGenerator: '@App\Service\Remote\UsetrenoQRCodeProvider' + $cacheDuration: 'PT60S' + App\Service\CurrencyListerInterface: '@App\Service\StaticCurrencyLister' App\Service\QRCodeQROptionsProviderInterface: '@App\Service\QRCodeQROptionsDefaultProvider' - App\Service\QRCodeGeneratorInterface: '@App\Service\Remote\UsetrenoQRCodeProvider' + App\Service\QRCodeGeneratorInterface: '@App\Service\CachedQRCodeGenerator' # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/src/Service/CacheableQRCodeGeneratorInterface.php b/src/Service/CacheableQRCodeGeneratorInterface.php new file mode 100644 index 0000000..9b80ac1 --- /dev/null +++ b/src/Service/CacheableQRCodeGeneratorInterface.php @@ -0,0 +1,12 @@ +cacheDuration = new \DateInterval($cacheDuration); + } + + public function generateQRCodeFromEntity(QRCode $entity): string + { + $key = $this->innerGenerator->getCacheKey($entity); + return $this->cache->get($key, function(ItemInterface $c) use ($entity) { + $this->logger->debug("cache miss for key " . $c->getKey(), [ + 'entity' => $entity, + ]); + + $c->expiresAfter($this->cacheDuration); + + return $this->innerGenerator->generateQRCodeFromEntity($entity); + }); + } +} diff --git a/src/Service/Remote/UsetrenoQRCodeProvider.php b/src/Service/Remote/UsetrenoQRCodeProvider.php index bd59053..379beb8 100644 --- a/src/Service/Remote/UsetrenoQRCodeProvider.php +++ b/src/Service/Remote/UsetrenoQRCodeProvider.php @@ -4,13 +4,12 @@ declare(strict_types=1); namespace App\Service\Remote; use App\Entity\QRCode\QRCode; -use App\Service\QRCodeGeneratorInterface; +use App\Service\CacheableQRCodeGeneratorInterface; use App\Service\Remote\Edge\QRCodeEntityConverter; -use App\Service\Remote\Exception\AuthorizeException; use App\Service\Remote\Exception\UsetrenoQRCodeException; use Psr\Log\LoggerInterface; -class UsetrenoQRCodeProvider implements QRCodeGeneratorInterface +class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface { const QRCODE_API = 'https://topapi.top-test.cz/chameleon/api/v1/qr-code/create-for-bank-account-payment'; const QRCODE_METHOD = 'POST'; @@ -86,4 +85,14 @@ class UsetrenoQRCodeProvider implements QRCodeGeneratorInterface return $data->data->base64Data; } + + public function getCacheKey(QRCode $entity): string + { + $edgeEntity = $this->codeEntityConverter->convert($entity); + $encodedEntity = json_encode($edgeEntity); + if ($encodedEntity === false) { + throw new \RuntimeException("Can't serialize edge entity"); + } + return base64_encode($encodedEntity); + } } From 1a3cf1c2e049492c419aa39f076ad90b125be649 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 16:41:03 +0100 Subject: [PATCH 12/17] feat: better data wf --- src/Controller/IndexController.php | 7 +- src/Entity/DTO/QRCode/QRCode.php | 15 ++++ src/Entity/DTO/QRCode/QRCodeMoney.php | 9 ++ .../QRCode/QRCodePaymentIdentification.php | 9 ++ src/Entity/DTO/QRCode/QRCodeQROptions.php | 11 +++ src/Entity/{ => Input}/QRCode/QRCode.php | 2 +- src/Entity/{ => Input}/QRCode/QRCodeMoney.php | 2 +- .../QRCode/QRCodePaymentIdentification.php | 3 +- .../{ => Input}/QRCode/QRCodeQROptions.php | 2 +- src/Form/Type/QRCodeMoneyType.php | 2 +- .../Type/QRCodePaymentIdentificationType.php | 2 +- src/Form/Type/QRCodeType.php | 2 +- .../CacheableQRCodeGeneratorInterface.php | 2 +- src/Service/CachedQRCodeGenerator.php | 3 +- src/Service/DTO/QRCodeEntityConverter.php | 58 ++++++++++++ src/Service/QRCodeGeneratorInterface.php | 2 +- .../QRCodeQROptionsDefaultProvider.php | 2 +- .../QRCodeQROptionsProviderInterface.php | 2 +- .../Remote/Edge/QRCodeEntityConverter.php | 34 +++---- src/Service/Remote/UsetrenoQRCodeProvider.php | 2 +- src/Service/StubQRCodeGenerator.php | 2 +- .../Remote/UsetrenoQRCodeProviderTest.php | 90 ++++++++++--------- 22 files changed, 186 insertions(+), 77 deletions(-) create mode 100644 src/Entity/DTO/QRCode/QRCode.php create mode 100644 src/Entity/DTO/QRCode/QRCodeMoney.php create mode 100644 src/Entity/DTO/QRCode/QRCodePaymentIdentification.php create mode 100644 src/Entity/DTO/QRCode/QRCodeQROptions.php rename src/Entity/{ => Input}/QRCode/QRCode.php (98%) rename src/Entity/{ => Input}/QRCode/QRCodeMoney.php (97%) rename src/Entity/{ => Input}/QRCode/QRCodePaymentIdentification.php (94%) rename src/Entity/{ => Input}/QRCode/QRCodeQROptions.php (96%) create mode 100644 src/Service/DTO/QRCodeEntityConverter.php diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index 02ebf42..0294c20 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -2,8 +2,9 @@ namespace App\Controller; -use App\Entity\QRCode\QRCode; +use App\Entity\Input\QRCode\QRCode; use App\Form\Type\QRCodeType; +use App\Service\DTO\QRCodeEntityConverter; use App\Service\QRCodeGeneratorInterface; use App\Service\QRCodeQROptionsProviderInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; class IndexController extends AbstractController { - public function __construct(private readonly QRCodeQROptionsProviderInterface $qrCodeQROptionsFactory) {} + public function __construct(private readonly QRCodeQROptionsProviderInterface $qrCodeQROptionsFactory, private readonly QRCodeEntityConverter $codeEntityConverter) {} #[Route('/', name: 'homepage')] public function indexAction( Request $request, @@ -33,7 +34,7 @@ class IndexController extends AbstractController { do session, nicmene u takovehleho typu aplikace me to prijde naopak kontraproduktivni. a zadani o tom mlci. Navic v pripade ze aplikace bude provozovana ve vice instancich by se musela resit memcache, redis... */ - $qrCodeImage = $qrCodeGenerator->generateQRCodeFromEntity($qrCode); + $qrCodeImage = $qrCodeGenerator->generateQRCodeFromEntity($this->codeEntityConverter->convert($qrCode)); $form = $this->createForm(QRCodeType::class, $this->createQrCodeEntity()); } } diff --git a/src/Entity/DTO/QRCode/QRCode.php b/src/Entity/DTO/QRCode/QRCode.php new file mode 100644 index 0000000..e0c6cda --- /dev/null +++ b/src/Entity/DTO/QRCode/QRCode.php @@ -0,0 +1,15 @@ +validator->validate($code)) !== 0) { + throw new \InvalidArgumentException("QRCode entity is not valid"); + } + + return new QRCode( + $code->getIban() ?? throw new \InvalidArgumentException("iban not set"), + \DateTimeImmutable::createFromMutable($code->getDueDate() ?? throw new \InvalidArgumentException("due date not set")), + $code->getMessage() ?? throw new \InvalidArgumentException("message not set"), + $this->convertMoney($code->getMoney() ?? throw new \InvalidArgumentException("money not set")), + $this->convertCodeOptions($code->getCodeQROptions() ?? throw new \InvalidArgumentException("codeQROptions not set")), + $this->convertIdentification($code->getPaymentIdentification()) + ); + } + + protected function convertMoney(\App\Entity\Input\QRCode\QRCodeMoney $money): QRCodeMoney + { + return new QRCodeMoney( + $money->getAmount() ?? throw new \InvalidArgumentException("amount not set"), + $money->getCurrency() ?? throw new \InvalidArgumentException("currency not set") + ); + } + + protected function convertCodeOptions(\App\Entity\Input\QRCode\QRCodeQROptions $options): QRCodeQROptions + { + return new QRCodeQROptions( + $options->getScale(), + $options->getMargin() + ); + } + + protected function convertIdentification(?\App\Entity\Input\QRCode\QRCodePaymentIdentification $ident): ?QRCodePaymentIdentification { + return match ($ident) { + null => null, + default => new QRCodePaymentIdentification( + $ident->getVariableSymbol(), + $ident->getSpecificSymbol(), + $ident->getConstantSymbol() + ) + }; + } +} diff --git a/src/Service/QRCodeGeneratorInterface.php b/src/Service/QRCodeGeneratorInterface.php index b9ec81f..f674124 100644 --- a/src/Service/QRCodeGeneratorInterface.php +++ b/src/Service/QRCodeGeneratorInterface.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Service; -use App\Entity\QRCode\QRCode; +use App\Entity\DTO\QRCode\QRCode; use App\Service\Exception\QRCodeGeneratorException; interface QRCodeGeneratorInterface diff --git a/src/Service/QRCodeQROptionsDefaultProvider.php b/src/Service/QRCodeQROptionsDefaultProvider.php index b52756c..44b5cf6 100644 --- a/src/Service/QRCodeQROptionsDefaultProvider.php +++ b/src/Service/QRCodeQROptionsDefaultProvider.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Service; -use App\Entity\QRCode\QRCodeQROptions; +use App\Entity\Input\QRCode\QRCodeQROptions; readonly final class QRCodeQROptionsDefaultProvider implements QRCodeQROptionsProviderInterface { const DEFAULT_SCALE = 8; diff --git a/src/Service/QRCodeQROptionsProviderInterface.php b/src/Service/QRCodeQROptionsProviderInterface.php index f830c86..c9445dd 100644 --- a/src/Service/QRCodeQROptionsProviderInterface.php +++ b/src/Service/QRCodeQROptionsProviderInterface.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Service; -use App\Entity\QRCode\QRCodeQROptions; +use App\Entity\Input\QRCode\QRCodeQROptions; interface QRCodeQROptionsProviderInterface { public function getDefault(): QRCodeQROptions; diff --git a/src/Service/Remote/Edge/QRCodeEntityConverter.php b/src/Service/Remote/Edge/QRCodeEntityConverter.php index 4fd76ce..44cb63c 100644 --- a/src/Service/Remote/Edge/QRCodeEntityConverter.php +++ b/src/Service/Remote/Edge/QRCodeEntityConverter.php @@ -3,10 +3,10 @@ declare(strict_types=1); namespace App\Service\Remote\Edge; -use App\Entity\QRCode\QRCode; -use App\Entity\QRCode\QRCodeMoney; -use App\Entity\QRCode\QRCodePaymentIdentification; -use App\Entity\QRCode\QRCodeQROptions; +use App\Entity\DTO\QRCode\QRCode; +use App\Entity\DTO\QRCode\QRCodeMoney; +use App\Entity\DTO\QRCode\QRCodePaymentIdentification; +use App\Entity\DTO\QRCode\QRCodeQROptions; use App\Entity\Remote\Usetreno\Edge\EdgeQRCode; use App\Entity\Remote\Usetreno\Edge\EdgeQRCodeMoney; use App\Entity\Remote\Usetreno\Edge\EdgeQRCodePaymentIdentification; @@ -19,28 +19,28 @@ class QRCodeEntityConverter public function convert(QRCode $code): EdgeQRCode { return new EdgeQRCode( - $code->getIban() ?? throw new MissingParameterException("iban not set"), - $code->getDueDate() ? $code->getDueDate()->format('Y-m-d') : throw new MissingParameterException("due date not set"), - $code->getMessage() ?? throw new MissingParameterException("message not set"), - $this->convertMoney($code->getMoney() ?? throw new MissingParameterException("money not set")), - $this->convertCodeOptions($code->getCodeQROptions() ?? throw new MissingParameterException("codeQROptions not set")), - $this->convertIdentification($code->getPaymentIdentification()) + $code->iban, + $code->dueDate->format('Y-m-d'), + $code->message, + $this->convertMoney($code->money), + $this->convertCodeOptions($code->qrOptions), + $this->convertIdentification($code->paymentIdentification) ); } protected function convertMoney(QRCodeMoney $money): EdgeQRCodeMoney { return new EdgeQRCodeMoney( - $money->getAmount() ?? throw new MissingParameterException("amount not set"), - $money->getCurrency() ?? throw new MissingParameterException("currency not set") + $money->amount, + $money->currency ); } protected function convertCodeOptions(QRCodeQROptions $options): EdgeQRCodeQROptions { return new EdgeQRCodeQROptions( - $options->getScale(), - $options->getMargin() + $options->scale, + $options->margin ); } @@ -53,9 +53,9 @@ class QRCodeEntityConverter "0" ), default => new EdgeQRCodePaymentIdentification( - $ident->getVariableSymbol() ?? "0", - $ident->getSpecificSymbol() ?? "0", - $ident->getConstantSymbol() ?? "0" + $ident->variableSymbol ?? "0", + $ident->specificSymbol ?? "0", + $ident->constantSymbol ?? "0" ) }; } diff --git a/src/Service/Remote/UsetrenoQRCodeProvider.php b/src/Service/Remote/UsetrenoQRCodeProvider.php index 379beb8..fe3d6bf 100644 --- a/src/Service/Remote/UsetrenoQRCodeProvider.php +++ b/src/Service/Remote/UsetrenoQRCodeProvider.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Service\Remote; -use App\Entity\QRCode\QRCode; +use App\Entity\DTO\QRCode\QRCode; use App\Service\CacheableQRCodeGeneratorInterface; use App\Service\Remote\Edge\QRCodeEntityConverter; use App\Service\Remote\Exception\UsetrenoQRCodeException; diff --git a/src/Service/StubQRCodeGenerator.php b/src/Service/StubQRCodeGenerator.php index 8dfc664..1279b00 100644 --- a/src/Service/StubQRCodeGenerator.php +++ b/src/Service/StubQRCodeGenerator.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Service; -use App\Entity\QRCode\QRCode; +use App\Entity\DTO\QRCode\QRCode; readonly final class StubQRCodeGenerator implements QRCodeGeneratorInterface { diff --git a/tests/Service/Remote/UsetrenoQRCodeProviderTest.php b/tests/Service/Remote/UsetrenoQRCodeProviderTest.php index 517f9aa..dd7e602 100644 --- a/tests/Service/Remote/UsetrenoQRCodeProviderTest.php +++ b/tests/Service/Remote/UsetrenoQRCodeProviderTest.php @@ -2,7 +2,10 @@ namespace App\Tests\Service\Remote; -use App\Entity\QRCode\QRCode; +use App\Entity\DTO\QRCode\QRCode; +use App\Entity\DTO\QRCode\QRCodeMoney; +use App\Entity\DTO\QRCode\QRCodePaymentIdentification; +use App\Entity\DTO\QRCode\QRCodeQROptions; use App\Entity\Remote\Usetreno\Edge\EdgeQRCode; use App\Entity\Remote\Usetreno\Edge\EdgeQRCodeMoney; use App\Entity\Remote\Usetreno\Edge\EdgeQRCodePaymentIdentification; @@ -30,57 +33,24 @@ class UsetrenoQRCodeProviderTest extends TestCase ] ]; - $edgeEntity = new EdgeQRCode( - "CZ0000", - (new \DateTime("now"))->format('Y-m-d'), - "foo", - new EdgeQRCodeMoney( - 100, - "CZK" - ), - new EdgeQRCodeQROptions( - 1, - 1 - ), - new EdgeQRCodePaymentIdentification( - "0", "0", "0" - ) - ); + [$edgeEntity, $qrCodeEntity] = $this->createQRCodeEntityPair(); - $entity = $this->createMock(QRCode::class); - - $qrCodeProvider = $this->createQRCodeProvider($successRequest, 200, $edgeEntity, $entity); - $data = $qrCodeProvider->generateQRCodeFromEntity($entity); + $qrCodeProvider = $this->createQRCodeProvider($successRequest, 200, $edgeEntity, $qrCodeEntity); + $data = $qrCodeProvider->generateQRCodeFromEntity($qrCodeEntity); $this->assertEquals($base64Image, $data); } public function testFailureRequest() { $this->expectException(QRCodeGeneratorException::class); + $failureRequest = [ "error" => "internal server error", ]; - $edgeEntity = new EdgeQRCode( - "CZ0000", - (new \DateTime("now"))->format('Y-m-d'), - "foo", - new EdgeQRCodeMoney( - 100, - "CZK" - ), - new EdgeQRCodeQROptions( - 1, - 1 - ), - new EdgeQRCodePaymentIdentification( - "0", "0", "0" - ) - ); + [$edgeEntity, $qrCodeEntity] = $this->createQRCodeEntityPair(); - $entity = $this->createMock(QRCode::class); - - $qrCodeProvider = $this->createQRCodeProvider($failureRequest, 500, $edgeEntity, $entity); - $data = $qrCodeProvider->generateQRCodeFromEntity($entity); + $qrCodeProvider = $this->createQRCodeProvider($failureRequest, 500, $edgeEntity, $qrCodeEntity); + $data = $qrCodeProvider->generateQRCodeFromEntity($qrCodeEntity); $this->assertEquals($failureRequest["data"]["base64Data"], $data); } @@ -107,4 +77,42 @@ class UsetrenoQRCodeProviderTest extends TestCase return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock); } + + protected function createQRCodeEntityPair() { + $edgeEntity = new EdgeQRCode( + "CZ0000", + (new \DateTime("now"))->format('Y-m-d'), + "foo", + new EdgeQRCodeMoney( + 100, + "CZK" + ), + new EdgeQRCodeQROptions( + 1, + 1 + ), + new EdgeQRCodePaymentIdentification( + "0", "0", "0" + ) + ); + + $qrCodeEntity = new QRCode( + "CZ0000", + new \DateTimeImmutable("now"), + "foo", + new QRCodeMoney( + 100, + "CZK" + ), + new QRCodeQROptions( + 1, + 1 + ), + new QRCodePaymentIdentification( + "0", "0", "0" + ) + ); + + return [$edgeEntity, $qrCodeEntity]; + } } From 192cfb9b73a5eccd1b2622199e45d8ce9be4b3c0 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 18:37:41 +0100 Subject: [PATCH 13/17] feat: add support for OTEL logs feat: add support for OTEL logs --- README.md | 1 + composer.json | 1 + composer.lock | 60 ++++++++++++++++++- config/packages/monolog.yaml | 7 +++ config/services.yaml | 1 + docker/telemetry/otelcol-config.yaml | 40 +++++++++---- .../Monolog/Handler/SymfonyOtelHandler.php | 33 ++++++++++ 7 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 src/Bridge/Monolog/Handler/SymfonyOtelHandler.php diff --git a/README.md b/README.md index dfb5e0d..b043552 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,4 @@ TODO: - [ ] Chybí speciální slovník nebo vypnutí slovníku pro testy - [ ] V reálný aplikaci bych použil Mockery, nicméně tady mě to přijde zbytečný - [ ] Nastavení cache ideálně v memcached/redis etc. +- [ ] Vylepsit OTEL logs diff --git a/composer.json b/composer.json index 5ee109a..a749567 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "nyholm/psr7": "*", "open-telemetry/exporter-otlp": "^1.0", "open-telemetry/opentelemetry-auto-symfony": "^1.0@beta", + "open-telemetry/opentelemetry-logger-monolog": "^1.0", "open-telemetry/opentelemetry-propagation-server-timing": "^0.0.1", "open-telemetry/opentelemetry-propagation-traceresponse": "^0.0.2", "open-telemetry/sdk": "^1.0", diff --git a/composer.lock b/composer.lock index 140508e..8a63298 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "15e05e23293f0f945b50f450fe8213c3", + "content-hash": "a62a11e50f44a0e917cf2ab753b0a5d8", "packages": [ { "name": "composer/semver", @@ -761,6 +761,64 @@ }, "time": "2023-12-12T11:41:45+00:00" }, + { + "name": "open-telemetry/opentelemetry-logger-monolog", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/contrib-logger-monolog.git", + "reference": "da70f3678aba1e8187f889e78f3a2a55f43f6395" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/contrib-logger-monolog/zipball/da70f3678aba1e8187f889e78f3a2a55f43f6395", + "reference": "da70f3678aba1e8187f889e78f3a2a55f43f6395", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.1|^2|^3", + "open-telemetry/api": "^1.0.0beta16" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "google/protobuf": ">=3.5.0", + "guzzlehttp/guzzle": "^7.4", + "nyholm/psr7": "^1.6", + "open-telemetry/exporter-otlp": ">=1.0.0beta6", + "open-telemetry/sdk": ">=1.0.0beta7", + "phan/phan": "^5.0", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.16", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OpenTelemetry\\Contrib\\Logs\\Monolog\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "OpenTelemetry Monolog handler.", + "homepage": "https://opentelemetry.io", + "keywords": [ + "handler", + "logs", + "monolog", + "open-telemetry", + "opentelemetry", + "otel" + ], + "support": { + "source": "https://github.com/opentelemetry-php/contrib-logger-monolog/tree/1.0.0" + }, + "time": "2023-10-17T21:44:43+00:00" + }, { "name": "open-telemetry/opentelemetry-propagation-server-timing", "version": "0.0.1", diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 8c9efa9..955e0e0 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -5,6 +5,10 @@ monolog: when@dev: monolog: handlers: + otel: # Tohle bych v realny aplikaci nepouzil na devu + type: service + id: App\Bridge\Monolog\Handler\SymfonyOtelHandler + main: type: stream path: "%kernel.logs_dir%/%kernel.environment%.log" @@ -40,6 +44,9 @@ when@test: when@prod: monolog: handlers: + otel: + type: service + id: App\Bridge\Monolog\Handler\SymfonyOtelHandler main: type: fingers_crossed action_level: error diff --git a/config/services.yaml b/config/services.yaml index f96070f..3118bb2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -45,6 +45,7 @@ services: $innerGenerator: '@App\Service\Remote\UsetrenoQRCodeProvider' $cacheDuration: 'PT60S' + App\Service\CurrencyListerInterface: '@App\Service\StaticCurrencyLister' App\Service\QRCodeQROptionsProviderInterface: '@App\Service\QRCodeQROptionsDefaultProvider' App\Service\QRCodeGeneratorInterface: '@App\Service\CachedQRCodeGenerator' diff --git a/docker/telemetry/otelcol-config.yaml b/docker/telemetry/otelcol-config.yaml index d087da7..ebbe007 100644 --- a/docker/telemetry/otelcol-config.yaml +++ b/docker/telemetry/otelcol-config.yaml @@ -11,22 +11,40 @@ processors: actions: - action: insert key: loki.attribute.labels - value: log.file, log.line, log.module_path, http.request_id, http.uri + value: context, code.filepath, code.namespace, code.function, code.lineno, http.request.method, http.request.body.size, url.full, url.scheme, url.path, http.route, http.response.status_code - action: insert - from_attribute: log.file - key: log.file + from_attribute: context + key: context - action: insert - from_attribute: log.line - key: log.line + from_attribute: code.namespace + key: code.namespace - action: insert - from_attribute: log.module_path - key: log.module_path + from_attribute: code.function + key: code.function - action: insert - from_attribute: http.request_id - key: http.request_id + from_attribute: code.lineno + key: code.lineno - action: insert - from_attribute: http.uri - key: http.uri + from_attribute: http.request.method + key: http.request.method + - action: insert + from_attribute: http.request.body.size + key: http.request.body.size + - action: insert + from_attribute: http.response.status_code + key: http.response.status_code + - action: insert + from_attribute: url.full + key: url.full + - action: insert + from_attribute: url.scheme + key: url.scheme + - action: insert + from_attribute: url.path + key: url.path + - action: insert + from_attribute: http.route + key: http.route - action: insert key: loki.format value: raw diff --git a/src/Bridge/Monolog/Handler/SymfonyOtelHandler.php b/src/Bridge/Monolog/Handler/SymfonyOtelHandler.php new file mode 100644 index 0000000..c7f4448 --- /dev/null +++ b/src/Bridge/Monolog/Handler/SymfonyOtelHandler.php @@ -0,0 +1,33 @@ +innerHandler = new Handler( + Globals::loggerProvider(), + LogLevel::INFO, //or `Logger::INFO`, or `Level::Info` depending on monolog version + true, + ); + } + + public function handle(LogRecord $record): bool + { + return $this->innerHandler->handle($record); + } +} From 13bdd096be775b5c5b1307b15c68e40e343bd26c Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 18:48:58 +0100 Subject: [PATCH 14/17] feat: add sentry feat: add sentry --- .env | 4 + composer.json | 1 + composer.lock | 1134 +++++++++++++++++++++++- config/bundles.php | 1 + config/packages/sentry.yaml | 26 + src/Controller/ExceptionController.php | 17 + symfony.lock | 12 + 7 files changed, 1194 insertions(+), 1 deletion(-) create mode 100644 config/packages/sentry.yaml create mode 100644 src/Controller/ExceptionController.php diff --git a/.env b/.env index 8a5077a..7edc32e 100644 --- a/.env +++ b/.env @@ -18,3 +18,7 @@ APP_ENV=dev APP_SECRET=f27f6cb11fb594fcb5ab4591ec0aac77 ###< symfony/framework-bundle ### + +###> sentry/sentry-symfony ### +SENTRY_DSN= +###< sentry/sentry-symfony ### diff --git a/composer.json b/composer.json index a749567..89dd5e7 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "open-telemetry/sdk": "^1.0", "open-telemetry/transport-grpc": "^1.0", "php-http/httplug": "*", + "sentry/sentry-symfony": "^4.13", "symfony/asset": "7.0.*", "symfony/asset-mapper": "7.0.*", "symfony/cache": "7.0.*", diff --git a/composer.lock b/composer.lock index 8a63298..67eaf0e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,74 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a62a11e50f44a0e917cf2ab753b0a5d8", + "content-hash": "61ab226932ff1cf0567a22bbf802e20c", "packages": [ + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, { "name": "composer/semver", "version": "3.4.0", @@ -258,6 +324,239 @@ ], "time": "2023-12-03T20:19:20+00:00" }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7||^2.0", + "php": ">=7.3", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/http-interop/http-factory-guzzle/issues", + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + }, + "time": "2021-07-21T13:50:14+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.17", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^0.12.66", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + }, + "time": "2021-10-08T21:21:46+00:00" + }, { "name": "monolog/monolog", "version": "3.5.0", @@ -1134,6 +1433,75 @@ }, "time": "2023-09-05T03:38:44+00:00" }, + { + "name": "php-http/client-common", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.1" + }, + "time": "2023-11-30T10:31:25+00:00" + }, { "name": "php-http/discovery", "version": "1.19.2", @@ -1269,6 +1637,130 @@ }, "time": "2023-04-14T15:10:03+00:00" }, + { + "name": "php-http/message", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.0" + }, + "time": "2023-05-17T06:43:38+00:00" + }, + { + "name": "php-http/message-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/1.1.0" + }, + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" + }, { "name": "php-http/promise", "version": "1.3.0", @@ -1683,6 +2175,321 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sentry/sdk", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php-sdk.git", + "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/24c235ff2027401cbea099bf88689e1a1f197c7a", + "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a", + "shasum": "" + }, + "require": { + "http-interop/http-factory-guzzle": "^1.0", + "sentry/sentry": "^3.22", + "symfony/http-client": "^4.3|^5.0|^6.0|^7.0" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "This is a metapackage shipping sentry/sentry with a recommended HTTP client.", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php-sdk/issues", + "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.6.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-12-04T10:49:33+00:00" + }, + { + "name": "sentry/sentry", + "version": "3.22.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/promises": "^1.5.3|^2.0", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "php-http/async-client-implementation": "^1.0", + "php-http/client-common": "^1.5|^2.0", + "php-http/discovery": "^1.15", + "php-http/httplug": "^1.1|^2.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0|^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "conflict": { + "php-http/client-common": "1.8.0", + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19|3.4.*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "http-interop/http-factory-guzzle": "^1.0", + "monolog/monolog": "^1.6|^2.0|^3.0", + "nikic/php-parser": "^4.10.3", + "php-http/mock-client": "^1.3", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5.14|^9.4", + "symfony/phpunit-bridge": "^5.2|^6.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "A PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/3.22.1" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-11-13T11:47:28+00:00" + }, + { + "name": "sentry/sentry-symfony", + "version": "4.13.2", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-symfony.git", + "reference": "bf049e69863465f2e0ba2555dbb5224641a37d67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/bf049e69863465f2e0ba2555dbb5224641a37d67", + "reference": "bf049e69863465f2e0ba2555dbb5224641a37d67", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7 || ^2.0", + "jean85/pretty-package-versions": "^1.5 || ^2.0", + "php": "^7.2||^8.0", + "sentry/sdk": "^3.6", + "sentry/sentry": "^3.22.1", + "symfony/cache-contracts": "^1.1||^2.4||^3.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/polyfill-php80": "^1.22", + "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0", + "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0" + }, + "require-dev": { + "doctrine/dbal": "^2.13||^3.0", + "doctrine/doctrine-bundle": "^1.12||^2.5", + "friendsofphp/php-cs-fixer": "^2.19||^3.40", + "masterminds/html5": "^2.8", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-symfony": "^1.0", + "phpunit/phpunit": "^8.5.14||^9.3.9", + "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/monolog-bundle": "^3.4", + "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0", + "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0", + "vimeo/psalm": "^4.3||^5.16.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.", + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.", + "symfony/cache": "Allow distributed tracing of cache pools using Sentry.", + "symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "releases/3.2.x": "3.2.x-dev", + "releases/2.x": "2.x-dev", + "releases/1.x": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/aliases.php" + ], + "psr-4": { + "Sentry\\SentryBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Cramer", + "email": "dcramer@gmail.com" + }, + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "Symfony integration for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "errors", + "logging", + "sentry", + "symfony" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-symfony/issues", + "source": "https://github.com/getsentry/sentry-symfony/tree/4.13.2" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2024-01-11T14:55:45+00:00" + }, { "name": "symfony/asset", "version": "v7.0.0", @@ -3641,6 +4448,78 @@ ], "time": "2023-08-08T10:20:21+00:00" }, + { + "name": "symfony/password-hasher", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "d2da68c2f7a240bd6edf7e96fdc7aca5e7beea66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/d2da68c2f7a240bd6edf7e96fdc7aca5e7beea66", + "reference": "d2da68c2f7a240bd6edf7e96fdc7aca5e7beea66", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v7.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-07T10:26:03+00:00" + }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.28.0", @@ -4215,6 +5094,89 @@ ], "time": "2023-11-25T08:38:27+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "c5e973032e9a32c6f1bfa87d7832853b84cbaf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/c5e973032e9a32c6f1bfa87d7832853b84cbaf22", + "reference": "c5e973032e9a32c6f1bfa87d7832853b84cbaf22", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-28T19:18:20+00:00" + }, { "name": "symfony/routing", "version": "v7.0.2", @@ -4375,6 +5337,176 @@ ], "time": "2023-10-20T16:35:23+00:00" }, + { + "name": "symfony/security-core", + "version": "v7.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "2ba040de8e6d93e07edc7307dc75b42e06137405" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/2ba040de8e6d93e07edc7307dc75b42e06137405", + "reference": "2ba040de8e6d93e07edc7307dc75b42e06137405", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/ldap": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/ldap": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v7.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-30T11:04:23+00:00" + }, + { + "name": "symfony/security-http", + "version": "v7.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "acc9931d75cd16de08b1663223cb8ab36f61cc0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/acc9931d75cd16de08b1663223cb8ab36f61cc0c", + "reference": "acc9931d75cd16de08b1663223cb8ab36f61cc0c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-30T11:04:23+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.4.1", diff --git a/config/bundles.php b/config/bundles.php index 1982a0d..50bcb8c 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -6,4 +6,5 @@ return [ Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Sentry\SentryBundle\SentryBundle::class => ['prod' => true], ]; diff --git a/config/packages/sentry.yaml b/config/packages/sentry.yaml new file mode 100644 index 0000000..9e4a04e --- /dev/null +++ b/config/packages/sentry.yaml @@ -0,0 +1,26 @@ +when@prod: + sentry: + dsn: '%env(SENTRY_DSN)%' + # this hooks into critical paths of the framework (and vendors) to perform + # automatic instrumentation (there might be some performance penalty) + # https://docs.sentry.io/platforms/php/guides/symfony/performance/instrumentation/automatic-instrumentation/ + tracing: + enabled: false + +# If you are using Monolog, you also need this additional configuration to log the errors correctly: +# https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration +# register_error_listener: false +# register_error_handler: false + +# monolog: +# handlers: +# sentry: +# type: sentry +# level: !php/const Monolog\Logger::ERROR +# hub_id: Sentry\State\HubInterface + +# Uncomment these lines to register a log message processor that resolves PSR-3 placeholders +# https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration +# services: +# Monolog\Processor\PsrLogMessageProcessor: +# tags: { name: monolog.processor, handler: sentry } diff --git a/src/Controller/ExceptionController.php b/src/Controller/ExceptionController.php new file mode 100644 index 0000000..5d86fb0 --- /dev/null +++ b/src/Controller/ExceptionController.php @@ -0,0 +1,17 @@ + Date: Thu, 18 Jan 2024 20:48:20 +0100 Subject: [PATCH 15/17] feat: add nubium/this-should-never-happen-exception --- composer.json | 1 + composer.lock | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 89dd5e7..0485b10 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "ext-iconv": "*", "grpc/grpc": "^1.57", "guzzlehttp/promises": "*", + "nubium/this-should-never-happen-exception": "^1.0", "nyholm/psr7": "*", "open-telemetry/exporter-otlp": "^1.0", "open-telemetry/opentelemetry-auto-symfony": "^1.0@beta", diff --git a/composer.lock b/composer.lock index 67eaf0e..d4f1c34 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "61ab226932ff1cf0567a22bbf802e20c", + "content-hash": "60708df22ad1ee4eca712d46f8e02c3b", "packages": [ { "name": "clue/stream-filter", @@ -658,6 +658,47 @@ ], "time": "2023-10-27T15:32:31+00:00" }, + { + "name": "nubium/this-should-never-happen-exception", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/nubium/this-should-never-happen-exception.git", + "reference": "3ed1b6f725881c527050c235e2503a8300427b86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nubium/this-should-never-happen-exception/zipball/3ed1b6f725881c527050c235e2503a8300427b86", + "reference": "3ed1b6f725881c527050c235e2503a8300427b86", + "shasum": "" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "~1.0", + "phpstan/phpstan": "~0.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nubium\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jiri Travnicek", + "email": "jiri.travnicek@nubium.cz" + } + ], + "description": "Extend this exception and throw it anytime something unexpected happens.", + "support": { + "issues": "https://github.com/nubium/this-should-never-happen-exception/issues", + "source": "https://github.com/nubium/this-should-never-happen-exception/tree/master" + }, + "time": "2018-03-27T10:16:09+00:00" + }, { "name": "nyholm/psr7", "version": "1.8.1", From 7eac908bc458980e28f878a85bc8fe5ecceb48d9 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 20:49:33 +0100 Subject: [PATCH 16/17] feat: add retry client trait for api requests feat: add retry client trait for api requests --- .../Remote/RetryingFailClientTrait.php | 58 +++++++++++++++++++ .../Remote/RetryingFailClientTraitTest.php | 56 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/Service/Remote/RetryingFailClientTrait.php create mode 100644 tests/Service/Remote/RetryingFailClientTraitTest.php diff --git a/src/Service/Remote/RetryingFailClientTrait.php b/src/Service/Remote/RetryingFailClientTrait.php new file mode 100644 index 0000000..72d572e --- /dev/null +++ b/src/Service/Remote/RetryingFailClientTrait.php @@ -0,0 +1,58 @@ + $catchableExceptions + * @param LoggerInterface $logger + * @param callable $callback + * @return mixed + * @throws Exception + */ + protected function retryingFailRequest( + int $count, + float $sleep, + array $catchableExceptions, + LoggerInterface $logger, + callable $callback + ): mixed { + for ($i = 0; ; $i++) { + try { + return $callback(); + } catch (Exception $e) { + foreach ($catchableExceptions as $exceptionClass) { + if ($e instanceof $exceptionClass) { + $logger->error("transport: fail request retrying... got catchable exception", [ + 'exception' => $e, + 'try' => $i + ]); + + usleep((int) ($sleep * 1_000_000)); + + if ($i == $count) { + throw $e; + } + + continue 2; + } + } + + throw $e; + } + } + + // phpstan fail + return null; + } +} diff --git a/tests/Service/Remote/RetryingFailClientTraitTest.php b/tests/Service/Remote/RetryingFailClientTraitTest.php new file mode 100644 index 0000000..e945864 --- /dev/null +++ b/tests/Service/Remote/RetryingFailClientTraitTest.php @@ -0,0 +1,56 @@ +callCount = 0; + } + + public function testSuccess() { + $trait = new class { + use RetryingFailClientTrait { + retryingFailRequest as public; // make the method public + } + }; + + + $result = $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () { + $this->callCount = $this->callCount + 1; + return 'foo'; + }); + + $this->assertEquals(1, $this->callCount); + $this->assertEquals('foo', $result); + } + public function testRetyingFail() { + $trait = new class { + use RetryingFailClientTrait { + retryingFailRequest as public; // make the method public + } + }; + + + try { + $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () { + $this->callCount = $this->callCount + 1; + throw new \RuntimeException("test"); + }); + } catch (\RuntimeException) { + // do nothing + } + + $this->assertEquals(3, $this->callCount); + } +} From 6e5f929dc31c596c54f604e874fad69f30bc6f9e Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Thu, 18 Jan 2024 20:50:23 +0100 Subject: [PATCH 17/17] feat: retries of api requests in case of failure --- config/services.yaml | 7 +++ src/Entity/Remote/Usetreno/AuthRequest.php | 1 + ...etrenoQRCodeRemoteServerErrorException.php | 9 +++ src/Service/Remote/UsetrenoHttpClient.php | 51 +++++++++++------ src/Service/Remote/UsetrenoQRCodeProvider.php | 57 +++++++++++++------ .../Service/Remote/UsetrenoHttpClientTest.php | 4 +- .../Remote/UsetrenoQRCodeProviderTest.php | 2 +- 7 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 src/Service/Remote/Exception/UsetrenoQRCodeRemoteServerErrorException.php diff --git a/config/services.yaml b/config/services.yaml index 3118bb2..f04a8e2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -39,6 +39,13 @@ services: arguments: $username: '%app.usetreno.username%' $password: '%app.usetreno.password%' + $retryCount: 2 + $retryWaitSeconds: 0.5 + + App\Service\Remote\UsetrenoQRCodeProvider: + arguments: + $retryCount: 2 + $retryWaitSeconds: 0.5 App\Service\CachedQRCodeGenerator: arguments: diff --git a/src/Entity/Remote/Usetreno/AuthRequest.php b/src/Entity/Remote/Usetreno/AuthRequest.php index 2d475d5..a13779e 100644 --- a/src/Entity/Remote/Usetreno/AuthRequest.php +++ b/src/Entity/Remote/Usetreno/AuthRequest.php @@ -1,4 +1,5 @@ static::AUTHORIZE_API ]); - $rq = $this->innerClient->request( - "POST", - static::AUTHORIZE_API, - [ - 'json' => new AuthRequest($this->username, $this->password), - ] - ); + $responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds, + [AuthorizeException::class, TransportException::class], $this->logger, function() { + $rq = $this->innerClient->request( + "POST", + static::AUTHORIZE_API, + [ + 'json' => new AuthRequest($this->username, $this->password), + ] + ); - $statusCode = $rq->getStatusCode(); - $responseData = $rq->getContent(false); + $statusCode = $rq->getStatusCode(); + $responseData = $rq->getContent(false); - if ($statusCode != 200) { - $this->logger->error("authorization request status code is not ok", [ - "AUTHORIZE_API" => static::AUTHORIZE_API, - "statusCode" => $statusCode, - "content" => $responseData, - "username" => $this->username - ]); + if ($statusCode != 200) { + $this->logger->error("authorization request status code is not ok", [ + "AUTHORIZE_API" => static::AUTHORIZE_API, + "statusCode" => $statusCode, + "content" => $responseData, + "username" => $this->username + ]); - throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)"); + throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)"); + } + + return $responseData; + }); + + if (!is_string($responseData)) { + throw new ThisShouldNeverHappenException("responseData is not a string"); } $this->authorizationToken = $this->processAuthorizeResponse($responseData); diff --git a/src/Service/Remote/UsetrenoQRCodeProvider.php b/src/Service/Remote/UsetrenoQRCodeProvider.php index fe3d6bf..c824e32 100644 --- a/src/Service/Remote/UsetrenoQRCodeProvider.php +++ b/src/Service/Remote/UsetrenoQRCodeProvider.php @@ -7,16 +7,23 @@ use App\Entity\DTO\QRCode\QRCode; use App\Service\CacheableQRCodeGeneratorInterface; use App\Service\Remote\Edge\QRCodeEntityConverter; use App\Service\Remote\Exception\UsetrenoQRCodeException; +use App\Service\Remote\Exception\UsetrenoQRCodeRemoteServerErrorException; +use Nubium\Exception\ThisShouldNeverHappenException; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\Exception\TransportException; class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface { + use RetryingFailClientTrait; + const QRCODE_API = 'https://topapi.top-test.cz/chameleon/api/v1/qr-code/create-for-bank-account-payment'; const QRCODE_METHOD = 'POST'; public function __construct(protected readonly LoggerInterface $logger, protected readonly UsetrenoHttpClient $client, - protected readonly QRCodeEntityConverter $codeEntityConverter) { } + protected readonly QRCodeEntityConverter $codeEntityConverter, + protected readonly float $retryWaitSeconds, + protected readonly int $retryCount) { } public function generateQRCodeFromEntity(QRCode $entity): string { @@ -27,27 +34,41 @@ class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface "edgeEntity" => $edgeEntity ]); - $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [ - 'json' => $edgeEntity, - ]); + $responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds, + [TransportException::class, UsetrenoQRCodeRemoteServerErrorException::class], $this->logger, + function() use ($edgeEntity) { + $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [ + 'json' => $edgeEntity, + ]); - $statusCode = $response->getStatusCode(); - $responseData = $response->getContent(false); + $statusCode = $response->getStatusCode(); + $responseData = $response->getContent(false); - if ($statusCode != 200) { - $this->logger->error("qrcode request status code is not ok", [ - "AUTHORIZE_API" => static::QRCODE_API, - "statusCode" => $statusCode, - "content" => $responseData, - ]); + if ($statusCode != 200) { + $this->logger->error("qrcode request status code is not ok", [ + "AUTHORIZE_API" => static::QRCODE_API, + "statusCode" => $statusCode, + "content" => $responseData, + ]); - throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)"); + if ($statusCode > 500) { + throw new UsetrenoQRCodeRemoteServerErrorException("Return code is not 200 OK (got: code: $statusCode)"); + } + + throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)"); + } + + $this->logger->debug("QRCode generation success", [ + "responseContent" => $responseData, + ]); + + return $responseData; + }); + + if (!is_string($responseData)) { + throw new ThisShouldNeverHappenException("responseData is not a string"); } - - $this->logger->debug("QRCode generation success", [ - "responseContent" => $responseData, - ]); - + return $this->parseBase64String($this->processQRCodeResponseEntity($responseData)); } diff --git a/tests/Service/Remote/UsetrenoHttpClientTest.php b/tests/Service/Remote/UsetrenoHttpClientTest.php index 92d7ef8..af7845d 100644 --- a/tests/Service/Remote/UsetrenoHttpClientTest.php +++ b/tests/Service/Remote/UsetrenoHttpClientTest.php @@ -24,7 +24,7 @@ class UsetrenoHttpClientTest extends TestCase ]); $authorizedRequestResponse = clone $authMockResponse; $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]); - $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar"); + $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0); $client->request("POST", "https://www.root.cz/"); $this->assertEquals("https://topapi.top-test.cz/chameleon/api/v1/token", $authMockResponse->getRequestUrl()); $headers = $authorizedRequestResponse->getRequestOptions()['headers']; @@ -48,7 +48,7 @@ class UsetrenoHttpClientTest extends TestCase $authorizedRequestResponse = clone $authMockResponse; $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]); - $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar"); + $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0); $client->request("POST", "https://www.root.cz/"); } } diff --git a/tests/Service/Remote/UsetrenoQRCodeProviderTest.php b/tests/Service/Remote/UsetrenoQRCodeProviderTest.php index dd7e602..d5d7e12 100644 --- a/tests/Service/Remote/UsetrenoQRCodeProviderTest.php +++ b/tests/Service/Remote/UsetrenoQRCodeProviderTest.php @@ -75,7 +75,7 @@ class UsetrenoQRCodeProviderTest extends TestCase ->method('convert') ->will($this->returnValue($edgeEntity)); - return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock); + return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock, 0, 0); } protected function createQRCodeEntityPair() {