diff --git a/config/feeds.php b/config/feeds.php index 6606358..bff21b3 100644 --- a/config/feeds.php +++ b/config/feeds.php @@ -2,6 +2,16 @@ declare(strict_types=1); +use DragonCode\LaravelFeed\Transformers\BooleanTransformer; +use DragonCode\LaravelFeed\Transformers\DateTimeTransformer; + +/** + * Laravel Feeds configuration + * + * This file defines how feeds are generated and presented, including + * formatting, persistence, scheduling, console UX and value transformers. + * Adjust the options below or override them via environment variables. + */ return [ /** * Pretty-print the generated feed output. @@ -9,10 +19,24 @@ * When enabled, the resulting XML/JSON will include indentation and * human‑friendly formatting. Disable for slightly smaller payload size. * - * By default, false + * Default: false */ 'pretty' => (bool) env('FEED_PRETTY', false), + /** + * Output format options. + */ + 'formats' => [ + /** + * Date/time format used when serializing timestamps to feeds. + * You may use any PHP date format constant, e.g. DATE_ATOM, DATE_RFC3339 + * or a custom PHP date() format string. + * + * Default: DATE_ATOM + */ + 'date' => DATE_ATOM, + ], + /** * Database table settings used by the package (e.g., for generation logs or state). */ @@ -22,11 +46,15 @@ * * Should match a connection defined in config/database.php under * the "connections" array. + * + * Default: sqlite */ 'connection' => env('DB_CONNECTION', 'sqlite'), /** * The database table name used by the package. + * + * Default: feeds */ 'table' => env('FEED_TABLE', 'feeds'), ], @@ -40,6 +68,8 @@ * * Controls how frequently a scheduled job may be executed to avoid * overlapping or excessively frequent runs. + * + * Default: 1440 (24 hours) */ 'ttl' => (int) env('FEED_SCHEDULE_TTL', 1440), @@ -48,6 +78,8 @@ * * When true, tasks will be dispatched to run asynchronously so they do * not block the current process. Set to false to run in the foreground. + * + * Default: true */ 'background' => (bool) env('FEED_SCHEDULE_RUN_BACKGROUND', true), ], @@ -62,8 +94,22 @@ * When set to true, the feed:generate command will display a * progress bar showing the execution progress. * - * Default is false. + * Default: false */ 'progress_bar' => (bool) env('FEED_CONSOLE_PROGRESS_BAR_ENABLED', false), ], + + /** + * Transformers convert rich/complex values to simple scalar representations + * suitable for feeds (XML/JSON). Order matters: the first transformer that + * supports the value will handle it. + * + * You may add your own transformers by implementing + * DragonCode\LaravelFeed\Contracts\Transformer and registering the class + * here, or publish a stub via the package's make command if available. + */ + 'transformers' => [ + DateTimeTransformer::class, + BooleanTransformer::class, + ], ]; diff --git a/docs/snippets/advanced-directive-array.xml b/docs/snippets/advanced-directive-array.xml index 119ffcb..7e17e1d 100644 --- a/docs/snippets/advanced-directive-array.xml +++ b/docs/snippets/advanced-directive-array.xml @@ -2,18 +2,18 @@ - Kayley Hermann - https://via.placeholder.com/640x480.png/00aa77?text=eos - https://via.placeholder.com/640x480.png/00ff66?text=voluptatem - https://via.placeholder.com/640x480.png/00ee33?text=qui - https://via.placeholder.com/640x480.png/005555?text=aut + Justen Barrows + https://via.placeholder.com/640x480.png/008877?text=reprehenderit + https://via.placeholder.com/640x480.png/0066aa?text=quasi + https://via.placeholder.com/640x480.png/00bb55?text=ab + https://via.placeholder.com/640x480.png/006666?text=eum - Dr. Alek Stamm PhD - https://via.placeholder.com/640x480.png/0077dd?text=necessitatibus - https://via.placeholder.com/640x480.png/00ee00?text=nulla - https://via.placeholder.com/640x480.png/003377?text=id - https://via.placeholder.com/640x480.png/0022bb?text=necessitatibus + Mr. Maximo Brown DDS + https://via.placeholder.com/640x480.png/00ddaa?text=velit + https://via.placeholder.com/640x480.png/0000ee?text=maxime + https://via.placeholder.com/640x480.png/005533?text=eos + https://via.placeholder.com/640x480.png/0000bb?text=ullam diff --git a/docs/snippets/advanced-directive-attributes.xml b/docs/snippets/advanced-directive-attributes.xml index b0c7909..0511473 100644 --- a/docs/snippets/advanced-directive-attributes.xml +++ b/docs/snippets/advanced-directive-attributes.xml @@ -1,16 +1,16 @@ - + https://example.com - Lila Jones - + Lera Fay + - Denis Hane - + Ernest Stanton + diff --git a/docs/snippets/advanced-directive-cdata.xml b/docs/snippets/advanced-directive-cdata.xml index 367b802..6ee692a 100644 --- a/docs/snippets/advanced-directive-cdata.xml +++ b/docs/snippets/advanced-directive-cdata.xml @@ -2,12 +2,12 @@ - Jazmyne Carroll]]> - qcrona@example.org + Abe Jenkins]]> + zpredovic@example.net - Prof. Aida Gusikowski]]> - will.amber@example.org + Mr. Hayden Stokes II]]> + blind@example.org diff --git a/docs/snippets/advanced-directive-mixed.xml b/docs/snippets/advanced-directive-mixed.xml index fe505a9..dee75a3 100644 --- a/docs/snippets/advanced-directive-mixed.xml +++ b/docs/snippets/advanced-directive-mixed.xml @@ -2,17 +2,17 @@ - Jana Purdy + Prof. Giovanni Hessel Foo - greenfelder.karley@example.net + cullen.farrell@example.net - Brandt Bernhard + Dr. Remington Torphy Foo - king.orlando@example.com + balistreri.erica@example.org diff --git a/docs/snippets/advanced-directive-value.xml b/docs/snippets/advanced-directive-value.xml index d7debc0..1e92ae6 100644 --- a/docs/snippets/advanced-directive-value.xml +++ b/docs/snippets/advanced-directive-value.xml @@ -2,12 +2,12 @@ - Elvie Lowe I - zwalker@example.org + Johnathan Moore MD + jena94@example.net - Trenton Lindgren DDS - josefa42@example.com + Kianna Schimmel I + garrison65@example.org diff --git a/docs/snippets/advanced-element-attribute-item.php b/docs/snippets/advanced-element-attribute-item.php index e22c0b5..63b8448 100644 --- a/docs/snippets/advanced-element-attribute-item.php +++ b/docs/snippets/advanced-element-attribute-item.php @@ -11,7 +11,7 @@ class AttributeFeedItem extends FeedItem public function attributes(): array { return [ - 'created_at' => $this->model->created_at->toDateTimeString(), + 'created_at' => $this->model->created_at, ]; } } diff --git a/docs/snippets/advanced-element-attribute.xml b/docs/snippets/advanced-element-attribute.xml index a3e2ae3..ecf95da 100644 --- a/docs/snippets/advanced-element-attribute.xml +++ b/docs/snippets/advanced-element-attribute.xml @@ -1,13 +1,13 @@ - + 1 - Jarrett Stark + Jeremie Legros - + 2 - Nathan Shields + Regan Hauck diff --git a/docs/snippets/advanced-element-header-footer.xml b/docs/snippets/advanced-element-header-footer.xml index 95527d2..44e72bb 100644 --- a/docs/snippets/advanced-element-header-footer.xml +++ b/docs/snippets/advanced-element-header-footer.xml @@ -3,11 +3,11 @@ 1 - Miss Myrtice Durgan Jr. + Dr. Stuart Raynor II 2 - Wayne Padberg + Zackery Crona diff --git a/docs/snippets/advanced-element-info-before-false.xml b/docs/snippets/advanced-element-info-before-false.xml index ac27496..461e988 100644 --- a/docs/snippets/advanced-element-info-before-false.xml +++ b/docs/snippets/advanced-element-info-before-false.xml @@ -6,11 +6,11 @@ 1 - Earnest Bashirian Jr. + Austyn Hand I 2 - Noemi Altenwerth III + Kathleen Dare diff --git a/docs/snippets/advanced-element-info.xml b/docs/snippets/advanced-element-info.xml index 7b01df1..32a2a69 100644 --- a/docs/snippets/advanced-element-info.xml +++ b/docs/snippets/advanced-element-info.xml @@ -6,11 +6,11 @@ 1 - Jaden Boehm + Willard Koch II 2 - Adelbert Kihn + Aida Greenholt diff --git a/docs/snippets/advanced-element-root.xml b/docs/snippets/advanced-element-root.xml index 7e183be..f024019 100644 --- a/docs/snippets/advanced-element-root.xml +++ b/docs/snippets/advanced-element-root.xml @@ -3,11 +3,11 @@ 1 - Frances Treutel + Lonnie Senger 2 - Mr. Brad Kirlin DDS + Winnifred Lang diff --git a/docs/snippets/receipt-instagram-feed.xml b/docs/snippets/receipt-instagram-feed.xml index a9e6138..be535dc 100644 --- a/docs/snippets/receipt-instagram-feed.xml +++ b/docs/snippets/receipt-instagram-feed.xml @@ -6,46 +6,46 @@ 1 - - - https://example.com/products/sint-dolor-omnis-doloribus-aut-numquam-aliquid-perspiciatis-doloribus - https://via.placeholder.com/640x480.png/00bbaa?text=quo - https://via.placeholder.com/640x480.png/009988?text=nostrum - https://via.placeholder.com/640x480.png/0066dd?text=rerum - et + + + https://example.com/products/blanditiis-incidunt-autem-officia-rerum + https://via.placeholder.com/640x480.png/0077aa?text=eius + https://via.placeholder.com/640x480.png/00aaaa?text=fuga + https://via.placeholder.com/640x480.png/001144?text=beatae + velit new in stock - 719 - 719 + 466 + 466 12345 active - - 24 + + 17 adult - + 1000 2000 2 - - - https://example.com/products/qui-et-veritatis-molestias-non-voluptatem - https://via.placeholder.com/640x480.png/00cc33?text=distinctio - https://via.placeholder.com/640x480.png/00aa33?text=omnis - https://via.placeholder.com/640x480.png/0033ee?text=voluptatem - corporis + + + https://example.com/products/facere-et-laboriosam-minus-impedit-sit + https://via.placeholder.com/640x480.png/00aa33?text=officiis + https://via.placeholder.com/640x480.png/0011cc?text=quis + https://via.placeholder.com/640x480.png/0088ee?text=saepe + neque new in stock - 183 - 183 + 553 + 553 12345 active - - 21 + + 12 adult - + 1000 2000 diff --git a/docs/snippets/receipt-sitemap-feed-item.php b/docs/snippets/receipt-sitemap-feed-item.php index 8d6f88a..2eb0862 100644 --- a/docs/snippets/receipt-sitemap-feed-item.php +++ b/docs/snippets/receipt-sitemap-feed-item.php @@ -21,7 +21,7 @@ public function toArray(): array return [ 'loc' => route('products.show', $this->model->slug), - 'lastmod' => $this->model->updated_at->toIso8601String(), + 'lastmod' => $this->model->updated_at, 'priority' => 0.9, ]; diff --git a/docs/snippets/receipt-sitemap-feed.xml b/docs/snippets/receipt-sitemap-feed.xml index a6102af..cc7df3d 100644 --- a/docs/snippets/receipt-sitemap-feed.xml +++ b/docs/snippets/receipt-sitemap-feed.xml @@ -2,12 +2,12 @@ - https://example.com/products/quo-et-ut-eum-labore-libero-est-asperiores + https://example.com/products/quia-ea-quaerat-et-doloremque-et-animi-repudiandae-ipsa 2025-09-04T04:08:12+00:00 0.9 - https://example.com/products/nemo-aperiam-vel-sit-eos-et-eos + https://example.com/products/dolorem-rerum-similique-ea-cum-eaque-omnis 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 06cda79..141dad4 100644 --- a/docs/snippets/receipt-yandex-feed.xml +++ b/docs/snippets/receipt-yandex-feed.xml @@ -17,36 +17,36 @@ - https://example.com/products/iure-ad-nisi-odit-fugiat-non-aut-exercitationem - GD-^V#W` - nemo quisquam voluptate praesentium - Ut rerum libero nobis. A quis corrupti assumenda laudantium aut similique. Iste magnam dolor non saepe dolores non aut. Accusamus et natus rerum provident. + https://example.com/products/voluptatum-eveniet-voluptatibus-veniam-est + GD-=YHK`%YP + maxime et rerum repellendus + Deleniti blanditiis dolorem voluptas mollitia quia. Beatae ea ut saepe aut ex illo rerum soluta. Aut omnis harum et cupiditate omnis minima. true - 980 + 479 RUR - aut - https://via.placeholder.com/640x480.png/000055?text=laborum - https://via.placeholder.com/640x480.png/002200?text=eligendi - https://via.placeholder.com/640x480.png/00eecc?text=quaerat - GD-^V#W` - 8 - male + necessitatibus + https://via.placeholder.com/640x480.png/0000dd?text=alias + https://via.placeholder.com/640x480.png/0011bb?text=sint + https://via.placeholder.com/640x480.png/001199?text=distinctio + GD-=YHK`%YP + 7 + female - https://example.com/products/sed-et-non-non - GD-P]H/Z - enim ut voluptatum natus - Omnis ut non iusto qui repudiandae sint fuga modi. Consequatur ipsum quibusdam labore. Culpa qui asperiores sint similique. + https://example.com/products/ut-voluptatem-et-odit-dolor-dolores-et + GD-_^WMW9, + reiciendis molestias qui fuga + Debitis sapiente quae omnis molestiae aut eligendi autem impedit. Illo quaerat odit laborum qui omnis. Doloribus rerum recusandae quidem vero dicta sapiente. true - 595 + 803 RUR - sint - https://via.placeholder.com/640x480.png/00ddcc?text=fuga - https://via.placeholder.com/640x480.png/00bb66?text=quia - https://via.placeholder.com/640x480.png/00bbaa?text=doloribus - GD-P]H/Z - 6 - male + minus + https://via.placeholder.com/640x480.png/0000ee?text=rem + https://via.placeholder.com/640x480.png/001144?text=earum + https://via.placeholder.com/640x480.png/008899?text=error + GD-_^WMW9, + 9 + female diff --git a/ide.json b/ide.json index 0fc213c..e81ea45 100644 --- a/ide.json +++ b/ide.json @@ -2,7 +2,7 @@ "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json", "codeGenerations": [ { - "id": "dragon-code.feeds.main", + "id": "dragon-code.feeds.feed", "name": "Create Feed", "classSuffix": "Feed", "regex": ".+", @@ -25,7 +25,7 @@ ] }, { - "id": "dragon-code.feeds.item", + "id": "dragon-code.feeds.feed-item", "name": "Create Feed Item", "classSuffix": "FeedItem", "regex": ".+", @@ -47,7 +47,7 @@ ] }, { - "id": "dragon-code.feeds.info", + "id": "dragon-code.feeds.feed-info", "name": "Create Feed Info", "classSuffix": "FeedInfo", "regex": ".+", @@ -66,6 +66,27 @@ } } ] + }, + { + "id": "dragon-code.feeds.transformer", + "name": "Create Feed Transformer", + "classSuffix": "Transformer", + "regex": ".+", + "files": [ + { + "appNamespace": "Transformers", + "name": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}.php", + "template": { + "type": "stub", + "path": "/stubs/transformer.stub", + "fallbackPath": "stubs/transformer.stub", + "parameters": { + "DummyNamespace": "${INPUT_FQN|namespace}", + "DummyClass": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}" + } + } + } + ] } ] } diff --git a/src/Contracts/Transformer.php b/src/Contracts/Transformer.php new file mode 100644 index 0000000..78c64c7 --- /dev/null +++ b/src/Contracts/Transformer.php @@ -0,0 +1,12 @@ +document = new DOMDocument('1.0', 'UTF-8'); @@ -101,15 +100,15 @@ protected function isPrefixed(string $key): bool return str_starts_with($key, '@'); } - protected function createElement(string $name, bool|float|int|string|null $value = ''): DOMNode + protected function createElement(string $name, mixed $value = ''): DOMNode { - return $this->document->createElement($name, $this->convertValue($value)); + return $this->document->createElement($name, $this->transformValue($value)); } protected function setAttributes(DOMNode $element, array $attributes): void { foreach ($attributes as $key => $value) { - $element->setAttribute($key, $this->convertValue($value)); + $element->setAttribute($key, $this->transformValue($value)); } } @@ -139,7 +138,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->convertValue($value)); + $element = $this->createElement($key, is_array($value) ? '' : $this->transformValue($value)); if (is_array($value)) { $this->performItem($element, $value); @@ -150,7 +149,7 @@ protected function setItems(DOMNode $parent, string $key, mixed $value): void protected function setRaw(DOMNode $parent, mixed $value): void { - $parent->nodeValue = $this->convertValue($value); + $parent->nodeValue = $this->transformValue($value); } protected function toXml(DOMNode $item): string @@ -163,19 +162,8 @@ protected function convertKey(int|string $key): string return str_replace(' ', '_', (string) $key); } - protected function convertValue(bool|float|int|string|null $value): string + protected function transformValue(mixed $value): string { - if (is_bool($value)) { - return $value ? 'true' : 'false'; - } - - return $this->removeControlCharacters( - htmlspecialchars((string) $value) - ); - } - - protected function removeControlCharacters(string $value): string - { - return preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $value); + return $this->transformer->transform($value); } } diff --git a/src/Services/TransformerService.php b/src/Services/TransformerService.php new file mode 100644 index 0000000..1310ade --- /dev/null +++ b/src/Services/TransformerService.php @@ -0,0 +1,40 @@ +transformers() as $transformer) { + if ($transformer->allow($value)) { + $value = $transformer->transform($value); + } + } + + return (string) $value; + } + + /** + * @return \DragonCode\LaravelFeed\Contracts\Transformer[] + */ + protected function transformers(): array + { + return (new Collection(config('feeds.transformers'))) + ->merge($this->force) + ->map(static fn (string $transformer) => new $transformer) + ->unique() + ->all(); + } +} diff --git a/src/Transformers/BooleanTransformer.php b/src/Transformers/BooleanTransformer.php new file mode 100644 index 0000000..38ff2a7 --- /dev/null +++ b/src/Transformers/BooleanTransformer.php @@ -0,0 +1,22 @@ +format( + $this->format() + ); + } + + protected function format(): string + { + return config('feeds.formats.date'); + } +} diff --git a/src/Transformers/SpecialCharsTransformer.php b/src/Transformers/SpecialCharsTransformer.php new file mode 100644 index 0000000..24f7f94 --- /dev/null +++ b/src/Transformers/SpecialCharsTransformer.php @@ -0,0 +1,27 @@ +removeControlCharacters( + htmlspecialchars((string) $value) + ); + } + + protected function removeControlCharacters(string $value): string + { + return preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $value); + } +} diff --git a/stubs/transformer.stub b/stubs/transformer.stub new file mode 100644 index 0000000..126dab0 --- /dev/null +++ b/stubs/transformer.stub @@ -0,0 +1,20 @@ +expect('DragonCode\LaravelFeed\Transformers') + ->toHaveSuffix('Transformer'); diff --git a/workbench/app/Feeds/Docs/Items/AttributeFeedItem.php b/workbench/app/Feeds/Docs/Items/AttributeFeedItem.php index 39fbc94..028d01d 100644 --- a/workbench/app/Feeds/Docs/Items/AttributeFeedItem.php +++ b/workbench/app/Feeds/Docs/Items/AttributeFeedItem.php @@ -11,7 +11,7 @@ class AttributeFeedItem extends FeedItem public function attributes(): array { return [ - 'created_at' => $this->model->created_at->toDateTimeString(), + 'created_at' => $this->model->created_at, ]; } } diff --git a/workbench/app/Feeds/Docs/Items/ReceiptSitemapFeedItem.php b/workbench/app/Feeds/Docs/Items/ReceiptSitemapFeedItem.php index e518781..9ff0937 100644 --- a/workbench/app/Feeds/Docs/Items/ReceiptSitemapFeedItem.php +++ b/workbench/app/Feeds/Docs/Items/ReceiptSitemapFeedItem.php @@ -21,7 +21,7 @@ public function toArray(): array return [ 'loc' => route('products.show', $this->model->slug), - 'lastmod' => $this->model->updated_at->toIso8601String(), + 'lastmod' => $this->model->updated_at, 'priority' => 0.9, ]; diff --git a/workbench/app/Feeds/Items/SitemapFeedItem.php b/workbench/app/Feeds/Items/SitemapFeedItem.php index 7443cd4..5758a68 100644 --- a/workbench/app/Feeds/Items/SitemapFeedItem.php +++ b/workbench/app/Feeds/Items/SitemapFeedItem.php @@ -19,7 +19,7 @@ public function toArray(): array return [ 'loc' => route('products.show', $this->model->article), - 'lastmod' => $this->model->updated_at->toIso8601String(), + 'lastmod' => $this->model->updated_at, 'priority' => 0.9, ];