Appearance
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
| Table | Purpose | Key Columns |
|---|---|---|
product_list_group | Top-level grouping containers | id, name, slug |
product_list | Individual curated lists | id, group_id (FK), image, small_banner, header_color, text_color, ord |
product_list_mui | Multi-language translations per list | id, product_list_id (FK), lang, slug, name, description, url, meta_title, meta_keywords, meta_description |
product_list_product_lp | Lookup table linking products to lists | id, product_list_id (FK), product_id (FK), ord |
shop_product_category_lists | Optional category-to-list assignments | id, category_id, product_list_id, priority |
Notable migrations:
20241125153308_add_product_list_url_field.php-- Addedurlcolumn toproduct_list_mui20241127121816_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
| Method | Route | Purpose | Auth Restriction |
|---|---|---|---|
index() | eshop/product_list_admin | List all groups | Standard |
add_group() | eshop/product_list_admin/add_group | Create group (name + auto-slug) | ADVISABLE only |
edit_group($groupId) | eshop/product_list_admin/edit_group/{id} | Edit group name | ADVISABLE 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, images | Standard |
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 product | Standard |
batchRemoveFromProductList(...) | eshop/product_list_admin/batchRemoveFromProductList/{groupId}/{listId} | Batch remove selected products | Standard |
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 reorder | Standard |
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 editorList Fields (MUI per language)
- name (required, unique per language via
is_unique_muivalidation) - 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 viaassignProductsToProductList()(deduplicates existing entries)remove_from_product_list-- Select products, choose a list (filtered to lists containing selected products), batch removes viabatchRemoveFromProductList()
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)
| Class | Path |
|---|---|
Entity | src/Domains/Product/ProductList/Repository/Entity.php |
MuiEntity | src/Domains/Product/ProductList/Repository/MuiEntity.php |
Repository | src/Domains/Product/ProductList/Repository/Repository.php |
RepositoryConfigurator | src/Domains/Product/ProductList/Repository/RepositoryConfigurator.php |
MuiRepository | src/Domains/Product/ProductList/Repository/MuiRepository.php |
MuiWriteRepository | src/Domains/Product/ProductList/Repository/MuiWriteRepository.php |
WriteRepository | src/Domains/Product/ProductList/Repository/WriteRepository.php |
Service | src/Domains/Product/ProductList/Service.php |
WriteService | src/Domains/Product/ProductList/WriteService.php |
WriteData | src/Domains/Product/ProductList/WriteData.php |
MuiWriteData | src/Domains/Product/ProductList/MuiWriteData.php |
Validator | src/Domains/Product/ProductList/Validator.php |
ListRequest | src/Domains/Product/ProductList/ListRequest.php |
FilterByTranslation | src/Domains/Product/ProductList/Repository/Specification/FilterByTranslation.php |
SortByTranslation | src/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 toMuiRepositoryviaproduct_list_idproductLps-- ONE_TO_MANY toProductLp\Repositoryviaproduct_list_idgroup-- BELONGS_TO toGroup\Repositoryviagroup_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
| Class | Path |
|---|---|
Entity | src/Domains/Product/ProductList/Group/Repository/Entity.php |
Repository | src/Domains/Product/ProductList/Group/Repository/Repository.php |
WriteRepository | src/Domains/Product/ProductList/Group/Repository/WriteRepository.php |
Service | src/Domains/Product/ProductList/Group/Service.php |
WriteService | src/Domains/Product/ProductList/Group/WriteService.php |
WriteData | src/Domains/Product/ProductList/Group/WriteData.php |
Validator | src/Domains/Product/ProductList/Group/Validator.php |
ListRequest | src/Domains/Product/ProductList/Group/ListRequest.php |
Entity properties: id, name, slugWriteData fields: name (required for create), slug (required for create)
ProductLp (product-list link)
| Class | Path |
|---|---|
Entity | src/Domains/Product/ProductList/ProductLp/Repository/Entity.php |
Repository | src/Domains/Product/ProductList/ProductLp/Repository/Repository.php |
RepositoryConfigurator | src/Domains/Product/ProductList/ProductLp/Repository/RepositoryConfigurator.php |
WriteRepository | src/Domains/Product/ProductList/ProductLp/Repository/WriteRepository.php |
Service | src/Domains/Product/ProductList/ProductLp/Service.php |
WriteService | src/Domains/Product/ProductList/ProductLp/WriteService.php |
WriteData | src/Domains/Product/ProductList/ProductLp/WriteData.php |
Validator | src/Domains/Product/ProductList/ProductLp/Validator.php |
ListRequest | src/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):
| Method | Path | Action | Notes |
|---|---|---|---|
| GET | /rest/product/product-list | index() | Paginated collection with filters/sorts |
| GET | /rest/product/product-list/item | item() | Single item by filter (e.g., slug) |
| GET | /rest/product/product-list/{id} | show($id) | Single item by ID |
| POST | /rest/product/product-list | store() | 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):
| Method | Path | Action |
|---|---|---|
| GET | /rest/product/product-list-group | index() |
| GET | /rest/product/product-list-group/item | item() |
| GET | /rest/product/product-list-group/{id} | show($id) |
| POST | /rest/product/product-list-group | store() |
| 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):
| Method | Path | Action |
|---|---|---|
| GET | /rest/product/product-list-product-lp | index() |
| GET | /rest/product/product-list-product-lp/item | item() |
| GET | /rest/product/product-list-product-lp/{id} | show($id) |
| POST | /rest/product/product-list-product-lp | store() |
| 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
| Schema | Properties |
|---|---|
ProductListResource | id, image (formatted file path), smallBanner, headerColor, textColor, ord, groupId, + relations: translations, group, productLps |
ProductListMuiResource | id, productListId, slug, metaTitle, metaKeywords, metaDescription, name, description, url, lang |
ProductListGroupResource | id, name, slug |
ProductListProductLpResource | id, 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
| Rule | Description |
|---|---|
| Group create/edit restricted | Only AUTH_ROLE_ADVISABLE can add/edit groups |
| Group delete disabled | Code exists but is commented out (@todo under construction) |
| List delete cascading | Removes all product_list_product_lp entries, then list, then MUI records |
| Unique names enforced | Group names unique in product_list_group.name; list names unique per language in product_list_mui.name |
| Slug auto-generation | Groups only (from name via greek_lower()); lists have no auto-slug in legacy admin |
| Duplicate product prevention | assignProductsToProductList() checks existence before insert |
| Drag-and-drop ordering | Lists within groups and products within lists, via AJAX update_order() |
| Image upload path | files/product_list/ for both image and small_banner |
| Image deletion | Checkbox on update form sets image to empty string in DB |
| PSCache integration | Storefront 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)
Related Flows
- AD-02 Product Management -- batch assign/remove products to/from lists
- AD-05 Category Management -- optional category-level product list assignment
- CF-01 Product Browsing -- category page product list display
- CF-26 Home Page -- homepage tab carousels and standalone product list sections
- SY-23 MUI Translation Pattern --
product_list_muicompanion table stores per-language name, slug, description, URL, and SEO fields - SY-25 File Upload & Storage --
imageandsmall_bannerfields are uploaded tofiles/product_list/via the file upload mechanism