diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index f172154..500b4ab 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -160,7 +160,7 @@ jobs: - name: "Run mutation tests with infection/infection" env: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - run: "vendor/bin/infection --show-mutations --only-covered --min-covered-msi=79 --min-msi=79 --coverage=.build/coverage --logger-github --no-progress -vv" + run: "vendor/bin/infection --show-mutations --only-covered --min-covered-msi=97 --min-msi=97 --coverage=.build/coverage --logger-github --no-progress -vv" # This is a meta job to avoid to have to constantly change the protection rules # whenever we touch the matrix. diff --git a/composer.json b/composer.json index 347a262..10c9b21 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "infection/infection": "^0.27.0", "laminas/laminas-modulemanager": "^2.14.0", "laminas/laminas-servicemanager": "^3.21.0", - "mimmi20/coding-standard": "^5.0.3", + "mimmi20/coding-standard": "^5.0.5", "nikic/php-parser": "^v4.15.5", "phpstan/extension-installer": "^1.3.1", "phpstan/phpstan": "^1.10.15", diff --git a/phpcs.xml b/phpcs.xml index aa9d8c0..0a3f6b7 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -15,6 +15,7 @@ + diff --git a/src/StreamFormatter.php b/src/StreamFormatter.php index 9dfc247..c79871f 100644 --- a/src/StreamFormatter.php +++ b/src/StreamFormatter.php @@ -12,7 +12,6 @@ namespace Mimmi20\Monolog\Formatter; -use DateTimeImmutable; use Monolog\Formatter\LineFormatter; use Monolog\Formatter\NormalizerFormatter; use Monolog\Level; @@ -28,16 +27,11 @@ use function array_keys; use function count; use function is_array; -use function is_bool; -use function is_iterable; use function is_scalar; use function is_string; -use function mb_strpos; use function str_repeat; use function str_replace; use function trim; -use function ucfirst; -use function var_export; final class StreamFormatter extends NormalizerFormatter { @@ -128,10 +122,6 @@ public function setFormatter(LineFormatter $formatter): void */ public function format(LogRecord $record): string { - /** @var array<(array|scalar|null)>|scalar|null $vars */ - /** @phpstan-var array{message: string, context: array, level: Level, level_name: string, channel: string, datetime: DateTimeImmutable, extra: array} $vars */ - $vars = $this->normalizeRecord($record); - $message = $this->getFormatter()->format($record); $levelName = Level::fromValue($record->level->value)->getName(); @@ -172,65 +162,8 @@ public function format(LogRecord $record): string ], ); - foreach (['extra', 'context'] as $element) { - if (empty($vars[$element]) || !is_iterable($vars[$element])) { - continue; - } - - $this->table->addRow(new TableSeparator()); - $this->table->addRow( - [new TableCell(ucfirst($element), ['colspan' => self::SPAN_ALL_COLUMS])], - ); - $this->table->addRow(new TableSeparator()); - - foreach ($vars[$element] as $key => $value) { - if (!is_string($key)) { - continue; - } - - if ( - is_array($record->{$element}) - && isset($record->{$element}[$key]) - && $record->{$element}[$key] instanceof Throwable - ) { - $exception = $record->{$element}[$key]; - - $value = [ - 'Code' => $exception->getCode(), - 'File' => $exception->getFile(), - 'Line' => $exception->getLine(), - 'Message' => $exception->getMessage(), - 'Trace' => $exception->getTraceAsString(), - 'Type' => $exception::class, - ]; - - $this->addFact($key, $value); - - $prev = $exception->getPrevious(); - - if ($prev instanceof Throwable) { - do { - $value = [ - 'Code' => $prev->getCode(), - 'File' => $prev->getFile(), - 'Line' => $prev->getLine(), - 'Message' => $prev->getMessage(), - 'Trace' => $prev->getTraceAsString(), - 'Type' => $prev::class, - ]; - - $this->addFact('previous Throwable', $value); - - $prev = $prev->getPrevious(); - } while ($prev instanceof Throwable); - } - - continue; - } - - $this->addFact($key, $value); - } - } + $this->addExtra($record->extra); + $this->addContext($record->context); $this->table->render(); @@ -272,38 +205,98 @@ private function getFormatter(): LineFormatter return $this->formatter; } - /** @throws RuntimeException if encoding fails and errors are not ignored */ - private function stringify(mixed $value): string + /** + * @param array $context + * + * @throws RuntimeException + */ + private function addContext(array $context): void { - return $this->replaceNewlines($this->convertToString($value)); + if ($context === []) { + return; + } + + $this->table->addRow(new TableSeparator()); + $this->table->addRow( + [new TableCell('Context', ['colspan' => self::SPAN_ALL_COLUMS])], + ); + $this->table->addRow(new TableSeparator()); + + foreach ($context as $key => $value) { + if (!is_string($key)) { + continue; + } + + $this->addFact($key, $this->normalize($value)); + } } - /** @throws RuntimeException if encoding fails and errors are not ignored */ - private function convertToString(mixed $data): string + /** + * @param array $extra + * + * @throws RuntimeException + */ + private function addExtra(array $extra): void { - if ($data === null || is_bool($data)) { - return var_export($data, true); + if ($extra === []) { + return; } - if (is_scalar($data)) { - return (string) $data; - } + $this->table->addRow(new TableSeparator()); + $this->table->addRow( + [new TableCell('Extra', ['colspan' => self::SPAN_ALL_COLUMS])], + ); + $this->table->addRow(new TableSeparator()); - return $this->toJson($data, true); + foreach ($extra as $key => $value) { + if (!is_string($key)) { + continue; + } + + if ($extra[$key] instanceof Throwable) { + $this->addThrowable($extra[$key]); + + continue; + } + + $this->addFact($key, $this->normalize($value)); + } } - /** @throws void */ - private function replaceNewlines(string $str): string + /** @throws RuntimeException */ + private function addThrowable(Throwable $exception): void { - if ($this->allowInlineLineBreaks) { - if (mb_strpos($str, '{') === 0) { - return str_replace(['\r', '\n'], ["\r", "\n"], $str); - } + $value = [ + 'Code' => $exception->getCode(), + 'File' => $exception->getFile(), + 'Line' => $exception->getLine(), + 'Message' => $exception->getMessage(), + 'Trace' => $exception->getTraceAsString(), + 'Type' => $exception::class, + ]; + + $this->addFact('Throwable', $value); - return $str; + $prev = $exception->getPrevious(); + + if (!$prev instanceof Throwable) { + return; } - return str_replace(["\r\n", "\r", "\n"], ' ', $str); + do { + $value = [ + 'Code' => $prev->getCode(), + 'File' => $prev->getFile(), + 'Line' => $prev->getLine(), + 'Message' => $prev->getMessage(), + 'Trace' => $prev->getTraceAsString(), + 'Type' => $prev::class, + ]; + + $this->addFact('previous Throwable', $value); + + $prev = $prev->getPrevious(); + } while ($prev instanceof Throwable); } /** @throws RuntimeException if encoding fails and errors are not ignored */ @@ -315,11 +308,7 @@ private function addFact(string $name, mixed $value): void $rowspan = count($value); foreach (array_keys($value) as $number => $key) { - $cellValue = $value[$key]; - - if (!is_string($cellValue)) { - $cellValue = $this->stringify($cellValue); - } + $cellValue = $this->stringify($value[$key]); if ($number === 0) { $this->table->addRow( @@ -341,17 +330,17 @@ private function addFact(string $name, mixed $value): void ), ], ); - } else { - $this->table->addRow([new TableCell((string) $key), new TableCell($cellValue)]); + + continue; } + + $this->table->addRow([new TableCell((string) $key), new TableCell($cellValue)]); } return; } - if (!is_string($value)) { - $value = $this->stringify($value); - } + $value = $this->stringify($value); $this->table->addRow( [ @@ -363,4 +352,50 @@ private function addFact(string $name, mixed $value): void ], ); } + + /** @throws RuntimeException if encoding fails and errors are not ignored */ + private function stringify(mixed $value): string + { + return $this->replaceNewlines($this->convertToString($value)); + } + + /** @throws RuntimeException if encoding fails and errors are not ignored */ + private function convertToString(mixed $data): string + { + if (is_string($data)) { + return $data; + } + + if ($data === null) { + return 'null'; + } + + if ($data === true) { + return 'true'; + } + + if ($data === false) { + return 'false'; + } + + if (is_scalar($data)) { + return (string) $data; + } + + return $this->toJson($data, true); + } + + /** @throws void */ + private function replaceNewlines(string $str): string + { + if ($this->allowInlineLineBreaks) { + return str_replace( + ['\\\\r\\\\n', '\\r\\n', '\\\\r', '\\r', '\\\\n', '\\n', "\r\n", "\r"], + "\n", + $str, + ); + } + + return str_replace(["\r\n", "\r", "\n"], ' ', $str); + } } diff --git a/tests/ConfigProviderTest.php b/tests/ConfigProviderTest.php index 1f4d5f4..1ec24dc 100644 --- a/tests/ConfigProviderTest.php +++ b/tests/ConfigProviderTest.php @@ -50,4 +50,35 @@ public function testGetMonologFormatterConfig(): void self::assertIsArray($factories); self::assertCount(1, $factories); } + + /** @throws Exception */ + public function testInvoke(): void + { + $config = ($this->provider)(); + self::assertIsArray($config); + self::assertCount(1, $config); + + self::assertArrayHasKey('monolog_formatters', $config); + + $monologFormatterConfig = $config['monolog_formatters']; + self::assertIsArray($monologFormatterConfig); + self::assertCount(2, $monologFormatterConfig); + + self::assertArrayNotHasKey('abstract_factories', $monologFormatterConfig); + self::assertArrayNotHasKey('delegators', $monologFormatterConfig); + self::assertArrayNotHasKey('initializers', $monologFormatterConfig); + self::assertArrayNotHasKey('invokables', $monologFormatterConfig); + self::assertArrayNotHasKey('services', $monologFormatterConfig); + self::assertArrayNotHasKey('shared', $monologFormatterConfig); + + self::assertArrayHasKey('aliases', $monologFormatterConfig); + $aliases = $monologFormatterConfig['aliases']; + self::assertIsArray($aliases); + self::assertCount(1, $aliases); + + self::assertArrayHasKey('factories', $monologFormatterConfig); + $factories = $monologFormatterConfig['factories']; + self::assertIsArray($factories); + self::assertCount(1, $factories); + } } diff --git a/tests/ModuleTest.php b/tests/ModuleTest.php index 72b0fb3..3b8928f 100644 --- a/tests/ModuleTest.php +++ b/tests/ModuleTest.php @@ -28,5 +28,26 @@ public function testGetConfig(): void self::assertIsArray($config); self::assertCount(1, $config); self::assertArrayHasKey('monolog_formatters', $config); + + $monologFormatterConfig = $config['monolog_formatters']; + self::assertIsArray($monologFormatterConfig); + self::assertCount(2, $monologFormatterConfig); + + self::assertArrayNotHasKey('abstract_factories', $monologFormatterConfig); + self::assertArrayNotHasKey('delegators', $monologFormatterConfig); + self::assertArrayNotHasKey('initializers', $monologFormatterConfig); + self::assertArrayNotHasKey('invokables', $monologFormatterConfig); + self::assertArrayNotHasKey('services', $monologFormatterConfig); + self::assertArrayNotHasKey('shared', $monologFormatterConfig); + + self::assertArrayHasKey('aliases', $monologFormatterConfig); + $aliases = $monologFormatterConfig['aliases']; + self::assertIsArray($aliases); + self::assertCount(1, $aliases); + + self::assertArrayHasKey('factories', $monologFormatterConfig); + $factories = $monologFormatterConfig['factories']; + self::assertIsArray($factories); + self::assertCount(1, $factories); } } diff --git a/tests/StreamFormatterTest.php b/tests/StreamFormatterTest.php index 580aa61..d0e3e1a 100644 --- a/tests/StreamFormatterTest.php +++ b/tests/StreamFormatterTest.php @@ -26,11 +26,15 @@ use RuntimeException; use stdClass; use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Output\BufferedOutput; -use Symfony\Component\Console\Output\Output; +use Symfony\Component\Console\Output\OutputInterface; use UnexpectedValueException; +use function assert; use function file_put_contents; +use function in_array; use function str_repeat; use function str_replace; @@ -74,7 +78,7 @@ public function testConstructWithDefaults(): void $table->expects(self::never()) ->method('render'); - $formatter = new StreamFormatter($output, $table); + $formatter = new StreamFormatter(output: $output, table: $table); self::assertSame(NormalizerFormatter::SIMPLE_DATE, $formatter->getDateFormat()); self::assertSame(9, $formatter->getMaxNormalizeDepth()); @@ -140,13 +144,13 @@ public function testConstructWithValues(): void ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - $format, - $tableStyle, - $dateFormat, - true, - false, + output: $output, + table: $table, + format: $format, + tableStyle: $tableStyle, + dateFormat: $dateFormat, + allowInlineLineBreaks: true, + includeStacktraces: false, ); self::assertSame($dateFormat, $formatter->getDateFormat()); @@ -213,13 +217,13 @@ public function testConstructWithValues2(): void ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - $format, - $tableStyle, - $dateFormat, - false, - true, + output: $output, + table: $table, + format: $format, + tableStyle: $tableStyle, + dateFormat: $dateFormat, + allowInlineLineBreaks: false, + includeStacktraces: true, ); self::assertSame($dateFormat, $formatter->getDateFormat()); @@ -286,13 +290,13 @@ public function testConstructWithValues3(): void ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - $format, - $tableStyle, - $dateFormat, - false, - false, + output: $output, + table: $table, + format: $format, + tableStyle: $tableStyle, + dateFormat: $dateFormat, + allowInlineLineBreaks: false, + includeStacktraces: false, ); self::assertSame($dateFormat, $formatter->getDateFormat()); @@ -365,13 +369,13 @@ public function testConstructWithValues4(): void ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - $format, - $tableStyle, - $dateFormat, - true, - false, + output: $output, + table: $table, + format: $format, + tableStyle: $tableStyle, + dateFormat: $dateFormat, + allowInlineLineBreaks: true, + includeStacktraces: false, ); self::assertSame($dateFormat, $formatter->getDateFormat()); @@ -420,6 +424,7 @@ public function testFormat(): void $message = 'test message'; $channel = 'test-channel'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $expected = 'rendered-content'; @@ -429,15 +434,18 @@ public function testFormat(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->with() - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher, $message): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame($message, $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -460,17 +468,73 @@ public function testFormat(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(3)) - ->method('addRow'); + $matcher = self::exactly(3); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); - $formatter = new StreamFormatter($output, $table); + $formatter = new StreamFormatter(output: $output, table: $table); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: [], extra: [], @@ -490,6 +554,7 @@ public function testFormat2(): void $message = 'test message'; $channel = 'test-channel'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $expected = 'rendered-content'; @@ -499,14 +564,18 @@ public function testFormat2(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', 220), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher, $message): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame($message, $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -529,19 +598,106 @@ public function testFormat2(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(15)) - ->method('addRow'); + $matcher = self::exactly(15); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 14 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); - $formatter = new StreamFormatter($output, $table); + $formatter = new StreamFormatter(output: $output, table: $table); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, - context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz']], + context: ['one' => null, 'two' => true, 0 => 'numeric-key', 'three' => false, 'four' => ['abc', 'xyz']], extra: ['app' => 'test-app'], ); @@ -559,6 +715,7 @@ public function testFormat3(): void $message = 'test message'; $channel = 'test-channel'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $expected = 'rendered-content'; @@ -568,14 +725,18 @@ public function testFormat3(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', 220), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame('test message true test-app', $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -598,19 +759,110 @@ public function testFormat3(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(15)) - ->method('addRow'); + $matcher = self::exactly(15); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 14 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); - $formatter = new StreamFormatter($output, $table, '%message% %context.two% %extra.app%'); + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.two% %extra.app%', + ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, - context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz']], + context: ['one' => null, 'two' => true, 0 => 'numeric-key', 'three' => false, 'four' => ['abc', 'xyz']], extra: ['app' => 'test-app'], ); @@ -628,6 +880,7 @@ public function testFormat4(): void $message = 'test message'; $channel = 'test-channel'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $expected = 'rendered-content'; @@ -637,14 +890,18 @@ public function testFormat4(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', 220), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame('test message ["abc","xyz"] test-app', $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -667,19 +924,110 @@ public function testFormat4(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(15)) - ->method('addRow'); + $matcher = self::exactly(15); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 14 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); - $formatter = new StreamFormatter($output, $table, '%message% %context.four% %extra.app%'); + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.four% %extra.app%', + ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, - context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz']], + context: ['one' => null, 'two' => true, 0 => 'numeric-key', 'three' => false, 'four' => ['abc', 'xyz']], extra: ['app' => 'test-app'], ); @@ -698,6 +1046,7 @@ public function testFormat5(): void $channel = 'test-channel'; $datetime = new DateTimeImmutable('now'); $tableStyle = 'default'; + $level = Level::Error; $expected = 'rendered-content'; @@ -707,14 +1056,18 @@ public function testFormat5(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', 220), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame('test message test test test-app', $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -737,26 +1090,113 @@ public function testFormat5(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(16)) - ->method('addRow'); + $matcher = self::exactly(16); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 14 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.five% %extra.app%', - $tableStyle, - null, - false, + output: $output, + table: $table, + format: '%message% %context.five% %extra.app%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: false, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, - context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], + context: ['one' => null, 'two' => true, 0 => 'numeric-key', 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], extra: ['app' => 'test-app'], ); @@ -775,6 +1215,7 @@ public function testFormat6(): void $channel = 'test-channel'; $datetime = new DateTimeImmutable('now'); $tableStyle = 'default'; + $level = Level::Error; $expected = 'rendered-content'; @@ -784,14 +1225,18 @@ public function testFormat6(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', 220), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame("test message test\ntest test-app", $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -814,27 +1259,114 @@ public function testFormat6(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(16)) - ->method('addRow'); + $matcher = self::exactly(16); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 14 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.five% %extra.app%', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% %context.five% %extra.app%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], - extra: ['app' => 'test-app'], + extra: ['app' => 'test-app', 0 => 'numeric-key'], ); $formatted = $formatter->format($record); @@ -853,6 +1385,7 @@ public function testFormat7(): void $tableStyle = 'default'; $datetime = new DateTimeImmutable('now'); $exception = new RuntimeException('error'); + $level = Level::Error; $expected = 'rendered-content'; @@ -862,14 +1395,21 @@ public function testFormat7(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', 220), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher, $exception): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame( + "test message test\ntest <[object] (RuntimeException(code: " . $exception->getCode() . '): error at ' . $exception->getFile() . ':' . $exception->getLine() . ')>', + $messages, + ), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -892,27 +1432,194 @@ public function testFormat7(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(22)) - ->method('addRow'); + $matcher = self::exactly(23); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 15, 17], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 16 => self::assertCount( + 1, + $row, + (string) $matcher->numberOfInvocations(), + ), + 8, 21 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 8) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 10) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 11) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 12) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 13) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 16) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + if ($matcher->numberOfInvocations() === 21) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('four', (string) $tableCell); + + return $table; + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.five% <%extra.Exception%>', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% %context.five% <%extra.Exception%>', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], - extra: ['app' => 'test-app', 'Exception' => $exception], + extra: ['app' => 'test-app', 0 => 'numeric-key', 'Exception' => $exception, 'system' => 'test-system'], ); $formatted = $formatter->format($record); @@ -930,8 +1637,8 @@ public function testFormat8(): void $channel = 'test-channel'; $tableStyle = 'default'; $datetime = new DateTimeImmutable('now'); - - $exception = new RuntimeException('error'); + $exception = new RuntimeException('error'); + $level = Level::Error; $expected = 'rendered-content'; @@ -941,14 +1648,18 @@ public function testFormat8(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame("test message test\ntest test-app", $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -971,27 +1682,184 @@ public function testFormat8(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(22)) - ->method('addRow'); + $matcher = self::exactly(23); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 15, 17], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 16 => self::assertCount( + 1, + $row, + (string) $matcher->numberOfInvocations(), + ), + 8, 21 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 8) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 10) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 11) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 12) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 13) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 16) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.five% %extra.app%', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% %context.five% %extra.app%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], - extra: ['app' => 'test-app', 'Exception' => $exception], + extra: ['app' => 'test-app', 0 => 'numeric-key', 'Exception' => $exception, 'system' => 'test-system'], ); $formatted = $formatter->format($record); @@ -1009,6 +1877,7 @@ public function testFormat9(): void $channel = 'test-channel'; $tableStyle = 'default'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $expected = 'rendered-content'; @@ -1022,14 +1891,18 @@ public function testFormat9(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame("test message test\ntest test-app", $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -1052,27 +1925,320 @@ public function testFormat9(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(34)) - ->method('addRow'); - $table->expects(self::once()) - ->method('render'); + $matcher = self::exactly(35); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 27, 29], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); - $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.five% %extra.app%', - $tableStyle, - null, - true, - ); + return $table; + } - $record = new LogRecord( - datetime: $datetime, - channel: $channel, - level: Level::Error, + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 28 => self::assertCount( + 1, + $row, + (string) $matcher->numberOfInvocations(), + ), + 8, 14, 20, 33 => self::assertCount( + 3, + $row, + (string) $matcher->numberOfInvocations(), + ), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 8) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 10) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 11) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 12) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 13) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 14) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('previous Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 15) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 16) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 17) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 18) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 19) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 20) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('previous Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 21) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 22) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 23) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 24) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 25) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 28) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); + $table->expects(self::once()) + ->method('render'); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.five% %extra.app%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, + ); + + $record = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level, message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], - extra: ['app' => 'test-app', 'Exception' => $exception3], + extra: ['app' => 'test-app', 0 => 'numeric-key', 'Exception' => $exception3, 'system' => 'test-system'], ); $formatted = $formatter->format($record); @@ -1090,6 +2256,7 @@ public function testFormat10(): void $channel = 'test-channel'; $tableStyle = 'default'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $exception1 = new RuntimeException('error'); $exception2 = new UnexpectedValueException('error', 4711, $exception1); @@ -1103,14 +2270,21 @@ public function testFormat10(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame( + "test message context.one test\ntest test-app extra.Exception", + $messages, + ), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -1133,27 +2307,320 @@ public function testFormat10(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(34)) - ->method('addRow'); + $matcher = self::exactly(35); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 27, 29], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 28 => self::assertCount( + 1, + $row, + (string) $matcher->numberOfInvocations(), + ), + 8, 14, 20, 33 => self::assertCount( + 3, + $row, + (string) $matcher->numberOfInvocations(), + ), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 8) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 10) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 11) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 12) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 13) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 14) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('previous Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 15) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 16) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 17) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 18) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 19) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 20) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('previous Throwable', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('Code', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 21) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('File', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 22) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Line', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 23) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Message', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 24) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Trace', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 25) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Type', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 28) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% context.one %context.five% %extra.app% extra.Exception', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% context.one %context.five% %extra.app% extra.Exception', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], - extra: ['app' => 'test-app', 'Exception' => $exception3], + extra: ['app' => 'test-app', 0 => 'numeric-key', 'Exception' => $exception3, 'system' => 'test-system'], ); $formatted = $formatter->format($record); @@ -1171,6 +2638,7 @@ public function testFormat11(): void $channel = 'test-channel'; $tableStyle = 'default'; $datetime = new DateTimeImmutable('now'); + $level = Level::Error; $expected = 'rendered-content'; @@ -1180,14 +2648,21 @@ public function testFormat11(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame( + "test message NULL test\ntest test-app test-app", + $messages, + ), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -1210,24 +2685,110 @@ public function testFormat11(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(12)) - ->method('addRow'); + $matcher = self::exactly(12); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: ['one' => null, 'five' => "test\ntest"], extra: ['app' => 'test-app'], @@ -1252,6 +2813,7 @@ public function testFormat12(): void $stdClass = new stdClass(); $stdClass->a = $channel; $stdClass->b = $message; + $level = Level::Error; $expected = 'rendered-content'; @@ -1261,14 +2823,18 @@ public function testFormat12(): void $output->expects(self::exactly(2)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected); - $output->expects(self::exactly(5)) + $matcher = self::exactly(5); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher, $formattedMessage): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame($formattedMessage, $messages), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -1291,24 +2857,111 @@ public function testFormat12(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(13)) - ->method('addRow'); + $matcher = self::exactly(13); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 13 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + return $table; + }, + ); $table->expects(self::once()) ->method('render'); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, context: ['one' => null, 'five' => "test\ntest", 'six' => $stdClass], extra: ['app' => 'test-app'], @@ -1343,6 +2996,7 @@ public function testFormat13(): void $stdClass = new stdClass(); $stdClass->a = $channel; $stdClass->b = $message; + $level = Level::Error; $expected = '============================================================================================================================================================================================================================================================================== @@ -1361,10 +3015,14 @@ public function testFormat13(): void +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Context | +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| one | NULL | +| one | null | +| two | true | +| three | false | +| four | 42 | | five | test | | | test | | six | stdClass | {"a":"test-channel","b":"test message"} | +| seven | 47.11 | +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ '; @@ -1373,20 +3031,20 @@ public function testFormat13(): void $table = new Table($output); $formatter = new StreamFormatter( - $output, - $table, - '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', - $tableStyle, - null, - true, + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, ); $record = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level, message: $message, - context: ['one' => null, 'five' => "test\ntest", 'six' => $stdClass], + context: ['one' => null, 'two' => true, 'three' => false, 'four' => 42, 'five' => "test\ntest", 'six' => $stdClass, 'seven' => 47.11], extra: ['app' => 'test-app'], ); @@ -1412,37 +3070,576 @@ public function testFormat13(): void * @throws Exception * @throws RuntimeException */ - public function testFormatBatch(): void + public function testFormat14(): void { - $message = 'test message'; - $channel = 'test-channel'; - $tableStyle = StreamFormatter::BOX_STYLE; - $datetime = new DateTimeImmutable('now'); + $message = ' test message '; + $channel = 'test-channel'; + $tableStyle = 'default'; + $datetime = new DateTimeImmutable('now'); + $formattedMessage = 'this is a formatted message'; + $stdClass = new stdClass(); + $stdClass->a = $channel; + $stdClass->b = $message; + $level = Level::Error; - $expected1 = 'rendered-content-1'; - $expected2 = 'rendered-content-2'; - $expected3 = 'rendered-content-3'; + $expected = '============================================================================================================================================================================================================================================================================== - $record1 = new LogRecord( - datetime: $datetime, - channel: $channel, - level: Level::Error, - message: $message, - context: [], - extra: [], - ); - $record2 = new LogRecord( - datetime: $datetime, - channel: $channel, - level: Level::Error, - message: $message, +this is a formatted message + ++----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| General Info | +| Time | ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' | +| Level | ERROR | ++----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Extra | ++----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| app | test-app | ++----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Context | ++----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| one | null | +| five | test | +| | test | +| six | stdClass | {"a":"test-channel","b":" test message "} | ++----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +'; + + $output = new BufferedOutput(); + $table = new Table($output); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, + ); + + $record = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level, + message: $message, + context: ['one' => null, 'five' => "test\ntest", 'six' => $stdClass], + extra: ['app' => 'test-app'], + ); + + $lineFormatter = $this->getMockBuilder(LineFormatter::class) + ->disableOriginalConstructor() + ->getMock(); + $lineFormatter->expects(self::once()) + ->method('format') + ->with($record) + ->willReturn($formattedMessage); + + $formatter->setFormatter($lineFormatter); + + $formatted = $formatter->format($record); + + self::assertSame( + str_replace("\r\n", "\n", $expected), + str_replace("\r\n", "\n", $formatted), + ); + } + + /** + * @throws Exception + * @throws RuntimeException + */ + public function testFormat15(): void + { + $message1 = 'test message\rtest message 2\ntest message 3\r\ntest message 4'; + $message2 = 'test message 5\rtest message 6\ntest message 7\r\ntest message 8'; + $message3 = "test1\ntest2\rtest3\r\ntest4"; + $channel = 'test-channel'; + $tableStyle = 'default'; + $datetime = new DateTimeImmutable('now'); + $formattedMessage = 'this is a formatted message'; + $stdClass = new stdClass(); + $stdClass->a = $channel; + $stdClass->b = $message1; + $level = Level::Error; + $appName = 'test-app'; + + $expected = 'rendered-content'; + + $output = $this->getMockBuilder(BufferedOutput::class) + ->disableOriginalConstructor() + ->getMock(); + $output->expects(self::exactly(2)) + ->method('fetch') + ->willReturnOnConsecutiveCalls('', $expected); + $matcher = self::exactly(5); + $output->expects($matcher) + ->method('writeln') + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher, $formattedMessage): void { + match ($matcher->numberOfInvocations()) { + 1 => self::assertSame(str_repeat('=', StreamFormatter::FULL_WIDTH), $messages), + 2, 4, 5 => self::assertSame('', $messages), + default => self::assertSame($formattedMessage, $messages), + }; + }, + ); + + $table = $this->getMockBuilder(Table::class) + ->disableOriginalConstructor() + ->getMock(); + $table->expects(self::once()) + ->method('setStyle') + ->with($tableStyle) + ->willReturnSelf(); + $table->expects(self::exactly(3)) + ->method('setColumnMaxWidth') + ->willReturnSelf(); + $table->expects(self::once()) + ->method('setColumnWidths') + ->with( + [StreamFormatter::WIDTH_FIRST_COLUMN, StreamFormatter::WIDTH_SECOND_COLUMN, StreamFormatter::WIDTH_THIRD_COLUMN], + ) + ->willReturnSelf(); + $table->expects(self::once()) + ->method('setRows') + ->with([]) + ->willReturnSelf(); + $matcher = self::exactly(14); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level, $message2, $message3, $appName): Table { + if (in_array($matcher->numberOfInvocations(), [4, 6, 8, 10], true)) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 5, 9 => self::assertCount(1, $row, (string) $matcher->numberOfInvocations()), + 13 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ($matcher->numberOfInvocations() === 1) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('General Info', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 2) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Time', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('Level', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($level->getName(), (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 5) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Extra', (string) $tableCell); + + return $table; + } + + if ($matcher->numberOfInvocations() === 7) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('app', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame($appName, (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 9) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell); + self::assertSame('Context', (string) $tableCell); + } + + if ($matcher->numberOfInvocations() === 11) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('one', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('null', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 12) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('five', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + str_replace( + ['\\\\r\\\\n', '\\r\\n', '\\\\r', '\\r', '\\\\n', '\\n', "\r\n", "\r"], + "\n", + $message3, + ), + (string) $tableCell2, + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 13) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('six', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame('stdClass', (string) $tableCell2); + + return $table; + } + + if ($matcher->numberOfInvocations() === 14) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell1); + self::assertSame('seven', (string) $tableCell1); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf(TableCell::class, $tableCell2); + self::assertSame( + str_replace( + ['\\\\r\\\\n', '\\r\\n', '\\\\r', '\\r', '\\\\n', '\\n', "\r\n", "\r"], + "\n", + $message2, + ), + (string) $tableCell2, + ); + + return $table; + } + + return $table; + }, + ); + $table->expects(self::once()) + ->method('render'); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, + ); + + $record = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level, + message: $message1, + context: ['one' => null, 'five' => $message3, 'six' => $stdClass, 'seven' => $message2], + extra: ['app' => $appName], + ); + + $lineFormatter = $this->getMockBuilder(LineFormatter::class) + ->disableOriginalConstructor() + ->getMock(); + $lineFormatter->expects(self::once()) + ->method('format') + ->with($record) + ->willReturn($formattedMessage); + + $formatter->setFormatter($lineFormatter); + + $formatted = $formatter->format($record); + + self::assertSame($expected, $formatted); + } + + /** + * @throws Exception + * @throws RuntimeException + */ + public function testFormat16(): void + { + $message1 = 'test message\rtest message 2\ntest message 3\r\ntest message 4'; + $message2 = 'test message 5\rtest message 6\ntest message 7\r\ntest message 8'; + $message3 = "test1\ntest2\rtest3\r\ntest4"; + $channel = 'test-channel'; + $tableStyle = 'default'; + $datetime = new DateTimeImmutable('now'); + $formattedMessage = 'this is a formatted message'; + $stdClass = new stdClass(); + $stdClass->a = $channel; + $stdClass->b = $message1; + $level = Level::Error; + $appName = 'test-app'; + + $expected = <<format(NormalizerFormatter::SIMPLE_DATE)} | + | Level | ERROR | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | Extra | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | app | test-app | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | Context | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | one | null | + | five | test1 test2 test3 test4 | + | six | stdClass | {"a":"test-channel","b":"test message\\\\rtest message 2\\\\ntest message 3\\\\r\\\\ntest message 4"} | + | seven | test message 5\\rtest message 6\\ntest mes | + | | sage 7\\r\\ntest message 8 | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + + TXT; + + $output = new BufferedOutput(); + $table = new Table($output); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: false, + ); + + $record = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level, + message: $message1, + context: ['one' => null, 'five' => $message3, 'six' => $stdClass, 'seven' => $message2], + extra: ['app' => $appName], + ); + + $lineFormatter = $this->getMockBuilder(LineFormatter::class) + ->disableOriginalConstructor() + ->getMock(); + $lineFormatter->expects(self::once()) + ->method('format') + ->with($record) + ->willReturn($formattedMessage); + + $formatter->setFormatter($lineFormatter); + + $formatted = $formatter->format($record); + + self::assertSame( + str_replace(["\r\n", "\r"], "\n", $expected), + str_replace(["\r\n", "\r"], "\n", $formatted), + ); + } + + /** + * @throws Exception + * @throws RuntimeException + */ + public function testFormat17(): void + { + $message1 = 'test message\rtest message 2\ntest message 3\r\ntest message 4'; + $message2 = 'test message 5\rtest message 6\ntest message 7\r\ntest message 8'; + $message3 = "test1\ntest2\rtest3\r\ntest4"; + $channel = 'test-channel'; + $tableStyle = 'default'; + $datetime = new DateTimeImmutable('now'); + $formattedMessage = 'this is a formatted message'; + $stdClass = new stdClass(); + $stdClass->a = $channel; + $stdClass->b = $message1; + $level = Level::Error; + $appName = 'test-app'; + + $expected = <<format(NormalizerFormatter::SIMPLE_DATE)} | + | Level | ERROR | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | Extra | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | app | test-app | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | Context | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | one | null | + | five | test1 | + | | test2 | + | | test3 | + | | test4 | + | six | stdClass | {"a":"test-channel","b":"test message | + | | | test message 2 | + | | | test message 3 | + | | | test message 4"} | + | seven | test message 5 | + | | test message 6 | + | | test message 7 | + | | test message 8 | + +----------------------+----------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + + TXT; + + $output = new BufferedOutput(); + $table = new Table($output); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: '%message% %context.one% %context.five% %context% %extra.app% %extra.app% %extra%', + tableStyle: $tableStyle, + dateFormat: null, + allowInlineLineBreaks: true, + ); + + $record = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level, + message: $message1, + context: ['one' => null, 'five' => $message3, 'six' => $stdClass, 'seven' => $message2], + extra: ['app' => $appName], + ); + + $lineFormatter = $this->getMockBuilder(LineFormatter::class) + ->disableOriginalConstructor() + ->getMock(); + $lineFormatter->expects(self::once()) + ->method('format') + ->with($record) + ->willReturn($formattedMessage); + + $formatter->setFormatter($lineFormatter); + + $formatted = $formatter->format($record); + + self::assertSame( + str_replace(["\r\n", "\r"], "\n", $expected), + str_replace(["\r\n", "\r"], "\n", $formatted), + ); + } + + /** + * @throws Exception + * @throws RuntimeException + */ + public function testFormatBatch(): void + { + $message = 'test message'; + $channel = 'test-channel'; + $tableStyle = StreamFormatter::BOX_STYLE; + $datetime = new DateTimeImmutable('now'); + $level1 = Level::Error; + $level2 = Level::Error; + $level3 = Level::Error; + + $expected1 = 'rendered-content-1'; + $expected2 = 'rendered-content-2'; + $expected3 = 'rendered-content-3'; + + $record1 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level1, + message: $message, + context: [], + extra: [], + ); + $record2 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: $level2, + message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz']], extra: ['app' => 'test-app'], ); $record3 = new LogRecord( datetime: $datetime, channel: $channel, - level: Level::Error, + level: $level3, message: $message, context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz'], 'five' => "test\ntest"], extra: ['app' => 'test-app'], @@ -1454,14 +3651,30 @@ public function testFormatBatch(): void $output->expects(self::exactly(6)) ->method('fetch') ->willReturnOnConsecutiveCalls('', $expected1, '', $expected2, '', $expected3); - $output->expects(self::exactly(15)) + $matcher = self::exactly(15); + $output->expects($matcher) ->method('writeln') - ->willReturnMap( - [ - [str_repeat('=', StreamFormatter::FULL_WIDTH), Output::OUTPUT_NORMAL, null], - ['', Output::OUTPUT_NORMAL, null], - [$message, Output::OUTPUT_NORMAL, null], - ], + ->willReturnCallback( + /** @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ + static function (string | iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL) use ($matcher, $message): void { + match ($matcher->numberOfInvocations()) { + 1, 6, 11 => self::assertSame( + str_repeat('=', StreamFormatter::FULL_WIDTH), + $messages, + (string) $matcher->numberOfInvocations(), + ), + 2, 4, 5, 7, 9, 10, 12, 14, 15 => self::assertSame( + '', + $messages, + (string) $matcher->numberOfInvocations(), + ), + default => self::assertSame( + $message, + $messages, + (string) $matcher->numberOfInvocations(), + ), + }; + }, ); $table = $this->getMockBuilder(Table::class) @@ -1484,12 +3697,240 @@ public function testFormatBatch(): void ->method('setRows') ->with([]) ->willReturnSelf(); - $table->expects(self::exactly(34)) - ->method('addRow'); + $matcher = self::exactly(34); + $table->expects($matcher) + ->method('addRow') + ->willReturnCallback( + static function (TableSeparator | array $row) use ($matcher, $table, $datetime, $level1, $level2, $level3): Table { + if ( + in_array( + $matcher->numberOfInvocations(), + [7, 9, 11, 13, 22, 24, 26, 28], + true, + ) + ) { + self::assertInstanceOf( + TableSeparator::class, + $row, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + self::assertIsArray($row, (string) $matcher->numberOfInvocations()); + + match ($matcher->numberOfInvocations()) { + 1, 4, 8, 12, 19, 23, 27 => self::assertCount( + 1, + $row, + (string) $matcher->numberOfInvocations(), + ), + 17, 32 => self::assertCount(3, $row, (string) $matcher->numberOfInvocations()), + default => self::assertCount(2, $row, (string) $matcher->numberOfInvocations()), + }; + + if ( + $matcher->numberOfInvocations() === 1 + || $matcher->numberOfInvocations() === 4 + || $matcher->numberOfInvocations() === 19 + ) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'General Info', + (string) $tableCell, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + if ( + $matcher->numberOfInvocations() === 2 + || $matcher->numberOfInvocations() === 5 + || $matcher->numberOfInvocations() === 20 + ) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'Time', + (string) $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + $datetime->format(NormalizerFormatter::SIMPLE_DATE), + (string) $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 3) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'Level', + (string) $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + $level1->getName(), + (string) $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 6) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'Level', + (string) $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + $level2->getName(), + (string) $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + if ($matcher->numberOfInvocations() === 21) { + $tableCell1 = $row[0]; + assert($tableCell1 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'Level', + (string) $tableCell1, + (string) $matcher->numberOfInvocations(), + ); + + $tableCell2 = $row[1]; + assert($tableCell2 instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + $level3->getName(), + (string) $tableCell2, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + if ( + $matcher->numberOfInvocations() === 8 + || $matcher->numberOfInvocations() === 23 + ) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'Extra', + (string) $tableCell, + (string) $matcher->numberOfInvocations(), + ); + + return $table; + } + + if ( + $matcher->numberOfInvocations() === 12 + || $matcher->numberOfInvocations() === 27 + ) { + $tableCell = $row[0]; + assert($tableCell instanceof TableCell); + + self::assertInstanceOf( + TableCell::class, + $tableCell, + (string) $matcher->numberOfInvocations(), + ); + self::assertSame( + 'Context', + (string) $tableCell, + (string) $matcher->numberOfInvocations(), + ); + } + + return $table; + }, + ); $table->expects(self::exactly(3)) ->method('render'); - $formatter = new StreamFormatter($output, $table); + $formatter = new StreamFormatter(output: $output, table: $table); $formatted = $formatter->formatBatch([$record1, $record2, $record3]); @@ -1537,7 +3978,7 @@ public function testFormatBatch2(): void ├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Context │ ├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ one │ NULL │ +│ one │ null │ │ two │ true │ │ three │ false │ │ four │ 0 │ abc │ @@ -1562,13 +4003,12 @@ public function testFormatBatch2(): void ├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Context │ ├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ one │ NULL │ +│ one │ null │ │ two │ true │ │ three │ false │ │ four │ 0 │ abc │ │ │ 1 │ xyz │ -│ five │ test │ -│ │ test │ +│ five │ test test │ └──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ '; @@ -1601,7 +4041,228 @@ public function testFormatBatch2(): void $output = new BufferedOutput(); $table = new Table($output); - $formatter = new StreamFormatter($output, $table, null, $tableStyle); + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: null, + tableStyle: $tableStyle, + ); + + $formatted = $formatter->formatBatch([$record1, $record2, $record3]); + + file_put_contents('output.txt', $formatted); + + self::assertSame( + str_replace("\r\n", "\n", $expected1 . $expected2 . $expected3), + str_replace("\r\n", "\n", $formatted), + ); + } + + /** + * @throws Exception + * @throws RuntimeException + */ + public function testFormatBatch3(): void + { + $message = 'test message'; + $channel = 'test-channel'; + $tableStyle = StreamFormatter::BOX_STYLE; + $datetime = new DateTimeImmutable('now'); + + $expected1 = '============================================================================================================================================================================================================================================================================== + +test message + +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ General Info │ +│ Time │ ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' │ +│ Level │ ERROR │ +└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +'; + $expected2 = '============================================================================================================================================================================================================================================================================== + +test message + +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ General Info │ +│ Time │ ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' │ +│ Level │ ERROR │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ Context │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ one │ null │ +│ two │ true │ +│ three │ false │ +│ four │ 0 │ abc │ +│ │ 1 │ xyz │ +└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +'; + $expected3 = '============================================================================================================================================================================================================================================================================== + +test message + +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ General Info │ +│ Time │ ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' │ +│ Level │ ERROR │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ Extra │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ app │ test-app │ +└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +'; + + $record1 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: Level::Error, + message: $message, + context: [], + extra: [], + ); + $record2 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: Level::Error, + message: $message, + context: ['one' => null, 'two' => true, 'three' => false, 'four' => ['abc', 'xyz']], + extra: [], + ); + $record3 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: Level::Error, + message: $message, + context: [], + extra: ['app' => 'test-app'], + ); + + $output = new BufferedOutput(); + $table = new Table($output); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: null, + tableStyle: $tableStyle, + ); + + $formatted = $formatter->formatBatch([$record1, $record2, $record3]); + + file_put_contents('output.txt', $formatted); + + self::assertSame( + str_replace("\r\n", "\n", $expected1 . $expected2 . $expected3), + str_replace("\r\n", "\n", $formatted), + ); + } + + /** + * @throws Exception + * @throws RuntimeException + */ + public function testFormatBatch4(): void + { + $message = ' test message '; + $channel = 'test-channel'; + $tableStyle = StreamFormatter::BOX_STYLE; + $datetime = new DateTimeImmutable('now'); + + $expected1 = '============================================================================================================================================================================================================================================================================== + +test message + +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ General Info │ +│ Time │ ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' │ +│ Level │ ERROR │ +└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +'; + $expected2 = '============================================================================================================================================================================================================================================================================== + +test message + +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ General Info │ +│ Time │ ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' │ +│ Level │ ERROR │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ Context │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ one │ null │ +│ two │ true │ +│ three │ false │ +│ four five │ 0 │ abc │ +│ │ 1 │ xyz │ +└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +'; + $expected3 = '============================================================================================================================================================================================================================================================================== + +test message + +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ General Info │ +│ Time │ ' . $datetime->format( + NormalizerFormatter::SIMPLE_DATE, + ) . ' │ +│ Level │ ERROR │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ Extra │ +├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ app │ test-app │ +└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + +'; + + $record1 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: Level::Error, + message: $message, + context: [], + extra: [], + ); + $record2 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: Level::Error, + message: $message, + context: ['one' => null, 'two' => true, 'three' => false, ' four_five ' => ['abc', 'xyz']], + extra: [], + ); + $record3 = new LogRecord( + datetime: $datetime, + channel: $channel, + level: Level::Error, + message: $message, + context: [], + extra: ['app' => 'test-app'], + ); + + $output = new BufferedOutput(); + $table = new Table($output); + + $formatter = new StreamFormatter( + output: $output, + table: $table, + format: null, + tableStyle: $tableStyle, + ); $formatted = $formatter->formatBatch([$record1, $record2, $record3]);