Okay, malá reklama pre potravinové reťazce: Vypustil som na nich projekt Prometheus. Systémy hlásia úspech, takto to vypadalo v obrazoch:

Okay, malá reklama pre potravinové reťazce: Vypustil som na nich projekt Prometheus. Systémy hlásia úspech, takto to vypadalo v obrazoch:

Jedná sa o vyhľadávanie v databáze, nie scrapping, čo je podľa zmlúv dovolené: https://hrubos.tech/blogy/content/images/20260911131807-nespcarpping.png

explanation

https://hrubos.tech/blogy/content/images/20260911125802-prometheus.png

prometheus2

https://hrubos.tech/blogy/content/images/20260911125947-prometheus2.png

prometheus3

https://hrubos.tech/blogy/content/images/20260911130020-prometheus3.png

prometheus4

https://hrubos.tech/blogy/content/images/20260911130057-prometheus4.png

Ochutnávka: tail -f storage/logs/lidl.log

á do /config/logging.php dať:

'lidl' => [
    'driver' => 'single',
    'path' => storage_path('logs/lidl.log'),
    'level' => 'info',
    'replace_placeholders' => true,
],

        'kaufland' => [
    'driver' => 'single',
    'path' => storage_path('logs/kaufland.log'),
    'level' => 'info',
    'replace_placeholders' => true,
],

Potom klasika kontrolér:

lesnavevericka@192 projects % curl -sS \
  'http://localhost:8000/api/interconnector/lidl'  
{"success":true,"shop":"lidl","scraped_at":"2026-09-11T10:57:26+00:00","search_terms":1602,"successful_searches":1158,"failed_searches":444,"raw_products":11394,"product_count":1012,"duplicates_removed":10382,"duration_seconds":1944.53,"file":"private\/prometheus\/lidl.json"}%                                                                                   lesnavevericka@192 projects % 

Náhľad jedného z kontrollérov:

<?php

namespace App\Http\Controllers\Interconnector;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Throwable;

class LidlController
{
    private const SEARCH_URL = 'https://www.lidl.sk/q/search';

    private const CSV_FILE = 'private/prometheus/potraviny.csv';

    private const OUTPUT_FILE = 'private/prometheus/lidl.json';

    private const PROGRESS_FILE =
        'private/prometheus/lidl-progress.json';

    private const USER_AGENT =
        'Mozilla/5.0 (X11; Linux x86_64; rv:142.0) '
        . 'Gecko/20100101 Firefox/142.0';

    /**
     * Koľko výrazov spracovať medzi checkpointmi.
     */
    private const CHECKPOINT_EVERY = 25;

