From 80ac64d1f0e6f56923bc8ab441c03a47c6c82b95 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Mon, 1 Sep 2025 21:47:12 +0300 Subject: [PATCH 1/2] Improved feed class generation --- composer.json | 10 +-- docs/topics/generation.topic | 71 +++++++++++++------ src/Console/Commands/FeedGenerateCommand.php | 18 +++-- src/Exceptions/FeedNotFoundException.php | 15 ++++ src/Exceptions/UnexpectedFeedException.php | 18 +++++ src/Helpers/FeedHelper.php | 58 +++++++++++++++ .../export_with_data_set____false__.snap | 0 .../export_with_data_set____true__.snap | 0 .../export_with_data_set____false__.snap | 0 .../export_with_data_set____true__.snap | 0 .../export_with_data_set____false__.snap | 0 .../export_with_data_set____true__.snap | 0 .../{ => Feeds}/SitemapTest/export.snap | 0 .../{ => Feeds}/YandexTest/export.snap | 0 .../Console/Generation/DefaultTest.php | 23 ++++++ .../Console/Generation/DisabledTest.php | 32 +++++++++ .../Generation/IncorrectParameterTest.php | 44 ++++++++++++ .../Console/Generation/SpecifiedTest.php | 34 +++++++++ .../Console/Generation/UnknownClassTest.php | 26 +++++++ tests/Feature/{ => Feeds}/EmptyTest.php | 0 tests/Feature/{ => Feeds}/FullTest.php | 0 tests/Feature/{ => Feeds}/PartialTest.php | 0 tests/Feature/{ => Feeds}/SitemapTest.php | 0 tests/Feature/{ => Feeds}/YandexTest.php | 0 tests/Helpers/cleanup.php | 17 ++++- tests/Helpers/expects.php | 4 +- tests/Pest.php | 20 ++++++ 27 files changed, 351 insertions(+), 39 deletions(-) create mode 100644 src/Exceptions/FeedNotFoundException.php create mode 100644 src/Exceptions/UnexpectedFeedException.php create mode 100644 src/Helpers/FeedHelper.php rename tests/.pest/snapshots/Feature/{ => Feeds}/EmptyTest/export_with_data_set____false__.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/EmptyTest/export_with_data_set____true__.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/FullTest/export_with_data_set____false__.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/FullTest/export_with_data_set____true__.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/PartialTest/export_with_data_set____false__.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/PartialTest/export_with_data_set____true__.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/SitemapTest/export.snap (100%) rename tests/.pest/snapshots/Feature/{ => Feeds}/YandexTest/export.snap (100%) create mode 100644 tests/Feature/Console/Generation/DefaultTest.php create mode 100644 tests/Feature/Console/Generation/DisabledTest.php create mode 100644 tests/Feature/Console/Generation/IncorrectParameterTest.php create mode 100644 tests/Feature/Console/Generation/SpecifiedTest.php create mode 100644 tests/Feature/Console/Generation/UnknownClassTest.php rename tests/Feature/{ => Feeds}/EmptyTest.php (100%) rename tests/Feature/{ => Feeds}/FullTest.php (100%) rename tests/Feature/{ => Feeds}/PartialTest.php (100%) rename tests/Feature/{ => Feeds}/SitemapTest.php (100%) rename tests/Feature/{ => Feeds}/YandexTest.php (100%) diff --git a/composer.json b/composer.json index 948b75a..95bc30b 100644 --- a/composer.json +++ b/composer.json @@ -66,13 +66,7 @@ "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", "style": "vendor/bin/pint --parallel --ansi", - "test": [ - "@clear", - "@php vendor/bin/pest --colors=always" - ], - "test:update": [ - "@clear", - "@php vendor/bin/pest --colors=always --update-snapshots" - ] + "test": "@php vendor/bin/pest --colors=always", + "test:update": "@php vendor/bin/pest --colors=always --update-snapshots" } } diff --git a/docs/topics/generation.topic b/docs/topics/generation.topic index 2c52e55..5278af7 100644 --- a/docs/topics/generation.topic +++ b/docs/topics/generation.topic @@ -12,39 +12,64 @@ - + + +

+ To generate feeds, create the classes of feeds and its element, add links to the file + %config-filename%, next call the console command: +

+ + + %command-generate% + +
+
+ + + + Please note that the specified feed will be executed even if it is disabled in the settings file. + + +

+ To generate a specific feed class, specify the class reference or name relative to the + App\Feeds namespace. +

+

- To generate feeds, create the classes of feeds and its element, add links to the file - %config-filename%, next call the console command: + For example:

