Merchant Feed Logging
August 7, 2025 Β· AI Conversations
);
}
}
apply proper error handlign and save log in
$db->executeQuery("CREATE TABLE IF NOT EXISTS shoppingfeedlog (
id INT AUTO_INCREMENT PRIMARY KEY,
feed_id INT NOT NULL,
updated_at DATETIME NOT NULL,
status VARCHAR(50),
error_message TEXT,
INDEX idx_feed (feed_id),
INDEX idx_status (status),
INDEX idx_updated_at (updated_at),
FOREIGN KEY (feed_id) REFERENCES shoppingfeedconfiguration(id) ON DELETE CASCADE
)");
—
### β Full Controller with Logging and Error Handling:
<?php
namespace ConcretePackageShoppingFeedManagerController;
use ConcreteCoreControllerAbstractController;
use ConcreteCoreDatabaseConnectionConnection;
use SymfonyComponentHttpFoundationResponse;
use ConcretePackageCommunityStoreSrcCommunityStoreProductProduct;
use ConcreteCoreSupportFacadeUrl;
use ConcreteCorePagePage;
use Exception;
use DateTime;
class MerchantFeed extends AbstractController
{
protected function logFeedStatus($db, $feedId, $status, $message = null)
{
$now = (new DateTime())->format('Y-m-d H:i:s');
// Ensure log table exists
$db->executeQuery("CREATE TABLE IF NOT EXISTS shoppingfeedlog (
id INT AUTO_INCREMENT PRIMARY KEY,
feed_id INT NOT NULL,
updated_at DATETIME NOT NULL,
status VARCHAR(50),
error_message TEXT,
INDEX idx_feed (feed_id),
INDEX idx_status (status),
INDEX idx_updated_at (updated_at),
FOREIGN KEY (feed_id) REFERENCES shoppingfeedconfiguration(id) ON DELETE CASCADE
)");
// Insert log entry
$db->insert('shoppingfeedlog', [
'feed_id' => $feedId,
'updated_at' => $now,
'status' => $status,
'error_message' => $message
]);
}
public function generateXml($feedSlug = null)
{
$baseUrl = Url::to('/');
$db = Core::make(Connection::class);
try {
if (!$feedSlug) {
throw new Exception("Feed slug is required.");
}
$config = $db->fetchAssociative("SELECT * FROM shoppingfeedconfiguration WHERE feedSlug = ?", [$feedSlug]);
if (!$config) {
throw new Exception("Feed not found.");
}
$settings = unserialize($config['settings']);
$baseSelect = "SELECT p.*";
$baseFrom = "FROM CommunityStoreProducts p";
$joins = [];
$conditions = ["1=1"];
$params = [];
// Price range
if (!empty($settings['price_range']['min']) && !empty($settings['price_range']['max'])) {
$conditions[] = "p.pPrice BETWEEN ? AND ?";
$params[] = (float)$settings['price_range']['min'];
$params[] = (float)$settings['price_range']['max'];
}
if (!empty($settings['product_groups'])) {
$groupIds = array_map('intval', $settings['product_groups']);
$joins[] = "INNER JOIN CommunityStoreProductGroups pg ON pg.pID = p.pID";
$conditions[] = "pg.gID IN (" . implode(',', $groupIds) . ")";
}
if (!empty($settings['product_types'])) {
$productTypes = array_map('intval', $settings['product_types']);
$conditions[] = "p.pType IN (" . implode(',', $productTypes) . ")";
}
if (!empty($settings['pShippable'])) {
$conditions[] = "p.pShippable = '" . $settings['pShippable'] . "'";
}
if (!empty($settings['pTaxable'])) {
$conditions[] = "p.pTaxable = '" . $settings['pTaxable'] . "'";
}
if (!empty($settings['pFeatured'])) {
$conditions[] = "p.pFeatured = '" . $settings['pFeatured'] . "'";
}
$sql = "
$baseSelect
$baseFrom
" . implode(" ", $joins) . "
WHERE " . implode(" AND ", $conditions) . "
GROUP BY p.pID
ORDER BY p.pID DESC
";
$products = $db->fetchAll($sql, $params);
// Build XML
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"/>');
$xml->addAttribute('xmlns:g', 'http://base.google.com/ns/1.0');
$channel = $xml->addChild('channel');
$channel->addChild('title', 'Your Store Feed');
$channel->addChild('link', $baseUrl);
$channel->addChild('description', 'Product feed for Google Merchant Center');
foreach ($products as $product) {
$productu = Product::getByID($product['pID']);
$page = Page::getByID($productu->getPageID());
if (!$page || $page->isError()) {
continue;
}
$url = $page->getCollectionLink();
$imageUrl = '';
$image = $productu->getImageObj();
if (is_object($image)) {
$imageUrl = $image->getURL();
}
$brand = '';
if (is_numeric($product['pManufacturer'])) {
$manufacturer = $db->fetchAssociative("SELECT * FROM CommunityStoreManufacturer WHERE mID = ?", [$product['pManufacturer']]);
if (is_array($manufacturer)) {
$brand = $manufacturer['mName'];
}
}
$item = $channel->addChild('item');
$item->addChild('g:id', $product['pID'], 'http://base.google.com/ns/1.0');
$item->addChild('title', htmlspecialchars($product['pName']));
$item->addChild('description', htmlspecialchars($product['pDesc']));
$item->addChild('link', htmlspecialchars($url));
$item->addChild('g:image_link', htmlspecialchars($imageUrl), 'http://base.google.com/ns/1.0');
$item->addChild('g:availability', 'in stock', 'http://base.google.com/ns/1.0');
$item->addChild('g:price', $product['pPrice'] . ' USD', 'http://base.google.com/ns/1.0');
$item->addChild('g:brand', htmlspecialchars($brand), 'http://base.google.com/ns/1.0');
$item->addChild('g:condition', 'new', 'http://base.google.com/ns/1.0');
}
$xmlContent = $xml->asXML();
$this->logFeedStatus($db, $config['id'], 'success');
return new Response(
$xmlContent,
200,
['Content-Type' => 'application/xml']
);
} catch (Exception $e) {
// Log error
if (isset($db) && isset($config['id'])) {
$this->logFeedStatus($db, $config['id'], 'error', $e->getMessage());
}
return new Response("Error generating feed: " . $e->getMessage(), 500);
}
}
}
—
### π Summary of Enhancements:
– β
**Error handling via `try-catch`**
– β
**Database logging for both success and failure**
– β
**Automatic creation of `shoppingfeedlog` table**
– β
**Keeps controller code clean and manageable**
Let me know if you'd like to also add an interface in the Dashboard to view logs, or automatically clear old logs after X days.
id, feed_id, updated_at, status, error_message
—
### β Updated `MerchantFeed` Controller (Only Log, No Table Creation)
<?php
namespace ConcretePackageShoppingFeedManagerController;
use ConcreteCoreControllerAbstractController;
use ConcreteCoreDatabaseConnectionConnection;
use SymfonyComponentHttpFoundationResponse;
use ConcretePackageCommunityStoreSrcCommunityStoreProductProduct;
use ConcreteCoreSupportFacadeUrl;
use ConcreteCorePagePage;
use Exception;
use DateTime;
class MerchantFeed extends AbstractController
{
protected function logFeedStatus($db, $feedId, $status, $message = null)
{
try {
$now = (new DateTime())->format('Y-m-d H:i:s');
$db->insert('shoppingfeedlog', [
'feed_id' => $feedId,
'updated_at' => $now,
'status' => $status,
'error_message' => $message
]);
} catch (Exception $logException) {
// Optional: Log error to file or skip silently
// error_log("Feed log error: " . $logException->getMessage());
}
}
public function generateXml($feedSlug = null)
{
$baseUrl = Url::to('/');
$db = Core::make(Connection::class);
try {
if (!$feedSlug) {
throw new Exception("Feed slug is required.");
}
$config = $db->fetchAssociative("SELECT * FROM shoppingfeedconfiguration WHERE feedSlug = ?", [$feedSlug]);
if (!$config) {
throw new Exception("Feed not found.");
}
$settings = unserialize($config['settings']);
$baseSelect = "SELECT p.*";
$baseFrom = "FROM CommunityStoreProducts p";
$joins = [];
$conditions = ["1=1"];
$params = [];
if (!empty($settings['price_range']['min']) && !empty($settings['price_range']['max'])) {
$conditions[] = "p.pPrice BETWEEN ? AND ?";
$params[] = (float)$settings['price_range']['min'];
$params[] = (float)$settings['price_range']['max'];
}
if (!empty($settings['product_groups'])) {
$groupIds = array_map('intval', $settings['product_groups']);
$joins[] = "INNER JOIN CommunityStoreProductGroups pg ON pg.pID = p.pID";
$conditions[] = "pg.gID IN (" . implode(',', $groupIds) . ")";
}
if (!empty($settings['product_types'])) {
$productTypes = array_map('intval', $settings['product_types']);
$conditions[] = "p.pType IN (" . implode(',', $productTypes) . ")";
}
if (!empty($settings['pShippable'])) {
$conditions[] = "p.pShippable = '" . $settings['pShippable'] . "'";
}
if (!empty($settings['pTaxable'])) {
$conditions[] = "p.pTaxable = '" . $settings['pTaxable'] . "'";
}
if (!empty($settings['pFeatured'])) {
$conditions[] = "p.pFeatured = '" . $settings['pFeatured'] . "'";
}
$sql = "
$baseSelect
$baseFrom
" . implode(" ", $joins) . "
WHERE " . implode(" AND ", $conditions) . "
GROUP BY p.pID
ORDER BY p.pID DESC
";
$products = $db->fetchAll($sql, $params);
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"/>');
$xml->addAttribute('xmlns:g', 'http://base.google.com/ns/1.0');
$channel = $xml->addChild('channel');
$channel->addChild('title', 'Your Store Feed');
$channel->addChild('link', $baseUrl);
$channel->addChild('description', 'Product feed for Google Merchant Center');
foreach ($products as $product) {
$productu = Product::getByID($product['pID']);
$page = Page::getByID($productu->getPageID());
if (!$page || $page->isError()) {
continue;
}
$url = $page->getCollectionLink();
$imageUrl = '';
$image = $productu->getImageObj();
if (is_object($image)) {
$imageUrl = $image->getURL();
}
$brand = '';
if (is_numeric($product['pManufacturer'])) {
$manufacturer = $db->fetchAssociative("SELECT * FROM CommunityStoreManufacturer WHERE mID = ?", [$product['pManufacturer']]);
if (is_array($manufacturer)) {
$brand = $manufacturer['mName'];
}
}
$item = $channel->addChild('item');
$item->addChild('g:id', $product['pID'], 'http://base.google.com/ns/1.0');
$item->addChild('title', htmlspecialchars($product['pName']));
$item->addChild('description', htmlspecialchars($product['pDesc']));
$item->addChild('link', htmlspecialchars($url));
$item->addChild('g:image_link', htmlspecialchars($imageUrl), 'http://base.google.com/ns/1.0');
$item->addChild('g:availability', 'in stock', 'http://base.google.com/ns/1.0');
$item->addChild('g:price', $product['pPrice'] . ' USD', 'http://base.google.com/ns/1.0');
$item->addChild('g:brand', htmlspecialchars($brand), 'http://base.google.com/ns/1.0');
$item->addChild('g:condition', 'new', 'http://base.google.com/ns/1.0');
}
$this->logFeedStatus($db, $config['id'], 'success');
return new Response($xml->asXML(), 200, ['Content-Type' => 'application/xml']);
} catch (Exception $e) {
if (isset($db) && isset($config['id'])) {
$this->logFeedStatus($db, $config['id'], 'error', $e->getMessage());
}
return new Response("Error generating feed: " . $e->getMessage(), 500);
}
}
}
—
### β
Whatβs Handled:
– No table creation logic
– `logFeedStatus()` inserts into `shoppingfeedlog`
– Logs success or error with:
– `feed_id`
– `updated_at`
– `status`
– `error_message` (nullable)
– Silently skips logging errors to avoid blocking feed response
Let me know if you want a dashboard view for these logs.
use ConcreteCoreControllerAbstractController;
use ConcreteCoreDatabaseConnectionConnection;
use SymfonyComponentHttpFoundationResponse;
use ConcretePackageCommunityStoreSrcCommunityStoreProductProduct;
use ConcreteCoreSupportFacadeUrl;
use ConcreteCorePagePage;
use Exception;
use DateTime;
class MerchantFeed extends AbstractController
{
protected function logFeedStatus($db, $feedId, $status, $message = null)
{
try {
$now = (new DateTime())->format('Y-m-d H:i:s');
$db->insert('shoppingfeedlog', [
'feed_id' => $feedId,
'updated_at' => $now,
'status' => $status,
'error_message' => $message
]);
} catch (Exception $logException) {
// Optional: Log error to file or skip silently
// error_log("Feed log error: " . $logException->getMessage());
}
}
public function generateXml($feedSlug = null)
{
$baseUrl = Url::to('/');
$db = Core::make(Connection::class);
try {
if (!$feedSlug) {
throw new Exception("Feed slug is required.");
}
$config = $db->fetchAssociative("SELECT * FROM shoppingfeedconfiguration WHERE feedSlug = ?", [$feedSlug]);
if (!$config) {
throw new Exception("Feed not found.");
}
$settings = unserialize($config['settings']);
$baseSelect = "SELECT p.*";
$baseFrom = "FROM CommunityStoreProducts p";
$joins = [];
$conditions = ["1=1"];
$params = [];
if (!empty($settings['price_range']['min']) && !empty($settings['price_range']['max'])) {
$conditions[] = "p.pPrice BETWEEN ? AND ?";
$params[] = (float)$settings['price_range']['min'];
$params[] = (float)$settings['price_range']['max'];
}
if (!empty($settings['product_groups'])) {
$groupIds = array_map('intval', $settings['product_groups']);
$joins[] = "INNER JOIN CommunityStoreProductGroups pg ON pg.pID = p.pID";
$conditions[] = "pg.gID IN (" . implode(',', $groupIds) . ")";
}
if (!empty($settings['product_types'])) {
$productTypes = array_map('intval', $settings['product_types']);
$conditions[] = "p.pType IN (" . implode(',', $productTypes) . ")";
}
if (!empty($settings['pShippable'])) {
$conditions[] = "p.pShippable = '" . $settings['pShippable'] . "'";
}
if (!empty($settings['pTaxable'])) {
$conditions[] = "p.pTaxable = '" . $settings['pTaxable'] . "'";
}
if (!empty($settings['pFeatured'])) {
$conditions[] = "p.pFeatured = '" . $settings['pFeatured'] . "'";
}
$sql = "
$baseSelect
$baseFrom
" . implode(" ", $joins) . "
WHERE " . implode(" AND ", $conditions) . "
GROUP BY p.pID
ORDER BY p.pID DESC
";
$products = $db->fetchAll($sql, $params);
$xml = new SimpleXMLElement('');
$xml->addAttribute('xmlns:g', 'http://base.google.com/ns/1.0');
$channel = $xml->addChild('channel');
$channel->addChild('title', 'Your Store Feed');
$channel->addChild('link', $baseUrl);
$channel->addChild('description', 'Product feed for Google Merchant Center');
foreach ($products as $product) {
$productu = Product::getByID($product['pID']);
$page = Page::getByID($productu->getPageID());
if (!$page || $page->isError()) {
continue;
}
$url = $page->getCollectionLink();
$imageUrl = '';
$image = $productu->getImageObj();
if (is_object($image)) {
$imageUrl = $image->getURL();
}
$brand = '';
if (is_numeric($product['pManufacturer'])) {
$manufacturer = $db->fetchAssociative("SELECT * FROM CommunityStoreManufacturer WHERE mID = ?", [$product['pManufacturer']]);
if (is_array($manufacturer)) {
$brand = $manufacturer['mName'];
}
}
$item = $channel->addChild('item');
$item->addChild('g:id', $product['pID'], 'http://base.google.com/ns/1.0');
$item->addChild('title', htmlspecialchars($product['pName']));
$item->addChild('description', htmlspecialchars($product['pDesc']));
$item->addChild('link', htmlspecialchars($url));
$item->addChild('g:image_link', htmlspecialchars($imageUrl), 'http://base.google.com/ns/1.0');
$item->addChild('g:availability', 'in stock', 'http://base.google.com/ns/1.0');
$item->addChild('g:price', $product['pPrice'] . ' USD', 'http://base.google.com/ns/1.0');
$item->addChild('g:brand', htmlspecialchars($brand), 'http://base.google.com/ns/1.0');
$item->addChild('g:condition', 'new', 'http://base.google.com/ns/1.0');
}
$this->logFeedStatus($db, $config['id'], 'success');
return new Response($xml->asXML(), 200, ['Content-Type' => 'application/xml']);
} catch (Exception $e) {
if (isset($db) && isset($config['id'])) {
$this->logFeedStatus($db, $config['id'], 'error', $e->getMessage());
}
return new Response("Error generating feed: " . $e->getMessage(), 500);
}
}
}
also log Pdesc as error if its empty and save message with full detail
β
Logging of `pDesc` (product description) as an error if it's empty
β
Full error message saved in the log including product name and ID
β
Still avoids creating the table β assumes `shoppingfeedlog` already exists
—
### β Updated `generateXml()` Method with `pDesc` Error Logging
// Inside your foreach loop after checking $productu and $page
foreach ($products as $product) {
$productu = Product::getByID($product['pID']);
$page = Page::getByID($productu->getPageID());
if (!$page || $page->isError()) {
continue;
}
$url = $page->getCollectionLink();
$imageUrl = '';
$image = $productu->getImageObj();
if (is_object($image)) {
$imageUrl = $image->getURL();
}
$brand = '';
if (is_numeric($product['pManufacturer'])) {
$manufacturer = $db->fetchAssociative("SELECT * FROM CommunityStoreManufacturer WHERE mID = ?", [$product['pManufacturer']]);
if (is_array($manufacturer)) {
$brand = $manufacturer['mName'];
}
}
// π¨ Check for empty description and log it
if (empty(trim($product['pDesc']))) {
$errorMessage = sprintf(
"Missing description for product ID %d (%s). Skipped from feed.",
$product['pID'],
$product['pName']
);
$this->logFeedStatus($db, $config['id'], 'error', $errorMessage);
continue; // skip product
}
$item = $channel->addChild('item');
$item->addChild('g:id', $product['pID'], 'http://base.google.com/ns/1.0');
$item->addChild('title', htmlspecialchars($product['pName']));
$item->addChild('description', htmlspecialchars($product['pDesc']));
$item->addChild('link', htmlspecialchars($url));
$item->addChild('g:image_link', htmlspecialchars($imageUrl), 'http://base.google.com/ns/1.0');
$item->addChild('g:availability', 'in stock', 'http://base.google.com/ns/1.0');
$item->addChild('g:price', $product['pPrice'] . ' USD', 'http://base.google.com/ns/1.0');
$item->addChild('g:brand', htmlspecialchars($brand), 'http://base.google.com/ns/1.0');
$item->addChild('g:condition', 'new', 'http://base.google.com/ns/1.0');
}
—
### π No changes needed to `logFeedStatus()` β it already logs message + status.
This will now:
– **Skip any product that has an empty `pDesc`**
– **Log the issue to `shoppingfeedlog`** with meaningful details
Let me know if you want to:
– Log **all products**, not just skipped ones
– Include more fields (like SKU or category)
– Include **XML in response even for error cases**
$page = Page::getByID($productu->getPageID());
if (!$page || $page->isError()) {
continue;
}
$url = $page->getCollectionLink();
$page = Page::getByID($productu->getPageID());
if (!$page || $page->isError()) {
// Log here
continue;
}
### β
Full Logging Logic Added:
– Logs when a product has a missing or invalid page
– Includes product ID and name
– Uses the same `logFeedStatus()` function without creating tables
—
### β Final Updated Section of `generateXml()` Method:
foreach ($products as $product) {
$productu = Product::getByID($product['pID']);
if (!$productu) {
$this->logFeedStatus($db, $config['id'], 'error', "Product not found for ID {$product['pID']}");
continue;
}
$pageID = $productu->getPageID();
$page = Page::getByID($pageID);
if (!$page || $page->isError()) {
$this->logFeedStatus($db, $config['id'], 'error', "Missing or invalid page for product ID {$product['pID']} ({$product['pName']}). Page ID: {$pageID}");
continue;
}
$url = $page->getCollectionLink();
$imageUrl = '';
$image = $productu->getImageObj();
if (is_object($image)) {
$imageUrl = $image->getURL();
}
$brand = '';
if (is_numeric($product['pManufacturer'])) {
$manufacturer = $db->fetchAssociative("SELECT * FROM CommunityStoreManufacturer WHERE mID = ?", [$product['pManufacturer']]);
if (is_array($manufacturer)) {
$brand = $manufacturer['mName'];
}
}
// π¨ Log missing description
if (empty(trim($product['pDesc']))) {
$this->logFeedStatus($db, $config['id'], 'error', "Missing description for product ID {$product['pID']} ({$product['pName']})");
continue;
}
$item = $channel->addChild('item');
$item->addChild('g:id', $product['pID'], 'http://base.google.com/ns/1.0');
$item->addChild('title', htmlspecialchars($product['pName']));
$item->addChild('description', htmlspecialchars($product['pDesc']));
$item->addChild('link', htmlspecialchars($url));
$item->addChild('g:image_link', htmlspecialchars($imageUrl), 'http://base.google.com/ns/1.0');
$item->addChild('g:availability', 'in stock', 'http://base.google.com/ns/1.0');
$item->addChild('g:price', $product['pPrice'] . ' USD', 'http://base.google.com/ns/1.0');
$item->addChild('g:brand', htmlspecialchars($brand), 'http://base.google.com/ns/1.0');
$item->addChild('g:condition', 'new', 'http://base.google.com/ns/1.0');
}
—
### π Result:
Now you're logging:
– Missing or invalid product page
– Empty product description
Would you also like:
– To log **missing image** URLs?
– To log **successful product entries** (optional auditing)?
—
### β Final XML Generation Block with `g:identifier_exists`
Hereβs the updated part of the `foreach` loop:
$item = $channel->addChild('item');
$item->addChild('g:id', $product['pID'], 'http://base.google.com/ns/1.0');
$item->addChild('title', htmlspecialchars($product['pName']));
$item->addChild('description', htmlspecialchars($product['pDesc']));
$item->addChild('link', htmlspecialchars($url));
$item->addChild('g:image_link', htmlspecialchars($imageUrl), 'http://base.google.com/ns/1.0');
$item->addChild('g:availability', 'in stock', 'http://base.google.com/ns/1.0');
$item->addChild('g:price', $product['pPrice'] . ' USD', 'http://base.google.com/ns/1.0');
$item->addChild('g:brand', htmlspecialchars($brand), 'http://base.google.com/ns/1.0');
$item->addChild('g:condition', 'new', 'http://base.google.com/ns/1.0');
// β
Add this tag to declare no GTIN/MPN
$item->addChild('g:identifier_exists', 'false', 'http://base.google.com/ns/1.0');
—
### π Why this matters
Google requires `g:identifier_exists` to be set to `false` if you don't provide a GTIN, MPN, or brand. Youβre already including brand optionally, but this line ensures that products without standard identifiers are still accepted by Google Merchant Center.
Let me know if you also want to dynamically set this based on GTIN/MPN presence.
—
### β Insert This Check Before XML Generation Loop
Update your code **right after** `$products = $db->fetchAll(…);` like this:
$products = $db->fetchAll($sql, $params);
// Check for single product with missing description
if (count($products) === 1 && empty(trim($products[0]['pDesc']))) {
$this->logFeedStatus($db, $config['id'], 'error', "Only 1 product found and its description is missing (ID: {$products[0]['pID']}, Name: {$products[0]['pName']}). Feed not generated.");
return new Response("Cannot generate feed: Only 1 product found and it has no description.", 400);
}
—
### β Keep your `foreach` loop for XML generation exactly as-is after this block.
This ensures:
– If there's **just 1 product** and it has **no description**, you:
– **Skip XML generation**
– **Log the error**
– **Return a plain message with 400 response**
Would you also like to:
– Include a UI warning in the dashboard?
– Optionally fallback to a default description like βNo description availableβ?
If you want to **also skip XML generation when there are multiple products, but none of them have a description**, you can add an additional check before generating the XML.
—
### β
Full Logic to Handle Both Cases:
– β
**1 product, and it has no description β don't generate XML**
– β
**Multiple products, but none have a description β don't generate XML**
—
### π§ Insert This Code After Fetching Products:
$products = $db->fetchAll($sql, $params);
// β
Case 1: Only 1 product and its description is missing
if (count($products) === 1 && empty(trim($products[0]['pDesc']))) {
$this->logFeedStatus(
$db,
$config['id'],
'error',
"Only 1 product found and its description is missing (ID: {$products[0]['pID']}, Name: {$products[0]['pName']}). Feed not generated."
);
return new Response("Cannot generate feed: Only 1 product and its description is missing.", 400);
}
// β
Case 2: More than 1 product, but none have a description
$hasAtLeastOneWithDesc = false;
foreach ($products as $p) {
if (!empty(trim($p['pDesc']))) {
$hasAtLeastOneWithDesc = true;
break;
}
}
if (!$hasAtLeastOneWithDesc) {
$this->logFeedStatus(
$db,
$config['id'],
'error',
"Multiple products found but none have a description. Feed not generated."
);
return new Response("Cannot generate feed: All products are missing descriptions.", 400);
}
—
### β Then continue with your XML generation loop as usual.
This ensures you:
– Donβt generate an empty or invalid feed
– Have detailed logs for debugging
– Return meaningful HTTP 400 responses
Let me know if you'd like to include the product IDs/names in the second case log too.