    public function index()
    {
        /*
         * PHP nesmie ukončiť script kvôli execution timeoutu.
         */
        ini_set('memory_limit', '4G');
        set_time_limit(0);

        $startedAt = microtime(true);

        Log::channel('lidl')->info(
            '========== LIDL RUN START =========='
        );

        try {
            /*
             * -----------------------------------------------------
             * TEST JEDNÉHO VÝRAZU
             * -----------------------------------------------------
             *
             * Napr.:
             *
             * /api/interconnector/lidl?q=ovocny+jogurt
             *
             * Tento režim nezasahuje do checkpointu ani lidl.json.
             */
            $testTerm = request()->query('q');

            if (
                is_string($testTerm) &&
                trim($testTerm) !== ''
            ) {
                $testTerm = trim($testTerm);

                Log::channel('lidl')->info(
                    '[Lidl] TEST REŽIM: "' . $testTerm . '"'
                );

                $found = $this->searchWebPage($testTerm);

                Log::channel('lidl')->info(
                    '[Lidl] TEST HOTOVÝ: ' .
                    count($found) .
                    ' produktov.'
                );

                return response()->json([
                    'success' => true,
                    'test' => true,
                    'shop' => 'lidl',
                    'search_term' => $testTerm,
                    'product_count' => count($found),
                    'products' => $found,
                    'duration_seconds' =>
                        round(
                            microtime(true) - $startedAt,
                            2
                        ),
                ]);
            }

            /*
             * -----------------------------------------------------
             * RESET
             * -----------------------------------------------------
             *
             * /api/interconnector/lidl?reset=1
             *
             * Vymaže checkpoint a začne od prvého výrazu.
             */
            $reset = request()->boolean('reset');

            if ($reset) {
                $this->deleteProgress();

                Log::channel('lidl')->info(
                    '[Lidl] RESET: checkpoint bol vymazaný.'
                );
            }

            /*
             * -----------------------------------------------------
             * CSV
             * -----------------------------------------------------
             */
            $terms = $this->loadSearchTerms();

            $total = count($terms);

            Log::channel('lidl')->info(
                '[Lidl] Načítaných vyhľadávacích výrazov: ' .
                $total
            );

            if ($total === 0) {
                throw new \RuntimeException(
                    'CSV neobsahuje žiadne vyhľadávacie výrazy.'
                );
            }

            /*
             * -----------------------------------------------------
             * OBNOVENIE CHECKPOINTU
             * -----------------------------------------------------
             */
            $progress = $this->loadProgress();

            $startIndex = 0;

            $products = [];

            $successfulSearches = 0;
            $failedSearches = 0;
            $rawProducts = 0;

            if ($progress !== null) {
                /*
                 * Kontrolujeme, či checkpoint patrí rovnakému
                 * počtu výrazov.
                 */
                if (
                    isset($progress['total_terms']) &&
                    (int) $progress['total_terms'] === $total
                ) {
                    $startIndex =
                        max(
                            0,
                            min(
                                $total,
                                (int) (
                                    $progress['next_index']
                                    ?? 0
                                )
                            )
                        );

                    $successfulSearches =
                        (int) (
                            $progress['successful_searches']
                            ?? 0
                        );

                    $failedSearches =
                        (int) (
                            $progress['failed_searches']
                            ?? 0
                        );

                    $rawProducts =
                        (int) (
                            $progress['raw_products']
                            ?? 0
                        );

                    /*
                     * Obnovíme produkty.
                     */
                    if (
                        isset($progress['products']) &&
                        is_array($progress['products'])
                    ) {
                        foreach (
                            $progress['products']
                            as $product
                        ) {
                            if (
                                !is_array($product) ||
                                !isset(
                                    $product['product_id']
                                )
                            ) {
                                continue;
                            }

                            $products[
                                (string)
                                $product['product_id']
                            ] = $product;
                        }
                    }

                    Log::channel('lidl')->info(
                        sprintf(
                            '[Lidl] OBNOVENIE: pokračujem od %d/%d. ' .
                            'Doteraz unikátnych produktov: %d, ' .
                            'úspešných: %d, chybných: %d.',
                            $startIndex,
                            $total,
                            count($products),
                            $successfulSearches,
                            $failedSearches
                        )
                    );
                } else {
                    Log::channel('lidl')->warning(
                        '[Lidl] Checkpoint má iný počet výrazov. ' .
                        'Začínam od nuly.'
                    );
                }
            } else {
                Log::channel('lidl')->info(
                    '[Lidl] Checkpoint neexistuje. ' .
                    'Začínam od prvého výrazu.'
                );
            }

            /*
             * -----------------------------------------------------
             * HLAVNÝ LOOP
             * -----------------------------------------------------
             */
            for (
                $index = $startIndex;
                $index < $total;
                $index++
            ) {
                $term = $terms[$index];

                $number = $index + 1;

                $percent = (int) floor(
                    ($number / $total) * 100
                );

                Log::channel('lidl')->info(
                    sprintf(
                        '[Lidl] [%d/%d] %d%% - "%s"',
                        $number,
                        $total,
                        $percent,
                        $term
                    )
                );

                $searchStarted = microtime(true);

                try {
                    $found = $this->searchWebPage($term);

                    $count = count($found);

                    $rawProducts += $count;

                    $successfulSearches++;

                    foreach ($found as $product) {
                        $productId =
                            $product['product_id']
                            ?? null;

                        if ($productId === null) {
                            continue;
                        }

                        $productId = (string) $productId;

                        /*
                         * Ak produkt už existuje, zachováme jeho
                         * existujúce search_terms a pridáme nový.
                         */
                        if (
                            isset(
                                $products[$productId]
                            )
                        ) {
                            $existing =
                                $products[$productId];

                            $existingTerms =
                                $existing['search_terms']
                                ?? [];

                            if (
                                !in_array(
                                    $term,
                                    $existingTerms,
                                    true
                                )
                            ) {
                                $existingTerms[] = $term;
                            }

                            $product['search_terms'] =
                                $existingTerms;

                            $products[$productId] =
                                $product;
                        } else {
                            $products[$productId] =
                                $product;
                        }
                    }

                    $elapsed =
                        microtime(true)
                        - $searchStarted;

                    Log::channel('lidl')->info(
                        sprintf(
                            '[Lidl]   OK: %d produktov ' .
                            '(%.2fs), spolu: %d',
                            $count,
                            $elapsed,
                            count($products)
                        )
                    );
                } catch (Throwable $e) {
                    $failedSearches++;

                    Log::channel('lidl')->error(
                        '[Lidl]   EXCEPTION: ' .
                        $e->getMessage()
                    );
                }

                /*
                 * -------------------------------------------------
                 * CHECKPOINT
                 * -------------------------------------------------
                 *
                 * Uložíme stav po každom 25. výraze.
                 *
                 * Ak webserver spojenie zabije, máme maximálne
                 * posledných 24 výrazov, o ktoré môžeme prísť.
                 */
                $completed = $index + 1;

                if (
                    $completed % self::CHECKPOINT_EVERY === 0 ||
                    $completed === $total
                ) {
                    $this->saveProgress([
                        'success' => true,
                        'shop' => 'lidl',

                        'updated_at' =>
                            now()->toIso8601String(),

                        'total_terms' => $total,

                        'completed_terms' =>
                            $completed,

                        'next_index' =>
                            $completed,

                        'successful_searches' =>
                            $successfulSearches,

                        'failed_searches' =>
                            $failedSearches,

                        'raw_products' =>
                            $rawProducts,

                        'product_count' =>
                            count($products),

                        'products' =>
                            array_values($products),
                    ]);

                    Log::channel('lidl')->info(
                        sprintf(
                            '[Lidl] CHECKPOINT: %d/%d ' .
                            '(%d%%), produkty: %d',
                            $completed,
                            $total,
                            (int) floor(
                                ($completed / $total) * 100
                            ),
                            count($products)
                        )
                    );
                }

                /*
                 * Pauza medzi Lidl requestami.
                 */
                usleep(200000);
            }

            /*
             * -----------------------------------------------------
             * CELÉ KOLO HOTOVÉ
             * -----------------------------------------------------
             */
            $products = array_values($products);

            /*
             * Ochrana proti zlému parseru.
             */
            if (count($products) === 0) {
                throw new \RuntimeException(
                    'Lidl parser returned 0 products.'
                );
            }

            /*
             * Ak máme extrémne veľa chýb a nulové výsledky
             * z väčšiny requestov, radšej JSON neprepíšeme.
             */
            if (
                $successfulSearches === 0
            ) {
                throw new \RuntimeException(
                    'Lidl nemal ani jedno úspešné vyhľadávanie.'
                );
            }

            /*
             * -----------------------------------------------------
             * FINÁLNY JSON
             * -----------------------------------------------------
             */
            $json = [
                'success' => true,
                'shop' => 'lidl',

                'scraped_at' =>
                    now()->toIso8601String(),

                'search_terms' =>
                    $total,

                'successful_searches' =>
                    $successfulSearches,

                'failed_searches' =>
                    $failedSearches,

                'raw_products' =>
                    $rawProducts,

                'product_count' =>
                    count($products),

                'duplicates_removed' =>
                    max(
                        0,
                        $rawProducts
                        - count($products)
                    ),

                'duration_seconds' =>
                    round(
                        microtime(true)
                        - $startedAt,
                        2
                    ),

                'products' =>
                    $products,
            ];

            /*
             * -----------------------------------------------------
             * ATOMICKÝ ZÁPIS
             * -----------------------------------------------------
             *
             * Najprv zapíšeme temporary súbor.
             * Až keď je kompletný, nahradíme lidl.json.
             */
            $jsonContent = json_encode(
                $json,
                JSON_PRETTY_PRINT |
                JSON_UNESCAPED_UNICODE |
                JSON_UNESCAPED_SLASHES |
                JSON_THROW_ON_ERROR
            );

            $temporaryFile =
                self::OUTPUT_FILE . '.tmp';

            Storage::disk('local')->put(
                $temporaryFile,
                $jsonContent
            );

            /*
             * Kontrola, že temporary JSON naozaj existuje
             * a nie je prázdny.
             */
            if (
                !Storage::disk('local')->exists(
                    $temporaryFile
                )
            ) {
                throw new \RuntimeException(
                    'Temporary Lidl JSON sa nepodarilo vytvoriť.'
                );
            }

            $savedContent =
                Storage::disk('local')->get(
                    $temporaryFile
                );

            if (
                trim($savedContent) === ''
            ) {
                throw new \RuntimeException(
                    'Temporary Lidl JSON je prázdny.'
                );
            }

            /*
             * Až teraz nahradíme ostrý JSON.
             */
            Storage::disk('local')->move(
                $temporaryFile,
                self::OUTPUT_FILE
            );

            /*
             * -----------------------------------------------------
             * CHECKPOINT UŽ NETREBA
             * -----------------------------------------------------
             */
            $this->deleteProgress();

            Log::channel('lidl')->info(
                sprintf(
                    '[Lidl] HOTOVO: %d unikátnych produktov.',
                    count($products)
                )
            );

            Log::channel('lidl')->info(
                sprintf(
                    '[Lidl] Úspešné vyhľadávania: %d, ' .
                    'chybné: %d.',
                    $successfulSearches,
                    $failedSearches
                )
            );

            Log::channel('lidl')->info(
                '[Lidl] JSON uložený do: ' .
                self::OUTPUT_FILE
            );

            Log::channel('lidl')->info(
                '========== LIDL RUN END =========='
            );

            return response()->json([
                'success' => true,
                'shop' => 'lidl',

                'scraped_at' =>
                    now()->toIso8601String(),

                'search_terms' =>
                    $total,

                'successful_searches' =>
                    $successfulSearches,

                'failed_searches' =>
                    $failedSearches,

                'raw_products' =>
                    $rawProducts,

                'product_count' =>
                    count($products),

                'duplicates_removed' =>
                    max(
                        0,
                        $rawProducts
                        - count($products)
                    ),

                'duration_seconds' =>
                    round(
                        microtime(true)
                        - $startedAt,
                        2
                    ),

                'file' =>
                    self::OUTPUT_FILE,
            ]);
        } catch (Throwable $e) {
            Log::channel('lidl')->error(
                '[Lidl] FATAL: ' .
                $e->getMessage()
            );

            /*
             * Skontrolujeme, či existuje starý JSON.
             */
            $oldJsonExists =
                Storage::disk('local')->exists(
                    self::OUTPUT_FILE
                );

            Log::channel('lidl')->error(
                '[Lidl] Existujúci JSON zachovaný: ' .
                (
                    $oldJsonExists
                        ? 'ÁNO'
                        : 'NIE'
                )
            );

            Log::channel('lidl')->info(
                '========== LIDL RUN FAILED =========='
            );

            /*
             * HTTP 500 je zámerný.
             *
             * Cron tak vie, že beh nedopadol kompletne.
             */
            return response()->json([
                'success' => false,
                'shop' => 'lidl',

                'error' =>
                    $e->getMessage(),

                'output_file' =>
                    self::OUTPUT_FILE,

                'existing_json_preserved' =>
                    $oldJsonExists,
            ], 500);
        }
    }

