Merge branch 'refs/heads/master' into feature/new-countries
All checks were successful
/ deploy-job (push) Successful in 0s
All checks were successful
/ deploy-job (push) Successful in 0s
# Conflicts: # src/Command/Migrate.php # templates/product.html.twig # templates/productList.html.twig
This commit is contained in:
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Krzysiej\RyobiCrawler\Command;
|
||||
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Krzysiej\RyobiCrawler\Models\Product;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Twig\Environment;
|
||||
|
||||
#[AsCommand(name: 'app:cache:warm-twig', description: 'Warmup twig cache')]
|
||||
class CacheWarmCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $twig,
|
||||
private RequestStack $requestStack,
|
||||
private FilesystemAdapter $cache,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->cache->clear();
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'sqlite',
|
||||
'database' => __DIR__ . '/../../database.sqlite',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
$progress = new ProgressBar($output);
|
||||
$progress->start();
|
||||
$products = Product::with([
|
||||
'price' => fn($query) => $query->orderByDesc('created_at'),
|
||||
'stock' => fn($query) => $query->orderByDesc('created_at'),
|
||||
])->get();
|
||||
$progress->setMaxSteps(count($products));
|
||||
|
||||
$request = Request::create('/cache-warm');
|
||||
$this->requestStack->push($request);
|
||||
|
||||
foreach ($products as $product) {
|
||||
$priceList = $product->price()->pluck('price');
|
||||
$stockList = $product->stock()->pluck('stock');
|
||||
$priceDates = $product->price()->pluck('created_at')->map(fn($date) => $date->format('Y-m-d'))->toArray();
|
||||
$stockDates = $product->stock()->pluck('created_at')->map(fn($date) => $date->format('Y-m-d'))->toArray();
|
||||
$this->twig->render('product.html.twig', [
|
||||
'product' => $product,
|
||||
'price_list' => $this->prepareChartData($priceDates, $priceList),
|
||||
'stock_list' => $this->prepareChartData($stockDates, $stockList),
|
||||
'price_dates' => implode("','", $priceDates),
|
||||
]);
|
||||
$progress->advance();
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
$output->writeln('');
|
||||
$output->writeln('DONE');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function prepareChartData($set1, $set2): string
|
||||
{
|
||||
$data = [];
|
||||
foreach ($set1 as $key => $value) {
|
||||
$data[] = ['x' => $value, 'y' => $set2[$key]];
|
||||
}
|
||||
$stringData = json_encode($data);
|
||||
|
||||
return str_replace(['"x"', '"y"'], ['x', 'y'], $stringData);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ class Migrate extends Command
|
||||
'productsUrl' => 'https://uk.ryobitools.eu/api/product-listing/get-products',
|
||||
'cultureCode' => 'en-GB',
|
||||
'currency' => 'GBP',
|
||||
'locale' => 'en',
|
||||
'locale' => 'uk',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
@@ -154,6 +154,30 @@ class Migrate extends Command
|
||||
]
|
||||
);
|
||||
}
|
||||
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'Poland')->exists()) {
|
||||
$id = Capsule::table('countries')->insertGetId(
|
||||
[
|
||||
'countryName' => 'Poland',
|
||||
'productsUrl' => 'https://pl.ryobitools.eu/api/product-listing/get-products',
|
||||
'cultureCode' => 'pl-PL',
|
||||
'currency' => 'PLN',
|
||||
'locale' => 'pl',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'UK')->exists()) {
|
||||
Capsule::table('countries')->insert([
|
||||
'countryName' => 'UK',
|
||||
'productsUrl' => 'https://uk.ryobitools.eu/api/product-listing/get-products',
|
||||
'cultureCode' => 'en-GB',
|
||||
'currency' => 'GBP',
|
||||
'locale' => 'uk',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasColumn('products', 'country_id')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) use ($id) {
|
||||
@@ -211,15 +235,78 @@ class Migrate extends Command
|
||||
$table->date('lastSeen')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasColumn('products', 'stock')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->integer('stock')->nullable();
|
||||
});
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('products', 'promotions')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->json('promotions')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
if (!count(Capsule::select('SELECT name FROM sqlite_master WHERE type = "index" and name = "products_skuid_country_id_unique"'))) {
|
||||
if (!$this->hasIndex('products_skuid_country_id_unique')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->integer('skuID')->unique(false)->change();
|
||||
if ($this->hasIndex('products_skuid_unique')) {
|
||||
$table->integer('skuID')->unique(false)->change();
|
||||
}
|
||||
$table->unique(['skuID', 'country_id']);
|
||||
});
|
||||
}
|
||||
|
||||
if (!$this->hasIndex('prices_product_id_index')) {
|
||||
Capsule::schema()->table('prices', function (Blueprint $table) {
|
||||
$table->index('product_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!$this->hasIndex('products_id_index')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->index('id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!$this->hasIndex('stocks_product_id_stock_index')) {
|
||||
Capsule::schema()->table('stocks', function (Blueprint $table) {
|
||||
$table->index(['product_id', 'stock']);
|
||||
});
|
||||
}
|
||||
|
||||
if (!$this->isFK('products', 'id', 'stocks', 'product_id')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->foreign('id')->references('product_id')->on('stocks');
|
||||
});
|
||||
}
|
||||
|
||||
if (!$this->isFK('products', 'id', 'prices', 'product_id')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->foreign('id')->references('product_id')->on('prices');
|
||||
});
|
||||
}
|
||||
if (!$this->isFK('products', 'country_id', 'countries', 'id')) {
|
||||
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||
$table->foreign('country_id')->references('id')->on('countries');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function isFK(string $table, string $column, string $fTable, string $fColumn): bool
|
||||
{
|
||||
$fkColumns = Capsule::schema()->getForeignKeys($table);
|
||||
|
||||
return !empty(array_filter($fkColumns, fn($fkColumn) => ($fkColumn['foreign_table'] == $fTable &&
|
||||
in_array($fColumn, $fkColumn['foreign_columns']) &&
|
||||
in_array($column, $fkColumn['columns']))
|
||||
));
|
||||
}
|
||||
|
||||
private function hasIndex(string $indexName): bool
|
||||
{
|
||||
return !!count(Capsule::select('SELECT name FROM sqlite_master WHERE type = "index" and name = ?', [$indexName]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(name: 'app:scrape', description: 'Scrape all products from Ryobi website')]
|
||||
class ScrapeWebsite extends Command
|
||||
{
|
||||
const COUNTRY_ID = 'country';
|
||||
private Client $client;
|
||||
|
||||
public function __construct(protected Capsule $database)
|
||||
@@ -30,39 +32,42 @@ class ScrapeWebsite extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->client = new Client();
|
||||
$this->addOption(self::COUNTRY_ID, 'c', InputOption::VALUE_OPTIONAL, 'Country id');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$countryId = intval($input->getOption(self::COUNTRY_ID));
|
||||
$output->writeln('Scrape products');
|
||||
$progress = new ProgressBar($output);
|
||||
$countries = Country::all();
|
||||
$countries = Country::all()->when($countryId, fn ($query) => $query->where('id', $countryId));
|
||||
foreach($countries as $country) {
|
||||
$output->writeln('Country name: ' . $country->countryName."\n");
|
||||
$progress->start();
|
||||
$output->writeln('Country name: ' . $country->countryName);
|
||||
$products = $this->getProducts($country);
|
||||
$progress = new ProgressBar($output);
|
||||
$progress->start();
|
||||
$progress->setMaxSteps(count($products));
|
||||
foreach ($products as $product) {
|
||||
$this->saveProduct($product, $country);
|
||||
$progress->advance();
|
||||
}
|
||||
$progress->finish();
|
||||
$output->writeln('');
|
||||
$output->writeln('Scrape products - DONE');
|
||||
$output->writeln('');
|
||||
$output->writeln("\nScrape products - DONE\n");
|
||||
}
|
||||
|
||||
$output->writeln('Update prices');
|
||||
$products = Product::all();
|
||||
$progress = new ProgressBar($output);
|
||||
$progress->setMaxSteps(count($products));
|
||||
$progress->start();
|
||||
foreach($products as $product) {
|
||||
$newestPrice = $product->newestPrice;
|
||||
$currentStock = $product->currentStock;
|
||||
$product->priceCurrent = $newestPrice->price;
|
||||
$product->productStandardPrice = $newestPrice->productStandardPrice;
|
||||
$product->lowestProductPrice30Days = $newestPrice->lowestProductPrice30Days;
|
||||
$product->priceLowest = $product->lowestPrice->price;
|
||||
$product->lastSeen = $newestPrice->created_at->format('Y-m-d');
|
||||
$product->stock = $currentStock->stock;
|
||||
$product->save(['timestamps' => false]);
|
||||
$progress->advance();
|
||||
}
|
||||
@@ -93,7 +98,7 @@ class ScrapeWebsite extends Command
|
||||
$products = array_merge($products, $responseObject->products);
|
||||
$page++;
|
||||
$canLoadMore = $responseObject->canLoadMore;
|
||||
} catch (GuzzleException) {
|
||||
} catch (GuzzleException $e) {
|
||||
return $products;
|
||||
}
|
||||
} while ($canLoadMore);
|
||||
@@ -103,17 +108,13 @@ class ScrapeWebsite extends Command
|
||||
|
||||
private function saveProduct(\stdClass $product, Country $country): void
|
||||
{
|
||||
// if ($product->skuID == 0) {
|
||||
// dump([$product->skuID, $product->name]);
|
||||
// }
|
||||
// return;
|
||||
/** @var Product $productModel */
|
||||
$productModel = Product::firstOrNew(['skuID' => $product->skuID, 'country_id' => $country->id]);
|
||||
|
||||
$productModel->skuID = $product->skuID;
|
||||
$productModel->name = $product->name;
|
||||
$productModel->availableQuantity = $product->availableQuantity;
|
||||
$productModel->categories = $product->categories;
|
||||
$productModel->categories = str_replace(['/', ' '], '', $product->categories);
|
||||
$productModel->image = $product->image;
|
||||
$productModel->subTitle = $product->subTitle;
|
||||
$productModel->variantCode = $product->variantCode;
|
||||
@@ -122,6 +123,7 @@ class ScrapeWebsite extends Command
|
||||
$productModel->lastSeen = date("Y-m-d");
|
||||
$productModel->touch('updated_at');
|
||||
$productModel->country()->associate($country);
|
||||
$productModel->promotions = $product->promotions;
|
||||
$productModel->save();
|
||||
$priceExists = $productModel->price()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user