diff --git a/config/feeds.php b/config/feeds.php index 419565e..cc47380 100644 --- a/config/feeds.php +++ b/config/feeds.php @@ -113,4 +113,17 @@ Transformers\EnumTransformer::class, // Transformers\NullTransformer::class, ], + + /** + * Converters define low-level serialization settings for specific output + * formats. You can tweak encoder flags and other options here. + */ + 'converters' => [ + 'json' => [ + /** + * JSON encoding flags used when exporting feeds to JSON. + */ + 'options' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, + ], + ], ]; diff --git a/docs/labels.list b/docs/labels.list new file mode 100644 index 0000000..17544cc --- /dev/null +++ b/docs/labels.list @@ -0,0 +1,14 @@ + + + + + Available for XML feeds + + + + Available for JSON feeds + + + diff --git a/docs/laravel-feeds.tree b/docs/laravel-feeds.tree index ff8b0c3..ae75efe 100644 --- a/docs/laravel-feeds.tree +++ b/docs/laravel-feeds.tree @@ -12,6 +12,7 @@ + diff --git a/docs/snippets/advanced-directive-array.xml b/docs/snippets/advanced-directive-array.xml index 0c874ff..8e118ea 100644 --- a/docs/snippets/advanced-directive-array.xml +++ b/docs/snippets/advanced-directive-array.xml @@ -2,18 +2,18 @@ - Miss Candace Fadel DVM - https://via.placeholder.com/640x480.png/002200?text=occaecati - https://via.placeholder.com/640x480.png/00ee99?text=odio - https://via.placeholder.com/640x480.png/006611?text=numquam - https://via.placeholder.com/640x480.png/004400?text=quibusdam + Giles Graham + https://via.placeholder.com/640x480.png/0066dd?text=ullam + https://via.placeholder.com/640x480.png/0033ff?text=autem + https://via.placeholder.com/640x480.png/00ddaa?text=voluptatibus + https://via.placeholder.com/640x480.png/00cc55?text=in - Mavis Botsford Sr. - https://via.placeholder.com/640x480.png/0000ee?text=aut - https://via.placeholder.com/640x480.png/0077dd?text=ut - https://via.placeholder.com/640x480.png/001155?text=quidem - https://via.placeholder.com/640x480.png/00aaff?text=eum + Katelyn Harber + https://via.placeholder.com/640x480.png/0066dd?text=voluptatem + https://via.placeholder.com/640x480.png/004488?text=quaerat + https://via.placeholder.com/640x480.png/006655?text=ipsam + https://via.placeholder.com/640x480.png/007788?text=nam diff --git a/docs/snippets/advanced-directive-attributes.xml b/docs/snippets/advanced-directive-attributes.xml index 2919b00..ac83283 100644 --- a/docs/snippets/advanced-directive-attributes.xml +++ b/docs/snippets/advanced-directive-attributes.xml @@ -1,16 +1,16 @@ - + https://example.com - Jules Herman - + Melvina Beer + - Donavon Borer - + Vivienne Willms + diff --git a/docs/snippets/advanced-directive-cdata.xml b/docs/snippets/advanced-directive-cdata.xml index 49b13c7..0ae74b7 100644 --- a/docs/snippets/advanced-directive-cdata.xml +++ b/docs/snippets/advanced-directive-cdata.xml @@ -2,12 +2,12 @@ - Deborah Koelpin]]> - dorris.keeling@example.com + Elmo Hilpert]]> + alex.mohr@example.net - Providenci Bednar]]> - amari30@example.com + Golden Sawayn]]> + jamison.ritchie@example.net diff --git a/docs/snippets/advanced-directive-mixed.xml b/docs/snippets/advanced-directive-mixed.xml index 877eb62..a108905 100644 --- a/docs/snippets/advanced-directive-mixed.xml +++ b/docs/snippets/advanced-directive-mixed.xml @@ -2,17 +2,17 @@ - Celestino Ledner + Dr. Hudson Waters Foo - katheryn80@example.net + macejkovic.lois@example.net - Dr. Tyrel Walter MD + Scarlett Walter Foo - maggie70@example.net + ahaag@example.org diff --git a/docs/snippets/advanced-directive-value.xml b/docs/snippets/advanced-directive-value.xml index 480789a..6902461 100644 --- a/docs/snippets/advanced-directive-value.xml +++ b/docs/snippets/advanced-directive-value.xml @@ -2,12 +2,12 @@ - Ignatius Schroeder - weimann.maegan@example.org + Eldon Lind IV + rlarkin@example.org - Dr. William Wintheiser II - gottlieb.curt@example.com + Miss Marcia Ebert Jr. + mayert.zackary@example.net diff --git a/docs/snippets/advanced-element-attribute.xml b/docs/snippets/advanced-element-attribute.xml index 9116715..5fa8a6f 100644 --- a/docs/snippets/advanced-element-attribute.xml +++ b/docs/snippets/advanced-element-attribute.xml @@ -1,13 +1,13 @@ - + 1 - Mr. Vincenzo Daugherty + Dr. Rory Streich I - + 2 - Dolly Abernathy + Dr. Nikko Oberbrunner diff --git a/docs/snippets/advanced-element-header-footer.xml b/docs/snippets/advanced-element-header-footer.xml index c9b1ccd..02ecc84 100644 --- a/docs/snippets/advanced-element-header-footer.xml +++ b/docs/snippets/advanced-element-header-footer.xml @@ -3,11 +3,11 @@ 1 - Layla Wisoky PhD + Ryann Effertz 2 - Pink Lang + Emely Ziemann MD diff --git a/docs/snippets/advanced-element-info-before-false.xml b/docs/snippets/advanced-element-info-before-false.xml index a25be86..085bf9a 100644 --- a/docs/snippets/advanced-element-info-before-false.xml +++ b/docs/snippets/advanced-element-info-before-false.xml @@ -6,11 +6,11 @@ 1 - Noe Ankunding + Suzanne Stehr 2 - Lucy Auer + Buddy Cruickshank DDS diff --git a/docs/snippets/advanced-element-info.xml b/docs/snippets/advanced-element-info.xml index 9f0f505..4d10eb1 100644 --- a/docs/snippets/advanced-element-info.xml +++ b/docs/snippets/advanced-element-info.xml @@ -6,11 +6,11 @@ 1 - Keith Jenkins + Dejuan Schoen DDS 2 - Brittany Heller IV + Miss Delilah Hartmann diff --git a/docs/snippets/advanced-element-root.xml b/docs/snippets/advanced-element-root.xml index ccf0ce1..2eb7719 100644 --- a/docs/snippets/advanced-element-root.xml +++ b/docs/snippets/advanced-element-root.xml @@ -3,11 +3,11 @@ 1 - Mr. Newton Kilback + Neal Rosenbaum 2 - May Feeney + Maryse Terry diff --git a/docs/snippets/create-feeds-feed-format.php b/docs/snippets/create-feeds-feed-format.php new file mode 100644 index 0000000..e23206b --- /dev/null +++ b/docs/snippets/create-feeds-feed-format.php @@ -0,0 +1,19 @@ + 1 - - - https://example.com/products/repudiandae-soluta-qui-et-vel-corporis - https://via.placeholder.com/640x480.png/007788?text=et - https://via.placeholder.com/640x480.png/00ff55?text=voluptatum - https://via.placeholder.com/640x480.png/00ffee?text=natus - quidem + + + https://example.com/products/aut-dolor-et-consequuntur-possimus-eos + https://via.placeholder.com/640x480.png/002222?text=et + https://via.placeholder.com/640x480.png/006677?text=maxime + https://via.placeholder.com/640x480.png/008866?text=recusandae + sapiente new in stock - 129 - 129 + 799 + 799 12345 active - - 45 + + 38 adult - + 1000 2000 2 - - - https://example.com/products/veniam-iste-in-explicabo-voluptas - https://via.placeholder.com/640x480.png/009999?text=eos - https://via.placeholder.com/640x480.png/002288?text=deleniti - https://via.placeholder.com/640x480.png/0044aa?text=dolore - cupiditate + + + https://example.com/products/illum-occaecati-corrupti-voluptate + https://via.placeholder.com/640x480.png/0055cc?text=sunt + https://via.placeholder.com/640x480.png/0099cc?text=sint + https://via.placeholder.com/640x480.png/00ccff?text=sequi + nostrum new in stock - 438 - 438 + 228 + 228 12345 active - - 24 + + 11 adult - + 1000 2000 diff --git a/docs/snippets/receipt-sitemap-feed.xml b/docs/snippets/receipt-sitemap-feed.xml index a36591c..7f35014 100644 --- a/docs/snippets/receipt-sitemap-feed.xml +++ b/docs/snippets/receipt-sitemap-feed.xml @@ -2,12 +2,12 @@ - https://example.com/products/error-velit-enim-voluptatum-doloremque-id + https://example.com/products/aperiam-voluptatem-atque-quia-et 2025-09-04T04:08:12+00:00 0.9 - https://example.com/products/quod-in-quae-rerum-totam + https://example.com/products/eligendi-aperiam-est-fugiat-nobis-consequatur-harum-nihil 2025-09-04T04:08:12+00:00 0.9 diff --git a/docs/snippets/receipt-yandex-feed.xml b/docs/snippets/receipt-yandex-feed.xml index 19392fa..1bf1309 100644 --- a/docs/snippets/receipt-yandex-feed.xml +++ b/docs/snippets/receipt-yandex-feed.xml @@ -16,36 +16,36 @@ - - https://example.com/products/adipisci-similique-est-sed-fugiat-cum-rerum-ex - GD-,PK?H - asperiores dolorem voluptatem in - Et quia eum voluptatibus et. Non iusto id explicabo. Deserunt quaerat est fuga harum rerum deserunt. + + https://example.com/products/velit-rerum-et-omnis-provident-rerum-inventore-est + GD-N~_Z(N + et et eligendi aliquam + Itaque esse qui tempora non. Illum et quia cum ea quo est. Sit animi et voluptate voluptatem voluptates animi. At illum asperiores vero animi. true - 103 + 957 RUR - modi - https://via.placeholder.com/640x480.png/009922?text=quaerat - https://via.placeholder.com/640x480.png/00cc44?text=corporis - https://via.placeholder.com/640x480.png/0088ee?text=tempora - GD-,PK?H - 2 + porro + https://via.placeholder.com/640x480.png/00bbcc?text=possimus + https://via.placeholder.com/640x480.png/00ffdd?text=qui + https://via.placeholder.com/640x480.png/005511?text=ullam + GD-N~_Z(N + 6 male - https://example.com/products/aspernatur-asperiores-fuga-explicabo-quo-sit-quia-qui - GD-#>XKQYVO - cumque a aut qui - Sed officiis natus commodi quis aut. Dicta molestiae distinctio dolores. Nemo velit rerum voluptatem perferendis iusto quia omnis. Autem voluptatum minus voluptatem occaecati eius. + https://example.com/products/deserunt-odio-earum-neque-ipsum-alias-magni + GD-'B'"SS + est veritatis officiis quae + Et velit libero dolorem sed est. Repellendus commodi tempore reiciendis quo. Cupiditate reprehenderit dolor molestiae nulla voluptas. Dolorem alias a architecto est quas dolore quas iste. true - 802 + 844 RUR - maxime - https://via.placeholder.com/640x480.png/0055dd?text=aperiam - https://via.placeholder.com/640x480.png/001144?text=velit - https://via.placeholder.com/640x480.png/008877?text=consequatur - GD-#>XKQYVO - 7 + doloribus + https://via.placeholder.com/640x480.png/0099ee?text=quo + https://via.placeholder.com/640x480.png/0044ee?text=expedita + https://via.placeholder.com/640x480.png/007777?text=fugiat + GD-'B'"SS + 5 female diff --git a/docs/topics/advanced-usage.topic b/docs/topics/advanced-usage.topic index 72961d0..bb79d6f 100644 --- a/docs/topics/advanced-usage.topic +++ b/docs/topics/advanced-usage.topic @@ -12,10 +12,7 @@ - -