    /**
     * -------------------------------------------------------------
     * CSV
     * -------------------------------------------------------------
     */
    private function loadSearchTerms(): array
    {
        $path = storage_path(
            'app/' . self::CSV_FILE
        );

        if (!is_file($path)) {
            throw new \RuntimeException(
                'CSV súbor neexistuje: ' . $path
            );
        }

        $content = file_get_contents($path);

        if ($content === false) {
            throw new \RuntimeException(
                'Nepodarilo sa načítať CSV.'
            );
        }

        /*
         * Odstránenie UTF-8 BOM.
         */
        $content = preg_replace(
            '/^\xEF\xBB\xBF/',
            '',
            $content
        );

        $lines = preg_split(
            '/\r\n|\r|\n/',
            $content
        );

        $terms = [];

        foreach ($lines as $line) {
            $line = trim($line);

            if ($line === '') {
                continue;
            }

            $values = str_getcsv(
                $line,
                ';'
            );

            foreach ($values as $value) {
                $value = trim($value);

                if ($value !== '') {
                    $terms[] = $value;
                }
            }
        }

        return array_values(
            array_unique($terms)
        );
    }

    /**
     * -------------------------------------------------------------
     * LIDL SEARCH
     * -------------------------------------------------------------
     */
    private function searchWebPage(
        string $term
    ): array {
        $url = self::SEARCH_URL . '?' .
            http_build_query(
                [
                    'q' => $term,
                    'category' =>
                        'Jedlo a nápoje',
                ],
                '',
                '&',
                PHP_QUERY_RFC3986
            );

        $response = Http::withHeaders([
            'User-Agent' =>
                self::USER_AGENT,

            'Accept' =>
                'text/html,application/xhtml+xml,'
                . 'application/xml;q=0.9,'
                . 'image/avif,image/webp,*/*;q=0.8',

            'Accept-Language' =>
                'sk-SK,sk;q=0.9,en;q=0.8',

            'Cache-Control' =>
                'no-cache',

            'Pragma' =>
                'no-cache',
        ])
            ->connectTimeout(20)
            ->timeout(60)
            ->get($url);

        if (!$response->successful()) {
            throw new \RuntimeException(
                'HTTP ' .
                $response->status() .
                ' pri vyhľadávaní "' .
                $term .
                '"'
            );
        }

        $html = $response->body();

        if (trim($html) === '') {
            throw new \RuntimeException(
                'Lidl vrátil prázdne HTML.'
            );
        }

        return $this->parseProductsFromHtml(
            $html,
            $term
        );
    }

