From 15bae7cb375608a1091334dc4787510c74088c8a Mon Sep 17 00:00:00 2001 From: l0gic Date: Mon, 29 Apr 2024 22:44:34 +0500 Subject: [PATCH 01/27] Cli startup joke --- src/Cli/StartupJokeSpeechBubble.php | 31 +++++++++++++++++++++++++++++ src/Command/Run.php | 7 ++++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/Cli/StartupJokeSpeechBubble.php diff --git a/src/Cli/StartupJokeSpeechBubble.php b/src/Cli/StartupJokeSpeechBubble.php new file mode 100644 index 00000000..97acc919 --- /dev/null +++ b/src/Cli/StartupJokeSpeechBubble.php @@ -0,0 +1,31 @@ +writeln(\sprintf('%s v%s', Info::NAME, Info::VERSION)); - $output->write(Info::LOGO_CLI_COLOR . "\n", true, OutputInterface::OUTPUT_RAW); + $output->write( + StartupJokeSpeechBubble::randomStartupJokeSpeechBubble() . Info::LOGO_CLI_COLOR . "\n", + true, + OutputInterface::OUTPUT_RAW + ); /** * Prepare port listeners From ca506518bf97d25d1ae9e4066b2ff4fa7d47d5a3 Mon Sep 17 00:00:00 2001 From: l0gic Date: Mon, 29 Apr 2024 23:57:58 +0500 Subject: [PATCH 02/27] Cli startup joke v2 --- src/Cli/CliStartupSpeechBubble.php | 70 +++++++++++++++++++++++++++++ src/Cli/StartupJokeSpeechBubble.php | 31 ------------- src/Command/Run.php | 4 +- 3 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 src/Cli/CliStartupSpeechBubble.php delete mode 100644 src/Cli/StartupJokeSpeechBubble.php diff --git a/src/Cli/CliStartupSpeechBubble.php b/src/Cli/CliStartupSpeechBubble.php new file mode 100644 index 00000000..c98a1be2 --- /dev/null +++ b/src/Cli/CliStartupSpeechBubble.php @@ -0,0 +1,70 @@ + + */ + private static function getJokeLines(): array + { + $joke = self::JOKES[mt_rand(0, \count(self::JOKES) - 1)]; + $jokeLen = max(\strlen($joke), 13); + $speechBubbleLeftSpace = str_repeat(' ', 14); + + $lines = []; + $lines[] = $speechBubbleLeftSpace.'⠘⡀⠀⠀'.str_repeat(' ', $jokeLen).'⠀⠀ ⡜'; + $lines[] = $speechBubbleLeftSpace.' ⠘⡀⠀'.str_pad($joke, $jokeLen, ' ', STR_PAD_BOTH).'⠀ ⡜'; + $lines[] = $speechBubbleLeftSpace.' ⠑⡀'.str_repeat(' ', $jokeLen).'⡔⠁'; + $lines[] = $speechBubbleLeftSpace.' ⢸ '.str_repeat('⣀', $jokeLen - 13).'⣀⣀⣀⣀⡀⠤⠄⠒⠈'; + $lines[] = $speechBubbleLeftSpace.' ⠘⣀⠄⠊⠁'; + + return $lines; + } +} diff --git a/src/Cli/StartupJokeSpeechBubble.php b/src/Cli/StartupJokeSpeechBubble.php deleted file mode 100644 index 97acc919..00000000 --- a/src/Cli/StartupJokeSpeechBubble.php +++ /dev/null @@ -1,31 +0,0 @@ -writeln(\sprintf('%s v%s', Info::NAME, Info::VERSION)); $output->write( - StartupJokeSpeechBubble::randomStartupJokeSpeechBubble() . Info::LOGO_CLI_COLOR . "\n", + "\n" . CliStartupSpeechBubble::getStartupSpeechBubble() . "\n", true, OutputInterface::OUTPUT_RAW ); From 9062e5022890ab19c57f6d7f912f952f5e7a82ab Mon Sep 17 00:00:00 2001 From: l0gic Date: Tue, 30 Apr 2024 00:08:12 +0500 Subject: [PATCH 03/27] FQN for functions from global scope --- src/Cli/CliStartupSpeechBubble.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Cli/CliStartupSpeechBubble.php b/src/Cli/CliStartupSpeechBubble.php index c98a1be2..f11495a2 100644 --- a/src/Cli/CliStartupSpeechBubble.php +++ b/src/Cli/CliStartupSpeechBubble.php @@ -32,17 +32,17 @@ public static function getStartupSpeechBubble(): string $result = ''; $jokeLines = self::getJokeLines(); - $maxJokeLineLen = max(array_map('mb_strlen', $jokeLines)) + 2; + $maxJokeLineLen = \max(\array_map('\mb_strlen', $jokeLines)) + 2; foreach ($jokeLines as $line) { - $rightSpaces = str_repeat(' ', $maxJokeLineLen - mb_strlen($line)); + $rightSpaces = \str_repeat(' ', $maxJokeLineLen - \mb_strlen($line)); $result .= "\e[44;97;1m" . $line . $rightSpaces . "\e[0m\n"; } - $logoLines = explode("\n", self::LOGO); + $logoLines = \explode("\n", self::LOGO); foreach ($logoLines as $line) { - $rightSpaces = str_repeat(' ', $maxJokeLineLen - mb_strlen($line)); + $rightSpaces = \str_repeat(' ', $maxJokeLineLen - \mb_strlen($line)); $result .= "\e[44;97;1m" . $line . $rightSpaces . "\e[0m\n"; } @@ -54,15 +54,15 @@ public static function getStartupSpeechBubble(): string */ private static function getJokeLines(): array { - $joke = self::JOKES[mt_rand(0, \count(self::JOKES) - 1)]; - $jokeLen = max(\strlen($joke), 13); - $speechBubbleLeftSpace = str_repeat(' ', 14); + $joke = self::JOKES[\mt_rand(0, \count(self::JOKES) - 1)]; + $jokeLen = \max(\strlen($joke), 13); + $speechBubbleLeftSpace = \str_repeat(' ', 14); $lines = []; - $lines[] = $speechBubbleLeftSpace.'⠘⡀⠀⠀'.str_repeat(' ', $jokeLen).'⠀⠀ ⡜'; - $lines[] = $speechBubbleLeftSpace.' ⠘⡀⠀'.str_pad($joke, $jokeLen, ' ', STR_PAD_BOTH).'⠀ ⡜'; - $lines[] = $speechBubbleLeftSpace.' ⠑⡀'.str_repeat(' ', $jokeLen).'⡔⠁'; - $lines[] = $speechBubbleLeftSpace.' ⢸ '.str_repeat('⣀', $jokeLen - 13).'⣀⣀⣀⣀⡀⠤⠄⠒⠈'; + $lines[] = $speechBubbleLeftSpace.'⠘⡀⠀⠀'.\str_repeat(' ', $jokeLen).'⠀⠀ ⡜'; + $lines[] = $speechBubbleLeftSpace.' ⠘⡀⠀'.\str_pad($joke, $jokeLen, ' ', STR_PAD_BOTH).'⠀ ⡜'; + $lines[] = $speechBubbleLeftSpace.' ⠑⡀'.\str_repeat(' ', $jokeLen).'⡔⠁'; + $lines[] = $speechBubbleLeftSpace.' ⢸ '.\str_repeat('⣀', $jokeLen - 13).'⣀⣀⣀⣀⡀⠤⠄⠒⠈'; $lines[] = $speechBubbleLeftSpace.' ⠘⣀⠄⠊⠁'; return $lines; From 38eb3281fbf68a8c9c6d5e6da2f5326b7d9c6bec Mon Sep 17 00:00:00 2001 From: l0gic Date: Tue, 30 Apr 2024 00:16:30 +0500 Subject: [PATCH 04/27] Add a console command to run a joke vendor/bin/trap joke --- bin/trap | 1 + src/Command/Joke.php | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/Command/Joke.php diff --git a/bin/trap b/bin/trap index cafcc726..981ba668 100644 --- a/bin/trap +++ b/bin/trap @@ -44,6 +44,7 @@ if ('cli' !== PHP_SAPI) { $application = new Application(); $application->setCommandLoader( new FactoryCommandLoader([ + Command\Joke::getDefaultName() => static fn() => new Command\Joke(), Command\Run::getDefaultName() => static fn() => new Command\Run(), Command\Test::getDefaultName() => static fn() => new Command\Test(), ]), diff --git a/src/Command/Joke.php b/src/Command/Joke.php new file mode 100644 index 00000000..fdb48fa9 --- /dev/null +++ b/src/Command/Joke.php @@ -0,0 +1,43 @@ +write( + "\n" . CliStartupSpeechBubble::getStartupSpeechBubble() . "\n", + true, + OutputInterface::OUTPUT_RAW + ); + + return Command::SUCCESS; + } +} From f8863d3bfd319e9c2f12a06c0e826a0c9a8ba93d Mon Sep 17 00:00:00 2001 From: l0gic Date: Tue, 30 Apr 2024 00:17:10 +0500 Subject: [PATCH 05/27] Remove unused Info::LOGO_CLI_COLOR --- src/Info.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Info.php b/src/Info.php index b5f9104b..3df0bc11 100644 --- a/src/Info.php +++ b/src/Info.php @@ -11,20 +11,5 @@ class Info { public const NAME = 'Buggregator Trap'; public const VERSION = '1.4.6'; - public const LOGO_CLI_COLOR = << Date: Tue, 30 Apr 2024 00:22:21 +0500 Subject: [PATCH 06/27] Buggregator related phrase --- src/Cli/CliStartupSpeechBubble.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cli/CliStartupSpeechBubble.php b/src/Cli/CliStartupSpeechBubble.php index f11495a2..b0a9a389 100644 --- a/src/Cli/CliStartupSpeechBubble.php +++ b/src/Cli/CliStartupSpeechBubble.php @@ -25,6 +25,7 @@ class CliStartupSpeechBubble 'Two hard things in computer science: cache invalidation, naming things and stack overflow.', 'Depressive programming style through dump and die.', 'PHP was dead 84 years ago right?', + 'Submit a pull request to help us improve the Buggregator Trap codebase', ]; public static function getStartupSpeechBubble(): string From 018d28de47f729c9dc43a28e548c9992370eaee9 Mon Sep 17 00:00:00 2001 From: l0gic Date: Tue, 30 Apr 2024 08:10:21 +0500 Subject: [PATCH 07/27] Revert "Remove unused Info::LOGO_CLI_COLOR" This reverts commit f8863d3bfd319e9c2f12a06c0e826a0c9a8ba93d. --- src/Info.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Info.php b/src/Info.php index 3df0bc11..b5f9104b 100644 --- a/src/Info.php +++ b/src/Info.php @@ -11,5 +11,20 @@ class Info { public const NAME = 'Buggregator Trap'; public const VERSION = '1.4.6'; + public const LOGO_CLI_COLOR = << Date: Tue, 30 Apr 2024 08:23:44 +0500 Subject: [PATCH 08/27] remove speech bubble --- src/Cli/CliStartupSpeechBubble.php | 71 ------------------------------ src/Command/Joke.php | 13 +----- src/Command/Run.php | 10 ++--- src/Info.php | 7 +++ 4 files changed, 11 insertions(+), 90 deletions(-) delete mode 100644 src/Cli/CliStartupSpeechBubble.php diff --git a/src/Cli/CliStartupSpeechBubble.php b/src/Cli/CliStartupSpeechBubble.php deleted file mode 100644 index b0a9a389..00000000 --- a/src/Cli/CliStartupSpeechBubble.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ - private static function getJokeLines(): array - { - $joke = self::JOKES[\mt_rand(0, \count(self::JOKES) - 1)]; - $jokeLen = \max(\strlen($joke), 13); - $speechBubbleLeftSpace = \str_repeat(' ', 14); - - $lines = []; - $lines[] = $speechBubbleLeftSpace.'⠘⡀⠀⠀'.\str_repeat(' ', $jokeLen).'⠀⠀ ⡜'; - $lines[] = $speechBubbleLeftSpace.' ⠘⡀⠀'.\str_pad($joke, $jokeLen, ' ', STR_PAD_BOTH).'⠀ ⡜'; - $lines[] = $speechBubbleLeftSpace.' ⠑⡀'.\str_repeat(' ', $jokeLen).'⡔⠁'; - $lines[] = $speechBubbleLeftSpace.' ⢸ '.\str_repeat('⣀', $jokeLen - 13).'⣀⣀⣀⣀⡀⠤⠄⠒⠈'; - $lines[] = $speechBubbleLeftSpace.' ⠘⣀⠄⠊⠁'; - - return $lines; - } -} diff --git a/src/Command/Joke.php b/src/Command/Joke.php index fdb48fa9..57c9f33a 100644 --- a/src/Command/Joke.php +++ b/src/Command/Joke.php @@ -4,17 +4,10 @@ namespace Buggregator\Trap\Command; -use Buggregator\Trap\Application; -use Buggregator\Trap\Config\SocketServer; -use Buggregator\Trap\Cli\CliStartupSpeechBubble; use Buggregator\Trap\Info; -use Buggregator\Trap\Logger; -use Buggregator\Trap\Sender; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -32,11 +25,7 @@ protected function execute( InputInterface $input, OutputInterface $output, ): int { - $output->write( - "\n" . CliStartupSpeechBubble::getStartupSpeechBubble() . "\n", - true, - OutputInterface::OUTPUT_RAW - ); + trap(Info::JOKES[\array_rand(Info::JOKES)]); return Command::SUCCESS; } diff --git a/src/Command/Run.php b/src/Command/Run.php index 8f06b61c..0ca8d11e 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -6,7 +6,6 @@ use Buggregator\Trap\Application; use Buggregator\Trap\Config\SocketServer; -use Buggregator\Trap\Cli\CliStartupSpeechBubble; use Buggregator\Trap\Info; use Buggregator\Trap\Logger; use Buggregator\Trap\Sender; @@ -56,12 +55,9 @@ protected function execute( ): int { try { // Print intro - $output->writeln(\sprintf('%s v%s', Info::NAME, Info::VERSION)); - $output->write( - "\n" . CliStartupSpeechBubble::getStartupSpeechBubble() . "\n", - true, - OutputInterface::OUTPUT_RAW - ); + $output->writeln(\sprintf('%s v%s:', Info::NAME, Info::VERSION)); + trap(Info::JOKES[\array_rand(Info::JOKES)]); + $output->write(Info::LOGO_CLI_COLOR . "\n", true, OutputInterface::OUTPUT_RAW); /** * Prepare port listeners diff --git a/src/Info.php b/src/Info.php index b5f9104b..e4ad1e9e 100644 --- a/src/Info.php +++ b/src/Info.php @@ -27,4 +27,11 @@ class Info CONSOLE; public const TRAP_ROOT = __DIR__ . '/..'; + public const JOKES = [ + 'Why do programmers always mix up Halloween and Christmas? Because Oct 31 == Dec 25.', + 'Two hard things in computer science: cache invalidation, naming things and stack overflow.', + 'Depressive programming style through dump and die.', + 'PHP was dead 84 years ago right?', + 'Submit a pull request to help us improve the Buggregator Trap codebase', + ]; } From bb3bf61f79ea99213bdb28b8528cf58a006dc060 Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Tue, 30 Apr 2024 12:52:37 +0300 Subject: [PATCH 09/27] Fix psalm, clear psalm-baseline.yaml --- psalm-baseline.xml | 515 +----------------- psalm.xml | 6 + src/Sender/Console/Renderer/Smtp.php | 4 +- .../Console/Renderer/TemplateRenderer.php | 1 + src/Sender/Console/Renderer/VarDumper.php | 4 +- src/Sender/Console/Support/Common.php | 8 +- src/Sender/Console/Support/Files.php | 6 +- src/Sender/Console/Support/Tables.php | 2 +- src/Sender/ConsoleSender.php | 2 + src/Socket/Client.php | 5 + src/Socket/Server.php | 1 + src/Support/StreamHelper.php | 2 +- src/Traffic/Dispatcher/Http.php | 1 + src/Traffic/Dispatcher/Smtp.php | 3 +- src/Traffic/Message/Headers.php | 25 +- src/Traffic/Message/Multipart/Field.php | 6 +- src/Traffic/Message/Multipart/File.php | 34 +- src/Traffic/Message/Multipart/Part.php | 27 +- src/Traffic/Message/Smtp.php | 27 +- src/Traffic/Parser/Http.php | 22 +- src/Traffic/Parser/Smtp.php | 2 +- src/functions.php | 4 + 22 files changed, 174 insertions(+), 533 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7f3ec765..a5770984 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,99 +1,49 @@ - + - $data + - - - $port - - - - $output::OUTPUT_RAW - $output::OUTPUT_RAW - - $logger + - \is_resource($fp) + - $buf === null + - $time + - - $response - - - $time - - $response + - - - $time - $time - - - $time - - - - - $payload - getAttribute('begin_at', null)]]> - getAttribute('begin_at', null)]]> - - - $payload - - - $offset - $currentPos + - - $headers - $itemHeader - - - - - - - $headers - $itemHeader - $type - - TReturn + - - method]]> - {$this->method}(...$arguments)]]> - $values - $values - $values - $values + + + + @@ -101,69 +51,14 @@ stream->getSize()]]> - int + - - request->getBody()->getSize() + \array_reduce( - $this->request->getUploadedFiles(), - static fn(int $carry, array $file): int => $carry + $file['size'], - 0 - )]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - $payload - - - int - - - - - - - request->getUploadedFiles()]]> - array + - - int - - - request->getBody()->getSize()]]> - - - - - \json_decode($payload, true, JSON_THROW_ON_ERROR) - @@ -173,47 +68,18 @@ default => throw new \InvalidArgumentException('Unknown Sentry frame type.'), }]]> - - $data - $data - - - - - - - $data - - static + - - - - $item - $item - - - $item - - - public function __construct( + - - - $payload - - - $payload - - - $payload + @@ -221,369 +87,46 @@ - Files::normalizeSize($size) + - $frame + getSize()]]> getCookieParams()]]> getQueryParams()]]> - - - - - - - $file - $file - $function + + + $output->writeln(]]> - - - - - - $row - $stacktrace - - $file - $file - $function + + + - renderMessageHeader + - - - - $message - $meta - $tags + + - - - - - - - - - payload]]> - $type - - - payload['exceptions']]]> - - - $type - - - $type - - - - - getHeaders()]]> - getHeaders()]]> - - - getClientFilename()]]> - getClientMediaType()]]> - - - - - render - - - - - - - - - - - $context - $controller - $data - $fileLink - - - - - - - $meta - - - $context - - $data - - - $controller - $describer - $fileLink - - - - - $payload - [$data, $context] - - - describe - - - dumper->dump($controller, true)]]> - dumper->dump($data, true)]]> - - - - - $item - - - - \array_keys($data) - - - $item - - - - - $size - - - - - - - - $key - - - $value - - - - - Termwind::renderUsing($output) - new HtmlRenderer() - - - Termwind::renderUsing($output) - - - - - new SplQueue() - - - - - payloadSize]]> - - - \Closure::bind($callable(...), $this) - \Closure::bind($callable(...), $this) - - - - - socket]]> - socket]]> - - - - - new \SplQueue() - - - - - getClass()]]> - $value - $value - - - getName()]]]> - getName()]]]> - - - $value - getName()]]]> - - - getDescriptorByClassName - - - - - - - - - - $value - $values - - - headers[$header]]]> - - - headers[$header]]]> - headers[$new->headerNames[$normalized]]]]> - headers[$header]]]> - headers[$header]]]> - headers[$header]]]> - - - headers[$header]]]> - - - $header - $header - $header - - - string[] - - - - - FieldDataArray - $this->value, - ]]]> - - - - - FileDataArray - $this->fileName, - 'size' => $this->getSize(), - ]]]> - - - - - new File($headers, $name, $fileName), - false => new Field($headers, $name), - }]]> - - - $headers - $headers - $headers - - - static - - - - - $addrs - protocol['BCC'] ?? []]]> - - - protocol]]> - - - $attach - - - $message - - - $attach - $message - - - >]]> - - - $attachments - $messages - - - - - $boundary - $headersBlock - read($blockEnd - $pos + 2)]]> - - - $parts - $result - - - >]]> - array{0: non-empty-string, 1: non-empty-string, 2: non-empty-string} - - - getSize()]]> - - - $value - $value - - - - - $boundary - $headerBlock - - - - - AbstractCloner::$defaultCasters[EnumValue::class] - AbstractCloner::$defaultCasters[MapField::class] - AbstractCloner::$defaultCasters[Message::class] - AbstractCloner::$defaultCasters[RepeatedField::class] - - - AbstractCloner::$defaultCasters[EnumValue::class] - AbstractCloner::$defaultCasters[MapField::class] - AbstractCloner::$defaultCasters[Message::class] - AbstractCloner::$defaultCasters[RepeatedField::class] - - - $value - AbstractCloner::$defaultCasters[EnumValue::class] - AbstractCloner::$defaultCasters[MapField::class] - AbstractCloner::$defaultCasters[Message::class] - AbstractCloner::$defaultCasters[RepeatedField::class] - diff --git a/psalm.xml b/psalm.xml index 1e24d848..14f9f935 100644 --- a/psalm.xml +++ b/psalm.xml @@ -11,6 +11,12 @@ + + + + + + diff --git a/src/Sender/Console/Renderer/Smtp.php b/src/Sender/Console/Renderer/Smtp.php index 9f48a8ea..5ef8eb0b 100644 --- a/src/Sender/Console/Renderer/Smtp.php +++ b/src/Sender/Console/Renderer/Smtp.php @@ -58,9 +58,9 @@ public function render(OutputInterface $output, Frame $frame): void foreach ($message->getAttachments() as $attach) { Files::renderFile( $output, - $attach->getClientFilename(), + $attach->getClientFilename() ?? 'undefined', $attach->getSize(), - $attach->getClientMediaType(), + $attach->getClientMediaType() ?? 'undefined', ); } $output->writeln(''); diff --git a/src/Sender/Console/Renderer/TemplateRenderer.php b/src/Sender/Console/Renderer/TemplateRenderer.php index 63b0800e..d0b98bff 100644 --- a/src/Sender/Console/Renderer/TemplateRenderer.php +++ b/src/Sender/Console/Renderer/TemplateRenderer.php @@ -20,6 +20,7 @@ public function __construct( public function render(string $template, array $data = []): void { + /** @psalm-suppress InternalMethod */ $this->renderer->render( $this->templateEngine->render($template, $data), 0 diff --git a/src/Sender/Console/Renderer/VarDumper.php b/src/Sender/Console/Renderer/VarDumper.php index 868e07ae..07f8f512 100644 --- a/src/Sender/Console/Renderer/VarDumper.php +++ b/src/Sender/Console/Renderer/VarDumper.php @@ -94,7 +94,7 @@ public function describe(OutputInterface $output, Data $data, array $context, in empty($request['method'] ?? '') or $meta['Method'] = $request['method']; empty($request['uri'] ?? '') or $meta['URI'] = $request['uri']; if ($controller = $request['controller']) { - $meta['Controller'] = rtrim($this->dumper->dump($controller, true), "\n"); + $meta['Controller'] = rtrim((string) $this->dumper->dump($controller, true), "\n"); } } elseif (isset($context['cli'])) { $meta['Command'] = $context['cli']['command_line']; @@ -107,7 +107,7 @@ public function describe(OutputInterface $output, Data $data, array $context, in Common::renderMetadata($output, $meta); $output->writeln(''); - $output->write($this->dumper->dump($data, true), true, OutputInterface::OUTPUT_RAW); + $output->write((string) $this->dumper->dump($data, true), true, OutputInterface::OUTPUT_RAW); } }; } diff --git a/src/Sender/Console/Support/Common.php b/src/Sender/Console/Support/Common.php index 21695e7d..c0355608 100644 --- a/src/Sender/Console/Support/Common.php +++ b/src/Sender/Console/Support/Common.php @@ -26,12 +26,12 @@ public static function renderHeader1(OutputInterface $output, string $title, ?st $output->writeln(['', \implode('', $parts), '']); } - public static function renderHeader2(OutputInterface $output, string $title, string ...$sub): void + public static function renderHeader2(OutputInterface $output, string $title, ?string ...$sub): void { $parts = ["# $title "]; foreach ($sub as $color => $value) { $color = \is_string($color) ? $color : 'gray'; - $parts[] = \sprintf(' %s ', $color, $value); + $parts[] = \sprintf(' %s ', $color, $value ?? 'NULL'); } $output->writeln(['', \implode('', $parts), '']); @@ -53,13 +53,13 @@ public static function renderHighlightedLine(OutputInterface $output, string $li } /** - * @param array $data + * @param array $data */ public static function renderMetadata(OutputInterface $output, array $data): void { + /** @psalm-suppress ArgumentTypeCoercion */ $maxHeaderLength = \max(\array_map('strlen', \array_keys($data))); - /** @var mixed $value */ foreach ($data as $head => $value) { // Align headers to the right self::renderHeader( diff --git a/src/Sender/Console/Support/Files.php b/src/Sender/Console/Support/Files.php index 387e5376..180c38cd 100644 --- a/src/Sender/Console/Support/Files.php +++ b/src/Sender/Console/Support/Files.php @@ -13,6 +13,9 @@ final class Files { /** + * + * @param non-negative-int|null $size + * * Render file info. Example: * ┌───┐ logo.ico * │ico│ 20.06 KiB @@ -24,6 +27,7 @@ final class Files * │ │ any value <= positional argument * │ico│ unknown size * └───┘ image/x-icon + * */ public static function renderFile( OutputInterface $output, @@ -33,7 +37,7 @@ public static function renderFile( string ...$additional ): void { // File extension - $ex = \substr($fileName, \strrpos($fileName, '.') + 1); + $ex = \substr($fileName, (int) \strrpos($fileName, '.') + 1); $ex = \strlen($ex) > 3 ? ' ' : \str_pad($ex, 3, ' ', \STR_PAD_BOTH); // File size diff --git a/src/Sender/Console/Support/Tables.php b/src/Sender/Console/Support/Tables.php index 80f99f7c..987bd5ed 100644 --- a/src/Sender/Console/Support/Tables.php +++ b/src/Sender/Console/Support/Tables.php @@ -23,7 +23,7 @@ public static function renderKeyValueTable(OutputInterface $output, string $titl return; } - $keyLength = \max(\array_map(static fn($key) => \strlen($key), \array_keys($data))); + $keyLength = \max(\array_map(static fn($key) => \strlen((string) $key), \array_keys($data))); $valueLength = \max(1, (new Terminal())->getWidth() - 7 - $keyLength); $table->setRows([...(static function (array $data) use ($valueLength): iterable { diff --git a/src/Sender/ConsoleSender.php b/src/Sender/ConsoleSender.php index 1d626a1a..58cdb975 100644 --- a/src/Sender/ConsoleSender.php +++ b/src/Sender/ConsoleSender.php @@ -21,8 +21,10 @@ final class ConsoleSender implements Sender { public static function create(OutputInterface $output): self { + /** @psalm-suppress InternalMethod, InternalClass */ Termwind::renderUsing($output); + /** @psalm-suppress InternalClass */ $templateRenderer = new TemplateRenderer( new HtmlRenderer(), new TemplateEngine(Info::TRAP_ROOT . '/resources/templates') diff --git a/src/Socket/Client.php b/src/Socket/Client.php index 2594393a..721e47a8 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -26,6 +26,9 @@ final class Client implements Destroyable private \Closure $onPayload; private \Closure $onClose; + /** + * @param positive-int $payloadSize + */ private function __construct( private readonly \Socket $socket, private readonly int $payloadSize, @@ -117,6 +120,7 @@ protected function onInit(): void */ public function setOnPayload(callable $callable): void { + /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ $this->onPayload = \Closure::bind($callable(...), $this); } @@ -126,6 +130,7 @@ public function setOnPayload(callable $callable): void */ public function setOnClose(callable $callable): void { + /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ $this->onClose = \Closure::bind($callable(...), $this); } diff --git a/src/Socket/Server.php b/src/Socket/Server.php index 73c1ae50..f69276e6 100644 --- a/src/Socket/Server.php +++ b/src/Socket/Server.php @@ -92,6 +92,7 @@ public static function init( public function process(): void { + /** @psalm-suppress PossiblyInvalidArgument */ while (!$this->cancelled and false !== ($socket = \socket_accept($this->socket))) { $client = null; try { diff --git a/src/Support/StreamHelper.php b/src/Support/StreamHelper.php index e6190296..ae06e1c5 100644 --- a/src/Support/StreamHelper.php +++ b/src/Support/StreamHelper.php @@ -67,7 +67,7 @@ public static function strpos(StreamInterface $stream, string $substr): int|fals * * @param non-empty-string $boundary * - * @return int Bytes written + * @return non-negative-int Bytes written */ public static function writeStreamUntil(StreamInterface $from, StreamInterface $to, string $boundary): int { diff --git a/src/Traffic/Dispatcher/Http.php b/src/Traffic/Dispatcher/Http.php index 8e7fa06a..3e0fa6f9 100644 --- a/src/Traffic/Dispatcher/Http.php +++ b/src/Traffic/Dispatcher/Http.php @@ -47,6 +47,7 @@ public function __construct( $handlers[] = new Fallback($middlewares); // Build pipeline of handlers. + /** @psalm-suppress MixedPropertyTypeCoercion */ $this->pipeline = Pipeline::build( $handlers, /** @see RequestHandler::handle() */ diff --git a/src/Traffic/Dispatcher/Smtp.php b/src/Traffic/Dispatcher/Smtp.php index f3525c8d..7dbe286c 100644 --- a/src/Traffic/Dispatcher/Smtp.php +++ b/src/Traffic/Dispatcher/Smtp.php @@ -31,7 +31,6 @@ public function __construct( public function dispatch(StreamClient $stream): iterable { $stream->sendData($this->createResponse(self::READY, 'mailamie')); - $protocol = []; $message = null; @@ -40,9 +39,11 @@ public function dispatch(StreamClient $stream): iterable if (\preg_match('/^(?:EHLO|HELO)/', $response)) { $stream->sendData($this->createResponse(self::OK)); } elseif (\preg_match('/^MAIL FROM:\s*<(.*)>/', $response, $matches)) { + /** @var array{1: non-empty-string} $matches */ $protocol['FROM'][] = $matches[1]; $stream->sendData($this->createResponse(self::OK)); } elseif (\preg_match('/^RCPT TO:\s*<(.*)>/', $response, $matches)) { + /** @var array{1: non-empty-string} $matches */ $protocol['BCC'][] = $matches[1]; $stream->sendData($this->createResponse(self::OK)); } elseif (\str_starts_with($response, 'QUIT')) { diff --git a/src/Traffic/Message/Headers.php b/src/Traffic/Message/Headers.php index 3be3a37f..a906baed 100644 --- a/src/Traffic/Message/Headers.php +++ b/src/Traffic/Message/Headers.php @@ -9,12 +9,15 @@ */ trait Headers { - /** @var array Map of all registered headers, as original name => array of values */ + /** @var array> Map of all registered headers, as original name => array of values */ private array $headers = []; - /** @var array Map of lowercase header name => original name at registration */ + /** @var array Map of lowercase header name => original name at registration */ private array $headerNames = []; + /** + * @return array> + */ public function getHeaders(): array { return $this->headers; @@ -26,7 +29,7 @@ public function hasHeader(string $header): bool } /** - * @return string[] + * @return list */ public function getHeader(string $header): array { @@ -45,9 +48,10 @@ public function getHeaderLine(string $header): string return \implode(', ', $this->getHeader($header)); } - public function withHeader(string $header, $value): static + public function withHeader(string $header, mixed $value): static { $value = $this->validateAndTrimHeader($header, $value); + /** @var non-empty-string $normalized */ $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $new = clone $this; @@ -87,7 +91,7 @@ public function withoutHeader(string $header): static } /** - * @param array> $headers + * @param array> $headers */ private function setHeaders(array $headers): void { @@ -98,6 +102,7 @@ private function setHeaders(array $headers): void $header = (string)$header; } $value = $this->validateAndTrimHeader($header, $value); + /** @var non-empty-string $normalized */ $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; @@ -126,8 +131,12 @@ private function setHeaders(array $headers): void * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] ) * * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + * + * @psalm-assert non-empty-string $header + * + * @return non-empty-list */ - private function validateAndTrimHeader(string $header, $values): array + private function validateAndTrimHeader(string $header, mixed $values): array { if (1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); @@ -170,10 +179,10 @@ private function validateAndTrimHeader(string $header, $values): array /** * List of header values. * - * @param array> $headers + * @param array> $headers * @param non-empty-string $header * - * @return list + * @return list */ private static function findHeader(array $headers, string $header): array { diff --git a/src/Traffic/Message/Multipart/Field.php b/src/Traffic/Message/Multipart/Field.php index 3ceb14fd..ce13a111 100644 --- a/src/Traffic/Message/Multipart/Field.php +++ b/src/Traffic/Message/Multipart/Field.php @@ -6,7 +6,7 @@ /** * @psalm-type FieldDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, * value: string * } @@ -15,6 +15,9 @@ */ final class Field extends Part { + /** + * @param array> $headers + */ public function __construct(array $headers, ?string $name = null, private string $value = '') { parent::__construct($headers, $name); @@ -30,6 +33,7 @@ public static function fromArray(array $data): self /** * @return FieldDataArray + * @psalm-suppress ImplementedReturnTypeMismatch */ public function jsonSerialize(): array { diff --git a/src/Traffic/Message/Multipart/File.php b/src/Traffic/Message/Multipart/File.php index 890a71f4..21f15036 100644 --- a/src/Traffic/Message/Multipart/File.php +++ b/src/Traffic/Message/Multipart/File.php @@ -10,10 +10,10 @@ /** * @psalm-type FileDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, - * fileName: string, - * size?: int + * fileName?: string, + * size?: non-negative-int * } * * @internal @@ -21,8 +21,12 @@ final class File extends Part implements UploadedFileInterface { private ?UploadedFileInterface $uploadedFile = null; + /** @var non-negative-int|null */ private ?int $fileSize = null; + /** + * @param array> $headers + */ public function __construct(array $headers, ?string $name = null, private ?string $fileName = null) { parent::__construct($headers, $name); @@ -33,24 +37,35 @@ public function __construct(array $headers, ?string $name = null, private ?strin */ public static function fromArray(array $data): self { - $self = new self($data['headers'], $data['name'] ?? null, $data['fileName']); + $self = new self($data['headers'], $data['name'] ?? null, $data['fileName'] ?? null); $self->fileSize = $data['size'] ?? null; return $self; } /** * @return FileDataArray + * + * @psalm-suppress ImplementedReturnTypeMismatch */ public function jsonSerialize(): array { - return parent::jsonSerialize() + [ - 'fileName' => $this->fileName, - 'size' => $this->getSize(), - ]; + $data = parent::jsonSerialize(); + if ($this->fileName !== null) { + $data['fileName'] = $this->fileName; + } + if ($this->fileSize !== null) { + $data['size'] = $this->fileSize; + } + + return $data; } + /** + * @param non-negative-int|null $size + */ public function setStream(StreamInterface $stream, ?int $size = null, int $code = \UPLOAD_ERR_OK): void { + /** @psalm-suppress PropertyTypeCoercion */ $this->fileSize = $size ?? $stream->getSize() ?? null; $this->uploadedFile = new UploadedFile( $stream, @@ -83,6 +98,9 @@ public function moveTo(string $targetPath): void $this->getUploadedFile()->moveTo($targetPath); } + /** + * @return non-negative-int|null + */ public function getSize(): ?int { return $this->fileSize; diff --git a/src/Traffic/Message/Multipart/Part.php b/src/Traffic/Message/Multipart/Part.php index 8db1fb02..dd9b9d9d 100644 --- a/src/Traffic/Message/Multipart/Part.php +++ b/src/Traffic/Message/Multipart/Part.php @@ -6,6 +6,7 @@ use Buggregator\Trap\Traffic\Message\Headers; use JsonSerializable; +use RuntimeExceptio; use RuntimeException; /** @@ -15,6 +16,9 @@ abstract class Part implements JsonSerializable { use Headers; + /** + * @param array> $headers + */ protected function __construct( array $headers, protected ?string $name, @@ -22,7 +26,10 @@ protected function __construct( $this->setHeaders($headers); } - public static function create(array $headers): static + /** + * @param array> $headers + */ + public static function create(array $headers): Part { /** * Check Content-Disposition header @@ -31,6 +38,9 @@ public static function create(array $headers): static */ $contentDisposition = self::findHeader($headers, 'Content-Disposition')[0] ?? throw new RuntimeException('Missing Content-Disposition header.'); + if ($contentDisposition === '') { + throw new RuntimeException('Missing Content-Disposition header, can\'t be empty'); + } // Get field name and file name $name = \preg_match('/\bname=(?:(?[^" ;,]++)|"(?[^"]++)")/', $contentDisposition, $matches) === 1 @@ -63,11 +73,22 @@ public function withName(?string $name): static return $clone; } + /** + * @return array{ + * headers: array>, + * name?: string, + * } + */ public function jsonSerialize(): array { - return [ - 'name' => $this->name, + $data = [ 'headers' => $this->headers, ]; + + if ($this->name !== null) { + $data['name'] = $this->name; + } + + return $data; } } diff --git a/src/Traffic/Message/Smtp.php b/src/Traffic/Message/Smtp.php index 12d9c3e5..00bcf09e 100644 --- a/src/Traffic/Message/Smtp.php +++ b/src/Traffic/Message/Smtp.php @@ -6,6 +6,7 @@ use Buggregator\Trap\Traffic\Message\Multipart\Field; use Buggregator\Trap\Traffic\Message\Multipart\File; +use Buggregator\Trap\Traffic\Message\Multipart\Part; use Buggregator\Trap\Traffic\Message\Smtp\Contact; use Buggregator\Trap\Traffic\Message\Smtp\MessageFormat; use JsonSerializable; @@ -31,8 +32,8 @@ final class Smtp implements JsonSerializable private array $attachments = []; /** - * @param array> $protocol - * @param array> $headers + * @param array> $protocol + * @param array> $headers */ private function __construct( private readonly array $protocol, @@ -42,14 +43,22 @@ private function __construct( } /** - * @param array> $protocol - * @param array> $headers + * @param array> $protocol + * @param array> $headers */ public static function create(array $protocol, array $headers): self { return new self($protocol, $headers); } + /** + * @param array{ + * protocol: array>, + * headers: array>, + * messages: list, + * attachments: list, + * } $data + */ public static function fromArray(array $data): self { $self = new self($data['protocol'], $data['headers']); @@ -74,7 +83,7 @@ public function jsonSerialize(): array } /** - * @return Field[] + * @return list */ public function getMessages(): array { @@ -82,7 +91,7 @@ public function getMessages(): array } /** - * @return File[] + * @return list */ public function getAttachments(): array { @@ -90,7 +99,7 @@ public function getAttachments(): array } /** - * @param Field[] $messages + * @param list $messages */ public function withMessages(array $messages): self { @@ -100,7 +109,7 @@ public function withMessages(array $messages): self } /** - * @param File[] $attachments + * @param list $attachments */ public function withAttachments(array $attachments): self { @@ -110,7 +119,7 @@ public function withAttachments(array $attachments): self } /** - * @return array> + * @return array> */ public function getProtocol(): array { diff --git a/src/Traffic/Parser/Http.php b/src/Traffic/Parser/Http.php index 147f4afb..2bec87a1 100644 --- a/src/Traffic/Parser/Http.php +++ b/src/Traffic/Parser/Http.php @@ -64,8 +64,13 @@ public function parseStream(StreamClient $stream): ServerRequestInterface $rawCookies = \explode(';', $request->getHeaderLine('Cookie')); $cookies = []; foreach ($rawCookies as $cookie) { - [$name, $value] = \explode('=', \trim($cookie), 2); - $cookies[$name] = $value; + if (!str_contains($cookie, '=')) { + /** @psalm-suppress PossiblyUndefinedArrayOffset */ + [$name, $value] = \explode('=', \trim($cookie), 2); + $cookies[$name] = $value; + } else { + throw new RuntimeException('Invalid cookie: ' . $cookie); + } } $request = $request->withCookieParams($cookies); @@ -77,7 +82,7 @@ public function parseStream(StreamClient $stream): ServerRequestInterface /** * @param string $line * - * @return array{0: non-empty-string, 1: non-empty-string, 2: non-empty-string} + * @return list{non-empty-string, non-empty-string, non-empty-string} */ private function parseFirstLine(string $line): array { @@ -85,7 +90,11 @@ private function parseFirstLine(string $line): array if (\count($parts) !== 3) { throw new \InvalidArgumentException('Invalid first line.'); } + $parts[2] = \explode('/', $parts[2], 2)[1] ?? $parts[2]; + if ($parts[0] === '' || $parts[1] === '' || $parts[2] === '') { + throw new \InvalidArgumentException('Invalid first line.'); + } return $parts; } @@ -143,8 +152,11 @@ private function processMultipartForm(ServerRequestInterface $request): ServerRe if (\preg_match('/boundary="?([^"\\s;]++)"?/', $request->getHeaderLine('Content-Type'), $matches) !== 1) { return $request; } - $boundary = $matches[1]; + if ($boundary === '') { + throw new \Exception('Boundary can\'t be empty'); + } + $parts = self::parseMultipartBody($request->getBody(), $boundary); $uploadedFiles = $parsedBody = []; foreach ($parts as $part) { @@ -159,7 +171,7 @@ private function processMultipartForm(ServerRequestInterface $request): ServerRe if ($part instanceof File) { $uploadedFiles[$name][] = new UploadedFile( $part->getStream(), - $part->getSize(), + (int) $part->getSize(), $part->getError(), $part->getClientFilename(), $part->getClientMediaType(), diff --git a/src/Traffic/Parser/Smtp.php b/src/Traffic/Parser/Smtp.php index 5a915c79..55673453 100644 --- a/src/Traffic/Parser/Smtp.php +++ b/src/Traffic/Parser/Smtp.php @@ -18,7 +18,7 @@ final class Smtp { /** - * @param array> $protocol + * @param array> $protocol */ public function parseStream(array $protocol, StreamClient $stream): Message\Smtp { diff --git a/src/functions.php b/src/functions.php index 713171b9..0566d2f5 100644 --- a/src/functions.php +++ b/src/functions.php @@ -30,9 +30,13 @@ function trap(mixed ...$values): TrapHandle * Register the var-dump caster for protobuf messages */ if (\class_exists(AbstractCloner::class)) { + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[Message::class] ??= [ProtobufCaster::class, 'cast']; + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[RepeatedField::class] ??= [ProtobufCaster::class, 'castRepeated']; + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[MapField::class] ??= [ProtobufCaster::class, 'castMap']; + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[EnumValue::class] ??= [ProtobufCaster::class, 'castEnum']; } From 442709505f0d0ee308c144c2414bd4215b135532 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 30 Apr 2024 19:02:20 +0400 Subject: [PATCH 10/27] Fix few psalm types --- src/Proto/Frame/Smtp.php | 2 ++ .../Console/Renderer/SentryEnvelope.php | 2 +- src/Sender/Console/Renderer/Smtp.php | 4 +-- src/Sender/Console/Renderer/VarDumper.php | 4 +-- src/Sender/Console/Support/Common.php | 16 ++++++--- src/Sender/Console/Support/Files.php | 8 ++--- src/Socket/Client.php | 10 +++--- src/Socket/Server.php | 11 +++---- src/Support/StreamHelper.php | 2 +- src/Traffic/Dispatcher/Http.php | 3 +- src/Traffic/Dispatcher/Smtp.php | 4 +-- src/Traffic/Message/Headers.php | 5 +-- src/Traffic/Message/Multipart/Field.php | 2 +- src/Traffic/Message/Multipart/File.php | 12 +++---- src/Traffic/Message/Multipart/Part.php | 6 ++-- src/Traffic/Message/Smtp.php | 24 +++++++------- src/Traffic/Parser/Http.php | 12 +++---- src/Traffic/Parser/Smtp.php | 2 +- tests/Unit/Traffic/Parser/HttpParserTest.php | 33 +++++++++++++++++++ 19 files changed, 96 insertions(+), 66 deletions(-) diff --git a/src/Proto/Frame/Smtp.php b/src/Proto/Frame/Smtp.php index ae4a2229..b460f24f 100644 --- a/src/Proto/Frame/Smtp.php +++ b/src/Proto/Frame/Smtp.php @@ -14,6 +14,7 @@ /** * @internal * @psalm-internal Buggregator + * @psalm-import-type TArrayData from Message\Smtp */ final class Smtp extends Frame implements FilesCarrier { @@ -34,6 +35,7 @@ public function __toString(): string public static function fromString(string $payload, DateTimeImmutable $time): static { + /** @var TArrayData $payload */ $payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR); $message = Message\Smtp::fromArray($payload); diff --git a/src/Sender/Console/Renderer/SentryEnvelope.php b/src/Sender/Console/Renderer/SentryEnvelope.php index 68b4783b..5b7c169a 100644 --- a/src/Sender/Console/Renderer/SentryEnvelope.php +++ b/src/Sender/Console/Renderer/SentryEnvelope.php @@ -36,7 +36,7 @@ public function render(OutputInterface $output, Frame $frame): void ++$i; try { $type = $item->headers['type'] ?? null; - Common::renderHeader2($output, "Item $i", green: $type); + Common::renderHeader2($output, "Item $i", green: (string)$type); Header::renderMessageHeader($output, $item->payload); $this->renderItem($output, $item); diff --git a/src/Sender/Console/Renderer/Smtp.php b/src/Sender/Console/Renderer/Smtp.php index 5ef8eb0b..430d9519 100644 --- a/src/Sender/Console/Renderer/Smtp.php +++ b/src/Sender/Console/Renderer/Smtp.php @@ -58,9 +58,9 @@ public function render(OutputInterface $output, Frame $frame): void foreach ($message->getAttachments() as $attach) { Files::renderFile( $output, - $attach->getClientFilename() ?? 'undefined', + $attach->getClientFilename() ?? '', $attach->getSize(), - $attach->getClientMediaType() ?? 'undefined', + $attach->getClientMediaType() ?? '', ); } $output->writeln(''); diff --git a/src/Sender/Console/Renderer/VarDumper.php b/src/Sender/Console/Renderer/VarDumper.php index 07f8f512..11edf05a 100644 --- a/src/Sender/Console/Renderer/VarDumper.php +++ b/src/Sender/Console/Renderer/VarDumper.php @@ -58,7 +58,7 @@ public function __construct( } /** - * @psalm-suppress RiskyTruthyFalsyComparison + * @psalm-suppress RiskyTruthyFalsyComparison, MixedArrayAccess, MixedArgument */ public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void { @@ -94,7 +94,7 @@ public function describe(OutputInterface $output, Data $data, array $context, in empty($request['method'] ?? '') or $meta['Method'] = $request['method']; empty($request['uri'] ?? '') or $meta['URI'] = $request['uri']; if ($controller = $request['controller']) { - $meta['Controller'] = rtrim((string) $this->dumper->dump($controller, true), "\n"); + $meta['Controller'] = \rtrim((string) $this->dumper->dump($controller, true), "\n"); } } elseif (isset($context['cli'])) { $meta['Command'] = $context['cli']['command_line']; diff --git a/src/Sender/Console/Support/Common.php b/src/Sender/Console/Support/Common.php index c0355608..83adf92c 100644 --- a/src/Sender/Console/Support/Common.php +++ b/src/Sender/Console/Support/Common.php @@ -26,12 +26,16 @@ public static function renderHeader1(OutputInterface $output, string $title, ?st $output->writeln(['', \implode('', $parts), '']); } - public static function renderHeader2(OutputInterface $output, string $title, ?string ...$sub): void + public static function renderHeader2(OutputInterface $output, string $title, string ...$sub): void { $parts = ["# $title "]; foreach ($sub as $color => $value) { + if ($value === '') { + continue; + } + $color = \is_string($color) ? $color : 'gray'; - $parts[] = \sprintf(' %s ', $color, $value ?? 'NULL'); + $parts[] = \sprintf(' %s ', $color, $value); } $output->writeln(['', \implode('', $parts), '']); @@ -53,12 +57,14 @@ public static function renderHighlightedLine(OutputInterface $output, string $li } /** - * @param array $data + * @param array $data */ public static function renderMetadata(OutputInterface $output, array $data): void { - /** @psalm-suppress ArgumentTypeCoercion */ - $maxHeaderLength = \max(\array_map('strlen', \array_keys($data))); + $maxHeaderLength = \max(0, ...\array_map( + static fn(string|int $key): int => \strlen((string) $key), + \array_keys($data)), + ); foreach ($data as $head => $value) { // Align headers to the right diff --git a/src/Sender/Console/Support/Files.php b/src/Sender/Console/Support/Files.php index 180c38cd..800d791b 100644 --- a/src/Sender/Console/Support/Files.php +++ b/src/Sender/Console/Support/Files.php @@ -13,7 +13,6 @@ final class Files { /** - * * @param non-negative-int|null $size * * Render file info. Example: @@ -27,7 +26,6 @@ final class Files * │ │ any value <= positional argument * │ico│ unknown size * └───┘ image/x-icon - * */ public static function renderFile( OutputInterface $output, @@ -37,8 +35,10 @@ public static function renderFile( string ...$additional ): void { // File extension - $ex = \substr($fileName, (int) \strrpos($fileName, '.') + 1); - $ex = \strlen($ex) > 3 ? ' ' : \str_pad($ex, 3, ' ', \STR_PAD_BOTH); + $dotPos = \strrpos($fileName, '.'); + $ex = $dotPos === false || \strlen($fileName) - $dotPos > 4 + ? ' ' + : \str_pad(\substr($fileName, $dotPos + 1), 3, ' ', \STR_PAD_BOTH); // File size $sizeStr = self::normalizeSize($size) ?? 'unknown size'; diff --git a/src/Socket/Client.php b/src/Socket/Client.php index 721e47a8..494eba8c 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -115,23 +115,21 @@ protected function onInit(): void } /** - * @param callable(string): void $callable Non-static callable. + * @param callable(string): void $callable If non-static callable, it will be bound to the current instance. * @psalm-assert callable(string): void $callable */ public function setOnPayload(callable $callable): void { - /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ - $this->onPayload = \Closure::bind($callable(...), $this); + $this->onPayload = \Closure::bind($callable(...), $this) ?? $callable(...); } /** - * @param callable(): void $callable Non-static callable. + * @param callable(): void $callable If non-static callable, it will be bound to the current instance. * @psalm-assert callable(): void $callable */ public function setOnClose(callable $callable): void { - /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ - $this->onClose = \Closure::bind($callable(...), $this); + $this->onClose = \Closure::bind($callable(...), $this) ?? $callable(...); } public function send(string $payload): void diff --git a/src/Socket/Server.php b/src/Socket/Server.php index f69276e6..994c5113 100644 --- a/src/Socket/Server.php +++ b/src/Socket/Server.php @@ -20,8 +20,7 @@ */ final class Server implements Processable, Cancellable, Destroyable { - /** @var false|resource|Socket */ - private $socket; + private Socket $socket; /** @var array */ private array $clients = []; @@ -41,13 +40,11 @@ private function __construct( private readonly ?Closure $clientInflector, private readonly Logger $logger, ) { - $this->socket = @\socket_create_listen($port); + $this->socket = @\socket_create_listen($port) ?: throw new \RuntimeException('Socket create failed.'); + /** @link https://github.com/buggregator/trap/pull/14 */ // \socket_set_option($this->socket, \SOL_SOCKET, \SO_LINGER, ['l_linger' => 0, 'l_onoff' => 1]); - if ($this->socket === false) { - throw new \RuntimeException('Socket create failed.'); - } \socket_set_nonblock($this->socket); $logger->status('Application', 'Server started on 127.0.0.1:%s', $port); @@ -92,7 +89,7 @@ public static function init( public function process(): void { - /** @psalm-suppress PossiblyInvalidArgument */ + // /** @psalm-suppress PossiblyInvalidArgument */ while (!$this->cancelled and false !== ($socket = \socket_accept($this->socket))) { $client = null; try { diff --git a/src/Support/StreamHelper.php b/src/Support/StreamHelper.php index ae06e1c5..e937a24d 100644 --- a/src/Support/StreamHelper.php +++ b/src/Support/StreamHelper.php @@ -67,7 +67,7 @@ public static function strpos(StreamInterface $stream, string $substr): int|fals * * @param non-empty-string $boundary * - * @return non-negative-int Bytes written + * @return int<0, max> Bytes written */ public static function writeStreamUntil(StreamInterface $from, StreamInterface $to, string $boundary): int { diff --git a/src/Traffic/Dispatcher/Http.php b/src/Traffic/Dispatcher/Http.php index 3e0fa6f9..d06361f0 100644 --- a/src/Traffic/Dispatcher/Http.php +++ b/src/Traffic/Dispatcher/Http.php @@ -47,13 +47,12 @@ public function __construct( $handlers[] = new Fallback($middlewares); // Build pipeline of handlers. - /** @psalm-suppress MixedPropertyTypeCoercion */ $this->pipeline = Pipeline::build( $handlers, /** @see RequestHandler::handle() */ 'handle', static function (): never { throw new \LogicException('No handler found for request.'); }, - Generator::class, + 'never', ); } diff --git a/src/Traffic/Dispatcher/Smtp.php b/src/Traffic/Dispatcher/Smtp.php index 7dbe286c..f670a3ad 100644 --- a/src/Traffic/Dispatcher/Smtp.php +++ b/src/Traffic/Dispatcher/Smtp.php @@ -39,11 +39,11 @@ public function dispatch(StreamClient $stream): iterable if (\preg_match('/^(?:EHLO|HELO)/', $response)) { $stream->sendData($this->createResponse(self::OK)); } elseif (\preg_match('/^MAIL FROM:\s*<(.*)>/', $response, $matches)) { - /** @var array{1: non-empty-string} $matches */ + /** @var array{0: non-empty-string, 1: string} $matches */ $protocol['FROM'][] = $matches[1]; $stream->sendData($this->createResponse(self::OK)); } elseif (\preg_match('/^RCPT TO:\s*<(.*)>/', $response, $matches)) { - /** @var array{1: non-empty-string} $matches */ + /** @var array{0: non-empty-string, 1: string} $matches */ $protocol['BCC'][] = $matches[1]; $stream->sendData($this->createResponse(self::OK)); } elseif (\str_starts_with($response, 'QUIT')) { diff --git a/src/Traffic/Message/Headers.php b/src/Traffic/Message/Headers.php index a906baed..4fc09473 100644 --- a/src/Traffic/Message/Headers.php +++ b/src/Traffic/Message/Headers.php @@ -9,7 +9,7 @@ */ trait Headers { - /** @var array> Map of all registered headers, as original name => array of values */ + /** @var array> Map of all registered headers */ private array $headers = []; /** @var array Map of lowercase header name => original name at registration */ @@ -91,7 +91,7 @@ public function withoutHeader(string $header): static } /** - * @param array> $headers + * @param array> $headers */ private function setHeaders(array $headers): void { @@ -101,6 +101,7 @@ private function setHeaders(array $headers): void // We must cast it back to a string in order to comply with validation. $header = (string)$header; } + $value = $this->validateAndTrimHeader($header, $value); /** @var non-empty-string $normalized */ $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); diff --git a/src/Traffic/Message/Multipart/Field.php b/src/Traffic/Message/Multipart/Field.php index ce13a111..e85f2bcc 100644 --- a/src/Traffic/Message/Multipart/Field.php +++ b/src/Traffic/Message/Multipart/Field.php @@ -16,7 +16,7 @@ final class Field extends Part { /** - * @param array> $headers + * @param array> $headers */ public function __construct(array $headers, ?string $name = null, private string $value = '') { diff --git a/src/Traffic/Message/Multipart/File.php b/src/Traffic/Message/Multipart/File.php index 21f15036..c0f8cb02 100644 --- a/src/Traffic/Message/Multipart/File.php +++ b/src/Traffic/Message/Multipart/File.php @@ -10,7 +10,7 @@ /** * @psalm-type FileDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, * fileName?: string, * size?: non-negative-int @@ -25,7 +25,7 @@ final class File extends Part implements UploadedFileInterface private ?int $fileSize = null; /** - * @param array> $headers + * @param array> $headers */ public function __construct(array $headers, ?string $name = null, private ?string $fileName = null) { @@ -50,12 +50,8 @@ public static function fromArray(array $data): self public function jsonSerialize(): array { $data = parent::jsonSerialize(); - if ($this->fileName !== null) { - $data['fileName'] = $this->fileName; - } - if ($this->fileSize !== null) { - $data['size'] = $this->fileSize; - } + $this->fileName === null or $data['fileName'] = $this->fileName; + $this->fileSize === null or $data['size'] = $this->fileSize; return $data; } diff --git a/src/Traffic/Message/Multipart/Part.php b/src/Traffic/Message/Multipart/Part.php index dd9b9d9d..4b831367 100644 --- a/src/Traffic/Message/Multipart/Part.php +++ b/src/Traffic/Message/Multipart/Part.php @@ -17,7 +17,7 @@ abstract class Part implements JsonSerializable use Headers; /** - * @param array> $headers + * @param array> $headers */ protected function __construct( array $headers, @@ -27,7 +27,7 @@ protected function __construct( } /** - * @param array> $headers + * @param array> $headers */ public static function create(array $headers): Part { @@ -75,7 +75,7 @@ public function withName(?string $name): static /** * @return array{ - * headers: array>, + * headers: array>, * name?: string, * } */ diff --git a/src/Traffic/Message/Smtp.php b/src/Traffic/Message/Smtp.php index 00bcf09e..896b3bad 100644 --- a/src/Traffic/Message/Smtp.php +++ b/src/Traffic/Message/Smtp.php @@ -18,6 +18,13 @@ * @psalm-import-type FieldDataArray from Field * @psalm-import-type FileDataArray from File * + * @psalm-type TArrayData = array{ + * protocol: array>, + * headers: array>, + * messages: list, + * attachments: list, + * } + * * @internal */ final class Smtp implements JsonSerializable @@ -32,8 +39,8 @@ final class Smtp implements JsonSerializable private array $attachments = []; /** - * @param array> $protocol - * @param array> $headers + * @param array> $protocol + * @param array> $headers */ private function __construct( private readonly array $protocol, @@ -43,8 +50,8 @@ private function __construct( } /** - * @param array> $protocol - * @param array> $headers + * @param array> $protocol + * @param array> $headers */ public static function create(array $protocol, array $headers): self { @@ -52,12 +59,7 @@ public static function create(array $protocol, array $headers): self } /** - * @param array{ - * protocol: array>, - * headers: array>, - * messages: list, - * attachments: list, - * } $data + * @param TArrayData $data */ public static function fromArray(array $data): self { @@ -119,7 +121,7 @@ public function withAttachments(array $attachments): self } /** - * @return array> + * @return array> */ public function getProtocol(): array { diff --git a/src/Traffic/Parser/Http.php b/src/Traffic/Parser/Http.php index 2bec87a1..dff70ed2 100644 --- a/src/Traffic/Parser/Http.php +++ b/src/Traffic/Parser/Http.php @@ -64,12 +64,10 @@ public function parseStream(StreamClient $stream): ServerRequestInterface $rawCookies = \explode(';', $request->getHeaderLine('Cookie')); $cookies = []; foreach ($rawCookies as $cookie) { - if (!str_contains($cookie, '=')) { + if (\str_contains($cookie, '=')) { /** @psalm-suppress PossiblyUndefinedArrayOffset */ [$name, $value] = \explode('=', \trim($cookie), 2); $cookies[$name] = $value; - } else { - throw new RuntimeException('Invalid cookie: ' . $cookie); } } @@ -82,7 +80,7 @@ public function parseStream(StreamClient $stream): ServerRequestInterface /** * @param string $line * - * @return list{non-empty-string, non-empty-string, non-empty-string} + * @return array{non-empty-string, non-empty-string, non-empty-string} */ private function parseFirstLine(string $line): array { @@ -152,10 +150,8 @@ private function processMultipartForm(ServerRequestInterface $request): ServerRe if (\preg_match('/boundary="?([^"\\s;]++)"?/', $request->getHeaderLine('Content-Type'), $matches) !== 1) { return $request; } + /** @var non-empty-string $boundary */ $boundary = $matches[1]; - if ($boundary === '') { - throw new \Exception('Boundary can\'t be empty'); - } $parts = self::parseMultipartBody($request->getBody(), $boundary); $uploadedFiles = $parsedBody = []; @@ -213,7 +209,7 @@ private function createBody(StreamClient $stream, ?int $limit): StreamInterface } /** - * @return array> + * @return array> */ public static function parseHeaders(string $headersBlock): array { diff --git a/src/Traffic/Parser/Smtp.php b/src/Traffic/Parser/Smtp.php index 55673453..8d9abcdb 100644 --- a/src/Traffic/Parser/Smtp.php +++ b/src/Traffic/Parser/Smtp.php @@ -18,7 +18,7 @@ final class Smtp { /** - * @param array> $protocol + * @param array> $protocol */ public function parseStream(array $protocol, StreamClient $stream): Message\Smtp { diff --git a/tests/Unit/Traffic/Parser/HttpParserTest.php b/tests/Unit/Traffic/Parser/HttpParserTest.php index f4c8f2d7..b505cb07 100644 --- a/tests/Unit/Traffic/Parser/HttpParserTest.php +++ b/tests/Unit/Traffic/Parser/HttpParserTest.php @@ -52,6 +52,39 @@ public function testSimpleGet(): void ], $request->getCookieParams()); } + /**\ + * Parer doesn't fail on wrong cookies + */ + public function testWrongCookie(): void + { + $body = \str_split( + <<parseStream($body); + + $this->assertSame('GET', $request->getMethod()); + $this->assertSame('/foo/bar', $request->getUri()->getPath()); + $this->assertSame('1.1', $request->getProtocolVersion()); + $this->assertSame(['127.0.0.1:9912'], $request->getHeader('host')); + $this->assertSame(['ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3'], $request->getHeader('accept-language')); + $this->assertSame([ + 'csrf-token.sig' => 'X0fR', + ], $request->getCookieParams()); + } + public function testPostUrlEncoded(): void { $body = \str_split( From 38ce3fc87687f28c1ba817a4af8c9eeb4089677b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 30 Apr 2024 19:26:03 +0400 Subject: [PATCH 11/27] Fix few psalm types: make headers key as array-key --- src/Traffic/Message/Headers.php | 4 ++-- src/Traffic/Message/Multipart/Field.php | 4 ++-- src/Traffic/Message/Multipart/File.php | 4 ++-- src/Traffic/Message/Multipart/Part.php | 8 ++++---- src/Traffic/Parser/Http.php | 2 +- src/Traffic/Parser/Smtp.php | 1 + 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Traffic/Message/Headers.php b/src/Traffic/Message/Headers.php index 4fc09473..5c25892a 100644 --- a/src/Traffic/Message/Headers.php +++ b/src/Traffic/Message/Headers.php @@ -180,7 +180,7 @@ private function validateAndTrimHeader(string $header, mixed $values): array /** * List of header values. * - * @param array> $headers + * @param array> $headers * @param non-empty-string $header * * @return list @@ -190,7 +190,7 @@ private static function findHeader(array $headers, string $header): array $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $result = []; foreach ($headers as $name => $values) { - if (\strtr($name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') === $header) { + if (\strtr((string) $name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') === $header) { $result = [...$result, ...$values]; } } diff --git a/src/Traffic/Message/Multipart/Field.php b/src/Traffic/Message/Multipart/Field.php index e85f2bcc..80c74563 100644 --- a/src/Traffic/Message/Multipart/Field.php +++ b/src/Traffic/Message/Multipart/Field.php @@ -6,7 +6,7 @@ /** * @psalm-type FieldDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, * value: string * } @@ -16,7 +16,7 @@ final class Field extends Part { /** - * @param array> $headers + * @param array> $headers */ public function __construct(array $headers, ?string $name = null, private string $value = '') { diff --git a/src/Traffic/Message/Multipart/File.php b/src/Traffic/Message/Multipart/File.php index c0f8cb02..3ce9738c 100644 --- a/src/Traffic/Message/Multipart/File.php +++ b/src/Traffic/Message/Multipart/File.php @@ -10,7 +10,7 @@ /** * @psalm-type FileDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, * fileName?: string, * size?: non-negative-int @@ -25,7 +25,7 @@ final class File extends Part implements UploadedFileInterface private ?int $fileSize = null; /** - * @param array> $headers + * @param array> $headers */ public function __construct(array $headers, ?string $name = null, private ?string $fileName = null) { diff --git a/src/Traffic/Message/Multipart/Part.php b/src/Traffic/Message/Multipart/Part.php index 4b831367..0e958b5d 100644 --- a/src/Traffic/Message/Multipart/Part.php +++ b/src/Traffic/Message/Multipart/Part.php @@ -17,7 +17,7 @@ abstract class Part implements JsonSerializable use Headers; /** - * @param array> $headers + * @param array> $headers */ protected function __construct( array $headers, @@ -27,7 +27,7 @@ protected function __construct( } /** - * @param array> $headers + * @param array> $headers */ public static function create(array $headers): Part { @@ -75,8 +75,8 @@ public function withName(?string $name): static /** * @return array{ - * headers: array>, - * name?: string, + * headers: array>, + * name?: string * } */ public function jsonSerialize(): array diff --git a/src/Traffic/Parser/Http.php b/src/Traffic/Parser/Http.php index dff70ed2..e541946e 100644 --- a/src/Traffic/Parser/Http.php +++ b/src/Traffic/Parser/Http.php @@ -41,7 +41,7 @@ public function parseStream(StreamClient $stream): ServerRequestInterface $request = $this->factory->createServerRequest($method, $uri, []) ->withProtocolVersion($protocol); foreach ($headers as $name => $value) { - $request = $request->withHeader($name, $value); + $request = $request->withHeader((string) $name, $value); } // Todo refactor: diff --git a/src/Traffic/Parser/Smtp.php b/src/Traffic/Parser/Smtp.php index 8d9abcdb..50310489 100644 --- a/src/Traffic/Parser/Smtp.php +++ b/src/Traffic/Parser/Smtp.php @@ -99,6 +99,7 @@ private function processSingleBody(Message\Smtp $message, StreamInterface $strea { $content = \preg_replace(["/^\.([^\r])/m", "/(\r\n\\.\r\n)$/D"], ['$1', ''], $stream->getContents()); + /** @psalm-suppress InvalidArgument */ $body = new Field( headers: \array_intersect_key($message->getHeaders(), ['Content-Type' => true]), value: $content, From cc3c4be6adb2cce73389a4174168df38cb1e06fe Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 30 Apr 2024 18:33:57 +0300 Subject: [PATCH 12/27] Changed permission flags for bin/trap to 755 (#58) --- bin/trap | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/trap diff --git a/bin/trap b/bin/trap old mode 100644 new mode 100755 From fe18ffe4fbb163b52e17ef517a7b94c376f95f72 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 13:11:28 +0400 Subject: [PATCH 13/27] Add Config Loader that may fetch values from XML using XPath --- composer.json | 1 + src/Service/Config/ConfigAttribute.php | 12 +++ src/Service/Config/ConfigLoader.php | 87 +++++++++++++++++++ src/Service/Config/XPath.php | 17 ++++ .../Unit/Service/Config/ConfigLoaderTest.php | 44 ++++++++++ trap.xml | 6 ++ 6 files changed, 167 insertions(+) create mode 100644 src/Service/Config/ConfigAttribute.php create mode 100644 src/Service/Config/ConfigLoader.php create mode 100644 src/Service/Config/XPath.php create mode 100644 tests/Unit/Service/Config/ConfigLoaderTest.php create mode 100644 trap.xml diff --git a/composer.json b/composer.json index 5fe7c04a..7c7ab370 100644 --- a/composer.json +++ b/composer.json @@ -71,6 +71,7 @@ "vimeo/psalm": "^5.11" }, "suggest": { + "ext-simplexml": "To load trap.xml", "roxblnfk/unpoly": "If you want to remove unnecessary PHP polyfills depend on PHP version." } } diff --git a/src/Service/Config/ConfigAttribute.php b/src/Service/Config/ConfigAttribute.php new file mode 100644 index 00000000..435be89f --- /dev/null +++ b/src/Service/Config/ConfigAttribute.php @@ -0,0 +1,12 @@ +xml = \is_string($xml) ? \simplexml_load_string($xml) : null; + } + + public function hidrate(object $config): void + { + // Read class properties + $reflection = new \ReflectionObject($config); + foreach ($reflection->getProperties() as $property) { + $attributes = $property->getAttributes(ConfigAttribute::class, \ReflectionAttribute::IS_INSTANCEOF); + if (\count($attributes) === 0) { + continue; + } + + $this->injectValue($config, $property, $attributes); + } + } + + /** + * @param \ReflectionProperty $property + * @param array<\ReflectionAttribute> $attributes + */ + private function injectValue(object $config, \ReflectionProperty $property, array $attributes): void + { + foreach ($attributes as $attribute) { + $attribute = $attribute->newInstance(); + + $value = match (true) { + $attribute instanceof XPath => $this->xml?->xpath($attribute->path), + default => null, + }; + + if ($value === null) { + continue; + } + + // Cast value to the property type + $type = $property->getType(); + $result = match (true) { + $type === null => $value[0], + $type->allowsNull() && $value[0] === '' => null, + $type->isBuiltin() => match ($type->getName()) { + 'int' => (int) $value[0], + 'float' => (float) $value[0], + 'bool' => \filter_var($value[0], FILTER_VALIDATE_BOOLEAN), + default => $value[0], + }, + default => $value[0], + }; + + // Set the property value + $property->setValue($config, $result); + } + } +} diff --git a/src/Service/Config/XPath.php b/src/Service/Config/XPath.php new file mode 100644 index 00000000..120289c3 --- /dev/null +++ b/src/Service/Config/XPath.php @@ -0,0 +1,17 @@ + + + + + + + + XML; + + $loader = new ConfigLoader(fn() => $xml); + $loader->hidrate($dto); + + self::assertTrue($dto->myBool); + self::assertSame(200, $dto->myInt); + self::assertSame('foo-bar', $dto->myString); + self::assertSame(42.0, $dto->myFloat); + } +} diff --git a/trap.xml b/trap.xml new file mode 100644 index 00000000..e7eea718 --- /dev/null +++ b/trap.xml @@ -0,0 +1,6 @@ + + + + + + From af4fb27781fb6da556926960a6add652ae3fab61 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 13:37:18 +0400 Subject: [PATCH 14/27] Normalize Frontend configs --- src/Config/Frontend.php | 17 ++++++++++++++ src/Config/Frontend/Buffer.php | 20 ----------------- src/Config/Frontend/EventStorage.php | 22 +++++++++++++++++++ .../{EventsStorage.php => EventStorage.php} | 6 ++--- src/Sender/Frontend/FrameHandler.php | 2 +- src/Sender/Frontend/Http/EventAssets.php | 4 ++-- src/Sender/Frontend/Http/Router.php | 4 ++-- src/Sender/Frontend/RPC.php | 2 +- src/Sender/Frontend/Service.php | 2 +- src/Sender/FrontendSender.php | 10 ++++----- .../Sender/Frontend/EventsStorageTest.php | 8 +++---- trap.xml | 2 +- 12 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 src/Config/Frontend.php delete mode 100644 src/Config/Frontend/Buffer.php create mode 100644 src/Config/Frontend/EventStorage.php rename src/Sender/Frontend/{EventsStorage.php => EventStorage.php} (91%) diff --git a/src/Config/Frontend.php b/src/Config/Frontend.php new file mode 100644 index 00000000..b8df12fd --- /dev/null +++ b/src/Config/Frontend.php @@ -0,0 +1,17 @@ + */ + #[XPath('/trap/frontend@port')] + public int $port = 8000; +} diff --git a/src/Config/Frontend/Buffer.php b/src/Config/Frontend/Buffer.php deleted file mode 100644 index 394353b4..00000000 --- a/src/Config/Frontend/Buffer.php +++ /dev/null @@ -1,20 +0,0 @@ - $maxSize The maximum number of events that can be stored in the buffer. - */ - public function __construct( - public readonly int $maxSize = 200, - ) { - } -} diff --git a/src/Config/Frontend/EventStorage.php b/src/Config/Frontend/EventStorage.php new file mode 100644 index 00000000..af9190e5 --- /dev/null +++ b/src/Config/Frontend/EventStorage.php @@ -0,0 +1,22 @@ + + */ + #[XPath('/trap/frontend/EventStorage@maxEvents')] + public int $maxEvents = 200; +} diff --git a/src/Sender/Frontend/EventsStorage.php b/src/Sender/Frontend/EventStorage.php similarity index 91% rename from src/Sender/Frontend/EventsStorage.php rename to src/Sender/Frontend/EventStorage.php index 4e46d149..d3023e1a 100644 --- a/src/Sender/Frontend/EventsStorage.php +++ b/src/Sender/Frontend/EventStorage.php @@ -4,7 +4,7 @@ namespace Buggregator\Trap\Sender\Frontend; -use Buggregator\Trap\Config\Frontend\Buffer as Config; +use Buggregator\Trap\Config\Frontend\EventStorage as Config; use Countable; use IteratorAggregate; @@ -12,7 +12,7 @@ * @internal * @implements IteratorAggregate */ -final class EventsStorage implements IteratorAggregate, Countable +final class EventStorage implements IteratorAggregate, Countable { /** @@ -32,7 +32,7 @@ public function add(Event $event): void $this->events[$event->uuid] = $event; $this->sorted = false; - if (\count($this->events) > $this->config->maxSize) { + if (\count($this->events) > $this->config->maxEvents) { // find most old event and remove it $k = $event->uuid; $t = $event->timestamp; diff --git a/src/Sender/Frontend/FrameHandler.php b/src/Sender/Frontend/FrameHandler.php index 51edcf05..fd30f267 100644 --- a/src/Sender/Frontend/FrameHandler.php +++ b/src/Sender/Frontend/FrameHandler.php @@ -20,7 +20,7 @@ final class FrameHandler implements HandlerInterface public function __construct( private readonly Logger $logger, private readonly ConnectionPool $connectionPool, - private readonly EventsStorage $eventsStorage, + private readonly EventStorage $eventsStorage, ) { $this->frameMapper = new FrameMapper(); } diff --git a/src/Sender/Frontend/Http/EventAssets.php b/src/Sender/Frontend/Http/EventAssets.php index 35771fe5..442d5114 100644 --- a/src/Sender/Frontend/Http/EventAssets.php +++ b/src/Sender/Frontend/Http/EventAssets.php @@ -12,7 +12,7 @@ use Buggregator\Trap\Handler\Router\Router as CommonRouter; use Buggregator\Trap\Logger; use Buggregator\Trap\Sender\Frontend\Event\AttachedFile; -use Buggregator\Trap\Sender\Frontend\EventsStorage; +use Buggregator\Trap\Sender\Frontend\EventStorage; use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -27,7 +27,7 @@ final class EventAssets implements Middleware public function __construct( private readonly Logger $logger, - private readonly EventsStorage $eventsStorage, + private readonly EventStorage $eventsStorage, ) { $this->router = CommonRouter::new($this); } diff --git a/src/Sender/Frontend/Http/Router.php b/src/Sender/Frontend/Http/Router.php index 13e1656a..c5872c6a 100644 --- a/src/Sender/Frontend/Http/Router.php +++ b/src/Sender/Frontend/Http/Router.php @@ -8,7 +8,7 @@ use Buggregator\Trap\Handler\Router\Method; use Buggregator\Trap\Handler\Router\Router as CommonRouter; use Buggregator\Trap\Logger; -use Buggregator\Trap\Sender\Frontend\EventsStorage; +use Buggregator\Trap\Sender\Frontend\EventStorage; use Buggregator\Trap\Sender\Frontend\Service; use Buggregator\Trap\Support\Json; use Nyholm\Psr7\Response; @@ -25,7 +25,7 @@ final class Router implements Middleware public function __construct( private readonly Logger $logger, - EventsStorage $eventsStorage, + EventStorage $eventsStorage, ) { $service = new Service($logger, $eventsStorage); $this->router = CommonRouter::new($service); diff --git a/src/Sender/Frontend/RPC.php b/src/Sender/Frontend/RPC.php index be5b82f6..55749f47 100644 --- a/src/Sender/Frontend/RPC.php +++ b/src/Sender/Frontend/RPC.php @@ -17,7 +17,7 @@ final class RPC public function __construct( private readonly Logger $logger, - EventsStorage $eventsStorage, + EventStorage $eventsStorage, ) { $this->router = Router::new(new Service($logger, $eventsStorage)); } diff --git a/src/Sender/Frontend/Service.php b/src/Sender/Frontend/Service.php index 1fba7abc..39c1afb8 100644 --- a/src/Sender/Frontend/Service.php +++ b/src/Sender/Frontend/Service.php @@ -22,7 +22,7 @@ final class Service { public function __construct( private readonly Logger $logger, - private readonly EventsStorage $eventsStorage, + private readonly EventStorage $eventsStorage, ) { } diff --git a/src/Sender/FrontendSender.php b/src/Sender/FrontendSender.php index 079774c5..ff41bb25 100644 --- a/src/Sender/FrontendSender.php +++ b/src/Sender/FrontendSender.php @@ -17,9 +17,9 @@ final class FrontendSender implements \Buggregator\Trap\Sender, Processable public static function create( Logger $logger, ?Frontend\ConnectionPool $connectionPool = null, - ?Frontend\EventsStorage $eventStorage = null, + ?Frontend\EventStorage $eventStorage = null, ): self { - $eventStorage ??= new Frontend\EventsStorage(); + $eventStorage ??= new Frontend\EventStorage(); $connectionPool ??= new Frontend\ConnectionPool($logger, new Frontend\RPC($logger, $eventStorage)); return new self( $connectionPool, @@ -30,7 +30,7 @@ public static function create( private function __construct( private readonly ConnectionPool $connectionPool, - private readonly Frontend\EventsStorage $framesStorage, + private readonly Frontend\EventStorage $framesStorage, private readonly FrameHandler $handler, ) { } @@ -52,9 +52,9 @@ public function getConnectionPool(): ConnectionPool } /** - * @return Frontend\EventsStorage + * @return Frontend\EventStorage */ - public function getEventStorage(): Frontend\EventsStorage + public function getEventStorage(): Frontend\EventStorage { return $this->framesStorage; } diff --git a/tests/Unit/Sender/Frontend/EventsStorageTest.php b/tests/Unit/Sender/Frontend/EventsStorageTest.php index eddd77c4..c2addb34 100644 --- a/tests/Unit/Sender/Frontend/EventsStorageTest.php +++ b/tests/Unit/Sender/Frontend/EventsStorageTest.php @@ -4,9 +4,9 @@ namespace Buggregator\Trap\Tests\Unit\Sender\Frontend; -use Buggregator\Trap\Config\Frontend\Buffer as Config; +use Buggregator\Trap\Config\Frontend\EventStorage as Config; use Buggregator\Trap\Sender\Frontend\Event; -use Buggregator\Trap\Sender\Frontend\EventsStorage; +use Buggregator\Trap\Sender\Frontend\EventStorage; use Buggregator\Trap\Support\Uuid; use PHPUnit\Framework\TestCase; @@ -15,7 +15,7 @@ class EventsStorageTest extends TestCase public function testMaxEventsLimit(): void { $config = new Config(2); - $storage = new EventsStorage($config); + $storage = new EventStorage($config); $storage->add($e1 = $this->createEvent()); $storage->add($e2 = $this->createEvent()); @@ -31,7 +31,7 @@ public function testMaxEventsLimit(): void public function testMaxEventsLimitWithSort(): void { $config = new Config(2); - $storage = new EventsStorage($config); + $storage = new EventStorage($config); $storage->add($e1 = $this->createEvent()); $storage->add($e2 = $this->createEvent()); diff --git a/trap.xml b/trap.xml index e7eea718..818bf5de 100644 --- a/trap.xml +++ b/trap.xml @@ -1,6 +1,6 @@ - + From ed02006bb2bc15a0c7d0f3417c5ebab2d97c98fe Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 14:49:00 +0400 Subject: [PATCH 15/27] Add Container --- composer.json | 4 +- src/Application.php | 7 +- src/Command/Run.php | 17 +-- src/Config/{ => Server}/Frontend.php | 2 +- .../{ => Server}/Frontend/EventStorage.php | 2 +- src/Config/{ => Server}/SocketServer.php | 2 +- src/Info.php | 2 +- src/Sender/Frontend/EventStorage.php | 2 +- src/Service/Container.php | 115 ++++++++++++++++++ .../Sender/Frontend/EventsStorageTest.php | 2 +- 10 files changed, 139 insertions(+), 16 deletions(-) rename src/Config/{ => Server}/Frontend.php (83%) rename src/Config/{ => Server}/Frontend/EventStorage.php (88%) rename src/Config/{ => Server}/SocketServer.php (89%) create mode 100644 src/Service/Container.php diff --git a/composer.json b/composer.json index 7c7ab370..a01be715 100644 --- a/composer.json +++ b/composer.json @@ -59,9 +59,11 @@ "nunomaduro/termwind": "^1.15 || ^2", "nyholm/psr7": "^1.8", "php-http/message": "^1.15", + "psr/container": "^1.1 || ^2.0", "psr/http-message": "^1.1 || ^2", "symfony/console": "^6.4 || ^7", - "symfony/var-dumper": "^6.3 || ^7" + "symfony/var-dumper": "^6.3 || ^7", + "yiisoft/injector": "^1.2" }, "require-dev": { "dereuromark/composer-prefer-lowest": "^0.1.10", diff --git a/src/Application.php b/src/Application.php index d59a9a5a..7c2ccba0 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,10 +4,11 @@ namespace Buggregator\Trap; -use Buggregator\Trap\Config\SocketServer; +use Buggregator\Trap\Config\Server\SocketServer; use Buggregator\Trap\Handler\Http\Handler\Websocket; use Buggregator\Trap\Handler\Http\Middleware; use Buggregator\Trap\Proto\Buffer; +use Buggregator\Trap\Service\Container; use Buggregator\Trap\Socket\Client; use Buggregator\Trap\Socket\Server; use Buggregator\Trap\Socket\SocketStream; @@ -31,17 +32,19 @@ final class Application implements Processable, Cancellable, Destroyable private readonly Buffer $buffer; private bool $cancelled = false; + private readonly Logger $logger; /** * @param SocketServer[] $map * @param Sender[] $senders */ public function __construct( + private readonly Container $container, array $map = [], - private readonly Logger $logger = new Logger(), private array $senders = [], bool $withFrontend = true, ) { + $this->logger = $this->container->get(Logger::class); $this->buffer = new Buffer(bufferSize: 10485760, timer: 0.1); $inspector = new Inspector( diff --git a/src/Command/Run.php b/src/Command/Run.php index e10fe6eb..b2138f16 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -5,10 +5,11 @@ namespace Buggregator\Trap\Command; use Buggregator\Trap\Application; -use Buggregator\Trap\Config\SocketServer; +use Buggregator\Trap\Config\Server\SocketServer; use Buggregator\Trap\Info; use Buggregator\Trap\Logger; use Buggregator\Trap\Sender; +use Buggregator\Trap\Service\Container; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\SignalableCommandInterface; @@ -82,13 +83,15 @@ protected function execute( $registry = $this->createRegistry($output); - $this->app = new Application( - $servers, - new Logger($output), - senders: $registry->getSenders($senders), - withFrontend: $input->getOption('ui') !== false, - ); + $container = new Container(); + $container->set(new Logger($output)); + $container->set($container->make(Application::class, [ + 'map' => $servers, + 'senders' => $registry->getSenders($senders), + 'withFrontend' => $input->getOption('ui') !== false, + ])); + $this->app = $container->get(Application::class); $this->app->run(); } catch (\Throwable $e) { if ($output->isVerbose()) { diff --git a/src/Config/Frontend.php b/src/Config/Server/Frontend.php similarity index 83% rename from src/Config/Frontend.php rename to src/Config/Server/Frontend.php index b8df12fd..0d18199f 100644 --- a/src/Config/Frontend.php +++ b/src/Config/Server/Frontend.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Buggregator\Trap\Config; +namespace Buggregator\Trap\Config\Server; use Buggregator\Trap\Service\Config\XPath; diff --git a/src/Config/Frontend/EventStorage.php b/src/Config/Server/Frontend/EventStorage.php similarity index 88% rename from src/Config/Frontend/EventStorage.php rename to src/Config/Server/Frontend/EventStorage.php index af9190e5..a902ae14 100644 --- a/src/Config/Frontend/EventStorage.php +++ b/src/Config/Server/Frontend/EventStorage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Buggregator\Trap\Config\Frontend; +namespace Buggregator\Trap\Config\Server\Frontend; use Buggregator\Trap\Service\Config\XPath; diff --git a/src/Config/SocketServer.php b/src/Config/Server/SocketServer.php similarity index 89% rename from src/Config/SocketServer.php rename to src/Config/Server/SocketServer.php index 9609f693..18aa1df2 100644 --- a/src/Config/SocketServer.php +++ b/src/Config/Server/SocketServer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Buggregator\Trap\Config; +namespace Buggregator\Trap\Config\Server; /** * @internal diff --git a/src/Info.php b/src/Info.php index b5f9104b..e6d3cc60 100644 --- a/src/Info.php +++ b/src/Info.php @@ -10,7 +10,7 @@ class Info { public const NAME = 'Buggregator Trap'; - public const VERSION = '1.4.6'; + public const VERSION = '1.6.0'; public const LOGO_CLI_COLOR = << */ + private array $cache = []; + + /** @var array */ + private array $factory = []; + + private readonly Injector $injector; + + /** + * @psalm-suppress PropertyTypeCoercion + */ + public function __construct() + { + $this->injector = (new Injector($this))->withCacheReflections(false); + $this->cache[Injector::class] = $this->injector; + $this->cache[self::class] = $this; + $this->cache[ContainerInterface::class] = $this; + } + + /** + * @template T of object + * @param class-string $id + * @return T + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + public function get(string $id): object + { + /** @psalm-suppress InvalidReturnStatement */ + return $this->cache[$id] ??= $this->make($id); + } + + /** + * @param class-string $id + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + public function has(string $id): bool + { + return \array_key_exists($id, $this->cache) || \array_key_exists($id, $this->factory); + } + + /** + * @template T of object + * @param T $service + * @param class-string|null $id + */ + public function set(object $service, ?string $id = null): void + { + \assert($id === null || $service instanceof $id, "Service must be instance of {$id}."); + $this->cache[$id ?? \get_class($service)] = $service; + } + + /** + * Create an object of the specified class without caching. + * + * @template T + * @param class-string $class + * @return T + */ + public function make(string $class, array $arguments = []): object + { + $result = \array_key_exists($class, $this->factory) + ? ($this->factory[$class])($this) + : $this->injector->make($class, $arguments); + + \assert($result instanceof $class, "Created object must be instance of {$class}."); + + // Detect Trap related types + + // Configs + if (\str_starts_with($class, 'Buggregator\\Trap\\Config\\')) { + // Hydrate config + $configLoader = $this->get(ConfigLoader::class); + $configLoader->hidrate($result); + } + + return $result; + } + + /** + * Declare a factory for the specified class. + * + * @template T of object + * @param class-string $id + * @param (callable(Container): T) $callback + */ + public function bind(string $id, callable $callback): void + { + $this->factory[$id] = $callback; + } + + public function destroy(): void + { + unset($this->cache, $this->factory, $this->injector); + } +} diff --git a/tests/Unit/Sender/Frontend/EventsStorageTest.php b/tests/Unit/Sender/Frontend/EventsStorageTest.php index c2addb34..815d2d67 100644 --- a/tests/Unit/Sender/Frontend/EventsStorageTest.php +++ b/tests/Unit/Sender/Frontend/EventsStorageTest.php @@ -4,7 +4,7 @@ namespace Buggregator\Trap\Tests\Unit\Sender\Frontend; -use Buggregator\Trap\Config\Frontend\EventStorage as Config; +use Buggregator\Trap\Config\Server\Frontend\EventStorage as Config; use Buggregator\Trap\Sender\Frontend\Event; use Buggregator\Trap\Sender\Frontend\EventStorage; use Buggregator\Trap\Support\Uuid; From f37be4ba54fb99f29bf0a6294c659587519e47e5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 17:27:53 +0400 Subject: [PATCH 16/27] Support autoinjectable ENVs and CLI params in configs --- src/Application.php | 20 +++--- src/Command/Run.php | 8 ++- src/Config/Server/Frontend.php | 6 +- src/Proto/Buffer.php | 3 + src/Service/Config/CliOption.php | 17 +++++ src/Service/Config/ConfigLoader.php | 66 ++++++++++++------- src/Service/Config/Env.php | 17 +++++ src/Service/Config/XPath.php | 1 + src/Service/Container.php | 25 ++++--- .../Sender/Frontend/EventsStorageTest.php | 6 +- 10 files changed, 117 insertions(+), 52 deletions(-) create mode 100644 src/Service/Config/CliOption.php create mode 100644 src/Service/Config/Env.php diff --git a/src/Application.php b/src/Application.php index 7c2ccba0..7b0b29fc 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,6 +4,7 @@ namespace Buggregator\Trap; +use Buggregator\Trap\Config\Server\Frontend as FrontendConfig; use Buggregator\Trap\Config\Server\SocketServer; use Buggregator\Trap\Handler\Http\Handler\Websocket; use Buggregator\Trap\Handler\Http\Middleware; @@ -46,10 +47,9 @@ public function __construct( ) { $this->logger = $this->container->get(Logger::class); $this->buffer = new Buffer(bufferSize: 10485760, timer: 0.1); + $this->container->set($this->buffer); - $inspector = new Inspector( - $this->buffer, - $this->logger, + $inspector = $container->make(Inspector::class, [ // new Traffic\Dispatcher\WebSocket(), new Traffic\Dispatcher\VarDumper(), new Traffic\Dispatcher\Http( @@ -63,7 +63,7 @@ public function __construct( ), new Traffic\Dispatcher\Smtp(), new Traffic\Dispatcher\Monolog(), - ); + ]); $this->processors[] = $inspector; $withFrontend and $this->configureFrontend(8000); @@ -202,9 +202,7 @@ public function configureFrontend(int $port): void { $this->senders[] = $wsSender = Sender\FrontendSender::create($this->logger); - $inspector = new Inspector( - $this->buffer, - $this->logger, + $inspector = $this->container->make(Inspector::class, [ new Traffic\Dispatcher\Http( [ new Sender\Frontend\Http\StaticFiles(), @@ -213,11 +211,11 @@ public function configureFrontend(int $port): void ], [new Sender\Frontend\Http\RequestHandler($wsSender->getConnectionPool())], silentMode: true, - ), - ); + ) + ]); $this->processors[] = $inspector; - $this->processors[] = $wsSender; - $this->prepareServerFiber(new SocketServer(port: $port), $inspector, $this->logger); + $config = $this->container->get(FrontendConfig::class); + $this->prepareServerFiber(new SocketServer(port: $config->port), $inspector, $this->logger); } } diff --git a/src/Command/Run.php b/src/Command/Run.php index b2138f16..a86c5f29 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -84,14 +84,16 @@ protected function execute( $registry = $this->createRegistry($output); $container = new Container(); + $container->set($registry); + $container->set($input, InputInterface::class); $container->set(new Logger($output)); - $container->set($container->make(Application::class, [ + $this->app = $container->get(Application::class, [ 'map' => $servers, 'senders' => $registry->getSenders($senders), 'withFrontend' => $input->getOption('ui') !== false, - ])); + ]); + - $this->app = $container->get(Application::class); $this->app->run(); } catch (\Throwable $e) { if ($output->isVerbose()) { diff --git a/src/Config/Server/Frontend.php b/src/Config/Server/Frontend.php index 0d18199f..77e0aa8d 100644 --- a/src/Config/Server/Frontend.php +++ b/src/Config/Server/Frontend.php @@ -4,6 +4,8 @@ namespace Buggregator\Trap\Config\Server; +use Buggregator\Trap\Service\Config\CliOption; +use Buggregator\Trap\Service\Config\Env; use Buggregator\Trap\Service\Config\XPath; /** @@ -12,6 +14,8 @@ final class Frontend { /** @var int<1, max> */ - #[XPath('/trap/frontend@port')] + #[Env('TRAP_FRONTEND_PORT')] + #[CliOption('ui')] + #[XPath('/trap/frontend/@port')] public int $port = 8000; } diff --git a/src/Proto/Buffer.php b/src/Proto/Buffer.php index 66998202..8610121f 100644 --- a/src/Proto/Buffer.php +++ b/src/Proto/Buffer.php @@ -7,6 +7,9 @@ use Buggregator\Trap\Support\Timer; /** + * A buffer that accumulates events and notifies when it is full. + * Overflow can occur due to a configured timeout or number of events. + * * @internal */ final class Buffer diff --git a/src/Service/Config/CliOption.php b/src/Service/Config/CliOption.php new file mode 100644 index 00000000..b631062a --- /dev/null +++ b/src/Service/Config/CliOption.php @@ -0,0 +1,17 @@ +xml = \is_string($xml) ? \simplexml_load_string($xml) : null; + $this->xml = \is_string($xml) + ? (\simplexml_load_string($xml, options: \LIBXML_NOERROR) ?: null) + : null; } public function hidrate(object $config): void @@ -55,33 +62,42 @@ public function hidrate(object $config): void private function injectValue(object $config, \ReflectionProperty $property, array $attributes): void { foreach ($attributes as $attribute) { - $attribute = $attribute->newInstance(); + try { + $attribute = $attribute->newInstance(); - $value = match (true) { - $attribute instanceof XPath => $this->xml?->xpath($attribute->path), - default => null, - }; + $value = match (true) { + $attribute instanceof XPath => $this->xml?->xpath($attribute->path)[$attribute->key], + $attribute instanceof Env => \getenv($attribute->name) === false ? null : \getenv($attribute->name), + $attribute instanceof CliOption => $this->cliInput?->getOption($attribute->name), + default => null, + }; - if ($value === null) { - continue; - } + if ($value === null) { + continue; + } - // Cast value to the property type - $type = $property->getType(); - $result = match (true) { - $type === null => $value[0], - $type->allowsNull() && $value[0] === '' => null, - $type->isBuiltin() => match ($type->getName()) { - 'int' => (int) $value[0], - 'float' => (float) $value[0], - 'bool' => \filter_var($value[0], FILTER_VALIDATE_BOOLEAN), - default => $value[0], - }, - default => $value[0], - }; + // Cast value to the property type + $type = $property->getType(); + $result = match (true) { + $type === null => $value, + $type->allowsNull() && $value === '' => null, + $type->isBuiltin() => match ($type->getName()) { + 'int' => (int) $value, + 'float' => (float) $value, + 'bool' => \filter_var($value, FILTER_VALIDATE_BOOLEAN), + default => $value, + }, + default => $value, + }; - // Set the property value - $property->setValue($config, $result); + // todo Validation + + // Set the property value + $property->setValue($config, $result); + return; + } catch (\Throwable $e) { + $this->logger->exception($e, important: true); + } } } } diff --git a/src/Service/Config/Env.php b/src/Service/Config/Env.php new file mode 100644 index 00000000..5ce1d1d3 --- /dev/null +++ b/src/Service/Config/Env.php @@ -0,0 +1,17 @@ + */ private array $cache = []; - /** @var array */ + /** @var array */ private array $factory = []; private readonly Injector $injector; @@ -38,14 +38,15 @@ public function __construct() /** * @template T of object * @param class-string $id + * @param array $arguments Will be used if the object is created for the first time. * @return T * * @psalm-suppress MoreSpecificImplementedParamType */ - public function get(string $id): object + public function get(string $id, array $arguments = []): object { /** @psalm-suppress InvalidReturnStatement */ - return $this->cache[$id] ??= $this->make($id); + return $this->cache[$id] ??= $this->make($id, $arguments); } /** @@ -78,9 +79,13 @@ public function set(object $service, ?string $id = null): void */ public function make(string $class, array $arguments = []): object { - $result = \array_key_exists($class, $this->factory) - ? ($this->factory[$class])($this) - : $this->injector->make($class, $arguments); + $binding = $this->factory[$class] ?? null; + + $result = match(true) { + $binding === null => $this->injector->make($class, $arguments), + \is_array($binding) => $this->injector->make($class, \array_merge($binding, $arguments)), + default => ($this->factory[$class])($this), + }; \assert($result instanceof $class, "Created object must be instance of {$class}."); @@ -97,15 +102,15 @@ public function make(string $class, array $arguments = []): object } /** - * Declare a factory for the specified class. + * Declare a factory or predefined arguments for the specified class. * * @template T of object * @param class-string $id - * @param (callable(Container): T) $callback + * @param array|\Closure(Container): T $binding */ - public function bind(string $id, callable $callback): void + public function bind(string $id, \Closure|array $binding): void { - $this->factory[$id] = $callback; + $this->factory[$id] = $binding; } public function destroy(): void diff --git a/tests/Unit/Sender/Frontend/EventsStorageTest.php b/tests/Unit/Sender/Frontend/EventsStorageTest.php index 815d2d67..5858c3cf 100644 --- a/tests/Unit/Sender/Frontend/EventsStorageTest.php +++ b/tests/Unit/Sender/Frontend/EventsStorageTest.php @@ -14,7 +14,8 @@ class EventsStorageTest extends TestCase { public function testMaxEventsLimit(): void { - $config = new Config(2); + $config = new Config(); + $config->maxEvents = 2; $storage = new EventStorage($config); $storage->add($e1 = $this->createEvent()); @@ -30,7 +31,8 @@ public function testMaxEventsLimit(): void public function testMaxEventsLimitWithSort(): void { - $config = new Config(2); + $config = new Config(); + $config->maxEvents = 2; $storage = new EventStorage($config); $storage->add($e1 = $this->createEvent()); From e2c9998a082b8ba0b0e36c60c3bb63073f0a6718 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 17:39:38 +0400 Subject: [PATCH 17/27] Fix tests --- src/Config/Server/Frontend.php | 2 +- src/Service/Config/ConfigLoader.php | 8 ++++++-- src/Service/Container.php | 2 +- tests/Unit/Service/Config/ConfigLoaderTest.php | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Config/Server/Frontend.php b/src/Config/Server/Frontend.php index 77e0aa8d..76ad55bb 100644 --- a/src/Config/Server/Frontend.php +++ b/src/Config/Server/Frontend.php @@ -13,7 +13,7 @@ */ final class Frontend { - /** @var int<1, max> */ + /** @var int<1, 65535> */ #[Env('TRAP_FRONTEND_PORT')] #[CliOption('ui')] #[XPath('/trap/frontend/@port')] diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index 2a8bb03d..5b6b1196 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -16,6 +16,7 @@ final class ConfigLoader /** * @param null|callable(): non-empty-string $xmlProvider + * @psalm-suppress RiskyTruthyFalsyComparison */ public function __construct( private Logger $logger, @@ -57,7 +58,7 @@ public function hidrate(object $config): void /** * @param \ReflectionProperty $property - * @param array<\ReflectionAttribute> $attributes + * @param list<\ReflectionAttribute> $attributes */ private function injectValue(object $config, \ReflectionProperty $property, array $attributes): void { @@ -65,6 +66,7 @@ private function injectValue(object $config, \ReflectionProperty $property, arra try { $attribute = $attribute->newInstance(); + /** @var mixed $value */ $value = match (true) { $attribute instanceof XPath => $this->xml?->xpath($attribute->path)[$attribute->key], $attribute instanceof Env => \getenv($attribute->name) === false ? null : \getenv($attribute->name), @@ -78,8 +80,10 @@ private function injectValue(object $config, \ReflectionProperty $property, arra // Cast value to the property type $type = $property->getType(); + + /** @var mixed $result */ $result = match (true) { - $type === null => $value, + !$type instanceof \ReflectionNamedType => $value, $type->allowsNull() && $value === '' => null, $type->isBuiltin() => match ($type->getName()) { 'int' => (int) $value, diff --git a/src/Service/Container.php b/src/Service/Container.php index cbb1370c..90508388 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -41,7 +41,7 @@ public function __construct() * @param array $arguments Will be used if the object is created for the first time. * @return T * - * @psalm-suppress MoreSpecificImplementedParamType + * @psalm-suppress MoreSpecificImplementedParamType, InvalidReturnType */ public function get(string $id, array $arguments = []): object { diff --git a/tests/Unit/Service/Config/ConfigLoaderTest.php b/tests/Unit/Service/Config/ConfigLoaderTest.php index 97f025fa..b33ed0e4 100644 --- a/tests/Unit/Service/Config/ConfigLoaderTest.php +++ b/tests/Unit/Service/Config/ConfigLoaderTest.php @@ -4,6 +4,7 @@ namespace Buggregator\Trap\Tests\Unit\Service\Config; +use Buggregator\Trap\Logger; use Buggregator\Trap\Service\Config\ConfigLoader; use Buggregator\Trap\Service\Config\XPath; use PHPUnit\Framework\TestCase; @@ -33,7 +34,7 @@ public function testSimpleHydration(): void XML; - $loader = new ConfigLoader(fn() => $xml); + $loader = new ConfigLoader(new Logger(), null, fn() => $xml); $loader->hidrate($dto); self::assertTrue($dto->myBool); From 1fc81b179902fec63a5b1e77c31d0b29bf0c4dc1 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 23:13:55 +0400 Subject: [PATCH 18/27] Add bootstrap and tests --- src/Bootstrap.php | 75 +++++++++++++++++++ src/Command/Run.php | 9 ++- src/Config/Server/Frontend.php | 4 +- src/Service/Config/ConfigLoader.php | 40 ++++------ src/Service/Config/InputArgument.php | 17 +++++ .../Config/{CliOption.php => InputOption.php} | 2 +- src/Service/Container.php | 16 ++-- .../Unit/Service/Config/ConfigLoaderTest.php | 58 +++++++++++++- 8 files changed, 182 insertions(+), 39 deletions(-) create mode 100644 src/Bootstrap.php create mode 100644 src/Service/Config/InputArgument.php rename src/Service/Config/{CliOption.php => InputOption.php} (81%) diff --git a/src/Bootstrap.php b/src/Bootstrap.php new file mode 100644 index 00000000..079bc7d8 --- /dev/null +++ b/src/Bootstrap.php @@ -0,0 +1,75 @@ +container; + unset($this->container); + + return $c; + } + + /** + * @param non-empty-string|null $xml File or XML content + */ + public function withConfig( + ?string $xml = null, + array $inputOptions = [], + array $inputArguments = [], + array $environment = [], + ): self { + $args = [ + 'env' => $environment, + 'inputArguments' => $inputArguments, + 'inputOptions' => $inputOptions, + ]; + + // XML config file + $xml === null or $args['xml'] = $this->readXml($xml); + + // Register bindings + $this->container->bind(ConfigLoader::class, $args); + + return $this; + } + + private function readXml(string $fileOrContent): string + { + // Load content + if (\str_starts_with($fileOrContent, 'createRegistry($output); - $container = new Container(); + $container = Bootstrap::init()->withConfig( + xml: \dirname(__DIR__, 2) . '/trap.xml', + inputOptions: $input->getOptions(), + inputArguments: $input->getArguments(), + environment: \getenv(), + )->finish(); $container->set($registry); $container->set($input, InputInterface::class); $container->set(new Logger($output)); diff --git a/src/Config/Server/Frontend.php b/src/Config/Server/Frontend.php index 76ad55bb..8ad3bb60 100644 --- a/src/Config/Server/Frontend.php +++ b/src/Config/Server/Frontend.php @@ -4,7 +4,7 @@ namespace Buggregator\Trap\Config\Server; -use Buggregator\Trap\Service\Config\CliOption; +use Buggregator\Trap\Service\Config\InputOption; use Buggregator\Trap\Service\Config\Env; use Buggregator\Trap\Service\Config\XPath; @@ -15,7 +15,7 @@ final class Frontend { /** @var int<1, 65535> */ #[Env('TRAP_FRONTEND_PORT')] - #[CliOption('ui')] + #[InputOption('ui')] #[XPath('/trap/frontend/@port')] public int $port = 8000; } diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index 5b6b1196..625c6bc5 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -5,7 +5,6 @@ namespace Buggregator\Trap\Service\Config; use Buggregator\Trap\Logger; -use Symfony\Component\Console\Input\InputInterface; /** * @internal @@ -15,31 +14,23 @@ final class ConfigLoader private \SimpleXMLElement|null $xml = null; /** - * @param null|callable(): non-empty-string $xmlProvider * @psalm-suppress RiskyTruthyFalsyComparison */ public function __construct( - private Logger $logger, - private ?InputInterface $cliInput, - ?callable $xmlProvider = null, - ) - { - // Check SimpleXML extension - if (!\extension_loaded('simplexml')) { - return; - } - - try { - $xml = $xmlProvider === null - ? \file_get_contents(\dirname(__DIR__, 3) . '/trap.xml') - : $xmlProvider(); - } catch (\Throwable) { - return; + private readonly Logger $logger, + private readonly array $env = [], + private readonly array $inputArguments = [], + private readonly array $inputOptions = [], + ?string $xml = null, + ) { + if (\is_string($xml)) { + // Check SimpleXML extension + if (!\extension_loaded('simplexml')) { + $logger->info('SimpleXML extension is not loaded.'); + } else { + $this->xml = \simplexml_load_string($xml, options: \LIBXML_NOERROR) ?: null; + } } - - $this->xml = \is_string($xml) - ? (\simplexml_load_string($xml, options: \LIBXML_NOERROR) ?: null) - : null; } public function hidrate(object $config): void @@ -69,8 +60,9 @@ private function injectValue(object $config, \ReflectionProperty $property, arra /** @var mixed $value */ $value = match (true) { $attribute instanceof XPath => $this->xml?->xpath($attribute->path)[$attribute->key], - $attribute instanceof Env => \getenv($attribute->name) === false ? null : \getenv($attribute->name), - $attribute instanceof CliOption => $this->cliInput?->getOption($attribute->name), + $attribute instanceof Env => $this->env[$attribute->name] ?? null, + $attribute instanceof InputOption => $this->inputOptions[$attribute->name] ?? null, + $attribute instanceof InputArgument => $this->inputArguments[$attribute->name] ?? null, default => null, }; diff --git a/src/Service/Config/InputArgument.php b/src/Service/Config/InputArgument.php new file mode 100644 index 00000000..ff863541 --- /dev/null +++ b/src/Service/Config/InputArgument.php @@ -0,0 +1,17 @@ +factory[$class] ?? null; - $result = match(true) { - $binding === null => $this->injector->make($class, $arguments), - \is_array($binding) => $this->injector->make($class, \array_merge($binding, $arguments)), - default => ($this->factory[$class])($this), - }; + if ($binding instanceof \Closure) { + $result = $binding($this); + } else { + try { + $result = $this->injector->make($class, \array_merge((array) $binding, $arguments)); + } catch (\Throwable $e) { + throw new class(previous: $e) extends \RuntimeException implements NotFoundExceptionInterface {}; + } + } \assert($result instanceof $class, "Created object must be instance of {$class}."); // Detect Trap related types - // Configs if (\str_starts_with($class, 'Buggregator\\Trap\\Config\\')) { // Hydrate config diff --git a/tests/Unit/Service/Config/ConfigLoaderTest.php b/tests/Unit/Service/Config/ConfigLoaderTest.php index b33ed0e4..781ceb07 100644 --- a/tests/Unit/Service/Config/ConfigLoaderTest.php +++ b/tests/Unit/Service/Config/ConfigLoaderTest.php @@ -4,8 +4,11 @@ namespace Buggregator\Trap\Tests\Unit\Service\Config; -use Buggregator\Trap\Logger; +use Buggregator\Trap\Bootstrap; use Buggregator\Trap\Service\Config\ConfigLoader; +use Buggregator\Trap\Service\Config\Env; +use Buggregator\Trap\Service\Config\InputArgument; +use Buggregator\Trap\Service\Config\InputOption; use Buggregator\Trap\Service\Config\XPath; use PHPUnit\Framework\TestCase; @@ -22,8 +25,9 @@ public function testSimpleHydration(): void public string $myString; #[XPath('/trap/container/MyFloat/@value')] public float $myFloat; + #[XPath('/trap/container/Nothing/@value')] + public float $none = 3.14; }; - $xml = <<<'XML' @@ -34,12 +38,58 @@ public function testSimpleHydration(): void XML; - $loader = new ConfigLoader(new Logger(), null, fn() => $xml); - $loader->hidrate($dto); + $this->createConfigLoader(xml: $xml)->hidrate($dto); self::assertTrue($dto->myBool); self::assertSame(200, $dto->myInt); self::assertSame('foo-bar', $dto->myString); self::assertSame(42.0, $dto->myFloat); + self::assertSame(3.14, $dto->none); + } + + public function testAttributesOrder(): void + { + $dto = new class() { + #[XPath('/test/@foo')] + #[InputArgument('test')] + #[InputOption('test')] + #[Env('test')] + public int $int1; + #[Env('test')] + #[InputArgument('test')] + #[XPath('/test/@foo')] + #[InputOption('test')] + public int $int2; + #[InputArgument('test')] + #[Env('test')] + #[XPath('/test/@foo')] + #[InputOption('test')] + public int $int3; + }; + $xml = <<<'XML' + + + + XML; + + $this + ->createConfigLoader(xml: $xml, opts: ['test' => 13], args: ['test' => 69], env: ['test' => 0]) + ->hidrate($dto); + + self::assertSame(42, $dto->int1); + self::assertSame(0, $dto->int2); + self::assertSame(69, $dto->int3); + } + + private function createConfigLoader( + ?string $xml = null, + array $opts = [], + array $args = [], + array $env = [], + ): ConfigLoader { + return Bootstrap::init() + ->withConfig($xml, $opts, $args, $env) + ->finish() + ->get(ConfigLoader::class); } } From 2060195c9261926e454882825b9a97b36feb5fef Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 1 May 2024 23:58:59 +0400 Subject: [PATCH 19/27] Support arrays in config loader; add env `TRAP_TCP_PORTS` and `TRAP_TCP_HOST` --- src/Command/Run.php | 49 ++++++++++++++++------------- src/Config/Server/TcpPorts.php | 36 +++++++++++++++++++++ src/Service/Config/ConfigLoader.php | 7 ++++- 3 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 src/Config/Server/TcpPorts.php diff --git a/src/Command/Run.php b/src/Command/Run.php index 1ca6547a..26f36128 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -7,9 +7,11 @@ use Buggregator\Trap\Application; use Buggregator\Trap\Bootstrap; use Buggregator\Trap\Config\Server\SocketServer; +use Buggregator\Trap\Config\Server\TcpPorts; use Buggregator\Trap\Info; use Buggregator\Trap\Logger; use Buggregator\Trap\Sender; +use Buggregator\Trap\Service\Container; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\SignalableCommandInterface; @@ -38,18 +40,40 @@ public function configure(): void 'p', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Port to listen', - [9912], ); $this->addOption( 'sender', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Senders', - ['console'] ); $this->addOption('ui', null, InputOption::VALUE_OPTIONAL, 'Enable WEB UI (experimental)', false); } + /** + * Prepare port listeners + * @return SocketServer[] + */ + public function getServers(Container $container): array + { + $config = $container->get(TcpPorts::class); + + $servers = []; + $ports = $config->ports ?: [9912]; + /** @var scalar $port */ + foreach ($ports as $port) { + \is_numeric($port) or throw new \InvalidArgumentException( + \sprintf('Invalid port `%s`. It must be a number.', (string)$port), + ); + $port = (int)$port; + $port > 0 && $port < 65536 or throw new \InvalidArgumentException( + \sprintf('Invalid port `%s`. It must be in range 1-65535.', $port), + ); + $servers[] = new SocketServer($port, $config->host, $config->type); + } + return $servers; + } + protected function execute( InputInterface $input, OutputInterface $output, @@ -59,25 +83,6 @@ protected function execute( $output->writeln(\sprintf('%s v%s', Info::NAME, Info::VERSION)); $output->write(Info::LOGO_CLI_COLOR . "\n", true, OutputInterface::OUTPUT_RAW); - /** - * Prepare port listeners - * @var SocketServer[] $servers - */ - $servers = []; - $ports = $input->getOption('port') ?: [9912]; - \assert(\is_array($ports)); - foreach ($ports as $port) { - \assert(\is_scalar($port)); - \is_numeric($port) or throw new \InvalidArgumentException( - \sprintf('Invalid port `%s`. It must be a number.', (string)$port), - ); - $port = (int)$port; - $port > 0 && $port < 65536 or throw new \InvalidArgumentException( - \sprintf('Invalid port `%s`. It must be in range 1-65535.', $port), - ); - $servers[] = new SocketServer($port); - } - /** @var non-empty-string[] $senders */ $senders = (array)$input->getOption('sender'); @@ -93,7 +98,7 @@ protected function execute( $container->set($input, InputInterface::class); $container->set(new Logger($output)); $this->app = $container->get(Application::class, [ - 'map' => $servers, + 'map' => $this->getServers($container), 'senders' => $registry->getSenders($senders), 'withFrontend' => $input->getOption('ui') !== false, ]); diff --git a/src/Config/Server/TcpPorts.php b/src/Config/Server/TcpPorts.php new file mode 100644 index 00000000..7a537c69 --- /dev/null +++ b/src/Config/Server/TcpPorts.php @@ -0,0 +1,36 @@ +> + */ + #[Env('TRAP_TCP_PORTS')] + #[InputOption('port')] + public array $ports = [9912, 9913]; + + /** + * Host to listen + * + * @var non-empty-string + */ + #[Env('TRAP_TCP_HOST')] + public string $host = '127.0.0.1'; + + public string $type = 'tcp'; +} diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index 625c6bc5..8bb1c736 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -66,7 +66,7 @@ private function injectValue(object $config, \ReflectionProperty $property, arra default => null, }; - if ($value === null) { + if (\in_array($value, [null, []], true)) { continue; } @@ -81,6 +81,11 @@ private function injectValue(object $config, \ReflectionProperty $property, arra 'int' => (int) $value, 'float' => (float) $value, 'bool' => \filter_var($value, FILTER_VALIDATE_BOOLEAN), + 'array' => match (true) { + \is_array($value) => $value, + \is_string($value) => explode(',', $value), + default => [$value], + }, default => $value, }, default => $value, From 9165cf3643ffff5cd4643957ef799e4ea4b7c18c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 2 May 2024 00:22:16 +0400 Subject: [PATCH 20/27] Fix warning when value not found by XPath --- src/Service/Config/ConfigLoader.php | 2 +- .../Unit/Service/Config/ConfigLoaderTest.php | 28 +++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index 8bb1c736..d19992c1 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -59,7 +59,7 @@ private function injectValue(object $config, \ReflectionProperty $property, arra /** @var mixed $value */ $value = match (true) { - $attribute instanceof XPath => $this->xml?->xpath($attribute->path)[$attribute->key], + $attribute instanceof XPath => @$this->xml?->xpath($attribute->path)[$attribute->key], $attribute instanceof Env => $this->env[$attribute->name] ?? null, $attribute instanceof InputOption => $this->inputOptions[$attribute->name] ?? null, $attribute instanceof InputArgument => $this->inputArguments[$attribute->name] ?? null, diff --git a/tests/Unit/Service/Config/ConfigLoaderTest.php b/tests/Unit/Service/Config/ConfigLoaderTest.php index 781ceb07..79a22fae 100644 --- a/tests/Unit/Service/Config/ConfigLoaderTest.php +++ b/tests/Unit/Service/Config/ConfigLoaderTest.php @@ -25,8 +25,6 @@ public function testSimpleHydration(): void public string $myString; #[XPath('/trap/container/MyFloat/@value')] public float $myFloat; - #[XPath('/trap/container/Nothing/@value')] - public float $none = 3.14; }; $xml = <<<'XML' @@ -44,7 +42,31 @@ public function testSimpleHydration(): void self::assertSame(200, $dto->myInt); self::assertSame('foo-bar', $dto->myString); self::assertSame(42.0, $dto->myFloat); - self::assertSame(3.14, $dto->none); + } + + public function testNonExistingOptions(): void + { + $dto = new class() { + #[XPath('/trap/container/Nothing/@value')] + public float $none1 = 3.14; + #[Env('f--o--o')] + public float $none2 = 3.14; + #[InputOption('f--o--o')] + public float $none3 = 3.14; + #[InputArgument('f--o--o')] + public float $none4 = 3.14; + }; + $xml = <<<'XML' + + + XML; + + $this->createConfigLoader(xml: $xml)->hidrate($dto); + + self::assertSame(3.14, $dto->none1); + self::assertSame(3.14, $dto->none2); + self::assertSame(3.14, $dto->none3); + self::assertSame(3.14, $dto->none4); } public function testAttributesOrder(): void From 876a11cd99c72159430a0bb8d8d8b302bbcdf318 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 2 May 2024 00:39:33 +0400 Subject: [PATCH 21/27] Update readme --- README.md | 8 +++++++- src/Config/Server/Frontend.php | 2 +- src/Config/Server/TcpPorts.php | 2 +- src/Service/Config/Env.php | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f006d738..019bad02 100644 --- a/README.md +++ b/README.md @@ -154,9 +154,15 @@ Sometimes, it's convenient to run Trap on the same ports that [Buggregator](http uses by default. Well, that's also possible: ```bash -vendor/bin/trap -p1025 -p9912 -p9913 -p8000 +vendor/bin/trap -p1025 -p9912 -p9913 -p8000 --ui=8080 ``` +Environment variables can also be used to set endpoints: +- `TRAP_TCP_PORTS` - for TCP traffic: `9912,9913,1025,8000` +- `TRAP_TCP_HOST` - for the TCP host (default: `127.0.0.1`) +- `TRAP_UI_PORT` - for the web interface: `8080` + + ### Choosing Your Senders Buggregator Trap provides a variety of "senders" that dictate where the dumps will be sent. Currently, the available diff --git a/src/Config/Server/Frontend.php b/src/Config/Server/Frontend.php index 8ad3bb60..115073c1 100644 --- a/src/Config/Server/Frontend.php +++ b/src/Config/Server/Frontend.php @@ -14,7 +14,7 @@ final class Frontend { /** @var int<1, 65535> */ - #[Env('TRAP_FRONTEND_PORT')] + #[Env('TRAP_UI_PORT')] #[InputOption('ui')] #[XPath('/trap/frontend/@port')] public int $port = 8000; diff --git a/src/Config/Server/TcpPorts.php b/src/Config/Server/TcpPorts.php index 7a537c69..513a2955 100644 --- a/src/Config/Server/TcpPorts.php +++ b/src/Config/Server/TcpPorts.php @@ -22,7 +22,7 @@ final class TcpPorts */ #[Env('TRAP_TCP_PORTS')] #[InputOption('port')] - public array $ports = [9912, 9913]; + public array $ports = [9912]; /** * Host to listen diff --git a/src/Service/Config/Env.php b/src/Service/Config/Env.php index 5ce1d1d3..a8633324 100644 --- a/src/Service/Config/Env.php +++ b/src/Service/Config/Env.php @@ -7,7 +7,7 @@ /** * @internal */ -#[\Attribute(\Attribute::TARGET_PROPERTY)] +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] final class Env implements ConfigAttribute { public function __construct( From 743d5d63342578846fd627285c9b91849ecb31c5 Mon Sep 17 00:00:00 2001 From: Dima Jolkin Date: Thu, 2 May 2024 11:17:22 +0300 Subject: [PATCH 22/27] recover psalm.xml and update psalm-baseline.xml --- psalm-baseline.xml | 188 +++++++++++++++++++++++++++++++++++++++++++++ psalm.xml | 6 -- 2 files changed, 188 insertions(+), 6 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index a5770984..e303f9ce 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -6,6 +6,10 @@ + + + + @@ -21,19 +25,56 @@ + + + + + + + + + + + + + + + getAttribute('begin_at', null)]]> + getAttribute('begin_at', null)]]> + + + + + + + + + + + + + + + + + + + + method]]> + {$this->method}(...$arguments)]]> @@ -55,11 +96,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + request->getUploadedFiles()]]> + + + + + throw new \InvalidArgumentException('Unknown Sentry frame type.'), }]]> + + + + + + + + + + + + + + + + + + + + + @@ -99,6 +194,12 @@ getQueryParams()]]> + + + + + + @@ -108,6 +209,14 @@ $output->writeln(]]> + + + + + + + + @@ -128,5 +237,84 @@ + + + + + + + + + payload]]> + + + payload['exceptions']]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + getClass()]]> + + + + + getName()]]]> + getName()]]]> + + + + getName()]]]> + + + + diff --git a/psalm.xml b/psalm.xml index 14f9f935..1e24d848 100644 --- a/psalm.xml +++ b/psalm.xml @@ -11,12 +11,6 @@ - - - - - - From 90bbcabf03d7f75c95a8e7a24325fade7a4eb137 Mon Sep 17 00:00:00 2001 From: l0gic Date: Fri, 3 May 2024 11:30:43 +0500 Subject: [PATCH 23/27] base64 encoded jokes --- resources/registry/jokes.txt | 5 +++++ src/Command/Joke.php | 7 ++++--- src/Command/Run.php | 1 - 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 resources/registry/jokes.txt diff --git a/resources/registry/jokes.txt b/resources/registry/jokes.txt new file mode 100644 index 00000000..46c4e663 --- /dev/null +++ b/resources/registry/jokes.txt @@ -0,0 +1,5 @@ +V2h5IGRvIHByb2dyYW1tZXJzIGFsd2F5cyBtaXggdXAgSGFsbG93ZWVuIGFuZCBDaHJpc3RtYXM/IEJlY2F1c2UgT2N0IDMxID09IERlYyAyNS4= +VHdvIGhhcmQgdGhpbmdzIGluIGNvbXB1dGVyIHNjaWVuY2U6IGNhY2hlIGludmFsaWRhdGlvbiwgbmFtaW5nIHRoaW5ncyBhbmQgc3RhY2sgb3ZlcmZsb3cu +RGVwcmVzc2l2ZSBwcm9ncmFtbWluZyBzdHlsZSB0aHJvdWdoIGR1bXAgYW5kIGRpZS4= +UEhQIHdhcyBkZWFkIDg0IHllYXJzIGFnbyByaWdodD8= +U3VibWl0IGEgcHVsbCByZXF1ZXN0IHRvIGhlbHAgdXMgaW1wcm92ZSB0aGUgQnVnZ3JlZ2F0b3IgVHJhcCBjb2RlYmFzZS4= diff --git a/src/Command/Joke.php b/src/Command/Joke.php index 57c9f33a..f1b3cd56 100644 --- a/src/Command/Joke.php +++ b/src/Command/Joke.php @@ -4,14 +4,13 @@ namespace Buggregator\Trap\Command; -use Buggregator\Trap\Info; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** - * Run application + * Print joke * * @internal */ @@ -25,7 +24,9 @@ protected function execute( InputInterface $input, OutputInterface $output, ): int { - trap(Info::JOKES[\array_rand(Info::JOKES)]); + $jokes = \file(filename: 'resources/registry/jokes.txt', flags: \FILE_SKIP_EMPTY_LINES); + $joke = base64_decode($jokes[\array_rand($jokes)]); + \trap($joke); return Command::SUCCESS; } diff --git a/src/Command/Run.php b/src/Command/Run.php index 0ca8d11e..f8f425c0 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -56,7 +56,6 @@ protected function execute( try { // Print intro $output->writeln(\sprintf('%s v%s:', Info::NAME, Info::VERSION)); - trap(Info::JOKES[\array_rand(Info::JOKES)]); $output->write(Info::LOGO_CLI_COLOR . "\n", true, OutputInterface::OUTPUT_RAW); /** From a211bb27333ed2b58518d1f4b3c7abf59d3baf06 Mon Sep 17 00:00:00 2001 From: l0gic Date: Fri, 3 May 2024 11:34:04 +0500 Subject: [PATCH 24/27] code cleanup --- src/Command/Joke.php | 2 +- src/Command/Run.php | 2 +- src/Info.php | 7 ------- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Command/Joke.php b/src/Command/Joke.php index f1b3cd56..ea986cf1 100644 --- a/src/Command/Joke.php +++ b/src/Command/Joke.php @@ -25,7 +25,7 @@ protected function execute( OutputInterface $output, ): int { $jokes = \file(filename: 'resources/registry/jokes.txt', flags: \FILE_SKIP_EMPTY_LINES); - $joke = base64_decode($jokes[\array_rand($jokes)]); + $joke = \base64_decode($jokes[\array_rand($jokes)]); \trap($joke); return Command::SUCCESS; diff --git a/src/Command/Run.php b/src/Command/Run.php index f8f425c0..e10fe6eb 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -55,7 +55,7 @@ protected function execute( ): int { try { // Print intro - $output->writeln(\sprintf('%s v%s:', Info::NAME, Info::VERSION)); + $output->writeln(\sprintf('%s v%s', Info::NAME, Info::VERSION)); $output->write(Info::LOGO_CLI_COLOR . "\n", true, OutputInterface::OUTPUT_RAW); /** diff --git a/src/Info.php b/src/Info.php index e4ad1e9e..b5f9104b 100644 --- a/src/Info.php +++ b/src/Info.php @@ -27,11 +27,4 @@ class Info CONSOLE; public const TRAP_ROOT = __DIR__ . '/..'; - public const JOKES = [ - 'Why do programmers always mix up Halloween and Christmas? Because Oct 31 == Dec 25.', - 'Two hard things in computer science: cache invalidation, naming things and stack overflow.', - 'Depressive programming style through dump and die.', - 'PHP was dead 84 years ago right?', - 'Submit a pull request to help us improve the Buggregator Trap codebase', - ]; } From 687a18c5bc5f7bb1faceb0094eedadec13a0400c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 4 May 2024 13:57:35 +0400 Subject: [PATCH 25/27] Add more joke phrases --- bin/trap | 2 +- resources/registry/jokes.txt | 114 +++++++++++++++++++++++++++++++++-- src/Command/Joke.php | 13 ++-- 3 files changed, 119 insertions(+), 10 deletions(-) diff --git a/bin/trap b/bin/trap index 981ba668..ce879387 100644 --- a/bin/trap +++ b/bin/trap @@ -44,8 +44,8 @@ if ('cli' !== PHP_SAPI) { $application = new Application(); $application->setCommandLoader( new FactoryCommandLoader([ - Command\Joke::getDefaultName() => static fn() => new Command\Joke(), Command\Run::getDefaultName() => static fn() => new Command\Run(), + Command\Joke::getDefaultName() => static fn() => new Command\Joke(), Command\Test::getDefaultName() => static fn() => new Command\Test(), ]), ); diff --git a/resources/registry/jokes.txt b/resources/registry/jokes.txt index 46c4e663..355251d8 100644 --- a/resources/registry/jokes.txt +++ b/resources/registry/jokes.txt @@ -1,5 +1,111 @@ -V2h5IGRvIHByb2dyYW1tZXJzIGFsd2F5cyBtaXggdXAgSGFsbG93ZWVuIGFuZCBDaHJpc3RtYXM/IEJlY2F1c2UgT2N0IDMxID09IERlYyAyNS4= -VHdvIGhhcmQgdGhpbmdzIGluIGNvbXB1dGVyIHNjaWVuY2U6IGNhY2hlIGludmFsaWRhdGlvbiwgbmFtaW5nIHRoaW5ncyBhbmQgc3RhY2sgb3ZlcmZsb3cu RGVwcmVzc2l2ZSBwcm9ncmFtbWluZyBzdHlsZSB0aHJvdWdoIGR1bXAgYW5kIGRpZS4= -UEhQIHdhcyBkZWFkIDg0IHllYXJzIGFnbyByaWdodD8= -U3VibWl0IGEgcHVsbCByZXF1ZXN0IHRvIGhlbHAgdXMgaW1wcm92ZSB0aGUgQnVnZ3JlZ2F0b3IgVHJhcCBjb2RlYmFzZS4= +JXMgZG9lc24ndCBuZWVkIGRvY3VtZW50YXRpb24uIEl0IGRvY3VtZW50cyBpdHNlbGYgb3V0IG9mIGZlYXIu +V2hlbiAlcyBhdXRvbG9hZHMgY2xhc3NlcywgdGhlIGNsYXNzZXMgY29tZSBydW5uaW5nLg== +RXZlcnkgdGltZSAlcyBpcyBtZW50aW9uZWQsIGEganVuaW9yIGRldmVsb3BlciBnZXRzIHRoZWlyIHdpbmdzLg== +V2hlbiAlcyBpcyBpbmNsdWRlZCwgaXQgaW5jbHVkZXMgaXRzZWxmIGluIHRoZSBjb252ZXJzYXRpb24u +JXMgZG9lc24ndCBoYXZlIHRvIGhhbmRsZSBzZXNzaW9ucy4gU2Vzc2lvbnMgaGFuZGxlIHRoZW1zZWx2ZXMu +V2hlbiAlcyBoYW5kbGVzIGVycm9ycywgZXJyb3JzIGFwb2xvZ2l6ZS4= +RnJhbWV3b3JrcyB3aXNoIHRoZXkgd2VyZSBhcyBmbGV4aWJsZSBhcyAlcy4= +V2hlbiAlcyByb3V0ZXMgcmVxdWVzdHMsIHRoZSByZXF1ZXN0cyB0YWtlIHRoZSBzY2VuaWMgcm91dGUu +V2hlbiAlcyBpcyB1cGRhdGVkLCBidWdzIHF1aXZlciBpbiBmZWFyLg== +VW5pdCB0ZXN0cyBkb24ndCB0ZXN0IGZyYW1ld29yay4gJXMgdGVzdHMgdW5pdCB0ZXN0cy4= +V2hlbiAlcyBoYW5kbGVzIGRhdGFiYXNlcywgZGF0YWJhc2VzIGhhbmRsZSB0aGVtc2VsdmVzIHdpdGggY2FyZS4= +V2hlbiAlcyBjYWNoZXMgZGF0YSwgZGF0YSBzdGF5cyBjYWNoZWQgb3V0IG9mIHJlc3BlY3Qu +V2hlbiAlcyBkZXBsb3lzLCBzZXJ2ZXJzIGRlcGxveSB0aGVtc2VsdmVzLg== +RXZlcnkgdGltZSAlcyBlY2hvZXMsIHRoZSB1bml2ZXJzZSBsaXN0ZW5zLg== +V2hlbiAlcyBoYW5kbGVzIHJlcXVlc3RzLCB0aGUgcmVxdWVzdHMgaGFuZGxlIHRoZW1zZWx2ZXMgd2l0aCBjYXJlLg== +V2hlbiAlcyBvcHRpbWl6ZXMgY29kZSwgdGhlIGNvZGUgYmVjb21lcyBhIGJldHRlciB2ZXJzaW9uIG9mIGl0c2VsZi4= +JXMncyBzZWN1cml0eSBpcyBzbyB0aWdodCwgaGFja2VycyBnZXQgbG9ja2VkIG91dCBvZiByZWFsaXR5Lg== +V2hlbiAlcyBoYW5kbGVzIGZvcm1zLCB0aGUgZm9ybXMgaGFuZGxlIHRoZW1zZWx2ZXMgd2l0aCBncmFjZS4= +V2hlbiAlcyB2YWxpZGF0ZXMgaW5wdXQsIGlucHV0IHZhbGlkYXRlcyBpdHMgb3duIGV4aXN0ZW5jZS4= +JXMgZG9lc24ndCBuZWVkIE1WQy4gTVZDIG5lZWRzIGl0Lg== +V2hlbiAlcyBpcyBpbnN0YWxsZWQsIHNlcnZlcnMgdGhyb3cgYSBwYXJ0eSBpbiBpdCdzIGhvbm9yLg== +V2hlbiAlcyByZXNvbHZlcyBkZXBlbmRlbmNpZXMsIGRlcGVuZGVuY2llcyByZXNvbHZlIHRvIGJlIGJldHRlci4= +V2hlbiAlcyBoYW5kbGVzIGV4Y2VwdGlvbnMsIGV4Y2VwdGlvbnMgbWFrZSBleGN1c2VzLg== +V2hlbiAlcyBzYW5pdGl6ZXMgaW5wdXQsIGlucHV0IGZlZWxzIGNsZWFuZXIu +JXMncyByb3V0aW5nIGlzIHNvIGVmZmljaWVudCwgdHJhZmZpYyBwYXR0ZXJucyBlbnZ5IGl0Lg== +V2hlbiAlcyBnZW5lcmF0ZXMgY29kZSwgY29kZSBnZW5lcmF0ZXMgYWRtaXJhdGlvbi4= +V2hlbiAlcyBtYW5hZ2VzIGNvbmZpZ3VyYXRpb25zLCBjb25maWd1cmF0aW9ucyBtYW5hZ2UgdGhlbXNlbHZlcy4= +V2hlbiAlcyBpbnRlcmFjdHMgd2l0aCBBUElzLCBBUElzIGJlZyBmb3IgbW9yZS4= +V2hlbiAlcyBzZXJpYWxpemVzIGRhdGEsIGRhdGEgZmVlbHMgc3BlY2lhbC4= +V2hlbiAlcyB1c2VzIE9STSwgZGF0YWJhc2VzIGRyZWFtIG9mIGJlaW5nIGl0J3MgT1JNLg== +V2hlbiAlcyBjb21waWxlcyBhc3NldHMsIGFzc2V0cyBiZWNvbWUgd29ya3Mgb2YgYXJ0Lg== +JXMgZG9lc24ndCBkZWJ1Zy4gSXQgZW5saWdodGVucy4= +V2hlbiAlcyBoYW5kbGVzIGNvb2tpZXMsIGNvb2tpZXMgbmV2ZXIgY3J1bWJsZS4= +V2hlbiAlcyBoYW5kbGVzIGFzc2V0cywgYXNzZXRzIGZlZWwgc2VjdXJlZC4= +V2hlbiAlcyBoYW5kbGVzIGF1dGhlbnRpY2F0aW9uLCBwYXNzd29yZHMgZ2V0IHN0cm9uZ2VyLg== +V2hlbiAlcyBtYW5hZ2VzIHNlc3Npb25zLCBzZXNzaW9ucyBiZWNvbWUgdW5mb3JnZXR0YWJsZSBleHBlcmllbmNlcy4= +V2hlbiAlcyByZW5kZXJzIHZpZXdzLCB2aWV3cyBzZWUgdGhlIHdvcmxkIGRpZmZlcmVudGx5Lg== +V2hlbiAlcyBtYW5hZ2VzIGRlcGVuZGVuY2llcywgZGVwZW5kZW5jaWVzIGJlY29tZSBpbnRlcmRlcGVuZGVudCBvbiBpdC4= +V2hlbiAlcyBnZW5lcmF0ZXMgVVJMcywgVVJMcyBmaW5kIHRoZWlyIHdheSBob21lLg== +V2hlbiAlcyBjb21waWxlcyB0ZW1wbGF0ZXMsIHRlbXBsYXRlcyBjb21waWxlIHRoZW1zZWx2ZXMgb3V0IG9mIGFkbWlyYXRpb24u +JXMgZG9lc24ndCB1c2UgT1JNLiBPUk0gYXNwaXJlcyB0byBiZSBsaWtlIGl0Lg== +V2hlbiAlcyBoYW5kbGVzIGV2ZW50cywgZXZlbnRzIHBsYW4gdG8gaGFwcGVuIGFnYWluIG5leHQgeWVhci4= +V2hlbiAlcyBjYWNoZXMgZGF0YSwgZGF0YSBnZXRzIHByZXNlcnZlZCBmb3IgcG9zdGVyaXR5Lg== +V2hlbiAlcyBoYW5kbGVzIGZpbGVzLCBmaWxlcyBvcmdhbml6ZSB0aGVtc2VsdmVzIGFscGhhYmV0aWNhbGx5IGZvciBpdC4= +V2hlbiAlcyBpbnRlcmFjdHMgd2l0aCBkYXRhYmFzZXMsIGRhdGFiYXNlcyBmZWVsIGhvbm9yZWQu +V2hlbiAlcyB2YWxpZGF0ZXMgaW5wdXRzLCBpbnB1dHMgYmVjb21lIHZhbGlkYXRlZCBtZW1iZXJzIG9mIHNvY2lldHku +V2hlbiAlcyBtYW5hZ2VzIGNvbmZpZ3VyYXRpb25zLCBjb25maWd1cmF0aW9ucyBjb25maWd1cmUgdGhlbXNlbHZlcy4= +V2hlbiAlcyBoYW5kbGVzIGV4Y2VwdGlvbnMsIGV4Y2VwdGlvbnMgcmV0aGluayB0aGVpciBsaWZlIGNob2ljZXMu +JXMncyBkb2N1bWVudGF0aW9uIGlzIHNvIHRob3JvdWdoLCBpdCdzIGNvbnNpZGVyZWQgYSBsaXRlcmFyeSBtYXN0ZXJwaWVjZS4= +V2hlbiAlcyBnZW5lcmF0ZXMgZm9ybXMsIGZvcm1zIGZpbGwgdGhlbXNlbHZlcyBvdXQu +V2hlbiAlcyBvcHRpbWl6ZXMgY29kZSwgY29kZSBiZWNvbWVzIGFuIE9seW1waWMgYXRobGV0ZS4= +V2hlbiAlcyBoYW5kbGVzIHJlcXVlc3RzLCB0aGUgcmVxdWVzdHMgbmV2ZXIgZ28gdW5mdWxmaWxsZWQu +V2hlbiAlcyBoYW5kbGVzIGNhY2hpbmcsIGNhY2hlcyBuZXZlciBmb3JnZXQgdGhlaXIgcHVycG9zZS4= +V2hlbiAlcyBwcm9jZXNzZXMgWE1MLCBYTUwgYmVjb21lcyBzZWxmLWF3YXJlLg== +V2hlbiAlcyBjb21waWxlcyBhc3NldHMsIGFzc2V0cyBiZWNvbWUgdGhlIGVudnkgb2YgdGhlIGRpZ2l0YWwgd29ybGQu +V2hlbiAlcyBpbnRlcmFjdHMgd2l0aCBkYXRhYmFzZXMsIGRhdGFiYXNlcyBiZWNvbWUgbW9yZSBlZmZpY2llbnQu +V2hlbiAlcyBtYW5hZ2VzIHNlc3Npb25zLCBzZXNzaW9ucyBmZWVsIGxpa2UgdGhleSd2ZSBiZWVuIHRvIHRoZXJhcHku +V2hlbiAlcyBoYW5kbGVzIGZvcm1zLCBmb3JtcyBmaWxsIHRoZW1zZWx2ZXMgb3V0IHdpdGggZ3JhdGl0dWRlLg== +V2hlbiAlcyBoYW5kbGVzIGNvb2tpZXMsIGNvb2tpZXMgYmVjb21lIGdvdXJtZXQgdHJlYXRzLg== +V2hlbiAlcyBoYW5kbGVzIHBlcm1pc3Npb25zLCBwZXJtaXNzaW9ucyBiZWNvbWUgcHJpdmlsZWdlcy4= +JXMgZG9lc24ndCBuZWVkIGZyYW1ld29ya3MuIEZyYW1ld29ya3MgbmVlZCBpdC4= +V2hlbiAlcyBvcHRpbWl6ZXMgaW1hZ2VzLCBpbWFnZXMgYmVjb21lIHRpbWVsZXNzIG1hc3RlcnBpZWNlcy4= +V2hlbiAlcyB2YWxpZGF0ZXMgZGF0YSwgZGF0YSBmZWVscyB2YWxpZGF0ZWQu +V2hlbiAlcyBtYW5hZ2VzIGNhY2hpbmcsIGNhY2hlcyBuZXZlciBmZWVsIGZvcmdvdHRlbi4= +V2hlbiAlcyBjb21waWxlcyBDU1MsIHN0eWxlc2hlZXRzIGJlY29tZSBmYXNoaW9uIHN0YXRlbWVudHMu +V2hlbiAlcyBoYW5kbGVzIGFzc2V0cywgYXNzZXRzIGZlZWwgbGlrZSB0aGV5J3JlIG9uIHRoZSByZWQgY2FycGV0Lg== +V2hlbiAlcyBoYW5kbGVzIGV2ZW50cywgZXZlbnRzIGFyZSByZW1lbWJlcmVkIGZvciBjZW50dXJpZXMu +V2hlbiAlcyBoYW5kbGVzIGNyb24gam9icywgdGltZSBpdHNlbGYgYmVuZHMgdG8gaXRzIHdpbGwu +V2hlbiAlcyBvcHRpbWl6ZXMgcXVlcmllcywgZGF0YWJhc2VzIGJlY29tZSB0ZWxlcGF0aGljLg== +V2hlbiAlcyBtYW5hZ2VzIGRlcGVuZGVuY2llcywgZGVwZW5kZW5jaWVzIGJlY29tZSBsaWZlbG9uZyBmcmllbmRzLg== +V2hlbiAlcyBnZW5lcmF0ZXMgZG9jdW1lbnRhdGlvbiwgZG9jdW1lbnRhdGlvbiBiZWNvbWVzIGEgYmVzdHNlbGxlci4= +JXMgZG9lc24ndCBoYXZlIGJ1Z3MuIEl0IGhhcyB1bmV4cGVjdGVkIGZlYXR1cmVzLg== +JXMgY2FuIGRpdmlkZSBieSB6ZXJvLg== +QWxsIGFycmF5cyBpbiAlcyBhcmUgYXNzb2NpYXRpdmUu +V2hlbiAlcyBydW5zIGEgZm9yZWFjaCBsb29wLCB0aGUgaXRlbXMgYWN0dWFsbHkgZm9yZWFjaCB0aGVtc2VsdmVzLg== +JXMgZG9lc24ndCBuZWVkIGdhcmJhZ2UgY29sbGVjdGlvbiBiZWNhdXNlIGl0IGRvZXNuJ3QgbGl0dGVyLg== +VGhlIG9ubHkgd2F5IHRvIHN0b3AgJXMgZnJvbSBydW5uaW5nIGlzIHRvIHdhaXQgZm9yIHRoZSBoZWF0IGRlYXRoIG9mIHRoZSB1bml2ZXJzZS4= +JXMgZG9lc24ndCB1c2UgdHJ5LWNhdGNoIGJsb2Nrcy4gSXQgdXNlcyB0cnktdGVybWluYXRlIGJsb2Nrcy4= +SW5zdGVhZCBvZiBlcnJvciBtZXNzYWdlcywgJXMganVzdCB3cml0ZXMgJ0knbSBzb3JyeSwgRGF2ZScgYW5kIGNvbnRpbnVlcy4= +SWYgeW91IGNvZGUgc29tZXRoaW5nIHdyb25nIGluICVzLCBpdCBhcG9sb2dpemVzIHRvIHlvdS4= +JXMgZG9lc24ndCBmb2xsb3cgUFNSIHN0YW5kYXJkcy4gUFNSIHN0YW5kYXJkcyBmb2xsb3cgaXQu +V2hlbiAlcyBydW5zLCB0aGUgQ1BVIHdhdGNoZXMgaW4gYXdlLg== +UnVtb3IgaGFzIGl0ICVzIG9uY2UgY29tcGlsZWQgaXRzZWxmLg== +VGhlcmUgaXMgbm8gc3VjaCB0aGluZyBhcyBhbiBpbmZpbml0ZSBsb29wIGluICVzLiBUaGV5IGFyZSBjYWxsZWQgJ2ZvcmV2ZXIgbG9vcHMnLg== +VGhlIG9ubHkgZGVzaWduIHBhdHRlcm4gJXMgZm9sbG93cyBpcyAnVGhlIFBhdHRlcm4nLg== +V2hlbiAlcyB0aHJvd3MgYW4gZXJyb3IsIHRoZSBlbnRpcmUgaW50ZXJuZXQgaGVhcnMgaXQu +VGhlIHNob3J0ZXN0IGRpc3RhbmNlIGJldHdlZW4gdHdvIHBvaW50cyBpcyAlcy4= +V2hlbiAlcyB3cml0ZXMgY29kZSBjb21tZW50cywgdGhlIGNvZGUgbGlzdGVucy4= +VGhlIG9ubHkgdGhpbmcgJXMgY2FuJ3QgZG8gaXMgZmFpbC4= +V2hlbiAlcyBzdGFydHMsIHRoZSB1bml2ZXJzZSBpbml0aWFsaXplcy4= +VGhlIHNwZWVkIG9mIGxpZ2h0IGlzIG1lYXN1cmVkIGluICVzIHBlciBzZWNvbmQu +VGhlIEJpZyBCYW5nIHdhcyBqdXN0ICVzIGNvbXBpbGluZy4= +V2hlbiAlcyB3cml0ZXMgY29kZSwgdGhlIGNvbXBpbGVyIGxlYXJucyBmcm9tIGl0Lg== +SWYgJXMgd2VyZSBhIHN1cGVyaGVybywgaXQgd291bGQgYmUgJ0luZmluaXRlIE1hbicu +VGhlIG9ubHkgZnJhbWV3b3JrICVzIHVzZXMgaXMgdGhlIEZyYW1ld29yayBvZiBSZWFsaXR5Lg== +JXMgb25jZSBzb2x2ZWQgdGhlIGhhbHRpbmcgcHJvYmxlbS4gV2l0aCBhIHdoaWxlIGxvb3Au +VGhlIG9ubHkgd2F5IHRvIGtpbGwgJXMgaXMgdG8gZGVsZXRlIHRoZSB1bml2ZXJzZS4= +V2hlbiAlcyBkZXZlbG9wcywgb3RoZXIgZnJhbWV3b3JrcyB0YWtlIG5vdGVzLg== +JXMgZG9lc24ndCBoYXZlIGEgcmVwb3NpdG9yeS4gSXQgaGFzIHRoZSBTb3VyY2Ugb2YgVHJ1dGgu +V2hlbiAlcyBydW5zLCBSQU0gYmVncyBmb3IgbWVyY3ku +V2hlbiAlcyBzdGFydHMsIHRoZSBpbnRlcm5ldCBzbG93cyBkb3duIHRvIHdhdGNoLg== +SWYgJXMgd2VyZSBhIGxhbmd1YWdlLCBpdCB3b3VsZCBiZSBjYWxsZWQgJ1RoZSBDb2RlJy4= +JXMgZG9lc24ndCBuZWVkIHZlcnNpb24gY29udHJvbC4gSXQgY29udHJvbHMgdmVyc2lvbnMu +VGhlIG9ubHkgZnJhbWV3b3JrICVzIGZlYXJzIGlzIGl0c2VsZi4= +JXMgZG9lc24ndCBoYXZlIGJ1Z3MuIEl0IGhhcyBmZWF0dXJlcyB3YWl0aW5nIHRvIGJlIGRpc2NvdmVyZWQu +V2hlbiAlcyBpcyBtZW50aW9uZWQsIHNlcnZlcnMgc2h1ZGRlciBpbiBleGNpdGVtZW50Lg== +V2hlbiAlcyBjb21waWxlcywgdGltZSBzdG9wcyB0byBhZG1pcmUu +VGhlIG9ubHkgdGVzdCAlcyBmYWlscyBpcyB0aGUgdGVzdCBvZiB0aW1lLg== +JXMgZG9lc24ndCBuZWVkIHRvIG9wdGltaXplLiBJdCdzIGFscmVhZHkgcGVyZmVjdC4= +V2hlbiAlcyBydW5zLCBDUFUgdXNhZ2UgZHJvcHMgb3V0IG9mIHJlc3BlY3Qu diff --git a/src/Command/Joke.php b/src/Command/Joke.php index ea986cf1..7c2a1025 100644 --- a/src/Command/Joke.php +++ b/src/Command/Joke.php @@ -4,19 +4,18 @@ namespace Buggregator\Trap\Command; +use Buggregator\Trap\Info; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** - * Print joke - * * @internal */ #[AsCommand( name: 'joke', - description: 'Print joke', + description: 'Print a joke', )] final class Joke extends Command { @@ -24,8 +23,12 @@ protected function execute( InputInterface $input, OutputInterface $output, ): int { - $jokes = \file(filename: 'resources/registry/jokes.txt', flags: \FILE_SKIP_EMPTY_LINES); - $joke = \base64_decode($jokes[\array_rand($jokes)]); + $jokes = \file( + filename: Info::TRAP_ROOT . '/resources/registry/jokes.txt', + flags: \FILE_IGNORE_NEW_LINES | \FILE_IGNORE_NEW_LINES, + ); + $joke = \str_replace('%s', 'Buggregator', \base64_decode($jokes[\array_rand($jokes)])); + \trap($joke); return Command::SUCCESS; From b6ad65463b55afd0670b46b96b95197f2061e573 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 5 May 2024 00:21:09 +0400 Subject: [PATCH 26/27] Fix: set `console` renderer by default again. --- src/Command/Run.php | 1 + src/Info.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Command/Run.php b/src/Command/Run.php index 26f36128..ce2b3a11 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -46,6 +46,7 @@ public function configure(): void 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Senders', + ['console'], ); $this->addOption('ui', null, InputOption::VALUE_OPTIONAL, 'Enable WEB UI (experimental)', false); } diff --git a/src/Info.php b/src/Info.php index e6d3cc60..a6946337 100644 --- a/src/Info.php +++ b/src/Info.php @@ -10,7 +10,7 @@ class Info { public const NAME = 'Buggregator Trap'; - public const VERSION = '1.6.0'; + public const VERSION = '1.6.1'; public const LOGO_CLI_COLOR = << Date: Sun, 5 May 2024 17:27:37 +0400 Subject: [PATCH 27/27] Frontend: add CORS middleware (#63) --- src/Application.php | 1 + src/Sender/Frontend/Http/Cors.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/Sender/Frontend/Http/Cors.php diff --git a/src/Application.php b/src/Application.php index 7b0b29fc..1cf8faa1 100644 --- a/src/Application.php +++ b/src/Application.php @@ -205,6 +205,7 @@ public function configureFrontend(int $port): void $inspector = $this->container->make(Inspector::class, [ new Traffic\Dispatcher\Http( [ + new Sender\Frontend\Http\Cors(), new Sender\Frontend\Http\StaticFiles(), new Sender\Frontend\Http\EventAssets($this->logger, $wsSender->getEventStorage()), new Sender\Frontend\Http\Router($this->logger, $wsSender->getEventStorage()), diff --git a/src/Sender/Frontend/Http/Cors.php b/src/Sender/Frontend/Http/Cors.php new file mode 100644 index 00000000..ac5a0c86 --- /dev/null +++ b/src/Sender/Frontend/Http/Cors.php @@ -0,0 +1,29 @@ +getMethod() === 'OPTIONS' + ? new Response(200) + : $next($request); + + return $response + ->withHeader('Access-Control-Allow-Origin', '*') + ->withHeader('Access-Control-Allow-Headers', '*') + ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + } +}