From b07d23b3786515e130521ea91ca756e6fff0218e Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Mon, 23 Oct 2023 11:23:36 -0300 Subject: [PATCH] Introduce value formatters --- .../DependencyInjection/Configuration.php | 28 ++++++++ .../SonataExporterExtension.php | 9 +++ .../Symfony/Resources/config/services.php | 7 ++ src/Formatter/BoolFormatter.php | 39 +++++++++++ src/Formatter/DateIntervalFormatter.php | 69 +++++++++++++++++++ src/Formatter/DateTimeFormatter.php | 35 ++++++++++ src/Formatter/EnumFormatter.php | 41 +++++++++++ src/Formatter/FormatterInterface.php | 19 +++++ src/Formatter/IterableFormatter.php | 49 +++++++++++++ src/Formatter/StringableFormatter.php | 30 ++++++++ src/Source/AbstractPropertySourceIterator.php | 16 ++--- src/Writer/CsvWriter.php | 24 ++++++- src/Writer/FormatAwareInterface.php | 21 ++++++ src/Writer/JsonWriter.php | 6 ++ src/Writer/WriterInterface.php | 3 + src/Writer/XlsWriter.php | 6 ++ src/Writer/XmlWriter.php | 5 ++ tests/Writer/CsvWriterTest.php | 36 ++++++++++ 18 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 src/Formatter/BoolFormatter.php create mode 100644 src/Formatter/DateIntervalFormatter.php create mode 100644 src/Formatter/DateTimeFormatter.php create mode 100644 src/Formatter/EnumFormatter.php create mode 100644 src/Formatter/FormatterInterface.php create mode 100644 src/Formatter/IterableFormatter.php create mode 100644 src/Formatter/StringableFormatter.php create mode 100644 src/Writer/FormatAwareInterface.php diff --git a/src/Bridge/Symfony/DependencyInjection/Configuration.php b/src/Bridge/Symfony/DependencyInjection/Configuration.php index 119200a0..ac8e3ee8 100644 --- a/src/Bridge/Symfony/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/DependencyInjection/Configuration.php @@ -75,6 +75,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(false) ->info('include the byte order mark') ->end() + ->arrayNode('formatters') + ->defaultValue($this->getDefaultFormatters()) + ->prototype('scalar')->end() + ->end() ->end() ->end() ->arrayNode('json') @@ -84,6 +88,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue('php://output') ->info('path to the output file') ->end() + ->arrayNode('formatters') + ->defaultValue($this->getDefaultFormatters()) + ->prototype('scalar')->end() + ->end() ->end() ->end() ->arrayNode('xls') @@ -97,6 +105,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(true) ->info('add column names as the first line') ->end() + ->arrayNode('formatters') + ->defaultValue($this->getDefaultFormatters()) + ->prototype('scalar')->end() + ->end() ->end() ->end() ->arrayNode('xlsx') @@ -114,6 +126,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(true) ->info('add filters in the first line') ->end() + ->arrayNode('formatters') + ->defaultValue($this->getDefaultFormatters()) + ->prototype('scalar')->end() + ->end() ->end() ->end() ->arrayNode('xml') @@ -135,6 +151,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue('data') ->info('name of elements corresponding to rows') ->end() + ->arrayNode('formatters') + ->defaultValue($this->getDefaultFormatters()) + ->prototype('scalar')->end() + ->end() ->end() ->end() ->end() @@ -157,4 +177,12 @@ private function getDefaultWriters(): array return $fields; } + + /** + * @return string[] + */ + private function getDefaultFormatters(): array + { + return ['datetime', 'dateinterval', 'bool', 'enum', 'stringable']; + } } diff --git a/src/Bridge/Symfony/DependencyInjection/SonataExporterExtension.php b/src/Bridge/Symfony/DependencyInjection/SonataExporterExtension.php index 326b212e..7c07a999 100644 --- a/src/Bridge/Symfony/DependencyInjection/SonataExporterExtension.php +++ b/src/Bridge/Symfony/DependencyInjection/SonataExporterExtension.php @@ -17,6 +17,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** @@ -60,6 +61,14 @@ private function configureExporter(ContainerBuilder $container, array $config): private function configureWriters(ContainerBuilder $container, array $config): void { foreach ($config as $format => $settings) { + if ($container->hasDefinition('sonata.exporter.writer.'.$format)) { + $writer = $container->getDefinition('sonata.exporter.writer.'.$format); + + foreach ($config[$format]['formatters'] as $formatter) { + $writer->addMethodCall('addFormatter', [new Reference('sonata.exporter.formatter.'.$formatter)]); + } + } + foreach ($settings as $key => $value) { $container->setParameter(sprintf( 'sonata.exporter.writer.%s.%s', diff --git a/src/Bridge/Symfony/Resources/config/services.php b/src/Bridge/Symfony/Resources/config/services.php index fc0b3d55..19e9b4e0 100644 --- a/src/Bridge/Symfony/Resources/config/services.php +++ b/src/Bridge/Symfony/Resources/config/services.php @@ -16,6 +16,8 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use Sonata\Exporter\Exporter; use Sonata\Exporter\ExporterInterface; +use Sonata\Exporter\Formatter\BoolFormatter; +use Sonata\Exporter\Formatter\EnumFormatter; use Sonata\Exporter\Writer\CsvWriter; use Sonata\Exporter\Writer\JsonWriter; use Sonata\Exporter\Writer\XlsWriter; @@ -67,4 +69,9 @@ $services->alias(Exporter::class, 'sonata.exporter.exporter'); $services->alias(ExporterInterface::class, 'sonata.exporter.exporter'); + + $services->set('sonata.exporter.formatter.bool', BoolFormatter::class) + ->tag('sonata.exporter.formatter'); + $services->set('sonata.exporter.formatter.enum', EnumFormatter::class) + ->tag('sonata.exporter.formatter'); }; diff --git a/src/Formatter/BoolFormatter.php b/src/Formatter/BoolFormatter.php new file mode 100644 index 00000000..acb8177a --- /dev/null +++ b/src/Formatter/BoolFormatter.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +final class BoolFormatter implements FormatterInterface +{ + private const LABEL_TRUE = 'yes'; + private const LABEL_FALSE = 'no'; + + public function __construct( + private string $trueLabel = self::LABEL_TRUE, + private string $falseLabel = self::LABEL_FALSE + ) { + } + + public function format(array $data): array + { + foreach ($data as $key => $value) { + if (!\is_bool($value)) { + continue; + } + + $data[$key] = $value ? $this->trueLabel : $this->falseLabel; + } + + return $data; + } +} diff --git a/src/Formatter/DateIntervalFormatter.php b/src/Formatter/DateIntervalFormatter.php new file mode 100644 index 00000000..0503b8ec --- /dev/null +++ b/src/Formatter/DateIntervalFormatter.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +final class DateIntervalFormatter implements FormatterInterface +{ + private const DATE_PARTS = [ + 'y' => 'Y', + 'm' => 'M', + 'd' => 'D', + ]; + private const TIME_PARTS = [ + 'h' => 'H', + 'i' => 'M', + 's' => 'S', + ]; + + public function format(array $data): array + { + foreach ($data as $key => $value) { + if (!$value instanceof \DateInterval) { + continue; + } + + $data[$key] = self::getDuration($value); + } + + return $data; + } + + /** + * @return string An ISO8601 duration + */ + private static function getDuration(\DateInterval $interval): string + { + $datePart = ''; + + foreach (self::DATE_PARTS as $datePartAttribute => $datePartAttributeString) { + if ($interval->$datePartAttribute !== 0) { + $datePart .= $interval->$datePartAttribute.$datePartAttributeString; + } + } + + $timePart = ''; + + foreach (self::TIME_PARTS as $timePartAttribute => $timePartAttributeString) { + if ($interval->$timePartAttribute !== 0) { + $timePart .= $interval->$timePartAttribute.$timePartAttributeString; + } + } + + if ('' === $datePart && '' === $timePart) { + return 'P0Y'; + } + + return 'P'.$datePart.('' !== $timePart ? 'T'.$timePart : ''); + } +} diff --git a/src/Formatter/DateTimeFormatter.php b/src/Formatter/DateTimeFormatter.php new file mode 100644 index 00000000..5198fbf8 --- /dev/null +++ b/src/Formatter/DateTimeFormatter.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +final class DateTimeFormatter implements FormatterInterface +{ + public function __construct( + private string $dateTimeFormat = \DateTimeInterface::RFC2822 + ) { + } + + public function format(array $data): array + { + foreach ($data as $key => $value) { + if (!$value instanceof \DateTimeInterface) { + continue; + } + + $data[$key] = $value->format($this->dateTimeFormat); + } + + return $data; + } +} diff --git a/src/Formatter/EnumFormatter.php b/src/Formatter/EnumFormatter.php new file mode 100644 index 00000000..9cbe7ceb --- /dev/null +++ b/src/Formatter/EnumFormatter.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +final class EnumFormatter implements FormatterInterface +{ + public function __construct( + private bool $useBackedEnumValue = true + ) { + } + + public function format(array $data): array + { + foreach ($data as $key => $value) { + if (!$value instanceof \UnitEnum) { + continue; + } + + if ($this->useBackedEnumValue && $value instanceof \BackedEnum) { + $data[$key] = $value->value; + + continue; + } + + $data[$key] = $value->name; + } + + return $data; + } +} diff --git a/src/Formatter/FormatterInterface.php b/src/Formatter/FormatterInterface.php new file mode 100644 index 00000000..fb97e4c3 --- /dev/null +++ b/src/Formatter/FormatterInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +interface FormatterInterface +{ + public function format(array $data): array; +} diff --git a/src/Formatter/IterableFormatter.php b/src/Formatter/IterableFormatter.php new file mode 100644 index 00000000..9b70bd9b --- /dev/null +++ b/src/Formatter/IterableFormatter.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +final class IterableFormatter implements FormatterInterface +{ + public function __construct( + private array $formatters = [] + ) { + } + + public function format(array $data, array $formatters = []): array + { + $this->formatters = $formatters; + foreach ($data as $key => $value) { + if (!is_iterable($value)) { + continue; + } + + if ($value instanceof \Iterator) { + $value = iterator_to_array($value); + } + + $data[$key] = '['.implode(', ', array_map([$this, 'formatFromIterable'], $value)).']'; + } + + return $data; + } + + private function formatFromIterable(mixed $value): mixed + { + foreach ($this->formatters as $formatter) { + $value = $formatter->format($value, $this->formatters); + } + + return $value; + } +} diff --git a/src/Formatter/StringableFormatter.php b/src/Formatter/StringableFormatter.php new file mode 100644 index 00000000..6f8977ba --- /dev/null +++ b/src/Formatter/StringableFormatter.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Formatter; + +final class StringableFormatter implements FormatterInterface +{ + public function format(array $data): array + { + foreach ($data as $key => $value) { + if (!$value instanceof \Stringable) { + continue; + } + + $data[$key] = (string) $value; + } + + return $data; + } +} diff --git a/src/Source/AbstractPropertySourceIterator.php b/src/Source/AbstractPropertySourceIterator.php index 830be23e..8c0abee4 100644 --- a/src/Source/AbstractPropertySourceIterator.php +++ b/src/Source/AbstractPropertySourceIterator.php @@ -130,16 +130,16 @@ protected function getCurrentData(object|array $current): array return $data; } - protected function getValue(mixed $value): bool|int|float|string|null + protected function getValue(mixed $value): bool|int|float|string|null|object { return match (true) { - \is_array($value) => '['.implode(', ', array_map([$this, 'getValue'], $value)).']', - $value instanceof \Traversable => '['.implode(', ', array_map([$this, 'getValue'], iterator_to_array($value))).']', - $value instanceof \DateTimeInterface => $value->format($this->dateTimeFormat), - $value instanceof \DateInterval => $this->getDuration($value), - $value instanceof \BackedEnum && $this->useBackedEnumValue => $value->value, - $value instanceof \UnitEnum => $value->name, - \is_object($value) => method_exists($value, '__toString') ? (string) $value : null, + // \is_array($value) => '['.implode(', ', array_map([$this, 'getValue'], $value)).']', + // $value instanceof \Traversable => '['.implode(', ', array_map([$this, 'getValue'], iterator_to_array($value))).']', + // $value instanceof \DateTimeInterface => $value->format($this->dateTimeFormat), + // $value instanceof \DateInterval => $this->getDuration($value), + // $value instanceof \BackedEnum && $this->useBackedEnumValue => $value->value, + // $value instanceof \UnitEnum => $value->name, + // \is_object($value) => method_exists($value, '__toString') ? (string) $value : null, default => $value, }; } diff --git a/src/Writer/CsvWriter.php b/src/Writer/CsvWriter.php index 2093ea52..2a02e312 100644 --- a/src/Writer/CsvWriter.php +++ b/src/Writer/CsvWriter.php @@ -14,12 +14,18 @@ namespace Sonata\Exporter\Writer; use Sonata\Exporter\Exception\InvalidDataFormatException; +use Sonata\Exporter\Formatter\FormatterInterface; /** * @author Thomas Rabaix */ -final class CsvWriter implements TypedWriterInterface +final class CsvWriter implements TypedWriterInterface, FormatAwareInterface { + /** + * @var array + */ + protected array $formatters = []; + /** * @var resource|null * @@ -59,6 +65,11 @@ public function getFormat(): string return 'csv'; } + public function addFormatter(FormatterInterface $formatter): void + { + $this->formatters[] = $formatter; + } + public function open(): void { $file = fopen($this->filename, 'w', false); @@ -105,7 +116,7 @@ public function write(array $data): void EXCEPTION); } - $result = @fputcsv($this->getFile(), $data, $this->delimiter, $this->enclosure, $this->escape); + $result = @fputcsv($this->getFile(), $this->format($data), $this->delimiter, $this->enclosure, $this->escape); if (false === $result) { throw new InvalidDataFormatException(); @@ -114,6 +125,15 @@ public function write(array $data): void ++$this->position; } + protected function format(array $data): array + { + foreach ($this->formatters as $formatter) { + $data = $formatter->format($data, $this->formatters); + } + + return $data; + } + /** * @return resource */ diff --git a/src/Writer/FormatAwareInterface.php b/src/Writer/FormatAwareInterface.php new file mode 100644 index 00000000..8f4714c1 --- /dev/null +++ b/src/Writer/FormatAwareInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\Exporter\Writer; + +use Sonata\Exporter\Formatter\FormatterInterface; + +interface FormatAwareInterface +{ + public function addFormatter(FormatterInterface $formatter): void; +} diff --git a/src/Writer/JsonWriter.php b/src/Writer/JsonWriter.php index 5980c2bc..1bed59cd 100644 --- a/src/Writer/JsonWriter.php +++ b/src/Writer/JsonWriter.php @@ -13,6 +13,8 @@ namespace Sonata\Exporter\Writer; +use Sonata\Exporter\Formatter\FormatterInterface; + /** * @author Thomas Rabaix */ @@ -48,6 +50,10 @@ public function getFormat(): string return 'json'; } + public function addFormatter(FormatterInterface $formatter): void + { + } + public function open(): void { $file = fopen($this->filename, 'w', false); diff --git a/src/Writer/WriterInterface.php b/src/Writer/WriterInterface.php index 87a3cf72..962875b7 100644 --- a/src/Writer/WriterInterface.php +++ b/src/Writer/WriterInterface.php @@ -14,6 +14,7 @@ namespace Sonata\Exporter\Writer; use Sonata\Exporter\Exception\SonataExporterException; +use Sonata\Exporter\Formatter\FormatterInterface; interface WriterInterface { @@ -30,4 +31,6 @@ public function open(): void; public function write(array $data): void; public function close(): void; + + public function addFormatter(FormatterInterface $formatter): void; } diff --git a/src/Writer/XlsWriter.php b/src/Writer/XlsWriter.php index 40d3b9f4..9c03c7e7 100644 --- a/src/Writer/XlsWriter.php +++ b/src/Writer/XlsWriter.php @@ -13,6 +13,8 @@ namespace Sonata\Exporter\Writer; +use Sonata\Exporter\Formatter\FormatterInterface; + /** * @author Thomas Rabaix */ @@ -50,6 +52,10 @@ public function getFormat(): string return 'xls'; } + public function addFormatter(FormatterInterface $formatter): void + { + } + public function open(): void { $file = fopen($this->filename, 'w', false); diff --git a/src/Writer/XmlWriter.php b/src/Writer/XmlWriter.php index d241c03c..f94aba25 100644 --- a/src/Writer/XmlWriter.php +++ b/src/Writer/XmlWriter.php @@ -15,6 +15,7 @@ use Sonata\Exporter\Exception\InvalidDataFormatException; use Sonata\Exporter\Exception\RuntimeException; +use Sonata\Exporter\Formatter\FormatterInterface; /** * @author Thomas Rabaix @@ -52,6 +53,10 @@ public function getFormat(): string return 'xml'; } + public function addFormatter(FormatterInterface $formatter): void + { + } + public function open(): void { $file = fopen($this->filename, 'w', false); diff --git a/tests/Writer/CsvWriterTest.php b/tests/Writer/CsvWriterTest.php index d85a524c..0d16b807 100644 --- a/tests/Writer/CsvWriterTest.php +++ b/tests/Writer/CsvWriterTest.php @@ -15,6 +15,11 @@ use PHPUnit\Framework\TestCase; use Sonata\Exporter\Exception\InvalidDataFormatException; +use Sonata\Exporter\Formatter\BoolFormatter; +use Sonata\Exporter\Formatter\DateTimeFormatter; +use Sonata\Exporter\Formatter\EnumFormatter; +use Sonata\Exporter\Formatter\IterableFormatter; +use Sonata\Exporter\Tests\Source\Fixtures\Suit; use Sonata\Exporter\Writer\CsvWriter; final class CsvWriterTest extends TestCase @@ -145,4 +150,35 @@ public function testWithBom(): void static::assertIsString($content); static::assertSame($expected, trim($content)); } + + public function testValueFormatting(): void + { + $writer = new CsvWriter($this->filename, ',', '"', '\\', false); + $writer->addFormatter(new BoolFormatter()); + $writer->addFormatter(new DateTimeFormatter()); + $writer->addFormatter(new EnumFormatter()); + $writer->addFormatter(new IterableFormatter()); + $writer->open(); + + $writer->write([ + ' john , ""2"', + 'doe', + '1', + true, + new \DateTimeImmutable('1986-03-22 19:45:54', new \DateTimeZone('America/Argentina/Buenos_Aires')), + Suit::Hearts, + [ + 'foo' => ['bool', 'float'], + 'bar' => ['string', 'int'], + ], + ]); + + $writer->close(); + + $expected = '" john , """"2""",doe,1,yes,"Sat, 22 Mar 1986 19:45:54 -0300",H,"[Array, Array]"'; + + $content = file_get_contents($this->filename); + static::assertIsString($content); + static::assertSame($expected, trim($content)); + } }