    /**
     * -------------------------------------------------------------
     * HTML PARSER
     * -------------------------------------------------------------
     */
    private function parseProductsFromHtml(
    string $html,
    string $searchTerm
): array {
    $products = [];

    // Lidl HTML obsahuje JSON zapísaný ako HTML entity (" atď.).
    $html = html_entity_decode(
        $html,
        ENT_QUOTES | ENT_HTML5,
        'UTF-8'
    );

    $needle = '"productId":';
    $offset = 0;
    $length = strlen($html);

    while (($position = strpos($html, $needle, $offset)) !== false) {
        $offset = $position + strlen($needle);

        // Produktové ID musí byť číslo.
        $idStart = $position + strlen($needle);

        while (
            $idStart < $length &&
            ctype_space($html[$idStart])
        ) {
            $idStart++;
        }

        $idEnd = $idStart;

        while (
            $idEnd < $length &&
            ctype_digit($html[$idEnd])
        ) {
            $idEnd++;
        }

        if ($idEnd === $idStart) {
            continue;
        }

        $productId = substr(
            $html,
            $idStart,
            $idEnd - $idStart
        );

        // Nájdeme začiatok JSON objektu, v ktorom je productId.
        $start = $this->findJsonObjectStart(
            $html,
            $position
        );

        if ($start === null) {
            continue;
        }

        // Nájdeme koniec JSON objektu.
        $json = $this->extractJsonObject(
            $html,
            $start
        );

        if ($json === null) {
            continue;
        }

        $data = json_decode($json, true);

        if (
            !is_array($data) ||
            !isset($data['productId']) ||
            !isset($data['fullTitle'])
        ) {
            continue;
        }

        // Bezpečnostná kontrola.
        if ((string) $data['productId'] !== $productId) {
            continue;
        }

        $product = $this->normalizeProduct(
            $data,
            $searchTerm
        );

        if ($product === null) {
            continue;
        }

        $products[(string) $product['product_id']] = $product;
    }

    return array_values($products);
}

private function findJsonObjectStart(
    string $text,
    int $position
): ?int {
    $depth = 0;
    $inString = false;
    $escaped = false;

    for ($i = $position - 1; $i >= 0; $i--) {
        $char = $text[$i];

        if ($char === '"' && !$escaped) {
            $inString = !$inString;
        }

        if ($inString) {
            $escaped = (
                $char === '\\' &&
                !$escaped
            );

            continue;
        }

        if ($char === '}') {
            $depth++;
        } elseif ($char === '{') {
            if ($depth === 0) {
                return $i;
            }

            $depth--;
        }

        $escaped = false;
    }

    return null;
}

private function extractJsonObject(
    string $text,
    int $start
): ?string {
    $length = strlen($text);
    $depth = 0;
    $inString = false;
    $escaped = false;

    for ($i = $start; $i < $length; $i++) {
        $char = $text[$i];

        if ($inString) {
            if ($char === '"' && !$escaped) {
                $inString = false;
            }

            $escaped = (
                $char === '\\' &&
                !$escaped
            );

            continue;
        }

        if ($char === '"') {
            $inString = true;
            $escaped = false;
            continue;
        }

        if ($char === '{') {
            $depth++;
        } elseif ($char === '}') {
            $depth--;

            if ($depth === 0) {
                return substr(
                    $text,
                    $start,
                    $i - $start + 1
                );
            }
        }
    }

    return null;
}







