diff --git a/CsvTable.php b/CsvTable.php index 5ea8e81..11097ec 100644 --- a/CsvTable.php +++ b/CsvTable.php @@ -162,10 +162,11 @@ public static function fromFile($filepath, $separator = ',', $enclosure = '"', $ /** * Format the CSV data. * - * @param callable|string|null $formatter - * A callable to formatter the output. Can be a function name, a class name, - * a closure, or an array containing a class name and a method name. If NULL - * is provided, the default formatter will be used. + * @param string|array|callable|null $formatter + * Formatter to use. Can be a function name, a class name, a closure, an + * array containing a class name and a method name, or a predefined + * formatter available as format method. If NULL is provided, the + * 'CSV" formatter will be used. * @param array $options * An array of options to pass to the formatter. Defaults to an empty array. * @@ -175,9 +176,18 @@ public static function fromFile($filepath, $separator = ',', $enclosure = '"', $ * @throws \Exception * When the formatter is not callable. */ - public function format(callable|string|null $formatter = NULL, array $options = []): string { - $formatter = $formatter ?? [static::class, 'formatCsv']; - $formatter = is_string($formatter) && class_exists($formatter) ? [$formatter, 'format'] : $formatter; + public function format(string|array|callable|null $formatter = NULL, array $options = []): string { + $formatter = $formatter ?? 'csv'; + + if (is_string($formatter) && !function_exists($formatter)) { + if (class_exists($formatter)) { + $formatter = [$formatter, 'format']; + } + else { + $method = 'format' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', strtolower($formatter)))); + $formatter = method_exists($this, $method) ? [$this, $method] : NULL; + } + } if (!is_callable($formatter)) { throw new \Exception('Formatter must be callable.'); diff --git a/README.md b/README.md index 6b0adae..ddb255e 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,10 @@ col21,col22,col23 col31,col32,col33 ``` -### Using `CsvTable::formatTextTable()` formatter +### Using `text_table` formatter ```php -print (CsvTable::fromFile($file))->format([CsvTable::class, 'formatTextTable']); +print (CsvTable::fromFile($file))->format('text_table'); ``` will produce table content: ```csv @@ -79,10 +79,10 @@ col21|col22|col23 col31|col32|col33 ``` -### Using `CsvTable::formatTextTable()` formatter without a header +### Using `text_table` formatter without a header ```php -print (CsvTable::fromFile($file))->withoutHeader()->format([CsvTable::class, 'formatTextTable']); +print (CsvTable::fromFile($file))->withoutHeader()->format('text_table'); ``` will produce table content: ```csv @@ -91,10 +91,10 @@ col21|col22|col23 col31|col32|col33 ``` -### Using `CsvTable::formatMarkdownTable()` formatter +### Using `markdown_table` formatter ```php -print (CsvTable::fromFile($file))->withoutHeader()->format([CsvTable::class, 'formatMarkdownTable']); +print (CsvTable::fromFile($file))->withoutHeader()->format('markdown_table'); ``` will produce Markdown table: ```markdown @@ -104,7 +104,7 @@ will produce Markdown table: | col31 | col32 | col33 | ``` -### Custom formatter as a callback +### Custom formatter as an anonymous callback ```php print (CsvTable::fromFile($file))->format(function ($header, $rows, $options) { @@ -128,6 +128,19 @@ col21|col22|col23 col31|col32|col33 ``` +### Custom formatter as a class with default `format` method + +```php +print (CsvTable::fromFile($file))->withoutHeader()->format(CustomFormatter::class); +``` + +### Custom formatter as a class with a custom method and options + +```php +$formatter_options = ['option1' => 'value1', 'option2' => 'value2']; +print (CsvTable::fromFile($file))->withoutHeader()->format([CustomFormatter::class, 'customFormat'], $formatter_options); +``` + ## Maintenance ```bash diff --git a/composer.json b/composer.json index be5d0da..c3d59f9 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,11 @@ "AlexSkrypnyk\\CsvTable\\": "" } }, + "autoload-dev": { + "psr-4": { + "AlexSkrypnyk\\CsvTable\\Tests\\": "tests" + } + }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, diff --git a/tests/CsvTableUnitTest.php b/tests/CsvTableUnitTest.php index 9b4cba2..42a5831 100644 --- a/tests/CsvTableUnitTest.php +++ b/tests/CsvTableUnitTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace AlexSkrypnyk\CsvTable\Tests; + use AlexSkrypnyk\CsvTable\CsvTable; use PHPUnit\Framework\TestCase; @@ -30,48 +32,6 @@ protected static function fixtureCsv(): string { EOD; } - /** - * Test the default behavior using default formatCsv() formatter. - * - * @dataProvider dataProviderDefault - * @group wip3 - */ - public function testDefault(string $csv, bool|null $with_header, string $expected): void { - $table = new CsvTable($csv); - - // Allows to assert default behavior. - if (!is_null($with_header)) { - if ($with_header) { - $table->withHeader(); - } - else { - $table->withoutHeader(); - } - } - - $actual = $table->format(); - - $this->assertEquals($expected, $actual); - } - - /** - * Data provider for testDefault(). - * - * @return array - * Data provider - */ - public static function dataProviderDefault(): array { - return [ - ['', NULL, ''], - ['', TRUE, ''], - ['', FALSE, ''], - - [self::fixtureCsv(), NULL, self::fixtureCsv()], - [self::fixtureCsv(), TRUE, self::fixtureCsv()], - [self::fixtureCsv(), FALSE, self::fixtureCsv()], - ]; - } - /** * Test getters. * @@ -103,66 +63,80 @@ public function testGetters(): void { } /** - * Test doFormat() formatter. + * Test creating of the class instance using fromFile(). */ - public function testFormatTable(): void { + public function testFromFile(): void { $csv = self::fixtureCsv(); + $file = tempnam(sys_get_temp_dir(), 'csv'); + file_put_contents((string) $file, $csv); - $actual = (new CsvTable($csv)) - ->format([CsvTable::class, 'formatTable']); - - $this->assertEquals(<<< EOD - col11|col12|col13 - ----------------- - col21|col22|col23 - col31|col32|col33 - EOD, $actual); + $actual = (CsvTable::fromFile((string) $file))->format(); + $this->assertEquals($csv, $actual); - $actual = (new CsvTable($csv)) - ->withoutHeader() - ->format([CsvTable::class, 'formatTable']); - $this->assertEquals(<<< EOD - col11|col12|col13 - col21|col22|col23 - col31|col32|col33 - EOD, $actual); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unable to read the file non-existing-file.csv'); + CsvTable::fromFile('non-existing-file.csv'); } /** - * Test pass not callable to format(). + * Test the default behavior using default formatCsv() formatter. + * + * @dataProvider dataProviderFormatterDefault */ - public function testFormatNotCallable(): void { - $csv = self::fixtureCsv(); + public function testFormatterDefault(string $csv, bool|null $with_header, string $expected): void { + $table = new CsvTable($csv); - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Formatter must be callable.'); - (new CsvTable($csv))->format('Not callable'); + // Allows to assert default behavior. + if (!is_null($with_header)) { + if ($with_header) { + $table->withHeader(); + } + else { + $table->withoutHeader(); + } + } + + $actual = $table->format(); + + $this->assertEquals($expected, $actual); } /** - * Test using a custom formatter. + * Data provider for testFormatterDefault(). + * + * @return array + * Data provider */ - public function testCustomFormatter(): void { - $csv = self::fixtureCsv(); + public static function dataProviderFormatterDefault(): array { + return [ + ['', NULL, ''], + ['', TRUE, ''], + ['', FALSE, ''], - $custom_formatter = static function ($header, $rows): string { - $output = ''; + [self::fixtureCsv(), NULL, self::fixtureCsv()], + [self::fixtureCsv(), TRUE, self::fixtureCsv()], + [self::fixtureCsv(), FALSE, self::fixtureCsv()], + ]; + } - if (count($header) > 0) { - $output = implode('|', $header); - $output .= "\n" . str_repeat('=', strlen($output)) . "\n"; - } + /** + * Test table formatter. + */ + public function testFormatterTable(): void { + $csv = self::fixtureCsv(); - return $output . implode("\n", array_map(static function ($row): string { - return implode('|', $row); - }, $rows)); - }; + $actual = (new CsvTable($csv))->format('table'); - $actual = (new CsvTable($csv))->format($custom_formatter); + $this->assertEquals(<<< EOD + col11|col12|col13 + ----------------- + col21|col22|col23 + col31|col32|col33 + EOD, $actual); + $actual = (new CsvTable($csv))->withoutHeader()->format('table'); $this->assertEquals(<<< EOD col11|col12|col13 - ================= col21|col22|col23 col31|col32|col33 EOD, $actual); @@ -171,7 +145,7 @@ public function testCustomFormatter(): void { /** * Test custom CSV separator. */ - public function testCustomCsvSeparator(): void { + public function testFormatterCsvSeparator(): void { $csv = self::fixtureCsv(); $csv_updated = str_replace(',', ';', self::fixtureCsv()); @@ -187,7 +161,7 @@ public function testCustomCsvSeparator(): void { /** * Test support for CSV multiline. */ - public function testCustomCsvMultiline(): void { + public function testFormatterCsvMultiline(): void { $csv = <<< EOD col11,col12,col13 col21,"col22\ncol22secondline",col23 @@ -197,37 +171,17 @@ public function testCustomCsvMultiline(): void { $this->assertEquals($csv, $actual); } - /** - * Test creating of the class instance using fromFile(). - * - * @throws Exception - */ - public function testFromFile(): void { - $csv = self::fixtureCsv(); - $file = tempnam(sys_get_temp_dir(), 'csv'); - file_put_contents((string) $file, $csv); - - $actual = (CsvTable::fromFile((string) $file))->format(); - $this->assertEquals($csv, $actual); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Unable to read the file non-existing-file.csv'); - CsvTable::fromFile('non-existing-file.csv'); - } - /** * Test formatMarkdownTable(). - * - * @group wip1 */ - public function testFormatMarkdownTable(): void { + public function testFormatterMarkdownTable(): void { $csv = <<< EOD col11a,col12ab,col13abc col21a,"col22ab cde",col23abc col31a,col32ab,"col33abcd" EOD; - $actual = (new CsvTable($csv))->format([CsvTable::class, 'formatMarkdownTable']); + $actual = (new CsvTable($csv))->format('markdown_table'); $this->assertEquals(<<< EOD | col11a | col12ab | col13abc | @@ -239,16 +193,16 @@ public function testFormatMarkdownTable(): void { } /** - * Test formatMarkdownTable() for multiline. + * Test Markdown table formatter for multiline. */ - public function testFormatMarkdownTableMultiline(): void { + public function testFormatterMarkdownTableMultiline(): void { $csv = <<< EOD col11a,col12ab,col13abc col21a,"col22ab\ncdef",col23abc col31a,col32ab,col33abcd EOD; - $actual = (new CsvTable($csv))->format([CsvTable::class, 'formatMarkdownTable']); + $actual = (new CsvTable($csv))->format('markdown_table'); $this->assertEquals(<<< EOD | col11a | col12ab | col13abc | @@ -260,16 +214,16 @@ public function testFormatMarkdownTableMultiline(): void { } /** - * Test formatMarkdownTable() for multiline and no header. + * Test Markdown table formatter without header. */ - public function testFormatMarkdownTableMultilineNoHeader(): void { + public function testFormatterMarkdownTableMultilineNoHeader(): void { $csv = <<< EOD col11a,col12ab,col13abc col21a,"col22ab\ncdef",col23abc col31a,col32ab,col33abcd EOD; - $actual = (new CsvTable($csv))->withoutHeader()->format([CsvTable::class, 'formatMarkdownTable']); + $actual = (new CsvTable($csv))->withoutHeader()->format('markdown_table'); $this->assertEquals(<<< EOD | col11a | col12ab | col13abc | @@ -280,18 +234,16 @@ public function testFormatMarkdownTableMultilineNoHeader(): void { } /** - * Test formatMarkdownTable() for custom separators. - * - * @group wip2 + * Test Markdown table formatter with custom separators. */ - public function testFormatMarkdownTableCustomSeparators(): void { + public function testFormatterMarkdownTableCustomSeparators(): void { $csv = <<< EOD col11a,col12ab,col13abc col21a,"col22ab cde",col23abc col31a,col32ab,"col33abcd" EOD; - $actual = (new CsvTable($csv))->format([CsvTable::class, 'formatMarkdownTable'], [ + $actual = (new CsvTable($csv))->format('markdown_table', [ 'column_separator' => '|', 'row_separator' => "\n", 'header_separator' => '=', @@ -306,4 +258,76 @@ public function testFormatMarkdownTableCustomSeparators(): void { EOD, $actual); } + /** + * Test pass not callable to format(). + */ + public function testFormatterCustomNotCallable(): void { + $csv = self::fixtureCsv(); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Formatter must be callable.'); + (new CsvTable($csv))->format('Not callable'); + } + + /** + * Test using a custom formatter function. + */ + public function testFormatterCustomFunction(): void { + $csv = self::fixtureCsv(); + + $custom_formatter = static function ($header, $rows): string { + $output = ''; + + if (count($header) > 0) { + $output = implode('|', $header); + $output .= "\n" . str_repeat('=', strlen($output)) . "\n"; + } + + return $output . implode("\n", array_map(static function ($row): string { + return implode('|', $row); + }, $rows)); + }; + + $actual = (new CsvTable($csv))->format($custom_formatter); + + $this->assertEquals(<<< EOD + col11|col12|col13 + ================= + col21|col22|col23 + col31|col32|col33 + EOD, $actual); + } + + /** + * Test using a custom formatter class with default callback. + */ + public function testFormatterCustomClassDefaultCallback(): void { + $csv = self::fixtureCsv(); + + $actual = (new CsvTable($csv))->format(TestFormatter::class); + + $this->assertEquals(<<< EOD + col11|col12|col13 + ================= + col21|col22|col23 + col31|col32|col33 + EOD, $actual); + } + + /** + * Test using a custom formatter class with custom callback. + */ + public function testFormatterCustomClassCustomCallback(): void { + $csv = self::fixtureCsv(); + + $actual = (new CsvTable($csv))->format([TestFormatter::class, 'customFormat']); + + $this->assertEquals(<<< EOD + col11!col12!col13 + ================= + col21!col22!col23 + col31!col32!col33 + EOD, $actual); + } + } diff --git a/tests/TestFormatter.php b/tests/TestFormatter.php new file mode 100644 index 0000000..4b92b53 --- /dev/null +++ b/tests/TestFormatter.php @@ -0,0 +1,57 @@ + $header + * The header. + * @param array> $rows + * The rows. + * @param array $options + * The options. + * + * @return string + * The formatted table. + */ + public static function format(array $header, array $rows, array $options = []): string { + $output = ''; + + $options += [ + 'delimiter' => '|', + ]; + + if (count($header) > 0) { + $output = implode($options['delimiter'], $header); + $output .= "\n" . str_repeat('=', strlen($output)) . "\n"; + } + + return $output . implode("\n", array_map(static function ($row) use ($options): string { + return implode($options['delimiter'], $row); + }, $rows)); + } + + /** + * Format a table with a custom delimiter. + * + * @param array $header + * The header. + * @param array> $rows + * The rows. + * + * @return string + * The formatted table. + */ + public static function customFormat(array $header, array $rows): string { + return static::format($header, $rows, ['delimiter' => '!']); + } + +}