diff --git a/README.md b/README.md index f4612ef..b36c698 100644 --- a/README.md +++ b/README.md @@ -16,27 +16,36 @@ consumers. > - Chunked queries to database > - Draft mode for a process > - Easy property mapping -> - Generation of any XML (feeds, sitemaps, etc.) +> - Generation of any feeds, sitemaps, etc. ## Installation -To get the latest version of **Laravel Feeds**, simply require the project -using [Composer](https://getcomposer.org): +You can install the **Laravel Feeds** package via [Composer](https://getcomposer.org): ```Bash composer require dragon-code/laravel-feeds ``` -After that, publish the configuration file by call the console command: +You should publish +the [migration](database/migrations/2025_09_01_231655_create_feeds_table.php) +and the [config/feeds.php](config/feeds.php) file with: ```bash -php artisan vendor:publish --tag=feeds +php artisan vendor:publish --tag="feeds" ``` +> [!WARNING] +> +> Before running migrations, check the database connection settings in the [config/feeds.php](config/feeds.php) file. + +Now you can run migrations and proceed to [create feeds](https://feeds.dragon-code.pro/create-feeds.html). + ## Basic Usage ### Create Feeds +To create a feed class, use the `make:feed` console command: + ```bash php artisan make:feed User -t ``` @@ -44,10 +53,26 @@ php artisan make:feed User -t As a result of executing the console command, the files `app/Feeds/UserFeed.php` and `app/Feeds/Items/UserFeedItem.php` will be created. -### Generate Feeds +> [!TIP] +> When creating a feed, an operation/migration will also be created to add it to the database. +> +> If the project uses the [Laravel Deploy Operations](https://deploy-operations.dragon-code.pro), then an operation +> class will be created, otherwise a migration class will be created. +> +> This is necessary to add and manage information about feeds in the database. + +Check the [operation/migration](https://feeds.dragon-code.pro/create-feeds.html) file that was created for you and run +the console command: + +```bash +# For Laravel Deploy Operations +php artisan operations + +# For Laravel Migrations +php artisan migrate +``` -To generate feeds, create the classes of feeds and its element, add links to the file `config/feeds.php`, next call the -console command: +To generate all active feeds, use the console command: ```bash php artisan feed:generate @@ -55,7 +80,7 @@ php artisan feed:generate ## Documentation -[📚 Check out the full documentation to learn everything that Laravel Feeds has to offer.](https://feeds.dragon-code.pro) +📚 You will find full documentation on the dedicated [documentation](https://feeds.dragon-code.pro) site. ## License diff --git a/composer.json b/composer.json index 95bc30b..b1444c8 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "php": "^8.2", "ext-dom": "*", "ext-libxml": "*", + "dragonmantank/cron-expression": "^3.4", "illuminate/database": "^11.0 || ^12.0", "illuminate/filesystem": "^11.0 || ^12.0", "illuminate/support": "^11.0 || ^12.0", @@ -21,6 +22,8 @@ }, "require-dev": { "dragon-code/codestyler": "^6.3", + "dragon-code/laravel-deploy-operations": "^7.1", + "mockery/mockery": "^1.6", "orchestra/testbench": "^9.0 || ^10.0", "pestphp/pest": "^3.0 || ^4.0", "pestphp/pest-plugin-laravel": "^3.0 || ^4.0", @@ -36,7 +39,8 @@ "psr-4": { "Tests\\": "tests/", "Workbench\\App\\": "workbench/app/", - "Workbench\\Database\\Factories\\": "workbench/database/factories/" + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" } }, "config": { @@ -63,10 +67,12 @@ "@clear", "@prepare" ], + "build": "@php vendor/bin/testbench workbench:build --ansi", "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "migrate": "@php vendor/bin/testbench migrate:fresh --seed --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", "style": "vendor/bin/pint --parallel --ansi", - "test": "@php vendor/bin/pest --colors=always", + "test": "@php vendor/bin/pest --parallel --colors=always", "test:update": "@php vendor/bin/pest --colors=always --update-snapshots" } } diff --git a/config/feeds.php b/config/feeds.php index 0a2945c..e51840f 100644 --- a/config/feeds.php +++ b/config/feeds.php @@ -3,11 +3,52 @@ declare(strict_types=1); return [ - 'channels' => [ - // App\Feeds\FooFeed::class => (bool) env('FEED_FOO_ENABLED', true), - // App\Feeds\BarFeed::class => (bool) env('FEED_BAR_ENABLED', true), - // App\Feeds\BazFeed::class => (bool) env('FEED_BAZ_ENABLED', false), + /** + * Pretty-print the generated feed output. + * + * When enabled, the resulting XML/JSON will include indentation and + * human‑friendly formatting. Disable for slightly smaller payload size. + * + * By default, false + */ + 'pretty' => (bool) env('FEED_PRETTY', false), + + /** + * Database table settings used by the package (e.g., for generation logs or state). + */ + 'table' => [ + /** + * The database connection name to use. + * + * Should match a connection defined in config/database.php under + * the "connections" array. + */ + 'connection' => env('DB_CONNECTION', 'sqlite'), + + /** + * The database table name used by the package. + */ + 'table' => env('FEED_TABLE', 'feeds'), ], - 'pretty' => (bool) env('FEED_PRETTY', false), + /** + * Scheduling options for feed generation/update tasks. + */ + 'schedule' => [ + /** + * Time To Live (in minutes) for the schedule lock or cache. + * + * Controls how frequently a scheduled job may be executed to avoid + * overlapping or excessively frequent runs. + */ + 'ttl' => (int) env('FEED_SCHEDULE_TTL', 1440), + + /** + * Run scheduled jobs in the background. + * + * 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. + */ + 'background' => (bool) env('FEED_SCHEDULE_RUN_BACKGROUND', true), + ], ]; diff --git a/database/migrations/2025_09_01_231655_create_feeds_table.php b/database/migrations/2025_09_01_231655_create_feeds_table.php new file mode 100644 index 0000000..3f8cbf2 --- /dev/null +++ b/database/migrations/2025_09_01_231655_create_feeds_table.php @@ -0,0 +1,48 @@ +schema()->create($this->table(), function (Blueprint $table) { + $table->id(); + + $table->string('class')->unique(); + $table->string('title'); + + $table->string('expression'); + + $table->boolean('is_active'); + + $table->timestamp('last_activity')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + $this->schema()->dropIfExists($this->table()); + } + + protected function schema(): Builder + { + return Schema::connection($this->connection()); + } + + protected function connection(): ?string + { + return config('feeds.table.connection'); + } + + protected function table(): string + { + return config('feeds.table.table'); + } +}; diff --git a/docs/images/laravel-idea.png b/docs/images/laravel-idea.png index 863dd73..9bb05b6 100644 Binary files a/docs/images/laravel-idea.png and b/docs/images/laravel-idea.png differ diff --git a/docs/images/laravel-idea_dark.png b/docs/images/laravel-idea_dark.png index 1684ccd..32c607b 100644 Binary files a/docs/images/laravel-idea_dark.png and b/docs/images/laravel-idea_dark.png differ diff --git a/docs/laravel-feeds.tree b/docs/laravel-feeds.tree index 5ff6d18..ff8b0c3 100644 --- a/docs/laravel-feeds.tree +++ b/docs/laravel-feeds.tree @@ -19,8 +19,8 @@ - - - + + + diff --git a/docs/snippets/generation-feed-storage.php b/docs/snippets/advanced-feed-chunk.php similarity index 60% rename from docs/snippets/generation-feed-storage.php rename to docs/snippets/advanced-feed-chunk.php index 01d0bf9..7817eec 100644 --- a/docs/snippets/generation-feed-storage.php +++ b/docs/snippets/advanced-feed-chunk.php @@ -6,5 +6,8 @@ class UserFeed extends Feed { - protected string $storage = 'public'; + public function chunkSize(): int + { + return 500; + } } diff --git a/docs/snippets/advanced-header.php b/docs/snippets/advanced-feed-header-and-footer.php similarity index 75% rename from docs/snippets/advanced-header.php rename to docs/snippets/advanced-feed-header-and-footer.php index 3341d28..c9c4c95 100644 --- a/docs/snippets/advanced-header.php +++ b/docs/snippets/advanced-feed-header-and-footer.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace App\Feeds; - use DragonCode\LaravelFeed\Feeds\Feed; class UserFeed extends Feed @@ -12,4 +10,9 @@ public function header(): string { return ''; } + + public function footer(): string + { + return ''; + } } diff --git a/docs/snippets/advanced-head-info.php b/docs/snippets/advanced-feed-info-class.php similarity index 71% rename from docs/snippets/advanced-head-info.php rename to docs/snippets/advanced-feed-info-class.php index 3370923..3d2b065 100644 --- a/docs/snippets/advanced-head-info.php +++ b/docs/snippets/advanced-feed-info-class.php @@ -11,7 +11,8 @@ class UserFeedInfo extends FeedInfo public function toArray(): array { return [ - // ... + 'name' => config('app.name'), + 'company' => config('app.name'), ]; } } diff --git a/docs/snippets/advanced-head-feed.php b/docs/snippets/advanced-feed-info.php similarity index 75% rename from docs/snippets/advanced-head-feed.php rename to docs/snippets/advanced-feed-info.php index ab7e675..24980f9 100644 --- a/docs/snippets/advanced-head-feed.php +++ b/docs/snippets/advanced-feed-info.php @@ -2,8 +2,7 @@ declare(strict_types=1); -namespace App\Feeds; - +use App\Feeds\Info\UserFeedInfo; use DragonCode\LaravelFeed\Feeds\Feed; use DragonCode\LaravelFeed\Feeds\Info\FeedInfo; @@ -11,6 +10,6 @@ class UserFeed extends Feed { public function info(): FeedInfo { - return new FeedInfo; + return new UserFeedInfo; } } diff --git a/docs/snippets/generation-feed-filename.php b/docs/snippets/advanced-feed-storage.php similarity index 63% rename from docs/snippets/generation-feed-filename.php rename to docs/snippets/advanced-feed-storage.php index cf913c4..eae1984 100644 --- a/docs/snippets/generation-feed-filename.php +++ b/docs/snippets/advanced-feed-storage.php @@ -6,8 +6,10 @@ class UserFeed extends Feed { + protected string $storage = 'public'; + public function filename(): string { - return 'your/path/may/be/here.xml'; + return 'some/path/will/be/here.xml'; } } diff --git a/docs/snippets/advanced-nested.php b/docs/snippets/advanced-nested.php index 02b98a9..b0963a4 100644 --- a/docs/snippets/advanced-nested.php +++ b/docs/snippets/advanced-nested.php @@ -11,11 +11,11 @@ class UserFeedItem extends FeedItem public function toArray(): array { return [ - 'name' => $this->model->name, + 'name' => $this->model->class, 'email' => $this->model->email, 'header' => [ - '@cdata' => '

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