    /**
     * -------------------------------------------------------------
     * NORMALIZE PRODUCT
     * -------------------------------------------------------------
     */
    private function normalizeProduct(
        array $data,
        string $searchTerm
    ): ?array {
        if (
            !isset($data['productId']) ||
            !isset($data['fullTitle'])
        ) {
            return null;
        }

        $productId =
            (string) $data['productId'];

        if ($productId === '') {
            return null;
        }

        $price = null;
        $currency = null;
        $packaging = null;
        $basePrice = null;
        $discount = null;

        if (
            isset($data['price']) &&
            is_array($data['price'])
        ) {
            $price =
                $data['price']['price']
                ?? null;

            $currency =
                $data['price']['currencyCode']
                ?? null;

            $packaging =
                $data['price']
                    ['packaging']
                    ['text']
                ?? null;

            $basePrice =
                $data['price']
                    ['basePrice']
                    ['text']
                ?? null;
        }

        /*
         * Regionálna cena.
         */
        if (
            isset($data['regionsPrices']) &&
            is_array(
                $data['regionsPrices']
            )
        ) {
            foreach (
                $data['regionsPrices']
                as $region
            ) {
                if (
                    !isset(
                        $region['currentPrice']
                    )
                ) {
                    continue;
                }

                $current =
                    $region['currentPrice'];

                if (
                    isset($current['price'])
                ) {
                    $price =
                        $current['price'];
                }

                if (
                    isset(
                        $current['currencyCode']
                    )
                ) {
                    $currency =
                        $current['currencyCode'];
                }

                if (
                    isset(
                        $current['packaging']['text']
                    )
                ) {
                    $packaging =
                        $current['packaging']['text'];
                }

                if (
                    isset(
                        $current['basePrice']['text']
                    )
                ) {
                    $basePrice =
                        $current['basePrice']['text'];
                }

                if (
                    isset(
                        $current['discount']
                    )
                ) {
                    $discount =
                        $current['discount'];
                }

                break;
            }
        }

        $brand = null;

        if (
            isset($data['brand']['name'])
        ) {
            $brand =
                $data['brand']['name'];
        }

        $canonicalUrl =
            $data['canonicalUrl']
            ?? $data['canonicalPath']
            ?? null;

        if (
            $canonicalUrl !== null &&
            !str_starts_with(
                $canonicalUrl,
                'http'
            )
        ) {
            $canonicalUrl =
                'https://www.lidl.sk/' .
                ltrim(
                    $canonicalUrl,
                    '/'
                );
        }

        return [
            'product_id' =>
                $productId,

            'name' =>
                trim(
                    $data['fullTitle']
                ),

            'brand' =>
                $brand,

            'price' =>
                $price,

            'currency' =>
                $currency,

            'packaging' =>
                $packaging,

            'base_price' =>
                $basePrice,

            'discount' =>
                $discount,

            'url' =>
                $canonicalUrl,

            'image' =>
                $data['image']
                ?? null,

            'nat' =>
                $data['nat']
                ?? null,

            'category' =>
                $data['category']
                ?? null,

            'product_type' =>
                $data['productType']
                ?? null,

            'online' =>
                $data['online']
                ?? null,

            'regions' =>
                $data['regions']
                ?? [],

            'lidl_plus' =>
                $data['lidlPlus']
                ?? [],

            'search_terms' => [
                $searchTerm,
            ],

            'source' =>
                'lidl.sk',
        ];
    }