- Reserved directives: -

+
  • @@ -23,10 +20,17 @@
  • + +

    + The use of directives is available only when generating feeds in the XML format. +

    + + +

    By default, the name of the root element is derived from the base name of the feed class. @@ -56,6 +60,9 @@ + + +

    To add information to the beginning of the root element (if present) or without it, override the @@ -87,6 +94,9 @@ + + +

    To change the header and footer, override the header and footer methods:

    @@ -101,6 +111,8 @@
    + + @@ -114,6 +126,8 @@ + + Indicates an array of attributes

    @@ -132,6 +146,8 @@ + + Indicates the applicable value "as it is" @@ -152,6 +168,8 @@ + + Allows the use of XML markup without transformation @@ -173,6 +191,8 @@ + + Allows you to use XML markup directly within the structure of the document

    @@ -189,6 +209,8 @@ + + Describes the possibility of listing an array of elements with the same keys

    diff --git a/docs/topics/create-feeds.topic b/docs/topics/create-feeds.topic index afe246b..04d955b 100644 --- a/docs/topics/create-feeds.topic +++ b/docs/topics/create-feeds.topic @@ -100,4 +100,24 @@ + + + + + +

    + You can generate feeds in different formats. + A feed class can be correctly exported only to the format for which it is intended. + This is due to the use of + directives and other format specifics. +

    + +

    + To apply the required format, simply override the $format property in the feed class: +

    + + + + + diff --git a/docs/topics/supported-formats.topic b/docs/topics/supported-formats.topic new file mode 100644 index 0000000..a1c7400 --- /dev/null +++ b/docs/topics/supported-formats.topic @@ -0,0 +1,25 @@ + + + + + List of supported feed export formats + List of supported feed export formats + List of supported feed export formats + + + + + +
  • xml
  • +
  • json
  • +
    + +

    + By default, the xml format is used. +

    +
    +
    diff --git a/src/Converters/ConvertToJson.php b/src/Converters/ConvertToJson.php new file mode 100644 index 0000000..8eb796b --- /dev/null +++ b/src/Converters/ConvertToJson.php @@ -0,0 +1,94 @@ +root()->name ? "{\n" : "[\n"; + } + + public function footer(Feed $feed): string + { + return $feed->root()->name ? "\n]\n}\n" : "\n]\n"; + } + + public function root(Feed $feed): string + { + return sprintf("\"%s\": [\n", $feed->root()->name); + } + + public function item(FeedItem $item, bool $isLast): string + { + $data = $this->performItem($item->toArray()); + + $suffix = $isLast ? '' : ','; + + return $this->toJson($data) . $suffix; + } + + public function info(array $info, bool $afterRoot): string + { + $data = $this->performItem($info); + + $json = $this->toJson($data); + + if (! $afterRoot) { + $json = mb_substr($json, 1, -1); + } + + return $json . ','; + } + + protected function performItem(array $data): array + { + foreach ($data as &$value) { + if (is_array($value)) { + $value = $this->performItem($value); + + continue; + } + + $value = $this->transformValue($value); + } + + return $data; + } + + protected function toJson(array $data): string + { + return json_encode($data, $this->jsonOptions()); + } + + protected function jsonOptions(): int + { + if ($this->pretty) { + return JSON_PRETTY_PRINT | $this->options; + } + + return $this->options; + } +} diff --git a/src/Converters/ConvertToXml.php b/src/Converters/ConvertToXml.php index 2e68058..bd43699 100644 --- a/src/Converters/ConvertToXml.php +++ b/src/Converters/ConvertToXml.php @@ -6,31 +6,70 @@ use DOMDocument; use DOMNode; +use DragonCode\LaravelFeed\Data\ElementData; +use DragonCode\LaravelFeed\Feeds\Feed; use DragonCode\LaravelFeed\Feeds\Items\FeedItem; use DragonCode\LaravelFeed\Services\TransformerService; +use DragonCode\LaravelFeed\Transformers\SpecialCharsTransformer; use Illuminate\Container\Attributes\Config; use Illuminate\Support\Str; +use function collect; use function is_array; +use function sprintf; use function str_replace; use function str_starts_with; +use function trim; -class ConvertToXml +class ConvertToXml extends Converter { protected DOMDocument $document; + protected array $transformers = [ + SpecialCharsTransformer::class, + ]; + public function __construct( #[Config('feeds.pretty')] bool $pretty, - protected TransformerService $transformer, + TransformerService $transformer, ) { + parent::__construct($pretty, $transformer); + $this->document = new DOMDocument('1.0', 'UTF-8'); $this->document->formatOutput = $pretty; $this->document->preserveWhiteSpace = ! $pretty; } - public function convertItem(FeedItem $item): string + public function header(Feed $feed): string + { + if (empty($value = $feed->header())) { + return ''; + } + + return trim($value) . PHP_EOL; + } + + public function footer(Feed $feed): string + { + $value = ''; + + if ($name = $feed->root()->name) { + $value .= "\n\n\n"; + } + + return $value . $feed->footer(); + } + + public function root(Feed $feed): string + { + return ! empty($feed->root()->attributes) + ? sprintf("<%s %s>\n\n", $feed->root()->name, $this->rootAttributes($feed->root())) + : sprintf("<%s>\n\n", $feed->root()->name); + } + + public function item(FeedItem $item, bool $isLast): string { $box = $this->performBox($item); @@ -39,7 +78,7 @@ public function convertItem(FeedItem $item): string return $this->toXml($box); } - public function convertInfo(array $info): string + public function info(array $info, bool $afterRoot): string { $box = $this->document->createDocumentFragment(); @@ -102,13 +141,13 @@ protected function isPrefixed(string $key): bool protected function createElement(string $name, mixed $value = ''): DOMNode { - return $this->document->createElement($name, $this->transformValue($value)); + return $this->document->createElement($name, (string) $this->transformValue($value)); } protected function setAttributes(DOMNode $element, array $attributes): void { foreach ($attributes as $key => $value) { - $element->setAttribute($key, $this->transformValue($value)); + $element->setAttribute($key, (string) $this->transformValue($value)); } } @@ -138,7 +177,7 @@ protected function setItemsArray(DOMNode $parent, array $value, string $key): vo protected function setItems(DOMNode $parent, string $key, mixed $value): void { - $element = $this->createElement($key, is_array($value) ? '' : $this->transformValue($value)); + $element = $this->createElement($key, is_array($value) ? '' : $value); if (is_array($value)) { $this->performItem($element, $value); @@ -149,7 +188,14 @@ protected function setItems(DOMNode $parent, string $key, mixed $value): void protected function setRaw(DOMNode $parent, mixed $value): void { - $parent->nodeValue = $this->transformValue($value); + $parent->nodeValue = (string) $this->transformValue($value); + } + + protected function rootAttributes(ElementData $item): string + { + return collect($item->attributes) + ->map(fn (mixed $value, int|string $key) => sprintf('%s="%s"', $key, $value)) + ->implode(' '); } protected function toXml(DOMNode $item): string @@ -161,9 +207,4 @@ protected function convertKey(int|string $key): string { return str_replace(' ', '_', (string) $key); } - - protected function transformValue(mixed $value): string - { - return $this->transformer->transform($value); - } } diff --git a/src/Converters/Converter.php b/src/Converters/Converter.php new file mode 100644 index 0000000..6cb8201 --- /dev/null +++ b/src/Converters/Converter.php @@ -0,0 +1,36 @@ +transformer->transform($value, $this->transformers); + } +} diff --git a/src/Enums/FeedFormatEnum.php b/src/Enums/FeedFormatEnum.php index e8c0673..89fc743 100644 --- a/src/Enums/FeedFormatEnum.php +++ b/src/Enums/FeedFormatEnum.php @@ -6,5 +6,6 @@ enum FeedFormatEnum: string { - case Xml = 'xml'; + case Xml = 'xml'; + case Json = 'json'; } diff --git a/src/Feeds/Feed.php b/src/Feeds/Feed.php index 766d8c5..1c1c769 100644 --- a/src/Feeds/Feed.php +++ b/src/Feeds/Feed.php @@ -43,7 +43,10 @@ public function chunkSize(): int public function header(): string { - return ''; + return match ($this->format()) { + FeedFormatEnum::Xml => '', + FeedFormatEnum::Json => '', + }; } public function footer(): string @@ -74,7 +77,7 @@ public function filename(): string ->ltrim('\\') ->replace('\\', ' ') ->kebab() - ->append('.', $this->format->value) + ->append('.', $this->format()->value) ->toString(); } @@ -89,4 +92,9 @@ public function storage(): Filesystem { return Storage::disk($this->storage); } + + public function format(): FeedFormatEnum + { + return $this->format; + } } diff --git a/src/Services/GeneratorService.php b/src/Services/GeneratorService.php index 9569c2e..55c9c5b 100644 --- a/src/Services/GeneratorService.php +++ b/src/Services/GeneratorService.php @@ -4,8 +4,10 @@ namespace DragonCode\LaravelFeed\Services; +use DragonCode\LaravelFeed\Converters\Converter; +use DragonCode\LaravelFeed\Converters\ConvertToJson; use DragonCode\LaravelFeed\Converters\ConvertToXml; -use DragonCode\LaravelFeed\Data\ElementData; +use DragonCode\LaravelFeed\Enums\FeedFormatEnum; use DragonCode\LaravelFeed\Feeds\Feed; use DragonCode\LaravelFeed\Queries\FeedQuery; use Illuminate\Console\OutputStyle; @@ -13,17 +15,15 @@ use Symfony\Component\Console\Helper\ProgressBar; use function blank; -use function collect; use function get_class; use function implode; -use function sprintf; -use function trim; class GeneratorService { public function __construct( protected FilesystemService $filesystem, - protected ConvertToXml $converter, + protected ConvertToXml $xmlConverter, + protected ConvertToJson $jsonConverter, protected FeedQuery $query, ) {} @@ -47,23 +47,32 @@ public function feed(Feed $feed, ?OutputStyle $output = null): void protected function performItem($file, Feed $feed, ?OutputStyle $output): void // @pest-ignore-type { + $count = $feed->builder()->count(); + // @codeCoverageIgnoreStart - $bar = $this->progressBar($feed, $output); + $bar = $this->progressBar($count, $output); // @codeCoverageIgnoreEnd - $feed->builder()->chunkById($feed->chunkSize(), function (Collection $models) use ($file, $feed, $bar) { - $content = []; + $progress = $count; - foreach ($models as $model) { - $content[] = $this->converter->convertItem( - $feed->item($model) - ); + $feed->builder()->chunkById( + $feed->chunkSize(), + function (Collection $models) use ($file, $feed, $bar, &$progress) { + $content = []; - $bar?->advance(); - } + foreach ($models as $model) { + $content[] = $this->converter($feed)->item( + item : $feed->item($model), + isLast: $progress <= 1 + ); + + $bar?->advance(); + $progress--; + } - $this->append($file, implode(PHP_EOL, $content), $feed->path()); - }); + $this->append($file, implode(PHP_EOL, $content), $feed->path()); + } + ); $bar?->finish(); $output?->newLine(); @@ -71,11 +80,9 @@ protected function performItem($file, Feed $feed, ?OutputStyle $output): void // protected function performHeader($file, Feed $feed): void // @pest-ignore-type { - if (empty($value = $feed->header())) { - return; - } + $value = $this->converter($feed)->header($feed); - $this->append($file, trim($value) . PHP_EOL, $feed->path()); + $this->append($file, $value, $feed->path()); } protected function performInfo($file, Feed $feed): void // @pest-ignore-type @@ -84,7 +91,7 @@ protected function performInfo($file, Feed $feed): void // @pest-ignore-type return; } - $value = $this->converter->convertInfo($info); + $value = $this->converter($feed)->info($info, $feed->root()->beforeInfo); $this->append($file, $value . PHP_EOL, $feed->path()); } @@ -95,39 +102,28 @@ protected function performRoot($file, Feed $feed, bool $when): void // @pest-ign return; } - if (! $name = $feed->root()->name) { + if (! $feed->root()->name) { return; } - $value = ! empty($feed->root()->attributes) - ? sprintf("<%s %s>\n\n", $name, $this->makeRootAttributes($feed->root())) - : sprintf("<%s>\n\n", $name); + $value = $this->converter($feed)->root($feed); $this->append($file, $value, $feed->path()); } protected function performFooter($file, Feed $feed): void // @pest-ignore-type { - $value = ''; - - if ($name = $feed->root()->name) { - $value .= "\n\n\n"; - } - - $value .= $feed->footer(); + $value = $this->converter($feed)->footer($feed); $this->append($file, $value, $feed->path()); } - protected function makeRootAttributes(ElementData $item): string - { - return collect($item->attributes) - ->map(fn (mixed $value, int|string $key) => sprintf('%s="%s"', $key, $value)) - ->implode(' '); - } - protected function append($file, string $content, string $path): void // @pest-ignore-type { + if (blank($content)) { + return; + } + $this->filesystem->append($file, $content, $path); } @@ -148,10 +144,16 @@ protected function setLastActivity(Feed $feed): void ); } - protected function progressBar(Feed $feed, ?OutputStyle $output): ?ProgressBar + protected function converter(Feed $feed): Converter { - return $output?->createProgressBar( - $feed->builder()->count() - ); + return match ($feed->format()) { + FeedFormatEnum::Xml => $this->xmlConverter, + FeedFormatEnum::Json => $this->jsonConverter, + }; + } + + protected function progressBar(int $count, ?OutputStyle $output): ?ProgressBar + { + return $output?->createProgressBar($count); } } diff --git a/src/Services/TransformerService.php b/src/Services/TransformerService.php index 1310ade..0ffd8bc 100644 --- a/src/Services/TransformerService.php +++ b/src/Services/TransformerService.php @@ -4,35 +4,30 @@ namespace DragonCode\LaravelFeed\Services; -use DragonCode\LaravelFeed\Transformers\SpecialCharsTransformer; use Illuminate\Support\Collection; use function config; class TransformerService { - protected array $force = [ - SpecialCharsTransformer::class, - ]; - - public function transform(mixed $value): string + public function transform(mixed $value, array $transformers = []): bool|float|int|string|null { - foreach ($this->transformers() as $transformer) { + foreach ($this->transformers($transformers) as $transformer) { if ($transformer->allow($value)) { $value = $transformer->transform($value); } } - return (string) $value; + return $value; } /** * @return \DragonCode\LaravelFeed\Contracts\Transformer[] */ - protected function transformers(): array + protected function transformers(array $transformers): array { return (new Collection(config('feeds.transformers'))) - ->merge($this->force) + ->merge($transformers) ->map(static fn (string $transformer) => new $transformer) ->unique() ->all(); diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonInfoTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonInfoTest/export_with_data_set____false__.snap new file mode 100644 index 0000000..fe10b55 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonInfoTest/export_with_data_set____false__.snap @@ -0,0 +1,6 @@ +[ +{"name":"Laravel","company":"Laravel","platform":"Laravel","url":"https://example.com","email":"test@example.com","currencies":{"@currency":[{"@attributes":{"id":"RUR","rate":"1"}}]},"categories":{"@category":[{"@attributes":{"id":41},"@value":"Домашние майки"},{"@attributes":{"id":539},"@value":"Велосипедки"},{"@attributes":{"id":44},"@value":"Ремни"}]}}, +{"id":1,"title":"Some 1","content":"Some content 1","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":2,"title":"Some 2","content":"Some content 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":3,"title":"Some 3","content":"Some content 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} +] diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonInfoTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonInfoTest/export_with_data_set____true__.snap new file mode 100644 index 0000000..8728af1 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonInfoTest/export_with_data_set____true__.snap @@ -0,0 +1,62 @@ +[ +{ + "name": "Laravel", + "company": "Laravel", + "platform": "Laravel", + "url": "https://example.com", + "email": "test@example.com", + "currencies": { + "@currency": [ + { + "@attributes": { + "id": "RUR", + "rate": "1" + } + } + ] + }, + "categories": { + "@category": [ + { + "@attributes": { + "id": 41 + }, + "@value": "Домашние майки" + }, + { + "@attributes": { + "id": 539 + }, + "@value": "Велосипедки" + }, + { + "@attributes": { + "id": 44 + }, + "@value": "Ремни" + } + ] + } +}, +{ + "id": 1, + "title": "Some 1", + "content": "Some content 1", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 2, + "title": "Some 2", + "content": "Some content 2", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 3, + "title": "Some 3", + "content": "Some content 3", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +} +] diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonRootInfoTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonRootInfoTest/export_with_data_set____false__.snap new file mode 100644 index 0000000..db5e4f9 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonRootInfoTest/export_with_data_set____false__.snap @@ -0,0 +1,8 @@ +{ +"name":"Laravel","company":"Laravel","platform":"Laravel","url":"https://example.com","email":"test@example.com","currencies":{"@currency":[{"@attributes":{"id":"RUR","rate":"1"}}]},"categories":{"@category":[{"@attributes":{"id":41},"@value":"Домашние майки"},{"@attributes":{"id":539},"@value":"Велосипедки"},{"@attributes":{"id":44},"@value":"Ремни"}]}, +"items": [ +{"id":1,"title":"Some 1","content":"Some content 1","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":2,"title":"Some 2","content":"Some content 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":3,"title":"Some 3","content":"Some content 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} +] +} diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonRootInfoTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonRootInfoTest/export_with_data_set____true__.snap new file mode 100644 index 0000000..c60ed0f --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonRootInfoTest/export_with_data_set____true__.snap @@ -0,0 +1,64 @@ +{ + + "name": "Laravel", + "company": "Laravel", + "platform": "Laravel", + "url": "https://example.com", + "email": "test@example.com", + "currencies": { + "@currency": [ + { + "@attributes": { + "id": "RUR", + "rate": "1" + } + } + ] + }, + "categories": { + "@category": [ + { + "@attributes": { + "id": 41 + }, + "@value": "Домашние майки" + }, + { + "@attributes": { + "id": 539 + }, + "@value": "Велосипедки" + }, + { + "@attributes": { + "id": 44 + }, + "@value": "Ремни" + } + ] + } +, +"items": [ +{ + "id": 1, + "title": "Some 1", + "content": "Some content 1", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 2, + "title": "Some 2", + "content": "Some content 2", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 3, + "title": "Some 3", + "content": "Some content 3", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +} +] +} diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonRootTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonRootTest/export_with_data_set____false__.snap new file mode 100644 index 0000000..ecc0a10 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonRootTest/export_with_data_set____false__.snap @@ -0,0 +1,7 @@ +{ +"items": [ +{"id":1,"title":"Some 1","content":"Some content 1","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":2,"title":"Some 2","content":"Some content 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":3,"title":"Some 3","content":"Some content 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} +] +} diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonRootTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonRootTest/export_with_data_set____true__.snap new file mode 100644 index 0000000..a732063 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonRootTest/export_with_data_set____true__.snap @@ -0,0 +1,25 @@ +{ +"items": [ +{ + "id": 1, + "title": "Some 1", + "content": "Some content 1", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 2, + "title": "Some 2", + "content": "Some content 2", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 3, + "title": "Some 3", + "content": "Some content 3", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +} +] +} diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonTest/export_with_data_set____false__.snap new file mode 100644 index 0000000..ad655f9 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonTest/export_with_data_set____false__.snap @@ -0,0 +1,5 @@ +[ +{"id":1,"title":"Some 1","content":"Some content 1","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":2,"title":"Some 2","content":"Some content 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"}, +{"id":3,"title":"Some 3","content":"Some content 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} +] diff --git a/tests/.pest/snapshots/Feature/Feeds/JsonTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/JsonTest/export_with_data_set____true__.snap new file mode 100644 index 0000000..87ba224 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/JsonTest/export_with_data_set____true__.snap @@ -0,0 +1,23 @@ +[ +{ + "id": 1, + "title": "Some 1", + "content": "Some content 1", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 2, + "title": "Some 2", + "content": "Some content 2", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +}, +{ + "id": 3, + "title": "Some 3", + "content": "Some content 3", + "created_at": "2025-09-04T04:08:12.000000Z", + "updated_at": "2025-09-04T04:08:12.000000Z" +} +] diff --git a/tests/Feature/Feeds/JsonInfoTest.php b/tests/Feature/Feeds/JsonInfoTest.php new file mode 100644 index 0000000..326af29 --- /dev/null +++ b/tests/Feature/Feeds/JsonInfoTest.php @@ -0,0 +1,14 @@ +with('boolean'); diff --git a/tests/Feature/Feeds/JsonRootInfoTest.php b/tests/Feature/Feeds/JsonRootInfoTest.php new file mode 100644 index 0000000..beae030 --- /dev/null +++ b/tests/Feature/Feeds/JsonRootInfoTest.php @@ -0,0 +1,14 @@ +with('boolean'); diff --git a/tests/Feature/Feeds/JsonRootTest.php b/tests/Feature/Feeds/JsonRootTest.php new file mode 100644 index 0000000..bef3130 --- /dev/null +++ b/tests/Feature/Feeds/JsonRootTest.php @@ -0,0 +1,14 @@ +with('boolean'); diff --git a/tests/Feature/Feeds/JsonTest.php b/tests/Feature/Feeds/JsonTest.php new file mode 100644 index 0000000..f12326a --- /dev/null +++ b/tests/Feature/Feeds/JsonTest.php @@ -0,0 +1,14 @@ +with('boolean'); diff --git a/tests/Helpers/expects.php b/tests/Helpers/expects.php index 66eb4ee..438e207 100644 --- a/tests/Helpers/expects.php +++ b/tests/Helpers/expects.php @@ -6,7 +6,7 @@ use function Pest\Laravel\artisan; -function expectFeedSnapshot(string $class): void +function expectFeedSnapshot(string $class, bool $isJson = false): void { $feed = findFeed($class); @@ -17,5 +17,12 @@ function expectFeedSnapshot(string $class): void ])->assertSuccessful()->run(); expect($instance->path())->toBeFile(); - expect(file_get_contents($instance->path()))->toMatchSnapshot(); + + $content = file_get_contents($instance->path()); + + if ($isJson) { + expect($content)->toBeJson(); + } + + expect($content)->toMatchSnapshot(); } diff --git a/workbench/app/Feeds/JsonFeed.php b/workbench/app/Feeds/JsonFeed.php new file mode 100644 index 0000000..8d30568 --- /dev/null +++ b/workbench/app/Feeds/JsonFeed.php @@ -0,0 +1,28 @@ +