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,
];