    /**
     * -------------------------------------------------------------
     * DOM FALLBACK
     * -------------------------------------------------------------
     */
    private function parseProductsFromDom(
        string $html,
        string $searchTerm
    ): array {
        $products = [];

        $dom =
            new \DOMDocument();

        libxml_use_internal_errors(true);

        $dom->loadHTML(
            '<?xml encoding="UTF-8">' .
            $html
        );

        libxml_clear_errors();

        $xpath =
            new \DOMXPath($dom);

        $links =
            $xpath->query(
                '//a[contains(@href, "/p/")]'
            );

        if ($links === false) {
            return [];
        }

        foreach ($links as $link) {
            $href =
                $link->getAttribute('href');

            if (
                !preg_match(
                    '~/p/[^/]+/p(\d+)~',
                    $href,
                    $match
                )
            ) {
                continue;
            }

            $productId =
                $match[1];

            $name =
                trim(
                    preg_replace(
                        '/\s+/',
                        ' ',
                        $link->textContent
                    )
                );

            if ($name === '') {
                continue;
            }

            if (
                !str_starts_with(
                    $href,
                    'http'
                )
            ) {
                $href =
                    'https://www.lidl.sk/' .
                    ltrim(
                        $href,
                        '/'
                    );
            }

            $products[$productId] = [
                'product_id' =>
                    $productId,

                'name' =>
                    $name,

                'brand' =>
                    null,

                'price' =>
                    null,

                'currency' =>
                    'EUR',

                'packaging' =>
                    null,

                'base_price' =>
                    null,

                'discount' =>
                    null,

                'url' =>
                    $href,

                'image' =>
                    null,

                'nat' =>
                    null,

                'category' =>
                    'Food',

                'product_type' =>
                    null,

                'online' =>
                    null,

                'regions' =>
                    [],

                'lidl_plus' =>
                    [],

                'search_terms' => [
                    $searchTerm,
                ],

                'source' =>
                    'lidl.sk',
            ];
        }

        return array_values(
            $products
        );
    }

