Skip to content

E‐commerce Feed

Rumen Damyanov edited this page Jul 29, 2025 · 1 revision

E-commerce Feed Example

This is a comprehensive example of implementing product feeds for e-commerce websites, including product catalogs, price alerts, inventory updates, and marketing feeds.

E-commerce Feed Types

E-commerce sites typically need multiple feed types:

  • Product Catalog Feeds - All products with details, pricing, and availability
  • New Arrivals Feeds - Recently added products
  • Sale/Discount Feeds - Products on sale or with special offers
  • Category Feeds - Products organized by categories
  • Brand Feeds - Products from specific brands
  • Price Alert Feeds - Price changes and notifications
  • Inventory Feeds - Stock level updates
  • Review Feeds - Product reviews and ratings
  • Wishlist Feeds - User wishlist updates

Database Schema

-- Products table
CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    description TEXT,
    short_description VARCHAR(500),
    sku VARCHAR(100) UNIQUE NOT NULL,
    brand_id INT,
    category_id INT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    sale_price DECIMAL(10,2) NULL,
    cost_price DECIMAL(10,2),
    stock_quantity INT DEFAULT 0,
    weight DECIMAL(8,2),
    dimensions VARCHAR(100),
    featured_image VARCHAR(255),
    gallery_images JSON,
    is_active BOOLEAN DEFAULT TRUE,
    is_featured BOOLEAN DEFAULT FALSE,
    is_digital BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_active (is_active, created_at),
    INDEX idx_category (category_id, is_active),
    INDEX idx_brand (brand_id, is_active),
    INDEX idx_featured (is_featured, created_at),
    INDEX idx_price (price, is_active),
    FULLTEXT idx_search (name, description)
);

-- Categories table
CREATE TABLE categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    parent_id INT NULL,
    image VARCHAR(255),
    sort_order INT DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (parent_id) REFERENCES categories(id)
);

-- Brands table
CREATE TABLE brands (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    logo VARCHAR(255),
    website VARCHAR(255),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Product attributes
CREATE TABLE product_attributes (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT NOT NULL,
    attribute_name VARCHAR(100) NOT NULL,
    attribute_value VARCHAR(255) NOT NULL,
    FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
    INDEX idx_product (product_id),
    INDEX idx_attribute (attribute_name, attribute_value)
);

-- Product reviews
CREATE TABLE product_reviews (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT NOT NULL,
    customer_name VARCHAR(100) NOT NULL,
    customer_email VARCHAR(255) NOT NULL,
    rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
    title VARCHAR(255),
    review_text TEXT,
    is_verified BOOLEAN DEFAULT FALSE,
    is_approved BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
    INDEX idx_product_approved (product_id, is_approved),
    INDEX idx_rating (rating),
    INDEX idx_created (created_at)
);

-- Inventory tracking
CREATE TABLE inventory_logs (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT NOT NULL,
    old_quantity INT NOT NULL,
    new_quantity INT NOT NULL,
    change_type ENUM('sale', 'restock', 'adjustment', 'return') NOT NULL,
    notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id),
    INDEX idx_product_date (product_id, created_at)
);

-- Price history
CREATE TABLE price_history (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT NOT NULL,
    old_price DECIMAL(10,2) NOT NULL,
    new_price DECIMAL(10,2) NOT NULL,
    old_sale_price DECIMAL(10,2) NULL,
    new_sale_price DECIMAL(10,2) NULL,
    change_reason VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id),
    INDEX idx_product_date (product_id, created_at)
);

Plain PHP E-commerce Feed Implementation

<?php
require 'vendor/autoload.php';

use Rumenx\Feed\FeedFactory;

class EcommerceFeedManager
{
    private PDO $pdo;
    private string $baseUrl;
    private string $storeName;
    private string $currency;

    public function __construct(PDO $pdo, string $baseUrl, string $storeName, string $currency = 'USD')
    {
        $this->pdo = $pdo;
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->storeName = $storeName;
        $this->currency = $currency;
    }

    public function generateProductCatalogFeed(int $limit = 100): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - Product Catalog');
        $feed->setDescription('Complete product catalog with pricing and availability');
        $feed->setLink($this->baseUrl);

        $products = $this->getAllProducts($limit);

