Skip to content

Product Lists Management (Admin)

Flow ID: AD-27 Module(s): eshop Complexity: Medium-High Last Updated: 2026-04-04

Business Context

Product Lists are a merchandising tool for curating hand-picked collections of products displayed on the homepage, category pages, and other storefront locations. The system uses a three-level hierarchy: Groups --> Lists --> Products. Groups act as named containers (e.g., "tabs", "home_tabs_extra") identified by slug; each group holds multiple Lists with MUI (multi-language) metadata and images; each list links to products via a lookup table with explicit ordering. This powers homepage tab carousels, standalone product list sections, and optional category-level product list assignments.

Architecture

The feature spans both legacy and modern layers:

  • Legacy admin layer: Traditional CodeIgniter controller + model + PHP views for the admin UI
  • Modern domain layer: Full PSR-4 domain with three sub-entities (ProductList, Group, ProductLp) under src/Domains/Product/ProductList/
  • Modern REST layer: Three REST controllers with full CRUD (read + write) endpoints

Database Schema

TablePurposeKey Columns
product_list_groupTop-level grouping containersid, name, slug
product_listIndividual curated listsid, group_id (FK), image, small_banner, header_color, text_color, ord
product_list_muiMulti-language translations per listid, product_list_id (FK), lang, slug, name, description, url, meta_title, meta_keywords, meta_description
product_list_product_lpLookup table linking products to listsid, product_list_id (FK), product_id (FK), ord
shop_product_category_listsOptional category-to-list assignmentsid, category_id, product_list_id, priority

Notable migrations:

  • 20241125153308_add_product_list_url_field.php -- Added url column to product_list_mui
  • 20241127121816_shop_product_category_lists.php -- Created the category-list bridge table

Admin Controller (Legacy)

Controller: ecommercen/eshop/controllers/Adv_product_list_admin.php (418 lines) Application wrapper: application/modules/eshop/controllers/Product_list_admin.php (extends, empty body) Model: ecommercen/eshop/models/Adv_product_list_model.php (374 lines) Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTSAdmin menu: MARKETING group

Controller Actions

MethodRoutePurposeAuth Restriction
index()eshop/product_list_adminList all groupsStandard
add_group()eshop/product_list_admin/add_groupCreate group (name + auto-slug)ADVISABLE only
edit_group($groupId)eshop/product_list_admin/edit_group/{id}Edit group nameADVISABLE only
lists($groupId)eshop/product_list_admin/lists/{groupId}Lists within a group (sortable)Standard
add_product_list($groupId)eshop/product_list_admin/add_product_list/{groupId}Create list with MUI, imagesStandard
update_list($groupId, $listId)eshop/product_list_admin/update_list/{groupId}/{listId}Edit list (MUI fields, images, image delete)Standard
products($groupId, $productListId)eshop/product_list_admin/products/{groupId}/{listId}Products in a list (sortable)Standard
remove_product_from_list(...)eshop/product_list_admin/remove_product_from_list/{groupId}/{listId}/{productId}Remove single productStandard
batchRemoveFromProductList(...)eshop/product_list_admin/batchRemoveFromProductList/{groupId}/{listId}Batch remove selected productsStandard
delete($type, $id, $groupId)eshop/product_list_admin/delete/{type}/{id}/{groupId}Delete list (group delete commented out)Standard (list), ADVISABLE (group, disabled)
update_order($target)eshop/product_list_admin/update_order/{target}AJAX drag-and-drop reorderStandard

Admin Views (7 templates)

application/views/admin/product_lists/
  groups/
    list.php          -- Group listing table with edit/view-lists/delete(disabled) actions
    create.php        -- Group creation form (name only), Scribe-how help widget
    update.php        -- Group edit form
  lists/
    list.php          -- Lists within group, sortable rows, image thumbnail preview
    create.php        -- List creation: MUI name, URL, image, small_banner, description (TinyMCE)
    update.php        -- List edit: same fields + image delete checkbox
  product_list_products.php -- Products in list: sortable, checkboxes for batch remove, product ID copy button, link to product editor

List Fields (MUI per language)

  • name (required, unique per language via is_unique_mui validation)
  • url -- optional link URL per language
  • description -- rich text via TinyMCE editor
  • image -- main banner image, uploaded to files/product_list/
  • small_banner -- mobile/small banner image, same upload directory

Group Slug Generation

Groups auto-generate slugs from the name using greek_lower() (handles Greek characters). Uniqueness enforced via loop with suffix counter (e.g., my-group, my-group-1).

