Compare commits

...

8 Commits

14 changed files with 83 additions and 25 deletions

View File

@ -1,6 +1,34 @@
symfony_example_app
===================
Generátor QR kódů pro platby z https://topapi.top-test.cz
Quick setup
-----------
```bash
# git clone https://git.nanobyte.cz/nanobyte-public/symfony_example_app.git
# cd symfony_example_app
# docker compose run --build php-fpm composer install
# docker compose up --build
```
- aplikace je dostupná na http://localhost:8000/
- na adrese http://localhost:3000/explore je běžící grafana (s loki a tempo)
- aplikace je nastavená (i na lokálu, běžně bych to nastavil až na devech/stage/PROD) aby posílala logy do lokiho a tracing do tempa pomocí otel protokolu
TODO: TODO:
----- -----
- [ ] Chybí speciální slovník nebo vypnutí slovníku pro testy - [ ] 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ý - [ ] 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. - [ ] Nastavení cache ideálně v memcached/redis etc.
- [ ] Vylepsit OTEL logs - [ ] Vyhezkat OTEL logs, OTEL tracing
- [ ] CI pipelines
- [ ] k8s deployment
- [ ] prometheus country na počet requestů/api (počet 200OK/500ERR)
- [ ] Je dost na zvážení zda nezobrazit výsledek remote validace (response_create_400.json) a nenechat uživatele špatné hodnoty opravit. Ovšem znamená to že lokální validátory jsou špatně, chybu by bylo vhodné zalogovat do Sentry (do logu etc.) a opravit ji...
- [ ] Na produkci bych statický content rozhodně netlačil přes app container ale přes static nginx container (asset-map:compile -> copy do nginx static containeru)
- [ ] xdebug v dockeru
Poznámky
--------
- Nejsem kodér (a javascript developer), nevypadá to nijak extra ;-)

View File

@ -31,7 +31,7 @@ services:
arguments: arguments:
$available_currencies: '%app.currencies%' $available_currencies: '%app.currencies%'
App\Service\StubQRCodeGenerator: App\Service\StubQRCodeProvider:
arguments: arguments:
$imagePath: '%kernel.project_dir%/assets/images/wip.png' $imagePath: '%kernel.project_dir%/assets/images/wip.png'
@ -47,7 +47,7 @@ services:
$retryCount: 2 $retryCount: 2
$retryWaitSeconds: 0.5 $retryWaitSeconds: 0.5
App\Service\CachedQRCodeGenerator: App\Service\CachedQRCodeProvider:
arguments: arguments:
$innerGenerator: '@App\Service\Remote\UsetrenoQRCodeProvider' $innerGenerator: '@App\Service\Remote\UsetrenoQRCodeProvider'
$cacheDuration: 'PT60S' $cacheDuration: 'PT60S'
@ -55,7 +55,7 @@ services:
App\Service\CurrencyListerInterface: '@App\Service\StaticCurrencyLister' App\Service\CurrencyListerInterface: '@App\Service\StaticCurrencyLister'
App\Service\QRCodeQROptionsProviderInterface: '@App\Service\QRCodeQROptionsDefaultProvider' App\Service\QRCodeQROptionsProviderInterface: '@App\Service\QRCodeQROptionsDefaultProvider'
App\Service\QRCodeGeneratorInterface: '@App\Service\CachedQRCodeGenerator' App\Service\QRCodeProviderInterface: '@App\Service\CachedQRCodeProvider'
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones

View File

