Subcategory Products – Journal3 OpenCart kiegészítő
Kategória oldalon megjelenő termékblokk, amely az aktuális kategória összes közvetlen alkategóriájának termékeit gyűjti össze és Journal3 natív termékkártyákon jeleníti meg – eladásszám, majd megtekintésszám szerinti sorrendben. Dynamic Content modulként helyezhető el a kategória layout-ban.
Mit csinál?
A blokk a kategória oldal layout-jában helyezhető el Journal3 Dynamic Content modulként. Automatikusan:
- Csak akkor jelenik meg, ha az aktuális kategóriának van legalább egy aktív, az adott store-hoz tartozó közvetlen alkategóriája.
- Összegyűjti az összes közvetlen alkategória aktív, raktáron lévő termékeit – duplikátumok nélkül, ha egy termék több alkategóriában is szerepel.
- Sorba rendezi őket: elsődlegesen eladásszám szerint csökkenően (
order_producttábla összesítése), másodlagosan megtekintésszám szerint csökkenően (product.viewed). - Legfeljebb annyi terméket jelenít meg, amennyit az admin termékszám-limit beállítása enged (
config_limit_admin). - A megjelenítéshez a Journal3 natív
journal3/productstemplate-jét használja – pontosan ugyanolyan termékkártyák jelennek meg, mint a kategória oldal fő terméklistájában, beleértve a hover képet, labeleket, extra gombokat, retina képeket és az akciós ár visszaszámlálót. - A grid/list nézetváltás automatikusan követi az oldal többi terméklistájának állapotát, mivel ugyanazt a
main-productskonténer struktúrát és CSS osztályokat használja, amelyekre a Journal3 JavaScript épít.
Hogyan működik?
- Az URL
pathGET paraméteréből (pl.?path=20_24) az utolsó szegmens adja meg az aktuális kategória ID-ját. - Lekérdezi az aktuális kategória közvetlen, aktív, store-hoz rendelt alkategóriáit. Ha nincs egy sem, a metódus üres stringet ad vissza – a blokk nem renderelődik.
- Egy fan-out mentes SQL lekérdezéssel összegyűjti az alkategóriák termékeit, és rendezi őket eladás majd megtekintés szerint. Csak engedélyezett, az aktuális store-hoz tartozó, elérhető dátumú, és raktáron lévő (
quantity > 0) termékek kerülnek be. - A Journal3 saját
model_journal3_product->getProduct()metódusával lekéri a teljes termékadatot Journal formátumban – tartalmazza asecond_image,special_date_endés egyéb Journal-specifikus mezőket is. - Az SQL által meghatározott sorrendet visszaállítja, mivel a Journal modell nem garantálja a sorrend megőrzését.
- Journal3 service-ek segítségével formázza az adatokat:
journal3_image(1x/2x retina képek),journal3_product_extras(labelek, extra gombok, CSS osztályok),journal3_url(SEO URL-ek). - Minden terméket a
$this->load->view('journal3/products', $data)hívással renderel – ez a Journal3 saját termékkártya template-je. - Az eredményt egy
main-productskonténer div-be csomagolja, amelynek CSS osztályai a Journal3 globális beállításaiból (globalProductView,globalProductGridType) jönnek – pontosan mint acategory.twig-ben.
SQL fan-out kezelése
Ha egy termék több alkategóriában is szerepel, és a product_to_category táblát közvetlenül joinolnánk az order_product táblával, az eladásszám N-szeresére növekedne. A lekérdezés ezt két subquery-vel kerüli el:
- p2c subquery:
SELECT DISTINCT product_id FROM product_to_category WHERE category_id IN (...)– minden termék egyszer szerepel, függetlenül attól, hány alkategóriában van. - os subquery: az
order_productösszesítése termék szinten történik meg a join előtt, így a p2c nem szorozza meg az összeget.
Követelmények
| Komponens | Verzió | Megjegyzés |
|---|---|---|
| OpenCart | 3.0.x | |
| Journal3 téma | 3.2.x | journal3_image, journal3_product_extras, journal3_url service-ek szükségesek |
| PHP | 7.x / 8.x |
Telepítés
1. Fájl másolása
Másold a controller fájlt az OpenCart gyökérkönyvtárába, megőrizve a mappastruktúrát:
| upload/ | ||
| └── | catalog/controller/journal3/ | |
| └── | subcategory_products.php | → catalog/controller/journal3/subcategory_products.php |
Külön Twig template fájl nem szükséges. A controller a Journal3 beépített journal3/products view-ját használja, amely már a témával együtt telepítve van.
Admin beállítás – Dynamic Content elhelyezése
- Nyisd meg: Journal Admin → Layouts → [Kategória layout szerkesztése]
- Kattints az „Add Module" gombra, és válaszd a Dynamic Content modult.
- A „Controller" mezőbe írd be:
journal3/subcategory_products - Helyezd el a blokkot a kívánt pozícióban (javasolt: a fő terméklista fölé, a
content_topzónába). - Mentsd el a layout-ot.
Fontos: A blokk kizárólag kategória oldalakon jelenik meg, ahol az URL tartalmaz path GET paramétert. Más oldalakon (főoldal, termékoldal stb.) a controller automatikusan üres stringet ad vissza, a blokk nem renderelődik.
Termékszám limit
A megjelenített termékek maximális száma az OpenCart admin „Limit (Admin)" beállításából jön:
$limit = (int)$this->config->get('config_limit_admin');
if ($limit < 1) { $limit = 12; }
Beállítás helye: Admin → System → Settings → [Store] → Option fül → „Limit (Admin)". Ha az érték nulla vagy üres, a fallback értéke 12.
A Dynamic Content modulnak nincs saját per-instance limit beállítása a Journal admin UI-ban. Ha az itt megadott értéktől eltérő limitet szeretnél, módosítsd a $limit változó értékét közvetlenül a controller fájlban.
Fájlszerkezet
| subcategory_products/ | |
| ├── subcategory_products.html | Ez a dokumentáció |
| └── upload/catalog/controller/journal3/ | |
| └── subcategory_products.php | Dynamic Content controller: SQL, adatformázás, Journal3 renderelés |
Teljes kód
<?php
class ControllerJournal3SubcategoryProducts extends Controller {
public function index($args = array()) {
// 1. Aktuális kategória azonosítása
$path = isset($this->request->get['path']) ? (string)$this->request->get['path'] : '';
if (empty($path)) { return ''; }
$parts = explode('_', $path);
$category_id = (int)end($parts);
if ($category_id < 1) { return ''; }
// 2. Konfiguráció
$db = $this->db;
$store_id = (int)$this->config->get('config_store_id');
$limit = (int)$this->config->get('config_limit_admin');
if ($limit < 1) { $limit = 12; }
// 3. Közvetlen alkategóriák – ha nincs, a blokk nem jelenik meg
$sub_result = $db->query("
SELECT c.category_id
FROM `" . DB_PREFIX . "category` c
INNER JOIN `" . DB_PREFIX . "category_to_store` c2s
ON c2s.category_id = c.category_id
WHERE c.parent_id = '" . (int)$category_id . "'
AND c.status = '1'
AND c2s.store_id = '" . $store_id . "'"
);
if (!$sub_result->num_rows) { return ''; }
$sub_ids_sql = implode(',', array_map('intval', array_column($sub_result->rows, 'category_id')));
// 4. Termékek: eladásszám DESC → megtekintés DESC (fan-out mentes)
$prod_result = $db->query("
SELECT p.product_id,
COALESCE(os.total_sold, 0) AS total_sold, p.viewed
FROM `" . DB_PREFIX . "product` p
INNER JOIN (
SELECT DISTINCT product_id FROM `" . DB_PREFIX . "product_to_category`
WHERE category_id IN (" . $sub_ids_sql . ")"
") p2c ON p2c.product_id = p.product_id
INNER JOIN `" . DB_PREFIX . "product_to_store` p2s
ON p2s.product_id = p.product_id AND p2s.store_id = '" . $store_id . "'
LEFT JOIN (
SELECT product_id, SUM(quantity) AS total_sold
FROM `" . DB_PREFIX . "order_product` GROUP BY product_id
) os ON os.product_id = p.product_id
WHERE p.status = '1'
AND p.quantity > 0
AND (p.date_available = '0000-00-00' OR p.date_available <= NOW())
ORDER BY total_sold DESC, p.viewed DESC
LIMIT " . $limit
);
if (!$prod_result->num_rows) { return ''; }
$product_ids = array_column($prod_result->rows, 'product_id');
// 5. Journal termékmodell + sorrend visszaállítása
$this->load->model('journal3/product');
$this->load->language('product/product');
$raw = $this->model_journal3_product->getProduct($product_ids);
if (empty($raw)) { return ''; }
$by_id = array();
foreach ($raw as $r) { $by_id[$r['product_id']] = $r; }
$results = array();
foreach ($product_ids as $id) {
if (isset($by_id[$id])) { $results[] = $by_id[$id]; }
}
// 6. Journal megjelenítési beállítások
$display = $this->journal3->get('globalProductView') ?: 'grid';
$grid_type = $this->journal3->get('globalProductGridType') ?: 'ipr';
$img_w = (int)($this->config->get('theme_journal3_image_product_width') ?: 240);
$img_h = (int)($this->config->get('theme_journal3_image_product_height') ?: 240);
$img_r = 'crop';
$settings = array(
'image_width' => $img_w, 'image_height' => $img_h, 'image_resize' => $img_r,
'swiper_carousel' => false,
'products_classes' => array('product-' . $display),
'moduleProductDescriptionLimit' => 160,
'moduleProductGridSecondImageStatus' => false,
'moduleProductStat1' => '',
'moduleProductStat2' => '',
'button_cart' => $this->language->get('button_cart'),
'button_wishlist' => $this->language->get('button_wishlist'),
'button_compare' => $this->language->get('button_compare'),
'text_tax' => $this->language->get('text_tax'),
);
// 7. Formázás + renderelés Journal journal3/products view-val
$rendered = '';
foreach ($results as $result) {
$src = $result['image'] ?: $this->journal3->get('placeholder');
$thumb = $this->journal3_image->resize($src, $img_w, $img_h, $img_r);
$thumb2x = $this->journal3_image->resize($src, $img_w*2, $img_h*2, $img_r);
$second_thumb = $second_thumb2x = false;
if ($this->journal3->is_desktop
&& $this->journal3->get('globalProductGridSecondImageStatus')
&& !empty($result['second_image'])) {
$second_thumb = $this->journal3_image->resize($result['second_image'], $img_w, $img_h, $img_r);
$second_thumb2x = $this->journal3_image->resize($result['second_image'], $img_w*2, $img_h*2, $img_r);
}
$price = ($this->customer->isLogged() || !$this->config->get('config_customer_price'))
? $this->currency->format($this->tax->calculate($result['price'], $result['tax_class_id'], $this->config->get('config_tax')), $this->session->data['currency'])
: false;
$special = false;
if (!is_null($result['special']) && (float)$result['special'] >= 0) {
$special = ($result['special_date_end'] && strtotime($result['special_date_end']) < time())
? false
: $this->currency->format($this->tax->calculate($result['special'], $result['tax_class_id'], $this->config->get('config_tax')), $this->session->data['currency']);
}
$tax = $this->config->get('config_tax') ? $this->currency->format((float)($special ? $result['special'] : $result['price']), $this->session->data['currency']) : false;
$rating = $this->config->get('config_review_status') ? $result['rating'] : false;
$classes = $this->journal3_product_extras->exclude_button($result);
$labels = $this->journal3_product_extras->labels($result);
$extra_buttons = $this->journal3_product_extras->extra_buttons($result);
$classes['out-of-stock'] = $result['quantity'] <= 0;
$classes['has-special'] = (bool)$special;
$classes['has-countdown'] = $special && $result['special_date_end'];
$classes['has-extra-button'] = (bool)$extra_buttons;
$classes['has-zero-price'] = ($special ? (float)$result['special'] : (float)$result['price']) <= 0;
$product_data = array(
'product_id' => $result['product_id'],
'name' => $result['name'],
'description' => mb_substr(trim(strip_tags(html_entity_decode($result['description'], ENT_QUOTES, 'UTF-8'))), 0, 160, 'UTF-8') . '..',
'price' => $price, 'special' => $special,
'tax' => $tax, 'rating' => $rating,
'minimum' => $result['minimum'] > 0 ? $result['minimum'] : 1,
'href' => $this->journal3_url->link('product/product', 'product_id=' . $result['product_id']),
'thumb' => $thumb, 'thumb2x' => $thumb2x,
'second_thumb' => $second_thumb, 'second_thumb2x' => $second_thumb2x,
'classes' => $classes, 'labels' => $labels,
'extra_buttons' => $extra_buttons, 'quantity' => $result['quantity'],
'stock_status' => $result['stock_status'],
'date_end' => $result['special_date_end'],
'price_value' => ($special ? $result['special'] > 0 : $result['price'] > 0),
'stat1' => $this->journal3_product_extras->stat($result, ''),
'stat2' => $this->journal3_product_extras->stat($result, ''),
'qid' => uniqid('q-'),
'button_cart' => $this->journal3->get('filterAddToCartStock') && $result['quantity'] <= 0
? $result['stock_status']
: $this->language->get('button_cart'),
);
$rendered .= $this->load->view('journal3/products', array_merge($settings, ['product' => $product_data]));
}
if (empty($rendered)) { return ''; }
// 8. Konténer – category.twig main-products struktúrája
return '<div class="main-products main-products-style product-'
. $display . ' ' . $grid_type . '-grid">' . $rendered . '</div>';
}
}
class_alias('ControllerJournal3SubcategoryProducts', '\Opencart\Catalog\Controller\Journal3\SubcategoryProducts');
Ismert problémák és megjegyzések
Termékszám limit nem konfigurálható Journal adminból
A Dynamic Content blokkoknak nincs per-instance beállítási lehetőségük a Journal admin UI-ban. A limit értéke az OpenCart Admin → System → Settings → Option → „Limit (Admin)" mezőjéből jön (config_limit_admin). Ha eltérő értéket szeretnél, módosítsd közvetlenül a controllerben a $limit változót.
Eladásszám minden rendelési státuszt tartalmaz
Az eladásszám az order_product tábla összes rekordját összesíti, státusztól függetlenül. Ha csak a teljesített rendeléseket szeretnéd figyelembe venni, az os subquery-t bővíteni kell egy INNER JOIN oc_order joinnal és megfelelő státusz szűrővel.
stat1 / stat2 üres értékkel hívódik
A Journal3 Products modulban a stat1 és stat2 értéke egy admin-konfigurált mező neve (pl. 'reviews', 'sold'). Mivel a Dynamic Content blokknak nincs ilyen konfigurációja, mindkét stat üres stringgel kerül átadásra – a stat mezők nem jelennek meg. Ha szükséges, módosítsd közvetlenül a controllerben.
Journal3 frissítés után: A controller a journal3/products beépített Journal template-et használja. Ha egy Journal frissítés megváltoztatja ennek a template-nek az elvárt adatstruktúráját, a blokk megjelenése változhat. Frissítés után ellenőrizd a blokk megjelenését.
Frissítésállóság: A controller fájl a catalog/controller/journal3/ mappában van. A Journal3 frissítései nem írják felül az itt lévő egyedi fájlokat – csak a Journal saját fájljait frissítik.