Product Assignment from Products Admin

Products can be assigned to lists in bulk from the Products Admin (Adv_products_admin.php) batch actions:

  • assign_product_to_product_list -- Select products, choose a list from dropdown, assigns via assignProductsToProductList() (deduplicates existing entries)
  • remove_from_product_list -- Select products, choose a list (filtered to lists containing selected products), batch removes via batchRemoveFromProductList()

The product listing page also loads productLists dropdown for the batch action modal via getAllProductListsCombo().

Modern Domain Layer

Namespace: Advisable\Domains\Product\ProductList\DI Registration: src/Domains/Product/container.php

Three sub-entities with full read + write services:

ProductList (main entity)

ClassPath
Entitysrc/Domains/Product/ProductList/Repository/Entity.php
MuiEntitysrc/Domains/Product/ProductList/Repository/MuiEntity.php
Repositorysrc/Domains/Product/ProductList/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Product/ProductList/Repository/RepositoryConfigurator.php
MuiRepositorysrc/Domains/Product/ProductList/Repository/MuiRepository.php
MuiWriteRepositorysrc/Domains/Product/ProductList/Repository/MuiWriteRepository.php
WriteRepositorysrc/Domains/Product/ProductList/Repository/WriteRepository.php
Servicesrc/Domains/Product/ProductList/Service.php
WriteServicesrc/Domains/Product/ProductList/WriteService.php
WriteDatasrc/Domains/Product/ProductList/WriteData.php
MuiWriteDatasrc/Domains/Product/ProductList/MuiWriteData.php
Validatorsrc/Domains/Product/ProductList/Validator.php
ListRequestsrc/Domains/Product/ProductList/ListRequest.php
FilterByTranslationsrc/Domains/Product/ProductList/Repository/Specification/FilterByTranslation.php
SortByTranslationsrc/Domains/Product/ProductList/Repository/Specification/SortByTranslation.php

Entity properties: id, image, small_banner, header_color, text_color, ord, group_idMuiEntity properties: id, product_list_id, slug, meta_title, meta_keywords, meta_description, name, description, url, lang

Relations (defined in RepositoryConfigurator):

  • translations -- ONE_TO_MANY to MuiRepository via product_list_id
  • productLps -- ONE_TO_MANY to ProductLp\Repository via product_list_id
  • group -- BELONGS_TO to Group\Repository via group_id

WriteService handles transactional create/update/delete with MUI translation support. On delete, translations are removed first, then the master record.

ListRequest allows filtering by id (exact), groupId (exact), name.{locale} (partial), slug.{locale} (exact). Sorting by id, ord, name.{locale}.

Group

ClassPath
Entitysrc/Domains/Product/ProductList/Group/Repository/Entity.php
Repositorysrc/Domains/Product/ProductList/Group/Repository/Repository.php
WriteRepositorysrc/Domains/Product/ProductList/Group/Repository/WriteRepository.php
Servicesrc/Domains/Product/ProductList/Group/Service.php
WriteServicesrc/Domains/Product/ProductList/Group/WriteService.php
WriteDatasrc/Domains/Product/ProductList/Group/WriteData.php
Validatorsrc/Domains/Product/ProductList/Group/Validator.php
ListRequestsrc/Domains/Product/ProductList/Group/ListRequest.php

Entity properties: id, name, slugWriteData fields: name (required for create), slug (required for create)

ClassPath
Entitysrc/Domains/Product/ProductList/ProductLp/Repository/Entity.php
Repositorysrc/Domains/Product/ProductList/ProductLp/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Product/ProductList/ProductLp/Repository/RepositoryConfigurator.php
WriteRepositorysrc/Domains/Product/ProductList/ProductLp/Repository/WriteRepository.php
Servicesrc/Domains/Product/ProductList/ProductLp/Service.php
WriteServicesrc/Domains/Product/ProductList/ProductLp/WriteService.php
WriteDatasrc/Domains/Product/ProductList/ProductLp/WriteData.php
Validatorsrc/Domains/Product/ProductList/ProductLp/Validator.php
ListRequestsrc/Domains/Product/ProductList/ProductLp/ListRequest.php

Entity properties: id, ord, product_list_id, product_idRelations: product -- BELONGS_TO Product\Repository via product_id

REST API Layer

DI Registration: src/Rest/Product/container.php

Endpoints

ProductList (/rest/product/product-list):

