Appearance
Product Badges (Admin)
Flow ID: AD-25 Module(s): badges, eshop Complexity: Low-Medium Last Updated: 2026-04-04
Business Context
Product badges are visual labels (image + localized text) overlaid on product cards and product pages. They communicate marketing messages such as "New", "Sale", or "Hot" at a glance. Each product can have at most one badge, stored as a foreign key (product_img_type) on the product record. Badge display priority yields to gift rule images when both exist -- gift images take precedence over badges on the storefront.
Architecture
Dual-layer implementation: Legacy HMVC admin panel for badge CRUD and product assignment, plus a modern Domain + REST API for programmatic access.
| Layer | Component | Path |
|---|---|---|
| Legacy Controller | Adv_badges | ecommercen/badges/controllers/Adv_badges.php |
| Legacy Model | Adv_badges_model | ecommercen/badges/models/Adv_badges_model.php |
| Admin Views | list, create, update | application/views/admin/badges/ |
| Domain Entity | Entity, MuiEntity | src/Domains/Product/Badge/Repository/ |
| Domain Service | Service (read), WriteService (write) | src/Domains/Product/Badge/ |
| REST Controller | Badge | src/Rest/Product/Controllers/Badge.php |
| REST Resources | Resource, Collection, MuiResource, MuiCollection | src/Rest/Product/Resources/Badge/ |
| DI Registration | Domain container, REST container | src/Domains/Product/container.php, src/Rest/Product/container.php |
| Integration Tests | ServiceTest, RepositoryTest | tests/Integration/Domains/Product/Badge/ |
Database Schema
shop_product_badges (master)
| Column | Type | Constraints |
|---|---|---|
id | int(11) | PK, AUTO_INCREMENT |
name | varchar(100) | NOT NULL |
badge | varchar(250) | NOT NULL (image filename) |
shop_product_badges_mui (translations)
| Column | Type | Constraints |
|---|---|---|
id | int(11) | PK, AUTO_INCREMENT |
badge_id | int(11) | NOT NULL, FK to shop_product_badges.id |
badge_text | text | DEFAULT NULL |
lang | varchar(2) | NOT NULL |
Indexes: badge_id, badge_id + lang composite.
Product linkage: shop_product.product_img_type (indexed) references shop_product_badges.id. This is a soft FK -- no database-level constraint.
Admin Controller (Legacy)
Controller: Adv_badges extends Admin_c
Authorization: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MEDIA, AUTH_ROLE_PRODUCTS
| Method | Route | Description |
|---|---|---|
index() | badges | List all badges with MUI text for current admin language |
add() | badges/add | Create badge: name + image upload + per-language badge_text |
edit($id) | badges/edit/{id} | Update badge: modify name, replace image, update translations |
delete($id) | badges/delete/{id} | Delete badge (blocked if assigned to products) |
remove_badge_from_products($id) | badges/remove_badge_from_products/{id} | Clear badge from all assigned products, report affected count |
Routes (from application/config/routes.php):
badges → badges
badges/(.+) → badges/$1
(\w{2})/badges → badges (language-prefixed)
(\w{2})/badges/(.+) → badges/$2Admin Views
| View | Purpose |
|---|---|
list.php | Table with ID, name, badge_text, badge image (80px height). Actions: remove-from-products (eraser icon), edit, delete (with JS confirmation). "Add" button top-right. Scribe-how tutorial integration. |
create.php | Form: name text input, image file upload, per-language badge_text inputs with language tab switching (mixed_crud_lang_tabs partial). Multipart form encoding. |
update.php | Form: pre-populated name, optional image replacement (with "show image" popup link and delete checkbox), per-language badge_text pre-populated from MUI records. |
Badge CRUD Flow
Create:
- Admin submits form with
name,badges_imagefile, per-languagebadge_text_{lang}fields. Advuploaderlibrary handles file upload tofiles/badges/directory.- If upload succeeds: model inserts master record, then one MUI row per admin language.
afterAdd($id)hook fires (empty by default, overridable in client repos).- Redirects to badge list.
Update:
- Admin submits form. Image upload is optional (only processed if a new file is provided).
- If new image uploaded:
badgecolumn updated. If not: existing image preserved. - MUI records updated via
updateOrInsertMui()(upsert pattern fromAdv_base_model). afterEdit($id)hook fires.
Delete:
canDeleteRecord($id)checks if any product hasproduct_img_type = $id.- If products reference this badge: deletion blocked, error flashed (
badges.error.used_in_products). - If no products reference it: deletes master + MUI rows, then clears
product_img_typeon any product (safety cleanup). afterDelete($id)hook fires.
Remove from products:
- Sets
product_img_type = NULLon all products referencing the badge. - Returns affected row count via success flash message.
Product-Badge Assignment
Badges are assigned to products through the product edit form, not through the badge admin. The product admin controller (Adv_products_admin) loads badges_combo (dropdown of badge names) for the create/edit/clone views. The product_img_type field is saved as part of the product record.
Assignment points:
| Context | How badge dropdown is loaded | View |
|---|---|---|
| Product create | badges_model->dropdownName() | admin/products/create.php |
| Product edit | badges_model->dropdownName() | admin/products/update.php |
| Product clone | badges_model->dropdownName() | admin/products/clone.php |
| ERP product add | badges_model->dropdownName() | admin/products/add_erp_product.php |
| New product create | badges_model->dropdownName() | admin/products/new_products_create.php |
Batch Badge Assignment (Mass Action)
The product list admin supports a bulk action put_icon to assign/remove badges from multiple selected products at once.
- Admin selects products via checkboxes on the product list page.
- Selects "put_icon" from the Marketing optgroup in the batch action dropdown.
- A badge dropdown appears (loaded via
badges_model->dropdownName(true)with a "None" option). - Selecting "None" clears the badge; selecting a badge ID assigns it.
- Executes
product_model->update_batch_icons($products, $iconId)which runsUPDATE shop_product SET product_img_type = ? WHERE id IN (...).
Product List Search/Filter by Badge
The admin product list includes a multiselect filter for badges:
- Dropdown:
badges_model->dropDownSearchProducts(true)(includes "All" option with value0). - Search condition:
WHERE shop_product.product_img_type IN (...)added to the product query. - Badge images also displayed in the product list table rows via
badgesImageslookup map.
Storefront Display
Badges are rendered on two surfaces:
Product cards (listing pages):
Adv_front_controller::renderBadges()loadsproductBadges(id => filename map) andproductBadgesText(id => text map) viapscache(cached model calls).getTheBadge($product, $productBadges, $productBadgesText)inapplication/helpers/main_theme_helper.phpresolves the display badge:- First checks for gift rule images (product-level, then vendor-level). If found, returns
badge_type = 'gift'. - Falls back to product badge if
product_img_typeis set. Returnsbadge_type = 'badge'.
- First checks for gift rule images (product-level, then vendor-level). If found, returns
badgeImage()helper resolves CSS classes and image src differently for gift vs badge types.- Badge image rendered via
html_picture()with responsivefrom_w600srcset profile.
Product page:
- Same
getTheBadge()resolution. - Badge displayed in a card below the gallery: image on left,
badge_texton right (if non-empty). - Default theme view:
application/views/default/layouts/library/product_page/product.php.
Vue/JSON state: Adv_front_controller also populates jsonState.productBadges for Vue components.
Model Helper Methods
| Method | Returns | Used By |
|---|---|---|
getAdminList() | All badges with MUI text for current language | List view, dropdown helpers, pscache |
getAdminRecord($id) | Badge master + all MUI rows keyed by lang | Edit view |
getRecord($id) | Single badge with MUI text for current language | General lookup |
dropdownImage() | [id => filename] map | Product list badge column, storefront rendering |
dropdownText() | [id => badge_text] map | Storefront badge text display |
dropdownName($allowNone) | [id => name] map, optional "None" entry | Product edit forms, batch action |
dropDownSearchProducts($allowNone) | [id => name] map, optional "All" entry with 0 key | Product list search filter |
canDeleteRecord($id) | bool | Delete guard |
removeBadgeFromProducts($id) | int (affected rows) | Remove-from-products action, post-delete cleanup |
REST API
Base path: /rest/product/badge
Authentication: Backend JWT. Roles: AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS, AUTH_ROLE_MEDIA.
| Method | Path | Action | Description |
|---|---|---|---|
| GET | /rest/product/badge | index() | Paginated collection with filter/sort |
| GET | /rest/product/badge/item | item() | Single entity by filter criteria |
| GET | /rest/product/badge/{id} | show($id) | Single entity by ID |
| POST | /rest/product/badge | store() | Create badge (JSON or multipart with file) |
| POST | /rest/product/badge/{id} | update($id) | Update badge (JSON or multipart with file) |
| DELETE | /rest/product/badge/{id} | destroy($id) | Delete badge + MUI records |
Language-prefixed routes also available: /{lang}/rest/product/badge/...
Filters: id (exact), name (partial), badge (partial), badgeText.{locale} (partial).
Sorts: id, name, badgeText.{locale}.
Relations: translations (one-to-many, shop_product_badges_mui).
File uploads: The controller uses HandlesUploadActions trait with UploadFieldConfig('files/badges/') for the badge field. Accepts both application/json (filename only) and multipart/form-data (with file binary).
Domain Layer (Modern)
Read path: Service -> Repository -> Entity
Service::all(ListRequest)— paginated collection with Specification pattern (Filter, Sort, Pagination, WithRelations).Service::get($id, $relations)— single entity with optional relation loading.Service::item(ListRequest)— single entity by filter.EntityimplementsFilterTranslationfor cross-table filtering.
Write path: WriteService -> WriteRepository + MuiWriteRepository
WriteService::create($data)— validates viaValidator, inserts master + translations in transaction.WriteService::update($id, $data)— validates, updates master (null fields excluded), replaces translations (delete + re-insert).WriteService::delete($id)— deletes MUI first, then master, in transaction.
Validation (Validator):
- Create:
namerequired and non-empty. - Update:
namecannot be empty string (but can be omitted/null for partial update). - Translations: each entry must have
langfield.
WriteData: name (string|null), badge (string|null). OpenAPI schema Badge.
MuiWriteData: lang (string), badgeText (string|null). Accepts both badge_text and badgeText keys in input. OpenAPI schema BadgeTranslation.
Resource Transformation
BadgeResource output:
json
{
"id": 1,
"name": "New Product",
"badge": "/files/badges/new-product.png",
"translations": [...]
}The badge field is formatted via formatFile() to prepend the /files/badges/ path prefix.
BadgeMuiResource output:
json
{
"id": 1,
"badgeId": 1,
"badgeText": "New Product",
"lang": "el"
}Product Resource integration: The Product REST resource includes badge as an optional relation (addResourceToData), and exposes productImgType (integer) as a direct field.
DI Container Registration
Domain (src/Domains/Product/container.php):
Badge\Repository\RepositoryBadge\Repository\RepositoryConfigurator(definestranslationsrelation: one-to-many viabadge_id)Badge\Repository\MuiRepository(withNullRelationConfigurator)Badge\ServiceBadge\Repository\WriteRepositoryBadge\Repository\MuiWriteRepositoryBadge\ValidatorBadge\WriteService
REST (src/Rest/Product/container.php):
Badgecontroller with injected:Badge\Service,Badge\Resource,Badge\Collection,Badge\ListRequest,Badge\WriteService,UploadService.
Client Extensibility
The legacy controller provides three empty hook methods for client repo overrides:
afterAdd($badgeId)— fires after successful badge creation.afterEdit($badgeId)— fires after successful badge update.afterDelete($badgeId)— fires after successful badge deletion.
Additionally, afterBatchActionSubmitUpdateIcon($products, $iconId) is called on the product admin controller after batch badge assignment.
Client repos can extend Adv_badges in application/controllers/badges.php to override these hooks for custom logic (e.g., cache invalidation, ERP sync, audit logging).
Business Rules
| Rule | Implementation |
|---|---|
| One badge per product | product_img_type is a single FK column on shop_product |
| Deletion blocked if in use | canDeleteRecord() checks shop_product.product_img_type |
| Gift images override badges | getTheBadge() checks gift rules first, falls back to badge |
| Badge image required on create (legacy) | Upload must succeed; error flashed if upload fails |
| Badge name required (REST) | Validator::validateForCreate() enforces non-empty name |
| MUI translations require lang | Validator::validateTranslations() enforces lang on each entry |
| Badge data cached on storefront | pscache wraps model calls in renderBadges() |
| Batch assignment supports "None" | Passing empty icon_id clears product_img_type |
Test Coverage
Integration tests in tests/Integration/Domains/Product/Badge/:
RepositoryTest (14 tests): get() by ID, match() with exact/IN filters, matchOne(), count(), sort ascending/descending, pagination (first/second page), relation loading (translations), MUI match by language/badge_id, empty result handling.
ServiceTest (12 tests): all() with pagination, filter, item(), get() with relations. WriteService: create, create with translations, create validation (missing/empty name), update fields, update replaces translations, delete removes badge + MUI, full round-trip lifecycle.
Related Flows
- AD-02 Product Management -- badge assignment on product create/edit/clone
- AD-09 Gift Rules Admin -- gift rule image configuration (images override badges on storefront)
- CF-01 Product Browsing -- badge display on product cards
- CF-02 Product Detail -- badge display on product page
- CF-14 Gift Rules -- gift images override badges in display priority
- SY-23 MUI Translation Pattern --
shop_product_badges_muicompanion table stores per-languagebadge_textvalues - SY-25 File Upload & Storage --
AdvUploader(legacy) andHandlesUploadActions(REST) handle badge image uploads tofiles/badges/