- %command-generate% + %command-generate% App\Feeds\UserFeed + %command-generate% UserFeed + %command-generate% User -
+ -

- Each feed can be created in a certain folder of a certain storage. -

+ +

+ Each feed can be created in a certain folder of a certain storage. +

-

- To indicate the storage, override the property of $storage in the feed class: -

+

+ To indicate the storage, override the property of $storage in the feed class: +

- + -

- By default, storage is public. -

+

+ By default, storage is public. +

-

- The path to the file inside the storage is indicated in the filename method: -

+

+ The path to the file inside the storage is indicated in the filename method: +

- + -

- By default, the class name in kebab-case is used. - For example, user-feed.xml for UserFeed class. -

+

+ By default, the class name in kebab-case is used. + For example, user-feed.xml for UserFeed class. +

+
diff --git a/src/Console/Commands/FeedGenerateCommand.php b/src/Console/Commands/FeedGenerateCommand.php index 66fb534..5d81bb6 100644 --- a/src/Console/Commands/FeedGenerateCommand.php +++ b/src/Console/Commands/FeedGenerateCommand.php @@ -4,6 +4,7 @@ namespace DragonCode\LaravelFeed\Console\Commands; +use DragonCode\LaravelFeed\Helpers\FeedHelper; use DragonCode\LaravelFeed\Services\Generator; use Illuminate\Console\Command; use Laravel\Prompts\Concerns\Colors; @@ -18,24 +19,33 @@ class FeedGenerateCommand extends Command { use Colors; - public function handle(Generator $generator): void + public function handle(Generator $generator, FeedHelper $helper): void { - foreach ($this->feedable() as $feed => $enabled) { + foreach ($this->feedable($helper) as $feed => $enabled) { $enabled ? $this->components->task($feed, fn () => $generator->feed(app($feed))) : $this->components->twoColumnDetail($feed, $this->messageYellow('SKIP')); } } - protected function feedable(): array + protected function feedable(FeedHelper $helper): array { - if ($feed = $this->argument('class')) { + if ($feed = $this->resolveFeedClass($helper)) { return [$feed => true]; } return config('feeds.channels'); } + protected function resolveFeedClass(FeedHelper $helper): ?string + { + if (! $class = $this->argument('class')) { + return null; + } + + return $helper->find((string) $class); + } + protected function messageYellow(string $message): string { if ($this->option('no-ansi')) { diff --git a/src/Exceptions/FeedNotFoundException.php b/src/Exceptions/FeedNotFoundException.php new file mode 100644 index 0000000..e9d5c0f --- /dev/null +++ b/src/Exceptions/FeedNotFoundException.php @@ -0,0 +1,15 @@ +ensure($class); + } + + if (class_exists($class = $this->resolve($class))) { + return $this->ensure($class); + } + + throw new FeedNotFoundException($class); + } + + protected function resolve(string $class): string + { + return Str::of($class) + ->replace('/', '\\') + ->ltrim('\\') + ->start($this->rootNamespace() . 'Feeds\\') + ->finish('Feed') + ->toString(); + } + + protected function ensure(string $class): string + { + if (! is_a($class, Feed::class, true)) { + throw new UnexpectedFeedException($class); + } + + return $class; + } + + protected function rootNamespace(): string + { + return $this->laravel->getNamespace(); + } +} diff --git a/tests/.pest/snapshots/Feature/EmptyTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/EmptyTest/export_with_data_set____false__.snap similarity index 100% rename from tests/.pest/snapshots/Feature/EmptyTest/export_with_data_set____false__.snap rename to tests/.pest/snapshots/Feature/Feeds/EmptyTest/export_with_data_set____false__.snap diff --git a/tests/.pest/snapshots/Feature/EmptyTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/EmptyTest/export_with_data_set____true__.snap similarity index 100% rename from tests/.pest/snapshots/Feature/EmptyTest/export_with_data_set____true__.snap rename to tests/.pest/snapshots/Feature/Feeds/EmptyTest/export_with_data_set____true__.snap diff --git a/tests/.pest/snapshots/Feature/FullTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____false__.snap similarity index 100% rename from tests/.pest/snapshots/Feature/FullTest/export_with_data_set____false__.snap rename to tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____false__.snap diff --git a/tests/.pest/snapshots/Feature/FullTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____true__.snap similarity index 100% rename from tests/.pest/snapshots/Feature/FullTest/export_with_data_set____true__.snap rename to tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____true__.snap diff --git a/tests/.pest/snapshots/Feature/PartialTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____false__.snap similarity index 100% rename from tests/.pest/snapshots/Feature/PartialTest/export_with_data_set____false__.snap rename to tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____false__.snap diff --git a/tests/.pest/snapshots/Feature/PartialTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____true__.snap similarity index 100% rename from tests/.pest/snapshots/Feature/PartialTest/export_with_data_set____true__.snap rename to tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____true__.snap diff --git a/tests/.pest/snapshots/Feature/SitemapTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/SitemapTest/export.snap similarity index 100% rename from tests/.pest/snapshots/Feature/SitemapTest/export.snap rename to tests/.pest/snapshots/Feature/Feeds/SitemapTest/export.snap diff --git a/tests/.pest/snapshots/Feature/YandexTest/export.snap b/tests/.pest/snapshots/Feature/Feeds/YandexTest/export.snap similarity index 100% rename from tests/.pest/snapshots/Feature/YandexTest/export.snap rename to tests/.pest/snapshots/Feature/Feeds/YandexTest/export.snap diff --git a/tests/Feature/Console/Generation/DefaultTest.php b/tests/Feature/Console/Generation/DefaultTest.php new file mode 100644 index 0000000..08dc4ed --- /dev/null +++ b/tests/Feature/Console/Generation/DefaultTest.php @@ -0,0 +1,23 @@ +collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => $command->expectsOutputToContain($feed)); + + $command->assertSuccessful()->run(); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => expect(app($feed)->path())->toBeReadableFile()); +}); diff --git a/tests/Feature/Console/Generation/DisabledTest.php b/tests/Feature/Console/Generation/DisabledTest.php new file mode 100644 index 0000000..9fa0476 --- /dev/null +++ b/tests/Feature/Console/Generation/DisabledTest.php @@ -0,0 +1,32 @@ +set('feeds.channels.' . SitemapFeed::class, false); + config()?->set('feeds.channels.' . YandexFeed::class, false); + + $command = artisan(FeedGenerateCommand::class); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => $command->expectsOutputToContain($feed)); + + $command->assertSuccessful()->run(); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => match ($feed) { + SitemapFeed::class, + YandexFeed::class => expect(app($feed)->path())->not->toBeReadableFile(), + default => expect(app($feed)->path())->toBeReadableFile() + }); +}); diff --git a/tests/Feature/Console/Generation/IncorrectParameterTest.php b/tests/Feature/Console/Generation/IncorrectParameterTest.php new file mode 100644 index 0000000..fe48160 --- /dev/null +++ b/tests/Feature/Console/Generation/IncorrectParameterTest.php @@ -0,0 +1,44 @@ + $name, + ])->run(); +}) + ->throws(FeedNotFoundException::class) + ->with([ + 'foo=bar', + 'foo+bar', + 'foo bar', + '123', + 123, + ]); + +test('may be correct', function (mixed $name) { + $command = artisan(FeedGenerateCommand::class, [ + 'class' => $name, + ]); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => $command->expectsOutputToContain($feed)); + + $command->assertSuccessful()->run(); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => expect(app($feed)->path())->toBeReadableFile()); +})->with([ + '', + 0, + null, +]); diff --git a/tests/Feature/Console/Generation/SpecifiedTest.php b/tests/Feature/Console/Generation/SpecifiedTest.php new file mode 100644 index 0000000..7649a00 --- /dev/null +++ b/tests/Feature/Console/Generation/SpecifiedTest.php @@ -0,0 +1,34 @@ + SitemapFeed::class, + ]); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each( + fn (string $feed) => $feed === SitemapFeed::class + ? $command->expectsOutputToContain($feed) + : $command->doesntExpectOutputToContain($feed) + ); + + $command->assertSuccessful()->run(); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each( + fn (string $feed) => $feed === SitemapFeed::class + ? expect(app($feed)->path())->toBeReadableFile() + : expect(app($feed)->path())->not->toBeReadableFile() + ); +}); diff --git a/tests/Feature/Console/Generation/UnknownClassTest.php b/tests/Feature/Console/Generation/UnknownClassTest.php new file mode 100644 index 0000000..5951a9a --- /dev/null +++ b/tests/Feature/Console/Generation/UnknownClassTest.php @@ -0,0 +1,26 @@ + $feed, + ])->run(); +}) + ->throws(UnexpectedFeedException::class) + ->with([ + TestCase::class, + WorkbenchServiceProvider::class, + + YandexFeedItem::class, + YandexFeedInfo::class, + ]); diff --git a/tests/Feature/EmptyTest.php b/tests/Feature/Feeds/EmptyTest.php similarity index 100% rename from tests/Feature/EmptyTest.php rename to tests/Feature/Feeds/EmptyTest.php diff --git a/tests/Feature/FullTest.php b/tests/Feature/Feeds/FullTest.php similarity index 100% rename from tests/Feature/FullTest.php rename to tests/Feature/Feeds/FullTest.php diff --git a/tests/Feature/PartialTest.php b/tests/Feature/Feeds/PartialTest.php similarity index 100% rename from tests/Feature/PartialTest.php rename to tests/Feature/Feeds/PartialTest.php diff --git a/tests/Feature/SitemapTest.php b/tests/Feature/Feeds/SitemapTest.php similarity index 100% rename from tests/Feature/SitemapTest.php rename to tests/Feature/Feeds/SitemapTest.php diff --git a/tests/Feature/YandexTest.php b/tests/Feature/Feeds/YandexTest.php similarity index 100% rename from tests/Feature/YandexTest.php rename to tests/Feature/Feeds/YandexTest.php diff --git a/tests/Helpers/cleanup.php b/tests/Helpers/cleanup.php index 6f018b7..e2216db 100644 --- a/tests/Helpers/cleanup.php +++ b/tests/Helpers/cleanup.php @@ -7,11 +7,22 @@ function deleteFile(string $filename): void { app(Filesystem::class)->delete( - app_path($filename) + $filename ); } -function deleteFeed(string $name): void +function deleteFeed(string $feedName): void { - deleteFile(feedPath($name)); + $path = app_path( + feedPath($feedName) + ); + + deleteFile($path); +} + +function deleteFeedResult(string $feedClass): void +{ + deleteFile( + app($feedClass)->path() + ); } diff --git a/tests/Helpers/expects.php b/tests/Helpers/expects.php index fe5c6a4..0e9f63a 100644 --- a/tests/Helpers/expects.php +++ b/tests/Helpers/expects.php @@ -13,7 +13,9 @@ function expectFeed(string $feed): void { $instance = app($feed); - artisan(FeedGenerateCommand::class)->assertSuccessful()->run(); + artisan(FeedGenerateCommand::class, [ + 'class' => $feed, + ])->assertSuccessful()->run(); expect($instance->path())->toBeReadableFile(); expect(file_get_contents($instance->path()))->toMatchSnapshot(); diff --git a/tests/Pest.php b/tests/Pest.php index 2e574ba..fa9a81d 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -4,6 +4,10 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; +use Workbench\App\Feeds\FullFeed; +use Workbench\App\Feeds\PartialFeed; +use Workbench\App\Feeds\SitemapFeed; +use Workbench\App\Feeds\YandexFeed; pest() ->printer() @@ -17,3 +21,19 @@ pest() ->extend(TestCase::class) ->in('Unit'); + +pest() + ->in('Feature/Console/Generation') + ->beforeEach(function () { + config()?->set('feeds.channels', [ + FullFeed::class => true, + PartialFeed::class => true, + SitemapFeed::class => true, + YandexFeed::class => true, + ]); + + config() + ?->collection('feeds.channels') + ?->keys() + ?->each(fn (string $feed) => deleteFeedResult($feed)); + }); From ef613ce05db3d294c34147baef3c7629e3777409 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Mon, 1 Sep 2025 22:00:17 +0300 Subject: [PATCH 2/2] Added compatibility with the old version of Laravel on tests --- testbench.yaml | 1 + .../Providers/WorkbenchServiceProvider.php | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/testbench.yaml b/testbench.yaml index 69af6bc..ada5349 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -2,6 +2,7 @@ laravel: '@testbench' providers: - DragonCode\LaravelFeed\LaravelFeedServiceProvider + - Workbench\App\Providers\WorkbenchServiceProvider migrations: - workbench/database/migrations diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php index d09c191..2682ce9 100644 --- a/workbench/app/Providers/WorkbenchServiceProvider.php +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -4,23 +4,27 @@ namespace Workbench\App\Providers; +use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Str; class WorkbenchServiceProvider extends ServiceProvider { - /** - * Register services. - */ - public function register(): void + public function boot(): void { - // + if ($this->isFreshLaravel()) { + return; + } + + Repository::macro('collection', function (string $key) { + return new Collection($this->get($key)); + }); } - /** - * Bootstrap services. - */ - public function boot(): void + protected function isFreshLaravel(): bool { - // + return Str::of(Application::VERSION)->before('.')->toString() === '12'; } }