-
Notifications
You must be signed in to change notification settings - Fork 94
E‐commerce Feed
Rumen Damyanov edited this page Jul 29, 2025
·
1 revision
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 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
-- 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)
);
<?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());
}
<?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();
}
}
<?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
}
}
<?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();
}
}
<?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;
}
}
- Product Information: Include complete product details (SKU, price, availability, etc.)
- Image Optimization: Ensure product images are high-quality and properly sized
- Inventory Sync: Keep stock levels synchronized across all feeds
- Price Monitoring: Track price changes and send alerts for significant updates
- Category Organization: Structure products logically with proper categorization
- Search Optimization: Include relevant keywords in product titles and descriptions
- Mobile Compatibility: Ensure feeds work well on mobile devices
- Platform Compliance: Follow specific feed requirements for Google Shopping, Facebook, etc.
- Performance: Cache feeds appropriately and optimize database queries
- Analytics: Track feed performance and conversion rates
- Learn about Feed Links for discovery implementation
- Check Advanced Features for enterprise functionality
- See Caching for performance optimization