diff --git a/app/Template/StaticPageLayout/Turbo.php b/app/Template/StaticPageLayout/Turbo.php index a4064549823615ae0787b5e0545688cca0b8396f..63cce2d776f34acc1cd70a60f6012b7e82ad5fd0 100644 --- a/app/Template/StaticPageLayout/Turbo.php +++ b/app/Template/StaticPageLayout/Turbo.php @@ -80,7 +80,7 @@ abstract readonly class Turbo extends StaticPageLayout </head> <body> <main class="body-content website"> - <nav class="primary-navigation" id="primary-navigation"> + <nav class="primary-navigation"> HTML; yield from $this->renderPrimaryNavigation($staticPage); yield <<<'HTML' @@ -105,7 +105,7 @@ abstract readonly class Turbo extends StaticPageLayout yield from $this->renderPrimaryBanner($staticPage); yield from $this->renderBodyContent($staticPage); yield <<<HTML - <footer class="primary-footer" id="primary-footer"> + <footer class="primary-footer"> <div class="primary-footer__copyright"> Copyright © {$currentYear} Distantmagic. Built with Resonance. @@ -170,7 +170,7 @@ abstract readonly class Turbo extends StaticPageLayout protected function renderPrimaryBanner(StaticPage $currentPage): Generator { yield <<<'HTML' - <div class="primary-banner" id="primary-banner"> + <div class="primary-banner"> <iframe frameborder="0" height="30" diff --git a/bin/resonance.php b/bin/resonance.php index fffcb039a51004f62e0f9b7a15a86b80a703b4fe..246a43f996e76c8032327a0fc4ac4e6c5c576a92 100644 --- a/bin/resonance.php +++ b/bin/resonance.php @@ -2,18 +2,8 @@ declare(strict_types=1); -require_once __DIR__.'/../constants.php'; -require_once __DIR__.'/../vendor/autoload.php'; - use Distantmagic\Resonance\ConsoleApplication; -use Distantmagic\Resonance\DependencyInjectionContainer; -use Swoole\Runtime; - -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -$container = new DependencyInjectionContainer(); -$container->phpProjectFiles->indexDirectory(DM_RESONANCE_ROOT); -$container->phpProjectFiles->indexDirectory(DM_APP_ROOT); -$container->registerSingletons(); +$container = require_once __DIR__.'/../container.php'; exit($container->make(ConsoleApplication::class)->run()); diff --git a/container.php b/container.php new file mode 100644 index 0000000000000000000000000000000000000000..c1f745b655bccd9b73d26c1714bbe4f28ee5f77d --- /dev/null +++ b/container.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +require_once __DIR__.'/vendor/autoload.php'; + +defined('DM_ROOT') or exit('Configuration is not loaded.'); + +use Distantmagic\Resonance\DependencyInjectionContainer; +use Swoole\Runtime; + +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + +$container = new DependencyInjectionContainer(); +$container->phpProjectFiles->indexDirectory(DM_RESONANCE_ROOT); +$container->phpProjectFiles->indexDirectory(DM_APP_ROOT); +$container->registerSingletons(); + +return $container; diff --git a/docs/pages/docs/features/templating/twig/rendering-templates.md b/docs/pages/docs/features/templating/twig/rendering-templates.md index f00642b830a9aec25f40998c576cab78cb349d19..9b007000f161588b10f0b618d06a9d8528629ddb 100644 --- a/docs/pages/docs/features/templating/twig/rendering-templates.md +++ b/docs/pages/docs/features/templating/twig/rendering-templates.md @@ -166,3 +166,12 @@ Generates URL that points to the requested route: {{ 'pages.homepage'|trans(request) }} </a> ``` + +If you use just a string, it will try to use your `App\HttpRouteSymbol` file +to match the route: + +```twig +<a href="{{ route('Homepage') }}"> + {{ 'pages.homepage'|trans(request) }} +</a> +``` diff --git a/docs/pages/tutorials/session-based-authentication/index.md b/docs/pages/tutorials/session-based-authentication/index.md index 9dec331ff17e63e7625bf148875256b018e758d4..0725b59f13e87c58b6e95e10114d320f1093141a 100644 --- a/docs/pages/tutorials/session-based-authentication/index.md +++ b/docs/pages/tutorials/session-based-authentication/index.md @@ -260,7 +260,7 @@ final readonly class LoginForm extends HttpResponder ```twig file:app/views/auth/login_form.twig <form - action="{{ route(constant('App\\HttpRouteSymbol::LoginValidation')) }}" + action="{{ route('LoginValidation') }}" method="post" > <input @@ -543,7 +543,7 @@ final readonly class LogoutForm extends HttpResponder ``` ```twig file:app/views/auth/logout_form.twig <form - action="{{ route(constant('App\\HttpRouteSymbol::LogoutValidation')) }}" + action="{{ route('LogoutValidation') }}" method="post" > <input diff --git a/phpunit.xml b/phpunit.xml index fcf0695d0ea6fdbc5257fe5aa506293098ba28ca..18666a2256d8c0564157a64ec90cf78c4347eb27 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <phpunit - bootstrap="vendor/autoload.php" + bootstrap="phpunit_bootstrap.php" colors="true" displayDetailsOnTestsThatTriggerWarnings="true" > diff --git a/phpunit_bootstrap.php b/phpunit_bootstrap.php new file mode 100644 index 0000000000000000000000000000000000000000..d09e7bf3b5938d3023c19066e61e989f72054cae --- /dev/null +++ b/phpunit_bootstrap.php @@ -0,0 +1,6 @@ +<?php + +declare(strict_types=1); + +require_once __DIR__.'/vendor/autoload.php'; +require_once __DIR__.'/constants.php'; diff --git a/src/Command/PostfixBounce.php b/src/Command/PostfixBounce.php new file mode 100644 index 0000000000000000000000000000000000000000..6db07429a0c4650fa7fd306d1390e1b1c9113998 --- /dev/null +++ b/src/Command/PostfixBounce.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Command; + +use Distantmagic\Resonance\Attribute\ConsoleCommand; +use Distantmagic\Resonance\Attribute\WantsFeature; +use Distantmagic\Resonance\Command; +use Distantmagic\Resonance\CoroutineCommand; +use Distantmagic\Resonance\Event\MailBounced; +use Distantmagic\Resonance\EventDispatcherInterface; +use Distantmagic\Resonance\Feature; +use Distantmagic\Resonance\PostfixBounceAnalyzer; +use Distantmagic\Resonance\SwooleConfiguration; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +#[ConsoleCommand( + name: 'postfix:bounce', + description: 'Handles email bounces (requires mailparse)' +)] +#[WantsFeature(Feature::Postfix)] +final class PostfixBounce extends CoroutineCommand +{ + public function __construct( + private readonly EventDispatcherInterface $eventDispatcher, + private readonly LoggerInterface $logger, + private readonly PostfixBounceAnalyzer $postfixBounceAnalyzer, + SwooleConfiguration $swooleConfiguration, + ) { + parent::__construct($swooleConfiguration); + } + + protected function executeInCoroutine(InputInterface $input, OutputInterface $output): int + { + if (!extension_loaded('mailparse') || !extension_loaded('http')) { + throw new RuntimeException('You need to install "http" and "mailparse" extensions'); + } + + $content = stream_get_contents(STDIN); + + if (false === $content || empty($content)) { + throw new RuntimeException('Expected email contents in STDIN'); + } + + $postfixBounceReport = $this->postfixBounceAnalyzer->extractReport($content); + + if ($postfixBounceReport) { + $this->eventDispatcher->dispatch(new MailBounced($postfixBounceReport)); + } + + return Command::SUCCESS; + } +} diff --git a/src/DatabaseConnection.php b/src/DatabaseConnection.php index 281deba8bf88efe80802bdc10cb2f548c3d07330..51dbceeb7dd05b669f1272fb1873bd368b834f02 100644 --- a/src/DatabaseConnection.php +++ b/src/DatabaseConnection.php @@ -116,11 +116,7 @@ readonly class DatabaseConnection implements ServerInfoAwareConnection $pdoPreparedStatement = $this->pdo->prepare($sql); $pdoPreparedStatement = $this->assertNotFalse($pdoPreparedStatement); - return new DatabasePreparedStatement( - $this->databaseConnectionPoolRepository->eventDispatcher, - $pdoPreparedStatement, - $sql, - ); + return new DatabasePreparedStatement($pdoPreparedStatement); } public function query(string $sql): DatabaseExecutedStatement diff --git a/src/DatabaseConnectionPoolRepository.php b/src/DatabaseConnectionPoolRepository.php index c4ee1a367a96e148908a73468b457ee8152b311e..b4e9bacbae6919fd17a0ee3d5e38793117a0f51f 100644 --- a/src/DatabaseConnectionPoolRepository.php +++ b/src/DatabaseConnectionPoolRepository.php @@ -18,7 +18,7 @@ readonly class DatabaseConnectionPoolRepository */ public Map $databaseConnectionPool; - public function __construct(public EventDispatcherInterface $eventDispatcher) + public function __construct() { $this->databaseConnectionPool = new Map(); } diff --git a/src/DatabasePreparedStatement.php b/src/DatabasePreparedStatement.php index 2eee6965ddfcbd6db9786e9b2a54bef115e9fedb..67f1a66292b132cddd23c57d7d91eebbe7240ca5 100644 --- a/src/DatabasePreparedStatement.php +++ b/src/DatabasePreparedStatement.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -use Distantmagic\Resonance\Event\SQLQueryBeforeExecute; use Doctrine\DBAL\Driver\PDO\ParameterTypeMap; use Doctrine\DBAL\Driver\Statement; use Doctrine\DBAL\ParameterType; @@ -15,9 +14,7 @@ use Swoole\Database\PDOStatementProxy; readonly class DatabasePreparedStatement implements Statement { public function __construct( - private EventDispatcherInterface $eventDispatcher, private PDOStatement|PDOStatementProxy $pdoStatement, - private string $sql, ) {} /** @@ -52,8 +49,6 @@ readonly class DatabasePreparedStatement implements Statement public function execute($params = null): DatabaseExecutedStatement { - $this->eventDispatcher->dispatch(new SQLQueryBeforeExecute($this->sql)); - /** * @var bool */ diff --git a/src/DoctrineConnectionRepository.php b/src/DoctrineConnectionRepository.php index ee44b12b2963de81f6610f1969bdb0a105844ad8..6f4af63c179402f9aba9cf5d9687fabe9453768e 100644 --- a/src/DoctrineConnectionRepository.php +++ b/src/DoctrineConnectionRepository.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use Distantmagic\Resonance\Attribute\ListensTo; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Event\HttpResponseReady; use Doctrine\DBAL\Configuration; @@ -22,7 +23,8 @@ use WeakMap; * * @template-extends EventListener<HttpResponseReady,void> */ -#[Singleton] +#[ListensTo(HttpResponseReady::class)] +#[Singleton(collection: SingletonCollection::EventListener)] readonly class DoctrineConnectionRepository extends EventListener { /** @@ -36,30 +38,12 @@ readonly class DoctrineConnectionRepository extends EventListener private DoctrineMySQLDriver $doctrineMySQLDriver, private DoctrinePostgreSQLDriver $doctrinePostgreSQLDriver, private DoctrineSQLiteDriver $doctrineSQLiteDriver, - private EventListenerAggregate $eventListenerAggregate, private LoggerInterface $logger, ) { /** * @var WeakMap<Request,Map<string,Connection>> */ $this->connections = new WeakMap(); - - /** - * False positive, $this IS an EventListenerInterface - * - * @psalm-suppress InvalidArgument - */ - $this->eventListenerAggregate->addListener(HttpResponseReady::class, $this); - } - - public function __destruct() - { - /** - * False positive, $this IS an EventListenerInterface - * - * @psalm-suppress InvalidArgument - */ - $this->eventListenerAggregate->removeListener(HttpResponseReady::class, $this); } /** diff --git a/src/Event/MailBounced.php b/src/Event/MailBounced.php index 03d5689cdb3d6d97fba7e8759b06dcfe4c12e1e7..067af4dc271d19d09589a0319e78644fad9a5bb0 100644 --- a/src/Event/MailBounced.php +++ b/src/Event/MailBounced.php @@ -5,24 +5,12 @@ declare(strict_types=1); namespace Distantmagic\Resonance\Event; use Distantmagic\Resonance\Event; +use Distantmagic\Resonance\MailBounceReportInterface; /** * @psalm-suppress PossiblyUnusedProperty used in listeners */ final readonly class MailBounced extends Event { - /** - * @param non-empty-string $recipient - * @param null|non-empty-string $diagnosticCode - * @param null|non-empty-string $notification - * @param null|non-empty-string $sender - * @param null|non-empty-string $status - */ - public function __construct( - public string $recipient, - public ?string $diagnosticCode, - public ?string $notification, - public ?string $sender, - public ?string $status, - ) {} + public function __construct(public MailBounceReportInterface $report) {} } diff --git a/src/Event/SQLQueryBeforeExecute.php b/src/Event/SQLQueryBeforeExecute.php deleted file mode 100644 index 96ce1dbb04cefda32cb62ecdda4b3b15277ea388..0000000000000000000000000000000000000000 --- a/src/Event/SQLQueryBeforeExecute.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance\Event; - -use Distantmagic\Resonance\Event; - -final readonly class SQLQueryBeforeExecute extends Event -{ - public function __construct(public string $sql) {} -} diff --git a/src/EventListener/SQLQueryLogger.php b/src/EventListener/SQLQueryLogger.php deleted file mode 100644 index 0a3e00334cfb0014ae56ccb5384d965f0fa1aa8f..0000000000000000000000000000000000000000 --- a/src/EventListener/SQLQueryLogger.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance\EventListener; - -use Distantmagic\Resonance\Attribute\ListensTo; -use Distantmagic\Resonance\Attribute\Singleton; -use Distantmagic\Resonance\DatabaseConfiguration; -use Distantmagic\Resonance\Event\SQLQueryBeforeExecute; -use Distantmagic\Resonance\EventListener; -use Distantmagic\Resonance\SingletonCollection; -use Doctrine\SqlFormatter\SqlFormatter; -use Psr\Log\LoggerInterface; - -/** - * @template-extends EventListener<SQLQueryBeforeExecute,void> - */ -#[ListensTo(SQLQueryBeforeExecute::class)] -#[Singleton(collection: SingletonCollection::EventListener)] -final readonly class SQLQueryLogger extends EventListener -{ - private SqlFormatter $sqlFormatter; - - public function __construct( - private DatabaseConfiguration $databaseConfiguration, - private LoggerInterface $logger, - ) { - $this->sqlFormatter = new SqlFormatter(); - } - - /** - * @param SQLQueryBeforeExecute $event - */ - public function handle(object $event): void - { - $this->logger->debug($this->sqlFormatter->format($event->sql)); - } - - public function shouldRegister(): bool - { - foreach ($this->databaseConfiguration->connectionPoolConfiguration as $connectionPoolConfiguration) { - if ($connectionPoolConfiguration->logQueries) { - return true; - } - } - - return false; - } -} diff --git a/src/MailBounceReportInterface.php b/src/MailBounceReportInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..99360fa80aee315e48ac506e5a9b9d81bf1a0edc --- /dev/null +++ b/src/MailBounceReportInterface.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Symfony\Component\Mime\Address; + +interface MailBounceReportInterface +{ + /** + * @return null|non-empty-string + */ + public function getDiagnosticCode(): ?string; + + /** + * @return null|non-empty-string + */ + public function getNotification(): ?string; + + public function getRecipient(): Address; + + public function getSender(): ?Address; + + /** + * @return null|non-empty-string + */ + public function getStatus(): ?string; +} diff --git a/src/Command/MailBounce.php b/src/PostfixBounceAnalyzer.php similarity index 68% rename from src/Command/MailBounce.php rename to src/PostfixBounceAnalyzer.php index 0ca83c438586277b12f7c5783c6a12e9e5e4de33..c5d649fedbddba0dab8dc3d90e401b57ad91548a 100644 --- a/src/Command/MailBounce.php +++ b/src/PostfixBounceAnalyzer.php @@ -2,23 +2,13 @@ declare(strict_types=1); -namespace Distantmagic\Resonance\Command; - -use Distantmagic\Resonance\Attribute\ConsoleCommand; -use Distantmagic\Resonance\Attribute\WantsFeature; -use Distantmagic\Resonance\Command; -use Distantmagic\Resonance\CoroutineCommand; -use Distantmagic\Resonance\Event\MailBounced; -use Distantmagic\Resonance\EventDispatcherInterface; -use Distantmagic\Resonance\Feature; -use Distantmagic\Resonance\MailerRepository; -use Distantmagic\Resonance\SwooleConfiguration; +namespace Distantmagic\Resonance; + +use Distantmagic\Resonance\Attribute\Singleton; use Generator; use http\Header; -use Psr\Log\LoggerInterface; use RuntimeException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Mime\Address; /** * @psalm-type PPartData = array{ @@ -27,34 +17,11 @@ use Symfony\Component\Console\Output\OutputInterface; * starting-pos-body: int, * } */ -#[ConsoleCommand( - name: 'mail:bounce', - description: 'Handles email bounces (requires mailparse)' -)] -#[WantsFeature(Feature::Postfix)] -final class MailBounce extends CoroutineCommand +#[Singleton] +readonly class PostfixBounceAnalyzer { - public function __construct( - private readonly EventDispatcherInterface $eventDispatcher, - private readonly LoggerInterface $logger, - private readonly MailerRepository $mailerRepository, - SwooleConfiguration $swooleConfiguration, - ) { - parent::__construct($swooleConfiguration); - } - - protected function executeInCoroutine(InputInterface $input, OutputInterface $output): int + public function extractReport(string $deliveryReport): ?PostfixBounceReport { - if (!extension_loaded('mailparse') || !extension_loaded('http')) { - throw new RuntimeException('You need to install "http" and "mailparse" extensions'); - } - - $content = stream_get_contents(STDIN); - - if (false === $content || empty($content)) { - throw new RuntimeException('Expected email contents in STDIN'); - } - /** * @var array{ * diagnostic-code?: non-empty-string, @@ -65,7 +32,7 @@ final class MailBounce extends CoroutineCommand */ $ret = []; - foreach ($this->parseMessageContent($content) as $partData => $partBody) { + foreach ($this->parseDeliveryReport($deliveryReport) as $partData => $partBody) { if (!isset($partData['content-description'])) { continue; } @@ -96,16 +63,54 @@ final class MailBounce extends CoroutineCommand * That alone is enough to notify about the bounce. */ if (isset($ret['original-recipient']) && !empty($ret['original-recipient'])) { - $this->eventDispatcher->dispatch(new MailBounced( - recipient: $ret['original-recipient'], + return new PostfixBounceReport( + recipient: new Address($ret['original-recipient']), diagnosticCode: empty($ret['diagnostic-code']) ? null : $ret['diagnostic-code'], notification: empty($ret['notification']) ? null : $ret['notification'], - sender: empty($ret['x-postfix-sender']) ? null : $ret['x-postfix-sender'], + sender: empty($ret['x-postfix-sender']) ? null : new Address($ret['x-postfix-sender']), status: empty($ret['status']) ? null : $ret['status'], - )); + ); } - return Command::SUCCESS; + return null; + } + + /** + * @return Generator<PPartData,string> + */ + private function parseDeliveryReport(string $content): Generator + { + $message = mailparse_msg_create(); + + try { + if (!mailparse_msg_parse($message, $content)) { + throw new RuntimeException('Unable to parse the email message'); + } + + $structure = mailparse_msg_get_structure($message); + + /** + * @var string $partId + */ + foreach ($structure as $partId) { + $part = mailparse_msg_get_part($message, $partId); + + /** + * @var PPartData $partData + */ + $partData = mailparse_msg_get_part_data($part); + + yield $partData => trim(mb_substr( + $content, + $partData['starting-pos-body'], + $partData['ending-pos-body'] - $partData['starting-pos-body'] + )); + } + } finally { + if (!mailparse_msg_free($message)) { + throw new RuntimeException('Unable to free the message resource'); + } + } } /** @@ -150,42 +155,4 @@ final class MailBounce extends CoroutineCommand } } } - - /** - * @return Generator<PPartData,string> - */ - private function parseMessageContent(string $content): Generator - { - $message = mailparse_msg_create(); - - try { - if (!mailparse_msg_parse($message, $content)) { - throw new RuntimeException('Unable to parse the email message'); - } - - $structure = mailparse_msg_get_structure($message); - - /** - * @var string $partId - */ - foreach ($structure as $partId) { - $part = mailparse_msg_get_part($message, $partId); - - /** - * @var PPartData $partData - */ - $partData = mailparse_msg_get_part_data($part); - - yield $partData => trim(mb_substr( - $content, - $partData['starting-pos-body'], - $partData['ending-pos-body'] - $partData['starting-pos-body'] - )); - } - } finally { - if (!mailparse_msg_free($message)) { - throw new RuntimeException('Unable to free the message resource'); - } - } - } } diff --git a/src/PostfixBounceReport.php b/src/PostfixBounceReport.php new file mode 100644 index 0000000000000000000000000000000000000000..1ffc078c8c3a7cce8ee40b50dc6f0f16f651eaeb --- /dev/null +++ b/src/PostfixBounceReport.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Symfony\Component\Mime\Address; + +/** + * @psalm-suppress PossiblyUnusedProperty used in listeners + */ +final readonly class PostfixBounceReport extends Event implements MailBounceReportInterface +{ + /** + * @param null|non-empty-string $diagnosticCode + * @param null|non-empty-string $notification + * @param null|non-empty-string $status + */ + public function __construct( + private Address $recipient, + private ?string $diagnosticCode, + private ?string $notification, + private ?Address $sender, + private ?string $status, + ) {} + + /** + * @return null|non-empty-string + */ + public function getDiagnosticCode(): ?string + { + return $this->diagnosticCode; + } + + /** + * @return null|non-empty-string + */ + public function getNotification(): ?string + { + return $this->notification; + } + + public function getRecipient(): Address + { + return $this->recipient; + } + + public function getSender(): ?Address + { + return $this->sender; + } + + /** + * @return null|non-empty-string + */ + public function getStatus(): ?string + { + return $this->status; + } +} diff --git a/src/PostfixBounceTest.php b/src/PostfixBounceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..40d32dca5973d5c46a319e1fe4841f20c90e6e36 --- /dev/null +++ b/src/PostfixBounceTest.php @@ -0,0 +1,96 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use PHPUnit\Framework\TestCase; + +/** + * @coversNothing + * + * @internal + */ +final class PostfixBounceTest extends TestCase +{ + use TestsDependencyInectionContainerTrait; + + public const DELIVERY_REPORT = <<<'REPORT' + From double-bounce@myhost Sat Feb 3 08:52:39 2024 + Return-Path: <double-bounce@myhost> + Received: by myhost (Postfix) + id 01862807342C; Sat, 3 Feb 2024 08:52:39 +0100 (CET) + Date: Sat, 3 Feb 2024 08:52:39 +0100 (CET) + From: Mail Delivery System <MAILER-DAEMON@myhost> + Subject: Postmaster Copy: Undelivered Mail + To: bounces@localhost + Auto-Submitted: auto-generated + MIME-Version: 1.0 + Content-Type: multipart/report; report-type=delivery-status; + boundary="EC0F4807342B.1706946759/myhost" + Content-Transfer-Encoding: 8bit + Message-Id: <20240203075239.01862807342C@myhost> + + This is a MIME-encapsulated message. + + --EC0F4807342B.1706946759/myhost + Content-Description: Notification + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit + + + <foo@example.com>: Domain example.com does not accept mail (nullMX) + + --EC0F4807342B.1706946759/myhost + Content-Description: Delivery report + Content-Type: message/delivery-status + + Reporting-MTA: dns; myhost + X-Postfix-Queue-ID: EC0F4807342B + X-Postfix-Sender: rfc822; test@example.com + Arrival-Date: Sat, 3 Feb 2024 08:52:38 +0100 (CET) + + Final-Recipient: rfc822; foo@example.com + Original-Recipient: rfc822;foo@example.com + Action: failed + Status: 5.1.0 + Diagnostic-Code: X-Postfix; Domain example.com does not accept mail (nullMX) + + --EC0F4807342B.1706946759/myhost + Content-Description: Undelivered Message Headers + Content-Type: text/rfc822-headers + Content-Transfer-Encoding: 8bit + + Return-Path: <test@example.com> + Received: by myhost (Postfix, from userid 1026) + id EC0F4807342B; Sat, 3 Feb 2024 08:52:38 +0100 (CET) + From: test@example.com + To: foo@example.com + Subject: hello + MIME-Version: 1.0 + Date: Sat, 03 Feb 2024 07:52:38 +0000 + Message-ID: <10b4a76ac9b4d29dfa3ca9a4cea89de8@example.com> + DKIM-Signature: v=1; q=dns/txt; a=rsa-sha256; + bh=def; d=mydomain; + h=From: To: Subject: MIME-Version: Date: Message-ID; i=@mydomain; + s=selector; t=1706946758; c=relaxed/relaxed; + b=abc + Content-Type: multipart/alternative; boundary=sy7-AJCQ + + --EC0F4807342B.1706946759/myhost-- + REPORT; + + public function test_delivery_report_is_analyzed(): void + { + $analyzer = self::$container->make(PostfixBounceAnalyzer::class); + + $report = $analyzer->extractReport(self::DELIVERY_REPORT); + + self::assertNotNull($report); + self::assertEquals('5.1.0', $report->getStatus()); + self::assertEquals('<foo@example.com>: Domain example.com does not accept mail (nullMX)', $report->getNotification()); + self::assertEquals('foo@example.com', $report->getRecipient()->getAddress()); + self::assertEquals('test@example.com', $report->getSender()?->getAddress()); + self::assertEquals('X-Postfix; Domain example.com does not accept mail (nullMX)', $report->getDiagnosticCode()); + } +} diff --git a/src/SessionManager.php b/src/SessionManager.php index 2b6b7be294e87a60ac32c7bb4c1c711840ccca55..f88d457e7b4f2d3a5234eb3839f74e6166182126 100644 --- a/src/SessionManager.php +++ b/src/SessionManager.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use Distantmagic\Resonance\Attribute\ListensTo; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Event\HttpResponseReady; use Swoole\Http\Request; @@ -13,7 +14,8 @@ use WeakMap; /** * @template-extends EventListener<HttpResponseReady,void> */ -#[Singleton] +#[ListensTo(HttpResponseReady::class)] +#[Singleton(collection: SingletonCollection::EventListener)] final readonly class SessionManager extends EventListener { /** @@ -22,34 +24,16 @@ final readonly class SessionManager extends EventListener private WeakMap $sessions; public function __construct( - private EventListenerAggregate $eventListenerAggregate, private RedisConfiguration $redisConfiguration, private RedisConnectionPoolRepository $redisConnectionPoolRepository, private SessionConfiguration $sessionConfiguration, ) { - /** - * False positive, $this IS an EventListenerInterface - * - * @psalm-suppress InvalidArgument - */ - $this->eventListenerAggregate->addListener(HttpResponseReady::class, $this); - /** * @var WeakMap<Request, Session> */ $this->sessions = new WeakMap(); } - public function __destruct() - { - /** - * False positive, $this IS an EventListenerInterface - * - * @psalm-suppress InvalidArgument - */ - $this->eventListenerAggregate->removeListener(HttpResponseReady::class, $this); - } - /** * @param HttpResponseReady $event */ diff --git a/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php b/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php index 9c19340b4780c330db784555db5f6943156ebf15..e1525f07bce3992d63ef178ed4e6949e84c4dbfa 100644 --- a/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php +++ b/src/SingletonProvider/DatabaseConnectionPoolRepositoryProvider.php @@ -7,7 +7,6 @@ namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\DatabaseConfiguration; use Distantmagic\Resonance\DatabaseConnectionPoolRepository; -use Distantmagic\Resonance\EventDispatcherInterface; use Distantmagic\Resonance\PHPProjectFiles; use Distantmagic\Resonance\SingletonContainer; use Distantmagic\Resonance\SingletonProvider; @@ -22,12 +21,11 @@ final readonly class DatabaseConnectionPoolRepositoryProvider extends SingletonP { public function __construct( private DatabaseConfiguration $databaseConfiguration, - private EventDispatcherInterface $eventDispatcher, ) {} public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): DatabaseConnectionPoolRepository { - $databaseConnectionPoolRepository = new DatabaseConnectionPoolRepository($this->eventDispatcher); + $databaseConnectionPoolRepository = new DatabaseConnectionPoolRepository(); foreach ($this->databaseConfiguration->connectionPoolConfiguration as $name => $connectionPoolConfiguration) { $pdoConfig = new PDOConfig(); diff --git a/src/SingletonProvider/RedisConnectionPoolRepositoryProvider.php b/src/SingletonProvider/RedisConnectionPoolRepositoryProvider.php index 741f99996e787c0fb37a8da26b66b2fa3c08d0bc..f52ab7c6f1561c5ecaf3cf968cb1f5f790d57c53 100644 --- a/src/SingletonProvider/RedisConnectionPoolRepositoryProvider.php +++ b/src/SingletonProvider/RedisConnectionPoolRepositoryProvider.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\Attribute\Singleton; -use Distantmagic\Resonance\EventDispatcherInterface; use Distantmagic\Resonance\PHPProjectFiles; use Distantmagic\Resonance\RedisConfiguration; use Distantmagic\Resonance\RedisConnectionPoolRepository; @@ -22,7 +21,6 @@ final readonly class RedisConnectionPoolRepositoryProvider extends SingletonProv { public function __construct( private RedisConfiguration $databaseConfiguration, - private EventDispatcherInterface $eventDispatcher, ) {} public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): RedisConnectionPoolRepository diff --git a/src/TwigFunctionRoute.php b/src/TwigFunctionRoute.php index 87448cea5e598b083df5c0ee55c71309970722a7..8a617869c583b13d8eaf34bb95a0d9fb6ee14c2d 100644 --- a/src/TwigFunctionRoute.php +++ b/src/TwigFunctionRoute.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; use Distantmagic\Resonance\Attribute\Singleton; +use RuntimeException; #[Singleton] readonly class TwigFunctionRoute @@ -15,9 +16,25 @@ readonly class TwigFunctionRoute * @param array<string,string> $params */ public function __invoke( - HttpRouteSymbolInterface $routeSymbol, + HttpRouteSymbolInterface|string $routeSymbol, array $params = [], ): string { - return $this->internalLinkBuilder->build($routeSymbol, $params); + if (!is_string($routeSymbol)) { + return $this->internalLinkBuilder->build($routeSymbol, $params); + } + + $resolvedSymbol = constant(sprintf('App\\HttpRouteSymbol::%s', $routeSymbol)); + + if (!($resolvedSymbol instanceof HttpRouteSymbolInterface)) { + throw new RuntimeException(sprintf( + 'Expected "%s"', + HttpRouteSymbolInterface::class, + )); + } + + return $this->internalLinkBuilder->build( + $resolvedSymbol, + $params, + ); } }