From d766fe602810861ffc75b4a776bd60e83e41ba09 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 30 Aug 2025 19:42:00 +0300 Subject: [PATCH] Add `make:feed` and `make:feed-item` commands --- README.md | 154 +++++++++++++++++- ide.json | 7 +- phpunit.xml | 3 + src/Concerns/InteractsWithName.php | 28 ++++ src/Console/Commands/FeedItemMakeCommand.php | 46 ++++++ src/Console/Commands/FeedMakeCommand.php | 35 ++++ src/LaravelFeedServiceProvider.php | 4 + stubs/feed.stub | 5 +- stubs/feed_item.stub | 6 +- .../Console/MakeItemTest/make_feed_item.snap | 30 ++++ .../Unit/Console/MakeTest/make_feed.snap | 34 ++++ tests/Expectations.php | 19 +++ tests/Helpers/cleanup.php | 17 ++ tests/Helpers/paths.php | 16 ++ tests/Pest.php | 4 + tests/Unit/Console/MakeItemTest.php | 18 ++ tests/Unit/Console/MakeTest.php | 18 ++ workbench/app/Feeds/EmptyFeed.php | 5 - workbench/app/Feeds/FilledFeed.php | 2 +- 19 files changed, 429 insertions(+), 22 deletions(-) create mode 100644 src/Concerns/InteractsWithName.php create mode 100644 src/Console/Commands/FeedItemMakeCommand.php create mode 100644 src/Console/Commands/FeedMakeCommand.php create mode 100644 tests/.pest/snapshots/Unit/Console/MakeItemTest/make_feed_item.snap create mode 100644 tests/.pest/snapshots/Unit/Console/MakeTest/make_feed.snap create mode 100644 tests/Expectations.php create mode 100644 tests/Helpers/cleanup.php create mode 100644 tests/Helpers/paths.php create mode 100644 tests/Unit/Console/MakeItemTest.php create mode 100644 tests/Unit/Console/MakeTest.php diff --git a/README.md b/README.md index 7572e54..2e68d8f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,26 @@ After that, publish the configuration file by call the console command: php artisan vendor:publish --tag=feeds ``` -## Basic Usage +## Usage + +### Create Feeds and Feed Items + +#### Use a Laravel Idea plugin for PHP Storm + +You can also easily create the desired classes using the [Laravel Idea](http://laravel-idea.com) plugin +for [PhpStorm](https://www.jetbrains.com/phpstorm/): + +![](.github/images/idea.png) + +#### Use Artisan + +```bash +# This will create a `app/Feeds/UserFeed.php` file +php artisan make:feed User + +# This will create a `app/Feeds/Items/UserFeedItem.php` file +php artisan make:feed-item User +``` ### Generate Feeds @@ -36,9 +55,38 @@ console command: php artisan feed:generate ``` -### Feed +Each feed can be created in a certain folder of a certain storage. + +To indicate the storage, reduce the property of `$storage` in the feed class: + +```php +class UserFeed extends Feed +{ + protected string $storage = 'public'; +} +``` + +By default, `public`. + +The path to the file inside the storage is indicated in the `filiname` method: + +```php +class UserFeed extends Feed +{ + public function filename(): string + { + return 'your/path/may/be/here.xml'; + } +} +``` + +By default, the class name in `kebab-case` is used. For example, `user-feed.xml` for `UserFeed` class. + +### Filling -Create a feed class. For example: +#### Feed + +For example, we use this content for the Feed class: ```php namespace App\Feeds; @@ -78,9 +126,9 @@ class UserFeed extends Feed } ``` -### Feed Item +#### Feed Item -Create a feed item class. For example: +For example, we use this content for the Feed Item class: ```php namespace App\Feeds\Items; @@ -155,12 +203,100 @@ According to this example, the XML file with the following contents will be gene ``` -### Laravel Idea Support +## Objects, attributes and more -You can also easily create the desired classes using the [Laravel Idea](http://laravel-idea.com) plugin -for [PhpStorm](https://www.jetbrains.com/phpstorm/): +### Setting the name of the root element -![](.github/images/idea.png) +```php +class UserFeed extends Feed +{ + public function rootItem(): ?string + { + return 'users'; + } +} +``` + +### Adding attributes for the main section + +```php +class UserFeedItem extends FeedItem +{ + public function attributes(): array + { + return [ + 'id' => $this->model->id, + 'created_at' => $this->model->created_at->format('Y-m-d'), + ]; + } + + public function toArray(): array + { + // ... + } +} +``` + +### Adding attributes for nested elements + +> [!NOTE] +> +> Reserved names are: +> +> - `@attributes` +> - `@cdata` +> - `@mixed` + +```php +class UserFeedItem extends FeedItem +{ + public function toArray(): array + { + return [ + 'name' => $this->model->name, + 'email' => $this->model->email, + + 'header' => [ + '@cdata' => '

