Scheduled automatic product feed updates

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Scheduled automatic product feed updates
Simple
from 1 business day to 3 business days
FAQ

Our competencies:

Development stages

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    879
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    453

Setting Up Automatic Product Feed Updates on Schedule

Product feeds — XML or CSV files consumed by Yandex.Market, Google Merchant, Facebook Catalog, partner aggregators. If feed updates manually or once daily via static export — price and inventory actuality is questionable. Automatic schedule solves this systematically.

Feed Formats

Each platform expects different format:

  • Yandex.Market — YML (Yandex Market Language), XML extension
  • Google Merchant — RSS 2.0 with g: namespace or TSV
  • Facebook/Instagram — CSV or XML with specific fields
  • Avito — proprietary XML

Same catalog must export to multiple formats. Architecture must account for this from start.

Generator Structure

// app/Services/Feed/FeedGenerator.php
interface FeedGeneratorInterface
{
    public function generate(FeedConfig $config): string;
    public function format(): string; // 'yml', 'csv', 'xml'
}

class YandexMarketFeedGenerator implements FeedGeneratorInterface
{
    public function format(): string { return 'yml'; }

    public function generate(FeedConfig $config): string
    {
        $products = Product::query()
            ->where('is_active', true)
            ->whereHas('stock', fn($q) => $q->where('quantity', '>', 0))
            ->when($config->category_ids, fn($q, $ids) => $q->whereIn('category_id', $ids))
            ->with(['category', 'images', 'attributes'])
            ->cursor(); // cursor() — don't load all in memory

        $xml = new \XMLWriter();
        $xml->openMemory();
        $xml->setIndent(true);
        $xml->startDocument('1.0', 'UTF-8');
        $xml->startElement('yml_catalog');
        $xml->writeAttribute('date', now()->format('Y-m-d H:i'));

        $xml->startElement('shop');
        $this->writeShopInfo($xml, $config);
        $xml->startElement('offers');

        foreach ($products as $product) {
            $this->writeOffer($xml, $product, $config);
        }

        $xml->endElement(); // offers
        $xml->endElement(); // shop
        $xml->endElement(); // yml_catalog

        return $xml->outputMemory();
    }

    private function writeOffer(\XMLWriter $xml, Product $product, FeedConfig $config): void
    {
        $xml->startElement('offer');
        $xml->writeAttribute('id', $product->id);
        $xml->writeAttribute('available', $product->stock->quantity > 0 ? 'true' : 'false');

        $xml->writeElement('url',          route('product.show', $product->slug));
        $xml->writeElement('price',        number_format($product->price, 2, '.', ''));
        $xml->writeElement('currencyId',   $config->currency ?? 'RUB');
        $xml->writeElement('categoryId',   $product->category_id);
        $xml->writeElement('name',         $product->name);
        $xml->writeElement('description',  strip_tags($product->description));

        foreach ($product->images->take(10) as $image) {
            $xml->writeElement('picture', $image->url);
        }

        $xml->endElement(); // offer
    }
}

Feed Config Model

CREATE TABLE feed_configs (
    id           SERIAL PRIMARY KEY,
    name         VARCHAR(255) NOT NULL,
    type         VARCHAR(32) NOT NULL,   -- 'yandex', 'google', 'facebook'
    schedule     VARCHAR(64) NOT NULL,   -- cron: '*/30 * * * *'
    output_path  VARCHAR(512) NOT NULL,  -- '/public/feeds/yandex.xml'
    is_active    BOOLEAN DEFAULT true,
    last_run_at  TIMESTAMPTZ,
    last_error   TEXT,
    options      JSONB DEFAULT '{}'
);

Artisan Command for Generation

// app/Console/Commands/GenerateFeed.php
class GenerateFeed extends Command
{
    protected $signature   = 'feed:generate {feed_id?} {--all}';
    protected $description = 'Generate product feed files';

    public function handle(): int
    {
        $configs = $this->option('all')
            ? FeedConfig::where('is_active', true)->get()
            : FeedConfig::whereKey($this->argument('feed_id'))->get();

        foreach ($configs as $config) {
            $this->generateOne($config);
        }

        return self::SUCCESS;
    }

    private function generateOne(FeedConfig $config): void
    {
        $start = microtime(true);
        try {
            $generator = FeedGeneratorFactory::make($config->type);
            $content   = $generator->generate($config);

            // Write to tmp, then atomically rename
            $tmp = $config->output_path . '.tmp';
            file_put_contents(public_path($tmp), $content);
            rename(public_path($tmp), public_path($config->output_path));

            $config->update([
                'last_run_at' => now(),
                'last_error'  => null,
            ]);

            $this->info(sprintf(
                '[%s] %s generated in %.2fs (%s)',
                $config->name,
                basename($config->output_path),
                microtime(true) - $start,
                $this->formatBytes(strlen($content))
            ));

        } catch (\Throwable $e) {
            $config->update(['last_error' => $e->getMessage()]);
            $this->error("[{$config->name}] Failed: " . $e->getMessage());
            report($e);
        }
    }
}

Atomic rename is important: if aggregator downloads feed while writing — it gets complete old version, not truncated.

Laravel Scheduler

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    // Read schedule from DB — flexible, no deploy on change
    FeedConfig::where('is_active', true)->each(function (FeedConfig $config) use ($schedule) {
        $schedule->command("feed:generate {$config->id}")
            ->cron($config->schedule)
            ->withoutOverlapping(10)  // don't run if previous still working
            ->runInBackground()
            ->onFailure(function () use ($config) {
                // Notify Slack/Telegram
                Notification::route('slack', config('services.slack.webhook'))
                    ->notify(new FeedGenerationFailed($config));
            });
    });
}

Typical schedules:

  • Prices and inventory: */15 * * * * (every 15 min)
  • Main catalog with descriptions: 0 * * * * (hourly)
  • Full export with images: 0 3 * * * (daily at night)

Freshness Monitoring

Yandex.Market blocks shops with feed older than 24h. Check freshness:

FeedConfig::where('is_active', true)->each(function (FeedConfig $config) {
    $maxAge    = $config->options['max_age_minutes'] ?? 60;
    $isStale   = $config->last_run_at?->diffInMinutes(now()) > $maxAge;

    if ($isStale || $config->last_error) {
        // Alert to monitoring
    }
});

Basic system with two formats (YML + Google) and web config management — 3–4 business days.