@ -5,7 +5,7 @@ namespace App\Controller;
use App\Entity\Input\QRCode\QRCode; use App\Entity\Input\QRCode\QRCode;
use App\Form\Type\QRCodeType; use App\Form\Type\QRCodeType;
use App\Service\DTO\QRCodeEntityConverter; use App\Service\DTO\QRCodeEntityConverter;
use App\Service\QRCodeGeneratorInterface; use App\Service\QRCodeProviderInterface;
use App\Service\QRCodeQROptionsProviderInterface; use App\Service\QRCodeQROptionsProviderInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -16,8 +16,8 @@ class IndexController extends AbstractController {
public function __construct(private readonly QRCodeQROptionsProviderInterface $qrCodeQROptionsFactory, private readonly QRCodeEntityConverter $codeEntityConverter) {} public function __construct(private readonly QRCodeQROptionsProviderInterface $qrCodeQROptionsFactory, private readonly QRCodeEntityConverter $codeEntityConverter) {}
#[Route('/', name: 'homepage')] #[Route('/', name: 'homepage')]
public function indexAction( public function indexAction(
Request $request, Request $request,
QRCodeGeneratorInterface $qrCodeGenerator, QRCodeProviderInterface $qrCodeGenerator,
): Response ): Response
{ {
$qrCodeImage = null; $qrCodeImage = null;

View File

@ -23,7 +23,7 @@ class QRCodeMoneyType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('amount', NumberType::class) ->add('amount', NumberType::class, ['invalid_message' => 'messages.not_valid_amount'])
->add('currency', ChoiceType::class, [ ->add('currency', ChoiceType::class, [
'choices' => array_combine( 'choices' => array_combine(
iterator_to_array($this->currencyLister->getCurrencies()), iterator_to_array($this->currencyLister->getCurrencies()),

View File

@ -5,7 +5,7 @@ namespace App\Service;
use App\Entity\DTO\QRCode\QRCode; use App\Entity\DTO\QRCode\QRCode;
interface CacheableQRCodeGeneratorInterface extends QRCodeGeneratorInterface interface CacheableQRCodeProviderInterface extends QRCodeProviderInterface
{ {
// We can't calculate cache key directly from QRCode // We can't calculate cache key directly from QRCode
public function getCacheKey(QRCode $entity): string; public function getCacheKey(QRCode $entity): string;

View File

@ -10,14 +10,14 @@ use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
final readonly class CachedQRCodeGenerator implements QRCodeGeneratorInterface final readonly class CachedQRCodeProvider implements QRCodeProviderInterface
{ {
private \DateInterval $cacheDuration; private \DateInterval $cacheDuration;
/** /**
* @throws Exception * @throws Exception
*/ */
public function __construct(private CacheableQRCodeGeneratorInterface $innerGenerator, private CacheInterface $cache, private LoggerInterface $logger, string $cacheDuration) public function __construct(private CacheableQRCodeProviderInterface $innerGenerator, private CacheInterface $cache, private LoggerInterface $logger, string $cacheDuration)
{ {
$this->cacheDuration = new \DateInterval($cacheDuration); $this->cacheDuration = new \DateInterval($cacheDuration);
} }

View File

@ -6,6 +6,7 @@ use App\Entity\DTO\QRCode\QRCode;
use App\Entity\DTO\QRCode\QRCodeMoney; use App\Entity\DTO\QRCode\QRCodeMoney;
use App\Entity\DTO\QRCode\QRCodePaymentIdentification; use App\Entity\DTO\QRCode\QRCodePaymentIdentification;
use App\Entity\DTO\QRCode\QRCodeQROptions; use App\Entity\DTO\QRCode\QRCodeQROptions;
use Nubium\Exception\ThisShouldNeverHappenException;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
final readonly class QRCodeEntityConverter final readonly class QRCodeEntityConverter
@ -20,11 +21,11 @@ final readonly class QRCodeEntityConverter
} }
return new QRCode( return new QRCode(
$code->getIban() ?? throw new \InvalidArgumentException("iban not set"), $code->getIban() ?? throw new ThisShouldNeverHappenException("iban not set"),
\DateTimeImmutable::createFromMutable($code->getDueDate() ?? throw new \InvalidArgumentException("due date not set")), \DateTimeImmutable::createFromMutable($code->getDueDate() ?? throw new ThisShouldNeverHappenException("due date not set")),
$code->getMessage() ?? throw new \InvalidArgumentException("message not set"), $code->getMessage() ?? throw new ThisShouldNeverHappenException("message not set"),
$this->convertMoney($code->getMoney() ?? throw new \InvalidArgumentException("money not set")), $this->convertMoney($code->getMoney() ?? throw new ThisShouldNeverHappenException("money not set")),
$this->convertCodeOptions($code->getCodeQROptions() ?? throw new \InvalidArgumentException("codeQROptions not set")), $this->convertCodeOptions($code->getCodeQROptions() ?? throw new ThisShouldNeverHappenException("codeQROptions not set")),
$this->convertIdentification($code->getPaymentIdentification()) $this->convertIdentification($code->getPaymentIdentification())
); );
} }
@ -32,8 +33,8 @@ final readonly class QRCodeEntityConverter
protected function convertMoney(\App\Entity\Input\QRCode\QRCodeMoney $money): QRCodeMoney protected function convertMoney(\App\Entity\Input\QRCode\QRCodeMoney $money): QRCodeMoney
{ {
return new QRCodeMoney( return new QRCodeMoney(
$money->getAmount() ?? throw new \InvalidArgumentException("amount not set"), $money->getAmount() ?? throw new ThisShouldNeverHappenException("amount not set"),
$money->getCurrency() ?? throw new \InvalidArgumentException("currency not set") $money->getCurrency() ?? throw new ThisShouldNeverHappenException("currency not set")
); );
} }

View File

@ -6,7 +6,7 @@ namespace App\Service;
use App\Entity\DTO\QRCode\QRCode; use App\Entity\DTO\QRCode\QRCode;
use App\Service\Exception\QRCodeGeneratorException; use App\Service\Exception\QRCodeGeneratorException;
interface QRCodeGeneratorInterface interface QRCodeProviderInterface
{ {
/** /**
* Generates QR code from entity * Generates QR code from entity

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service\Remote; namespace App\Service\Remote;
use App\Entity\DTO\QRCode\QRCode; use App\Entity\DTO\QRCode\QRCode;
use App\Service\CacheableQRCodeGeneratorInterface; use App\Service\CacheableQRCodeProviderInterface;
use App\Service\Remote\Edge\QRCodeEntityConverter; use App\Service\Remote\Edge\QRCodeEntityConverter;
use App\Service\Remote\Exception\UsetrenoQRCodeException; use App\Service\Remote\Exception\UsetrenoQRCodeException;
use App\Service\Remote\Exception\UsetrenoQRCodeRemoteServerErrorException; use App\Service\Remote\Exception\UsetrenoQRCodeRemoteServerErrorException;
@ -12,7 +12,7 @@ use Nubium\Exception\ThisShouldNeverHappenException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Exception\TransportException;
class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface class UsetrenoQRCodeProvider implements CacheableQRCodeProviderInterface
{ {
use RetryingFailClientTrait; use RetryingFailClientTrait;

View File

@ -5,7 +5,7 @@ namespace App\Service;
use App\Entity\DTO\QRCode\QRCode; use App\Entity\DTO\QRCode\QRCode;
readonly final class StubQRCodeGenerator implements QRCodeGeneratorInterface readonly final class StubQRCodeProvider implements QRCodeProviderInterface
{ {
public function __construct(private string $imagePath) {} public function __construct(private string $imagePath) {}

View File

@ -0,0 +1,12 @@
{% extends 'base.html.twig' %}
{% block body %}
<p>
<div class="container">
<div style="text-align: center;">
<h1>{{ "page not found" | trans }}</h1>
<a href="{{ path('homepage') }}">{{ "return to the homepage" | trans }}</a>
</div>
</div>
</p>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block body %}
<p>
<div class="container">
<div style="text-align: center;">
<h1>{{ "internal server error" | trans }}</h1>
{{ "our developers are doing their best to fix it. please try again later." | trans }}
<a href="{{ path('homepage') }}">{{ "return to the homepage" | trans }}</a>
</div>
</div>
</p>
{% endblock %}

View File

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace App\Tests\Controller; namespace App\Tests\Controller;
use App\Service\QRCodeGeneratorInterface; use App\Service\QRCodeProviderInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@ -76,7 +76,7 @@ class IndexControllerTest extends WebTestCase
private function generateClientForSubmitTest(): KernelBrowser private function generateClientForSubmitTest(): KernelBrowser
{ {
$mock = $this->createMock(QRCodeGeneratorInterface::class); $mock = $this->createMock(QRCodeProviderInterface::class);
$mock->expects($this->any()) $mock->expects($this->any())
->method('generateQRCodeFromEntity') ->method('generateQRCodeFromEntity')
->will($this->returnValue('foo')); ->will($this->returnValue('foo'));
@ -85,7 +85,7 @@ class IndexControllerTest extends WebTestCase
$client = static::createClient(); $client = static::createClient();
$client->disableReboot(); $client->disableReboot();
self::getContainer()->set('App\Service\QRCodeGeneratorInterface', $mock); self::getContainer()->set('App\Service\QRCodeProviderInterface', $mock);
return $client; return $client;
} }

View File

@ -9,3 +9,7 @@ Generate qr code: "Vygenerovat QR kód"
Iban: "IBAN" Iban: "IBAN"
QR code generator: "Generátor bankovních QR kódů" QR code generator: "Generátor bankovních QR kódů"
You can generate another QR code here: "Další QR kód si můžete vygenerovat zde" You can generate another QR code here: "Další QR kód si můžete vygenerovat zde"
page not found: 'Stránka nenalezena'
return to the homepage: 'Zpět na homepage'
"our developers are doing their best to fix it. please try again later.": "Naši programátoři dělají maximum aby to opravili. Zkuste to prosím později."
internal server error: 'Něco se pokazilo :-('