diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bbeb413..9e0e0d3 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -11,7 +11,7 @@ jobs:
os: [ubuntu-latest]
php: [8.1, 8.2]
laravel: [10.*]
- stability: [prefer-lowest, prefer-stable]
+ stability: [prefer-stable]
include:
- laravel: 10.*
testbench: 8.*
diff --git a/README.md b/README.md
index 1daa00c..142b7ef 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@
This package tracks if products exist in Magento by storing the status locally in the DB.
-We developed this to prevent multiple calls when multiple packages need to check product existance in Magento.
-This package does do the assumption that once a product exists in Magento it will always be there.
+We developed this to prevent multiple calls when multiple packages need to check product existence in Magento.
## Installation
@@ -26,6 +25,9 @@ $schedule->command(\JustBetter\MagentoProducts\Commands\CheckKnownProductsExiste
$schedule->command(\JustBetter\MagentoProducts\Commands\DiscoverMagentoProductsCommand::class)->daily();
```
+> [!IMPORTANT]
+> This package requires Job Batching
+
## Usage
### Checking if a product exists in Magento
diff --git a/composer.json b/composer.json
index 95bbc78..a81ad5b 100644
--- a/composer.json
+++ b/composer.json
@@ -9,10 +9,13 @@
"laravel/framework": "^10.0"
},
"require-dev": {
- "laravel/pint": "^1.6",
- "nunomaduro/larastan": "^2.5",
- "phpstan/phpstan-mockery": "^1.1",
- "phpunit/phpunit": "^10.0"
+ "laravel/pint": "^1.6",
+ "nunomaduro/larastan": "^2.5",
+ "orchestra/testbench": "^8.0",
+ "pestphp/pest": "^2.0",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpunit/phpunit": "^10.0",
+ "doctrine/dbal": "^3.7.1"
},
"authors": [
{
@@ -43,7 +46,10 @@
"fix-style": "pint"
},
"config": {
- "sort-packages": true
+ "sort-packages": true,
+ "allow-plugins": {
+ "pestphp/pest-plugin": true
+ }
},
"extra": {
"laravel": {
diff --git a/database/migrations/2023_11_21_102103_magento_products_retrieved_field.php b/database/migrations/2023_11_21_102103_magento_products_retrieved_field.php
new file mode 100644
index 0000000..904d158
--- /dev/null
+++ b/database/migrations/2023_11_21_102103_magento_products_retrieved_field.php
@@ -0,0 +1,20 @@
+boolean('retrieved')->default(false)->after('last_checked');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropColumns('magento_products', ['retrieved']);
+ }
+};
diff --git a/phpunit.xml b/phpunit.xml
index bcf40ad..c2f1f77 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,17 +1,14 @@
-
-
-
- ./tests/*
-
-
-
-
- ./src
-
-
+
+
+
+ ./tests/*
+
+
+
+
diff --git a/src/Actions/CheckRemovedProducts.php b/src/Actions/CheckRemovedProducts.php
new file mode 100644
index 0000000..2eefd19
--- /dev/null
+++ b/src/Actions/CheckRemovedProducts.php
@@ -0,0 +1,30 @@
+where('exists_in_magento', '=', true)
+ ->where('retrieved', '=', false);
+
+ $skus = $query->select(['sku'])->get();
+
+ $query->update([
+ 'exists_in_magento' => false,
+ ]);
+
+ $skus->each(fn (MagentoProduct $product) => ProductDeletedInMagentoEvent::dispatch($product->sku));
+ }
+
+ public static function bind(): void
+ {
+ app()->singleton(ChecksRemovedProducts::class, static::class);
+ }
+}
diff --git a/src/Actions/DiscoverMagentoProducts.php b/src/Actions/DiscoverMagentoProducts.php
new file mode 100644
index 0000000..88769d6
--- /dev/null
+++ b/src/Actions/DiscoverMagentoProducts.php
@@ -0,0 +1,46 @@
+update(['retrieved' => false]);
+ }
+
+ $skus = $this->magentoSkus->retrieve($page);
+
+ $hasNextPage = $skus->count() == config('magento-products.page_size');
+
+ if ($hasNextPage) {
+ $batch->add(new DiscoverMagentoProductsJob($page + 1));
+ }
+
+ MagentoProduct::query()
+ ->whereIn('sku', $skus)
+ ->update(['retrieved' => true]);
+
+ $this->processor->process($skus);
+ }
+
+ public static function bind(): void
+ {
+ app()->singleton(DiscoversMagentoProducts::class, static::class);
+ }
+}
diff --git a/src/Actions/ProcessMagentoSkus.php b/src/Actions/ProcessMagentoSkus.php
index 2b45d7a..5aa6c5b 100644
--- a/src/Actions/ProcessMagentoSkus.php
+++ b/src/Actions/ProcessMagentoSkus.php
@@ -15,6 +15,7 @@ public function process(Enumerable $skus): void
->whereIn('sku', $skus)
->where('exists_in_magento', true)
->select(['sku'])
+ ->distinct()
->get()
->pluck('sku');
diff --git a/src/Commands/DiscoverMagentoProductsCommand.php b/src/Commands/DiscoverMagentoProductsCommand.php
index 64b5390..b52a200 100644
--- a/src/Commands/DiscoverMagentoProductsCommand.php
+++ b/src/Commands/DiscoverMagentoProductsCommand.php
@@ -3,6 +3,8 @@
namespace JustBetter\MagentoProducts\Commands;
use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Bus;
+use JustBetter\MagentoProducts\Jobs\CheckRemovedProductsJob;
use JustBetter\MagentoProducts\Jobs\DiscoverMagentoProductsJob;
class DiscoverMagentoProductsCommand extends Command
@@ -13,11 +15,11 @@ class DiscoverMagentoProductsCommand extends Command
public function handle(): int
{
- $this->info('Dispatching...');
-
- DiscoverMagentoProductsJob::dispatch();
-
- $this->info('Done!');
+ Bus::batch([new DiscoverMagentoProductsJob])
+ ->name('Discover Magento Products')
+ ->then(fn () => CheckRemovedProductsJob::dispatch())
+ ->onQueue(config('magento-products.queue'))
+ ->dispatch();
return static::SUCCESS;
}
diff --git a/src/Contracts/ChecksRemovedProducts.php b/src/Contracts/ChecksRemovedProducts.php
new file mode 100644
index 0000000..59be795
--- /dev/null
+++ b/src/Contracts/ChecksRemovedProducts.php
@@ -0,0 +1,8 @@
+onQueue(config('magento-products.queue'));
+ }
+
+ public function handle(ChecksRemovedProducts $contract): void
+ {
+ $contract->check();
+ }
+}
diff --git a/src/Jobs/DiscoverMagentoProductsJob.php b/src/Jobs/DiscoverMagentoProductsJob.php
index c336c17..555be1d 100644
--- a/src/Jobs/DiscoverMagentoProductsJob.php
+++ b/src/Jobs/DiscoverMagentoProductsJob.php
@@ -9,8 +9,7 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
-use JustBetter\MagentoProducts\Contracts\ProcessesMagentoSkus;
-use JustBetter\MagentoProducts\Contracts\RetrievesMagentoSkus;
+use JustBetter\MagentoProducts\Contracts\DiscoversMagentoProducts;
class DiscoverMagentoProductsJob implements ShouldBeUnique, ShouldQueue
{
@@ -31,23 +30,13 @@ public function __construct(public int $page = 0)
$this->onQueue(config('magento-products.queue'));
}
- public function handle(
- RetrievesMagentoSkus $retrievesMagentoSkus,
- ProcessesMagentoSkus $processesMagentoSkus
- ): void {
- $skus = $retrievesMagentoSkus->retrieve($this->page);
-
- $hasNextPage = $skus->count() == config('magento-products.page_size', 50);
-
- if ($hasNextPage) {
- if ($this->batching()) {
- $this->batch()->add(new static($this->page + 1)); /** @phpstan-ignore-line */
- } else {
- static::dispatch($this->page + 1);
- }
+ public function handle(DiscoversMagentoProducts $contract): void
+ {
+ if ($this->batch() === null || $this->batch()->cancelled()) {
+ return;
}
- $processesMagentoSkus->process($skus);
+ $contract->discover($this->page, $this->batch());
}
public function tags(): array
diff --git a/src/Models/MagentoProduct.php b/src/Models/MagentoProduct.php
index abf8893..eb7bee8 100644
--- a/src/Models/MagentoProduct.php
+++ b/src/Models/MagentoProduct.php
@@ -2,10 +2,9 @@
namespace JustBetter\MagentoProducts\Models;
-use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
-use JustBetter\MagentoProducts\Contracts\ChecksMagentoExistence;
+use Illuminate\Support\Carbon;
/**
* @property int $id
@@ -15,6 +14,9 @@
* @property bool $enabled
* @property ?array $data
* @property ?Carbon $last_checked
+ * @property bool $retrieved
+ * @property ?Carbon $created_at
+ * @property ?Carbon $updated_at
*/
class MagentoProduct extends Model
{
@@ -24,6 +26,7 @@ class MagentoProduct extends Model
'data' => 'array',
'exists_in_magento' => 'boolean',
'last_checked' => 'datetime',
+ 'retrieved' => 'boolean',
];
public static function findBySku(string $sku, string $store = null): ?static
@@ -36,17 +39,4 @@ public static function findBySku(string $sku, string $store = null): ?static
return $item;
}
-
- /**
- * @deprecated Use the action ChecksMagentoExistence instead
- *
- * @codeCoverageIgnore
- */
- public static function existsInMagento(string $sku): bool
- {
- /** @var ChecksMagentoExistence $action */
- $action = app(ChecksMagentoExistence::class);
-
- return $action->exists($sku);
- }
}
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
index 367818d..99a9205 100644
--- a/src/ServiceProvider.php
+++ b/src/ServiceProvider.php
@@ -8,6 +8,8 @@
use JustBetter\MagentoProducts\Actions\CheckKnownProducts;
use JustBetter\MagentoProducts\Actions\CheckMagentoEnabled;
use JustBetter\MagentoProducts\Actions\CheckMagentoExistence;
+use JustBetter\MagentoProducts\Actions\CheckRemovedProducts;
+use JustBetter\MagentoProducts\Actions\DiscoverMagentoProducts;
use JustBetter\MagentoProducts\Actions\ProcessMagentoSkus;
use JustBetter\MagentoProducts\Actions\RetrieveMagentoSkus;
use JustBetter\MagentoProducts\Actions\RetrieveProductData;
@@ -30,6 +32,8 @@ public function register(): void
ProcessMagentoSkus::bind();
RetrieveMagentoSkus::bind();
RetrieveProductData::bind();
+ DiscoverMagentoProducts::bind();
+ CheckRemovedProducts::bind();
}
public function boot(): void
diff --git a/tests/Actions/CheckRemovedProductsTest.php b/tests/Actions/CheckRemovedProductsTest.php
new file mode 100644
index 0000000..5256efb
--- /dev/null
+++ b/tests/Actions/CheckRemovedProductsTest.php
@@ -0,0 +1,41 @@
+create([
+ 'sku' => '::sku_1::',
+ 'exists_in_magento' => true,
+ 'retrieved' => false,
+ ]);
+
+ MagentoProduct::query()->create([
+ 'sku' => '::sku_2::',
+ 'exists_in_magento' => false,
+ 'retrieved' => false,
+ ]);
+
+ /** @var CheckRemovedProducts $action */
+ $action = app(CheckRemovedProducts::class);
+ $action->check();
+
+ /** @var ?MagentoProduct $removedProduct */
+ $removedProduct = MagentoProduct::query()->firstWhere('sku', '=', '::sku_1::');
+
+ $this->assertNotNull($removedProduct);
+ $this->assertFalse($removedProduct->exists_in_magento);
+
+ Event::assertDispatchedTimes(ProductDeletedInMagentoEvent::class, 1);
+ }
+}
diff --git a/tests/Actions/DiscoverMagentoProductsTest.php b/tests/Actions/DiscoverMagentoProductsTest.php
new file mode 100644
index 0000000..a9c77f6
--- /dev/null
+++ b/tests/Actions/DiscoverMagentoProductsTest.php
@@ -0,0 +1,142 @@
+set('magento-products.page_size', 2);
+ }
+
+ public function test_it_processes_single_page(): void
+ {
+ $skus = collect(['123']);
+
+ $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) use ($skus) {
+ $mock->shouldReceive('retrieve')->with(0)->once()
+ ->andReturn($skus);
+ });
+
+ $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) use ($skus) {
+ $mock->shouldReceive('process')->with($skus)->once();
+ });
+
+ $job = new DiscoverMagentoProductsJob();
+ $job->withFakeBatch();
+
+ /** @var Batch $batch */
+ $batch = $job->batch();
+
+ /** @var DiscoverMagentoProducts $action */
+ $action = app(DiscoverMagentoProducts::class);
+ $action->discover(0, $batch);
+ }
+
+ public function test_it_dispatches_next_job(): void
+ {
+ $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) {
+ $mock->shouldReceive('retrieve')->with(0)->once()
+ ->andReturn(collect(['123', '456']));
+ });
+
+ $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) {
+ $mock->shouldReceive('process')->once();
+ });
+
+ $job = new DiscoverMagentoProductsJob();
+ $job->withFakeBatch();
+
+ /** @var Batch $batch */
+ $batch = $job->batch();
+
+ /** @var DiscoverMagentoProducts $action */
+ $action = app(DiscoverMagentoProducts::class);
+ $action->discover(0, $batch);
+
+ /** @var ?DiscoverMagentoProductsJob $addedJob */
+ $addedJob = $job->batch()->added[0] ?? null;
+
+ $this->assertNotNull($addedJob);
+ $this->assertEquals(1, $addedJob->page);
+ }
+
+ public function test_it_sets_retrieved_false(): void
+ {
+ MagentoProduct::query()->create([
+ 'sku' => '123',
+ 'exists_in_magento' => true,
+ 'retrieved' => true,
+ ]);
+
+ $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) {
+ $mock->shouldReceive('retrieve')->with(0)->once()
+ ->andReturn(collect());
+ });
+
+ $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) {
+ $mock->shouldReceive('process')->once();
+ });
+
+ $job = new DiscoverMagentoProductsJob();
+ $job->withFakeBatch();
+
+ /** @var Batch $batch */
+ $batch = $job->batch();
+
+ /** @var DiscoverMagentoProducts $action */
+ $action = app(DiscoverMagentoProducts::class);
+ $action->discover(0, $batch);
+
+ /** @var ?MagentoProduct $product */
+ $product = MagentoProduct::query()->first();
+
+ $this->assertNotNull($product);
+ $this->assertFalse($product->retrieved);
+ }
+
+ public function test_it_sets_retrieved_true(): void
+ {
+ MagentoProduct::query()->create([
+ 'sku' => '123',
+ 'exists_in_magento' => true,
+ 'retrieved' => true,
+ ]);
+
+ $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) {
+ $mock->shouldReceive('retrieve')->with(0)->once()
+ ->andReturn(collect(['123']));
+ });
+
+ $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) {
+ $mock->shouldReceive('process')->once();
+ });
+
+ $job = new DiscoverMagentoProductsJob();
+ $job->withFakeBatch();
+
+ /** @var Batch $batch */
+ $batch = $job->batch();
+
+ /** @var DiscoverMagentoProducts $action */
+ $action = app(DiscoverMagentoProducts::class);
+ $action->discover(0, $batch);
+
+ /** @var ?MagentoProduct $product */
+ $product = MagentoProduct::query()->first();
+
+ $this->assertNotNull($product);
+ $this->assertTrue($product->retrieved);
+ }
+}
diff --git a/tests/Commands/DiscoverMagentoProductsCommandTest.php b/tests/Commands/DiscoverMagentoProductsCommandTest.php
index 98a1311..5aa2ec2 100644
--- a/tests/Commands/DiscoverMagentoProductsCommandTest.php
+++ b/tests/Commands/DiscoverMagentoProductsCommandTest.php
@@ -3,6 +3,7 @@
namespace JustBetter\MagentoProducts\Tests\Commands;
use Illuminate\Support\Facades\Bus;
+use Illuminate\Support\Testing\Fakes\PendingBatchFake;
use JustBetter\MagentoProducts\Commands\DiscoverMagentoProductsCommand;
use JustBetter\MagentoProducts\Jobs\DiscoverMagentoProductsJob;
use JustBetter\MagentoProducts\Tests\TestCase;
@@ -15,6 +16,8 @@ public function test_it_dispatches_job(): void
$this->artisan(DiscoverMagentoProductsCommand::class);
- Bus::assertDispatched(DiscoverMagentoProductsJob::class);
+ Bus::assertBatched(function (PendingBatchFake $batch) {
+ return $batch->jobs->count() === 1 && get_class($batch->jobs->first()) === DiscoverMagentoProductsJob::class;
+ });
}
}
diff --git a/tests/Jobs/CheckRemovedProductsJobTest.php b/tests/Jobs/CheckRemovedProductsJobTest.php
new file mode 100644
index 0000000..1e9d503
--- /dev/null
+++ b/tests/Jobs/CheckRemovedProductsJobTest.php
@@ -0,0 +1,20 @@
+mock(ChecksRemovedProducts::class, function (MockInterface $mock): void {
+ $mock->shouldReceive('check')->once();
+ });
+
+ CheckRemovedProductsJob::dispatch();
+ }
+}
diff --git a/tests/Jobs/DiscoverMagentoProductsJobTest.php b/tests/Jobs/DiscoverMagentoProductsJobTest.php
index 3e60aa4..6c780ca 100644
--- a/tests/Jobs/DiscoverMagentoProductsJobTest.php
+++ b/tests/Jobs/DiscoverMagentoProductsJobTest.php
@@ -2,57 +2,40 @@
namespace JustBetter\MagentoProducts\Tests\Jobs;
-use JustBetter\MagentoProducts\Contracts\ProcessesMagentoSkus;
-use JustBetter\MagentoProducts\Contracts\RetrievesMagentoSkus;
+use Illuminate\Support\Facades\Bus;
+use JustBetter\MagentoProducts\Contracts\DiscoversMagentoProducts;
use JustBetter\MagentoProducts\Jobs\DiscoverMagentoProductsJob;
use JustBetter\MagentoProducts\Tests\TestCase;
use Mockery\MockInterface;
class DiscoverMagentoProductsJobTest extends TestCase
{
- protected function setUp(): void
+ public function test_it_calls_action(): void
{
- parent::setUp();
- }
-
- public function test_it_processes_single_page(): void
- {
- config()->set('magento-products.page_size', 2);
-
- $skus = collect(['123']);
-
- $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) use ($skus) {
- $mock->shouldReceive('retrieve')->with(0)->once()
- ->andReturn($skus);
+ $this->mock(DiscoversMagentoProducts::class, function (MockInterface $mock): void {
+ $mock->shouldReceive('discover')->once();
});
- $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) use ($skus) {
- $mock->shouldReceive('process')->with($skus)->once();
- });
+ $job = new DiscoverMagentoProductsJob(0);
+ $job->withFakeBatch();
- DiscoverMagentoProductsJob::dispatch(0);
+ Bus::dispatch($job);
}
- public function test_it_processes_two_pages(): void
+ public function test_it_stops_when_batch_is_cancelled(): void
{
- config()->set('magento-products.page_size', 2);
-
- $this->mock(RetrievesMagentoSkus::class, function (MockInterface $mock) {
- $mock->shouldReceive('retrieve')->with(0)->once()
- ->andReturn(collect(['123', '456']));
-
- $mock->shouldReceive('retrieve')->with(1)->once()
- ->andReturn(collect(['789']));
+ $this->mock(DiscoversMagentoProducts::class, function (MockInterface $mock): void {
+ $mock->shouldNotReceive('discover');
});
- $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) {
- $mock->shouldReceive('process')->twice();
- });
+ $job = new DiscoverMagentoProductsJob(0);
+ $job->withFakeBatch();
+ $job->batch()?->cancel();
- DiscoverMagentoProductsJob::dispatch(0);
+ Bus::dispatch($job);
}
- public function test_tags(): void
+ public function test_it_has_tags(): void
{
$job = new DiscoverMagentoProductsJob(0);