MethodPathActionNotes
GET/rest/product/product-listindex()Paginated collection with filters/sorts
GET/rest/product/product-list/itemitem()Single item by filter (e.g., slug)
GET/rest/product/product-list/{id}show($id)Single item by ID
POST/rest/product/product-liststore()Create (JSON or multipart with file uploads)
POST/rest/product/product-list/{id}update($id)Update (JSON or multipart)
DELETE/rest/product/product-list/{id}destroy($id)Delete with MUI cleanup

Uses HandlesUploadActions trait for file upload support. Upload fields: image and small_banner to files/product_list/.

ProductListGroup (/rest/product/product-list-group):

MethodPathAction
GET/rest/product/product-list-groupindex()
GET/rest/product/product-list-group/itemitem()
GET/rest/product/product-list-group/{id}show($id)
POST/rest/product/product-list-groupstore()
POST/rest/product/product-list-group/{id}update($id)
DELETE/rest/product/product-list-group/{id}destroy($id)

Uses HandlesWriteActions trait (no file fields).

ProductListProductLp (/rest/product/product-list-product-lp):

MethodPathAction
GET/rest/product/product-list-product-lpindex()
GET/rest/product/product-list-product-lp/itemitem()
GET/rest/product/product-list-product-lp/{id}show($id)
POST/rest/product/product-list-product-lpstore()
POST/rest/product/product-list-product-lp/{id}update($id)
DELETE/rest/product/product-list-product-lp/{id}destroy($id)

Uses HandlesWriteActions trait. Supports with=product relation inclusion.

All REST routes support locale-prefixed variants (e.g., el/rest/product/product-list). No REST policy restrictions found -- access controlled by JWT auth only.

REST Resources

SchemaProperties
ProductListResourceid, image (formatted file path), smallBanner, headerColor, textColor, ord, groupId, + relations: translations, group, productLps
ProductListMuiResourceid, productListId, slug, metaTitle, metaKeywords, metaDescription, name, description, url, lang
ProductListGroupResourceid, name, slug
ProductListProductLpResourceid, ord, productListId, productId, + relation: product

Storefront Integration

Product lists appear on the storefront through two mechanisms:

1. Homepage tabs (Adv_home::homeTabs()): Configured via application/config/default.php home_tabs array. Each tab entry has a dataKey (group slug), viewKey (template variable), and limit. The controller calls product_list_model->getGroupWithProducts($slug, $limit) which fetches all lists within the group and their products. Results are cached via PSCache.

2. Homepage standalone lists (Adv_home::homeProductLists()): Configured via application/config/default.php home_productlists array. Each entry maps a template variable key to a specific product_list ID and limit. The controller calls product_list_model->getProductListById($id, $limit).

3. Category pages (optional): Gated by category_product_lists config flag (default: false in application/config/main.php). When enabled, categories can have product lists assigned via shop_product_category_lists bridge table with priority ordering. The Adv_product_categories_admin.php controller manages assignment; Adv_product_categories.php renders them on the frontend via renderCategoryProductLists().

Storefront Product Filtering

getFrontListProducts() applies visibility filters: stock > 0 OR negative_stock = 1, soft_delete = 0, active = 1, price > 0. Products are joined with vendor and product code tables. Results ordered by ord ASC.

Business Rules

RuleDescription
Group create/edit restrictedOnly AUTH_ROLE_ADVISABLE can add/edit groups
Group delete disabledCode exists but is commented out (@todo under construction)
List delete cascadingRemoves all product_list_product_lp entries, then list, then MUI records
Unique names enforcedGroup names unique in product_list_group.name; list names unique per language in product_list_mui.name
Slug auto-generationGroups only (from name via greek_lower()); lists have no auto-slug in legacy admin
Duplicate product preventionassignProductsToProductList() checks existence before insert
Drag-and-drop orderingLists within groups and products within lists, via AJAX update_order()
Image upload pathfiles/product_list/ for both image and small_banner
Image deletionCheckbox on update form sets image to empty string in DB
PSCache integrationStorefront queries cached with TTL; admin modifications do not explicitly invalidate

Extension Points

The legacy controller defines empty protected hook methods for client-repo overrides:

  • afterAddGroup($groupId)
  • afterEditGroup($groupId)
  • afterUpdateList($listId)
  • afterAddProductList($id)
  • afterRemoveList($id)
  • afterRemoveProductFromProductList($productListId, $productId)

Products admin also provides:

  • afterBatchActionSubmitAssignProductToProductList($products, $prodListId)
  • afterBatchActionSubmitRemoveFromProductList($products, $prodListId)