    /**
     * -------------------------------------------------------------
     * PROGRESS
     * -------------------------------------------------------------
     */
    private function loadProgress(): ?array
    {
        if (
            !Storage::disk('local')->exists(
                self::PROGRESS_FILE
            )
        ) {
            return null;
        }

        try {
            $content =
                Storage::disk('local')->get(
                    self::PROGRESS_FILE
                );

            if (
                trim($content) === ''
            ) {
                return null;
            }

            $data =
                json_decode(
                    $content,
                    true,
                    512,
                    JSON_THROW_ON_ERROR
                );

            return is_array($data)
                ? $data
                : null;
        } catch (Throwable $e) {
            Log::channel('lidl')->warning(
                '[Lidl] Nepodarilo sa načítať ' .
                'checkpoint: ' .
                $e->getMessage()
            );

            return null;
        }
    }

    /**
     * Uloží checkpoint.
     */
    private function saveProgress(
        array $progress
    ): void {
        $content =
            json_encode(
                $progress,
                JSON_PRETTY_PRINT |
                JSON_UNESCAPED_UNICODE |
                JSON_UNESCAPED_SLASHES |
                JSON_THROW_ON_ERROR
            );

        Storage::disk('local')->put(
            self::PROGRESS_FILE,
            $content
        );
    }

    /**
     * Vymaže checkpoint.
     */
    private function deleteProgress(): void
    {
        if (
            Storage::disk('local')->exists(
                self::PROGRESS_FILE
            )
        ) {
            Storage::disk('local')->delete(
                self::PROGRESS_FILE
            );
        }
    }
}


Author: AarNoma

The first Slovak cyborg 1 system