', + '@cdata' => '

' . $this->model->class . '

', ], 'names' => [ diff --git a/docs/snippets/config-channels.ini b/docs/snippets/config-channels.ini deleted file mode 100644 index 1513607..0000000 --- a/docs/snippets/config-channels.ini +++ /dev/null @@ -1,2 +0,0 @@ -FEED_FOO_ENABLED = true -FEED_BAR_ENABLED = false diff --git a/docs/snippets/config-pretty-false.xml b/docs/snippets/config-pretty-false.xml deleted file mode 100644 index 5d3495e..0000000 --- a/docs/snippets/config-pretty-false.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - [NEWS]:Some 1Some content 1Some extra data - [NEWS]:Some 2Some content 2Some extra data - diff --git a/docs/snippets/config-pretty-true.xml b/docs/snippets/config-pretty-true.xml deleted file mode 100644 index 7a42836..0000000 --- a/docs/snippets/config-pretty-true.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - [NEWS]:Some 1 - Some content 1 - Some extra data - - - [NEWS]:Some 2 - Some content 2 - Some extra data - - diff --git a/docs/snippets/feeds-feed.php b/docs/snippets/create-feeds-feed.php similarity index 100% rename from docs/snippets/feeds-feed.php rename to docs/snippets/create-feeds-feed.php diff --git a/docs/snippets/feeds-feed-item-result.xml b/docs/snippets/feeds-feed-item-result.xml deleted file mode 100644 index 152f1f9..0000000 --- a/docs/snippets/feeds-feed-item-result.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - John Doe - john.doe@example.com -
John Doe]]>
-
-
diff --git a/docs/snippets/feeds-feed-item.php b/docs/snippets/generation-feed-item.php similarity index 51% rename from docs/snippets/feeds-feed-item.php rename to docs/snippets/generation-feed-item.php index db0176a..6423c11 100644 --- a/docs/snippets/feeds-feed-item.php +++ b/docs/snippets/generation-feed-item.php @@ -12,16 +12,8 @@ class UserFeedItem extends FeedItem public function toArray(): array { return [ - 'name' => $this->model->name, + 'name' => $this->model->class, 'email' => $this->model->email, - - 'header' => [ - '@attributes' => [ - 'my-key-1' => 'my value 1', - 'my-key-2' => 'my value 2', - ], - '@cdata' => '

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

', - ], ]; } } diff --git a/docs/snippets/receipt-instagram-config.php b/docs/snippets/receipt-instagram-config.php deleted file mode 100644 index 533ad16..0000000 --- a/docs/snippets/receipt-instagram-config.php +++ /dev/null @@ -1,9 +0,0 @@ - [ - App\Feeds\InstagramFeed::class => (bool) env('FEED_INSTAGRAM_ENABLED', true), - ], -]; diff --git a/docs/snippets/receipt-sitemap-config.php b/docs/snippets/receipt-sitemap-config.php deleted file mode 100644 index 803e0cd..0000000 --- a/docs/snippets/receipt-sitemap-config.php +++ /dev/null @@ -1,9 +0,0 @@ - [ - App\Feeds\Sitemaps\ProductFeed::class => (bool) env('FEED_SITEMAP_POSTS_ENABLED', true), - ], -]; diff --git a/docs/snippets/receipt-yandex-config.php b/docs/snippets/receipt-yandex-config.php deleted file mode 100644 index 4e2a66a..0000000 --- a/docs/snippets/receipt-yandex-config.php +++ /dev/null @@ -1,9 +0,0 @@ - [ - App\Feeds\YandexFeed::class => (bool) env('FEED_YANDEX_ENABLED', true), - ], -]; diff --git a/docs/snippets/schedule-setup-manual.php b/docs/snippets/schedule-setup-manual.php new file mode 100644 index 0000000..66de617 --- /dev/null +++ b/docs/snippets/schedule-setup-manual.php @@ -0,0 +1,20 @@ +withoutOverlapping() + ->runInBackground() + ->daily(); + +Schedule::command(FeedGenerateCommand::class, [222]) + ->withoutOverlapping() + ->runInBackground() + ->hourly(); + +Schedule::call(function () { + // ... other action +})->everySecond(); diff --git a/docs/snippets/schedule-setup.php b/docs/snippets/schedule-setup.php new file mode 100644 index 0000000..1492c95 --- /dev/null +++ b/docs/snippets/schedule-setup.php @@ -0,0 +1,12 @@ +commands(); + +Schedule::call(function () { + // ... other action +})->everySecond(); diff --git a/docs/topics/advanced-usage.topic b/docs/topics/advanced-usage.topic index 55b4c62..1a0d4de 100644 --- a/docs/topics/advanced-usage.topic +++ b/docs/topics/advanced-usage.topic @@ -24,68 +24,118 @@ - - + + +

+ To add a root element and/or its attributes, override the root method: +

+ + + +

+ To disable the addition of the main element, specify its name as empty - null or + "". +

+
+ + +

+ To add information to the beginning of the root element (if present) or without it, + override the + info method in the feed class and call the creation of an object that extend the + DragonCode\LaravelFeed\Feeds\Info\FeedInfo class: +

+ + + +
+ + +

+ To change the header and footer, override the header and footer methods: +

+ + +
- -

- In some cases, you need to add various information to the beginning of the file. - To do this, use the info method: -

+ + + + - - + + + - -

- If it is necessary to change the file cap, override the header method in the feed class: -

+ + +

+ In some cases, you need to place an array of elements with the same names. +

- -
+

+ To do this, add a @ symbol to the beginning of the key name: +

- - - + - - - +

+ Result: +

- -

- In some cases, you need to place an array of elements with the same names. -

+ -

- To do this, add a @ symbol to the beginning of the key name: -

+

+ You can also use values for such elements. To insert them, use the reserved word @value. +

- +

+ For example: +

-

- Result: -

+ - +

+ Result: +

-

- You can also use values for such elements. To insert them, use the reserved word @value. -

+ + +
+
+

+ By default, feeds will be stored in the public storage, + and the file name will be automatically generated from the feed class name after + App\Feeds\ in kebab-case format. For example:

- + + # App\Feeds\UserFeed + user-feed.xml + + # App\Feeds\Sitemaps\ProductFeed + sitemaps-product-feed.xml +

- Result: + You can change these values by overriding the $storage property and the + filename method:

- + +
+ + +

+ Database queries use chunks, which are 1000 bytes by default. + You can change this by overriding the chunkSize method: +

+
diff --git a/docs/topics/contributions.topic b/docs/topics/contributions.topic index da710dd..1f50566 100644 --- a/docs/topics/contributions.topic +++ b/docs/topics/contributions.topic @@ -4,7 +4,7 @@ + title="Contribution guide" id="contributions"> Instructions for %instance% project developers Instructions for %instance% project developers diff --git a/docs/topics/create-feeds.topic b/docs/topics/create-feeds.topic index 8ee6714..fe7f58b 100644 --- a/docs/topics/create-feeds.topic +++ b/docs/topics/create-feeds.topic @@ -4,7 +4,7 @@ + title="Create feeds" id="create-feeds" help-id="make-feeds;create-feeds;feeds"> Instructions for creating and filling feed, feed items, and feed info classes Instructions for creating and filling feed, feed items, and feed info classes @@ -12,85 +12,92 @@ - - -

- You can also create the desired classes using the - Laravel Idea plugin for - PhpStorm: -

- - laravel idea -
- - -

- To create a feed class, use the console command: -

- - - %command-make% - - -

- As a result of executing the console command, the file - app/Feeds/UserFeed.php will be created. -

- -

- Also, this console command can create classes of the element and information along with the feed class. - To do this, use --item and --info parameters. -

- - - # Create a feed class and information class - %command-make% --info - - # Create a feed class and item class - %command-make% --item - - # Create a feed class, item class and information class - %command-make% --item --info - - -

- Shortcuts are also available: -

- - - # Create a feed class and item class - %command-make% -%command-shortcut-item% - - # Create a feed class and info class - %command-make% -%command-shortcut-info% - - # Create a feed class, item class and information class - %command-make% -%command-shortcut-info%%command-shortcut-item% - -
-
+ +

+ The most convenient way to create the necessary classes is to use the + Laravel Idea plugin for + PhpStorm. +

+ + laravel idea +
+ +

+ To create a feed class, use the %command-make-short% console command: +

+ + + %command-make% + + +

+ This will create a feed file. For example, app/Feeds/UserFeed.php. +

+ +

+ Also, this console command can create classes of the element and information along with the feed class. + To do this, use --item and --info parameters. +

+ + + # Create a feed class and information class + %command-make% --info + + # Create a feed class and item class + %command-make% --item + + # Create a feed class, item class and information class + %command-make% --item --info + + +

+ Shortcuts are also available: +

+ + + # Create a feed class and item class + %command-make% -%command-shortcut-item% + + # Create a feed class and info class + %command-make% -%command-shortcut-info% + + # Create a feed class, item class and information class + %command-make% -%command-shortcut-info%%command-shortcut-item% + + + +

+ When creating a feed, an operation/migration will also be created to add it to the database. +

+ +

+ If the project uses the + + Laravel Deploy Operations + + , then an operation class will be created, otherwise a migration class will be created. +

+ +

+ This is necessary to add and manage information about feeds in the database. +

+

- For example, we use this content for the Feed class: + Fill in the main feed class. For example:

- -
- - -

- For example, we use this content for the Feed Item class: -

+ - - -

- According to this example, the XML file with the following contents will be generated as a result: -

+ +

+ Fill in the feed item class. For example: +

- + +
diff --git a/docs/topics/generation.topic b/docs/topics/generation.topic index c2eacd1..318d557 100644 --- a/docs/topics/generation.topic +++ b/docs/topics/generation.topic @@ -4,72 +4,78 @@ + title="Generation & schedule" id="generation"> - Information on feed generation - Information on feed generation - Information on feed generation + Information on generating feed files for later distribution + Information on generating feed files for later distribution + Information on generating feed files for later distribution - - -

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

+

+ To generate all active feeds, use the console command: +

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

+ As a result, all active feeds (is_active = true) will be launched from the + feeds table (or the one you specified in the %config-filename% file). +

+

- To generate a specific feed class, specify the class reference or name relative to the - App\Feeds namespace. + Please note that when you call the command without a parameter, + the console will display links to all feeds, + even those with the is_active property set to false.

- For example: + However, instead of being launched for execution, such feeds will be marked with the + SKIP status.

+
- - %command-generate% App\Feeds\UserFeed - %command-generate% UserFeed - %command-generate% User - -
+ + + Please note that the specified feed + will be executed + even if it is disabled in the + feeds table in the database. + -

- Each feed can be created in a certain folder of a certain storage. + To generate a specific feed, call the %command-generate-short% console command, + passing the feed ID from the feeds table as its parameter:

+ + %command-generate% 123 + +
+ +

- To indicate the storage, override the property of $storage in the feed class: + To automate feed generation, use a + schedule. + To do this, specify the helper call in the + routes/console.php file (the default path, unless you have changed it):

- +

- By default, storage is public. + This will enable you to register calls to all active feeds according to the launch schedule you specified in the + expression column of the database.

- The path to the file inside the storage is indicated in the filename method: + You can also specify the schedule directly according to your own rules. + In this case, only the feeds you specify will be executed.

- - -

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

+
diff --git a/docs/topics/installation.topic b/docs/topics/installation.topic index 34a1184..b038760 100644 --- a/docs/topics/installation.topic +++ b/docs/topics/installation.topic @@ -4,7 +4,7 @@ + title="Installation & setup" id="installation"> Information on installing and configuring %instance% Information on installing and configuring %instance% @@ -13,71 +13,40 @@

- To get the latest version of - %instance% - , simply require the project using - Composer: + You can install the package via Composer:

composer require %package-name% - - Information on configuring %instance% - Information on configuring %instance% - - - -

- You can publish a %config-filename% configuration file using the console command: -

- - - php artisan vendor:publish --tag="%package-tags%" - -
- -

- This is the contents of the published %config-filename% file: -

- - -
- - -

- This setting contains information about the generated feeds. - They can be both turned on and turned off depending on the environment or other conditions. -

+

+ You should publish the + migration + and the + %config-filename% + config file with: +

- -
+ + php artisan vendor:publish --tag="%package-tags%" + - -

- This setting indicates in what form the XML records should be exported - using or without formatting. -

- - - + +

+ Before running migrations + , check the database connection settings in the + %config-filename% file. +

+
- - - -
-
+

+ Now you can run migrations and proceed to create feeds. +

- + + php artisan migrate + - - -
diff --git a/docs/topics/introduction.topic b/docs/topics/introduction.topic index 1f4d479..73d7c5b 100644 --- a/docs/topics/introduction.topic +++ b/docs/topics/introduction.topic @@ -6,8 +6,6 @@ xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd" title="Introduction" id="introduction"> - Getting Started - 📃 %instance% @@ -17,16 +15,15 @@ - + - Getting started + Usage - - - + +
diff --git a/docs/topics/instagram.topic b/docs/topics/receipt-instagram.topic similarity index 80% rename from docs/topics/instagram.topic rename to docs/topics/receipt-instagram.topic index 975c94c..3090706 100644 --- a/docs/topics/instagram.topic +++ b/docs/topics/receipt-instagram.topic @@ -4,7 +4,7 @@ + title="Instagram" id="receipt-instagram" help-id="instagram"> Feed generation recipe for Instagram Feed generation recipe for Instagram @@ -26,12 +26,8 @@
- -

- Add a link to the feed class in the %config-filename% file: -

- - + + diff --git a/docs/topics/sitemap.topic b/docs/topics/receipt-sitemap.topic similarity index 86% rename from docs/topics/sitemap.topic rename to docs/topics/receipt-sitemap.topic index 9a12fb4..54e5c07 100644 --- a/docs/topics/sitemap.topic +++ b/docs/topics/receipt-sitemap.topic @@ -4,7 +4,7 @@ + title="Sitemap" id="receipt-sitemap" help-id="sitemap"> Recipe for creating a sitemap Recipe for creating a sitemap @@ -26,12 +26,8 @@ - -

- Add a link to the feed class in the %config-filename% file: -

- - + + diff --git a/docs/topics/yandex.topic b/docs/topics/receipt-yandex.topic similarity index 82% rename from docs/topics/yandex.topic rename to docs/topics/receipt-yandex.topic index 6796b1e..b540e34 100644 --- a/docs/topics/yandex.topic +++ b/docs/topics/receipt-yandex.topic @@ -4,7 +4,7 @@ + title="Yandex" id="receipt-yandex" help-id="yandex"> Feed generation recipe for Yandex Feed generation recipe for Yandex @@ -30,12 +30,8 @@ - -

- Add a link to the feed class in the %config-filename% file: -

- - + + diff --git a/docs/topics/snippet-generate.topic b/docs/topics/snippet-generate.topic index 2ba7ffc..cc80a0e 100644 --- a/docs/topics/snippet-generate.topic +++ b/docs/topics/snippet-generate.topic @@ -16,4 +16,19 @@ %command-generate% + + +

+ Check the operation/migration + file that was created for you and run the console command: +

+ + + # For Laravel Deploy Operations + php artisan operations + + # For Laravel Migrations + php artisan migrate + + diff --git a/docs/v.list b/docs/v.list index e95359b..1db1fde 100644 --- a/docs/v.list +++ b/docs/v.list @@ -7,14 +7,16 @@ - + + + + + - @@ -22,6 +24,4 @@ - - diff --git a/ide.json b/ide.json index d57bac0..0fc213c 100644 --- a/ide.json +++ b/ide.json @@ -2,8 +2,8 @@ "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json", "codeGenerations": [ { - "id": "dragon-code.xml-feeds.main", - "name": "Create XML Feed", + "id": "dragon-code.feeds.main", + "name": "Create Feed", "classSuffix": "Feed", "regex": ".+", "files": [ @@ -25,8 +25,8 @@ ] }, { - "id": "dragon-code.xml-feeds.item", - "name": "Create XML Feed Item", + "id": "dragon-code.feeds.item", + "name": "Create Feed Item", "classSuffix": "FeedItem", "regex": ".+", "files": [ @@ -47,8 +47,8 @@ ] }, { - "id": "dragon-code.xml-feeds.info", - "name": "Create XML Feed Info", + "id": "dragon-code.feeds.info", + "name": "Create Feed Info", "classSuffix": "FeedInfo", "regex": ".+", "files": [ diff --git a/phpunit.xml b/phpunit.xml index 0f2a031..7ca863e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -27,9 +27,14 @@ + + + + + + - diff --git a/src/Casts/ClassCast.php b/src/Casts/ClassCast.php new file mode 100644 index 0000000..167ac31 --- /dev/null +++ b/src/Casts/ClassCast.php @@ -0,0 +1,35 @@ +isValid($value)) { + throw new InvalidExpressionException($value); + } + + return Str::of($value)->squish()->trim()->toString(); + } + + protected function isValid(string $value): bool + { + return CronExpression::isValidExpression($value); + } +} diff --git a/src/Console/Commands/FeedGenerateCommand.php b/src/Console/Commands/FeedGenerateCommand.php index 5d81bb6..9495f4f 100644 --- a/src/Console/Commands/FeedGenerateCommand.php +++ b/src/Console/Commands/FeedGenerateCommand.php @@ -4,7 +4,8 @@ namespace DragonCode\LaravelFeed\Console\Commands; -use DragonCode\LaravelFeed\Helpers\FeedHelper; +use DragonCode\LaravelFeed\Exceptions\InvalidFeedArgumentException; +use DragonCode\LaravelFeed\Queries\FeedQuery; use DragonCode\LaravelFeed\Services\Generator; use Illuminate\Console\Command; use Laravel\Prompts\Concerns\Colors; @@ -12,38 +13,37 @@ use Symfony\Component\Console\Input\InputArgument; use function app; -use function config; +use function is_numeric; #[AsCommand('feed:generate', 'Generate XML feeds')] class FeedGenerateCommand extends Command { use Colors; - public function handle(Generator $generator, FeedHelper $helper): void + public function handle(Generator $generator, FeedQuery $query): void { - foreach ($this->feedable($helper) as $feed => $enabled) { + foreach ($this->feedable($query) as $feed => $enabled) { $enabled ? $this->components->task($feed, fn () => $generator->feed(app($feed))) : $this->components->twoColumnDetail($feed, $this->messageYellow('SKIP')); } } - protected function feedable(FeedHelper $helper): array + protected function feedable(FeedQuery $feeds): array { - if ($feed = $this->resolveFeedClass($helper)) { - return [$feed => true]; + if (! $id = $this->argument('feed')) { + return $feeds->all() + ->pluck('is_active', 'class') + ->all(); } - return config('feeds.channels'); - } - - protected function resolveFeedClass(FeedHelper $helper): ?string - { - if (! $class = $this->argument('class')) { - return null; + if (! is_numeric($id)) { + throw new InvalidFeedArgumentException($id); } - return $helper->find((string) $class); + $feed = $feeds->find((int) $id); + + return [$feed->class => true]; } protected function messageYellow(string $message): string @@ -58,7 +58,7 @@ protected function messageYellow(string $message): string protected function getArguments(): array { return [ - ['class', InputArgument::OPTIONAL, 'The feed class for generation'], + ['feed', InputArgument::OPTIONAL, 'The Feed ID for generation (from the database)'], ]; } } diff --git a/src/Console/Commands/FeedMakeCommand.php b/src/Console/Commands/FeedMakeCommand.php index 7173a5a..cce1bd7 100644 --- a/src/Console/Commands/FeedMakeCommand.php +++ b/src/Console/Commands/FeedMakeCommand.php @@ -4,11 +4,18 @@ namespace DragonCode\LaravelFeed\Console\Commands; +use DragonCode\LaravelDeployOperations\Operation; use DragonCode\LaravelFeed\Concerns\InteractsWithName; +use DragonCode\LaravelFeed\Helpers\ClassExistsHelper; +use DragonCode\LaravelFeed\Publishers\MigrationPublisher; +use DragonCode\LaravelFeed\Publishers\OperationPublisher; use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; +use function app; +use function vsprintf; + #[AsCommand('make:feed', 'Create a new feed')] class FeedMakeCommand extends GeneratorCommand { @@ -33,6 +40,23 @@ public function handle(): void (bool) $this->option('force') ); } + + $this->makeOperation( + $this->argument('name'), + $this->getQualifyClass() + ); + } + + protected function makeOperation(string $name, string $class): void + { + $publisher = $this->hasOperations() + ? app(OperationPublisher::class, ['title' => $name, 'class' => $class]) + : app(MigrationPublisher::class, ['title' => $name, 'class' => $class]); + + $this->components->info(vsprintf('%s [%s] created successfully.', [ + $publisher->name(), + $publisher->publish(), + ])); } protected function makeFeedItem(string $name, bool $force): void @@ -61,6 +85,16 @@ protected function getDefaultNamespace($rootNamespace): string return $rootNamespace . '\Feeds'; } + protected function getQualifyClass(): string + { + return $this->qualifyClass($this->getNameInput()); + } + + protected function hasOperations(): bool + { + return app(ClassExistsHelper::class)->exists(Operation::class); + } + protected function getOptions(): array { return [ diff --git a/src/Services/ConvertToXml.php b/src/Converters/ConvertToXml.php similarity index 99% rename from src/Services/ConvertToXml.php rename to src/Converters/ConvertToXml.php index c0bd13a..ed6bc74 100644 --- a/src/Services/ConvertToXml.php +++ b/src/Converters/ConvertToXml.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace DragonCode\LaravelFeed\Services; +namespace DragonCode\LaravelFeed\Converters; use DOMDocument; use DOMNode; diff --git a/src/Enums/FeedFormatEnum.php b/src/Enums/FeedFormatEnum.php new file mode 100644 index 0000000..e8c0673 --- /dev/null +++ b/src/Enums/FeedFormatEnum.php @@ -0,0 +1,10 @@ +filename ??= Str::kebab(class_basename($this)) . '.xml'; + return $this->filename ??= Str::of(static::class) + ->after($this->laravel->getNamespace() . 'Feeds\\') + ->ltrim('\\') + ->replace('\\', ' ') + ->kebab() + ->append('.', $this->format->value) + ->toString(); } public function path(): string diff --git a/src/Helpers/ClassExistsHelper.php b/src/Helpers/ClassExistsHelper.php new file mode 100644 index 0000000..007f984 --- /dev/null +++ b/src/Helpers/ClassExistsHelper.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/src/Helpers/ScheduleFeedHelper.php b/src/Helpers/ScheduleFeedHelper.php new file mode 100644 index 0000000..dfdfad0 --- /dev/null +++ b/src/Helpers/ScheduleFeedHelper.php @@ -0,0 +1,46 @@ +query->active()->each( + fn (Feed $feed) => $this->register($feed) + ); + } + + protected function register(Feed $feed): void + { + $event = $this->event($feed); + + if ($this->canBackground) { + $event->runInBackground(); + } + } + + protected function event(Feed $feed): Event + { + return Schedule::command(FeedGenerateCommand::class, [$feed->id]) + ->withoutOverlapping($this->ttl) + ->cron($feed->expression); + } +} diff --git a/src/LaravelFeedServiceProvider.php b/src/LaravelFeedServiceProvider.php index d0ae2b8..3eda0e6 100644 --- a/src/LaravelFeedServiceProvider.php +++ b/src/LaravelFeedServiceProvider.php @@ -25,6 +25,7 @@ public function boot(): void $this->registerCommands(); $this->publishConfig(); + $this->migrations(); } protected function publishConfig(): void @@ -34,6 +35,13 @@ protected function publishConfig(): void ], ['config', 'feeds']); } + protected function migrations(): void + { + $this->publishesMigrations([ + __DIR__ . '/../database/migrations' => $this->app->databasePath('migrations'), + ], 'feeds'); + } + protected function registerCommands(): void { $this->commands([ diff --git a/src/Models/Feed.php b/src/Models/Feed.php new file mode 100644 index 0000000..c4a7da9 --- /dev/null +++ b/src/Models/Feed.php @@ -0,0 +1,56 @@ + '* * * * *', + + 'is_active' => true, + ]; + + public function getConnectionName(): ?string + { + return config('feeds.table.connection'); + } + + public function getTable(): ?string + { + return config('feeds.table.table') ?? parent::getTable(); + } + + protected function casts(): array + { + return [ + 'class' => ClassCast::class, + 'expression' => ExpressionCast::class, + + 'is_active' => 'boolean', + + 'last_activity' => 'datetime', + ]; + } +} diff --git a/src/Publishers/MigrationPublisher.php b/src/Publishers/MigrationPublisher.php new file mode 100644 index 0000000..43a16af --- /dev/null +++ b/src/Publishers/MigrationPublisher.php @@ -0,0 +1,20 @@ +classBasename() + ->before('Publisher') + ->toString(); + } + + public function publish(): string + { + return $this->store( + $this->path(), + $this->replace() + ); + } + + protected function store(string $path, string $contents): string + { + $this->filesystem->ensureDirectoryExists(dirname($path)); + $this->filesystem->put($path, $contents); + + return $path; + } + + protected function replace(): string + { + return str_replace( + ['DummyClass', 'DummyBaseClass', 'DummyTitle'], + [$this->class, $this->baseClass(), $this->title()], + $this->load() + ); + } + + protected function baseClass(): string + { + return class_basename($this->class); + } + + protected function title(): string + { + return Str::of($this->title) + ->replace(['\\', '/'], ': ') + ->snake(' ') + ->title() + ->toString(); + } + + protected function path(): string + { + return vsprintf('%s/%s_%s.php', [ + $this->basePath(), + $this->date(), + $this->filename(), + ]); + } + + protected function filename(): string + { + return Str::of($this->title) + ->snake() + ->prepend('create_') + ->append('_feed') + ->toString(); + } + + protected function date(): string + { + return Carbon::now()->format('Y_m_d_His'); + } + + protected function load(): string + { + return file_get_contents($this->template()); + } +} diff --git a/src/Queries/FeedQuery.php b/src/Queries/FeedQuery.php new file mode 100644 index 0000000..eeb1ca9 --- /dev/null +++ b/src/Queries/FeedQuery.php @@ -0,0 +1,64 @@ + $class, + 'title' => $title, + 'expression' => $expression, + 'is_active' => $isActive, + ]); + } + + public function find(int $id): Feed + { + return Feed::findOr($id, callback: static fn () => throw new FeedNotFoundException($id)); + } + + public function all(): Builder + { + return Feed::query()->orderBy('id'); + } + + public function active(): Builder + { + return Feed::query() + ->where('is_active', true) + ->orderBy('id'); + } + + public function setLastActivity(string $class): void + { + Feed::query() + ->whereClass($class) + ->update(['last_activity' => now()]); + } + + public function delete(int $id): void + { + Feed::destroy($id); + } + + public function restore(int $id): void + { + Feed::query() + ->whereId($id) + ->restore(); + } +} diff --git a/src/Services/Filesystem.php b/src/Services/Filesystem.php index bff317d..f7bcb4d 100644 --- a/src/Services/Filesystem.php +++ b/src/Services/Filesystem.php @@ -4,12 +4,16 @@ namespace DragonCode\LaravelFeed\Services; +use DragonCode\LaravelFeed\Exceptions\OpenFeedException; +use DragonCode\LaravelFeed\Exceptions\WriteFeedException; use Illuminate\Filesystem\Filesystem as File; +use function blank; use function dirname; use function fclose; use function fopen; use function fwrite; +use function is_resource; class Filesystem { @@ -27,13 +31,26 @@ public function open(string $path) $this->ensureFileDelete($path); $this->ensureDirectory($path); - return fopen($path, 'ab'); + $resource = fopen($path, 'ab'); + + if ($resource === false) { + throw new OpenFeedException($path); + } + + return $resource; } - public function append($resource, string $content): void + /** + * @param resource $resource + */ + public function append($resource, string $content, string $path): void { - if (! empty($content)) { - fwrite($resource, $content); + if (blank($content)) { + return; + } + + if (fwrite($resource, $content) === false) { + throw new WriteFeedException($path); } } @@ -59,6 +76,10 @@ public function release($resource, string $path): void */ public function close($resource): void { + if (! is_resource($resource)) { + return; + } + fclose($resource); } diff --git a/src/Services/Generator.php b/src/Services/Generator.php index 962335a..25ebea6 100644 --- a/src/Services/Generator.php +++ b/src/Services/Generator.php @@ -4,12 +4,15 @@ namespace DragonCode\LaravelFeed\Services; +use DragonCode\LaravelFeed\Converters\ConvertToXml; use DragonCode\LaravelFeed\Data\ElementData; use DragonCode\LaravelFeed\Feeds\Feed; +use DragonCode\LaravelFeed\Queries\FeedQuery; use Illuminate\Database\Eloquent\Collection; use function blank; use function collect; +use function get_class; use function implode; use function sprintf; @@ -18,6 +21,7 @@ class Generator public function __construct( protected Filesystem $filesystem, protected ConvertToXml $converter, + protected FeedQuery $query, ) {} public function feed(Feed $feed): void @@ -33,6 +37,8 @@ public function feed(Feed $feed): void $this->performFooter($file, $feed); $this->release($file, $path); + + $this->setLastActivity($feed); } protected function performItem($file, Feed $feed): void @@ -46,13 +52,13 @@ protected function performItem($file, Feed $feed): void ); } - $this->append($file, implode(PHP_EOL, $content)); + $this->append($file, implode(PHP_EOL, $content), $feed->path()); }); } protected function performHeader($file, Feed $feed): void { - $this->append($file, $feed->header()); + $this->append($file, $feed->header(), $feed->path()); } protected function performInfo($file, Feed $feed): void @@ -63,7 +69,7 @@ protected function performInfo($file, Feed $feed): void $value = $this->converter->convertInfo($info); - $this->append($file, PHP_EOL . $value); + $this->append($file, PHP_EOL . $value, $feed->path()); } protected function performRoot($file, Feed $feed): void @@ -76,7 +82,7 @@ protected function performRoot($file, Feed $feed): void ? sprintf("\n<%s %s>\n", $name, $this->makeRootAttributes($feed->root())) : sprintf("\n<%s>\n", $name); - $this->append($file, $value); + $this->append($file, $value, $feed->path()); } protected function performFooter($file, Feed $feed): void @@ -87,7 +93,9 @@ protected function performFooter($file, Feed $feed): void $value .= "\n\n"; } - $this->append($file, $value . $feed->footer()); + $value .= $feed->footer(); + + $this->append($file, $value, $feed->path()); } protected function makeRootAttributes(ElementData $item): string @@ -97,9 +105,9 @@ protected function makeRootAttributes(ElementData $item): string ->implode(' '); } - protected function append($file, string $content): void + protected function append($file, string $content, string $path): void { - $this->filesystem->append($file, $content); + $this->filesystem->append($file, $content, $path); } protected function release($file, string $path): void @@ -111,4 +119,11 @@ protected function openFile(string $path) { return $this->filesystem->open($path); } + + protected function setLastActivity(Feed $feed): void + { + $this->query->setLastActivity( + get_class($feed) + ); + } } diff --git a/stubs/feed_activate_migration.stub b/stubs/feed_activate_migration.stub new file mode 100644 index 0000000..e69de29 diff --git a/stubs/feed_activate_operation.stub b/stubs/feed_activate_operation.stub new file mode 100644 index 0000000..e69de29 diff --git a/stubs/migration.stub b/stubs/migration.stub new file mode 100644 index 0000000..2d91f28 --- /dev/null +++ b/stubs/migration.stub @@ -0,0 +1,20 @@ +create( + class : DummyBaseClass::class, + title : 'DummyTitle', + expression: '* * * * *' + ); + } +}; diff --git a/stubs/operation.stub b/stubs/operation.stub new file mode 100644 index 0000000..eabdbab --- /dev/null +++ b/stubs/operation.stub @@ -0,0 +1,23 @@ +create( + class : DummyBaseClass::class, + title : 'DummyTitle', + expression: '* * * * *' + ); + } + + public function withinTransactions(): bool + { + return false; + } +}; diff --git a/testbench.yaml b/testbench.yaml index ada5349..e0ad071 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -1,10 +1,13 @@ laravel: '@testbench' providers: - - DragonCode\LaravelFeed\LaravelFeedServiceProvider - Workbench\App\Providers\WorkbenchServiceProvider + - DragonCode\LaravelFeed\LaravelFeedServiceProvider + - Spatie\LaravelData\LaravelDataServiceProvider + - DragonCode\LaravelDeployOperations\ServiceProvider migrations: + - database/migrations - workbench/database/migrations workbench: @@ -20,3 +23,7 @@ workbench: - create-sqlite-db - db-wipe - migrate-fresh + +env: + CACHE_DRIVER: array + CACHE_STORE: array diff --git a/tests/.pest/snapshots/Unit/Console/MakeTest/make_feed.snap b/tests/.pest/snapshots/Unit/Console/Make/FeedTest/make_feed.snap similarity index 100% rename from tests/.pest/snapshots/Unit/Console/MakeTest/make_feed.snap rename to tests/.pest/snapshots/Unit/Console/Make/FeedTest/make_feed.snap diff --git a/tests/.pest/snapshots/Unit/Console/MakeTest/make_with_info.snap b/tests/.pest/snapshots/Unit/Console/Make/InfoTest/make_with_info.snap similarity index 100% rename from tests/.pest/snapshots/Unit/Console/MakeTest/make_with_info.snap rename to tests/.pest/snapshots/Unit/Console/Make/InfoTest/make_with_info.snap diff --git a/tests/.pest/snapshots/Unit/Console/MakeTest/make_with_info__2.snap b/tests/.pest/snapshots/Unit/Console/Make/InfoTest/make_with_info__2.snap similarity index 100% rename from tests/.pest/snapshots/Unit/Console/MakeTest/make_with_info__2.snap rename to tests/.pest/snapshots/Unit/Console/Make/InfoTest/make_with_info__2.snap diff --git a/tests/.pest/snapshots/Unit/Console/MakeTest/make_with_item.snap b/tests/.pest/snapshots/Unit/Console/Make/ItemTest/make_with_item.snap similarity index 100% rename from tests/.pest/snapshots/Unit/Console/MakeTest/make_with_item.snap rename to tests/.pest/snapshots/Unit/Console/Make/ItemTest/make_with_item.snap diff --git a/tests/.pest/snapshots/Unit/Console/MakeTest/make_with_item__2.snap b/tests/.pest/snapshots/Unit/Console/Make/ItemTest/make_with_item__2.snap similarity index 100% rename from tests/.pest/snapshots/Unit/Console/MakeTest/make_with_item__2.snap rename to tests/.pest/snapshots/Unit/Console/Make/ItemTest/make_with_item__2.snap diff --git a/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_EmptyFeed___.snap b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_EmptyFeed___.snap new file mode 100644 index 0000000..3eef6fb --- /dev/null +++ b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_EmptyFeed___.snap @@ -0,0 +1 @@ +empty-feed.xml \ No newline at end of file diff --git a/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_FullFeed___.snap b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_FullFeed___.snap new file mode 100644 index 0000000..f7bbd67 --- /dev/null +++ b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_FullFeed___.snap @@ -0,0 +1 @@ +nested/full.xml \ No newline at end of file diff --git a/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_PartialFeed___.snap b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_PartialFeed___.snap new file mode 100644 index 0000000..f6cded8 --- /dev/null +++ b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_PartialFeed___.snap @@ -0,0 +1 @@ +partial-feed.xml \ No newline at end of file diff --git a/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_SitemapFeed___.snap b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_SitemapFeed___.snap new file mode 100644 index 0000000..e5f01af --- /dev/null +++ b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_SitemapFeed___.snap @@ -0,0 +1 @@ +sitemaps/products.xml \ No newline at end of file diff --git a/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_YandexFeed___.snap b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_YandexFeed___.snap new file mode 100644 index 0000000..7e66387 --- /dev/null +++ b/tests/.pest/snapshots/Unit/Feeds/FilenameTest/filename_with_data_set_____Workbench_App_Feeds_YandexFeed___.snap @@ -0,0 +1 @@ +yandex.xml \ No newline at end of file diff --git a/tests/Expectations.php b/tests/Expectations.php index 2ff9cef..26d9f96 100644 --- a/tests/Expectations.php +++ b/tests/Expectations.php @@ -2,26 +2,42 @@ declare(strict_types=1); -expect()->extend('toMatchFeedSnapshot', function () { - $content = file_get_contents(feedPath($this->value . 'Feed')); +expect()->extend('toMatchFileSnapshot', function () { + $content = file_get_contents($this->value); expect($content)->toMatchSnapshot(); return $this; }); +expect()->extend('toMatchFeedSnapshot', function () { + $path = feedPath($this->value . 'Feed'); + + expect($path)->toMatchFileSnapshot(); + + return $this; +}); + expect()->extend('toMatchFeedItemSnapshot', function () { - $content = file_get_contents(feedPath('Items/' . $this->value . 'FeedItem')); + $path = feedPath('Items/' . $this->value . 'FeedItem'); - expect($content)->toMatchSnapshot(); + expect($path)->toMatchFileSnapshot(); return $this; }); expect()->extend('toMatchFeedInfoSnapshot', function () { - $content = file_get_contents(feedPath('Info/' . $this->value . 'FeedInfo')); + $path = feedPath('Info/' . $this->value . 'FeedInfo'); - expect($content)->toMatchSnapshot(); + expect($path)->toMatchFileSnapshot(); + + return $this; +}); + +expect()->extend('toMatchGeneratedFeed', function () { + $path = app($this->value->class)->path(); + + expect($path)->toBeFile(); return $this; }); diff --git a/tests/Feature/Console/Generation/DefaultTest.php b/tests/Feature/Console/Generation/DefaultTest.php index 08dc4ed..24aac4f 100644 --- a/tests/Feature/Console/Generation/DefaultTest.php +++ b/tests/Feature/Console/Generation/DefaultTest.php @@ -3,21 +3,20 @@ declare(strict_types=1); use DragonCode\LaravelFeed\Console\Commands\FeedGenerateCommand; +use DragonCode\LaravelFeed\Models\Feed; use function Pest\Laravel\artisan; test('generate', function () { $command = artisan(FeedGenerateCommand::class); - config() - ?->collection('feeds.channels') - ?->keys() - ?->each(fn (string $feed) => $command->expectsOutputToContain($feed)); + getAllFeeds()->each( + fn (Feed $feed) => $command->expectsOutputToContain($feed->class) + ); $command->assertSuccessful()->run(); - config() - ?->collection('feeds.channels') - ?->keys() - ?->each(fn (string $feed) => expect(app($feed)->path())->toBeReadableFile()); + getAllFeeds()->each( + fn (Feed $feed) => expect($feed)->toMatchGeneratedFeed() + ); }); diff --git a/tests/Feature/Console/Generation/DisabledTest.php b/tests/Feature/Console/Generation/DisabledTest.php index 9fa0476..b48bb86 100644 --- a/tests/Feature/Console/Generation/DisabledTest.php +++ b/tests/Feature/Console/Generation/DisabledTest.php @@ -3,30 +3,31 @@ declare(strict_types=1); use DragonCode\LaravelFeed\Console\Commands\FeedGenerateCommand; +use DragonCode\LaravelFeed\Models\Feed; use Workbench\App\Feeds\SitemapFeed; use Workbench\App\Feeds\YandexFeed; use function Pest\Laravel\artisan; test('generate', function () { - config()?->set('feeds.channels.' . SitemapFeed::class, false); - config()?->set('feeds.channels.' . YandexFeed::class, false); + disableFeeds([ + SitemapFeed::class, + YandexFeed::class, + ]); $command = artisan(FeedGenerateCommand::class); - config() - ?->collection('feeds.channels') - ?->keys() - ?->each(fn (string $feed) => $command->expectsOutputToContain($feed)); + getAllFeeds()->each( + fn (Feed $feed) => $command->expectsOutputToContain($feed->class) + ); $command->assertSuccessful()->run(); - config() - ?->collection('feeds.channels') - ?->keys() - ?->each(fn (string $feed) => match ($feed) { + getAllFeeds()->each( + fn (Feed $feed) => match ($feed->class) { SitemapFeed::class, - YandexFeed::class => expect(app($feed)->path())->not->toBeReadableFile(), - default => expect(app($feed)->path())->toBeReadableFile() - }); + YandexFeed::class => expect($feed)->not->toMatchGeneratedFeed(), + default => expect($feed)->toMatchGeneratedFeed() + } + ); }); diff --git a/tests/Feature/Console/Generation/FoundTest.php b/tests/Feature/Console/Generation/FoundTest.php new file mode 100644 index 0000000..8b65b54 --- /dev/null +++ b/tests/Feature/Console/Generation/FoundTest.php @@ -0,0 +1,31 @@ + $id, + ]); + + getAllFeeds()->each( + fn (Feed $feed) => $id === $feed->id + ? $command->expectsOutputToContain($feed->class) + : $command->doesntExpectOutputToContain($feed->class) + ); + + $command->assertSuccessful()->run(); + + getAllFeeds()->each( + fn (Feed $feed) => $id === $feed->id + ? expect($feed)->toMatchGeneratedFeed() + : expect($feed)->not->toMatchGeneratedFeed() + ); +})->with([ + fn () => Feed::query()->latest()->first()->id, + fn () => Feed::query()->oldest()->first()->id, +]); diff --git a/tests/Feature/Console/Generation/IncorrectParameterTest.php b/tests/Feature/Console/Generation/IncorrectParameterTest.php index fe48160..a3937ee 100644 --- a/tests/Feature/Console/Generation/IncorrectParameterTest.php +++ b/tests/Feature/Console/Generation/IncorrectParameterTest.php @@ -3,42 +3,21 @@ declare(strict_types=1); use DragonCode\LaravelFeed\Console\Commands\FeedGenerateCommand; -use DragonCode\LaravelFeed\Exceptions\FeedNotFoundException; +use DragonCode\LaravelFeed\Exceptions\InvalidFeedArgumentException; use function Pest\Laravel\artisan; -test('incorrect', function (mixed $name) { +test('incorrect', function (mixed $id) { artisan(FeedGenerateCommand::class, [ - 'class' => $name, + 'feed' => $id, ])->run(); }) - ->throws(FeedNotFoundException::class) + ->throws(InvalidFeedArgumentException::class, 'Feed ID must be of type integer, [string] given.') ->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/NotFoundTest.php b/tests/Feature/Console/Generation/NotFoundTest.php new file mode 100644 index 0000000..58bc0d7 --- /dev/null +++ b/tests/Feature/Console/Generation/NotFoundTest.php @@ -0,0 +1,14 @@ + 123, + ])->run(); +})->throws(FeedNotFoundException::class, 'Feed [123] not found.'); diff --git a/tests/Feature/Console/Generation/SpecifiedTest.php b/tests/Feature/Console/Generation/SpecifiedTest.php index 7649a00..205f707 100644 --- a/tests/Feature/Console/Generation/SpecifiedTest.php +++ b/tests/Feature/Console/Generation/SpecifiedTest.php @@ -3,32 +3,29 @@ declare(strict_types=1); use DragonCode\LaravelFeed\Console\Commands\FeedGenerateCommand; +use DragonCode\LaravelFeed\Models\Feed; use Workbench\App\Feeds\SitemapFeed; use function Pest\Laravel\artisan; test('generate', function () { + $source = findFeed(SitemapFeed::class); + $command = artisan(FeedGenerateCommand::class, [ - 'class' => SitemapFeed::class, + 'feed' => $source->id, ]); - config() - ?->collection('feeds.channels') - ?->keys() - ?->each( - fn (string $feed) => $feed === SitemapFeed::class - ? $command->expectsOutputToContain($feed) - : $command->doesntExpectOutputToContain($feed) - ); + getAllFeeds()->each( + fn (Feed $feed) => $source->id === $feed->id + ? $command->expectsOutputToContain($feed->class) + : $command->doesntExpectOutputToContain($feed->class) + ); $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() - ); + getAllFeeds()->each( + fn (Feed $feed) => $source->id === $feed->id + ? expect($feed)->toMatchGeneratedFeed() + : expect($feed)->not->toMatchGeneratedFeed() + ); }); diff --git a/tests/Feature/Console/Generation/UnknownClassTest.php b/tests/Feature/Console/Generation/UnknownClassTest.php deleted file mode 100644 index 5951a9a..0000000 --- a/tests/Feature/Console/Generation/UnknownClassTest.php +++ /dev/null @@ -1,26 +0,0 @@ - $feed, - ])->run(); -}) - ->throws(UnexpectedFeedException::class) - ->with([ - TestCase::class, - WorkbenchServiceProvider::class, - - YandexFeedItem::class, - YandexFeedInfo::class, - ]); diff --git a/tests/Feature/Feeds/EmptyTest.php b/tests/Feature/Feeds/EmptyTest.php index 8d5e406..931f9f8 100644 --- a/tests/Feature/Feeds/EmptyTest.php +++ b/tests/Feature/Feeds/EmptyTest.php @@ -7,5 +7,5 @@ test('export', function (bool $pretty) { setPrettyXml($pretty); - expectFeed(EmptyFeed::class); + expectFeedSnapshot(EmptyFeed::class); })->with('boolean'); diff --git a/tests/Feature/Feeds/FullTest.php b/tests/Feature/Feeds/FullTest.php index d3df498..204dfef 100644 --- a/tests/Feature/Feeds/FullTest.php +++ b/tests/Feature/Feeds/FullTest.php @@ -10,5 +10,5 @@ createNews(...NewsFakeData::toArray()); - expectFeed(FullFeed::class); + expectFeedSnapshot(FullFeed::class); })->with('boolean'); diff --git a/tests/Feature/Feeds/PartialTest.php b/tests/Feature/Feeds/PartialTest.php index 27a2b6e..aa0b0ca 100644 --- a/tests/Feature/Feeds/PartialTest.php +++ b/tests/Feature/Feeds/PartialTest.php @@ -14,5 +14,5 @@ createNews(...NewsFakeData::toArray()); - expectFeed(PartialFeed::class); + expectFeedSnapshot(PartialFeed::class); })->with('boolean'); diff --git a/tests/Feature/Feeds/SitemapTest.php b/tests/Feature/Feeds/SitemapTest.php index 6cc3914..15e6a82 100644 --- a/tests/Feature/Feeds/SitemapTest.php +++ b/tests/Feature/Feeds/SitemapTest.php @@ -7,5 +7,5 @@ test('export', function () { createProducts(); - expectFeed(SitemapFeed::class); + expectFeedSnapshot(SitemapFeed::class); }); diff --git a/tests/Feature/Feeds/YandexTest.php b/tests/Feature/Feeds/YandexTest.php index e7b77f9..00ad370 100644 --- a/tests/Feature/Feeds/YandexTest.php +++ b/tests/Feature/Feeds/YandexTest.php @@ -7,5 +7,5 @@ test('export', function () { createProducts(); - expectFeed(YandexFeed::class); + expectFeedSnapshot(YandexFeed::class); }); diff --git a/tests/Feature/Queries/Create/ClassTest.php b/tests/Feature/Queries/Create/ClassTest.php new file mode 100644 index 0000000..d4068ba --- /dev/null +++ b/tests/Feature/Queries/Create/ClassTest.php @@ -0,0 +1,23 @@ +create( + class : EmptyFeed::class, + title : 'Some', + expression: $value + ); +})->throws( + exception: InvalidExpressionException::class, +)->with([ + 'foo', + '123', + 'foo 1 2 3', + '* * * * * *', + '* * *', +]); diff --git a/tests/Feature/Queries/Create/ExpressionTest.php b/tests/Feature/Queries/Create/ExpressionTest.php new file mode 100644 index 0000000..7c9dc20 --- /dev/null +++ b/tests/Feature/Queries/Create/ExpressionTest.php @@ -0,0 +1,29 @@ +create( + class: 'foo', + title: 'Some', + ); +})->throws( + exception : UnexpectedClassException::class, + exceptionMessage: 'Class [foo] does not exist.' +); + +test('not extending', function () { + app(FeedQuery::class)->create( + class: ProductFakeData::class, + title: 'Some', + ); +})->throws( + exception : UnknownFeedClassException::class, + exceptionMessage: sprintf('The [%s] class must extend from the %s class.', ProductFakeData::class, Feed::class) +); diff --git a/tests/Feature/Queries/Create/LastActivity.php b/tests/Feature/Queries/Create/LastActivity.php new file mode 100644 index 0000000..06fbe97 --- /dev/null +++ b/tests/Feature/Queries/Create/LastActivity.php @@ -0,0 +1,35 @@ + $feed->id, + + 'last_activity' => null, + ]); + + artisan(FeedGenerateCommand::class) + ->expectsOutputToContain($feed->class) + ->assertSuccessful() + ->run(); + + assertDatabaseMissing(Feed::class, [ + 'id' => $feed->id, + + 'last_activity' => null, + ]); + + $feed->refresh(); + + expect($feed->last_activity)->toDateTime(); +}); diff --git a/tests/Feature/Queries/Create/SuccessTest.php b/tests/Feature/Queries/Create/SuccessTest.php new file mode 100644 index 0000000..18fce60 --- /dev/null +++ b/tests/Feature/Queries/Create/SuccessTest.php @@ -0,0 +1,24 @@ +forceDelete(); + + $feed = app(FeedQuery::class)->create( + class : EmptyFeed::class, + title : 'Some', + expression: '*/15 */2 * 1 *' + ); + + expect($feed) + ->class->toBe(EmptyFeed::class) + ->title->toBe('Some') + ->expression->toBe('*/15 */2 * 1 *') + ->is_active->toBeTrue() + ->last_activity->toBeNull(); +}); diff --git a/tests/Helpers/cleanup.php b/tests/Helpers/cleanup.php index e2216db..89c1d95 100644 --- a/tests/Helpers/cleanup.php +++ b/tests/Helpers/cleanup.php @@ -3,26 +3,20 @@ declare(strict_types=1); use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\ParallelTesting; -function deleteFile(string $filename): void +function deleteOperations(): void { - app(Filesystem::class)->delete( - $filename + (new Filesystem)->deleteDirectory( + config('deploy-operations.path') ); } -function deleteFeed(string $feedName): void +function deleteMigrations(): void { - $path = app_path( - feedPath($feedName) - ); - - deleteFile($path); -} + $token = ParallelTesting::token() ?: '0'; -function deleteFeedResult(string $feedClass): void -{ - deleteFile( - app($feedClass)->path() + (new Filesystem)->deleteDirectory( + database_path($token) ); } diff --git a/tests/Helpers/expects.php b/tests/Helpers/expects.php index 0e9f63a..19ff160 100644 --- a/tests/Helpers/expects.php +++ b/tests/Helpers/expects.php @@ -6,17 +6,16 @@ use function Pest\Laravel\artisan; -/** - * @param class-string $feed - */ -function expectFeed(string $feed): void +function expectFeedSnapshot(string $class): void { - $instance = app($feed); + $feed = findFeed($class); + + $instance = app($feed->class); artisan(FeedGenerateCommand::class, [ - 'class' => $feed, + 'feed' => $feed->id, ])->assertSuccessful()->run(); - expect($instance->path())->toBeReadableFile(); + expect($instance->path())->toBeFile(); expect(file_get_contents($instance->path()))->toMatchSnapshot(); } diff --git a/tests/Helpers/feeds.php b/tests/Helpers/feeds.php new file mode 100644 index 0000000..2ecdb1b --- /dev/null +++ b/tests/Helpers/feeds.php @@ -0,0 +1,26 @@ +whereIn('class', Arr::wrap($classes)) + ->update(['is_active' => false]); +} + +function getAllFeeds(): Collection +{ + return Feed::get(); +} + +function findFeed(string $class): Feed +{ + return Feed::query() + ->whereClass($class) + ->firstOrFail(); +} diff --git a/tests/Helpers/mocks.php b/tests/Helpers/mocks.php new file mode 100644 index 0000000..2d3c8e3 --- /dev/null +++ b/tests/Helpers/mocks.php @@ -0,0 +1,30 @@ +forgetInstance(ClassExistsHelper::class); + + app()->singleton(ClassExistsHelper::class, function () use ($installed) { + $mock = mock(ClassExistsHelper::class); + $mock->shouldReceive('exists')->andReturn($installed); + + return $mock; + }); +} + +function mockPaths(): void +{ + $token = ParallelTesting::token() ?: '0'; + + $operations = config('deploy-operations.path'); + $migrations = database_path($token); + + config()?->set('deploy-operations.path', $operations . '/' . $token); + + app()->useDatabasePath($migrations); +} diff --git a/tests/Pest.php b/tests/Pest.php index fa9a81d..47a69f5 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -3,11 +3,8 @@ declare(strict_types=1); use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Carbon; 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() @@ -16,24 +13,32 @@ pest() ->extend(TestCase::class) ->use(RefreshDatabase::class) - ->in('Feature'); + ->in('Feature') + ->beforeEach(function () { + mockOperations(); + mockPaths(); -pest() - ->extend(TestCase::class) - ->in('Unit'); + deleteOperations(); + deleteMigrations(); + }) + ->afterEach(function () { + deleteOperations(); + deleteMigrations(); + }); pest() - ->in('Feature/Console/Generation') + ->extend(TestCase::class) + ->in('Unit') ->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)); + Carbon::setTestNow('2025-09-03 01:50:24'); + + mockOperations(); + mockPaths(); + + deleteOperations(); + deleteMigrations(); + }) + ->afterEach(function () { + deleteOperations(); + deleteMigrations(); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index 42bf8ab..0f835b2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,8 +6,11 @@ use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as BaseTestCase; +use Workbench\Database\Seeders\DatabaseSeeder; class TestCase extends BaseTestCase { use WithWorkbench; + + public string $seeder = DatabaseSeeder::class; } diff --git a/tests/Unit/Console/Make/FeedTest.php b/tests/Unit/Console/Make/FeedTest.php new file mode 100644 index 0000000..785ec15 --- /dev/null +++ b/tests/Unit/Console/Make/FeedTest.php @@ -0,0 +1,21 @@ + 'FooBar', + '--force' => true, + ]) + ->expectsOutputToContain(resolvePath('app/Feeds/FooBarFeed.php] created successfully')) + ->doesntExpectOutputToContain(resolvePath('app/Feeds/Items')) + ->doesntExpectOutputToContain(resolvePath('app/Feeds/Info')) + ->assertSuccessful() + ->run(); + + expect('FooBar')->toMatchFeedSnapshot(); +}); diff --git a/tests/Unit/Console/Make/InfoTest.php b/tests/Unit/Console/Make/InfoTest.php new file mode 100644 index 0000000..f7e0274 --- /dev/null +++ b/tests/Unit/Console/Make/InfoTest.php @@ -0,0 +1,24 @@ + 'QweRty', + '--info' => true, + '--force' => true, + ]) + ->expectsOutputToContain(resolvePath('app/Feeds/QweRtyFeed.php] created successfully')) + ->doesntExpectOutputToContain(resolvePath('app/Feeds/Items/QweRtyFeedItem.php] created successfully')) + ->expectsOutputToContain(resolvePath('app/Feeds/Info/QweRtyFeedInfo')) + ->assertSuccessful() + ->run(); + + expect('QweRty') + ->toMatchFeedSnapshot() + ->toMatchFeedInfoSnapshot(); +}); diff --git a/tests/Unit/Console/Make/ItemTest.php b/tests/Unit/Console/Make/ItemTest.php new file mode 100644 index 0000000..56fa99a --- /dev/null +++ b/tests/Unit/Console/Make/ItemTest.php @@ -0,0 +1,24 @@ + 'QweRty', + '--item' => true, + '--force' => true, + ]) + ->expectsOutputToContain(resolvePath('app/Feeds/QweRtyFeed.php] created successfully')) + ->expectsOutputToContain(resolvePath('app/Feeds/Items/QweRtyFeedItem.php] created successfully')) + ->doesntExpectOutputToContain(resolvePath('app/Feeds/Info')) + ->assertSuccessful() + ->run(); + + expect('QweRty') + ->toMatchFeedSnapshot() + ->toMatchFeedItemSnapshot(); +}); diff --git a/tests/Unit/Console/Make/MigrationTest.php b/tests/Unit/Console/Make/MigrationTest.php new file mode 100644 index 0000000..b51c3e7 --- /dev/null +++ b/tests/Unit/Console/Make/MigrationTest.php @@ -0,0 +1,21 @@ + 'FooBar', + '--force' => true, + ]) + ->expectsOutputToContain(resolvePath('app/Feeds/FooBarFeed.php] created successfully')) + ->doesntExpectOutputToContain('Operation') + ->expectsOutputToContain('Migration') + ->assertSuccessful() + ->run(); +}); diff --git a/tests/Unit/Console/Make/OperationTest.php b/tests/Unit/Console/Make/OperationTest.php new file mode 100644 index 0000000..5afb3e2 --- /dev/null +++ b/tests/Unit/Console/Make/OperationTest.php @@ -0,0 +1,19 @@ + 'FooBar', + '--force' => true, + ]) + ->expectsOutputToContain(resolvePath('app/Feeds/FooBarFeed.php] created successfully')) + ->expectsOutputToContain('Operation') + ->doesntExpectOutputToContain('Migration') + ->assertSuccessful() + ->run(); +}); diff --git a/tests/Unit/Console/MakeInfoTest.php b/tests/Unit/Console/MakeInfoTest.php index 109dee2..51b4307 100644 --- a/tests/Unit/Console/MakeInfoTest.php +++ b/tests/Unit/Console/MakeInfoTest.php @@ -7,8 +7,6 @@ use function Pest\Laravel\artisan; test('make feed item', function () { - deleteFeed('Info/FooBar'); - artisan(FeedInfoMakeCommand::class, [ 'name' => 'FooBar', '--force' => true, diff --git a/tests/Unit/Console/MakeItemTest.php b/tests/Unit/Console/MakeItemTest.php index 0f9e3f5..12da122 100644 --- a/tests/Unit/Console/MakeItemTest.php +++ b/tests/Unit/Console/MakeItemTest.php @@ -7,8 +7,6 @@ use function Pest\Laravel\artisan; test('make feed item', function () { - deleteFeed('Items/FooBar'); - artisan(FeedItemMakeCommand::class, [ 'name' => 'FooBar', '--force' => true, diff --git a/tests/Unit/Console/MakeTest.php b/tests/Unit/Console/MakeTest.php deleted file mode 100644 index 2aaf173..0000000 --- a/tests/Unit/Console/MakeTest.php +++ /dev/null @@ -1,63 +0,0 @@ - 'FooBar', - '--force' => true, - ]) - ->expectsOutputToContain(resolvePath('app/Feeds/FooBarFeed.php] created successfully')) - ->doesntExpectOutputToContain(resolvePath('app/Feeds/Items')) - ->doesntExpectOutputToContain(resolvePath('app/Feeds/Info')) - ->assertSuccessful() - ->run(); - - expect('FooBar')->toMatchFeedSnapshot(); -}); - -test('make with item', function () { - deleteFeed('QweRty'); - deleteFeed('Items/QweRty'); - - artisan(FeedMakeCommand::class, [ - 'name' => 'QweRty', - '--item' => true, - '--force' => true, - ]) - ->expectsOutputToContain(resolvePath('app/Feeds/QweRtyFeed.php] created successfully')) - ->expectsOutputToContain(resolvePath('app/Feeds/Items/QweRtyFeedItem.php] created successfully')) - ->doesntExpectOutputToContain(resolvePath('app/Feeds/Info')) - ->assertSuccessful() - ->run(); - - expect('QweRty') - ->toMatchFeedSnapshot() - ->toMatchFeedItemSnapshot(); -}); - -test('make with info', function () { - deleteFeed('QweRty'); - deleteFeed('Items/QweRty'); - - artisan(FeedMakeCommand::class, [ - 'name' => 'QweRty', - '--info' => true, - '--force' => true, - ]) - ->expectsOutputToContain(resolvePath('app/Feeds/QweRtyFeed.php] created successfully')) - ->doesntExpectOutputToContain(resolvePath('app/Feeds/Items/QweRtyFeedItem.php] created successfully')) - ->expectsOutputToContain(resolvePath('app/Feeds/Info/QweRtyFeedInfo')) - ->assertSuccessful() - ->run(); - - expect('QweRty') - ->toMatchFeedSnapshot() - ->toMatchFeedInfoSnapshot(); -}); diff --git a/tests/Unit/Feeds/FilenameTest.php b/tests/Unit/Feeds/FilenameTest.php new file mode 100644 index 0000000..4bd2afb --- /dev/null +++ b/tests/Unit/Feeds/FilenameTest.php @@ -0,0 +1,23 @@ +filename())->toMatchSnapshot(); +})->with([ + EmptyFeed::class, + FullFeed::class, + PartialFeed::class, + SitemapFeed::class, + YandexFeed::class, +]); diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php index 2682ce9..2f760f6 100644 --- a/workbench/app/Providers/WorkbenchServiceProvider.php +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -4,27 +4,23 @@ namespace Workbench\App\Providers; -use Illuminate\Config\Repository; -use Illuminate\Foundation\Application; -use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; -use Illuminate\Support\Str; + +use function array_keys; +use function config; class WorkbenchServiceProvider extends ServiceProvider { public function boot(): void { - if ($this->isFreshLaravel()) { - return; + foreach ($this->disks() as $disk) { + Storage::fake($disk); } - - Repository::macro('collection', function (string $key) { - return new Collection($this->get($key)); - }); } - protected function isFreshLaravel(): bool + protected function disks(): array { - return Str::of(Application::VERSION)->before('.')->toString() === '12'; + return array_keys(config('filesystems.disks')); } } diff --git a/workbench/database/seeders/DatabaseSeeder.php b/workbench/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..df3c3aa --- /dev/null +++ b/workbench/database/seeders/DatabaseSeeder.php @@ -0,0 +1,15 @@ +call(FeedSeeder::class); + } +} diff --git a/workbench/database/seeders/FeedSeeder.php b/workbench/database/seeders/FeedSeeder.php new file mode 100644 index 0000000..152c21e --- /dev/null +++ b/workbench/database/seeders/FeedSeeder.php @@ -0,0 +1,39 @@ +feeds as $feed) { + $this->store($feed); + } + } + + protected function store(string $name): void + { + Feed::create([ + 'class' => $name, + 'title' => $name, + ]); + } +}