        foreach ($products as $product) {
            $categories = $this->getProductCategories($product['id']);
            $attributes = $this->getProductAttributes($product['id']);
            $reviews = $this->getProductReviewStats($product['id']);

            $item = [
                'title' => $product['name'],
                'link' => $this->baseUrl . '/products/' . $product['slug'],
                'pubdate' => $product['created_at'],
                'description' => $this->buildProductDescription($product, $attributes),
                'category' => $categories,
                'guid' => $this->baseUrl . '/products/' . $product['slug']
            ];

            // Add e-commerce specific elements
            $item['product'] = [
                'sku' => $product['sku'],
                'price' => $product['price'],
                'sale_price' => $product['sale_price'],
                'currency' => $this->currency,
                'brand' => $product['brand_name'],
                'availability' => $product['stock_quantity'] > 0 ? 'in stock' : 'out of stock',
                'condition' => 'new',
                'weight' => $product['weight'],
                'dimensions' => $product['dimensions']
            ];

            if ($reviews['count'] > 0) {
                $item['product']['rating'] = $reviews['average_rating'];
                $item['product']['review_count'] = $reviews['count'];
            }

            if ($product['featured_image']) {
                $item['enclosure'] = [
                    'url' => $this->baseUrl . '/' . $product['featured_image'],
                    'type' => 'image/jpeg',
                    'length' => $this->getFileSize($product['featured_image'])
                ];
            }

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generateNewArrivalsDeeed(int $days = 7, int $limit = 50): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - New Arrivals');
        $feed->setDescription('Recently added products');
        $feed->setLink($this->baseUrl . '/new-arrivals');

        $products = $this->getNewArrivals($days, $limit);

        foreach ($products as $product) {
            $item = [
                'title' => '[NEW] ' . $product['name'],
                'link' => $this->baseUrl . '/products/' . $product['slug'],
                'pubdate' => $product['created_at'],
                'description' => $product['short_description'] ?: $product['description'],
                'category' => [$product['category_name'], 'New Arrivals'],
                'guid' => $this->baseUrl . '/products/' . $product['slug']
            ];

            $item['product'] = [
                'sku' => $product['sku'],
                'price' => $product['price'],
                'currency' => $this->currency,
                'brand' => $product['brand_name'],
                'availability' => 'in stock'
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generateSaleFeed(int $limit = 50): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - Sale Items');
        $feed->setDescription('Products on sale with special pricing');
        $feed->setLink($this->baseUrl . '/sale');

        $products = $this->getSaleProducts($limit);

        foreach ($products as $product) {
            $discount = $this->calculateDiscount($product['price'], $product['sale_price']);

            $item = [
                'title' => '[SALE ' . $discount . '% OFF] ' . $product['name'],
                'link' => $this->baseUrl . '/products/' . $product['slug'],
                'pubdate' => $product['updated_at'],
                'description' => $this->buildSaleDescription($product, $discount),
                'category' => [$product['category_name'], 'Sale'],
                'guid' => $this->baseUrl . '/products/' . $product['slug']
            ];

            $item['product'] = [
                'sku' => $product['sku'],
                'price' => $product['price'],
                'sale_price' => $product['sale_price'],
                'currency' => $this->currency,
                'discount_percentage' => $discount,
                'savings' => $product['price'] - $product['sale_price'],
                'brand' => $product['brand_name'],
                'availability' => $product['stock_quantity'] > 0 ? 'in stock' : 'out of stock'
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generateCategoryFeed(string $categorySlug, int $limit = 50): string
    {
        $category = $this->getCategoryBySlug($categorySlug);
        if (!$category) {
            throw new InvalidArgumentException("Category '{$categorySlug}' not found");
        }

        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - ' . $category['name']);
        $feed->setDescription($category['description'] ?: 'Products in ' . $category['name'] . ' category');
        $feed->setLink($this->baseUrl . '/category/' . $categorySlug);

        $products = $this->getProductsByCategory($category['id'], $limit);

        foreach ($products as $product) {
            $item = [
                'title' => $product['name'],
                'link' => $this->baseUrl . '/products/' . $product['slug'],
                'pubdate' => $product['created_at'],
                'description' => $product['short_description'] ?: $product['description'],
                'category' => [$category['name']],
                'guid' => $this->baseUrl . '/products/' . $product['slug']
            ];

            $item['product'] = [
                'sku' => $product['sku'],
                'price' => $product['price'],
                'sale_price' => $product['sale_price'],
                'currency' => $this->currency,
                'brand' => $product['brand_name'],
                'availability' => $product['stock_quantity'] > 0 ? 'in stock' : 'out of stock'
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generateBrandFeed(string $brandSlug, int $limit = 50): string
    {
        $brand = $this->getBrandBySlug($brandSlug);
        if (!$brand) {
            throw new InvalidArgumentException("Brand '{$brandSlug}' not found");
        }

        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - ' . $brand['name'] . ' Products');
        $feed->setDescription('All products from ' . $brand['name']);
        $feed->setLink($this->baseUrl . '/brand/' . $brandSlug);

        $products = $this->getProductsByBrand($brand['id'], $limit);

        foreach ($products as $product) {
            $item = [
                'title' => $product['name'],
                'link' => $this->baseUrl . '/products/' . $product['slug'],
                'pubdate' => $product['created_at'],
                'description' => $product['short_description'] ?: $product['description'],
                'category' => [$brand['name'], $product['category_name']],
                'guid' => $this->baseUrl . '/products/' . $product['slug']
            ];

            $item['product'] = [
                'sku' => $product['sku'],
                'price' => $product['price'],
                'sale_price' => $product['sale_price'],
                'currency' => $this->currency,
                'brand' => $brand['name'],
                'availability' => $product['stock_quantity'] > 0 ? 'in stock' : 'out of stock'
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generatePriceAlertFeed(int $hours = 24, int $limit = 100): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - Price Updates');
        $feed->setDescription('Recent price changes and alerts');
        $feed->setLink($this->baseUrl . '/price-alerts');

        $priceChanges = $this->getRecentPriceChanges($hours, $limit);

        foreach ($priceChanges as $change) {
            $priceDirection = $change['new_price'] > $change['old_price'] ? 'INCREASED' : 'DECREASED';
            $priceDiff = abs($change['new_price'] - $change['old_price']);

            $item = [
                'title' => "[PRICE {$priceDirection}] " . $change['product_name'],
                'link' => $this->baseUrl . '/products/' . $change['product_slug'],
                'pubdate' => $change['created_at'],
                'description' => $this->buildPriceAlertDescription($change, $priceDirection, $priceDiff),
                'category' => ['Price Alert', $change['category_name']],
                'guid' => $this->baseUrl . '/price-alerts/' . $change['id']
            ];

            $item['price_alert'] = [
                'old_price' => $change['old_price'],
                'new_price' => $change['new_price'],
                'difference' => $priceDiff,
                'direction' => strtolower($priceDirection),
                'currency' => $this->currency,
                'reason' => $change['change_reason']
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generateInventoryFeed(int $hours = 6, int $limit = 100): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - Inventory Updates');
        $feed->setDescription('Recent stock level changes');
        $feed->setLink($this->baseUrl . '/inventory-updates');

        $inventoryChanges = $this->getRecentInventoryChanges($hours, $limit);

        foreach ($inventoryChanges as $change) {
            $stockStatus = $change['new_quantity'] > 0 ? 'IN STOCK' : 'OUT OF STOCK';
            $changeType = strtoupper($change['change_type']);

            $item = [
                'title' => "[{$stockStatus}] " . $change['product_name'],
                'link' => $this->baseUrl . '/products/' . $change['product_slug'],
                'pubdate' => $change['created_at'],
                'description' => $this->buildInventoryDescription($change),
                'category' => ['Inventory Update', $change['category_name']],
                'guid' => $this->baseUrl . '/inventory/' . $change['id']
            ];

            $item['inventory'] = [
                'old_quantity' => $change['old_quantity'],
                'new_quantity' => $change['new_quantity'],
                'change_type' => $change['change_type'],
                'availability' => $change['new_quantity'] > 0 ? 'in stock' : 'out of stock',
                'notes' => $change['notes']
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    public function generateReviewsFeed(int $limit = 50): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->storeName . ' - Customer Reviews');
        $feed->setDescription('Latest customer product reviews');
        $feed->setLink($this->baseUrl . '/reviews');

        $reviews = $this->getLatestReviews($limit);

        foreach ($reviews as $review) {
            $starRating = str_repeat('', $review['rating']) . str_repeat('', 5 - $review['rating']);

            $item = [
                'title' => $starRating . ' ' . ($review['title'] ?: 'Review for ' . $review['product_name']),
                'author' => $review['customer_name'],
                'link' => $this->baseUrl . '/products/' . $review['product_slug'] . '#review-' . $review['id'],
                'pubdate' => $review['created_at'],
                'description' => $review['review_text'],
                'category' => ['Review', $review['category_name']],
                'guid' => $this->baseUrl . '/reviews/' . $review['id']
            ];

            $item['review'] = [
                'rating' => $review['rating'],
                'product_name' => $review['product_name'],
                'verified' => $review['is_verified'],
                'helpful_votes' => 0 // If you track this
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    // Database query methods
    private function getAllProducts(int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.name, p.slug, p.description, p.short_description, p.sku,
                p.price, p.sale_price, p.stock_quantity, p.weight, p.dimensions,
                p.featured_image, p.created_at, p.updated_at,
                b.name as brand_name, c.name as category_name
            FROM products p
            LEFT JOIN brands b ON p.brand_id = b.id
            JOIN categories c ON p.category_id = c.id
            WHERE p.is_active = 1
            ORDER BY p.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getNewArrivals(int $days, int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.name, p.slug, p.description, p.short_description, p.sku,
                p.price, p.sale_price, p.created_at,
                b.name as brand_name, c.name as category_name
            FROM products p
            LEFT JOIN brands b ON p.brand_id = b.id
            JOIN categories c ON p.category_id = c.id
            WHERE p.is_active = 1 
            AND p.created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)
            ORDER BY p.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$days, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getSaleProducts(int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.name, p.slug, p.description, p.short_description, p.sku,
                p.price, p.sale_price, p.stock_quantity, p.updated_at,
                b.name as brand_name, c.name as category_name
            FROM products p
            LEFT JOIN brands b ON p.brand_id = b.id
            JOIN categories c ON p.category_id = c.id
            WHERE p.is_active = 1 AND p.sale_price IS NOT NULL AND p.sale_price < p.price
            ORDER BY ((p.price - p.sale_price) / p.price) DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getProductsByCategory(int $categoryId, int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.name, p.slug, p.description, p.short_description, p.sku,
                p.price, p.sale_price, p.stock_quantity, p.created_at,
                b.name as brand_name
            FROM products p
            LEFT JOIN brands b ON p.brand_id = b.id
            WHERE p.is_active = 1 AND p.category_id = ?
            ORDER BY p.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$categoryId, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getProductsByBrand(int $brandId, int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.name, p.slug, p.description, p.short_description, p.sku,
                p.price, p.sale_price, p.stock_quantity, p.created_at,
                c.name as category_name
            FROM products p
            JOIN categories c ON p.category_id = c.id
            WHERE p.is_active = 1 AND p.brand_id = ?
            ORDER BY p.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$brandId, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getRecentPriceChanges(int $hours, int $limit): array
    {
        $sql = "
            SELECT 
                ph.id, ph.old_price, ph.new_price, ph.change_reason, ph.created_at,
                p.name as product_name, p.slug as product_slug,
                c.name as category_name
            FROM price_history ph
            JOIN products p ON ph.product_id = p.id
            JOIN categories c ON p.category_id = c.id
            WHERE ph.created_at >= DATE_SUB(NOW(), INTERVAL ? HOUR)
            ORDER BY ph.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$hours, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getRecentInventoryChanges(int $hours, int $limit): array
    {
        $sql = "
            SELECT 
                il.id, il.old_quantity, il.new_quantity, il.change_type, il.notes, il.created_at,
                p.name as product_name, p.slug as product_slug,
                c.name as category_name
            FROM inventory_logs il
            JOIN products p ON il.product_id = p.id
            JOIN categories c ON p.category_id = c.id
            WHERE il.created_at >= DATE_SUB(NOW(), INTERVAL ? HOUR)
            ORDER BY il.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$hours, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getLatestReviews(int $limit): array
    {
        $sql = "
            SELECT 
                pr.id, pr.rating, pr.title, pr.review_text, pr.customer_name,
                pr.is_verified, pr.created_at,
                p.name as product_name, p.slug as product_slug,
                c.name as category_name
            FROM product_reviews pr
            JOIN products p ON pr.product_id = p.id
            JOIN categories c ON p.category_id = c.id
            WHERE pr.is_approved = 1
            ORDER BY pr.created_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getProductCategories(int $productId): array
    {
        // For this example, we're using a single category per product
        // In a more complex setup, you might have multiple categories
        return [];
    }

    private function getProductAttributes(int $productId): array
    {
        $stmt = $this->pdo->prepare("
            SELECT attribute_name, attribute_value 
            FROM product_attributes 
            WHERE product_id = ?
        ");
        $stmt->execute([$productId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getProductReviewStats(int $productId): array
    {
        $stmt = $this->pdo->prepare("
            SELECT 
                COUNT(*) as count,
                AVG(rating) as average_rating
            FROM product_reviews 
            WHERE product_id = ? AND is_approved = 1
        ");
        $stmt->execute([$productId]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        return [
            'count' => (int)$result['count'],
            'average_rating' => round((float)$result['average_rating'], 1)
        ];
    }

    private function getCategoryBySlug(string $slug): ?array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM categories WHERE slug = ? AND is_active = 1");
        $stmt->execute([$slug]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function getBrandBySlug(string $slug): ?array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM brands WHERE slug = ? AND is_active = 1");
        $stmt->execute([$slug]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    // Helper methods
    private function buildProductDescription(array $product, array $attributes): string
    {
        $description = $product['short_description'] ?: substr(strip_tags($product['description']), 0, 200) . '...';
        
        $description .= "\n\nProduct Details:";
        $description .= "\n• SKU: " . $product['sku'];
        $description .= "\n• Price: " . $this->currency . ' ' . number_format($product['price'], 2);
        
        if ($product['sale_price']) {
            $description .= "\n• Sale Price: " . $this->currency . ' ' . number_format($product['sale_price'], 2);
        }
        
        $description .= "\n• Availability: " . ($product['stock_quantity'] > 0 ? 'In Stock' : 'Out of Stock');
        
        if (!empty($attributes)) {
            $description .= "\n\nAttributes:";
            foreach ($attributes as $attr) {
                $description .= "\n" . $attr['attribute_name'] . ': ' . $attr['attribute_value'];
            }
        }
        
        return $description;
    }

    private function buildSaleDescription(array $product, int $discount): string
    {
        $savings = $product['price'] - $product['sale_price'];
        
        return sprintf(
            "🏷️ SALE: Save %s%% (%s%s) on %s\n\nOriginal Price: %s%s\nSale Price: %s%s\n\n%s",
            $discount,
            $this->currency,
            number_format($savings, 2),
            $product['name'],
            $this->currency,
            number_format($product['price'], 2),
            $this->currency,
            number_format($product['sale_price'], 2),
            $product['short_description'] ?: $product['description']
        );
    }

    private function buildPriceAlertDescription(array $change, string $direction, float $diff): string
    {
        return sprintf(
            "💰 Price %s Alert for %s\n\nOld Price: %s%s\nNew Price: %s%s\nDifference: %s%s\n\nReason: %s",
            $direction,
            $change['product_name'],
            $this->currency,
            number_format($change['old_price'], 2),
            $this->currency,
            number_format($change['new_price'], 2),
            $this->currency,
            number_format($diff, 2),
            $change['change_reason'] ?: 'Price adjustment'
        );
    }

    private function buildInventoryDescription(array $change): string
    {
        $emoji = match($change['change_type']) {
            'sale' => '🛒',
            'restock' => '📦',
            'adjustment' => '⚖️',
            'return' => '↩️',
            default => '📊'
        };

        return sprintf(
            "%s Inventory Update for %s\n\nPrevious Stock: %d\nCurrent Stock: %d\nChange Type: %s\n\n%s",
            $emoji,
            $change['product_name'],
            $change['old_quantity'],
            $change['new_quantity'],
            ucwords($change['change_type']),
            $change['notes'] ?: 'Inventory level updated'
        );
    }

    private function calculateDiscount(float $originalPrice, float $salePrice): int
    {
        return round((($originalPrice - $salePrice) / $originalPrice) * 100);
    }

    private function getFileSize(string $filePath): int
    {
        $fullPath = $_SERVER['DOCUMENT_ROOT'] . '/' . ltrim($filePath, '/');
        return file_exists($fullPath) ? filesize($fullPath) : 0;
    }
}

// Router implementation
try {
    $pdo = new PDO('mysql:host=localhost;dbname=ecommerce', $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $ecommerceFeed = new EcommerceFeedManager($pdo, 'https://mystore.com', 'My Store', 'USD');

    $path = $_SERVER['REQUEST_URI'] ?? '/';
    
    header('Content-Type: application/rss+xml; charset=utf-8');
    header('Cache-Control: public, max-age=1800'); // 30 minutes cache

    if ($path === '/feed/products.xml') {
        echo $ecommerceFeed->generateProductCatalogFeed();
    } elseif ($path === '/feed/new-arrivals.xml') {
        echo $ecommerceFeed->generateNewArrivalsFeed();
    } elseif ($path === '/feed/sale.xml') {
        echo $ecommerceFeed->generateSaleFeed();
    } elseif (preg_match('/^\/feed\/category\/([^\/]+)\.xml$/', $path, $matches)) {
        echo $ecommerceFeed->generateCategoryFeed($matches[1]);
    } elseif (preg_match('/^\/feed\/brand\/([^\/]+)\.xml$/', $path, $matches)) {
        echo $ecommerceFeed->generateBrandFeed($matches[1]);
    } elseif ($path === '/feed/price-alerts.xml') {
        echo $ecommerceFeed->generatePriceAlertFeed();
    } elseif ($path === '/feed/inventory.xml') {
        echo $ecommerceFeed->generateInventoryFeed();
    } elseif ($path === '/feed/reviews.xml') {
        echo $ecommerceFeed->generateReviewsFeed();
    } else {
        http_response_code(404);
        echo '<?xml version="1.0"?><error>Feed not found</error>';
    }

} catch (Exception $e) {
    http_response_code(500);
    echo '<?xml version="1.0"?><error>Feed generation failed</error>';
    error_log('E-commerce feed error: ' . $e->getMessage());
}

Laravel E-commerce Feed Implementation

Eloquent Models

<?php
// app/Models/Product.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Product extends Model
{
    protected $fillable = [
        'name', 'slug', 'description', 'short_description', 'sku',
        'brand_id', 'category_id', 'price', 'sale_price', 'cost_price',
        'stock_quantity', 'weight', 'dimensions', 'featured_image',
        'is_active', 'is_featured', 'is_digital'
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'sale_price' => 'decimal:2',
        'cost_price' => 'decimal:2',
        'weight' => 'decimal:2',
        'stock_quantity' => 'integer',
        'is_active' => 'boolean',
        'is_featured' => 'boolean',
        'is_digital' => 'boolean',
        'gallery_images' => 'array'
    ];

    public function brand(): BelongsTo
    {
        return $this->belongsTo(Brand::class);
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function reviews(): HasMany
    {
        return $this->hasMany(ProductReview::class);
    }

    public function attributes(): HasMany
    {
        return $this->hasMany(ProductAttribute::class);
    }

    public function priceHistory(): HasMany
    {
        return $this->hasMany(PriceHistory::class);
    }

    public function inventoryLogs(): HasMany
    {
        return $this->hasMany(InventoryLog::class);
    }

    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeInStock($query)
    {
        return $query->where('stock_quantity', '>', 0);
    }

    public function scopeOnSale($query)
    {
        return $query->whereNotNull('sale_price')
                    ->whereColumn('sale_price', '<', 'price');
    }

    public function scopeFeatured($query)
    {
        return $query->where('is_featured', true);
    }

    public function getUrlAttribute(): string
    {
        return route('products.show', $this->slug);
    }

    public function getIsOnSaleAttribute(): bool
    {
        return $this->sale_price && $this->sale_price < $this->price;
    }

    public function getDiscountPercentageAttribute(): int
    {
        if (!$this->is_on_sale) {
            return 0;
        }

        return round((($this->price - $this->sale_price) / $this->price) * 100);
    }

    public function getEffectivePriceAttribute(): float
    {
        return $this->sale_price ?: $this->price;
    }

    public function getIsInStockAttribute(): bool
    {
        return $this->stock_quantity > 0;
    }

    public function getAverageRatingAttribute(): float
    {
        return $this->reviews()
            ->where('is_approved', true)
            ->avg('rating') ?: 0;
    }

    public function getReviewCountAttribute(): int
    {
        return $this->reviews()
            ->where('is_approved', true)
            ->count();
    }
}

Laravel E-commerce Feed Controller

<?php
// app/Http/Controllers/EcommerceFeedController.php

namespace App\Http\Controllers;

use App\Models\Product;
use App\Models\Category;
use App\Models\Brand;
use App\Models\PriceHistory;
use App\Models\InventoryLog;
use App\Models\ProductReview;
use Illuminate\Http\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Rumenx\Feed\FeedFactory;

class EcommerceFeedController extends Controller
{
    public function products(): Response
    {
        $feedXml = Cache::remember('ecommerce:feed:products', now()->addMinutes(30), function () {
            return $this->generateProductCatalogFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function newArrivals(): Response
    {
        $feedXml = Cache::remember('ecommerce:feed:new-arrivals', now()->addMinutes(15), function () {
            return $this->generateNewArrivalsFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function sale(): Response
    {
        $feedXml = Cache::remember('ecommerce:feed:sale', now()->addMinutes(10), function () {
            return $this->generateSaleFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function category(Category $category): Response
    {
        $feedXml = Cache::remember("ecommerce:feed:category:{$category->slug}", now()->addMinutes(30), function () use ($category) {
            return $this->generateCategoryFeed($category);
        });

        return $this->responseWithXml($feedXml);
    }

    public function brand(Brand $brand): Response
    {
        $feedXml = Cache::remember("ecommerce:feed:brand:{$brand->slug}", now()->addMinutes(30), function () use ($brand) {
            return $this->generateBrandFeed($brand);
        });

        return $this->responseWithXml($feedXml);
    }

    public function priceAlerts(): Response
    {
        $feedXml = Cache::remember('ecommerce:feed:price-alerts', now()->addMinutes(5), function () {
            return $this->generatePriceAlertFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function inventory(): Response
    {
        $feedXml = Cache::remember('ecommerce:feed:inventory', now()->addMinutes(5), function () {
            return $this->generateInventoryFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function reviews(): Response
    {
        $feedXml = Cache::remember('ecommerce:feed:reviews', now()->addMinutes(15), function () {
            return $this->generateReviewsFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    private function generateProductCatalogFeed(): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - Product Catalog');
        $feed->setDescription('Complete product catalog with pricing and availability');
        $feed->setLink(url('/'));

        $products = Product::with(['brand', 'category', 'reviews'])
            ->active()
            ->latest()
            ->limit(100)
            ->get();

        foreach ($products as $product) {
            $item = [
                'title' => $product->name,
                'link' => $product->url,
                'pubdate' => $product->created_at,
                'description' => $this->buildProductDescription($product),
                'category' => [$product->category->name],
                'guid' => $product->url
            ];

            $item['product'] = [
                'sku' => $product->sku,
                'price' => $product->price,
                'sale_price' => $product->sale_price,
                'currency' => config('app.currency', 'USD'),
                'brand' => $product->brand?->name,
                'availability' => $product->is_in_stock ? 'in stock' : 'out of stock',
                'condition' => 'new',
                'weight' => $product->weight,
                'dimensions' => $product->dimensions
            ];

            if ($product->review_count > 0) {
                $item['product']['rating'] = $product->average_rating;
                $item['product']['review_count'] = $product->review_count;
            }

            if ($product->featured_image) {
                $item['enclosure'] = [
                    'url' => asset($product->featured_image),
                    'type' => 'image/jpeg',
                    'length' => 0
                ];
            }

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    private function generateNewArrivalsFeed(): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - New Arrivals');
        $feed->setDescription('Recently added products');
        $feed->setLink(url('/new-arrivals'));

        $products = Product::with(['brand', 'category'])
            ->active()
            ->where('created_at', '>=', now()->subDays(7))
            ->latest()
            ->limit(50)
            ->get();

        foreach ($products as $product) {
            $feed->addItem([
                'title' => '[NEW] ' . $product->name,
                'link' => $product->url,
                'pubdate' => $product->created_at,
                'description' => $product->short_description ?: Str::limit($product->description, 200),
                'category' => [$product->category->name, 'New Arrivals'],
                'guid' => $product->url,
                'product' => [
                    'sku' => $product->sku,
                    'price' => $product->price,
                    'currency' => config('app.currency', 'USD'),
                    'brand' => $product->brand?->name,
                    'availability' => 'in stock'
                ]
            ]);
        }

        return $feed->render('rss');
    }

    private function generateSaleFeed(): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - Sale Items');
        $feed->setDescription('Products on sale with special pricing');
        $feed->setLink(url('/sale'));

        $products = Product::with(['brand', 'category'])
            ->active()
            ->onSale()
            ->orderByRaw('((price - sale_price) / price) DESC')
            ->limit(50)
            ->get();

        foreach ($products as $product) {
            $item = [
                'title' => "[SALE {$product->discount_percentage}% OFF] " . $product->name,
                'link' => $product->url,
                'pubdate' => $product->updated_at,
                'description' => $this->buildSaleDescription($product),
                'category' => [$product->category->name, 'Sale'],
                'guid' => $product->url
            ];

            $item['product'] = [
                'sku' => $product->sku,
                'price' => $product->price,
                'sale_price' => $product->sale_price,
                'currency' => config('app.currency', 'USD'),
                'discount_percentage' => $product->discount_percentage,
                'savings' => $product->price - $product->sale_price,
                'brand' => $product->brand?->name,
                'availability' => $product->is_in_stock ? 'in stock' : 'out of stock'
            ];

            $feed->addItem($item);
        }

        return $feed->render('rss');
    }

    private function generateCategoryFeed(Category $category): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - ' . $category->name);
        $feed->setDescription($category->description ?: 'Products in ' . $category->name . ' category');
        $feed->setLink(route('categories.show', $category->slug));

        $products = $category->products()
            ->with(['brand'])
            ->active()
            ->latest()
            ->limit(50)
            ->get();

        foreach ($products as $product) {
            $feed->addItem([
                'title' => $product->name,
                'link' => $product->url,
                'pubdate' => $product->created_at,
                'description' => $product->short_description ?: Str::limit($product->description, 200),
                'category' => [$category->name],
                'guid' => $product->url,
                'product' => [
                    'sku' => $product->sku,
                    'price' => $product->price,
                    'sale_price' => $product->sale_price,
                    'currency' => config('app.currency', 'USD'),
                    'brand' => $product->brand?->name,
                    'availability' => $product->is_in_stock ? 'in stock' : 'out of stock'
                ]
            ]);
        }

        return $feed->render('rss');
    }

    private function buildProductDescription(Product $product): string
    {
        $description = $product->short_description ?: Str::limit(strip_tags($product->description), 200);
        
        $description .= "\n\nProduct Details:";
        $description .= "\n• SKU: " . $product->sku;
        $description .= "\n• Price: " . config('app.currency', 'USD') . ' ' . number_format($product->price, 2);
        
        if ($product->sale_price) {
            $description .= "\n• Sale Price: " . config('app.currency', 'USD') . ' ' . number_format($product->sale_price, 2);
        }
        
        $description .= "\n• Availability: " . ($product->is_in_stock ? 'In Stock' : 'Out of Stock');
        
        if ($product->brand) {
            $description .= "\n• Brand: " . $product->brand->name;
        }
        
        return $description;
    }

    private function buildSaleDescription(Product $product): string
    {
        $savings = $product->price - $product->sale_price;
        
        return sprintf(
            "🏷️ SALE: Save %d%% (%s%s) on %s\n\nOriginal Price: %s%s\nSale Price: %s%s\n\n%s",
            $product->discount_percentage,
            config('app.currency', 'USD'),
            number_format($savings, 2),
            $product->name,
            config('app.currency', 'USD'),
            number_format($product->price, 2),
            config('app.currency', 'USD'),
            number_format($product->sale_price, 2),
            $product->short_description ?: Str::limit($product->description, 200)
        );
    }

    private function responseWithXml(string $xml): Response
    {
        return response($xml)
            ->header('Content-Type', 'application/rss+xml; charset=utf-8')
            ->header('Cache-Control', 'public, max-age=1800'); // 30 minutes
    }
}

Google Shopping Feed

<?php

class GoogleShoppingFeedGenerator
{
    private EcommerceFeedManager $feedManager;

    public function __construct(EcommerceFeedManager $feedManager)
    {
        $this->feedManager = $feedManager;
    }

    public function generateGoogleShoppingFeed(): string
    {
        $products = $this->feedManager->getAllProducts(1000);
        
        $xml = new DOMDocument('1.0', 'UTF-8');
        $xml->formatOutput = true;

        $rss = $xml->createElement('rss');
        $rss->setAttribute('version', '2.0');
        $rss->setAttribute('xmlns:g', 'http://base.google.com/ns/1.0');
        $xml->appendChild($rss);

        $channel = $xml->createElement('channel');
        $rss->appendChild($channel);

        $channel->appendChild($xml->createElement('title', $this->feedManager->getStoreName() . ' Products'));
        $channel->appendChild($xml->createElement('description', 'Product feed for Google Shopping'));
        $channel->appendChild($xml->createElement('link', $this->feedManager->getBaseUrl()));

        foreach ($products as $product) {
            $item = $xml->createElement('item');
            
            $item->appendChild($xml->createElement('g:id', $product['sku']));
            $item->appendChild($xml->createElement('g:title', htmlspecialchars($product['name'])));
            $item->appendChild($xml->createElement('g:description', htmlspecialchars($product['description'])));
            $item->appendChild($xml->createElement('g:link', $this->feedManager->getBaseUrl() . '/products/' . $product['slug']));
            $item->appendChild($xml->createElement('g:image_link', $this->feedManager->getBaseUrl() . '/' . $product['featured_image']));
            $item->appendChild($xml->createElement('g:availability', $product['stock_quantity'] > 0 ? 'in stock' : 'out of stock'));
            $item->appendChild($xml->createElement('g:price', $product['price'] . ' ' . $this->feedManager->getCurrency()));
            
            if ($product['sale_price']) {
                $item->appendChild($xml->createElement('g:sale_price', $product['sale_price'] . ' ' . $this->feedManager->getCurrency()));
            }
            
            $item->appendChild($xml->createElement('g:brand', $product['brand_name']));
            $item->appendChild($xml->createElement('g:condition', 'new'));
            $item->appendChild($xml->createElement('g:product_type', $product['category_name']));

            if ($product['weight']) {
                $item->appendChild($xml->createElement('g:shipping_weight', $product['weight'] . ' kg'));
            }

            $channel->appendChild($item);
        }

        return $xml->saveXML();
    }
}

Facebook Catalog Feed

<?php

class FacebookCatalogFeedGenerator
{
    public function generateFacebookCatalogFeed(array $products): string
    {
        $csv = "id,title,description,availability,condition,price,link,image_link,brand,product_type\n";
        
        foreach ($products as $product) {
            $csv .= sprintf(
                "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s %s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
                $product['sku'],
                str_replace('"', '""', $product['name']),
                str_replace('"', '""', strip_tags($product['description'])),
                $product['stock_quantity'] > 0 ? 'in stock' : 'out of stock',
                'new',
                $product['sale_price'] ?: $product['price'],
                $this->currency,
                $this->baseUrl . '/products/' . $product['slug'],
                $this->baseUrl . '/' . $product['featured_image'],
                $product['brand_name'],
                $product['category_name']
            );
        }
        
        return $csv;
    }
}

Best Practices for E-commerce Feeds

  1. Product Information: Include complete product details (SKU, price, availability, etc.)
  2. Image Optimization: Ensure product images are high-quality and properly sized
  3. Inventory Sync: Keep stock levels synchronized across all feeds
  4. Price Monitoring: Track price changes and send alerts for significant updates
  5. Category Organization: Structure products logically with proper categorization
  6. Search Optimization: Include relevant keywords in product titles and descriptions
  7. Mobile Compatibility: Ensure feeds work well on mobile devices
  8. Platform Compliance: Follow specific feed requirements for Google Shopping, Facebook, etc.
  9. Performance: Cache feeds appropriately and optimize database queries
  10. Analytics: Track feed performance and conversion rates

Next Steps

Clone this wiki locally