diff --git a/README.md b/README.md index a20d54b..e171c4d 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,39 @@ class UserFeed extends Feed } ``` +#### Setting the head info + +In some cases, you need to add various information to the beginning of the file. +To do this, use the `info` method: + +```php +use DragonCode\LaravelFeed\Data\ElementData; +use DragonCode\LaravelFeed\Feeds\Feed; +use DragonCode\LaravelFeed\Feeds\Info\FeedInfo; + +class UserFeed extends Feed +{ + public function info(): FeedInfo + { + return new FeedInfo(); + } +} +``` + +```php +use DragonCode\LaravelFeed\Feeds\Info\FeedInfo; + +class UserFeedInfo extends FeedInfo +{ + public function toArray(): array + { + return [ + // ... + ]; + } +} +``` + #### Adding attributes for the main section ```php diff --git a/composer.json b/composer.json index 5c67c7f..596ea66 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "require": { "php": "^8.2", "ext-dom": "*", + "ext-libxml": ">=2.6.21", "illuminate/database": "^11.0 || ^12.0", "illuminate/filesystem": "^11.0 || ^12.0", "illuminate/support": "^11.0 || ^12.0", diff --git a/ide.json b/ide.json index 6045603..d57bac0 100644 --- a/ide.json +++ b/ide.json @@ -45,6 +45,27 @@ } } ] + }, + { + "id": "dragon-code.xml-feeds.info", + "name": "Create XML Feed Info", + "classSuffix": "FeedInfo", + "regex": ".+", + "files": [ + { + "appNamespace": "Feeds\\Info", + "name": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}.php", + "template": { + "type": "stub", + "path": "/stubs/feed_info.stub", + "fallbackPath": "stubs/feed_info.stub", + "parameters": { + "DummyNamespace": "${INPUT_FQN|namespace}", + "DummyClass": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}" + } + } + } + ] } ] } diff --git a/src/Console/Commands/FeedInfoMakeCommand.php b/src/Console/Commands/FeedInfoMakeCommand.php new file mode 100644 index 0000000..3068198 --- /dev/null +++ b/src/Console/Commands/FeedInfoMakeCommand.php @@ -0,0 +1,35 @@ +filename ??= Str::kebab(class_basename($this)) . '.xml'; diff --git a/src/Feeds/Info/FeedInfo.php b/src/Feeds/Info/FeedInfo.php new file mode 100644 index 0000000..14aa3fe --- /dev/null +++ b/src/Feeds/Info/FeedInfo.php @@ -0,0 +1,15 @@ +commands([ FeedGenerateCommand::class, - FeedMakeCommand::class, + FeedInfoMakeCommand::class, FeedItemMakeCommand::class, + FeedMakeCommand::class, ]); } } diff --git a/src/Services/ConvertToXml.php b/src/Services/ConvertToXml.php index ae72923..c0bd13a 100644 --- a/src/Services/ConvertToXml.php +++ b/src/Services/ConvertToXml.php @@ -5,7 +5,7 @@ namespace DragonCode\LaravelFeed\Services; use DOMDocument; -use DOMElement; +use DOMNode; use DragonCode\LaravelFeed\Feeds\Items\FeedItem; use Illuminate\Container\Attributes\Config; use Illuminate\Support\Str; @@ -31,7 +31,7 @@ public function __construct( $this->document->preserveWhiteSpace = ! $pretty; } - public function convert(FeedItem $item): string + public function convertItem(FeedItem $item): string { $box = $this->performBox($item); @@ -40,7 +40,16 @@ public function convert(FeedItem $item): string return $this->toXml($box); } - protected function performBox(FeedItem $item): DOMElement + public function convertInfo(array $info): string + { + $box = $this->document->createDocumentFragment(); + + $this->performItem($box, $info); + + return $this->toXml($box); + } + + protected function performBox(FeedItem $item): DOMNode { $element = $this->createElement($item->name()); @@ -51,7 +60,7 @@ protected function performBox(FeedItem $item): DOMElement return $element; } - protected function performItem(DOMElement $parent, array $items): void + protected function performItem(DOMNode $parent, array $items): void { foreach ($items as $key => $value) { $key = $this->convertKey($key); @@ -92,26 +101,26 @@ protected function isPrefixed(string $key): bool return str_starts_with($key, '@'); } - protected function createElement(string $name, bool|float|int|string|null $value = ''): DOMElement + protected function createElement(string $name, bool|float|int|string|null $value = ''): DOMNode { return $this->document->createElement($name, $this->convertValue($value)); } - protected function setAttributes(DOMElement $element, array $attributes): void + protected function setAttributes(DOMNode $element, array $attributes): void { foreach ($attributes as $key => $value) { $element->setAttribute($key, $this->convertValue($value)); } } - protected function setCData(DOMElement $element, string $value): void + protected function setCData(DOMNode $element, string $value): void { $element->appendChild( $this->document->createCDATASection($value) ); } - protected function setMixed(DOMElement $element, string $value): void + protected function setMixed(DOMNode $element, string $value): void { $fragment = $this->document->createDocumentFragment(); $fragment->appendXML($value); @@ -119,7 +128,7 @@ protected function setMixed(DOMElement $element, string $value): void $element->appendChild($fragment); } - protected function setItemsArray(DOMElement $parent, $value, string $key): void + protected function setItemsArray(DOMNode $parent, $value, string $key): void { $key = Str::substr($key, 1); @@ -128,7 +137,7 @@ protected function setItemsArray(DOMElement $parent, $value, string $key): void } } - protected function setItems(DOMElement $parent, string $key, mixed $value): void + protected function setItems(DOMNode $parent, string $key, mixed $value): void { $element = $this->createElement($key, is_array($value) ? '' : $this->convertValue($value)); @@ -139,14 +148,14 @@ protected function setItems(DOMElement $parent, string $key, mixed $value): void $parent->appendChild($element); } - protected function setRaw(DOMElement $parent, mixed $value): void + protected function setRaw(DOMNode $parent, mixed $value): void { $parent->nodeValue = $this->convertValue($value); } - protected function toXml(DOMElement $item): string + protected function toXml(DOMNode $item): string { - return $this->document->saveXML($item); + return $this->document->saveXML($item, LIBXML_COMPACT); } protected function convertKey(int|string $key): string diff --git a/src/Services/Generator.php b/src/Services/Generator.php index c076666..962335a 100644 --- a/src/Services/Generator.php +++ b/src/Services/Generator.php @@ -8,6 +8,7 @@ use DragonCode\LaravelFeed\Feeds\Feed; use Illuminate\Database\Eloquent\Collection; +use function blank; use function collect; use function implode; use function sprintf; @@ -26,6 +27,8 @@ public function feed(Feed $feed): void ); $this->performHeader($file, $feed); + $this->performInfo($file, $feed); + $this->performRoot($file, $feed); $this->performItem($file, $feed); $this->performFooter($file, $feed); @@ -38,7 +41,7 @@ protected function performItem($file, Feed $feed): void $content = []; foreach ($models as $model) { - $content[] = $this->converter->convert( + $content[] = $this->converter->convertItem( $feed->item($model) ); } @@ -49,14 +52,30 @@ protected function performItem($file, Feed $feed): void protected function performHeader($file, Feed $feed): void { - $value = $feed->header(); + $this->append($file, $feed->header()); + } - if ($name = $feed->root()->name) { - $value .= ! empty($feed->root()->attributes) - ? sprintf("\n<%s %s>\n", $name, $this->makeRootAttributes($feed->root())) - : sprintf("\n<%s>\n", $name); + protected function performInfo($file, Feed $feed): void + { + if (blank($info = $feed->info()->toArray())) { + return; + } + + $value = $this->converter->convertInfo($info); + + $this->append($file, PHP_EOL . $value); + } + + protected function performRoot($file, Feed $feed): void + { + if (! $name = $feed->root()->name) { + return; } + $value = ! empty($feed->root()->attributes) + ? sprintf("\n<%s %s>\n", $name, $this->makeRootAttributes($feed->root())) + : sprintf("\n<%s>\n", $name); + $this->append($file, $value); } diff --git a/stubs/feed_info.php b/stubs/feed_info.php new file mode 100644 index 0000000..6d45f30 --- /dev/null +++ b/stubs/feed_info.php @@ -0,0 +1,17 @@ + - Laravel - Laravel - http://localhost - Laravel - test@example.com - - - - - Домашние майки - Велосипедки - Ремни - +Laravel +Laravel +Laravel +http://localhost +test@example.com + + + + + Домашние майки + Велосипедки + Ремни + + http://localhost/products/GD-PRDCT-1 diff --git a/tests/.pest/snapshots/Unit/Console/MakeInfoTest/make_feed_item.snap b/tests/.pest/snapshots/Unit/Console/MakeInfoTest/make_feed_item.snap new file mode 100644 index 0000000..755a702 --- /dev/null +++ b/tests/.pest/snapshots/Unit/Console/MakeInfoTest/make_feed_item.snap @@ -0,0 +1,17 @@ +extend('toMatchFeedInfoSnapshot', function () { + $content = file_get_contents(feedPath('Info/' . $this->value . 'FeedInfo')); + + expect($content)->toMatchSnapshot(); + + return $this; +}); diff --git a/tests/Unit/Console/MakeInfoTest.php b/tests/Unit/Console/MakeInfoTest.php new file mode 100644 index 0000000..109dee2 --- /dev/null +++ b/tests/Unit/Console/MakeInfoTest.php @@ -0,0 +1,18 @@ + 'FooBar', + '--force' => true, + ])->assertSuccessful()->run(); + + expect('FooBar')->toMatchFeedInfoSnapshot(); +}); diff --git a/workbench/app/Feeds/Info/YandexFeedInfo.php b/workbench/app/Feeds/Info/YandexFeedInfo.php new file mode 100644 index 0000000..3f9e710 --- /dev/null +++ b/workbench/app/Feeds/Info/YandexFeedInfo.php @@ -0,0 +1,50 @@ + config('app.name'), + 'company' => config('app.name'), + 'platform' => config('app.name'), + + 'url' => config('app.url'), + 'email' => config('emails.manager'), + + 'currencies' => [ + '@currency' => [ + [ + '@attributes' => [ + 'id' => 'RUR', + 'rate' => '1', + ], + ], + ], + ], + + 'categories' => [ + '@category' => [ + [ + '@attributes' => ['id' => 41], + '@value' => 'Домашние майки', + ], + [ + '@attributes' => ['id' => 539], + '@value' => 'Велосипедки', + ], + [ + '@attributes' => ['id' => 44], + '@value' => 'Ремни', + ], + ], + ], + ]; + } +} diff --git a/workbench/app/Feeds/YandexFeed.php b/workbench/app/Feeds/YandexFeed.php index 4860433..2786182 100644 --- a/workbench/app/Feeds/YandexFeed.php +++ b/workbench/app/Feeds/YandexFeed.php @@ -6,9 +6,11 @@ use DragonCode\LaravelFeed\Data\ElementData; use DragonCode\LaravelFeed\Feeds\Feed; +use DragonCode\LaravelFeed\Feeds\Info\FeedInfo; use DragonCode\LaravelFeed\Feeds\Items\FeedItem; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Workbench\App\Feeds\Info\YandexFeedInfo; use Workbench\App\Models\Product; class YandexFeed extends Feed @@ -25,28 +27,12 @@ public function root(): ElementData public function header(): string { - $date = '2025-08-30T21:14:49+00:00'; - $name = config('app.name'); - $url = config('app.url'); - $email = config('emails.manager'); + $date = '2025-08-30T21:14:49+00:00'; return << - $name - $name - $url - $name - $email - - - - - Домашние майки - Велосипедки - Ремни - XML; } @@ -55,13 +41,18 @@ public function footer(): string return "\n\n"; } - public function filename(): string + public function info(): FeedInfo { - return 'yandex.xml'; + return new YandexFeedInfo; } public function item(Model $model): FeedItem { return new Items\YandexFeedItem($model); } + + public function filename(): string + { + return 'yandex.xml'; + } }