' . $this->model->name . '

', + ], + + 'names' => [ + 'Good guy' => [ + '@attributes' => [ + 'my-key-1' => 'my value 1', + 'my-key-2' => 'my value 2', + ], + + 'name' => 'Luke Skywalker', + 'weapon' => 'Lightsaber', + ], + + 'Bad guy' => [ + 'name' => [ + '@cdata' => '

Sauron

', + ], + + 'weapon' => 'Evil Eye', + ], + ], + ]; + } +} +``` + +### Header information + +If it is necessary to change the file cap, override the `header` method in the feed class: + +```php +class UserFeed extends Feed +{ + public function header(): string + { + return ''; + } +} +``` ## License diff --git a/ide.json b/ide.json index 5bd7e62..edac5a1 100644 --- a/ide.json +++ b/ide.json @@ -15,7 +15,9 @@ "path": "/stubs/feed.stub", "parameters": { "DummyNamespace": "${INPUT_FQN|namespace}", - "DummyClass": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}" + "DummyClass": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}", + "NamespacedDummyUserModel": "App\\Models\\User", + "DummyUser": "User" } } } @@ -35,7 +37,8 @@ "path": "/stubs/feed_item.stub", "parameters": { "DummyNamespace": "${INPUT_FQN|namespace}", - "DummyClass": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}" + "DummyClass": "${INPUT_CLASS|replace: ,_|className|upperCamelCase}", + "NamespacedDummyUserModel": "App\\Models\\User" } } } diff --git a/phpunit.xml b/phpunit.xml index 0d3debd..0f2a031 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,9 @@ ./tests/Feature + + ./tests/Unit + diff --git a/src/Concerns/InteractsWithName.php b/src/Concerns/InteractsWithName.php new file mode 100644 index 0000000..d1117ef --- /dev/null +++ b/src/Concerns/InteractsWithName.php @@ -0,0 +1,28 @@ +type); + } + + protected function buildClass($name): string + { + return str_replace( + ['DummyUser'], + class_basename($this->userProviderModel()), + parent::buildClass($name) + ); + } +} diff --git a/src/Console/Commands/FeedItemMakeCommand.php b/src/Console/Commands/FeedItemMakeCommand.php new file mode 100644 index 0000000..366da94 --- /dev/null +++ b/src/Console/Commands/FeedItemMakeCommand.php @@ -0,0 +1,46 @@ +userProviderModel(), + parent::buildClass($name) + ); + } + + protected function getStub(): string + { + return __DIR__ . '/../../../stubs/feed_item.stub'; + } + + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace . '\Feeds\Items'; + } + + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the feed already exists'], + ]; + } +} diff --git a/src/Console/Commands/FeedMakeCommand.php b/src/Console/Commands/FeedMakeCommand.php new file mode 100644 index 0000000..7c8cd48 --- /dev/null +++ b/src/Console/Commands/FeedMakeCommand.php @@ -0,0 +1,35 @@ +commands([ FeedGenerateCommand::class, + FeedMakeCommand::class, + FeedItemMakeCommand::class, ]); } } diff --git a/stubs/feed.stub b/stubs/feed.stub index aaeecb5..5acf84a 100644 --- a/stubs/feed.stub +++ b/stubs/feed.stub @@ -8,12 +8,13 @@ use DragonCode\LaravelFeed\Feed; use DragonCode\LaravelFeed\FeedItem; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use NamespacedDummyUserModel; class DummyClass extends Feed { public function builder(): Builder { - return \App\Models\User::query(); + return DummyUser::query(); } public function rootItem(): ?string @@ -28,6 +29,6 @@ class DummyClass extends Feed public function item(Model $model): FeedItem { - return new \App\Feeds\Items\UserFeedItem($model); + return new Items\DummyUserFeedItem($model); } } diff --git a/stubs/feed_item.stub b/stubs/feed_item.stub index ec21a2a..c9b1743 100644 --- a/stubs/feed_item.stub +++ b/stubs/feed_item.stub @@ -6,13 +6,13 @@ namespace DummyNamespace; use DragonCode\LaravelFeed\FeedItem; -/** @property-read \App\Models\User $model */ +/** @property-read NamespacedDummyUserModel $model */ class DummyClass extends FeedItem { public function toArray(): array { return [ - '_attributes' => [ + '@attributes' => [ 'id' => $this->model->id, 'updated_at' => $this->model->updated_at->toDateTimeString(), @@ -21,7 +21,7 @@ class DummyClass extends FeedItem ], 'name' => [ - '_cdata' => '

' . $this->model->name . '

', + '@cdata' => '

' . $this->model->name . '

', ], 'email' => $this->model->email, diff --git a/tests/.pest/snapshots/Unit/Console/MakeItemTest/make_feed_item.snap b/tests/.pest/snapshots/Unit/Console/MakeItemTest/make_feed_item.snap new file mode 100644 index 0000000..65996ee --- /dev/null +++ b/tests/.pest/snapshots/Unit/Console/MakeItemTest/make_feed_item.snap @@ -0,0 +1,30 @@ + [ + 'id' => $this->model->id, + + 'updated_at' => $this->model->updated_at->toDateTimeString(), + + 'verified' => ! empty($this->model->email_verified_at), + ], + + 'name' => [ + '@cdata' => '

' . $this->model->name . '

', + ], + + 'email' => $this->model->email, + ]; + } +} diff --git a/tests/.pest/snapshots/Unit/Console/MakeTest/make_feed.snap b/tests/.pest/snapshots/Unit/Console/MakeTest/make_feed.snap new file mode 100644 index 0000000..f9a2946 --- /dev/null +++ b/tests/.pest/snapshots/Unit/Console/MakeTest/make_feed.snap @@ -0,0 +1,34 @@ +extend('toMatchFeedSnapshot', function () { + $content = file_get_contents(feedPath($this->value . 'Feed')); + + expect($content)->toMatchSnapshot(); + + return $this; +}); + +expect()->extend('toMatchFeedItemSnapshot', function () { + $content = file_get_contents(feedPath('Items/' . $this->value . 'FeedItem')); + + expect($content)->toMatchSnapshot(); + + return $this; +}); diff --git a/tests/Helpers/cleanup.php b/tests/Helpers/cleanup.php new file mode 100644 index 0000000..6f018b7 --- /dev/null +++ b/tests/Helpers/cleanup.php @@ -0,0 +1,17 @@ +delete( + app_path($filename) + ); +} + +function deleteFeed(string $name): void +{ + deleteFile(feedPath($name)); +} diff --git a/tests/Helpers/paths.php b/tests/Helpers/paths.php new file mode 100644 index 0000000..85700b8 --- /dev/null +++ b/tests/Helpers/paths.php @@ -0,0 +1,16 @@ +extend(TestCase::class) ->use(RefreshDatabase::class) ->in('Feature'); + +pest() + ->extend(TestCase::class) + ->in('Unit'); diff --git a/tests/Unit/Console/MakeItemTest.php b/tests/Unit/Console/MakeItemTest.php new file mode 100644 index 0000000..0f9e3f5 --- /dev/null +++ b/tests/Unit/Console/MakeItemTest.php @@ -0,0 +1,18 @@ + 'FooBar', + '--force' => true, + ])->assertSuccessful()->run(); + + expect('FooBar')->toMatchFeedItemSnapshot(); +}); diff --git a/tests/Unit/Console/MakeTest.php b/tests/Unit/Console/MakeTest.php new file mode 100644 index 0000000..b1d9af9 --- /dev/null +++ b/tests/Unit/Console/MakeTest.php @@ -0,0 +1,18 @@ + 'FooBar', + '--force' => true, + ])->assertSuccessful()->run(); + + expect('FooBar')->toMatchFeedSnapshot(); +}); diff --git a/workbench/app/Feeds/EmptyFeed.php b/workbench/app/Feeds/EmptyFeed.php index c48b4bf..0dc6644 100644 --- a/workbench/app/Feeds/EmptyFeed.php +++ b/workbench/app/Feeds/EmptyFeed.php @@ -21,9 +21,4 @@ public function rootItem(): ?string { return class_basename($this); } - - public function filename(): string - { - return 'empty/feed.xml'; - } } diff --git a/workbench/app/Feeds/FilledFeed.php b/workbench/app/Feeds/FilledFeed.php index a5f3221..6134e2a 100644 --- a/workbench/app/Feeds/FilledFeed.php +++ b/workbench/app/Feeds/FilledFeed.php @@ -28,7 +28,7 @@ public function rootItem(): ?string public function filename(): string { - return 'partial/feed.xml'; + return 'nested/filled.xml'; } public function item(Model $model): FeedItem