diff --git a/docs/snippets/advanced-directive-array-1.xml b/docs/snippets/advanced-directive-array-1.xml new file mode 100644 index 0000000..b250e82 --- /dev/null +++ b/docs/snippets/advanced-directive-array-1.xml @@ -0,0 +1,19 @@ + + + + + Cedrick Stoltenberg + https://via.placeholder.com/640x480.png/006611?text=sapiente + https://via.placeholder.com/640x480.png/0099aa?text=voluptates + https://via.placeholder.com/640x480.png/008833?text=facere + https://via.placeholder.com/640x480.png/005500?text=illo + + + Cale Murazik + https://via.placeholder.com/640x480.png/00bb22?text=nisi + https://via.placeholder.com/640x480.png/0022aa?text=illo + https://via.placeholder.com/640x480.png/0066ee?text=omnis + https://via.placeholder.com/640x480.png/0099bb?text=maxime + + + diff --git a/docs/snippets/advanced-directive-array.xml b/docs/snippets/advanced-directive-array.xml index 87fed3b..ed42be1 100644 --- a/docs/snippets/advanced-directive-array.xml +++ b/docs/snippets/advanced-directive-array.xml @@ -2,18 +2,18 @@ - Dr. Mozelle Hand V - https://via.placeholder.com/640x480.png/00ddee?text=iste - https://via.placeholder.com/640x480.png/00aaaa?text=dolorem - https://via.placeholder.com/640x480.png/002288?text=quasi - https://via.placeholder.com/640x480.png/00aa66?text=explicabo + Cyril Kunze + https://via.placeholder.com/640x480.png/0044bb?text=sit + https://via.placeholder.com/640x480.png/0055ee?text=accusantium + https://via.placeholder.com/640x480.png/00dd22?text=qui + https://via.placeholder.com/640x480.png/0011dd?text=molestias - Lori Gislason - https://via.placeholder.com/640x480.png/001144?text=nostrum - https://via.placeholder.com/640x480.png/002200?text=numquam - https://via.placeholder.com/640x480.png/005544?text=consequatur - https://via.placeholder.com/640x480.png/0011dd?text=maiores + Norberto Cassin I + https://via.placeholder.com/640x480.png/006622?text=in + https://via.placeholder.com/640x480.png/009988?text=qui + https://via.placeholder.com/640x480.png/003300?text=ipsam + https://via.placeholder.com/640x480.png/00dd88?text=similique diff --git a/docs/snippets/advanced-directive-attributes-1.xml b/docs/snippets/advanced-directive-attributes-1.xml new file mode 100644 index 0000000..9854d56 --- /dev/null +++ b/docs/snippets/advanced-directive-attributes-1.xml @@ -0,0 +1,16 @@ + + + + +https://example.com + + + Mr. Cameron Nader + + + + Prof. Ernestine Murphy MD + + + + diff --git a/docs/snippets/advanced-directive-attributes.xml b/docs/snippets/advanced-directive-attributes.xml index ff4fc6c..6f8f833 100644 --- a/docs/snippets/advanced-directive-attributes.xml +++ b/docs/snippets/advanced-directive-attributes.xml @@ -1,16 +1,16 @@ - + https://example.com - Doyle Donnelly - + Ada Klocko + - Sally Pagac Sr. - + Garrett Yost + diff --git a/docs/snippets/advanced-directive-cdata-1.xml b/docs/snippets/advanced-directive-cdata-1.xml new file mode 100644 index 0000000..9b52be0 --- /dev/null +++ b/docs/snippets/advanced-directive-cdata-1.xml @@ -0,0 +1,13 @@ + + + + + Oral Lang]]> + margie.bartell@example.net + + + Prof. Kellen Schroeder DDS]]> + awindler@example.net + + + diff --git a/docs/snippets/advanced-directive-cdata.xml b/docs/snippets/advanced-directive-cdata.xml index edf02ce..4aa67c2 100644 --- a/docs/snippets/advanced-directive-cdata.xml +++ b/docs/snippets/advanced-directive-cdata.xml @@ -2,12 +2,12 @@ - Sallie Price]]> - ssporer@example.net + Thalia Baumbach]]> + fermin.crooks@example.com - Dr. Nedra Weimann]]> - beau.feest@example.net + Pauline Dicki]]> + bkuhic@example.org diff --git a/docs/snippets/advanced-directive-mixed-1.xml b/docs/snippets/advanced-directive-mixed-1.xml new file mode 100644 index 0000000..9e466c0 --- /dev/null +++ b/docs/snippets/advanced-directive-mixed-1.xml @@ -0,0 +1,19 @@ + + + + + Rod Mraz III + + Foo + winona.fay@example.com + + + + Chelsey Wintheiser + + Foo + else71@example.com + + + + diff --git a/docs/snippets/advanced-directive-mixed.xml b/docs/snippets/advanced-directive-mixed.xml index 7b97f98..f50f87d 100644 --- a/docs/snippets/advanced-directive-mixed.xml +++ b/docs/snippets/advanced-directive-mixed.xml @@ -2,17 +2,17 @@ - Bradley Krajcik + Dewitt Brown Foo - alexander73@example.net + lakin.augusta@example.com - Jamil Hilpert PhD + Bessie DuBuque Foo - vwalsh@example.net + ali52@example.org diff --git a/docs/snippets/advanced-directive-value-1.xml b/docs/snippets/advanced-directive-value-1.xml new file mode 100644 index 0000000..272cd5c --- /dev/null +++ b/docs/snippets/advanced-directive-value-1.xml @@ -0,0 +1,13 @@ + + + + + Onie Hahn + mozelle.eichmann@example.com + + + Heaven Hauck + grady.rocky@example.org + + + diff --git a/docs/snippets/advanced-directive-value.xml b/docs/snippets/advanced-directive-value.xml index eec4b5d..8950115 100644 --- a/docs/snippets/advanced-directive-value.xml +++ b/docs/snippets/advanced-directive-value.xml @@ -2,12 +2,12 @@ - Mr. Cyril Douglas - grant.kassulke@example.com + Johnson Schoen + hamill.myrtie@example.com - Prof. Camille Veum DVM - hfranecki@example.com + Morton Abernathy + randall.corkery@example.com diff --git a/docs/snippets/advanced-element-attribute-1.xml b/docs/snippets/advanced-element-attribute-1.xml new file mode 100644 index 0000000..953531a --- /dev/null +++ b/docs/snippets/advanced-element-attribute-1.xml @@ -0,0 +1,13 @@ + + + + + 1 + Elody Durgan + + + 2 + Cierra Bauch + + + diff --git a/docs/snippets/advanced-element-attribute.xml b/docs/snippets/advanced-element-attribute.xml index e1c5321..098d7a4 100644 --- a/docs/snippets/advanced-element-attribute.xml +++ b/docs/snippets/advanced-element-attribute.xml @@ -1,13 +1,13 @@ - + 1 - Efren Prosacco + Mrs. Janet Kuhn - + 2 - Elwyn Kuphal DDS + Kristin Gleason diff --git a/docs/snippets/advanced-element-header-footer-1.xml b/docs/snippets/advanced-element-header-footer-1.xml new file mode 100644 index 0000000..d48e863 --- /dev/null +++ b/docs/snippets/advanced-element-header-footer-1.xml @@ -0,0 +1,15 @@ + + + + + 1 + Paxton Bradtke + + + 2 + Riley Grant III + + + + +This is a custom footer element \ No newline at end of file diff --git a/docs/snippets/advanced-element-header-footer.xml b/docs/snippets/advanced-element-header-footer.xml index 50010f8..8d2065a 100644 --- a/docs/snippets/advanced-element-header-footer.xml +++ b/docs/snippets/advanced-element-header-footer.xml @@ -3,11 +3,11 @@ 1 - Arnaldo Mohr + Dr. Jordyn Hintz 2 - Dillan Beahan + Garland Mraz IV diff --git a/docs/snippets/advanced-element-info-1.xml b/docs/snippets/advanced-element-info-1.xml new file mode 100644 index 0000000..bb7e763 --- /dev/null +++ b/docs/snippets/advanced-element-info-1.xml @@ -0,0 +1,16 @@ + + + +Laravel +https://example.com + + + 1 + Mr. Odell Keeling MD + + + 2 + Cesar Swaniawski + + + diff --git a/docs/snippets/advanced-element-info-before-false-1.xml b/docs/snippets/advanced-element-info-before-false-1.xml new file mode 100644 index 0000000..4270321 --- /dev/null +++ b/docs/snippets/advanced-element-info-before-false-1.xml @@ -0,0 +1,16 @@ + +Laravel +https://example.com + + + + + 1 + Mr. Grayce Borer IV + + + 2 + Melissa Windler PhD + + + diff --git a/docs/snippets/advanced-element-info-before-false.xml b/docs/snippets/advanced-element-info-before-false.xml index 1ce5b52..11e53cb 100644 --- a/docs/snippets/advanced-element-info-before-false.xml +++ b/docs/snippets/advanced-element-info-before-false.xml @@ -6,11 +6,11 @@ 1 - Hilton Rath + Willy Wilkinson 2 - Prof. Juanita Oberbrunner + Delphine Mohr diff --git a/docs/snippets/advanced-element-info.xml b/docs/snippets/advanced-element-info.xml index 091c75b..4a9b800 100644 --- a/docs/snippets/advanced-element-info.xml +++ b/docs/snippets/advanced-element-info.xml @@ -6,11 +6,11 @@ 1 - Sigurd Mueller + Neha Pfannerstill V 2 - Kiera Hansen + Quincy Walter diff --git a/docs/snippets/advanced-element-root-1.xml b/docs/snippets/advanced-element-root-1.xml new file mode 100644 index 0000000..17d5472 --- /dev/null +++ b/docs/snippets/advanced-element-root-1.xml @@ -0,0 +1,13 @@ + + + + + 1 + Trenton Larson + + + 2 + Xzavier Spinka Jr. + + + diff --git a/docs/snippets/advanced-element-root.xml b/docs/snippets/advanced-element-root.xml index 15edaaf..780dfb6 100644 --- a/docs/snippets/advanced-element-root.xml +++ b/docs/snippets/advanced-element-root.xml @@ -3,11 +3,11 @@ 1 - Darby Davis MD + Verna Goldner DVM 2 - Dr. Mattie Rippin + Dr. Samara Ziemann II diff --git a/docs/snippets/receipt-instagram-feed-1.xml b/docs/snippets/receipt-instagram-feed-1.xml new file mode 100644 index 0000000..cea1bd6 --- /dev/null +++ b/docs/snippets/receipt-instagram-feed-1.xml @@ -0,0 +1,59 @@ + + + Laravel + https://example.com + + + + 1 + + + https://example.com/products/in-illum-dolores-officiis-ea + https://via.placeholder.com/640x480.png/008877?text=repudiandae + https://via.placeholder.com/640x480.png/008877?text=repudiandae + The Best + new + in stock + 100 + 100 + 12345 + active + 123 + 456 + Some foo + Some bar + Some baz + a + b + c + + + 2 + + + https://example.com/products/ut-adipisci-consectetur-non-et + https://via.placeholder.com/640x480.png/009966?text=beatae + https://via.placeholder.com/640x480.png/009966?text=beatae + https://via.placeholder.com/640x480.png/000011?text=deleniti + https://via.placeholder.com/640x480.png/009999?text=voluptates + The Best + new + in stock + 250 + 250 + 12345 + active + 123 + 456 + Some foo + Some bar + Some baz + a + b + c + + + + + + \ No newline at end of file diff --git a/docs/snippets/receipt-instagram-feed.xml b/docs/snippets/receipt-instagram-feed.xml index 680585c..b8dfa84 100644 --- a/docs/snippets/receipt-instagram-feed.xml +++ b/docs/snippets/receipt-instagram-feed.xml @@ -8,7 +8,7 @@ 1 - https://example.com/products/ratione-minima-officia-adipisci-ratione-consectetur + https://example.com/products/qui-ut-ratione-sed-et-ratione https://via.placeholder.com/640x480.png/008877?text=repudiandae https://via.placeholder.com/640x480.png/008877?text=repudiandae The Best @@ -31,7 +31,7 @@ 2 - https://example.com/products/accusamus-animi-animi-earum-quis + https://example.com/products/sunt-magnam-dolores-a-omnis https://via.placeholder.com/640x480.png/009966?text=beatae https://via.placeholder.com/640x480.png/009966?text=beatae https://via.placeholder.com/640x480.png/000011?text=deleniti diff --git a/docs/snippets/receipt-rss-feed-1.xml b/docs/snippets/receipt-rss-feed-1.xml new file mode 100644 index 0000000..8836509 --- /dev/null +++ b/docs/snippets/receipt-rss-feed-1.xml @@ -0,0 +1,34 @@ + + + + + + Some 1 + https://example.com/news/some-1 + 1 + + Some category 1 + Wed, 03 Sep 2025 18:52:32 +0000 + bar + + + Some 2 + https://example.com/news/some-2 + 2 + + Some category 2 + Wed, 03 Sep 2025 13:19:41 +0000 + bar + + + Some 3 + https://example.com/news/some-3 + 3 + + Some category 3 + Thu, 04 Sep 2025 03:28:15 +0000 + bar + + + + \ No newline at end of file diff --git a/docs/snippets/receipt-rss-feed.xml b/docs/snippets/receipt-rss-feed.xml index 0890dff..2f3e4e5 100644 --- a/docs/snippets/receipt-rss-feed.xml +++ b/docs/snippets/receipt-rss-feed.xml @@ -8,7 +8,7 @@ 1 Some category 1 - Wed, 03 Sep 2025 13:26:59 +0000 + Wed, 03 Sep 2025 11:09:19 +0000 bar @@ -17,7 +17,16 @@ 2 Some category 2 - Wed, 03 Sep 2025 14:17:03 +0000 + Wed, 03 Sep 2025 22:21:20 +0000 + bar + + + Some 3 + https://example.com/news/some-3 + 3 + + Some category 3 + Wed, 03 Sep 2025 23:36:51 +0000 bar diff --git a/docs/snippets/receipt-sitemap-feed-1.xml b/docs/snippets/receipt-sitemap-feed-1.xml new file mode 100644 index 0000000..62a4bee --- /dev/null +++ b/docs/snippets/receipt-sitemap-feed-1.xml @@ -0,0 +1,15 @@ + + + + + https://example.com/products/reiciendis-animi-ut-voluptatem-quaerat-odit-suscipit + 2025-08-31T20:00:00+00:00 + 0.9 + + + https://example.com/products/consequatur-beatae-non-sint-totam-voluptatem + 2025-08-30T19:00:00+00:00 + 0.9 + + + diff --git a/docs/snippets/receipt-sitemap-feed.xml b/docs/snippets/receipt-sitemap-feed.xml index 2a28a0f..491d3a4 100644 --- a/docs/snippets/receipt-sitemap-feed.xml +++ b/docs/snippets/receipt-sitemap-feed.xml @@ -2,12 +2,12 @@ - https://example.com/products/dolor-culpa-reiciendis-illo-magnam-nisi-quisquam-labore-aspernatur + https://example.com/products/voluptatem-dolores-iure-sint-autem-dolores-quo-itaque 2025-08-31T20:00:00+00:00 0.9 - https://example.com/products/quas-tempora-quia-animi-veniam-tempore-et-at + https://example.com/products/veritatis-voluptates-officiis-aperiam-voluptas-vel-non 2025-08-30T19:00:00+00:00 0.9 diff --git a/docs/snippets/receipt-yandex-feed-1.xml b/docs/snippets/receipt-yandex-feed-1.xml new file mode 100644 index 0000000..f3db52c --- /dev/null +++ b/docs/snippets/receipt-yandex-feed-1.xml @@ -0,0 +1,47 @@ + + + +My App +My Company +My Platform +https://example.com +feeds@example.com + + + + + Foo + Bar + +bar + + + + + https://example.com/products/qui-sunt-nihil-placeat-numquam-rerum-laboriosam-dolores-aliquid + GD-PRDCT-1 + Some 1 + Some description 1 + 100 + RUR + The Best + https://via.placeholder.com/640x480.png/008877?text=repudiandae + bar + + + https://example.com/products/laudantium-perferendis-error-ad-explicabo-eos-aspernatur + GD-PRDCT-2 + Some 2 + Some description 2 + 250 + RUR + The Best + https://via.placeholder.com/640x480.png/009966?text=beatae + https://via.placeholder.com/640x480.png/000011?text=deleniti + https://via.placeholder.com/640x480.png/009999?text=voluptates + bar + + + + + \ No newline at end of file diff --git a/docs/snippets/receipt-yandex-feed.xml b/docs/snippets/receipt-yandex-feed.xml index 0f47c65..d66edda 100644 --- a/docs/snippets/receipt-yandex-feed.xml +++ b/docs/snippets/receipt-yandex-feed.xml @@ -18,7 +18,7 @@ - https://example.com/products/ipsa-maiores-odit-dicta-temporibus-et-rerum + https://example.com/products/a-doloremque-et-nihil GD-PRDCT-1 Some 1 Some description 1 @@ -29,7 +29,7 @@ bar - https://example.com/products/quo-rerum-qui-eos-eius-quaerat-voluptatem-et + https://example.com/products/pariatur-molestiae-vitae-odit-qui GD-PRDCT-2 Some 2 Some description 2 diff --git a/src/Feeds/Feed.php b/src/Feeds/Feed.php index 7a0e465..69ebb0a 100644 --- a/src/Feeds/Feed.php +++ b/src/Feeds/Feed.php @@ -46,6 +46,16 @@ public function chunkSize(): int return 1000; } + public function perFile(): int + { + return 0; + } + + public function maxFiles(): int + { + return 0; + } + public function header(): string { return match ($this->format()) { @@ -87,10 +97,26 @@ public function filename(): string ->toString(); } - public function path(): string + public function path(int|string $suffix = ''): string { + if (empty($suffix)) { + return $this->storage()->path( + $this->filename() + ); + } + + $filename = $this->filename(); + + $directory = pathinfo($filename, PATHINFO_DIRNAME); + $basename = pathinfo($filename, PATHINFO_FILENAME); + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + if ($suffix) { + $suffix = '-' . $suffix; + } + return $this->storage()->path( - $this->filename() + "$directory/$basename$suffix.$extension" ); } diff --git a/src/Services/ExportService.php b/src/Services/ExportService.php new file mode 100644 index 0000000..670cb57 --- /dev/null +++ b/src/Services/ExportService.php @@ -0,0 +1,199 @@ +perFile = $this->perFile($this->feed); + $this->maxFiles = $this->maxFiles($this->feed); + $this->total = $this->total(); + + $this->progressBar = $this->createProgressBar( + $this->total + ); + } + + public function chunk(int $chunk): static + { + $this->chunk = $chunk; + + return $this; + } + + public function file(Closure $create, Closure $close): static + { + $this->createFile = $create; + $this->closeFile = $close; + + return $this; + } + + public function item(Closure $callback): static + { + $this->item = $callback; + + return $this; + } + + public function export(): void + { + $this->feed->builder() + ->lazyById($this->chunk) + ->each(function (Model $model) { + $this->line++; + $this->records++; + $this->total--; + + $this->content[] = value($this->item, $model, $this->isLastItem()); + + $this->store(); + + if ($this->total <= 0) { + return false; + } + + if ($this->maxFiles && $this->file >= $this->maxFiles) { + return false; + } + }); + + $this->store(true); + + $this->progressBar?->finish(); + } + + protected function store(bool $force = false): void + { + if ($force || $this->records >= $this->perFile || $this->line >= $this->chunk) { + $this->line = 0; + + $this->append(); + + $this->content = []; + } + + if ($force || $this->records >= $this->perFile) { + $this->records = 0; + + $this->releaseFile(); + } + } + + protected function isLastItem(): bool + { + return $this->line >= min($this->perFile, $this->total); + } + + protected function getFile() // @pest-ignore-type + { + if (! empty($this->resource)) { + return $this->resource; + } + + return $this->resource ??= value($this->createFile); + } + + protected function releaseFile(): void + { + if ($this->resource === null) { + return; + } + + $index = $this->maxFiles ? $this->file : 0; + + value($this->closeFile, $this->resource, $index); + + $this->resource = null; + + $this->file++; + } + + protected function append(): void + { + if (blank($this->content)) { + return; + } + + $this->filesystem->append($this->getFile(), implode(PHP_EOL, $this->content), $this->feed->path()); + } + + protected function perFile(Feed $feed): int + { + if ($count = max($feed->perFile(), 0)) { + return $count; + } + + return $this->modelCount(); + } + + protected function maxFiles(Feed $feed): int + { + return max($feed->maxFiles(), 0); + } + + protected function total(): int + { + if ($this->maxFiles <= 0) { + return $this->modelCount(); + } + + return $this->perFile * $this->maxFiles; + } + + protected function modelCount(): int + { + return $this->modelCount ??= $this->feed->builder()->count(); + } + + protected function createProgressBar(int $total): ?ProgressBar + { + return $this->output?->createProgressBar($total); + } +} diff --git a/src/Services/FilesystemService.php b/src/Services/FilesystemService.php index 37ea3b0..3ac08dd 100644 --- a/src/Services/FilesystemService.php +++ b/src/Services/FilesystemService.php @@ -16,10 +16,7 @@ use function dirname; use function fclose; -use function fflush; -use function flock; use function fopen; -use function ftruncate; use function fwrite; use function is_resource; use function microtime; @@ -47,8 +44,6 @@ public function createDraft(string $filename) // @pest-ignore-type // @codeCoverageIgnoreEnd } - $this->lock($resource); - return $resource; // @codeCoverageIgnoreStart } catch (Throwable $e) { @@ -77,7 +72,6 @@ public function release($resource, string $path): void // @pest-ignore-type try { $temp = $this->getMetaPath($resource); - $this->unlock($resource); $this->close($resource); if ($this->file->exists($path)) { @@ -93,7 +87,7 @@ public function release($resource, string $path): void // @pest-ignore-type $this->cleanTemporaryDirectory($temp); // @codeCoverageIgnoreStart } catch (Throwable $e) { - throw new CloseFeedException($temp, $e); + throw new CloseFeedException($path, $e); } // @codeCoverageIgnoreEnd } @@ -145,27 +139,4 @@ protected function getMetaPath($file): string // @pest-ignore-type return $meta['uri'] ?? throw new ResourceMetaException; } - - /** - * @param resource $resource - */ - protected function lock($resource): void // @pest-ignore-type - { - if (! flock($resource, LOCK_EX)) { - // @codeCoverageIgnoreStart - throw new RuntimeException('Resource lock error. The resource may be in use by another process.'); - // @codeCoverageIgnoreEnd - } - - ftruncate($resource, 0); - } - - /** - * @param resource $resource - */ - protected function unlock($resource): void // @pest-ignore-type - { - fflush($resource); - flock($resource, LOCK_UN); - } } diff --git a/src/Services/GeneratorService.php b/src/Services/GeneratorService.php index 7243947..cb46b9b 100644 --- a/src/Services/GeneratorService.php +++ b/src/Services/GeneratorService.php @@ -4,6 +4,7 @@ namespace DragonCode\LaravelFeed\Services; +use Closure; use DragonCode\LaravelFeed\Converters\Converter; use DragonCode\LaravelFeed\Events\FeedFinishedEvent; use DragonCode\LaravelFeed\Events\FeedStartingEvent; @@ -12,14 +13,12 @@ use DragonCode\LaravelFeed\Helpers\ConverterHelper; use DragonCode\LaravelFeed\Queries\FeedQuery; use Illuminate\Console\OutputStyle; -use Illuminate\Database\Eloquent\Collection; -use Symfony\Component\Console\Helper\ProgressBar; +use Illuminate\Database\Eloquent\Model; use Throwable; use function blank; use function event; use function get_class; -use function implode; class GeneratorService { @@ -34,18 +33,7 @@ public function feed(Feed $feed, ?OutputStyle $output = null): void try { $this->started($feed); - $file = $this->createDraft( - $feed->filename() - ); - - $this->performHeader($file, $feed); - $this->performRoot($file, $feed, true); - $this->performInfo($file, $feed); - $this->performRoot($file, $feed, false); - $this->performItem($file, $feed, $output); - $this->performFooter($file, $feed); - - $this->release($file, $feed->path()); + $this->export($feed, $output, $this->filesystem); $this->setLastActivity($feed); @@ -55,37 +43,42 @@ public function feed(Feed $feed, ?OutputStyle $output = null): void } } - protected function performItem($file, Feed $feed, ?OutputStyle $output): void // @pest-ignore-type + protected function export(Feed $feed, ?OutputStyle $output, FilesystemService $filesystem): void { - $count = $feed->builder()->count(); - - // @codeCoverageIgnoreStart - $bar = $this->progressBar($count, $output); - // @codeCoverageIgnoreEnd - - $progress = $count; + (new ExportService($feed, $filesystem, $output)) + ->file( + create: $this->createFile($feed), + close : $this->closeFile($feed) + ) + ->item(fn (Model $model, bool $last) => $this->converter($feed)->item( + item : $feed->item($model), + isLast: $last + )) + ->chunk($feed->chunkSize()) + ->export(); + } - $feed->builder()->chunkById( - $feed->chunkSize(), - function (Collection $models) use ($file, $feed, $bar, &$progress) { - $content = []; + protected function createFile(Feed $feed): Closure + { + return function () use ($feed) { + $file = $this->createDraft($feed->filename()); - foreach ($models as $model) { - $content[] = $this->converter($feed)->item( - item: $feed->item($model), - isLast: $progress <= 1 - ); + $this->performHeader($file, $feed); + $this->performRoot($file, $feed, true); + $this->performInfo($file, $feed); + $this->performRoot($file, $feed, false); - $bar?->advance(); - $progress--; - } + return $file; + }; + } - $this->append($file, implode(PHP_EOL, $content), $feed->path()); - } - ); + protected function closeFile(Feed $feed): Closure + { + return function ($file, int $index) use ($feed) { + $this->performFooter($file, $feed); - $bar?->finish(); - $output?->newLine(); + $this->release($file, $feed->path($index)); + }; } protected function performHeader($file, Feed $feed): void // @pest-ignore-type @@ -161,11 +154,6 @@ protected function converter(Feed $feed): Converter ); } - protected function progressBar(int $count, ?OutputStyle $output): ?ProgressBar - { - return $output?->createProgressBar($count); - } - protected function started(Feed $feed): void { event(new FeedStartingEvent(get_class($feed))); diff --git a/tests/.pest/snapshots/Feature/Feeds/Defaults/EmptyTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/Defaults/EmptyTest/export_with_data_set____false__.snap deleted file mode 100644 index e69de29..0000000 diff --git a/tests/.pest/snapshots/Feature/Feeds/Defaults/EmptyTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/Defaults/EmptyTest/export_with_data_set____true__.snap deleted file mode 100644 index e69de29..0000000 diff --git a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/DefaultTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/Formats/Json/DefaultTest/export_with_data_set____true__.snap deleted file mode 100644 index 8e62410..0000000 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/DefaultTest/export_with_data_set____true__.snap +++ /dev/null @@ -1,26 +0,0 @@ -[ -{ - "id": 1, - "title": "Some 1", - "content": "Some content 1", - "category": "Some category 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", - "category": "Some category 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", - "category": "Some category 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/Formats/Json/InfoTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/Formats/Json/InfoTest/export_with_data_set____false__.snap deleted file mode 100644 index 0194d9c..0000000 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/InfoTest/export_with_data_set____false__.snap +++ /dev/null @@ -1,6 +0,0 @@ -[ -{"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","category":"Some category 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","category":"Some category 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","category":"Some category 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/Formats/Json/InfoTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/Formats/Json/InfoTest/export_with_data_set____true__.snap deleted file mode 100644 index f7359a7..0000000 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/InfoTest/export_with_data_set____true__.snap +++ /dev/null @@ -1,65 +0,0 @@ -[ -{ - "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", - "category": "Some category 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", - "category": "Some category 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", - "category": "Some category 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/Formats/Json/RootInfoTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootInfoTest/export_with_data_set____false__.snap deleted file mode 100644 index 37f763e..0000000 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootInfoTest/export_with_data_set____false__.snap +++ /dev/null @@ -1,8 +0,0 @@ -{ -"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","category":"Some category 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","category":"Some category 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","category":"Some category 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/Formats/Json/RootInfoTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootInfoTest/export_with_data_set____true__.snap deleted file mode 100644 index 6beb376..0000000 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootInfoTest/export_with_data_set____true__.snap +++ /dev/null @@ -1,67 +0,0 @@ -{ - - "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", - "category": "Some category 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", - "category": "Some category 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", - "category": "Some category 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/Formats/Json/RootTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootTest/export_with_data_set____true__.snap deleted file mode 100644 index 77e1f56..0000000 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootTest/export_with_data_set____true__.snap +++ /dev/null @@ -1,28 +0,0 @@ -{ -"items": [ -{ - "id": 1, - "title": "Some 1", - "content": "Some content 1", - "category": "Some category 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", - "category": "Some category 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", - "category": "Some category 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/Split/CsvTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/Split/CsvTest/export.snap new file mode 100644 index 0000000..2b0dc4b --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/CsvTest/export.snap @@ -0,0 +1,2 @@ +1;Some 1;Some content 1;Some category 1;2025-09-04T04:08:12.000000Z;2025-09-04T04:08:12.000000Z +2;Some 2;Some content 2;Some category 2;2025-09-04T04:08:12.000000Z;2025-09-04T04:08:12.000000Z \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/CsvTest/export__2.snap b/tests/.pest/snapshots/Feature/Feeds/Split/CsvTest/export__2.snap new file mode 100644 index 0000000..36e59ef --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/CsvTest/export__2.snap @@ -0,0 +1 @@ +3;Some 3;Some content 3;Some category 3;2025-09-04T04:08:12.000000Z;2025-09-04T04:08:12.000000Z \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/Split/JsonLinesTest/export.snap similarity index 56% rename from tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootTest/export_with_data_set____false__.snap rename to tests/.pest/snapshots/Feature/Feeds/Split/JsonLinesTest/export.snap index 1a8e82e..0385f70 100644 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/RootTest/export_with_data_set____false__.snap +++ b/tests/.pest/snapshots/Feature/Feeds/Split/JsonLinesTest/export.snap @@ -1,7 +1,2 @@ -{ -"items": [ -{"id":1,"title":"Some 1","content":"Some content 1","category":"Some category 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","category":"Some category 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","category":"Some category 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} -] -} +{"id":1,"title":"Some 1","content":"Some content 1","category":"Some category 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","category":"Some category 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/JsonLinesTest/export__2.snap b/tests/.pest/snapshots/Feature/Feeds/Split/JsonLinesTest/export__2.snap new file mode 100644 index 0000000..df5d32a --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/JsonLinesTest/export__2.snap @@ -0,0 +1 @@ +{"id":3,"title":"Some 3","content":"Some content 3","category":"Some category 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/DefaultTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/Split/JsonTest/export.snap similarity index 66% rename from tests/.pest/snapshots/Feature/Feeds/Formats/Json/DefaultTest/export_with_data_set____false__.snap rename to tests/.pest/snapshots/Feature/Feeds/Split/JsonTest/export.snap index e2ca44d..971a0b0 100644 --- a/tests/.pest/snapshots/Feature/Feeds/Formats/Json/DefaultTest/export_with_data_set____false__.snap +++ b/tests/.pest/snapshots/Feature/Feeds/Split/JsonTest/export.snap @@ -1,5 +1,4 @@ [ {"id":1,"title":"Some 1","content":"Some content 1","category":"Some category 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","category":"Some category 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","category":"Some category 3","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","category":"Some category 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} ] diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/JsonTest/export__2.snap b/tests/.pest/snapshots/Feature/Feeds/Split/JsonTest/export__2.snap new file mode 100644 index 0000000..df1c334 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/JsonTest/export__2.snap @@ -0,0 +1,3 @@ +[ +{"id":3,"title":"Some 3","content":"Some content 3","category":"Some category 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/Split/MaxFilesTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/Split/MaxFilesTest/export.snap new file mode 100644 index 0000000..e5706ae --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/MaxFilesTest/export.snap @@ -0,0 +1 @@ +{"id":1,"title":"Some 1","content":"Some content 1","category":"Some category 1","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/PerFileTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/Split/PerFileTest/export.snap new file mode 100644 index 0000000..0385f70 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/PerFileTest/export.snap @@ -0,0 +1,2 @@ +{"id":1,"title":"Some 1","content":"Some content 1","category":"Some category 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","category":"Some category 2","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/PerFileTest/export__2.snap b/tests/.pest/snapshots/Feature/Feeds/Split/PerFileTest/export__2.snap new file mode 100644 index 0000000..df5d32a --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/PerFileTest/export__2.snap @@ -0,0 +1 @@ +{"id":3,"title":"Some 3","content":"Some content 3","category":"Some category 3","created_at":"2025-09-04T04:08:12.000000Z","updated_at":"2025-09-04T04:08:12.000000Z"} \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/RssTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/Split/RssTest/export.snap new file mode 100644 index 0000000..8631673 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/RssTest/export.snap @@ -0,0 +1,23 @@ + + + + + + Some 1 + https://example.com/news/some-1 + https://example.com/news/some-1 + + Some category 1 + Thu, 04 Sep 2025 04:08:12 +0000 + + + Some 2 + https://example.com/news/some-2 + https://example.com/news/some-2 + + Some category 2 + Thu, 04 Sep 2025 04:08:12 +0000 + + + + \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/RssTest/export__2.snap b/tests/.pest/snapshots/Feature/Feeds/Split/RssTest/export__2.snap new file mode 100644 index 0000000..45af7ca --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/RssTest/export__2.snap @@ -0,0 +1,15 @@ + + + + + + Some 3 + https://example.com/news/some-3 + https://example.com/news/some-3 + + Some category 3 + Thu, 04 Sep 2025 04:08:12 +0000 + + + + \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/XmlTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/Split/XmlTest/export.snap new file mode 100644 index 0000000..1d8cbba --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/XmlTest/export.snap @@ -0,0 +1,41 @@ + + + + + [NEWS]:Some 1 + Some content 1 + Some extra data + + + Luke Skywalker + Lightsaber + + + Sauron]]> + Evil Eye + + + line +line with some html/xml tag +line with & symbol + + + [NEWS]:Some 2 + Some content 2 + Some extra data + + + Luke Skywalker + Lightsaber + + + Sauron]]> + Evil Eye + + + line +line with some html/xml tag +line with & symbol + + + diff --git a/tests/.pest/snapshots/Feature/Feeds/Split/XmlTest/export__2.snap b/tests/.pest/snapshots/Feature/Feeds/Split/XmlTest/export__2.snap new file mode 100644 index 0000000..fca5fbb --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/Split/XmlTest/export__2.snap @@ -0,0 +1,23 @@ + + + + + [NEWS]:Some 3 + Some content 3 + Some extra data + + + Luke Skywalker + Lightsaber + + + Sauron]]> + Evil Eye + + + line +line with some html/xml tag +line with & symbol + + + diff --git a/tests/Feature/Feeds/Split/CsvTest.php b/tests/Feature/Feeds/Split/CsvTest.php new file mode 100644 index 0000000..b0b8309 --- /dev/null +++ b/tests/Feature/Feeds/Split/CsvTest.php @@ -0,0 +1,13 @@ + $feed->id, ])->assertSuccessful()->run(); - expect($instance->path())->toBeFile(); + foreach ($indexes as $index) { + expect($instance->path($index))->toBeFile(); - $content = file_get_contents($instance->path()); + $content = file_get_contents($instance->path($index)); - match ($format) { - FeedFormatEnum::Json => expect($content)->toBeJson(), - FeedFormatEnum::JsonLines => expect($content)->toBeJsonLines(), - FeedFormatEnum::Csv => expect($content)->toBeCsv(), - FeedFormatEnum::Rss => expect($content)->toBeRss(), - default => null - }; + match ($format) { + FeedFormatEnum::Json => expect($content)->toBeJson(), + FeedFormatEnum::JsonLines => expect($content)->toBeJsonLines(), + FeedFormatEnum::Csv => expect($content)->toBeCsv(), + FeedFormatEnum::Rss => expect($content)->toBeRss(), + default => null + }; - expect($content)->toMatchSnapshot(); + expect($content)->toMatchSnapshot(); + } } diff --git a/tests/Helpers/models.php b/tests/Helpers/models.php index 20db89c..c54af03 100644 --- a/tests/Helpers/models.php +++ b/tests/Helpers/models.php @@ -8,7 +8,7 @@ function createNews(...$sequence): void { - News::factory()->count(3)->sequence( + News::factory()->count(count($sequence))->sequence( ...$sequence )->createMany(); } diff --git a/workbench/app/Feeds/SplitCsvFeed.php b/workbench/app/Feeds/SplitCsvFeed.php new file mode 100644 index 0000000..96bbfa5 --- /dev/null +++ b/workbench/app/Feeds/SplitCsvFeed.php @@ -0,0 +1,33 @@ +', + '', + ]); + } + + public function footer(): string + { + return ''; + } +} diff --git a/workbench/app/Feeds/SplitXmlFeed.php b/workbench/app/Feeds/SplitXmlFeed.php new file mode 100644 index 0000000..eeaecb1 --- /dev/null +++ b/workbench/app/Feeds/SplitXmlFeed.php @@ -0,0 +1,40 @@ +