Appearance
Are you an LLM? You can read better optimized documentation at /flows/admin/AD-28-order-tags.md for this page in Markdown format
Order Tags (Admin)
Flow ID: AD-28 Module(s): eshop, Order (domain) Complexity: Low-Medium Last Updated: 2026-04-04
Business Context
Order tags are flat, user-defined labels for categorizing orders. Admins create tags (e.g. "VIP", "Express", "Bulk") and assign them to orders individually or in bulk. Tags appear in the order listing and serve as filter criteria for order searches. The entire feature is gated by the ORDER_TAGS_ENABLED registry setting under the OTHER group.
Architecture
Two-layer implementation:
- Legacy layer -- Tag CRUD admin controller + order-tag assignment in the orders admin controller, backed by the
Adv_order_tags_modellegacy model. - Modern domain layer -- Full DDD stack under
src/Domains/Order/OrderTag/with read Service, WriteService, and REST API.
Both layers operate on the same database tables. The legacy layer handles the admin UI (server-rendered views), while the modern layer exposes a REST API for programmatic/frontend consumption.
Database Schema
shop_order_tags -- Tag definitions.
| Column | Type | Constraints |
|---|---|---|
id | int(10) unsigned | PK, auto-increment |
slug | varchar(50) | UNIQUE (idx_slug) |
title | varchar(25) | INDEX (idx_title) |
shop_order_tags_lp -- Many-to-many link table (orders to tags).
| Column | Type | Constraints |
|---|---|---|
order_id | int(10) unsigned | Composite PK with tag_id, INDEX (order_id) |
tag_id | int(10) unsigned | Composite PK with order_id, INDEX (idx_tag_id) |
entry_date | timestamp | DEFAULT current_timestamp() |
No MUI companion table -- tags are single-language (title only, max 25 characters). An additional index on order_id was added in migration 20250515115441_admin_orders_performance.php for query performance.
Feature Gate
Registry key: OTHER / ORDER_TAGS_ENABLED (integer 0/1).
Controlled from Admin > Settings > Other Settings via Adv_settings.php. When disabled:
- Admin menu hides the "Order Tags" entry (
AdminMenu::parseOrderTags()filters it out). - Order listing hides tag filter dropdown, tag display columns, and batch tag actions.
- Per-order tag assignment button is hidden.
assign_tags()in the orders admin redirects back if feature is off.
Legacy Admin Controller
ecommercen/eshop/controllers/Adv_order_tags_admin.php (143 lines)
Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_ORDERS -- plus ORDER_TAGS_ENABLED check.
| Action | Route | Description |
|---|---|---|
index($offset) | order_tags, order_tags/{offset} | Paginated list (50/page) with keyword search on title and id. Session-persisted search via tagsSearch. |
add() | order_tags/add | Create form. Title field, max 25 chars. Auto-generates slug via createSlug(). Duplicate title check. |
edit($id) | order_tags/edit/{id} | Edit form. Same validation + duplicate check (excluding current record). |
delete($id) | order_tags/delete/{id} | Deletes tag AND all link-table entries in a transaction. |
resetIndex() | order_tags/resetIndex | Clears session search state. |
Views (in application/views/admin/order_tags/):
list.php-- Table with ID, title, edit/delete actions. Keyword search form. Pagination.create.php-- Single text input for title (maxlength 25).update.php-- Same form pre-populated with existing title.
Legacy Model
ecommercen/eshop/models/Adv_order_tags_model.php (178 lines)
Tables: shop_order_tags (tags), shop_order_tags_lp (link table).
| Method | Description |
|---|---|
getRecords($conditions, $limit, $offset) | Paginated list with optional keyword LIKE search. Returns {total_count, records}. |
getAllRecords() | All tags as object array. |
getAllResultsTransformed() | All tags as [id => title] map -- used for dropdowns. |
getRecord($id) | Single tag by ID. |
addRecord($data) | Insert, returns insert ID. |
updateRecord($id, $data) | Update by ID. |
deleteRecord($id) | Transaction: delete from link table by tag_id, then delete tag. |
titleExists($title, $excludeId) | Duplicate title check (optionally excluding an ID). |
assignTagsToOrders($orderIds, $tagIds) | Additive: inserts into link table, skipping existing pairs. |
unassignTagsFromOrders($orderIds, $tagIds) | Removes specific tag-order pairs from link table. |
resetAndAssignTagsToOrders($orderIds, $tagIds) | Replace mode: deletes all existing tags for given orders, then batch-inserts new set. Transaction with rollback. |
getOrderTags($orderId) | Returns [tagId => title] map for a single order. |
getTagsForOrders($orders) | Enriches an array of order objects with ->tags property. |
Order-Tag Assignment (Orders Admin)
ecommercen/eshop/controllers/Adv_orders_admin.php -- integrates tags into order management.
Bulk assignment (batch actions):
- User selects orders in the listing, chooses "Set order tags" or "Unset order tags" from the batch action dropdown.
- Redirects to
orders_admin/batch_actionwith selected order serials stored in session. - Dual-listbox UI (
batch_update.php) shows available tags on the left, selected on the right. - On submit:
set_order_tags: callsassignTagsToOrders()(additive -- does not remove existing tags).unset_order_tags: callsunassignTagsFromOrders()(removes only selected tags).
Per-order assignment:
- Hashtag button on each order row links to
orders_admin/assign_tags/{orderId}. assign_tags($orderId)rendersupdate_tags.phpwith dual-listbox (available vs. currently assigned).- On submit: calls
resetAndAssignTagsToOrders()-- replaces all tags for that order with the new selection.
Order listing integration:
- Tags are loaded for each order via
getTagsForOrders()after the main query. - Each order row displays tags in a horizontal slider (
native_slider) with#tagformat. - Tag filter dropdown in the search form uses
getAllResultsTransformed()for the multi-select. - The order model (
Adv_order_model) LEFT JOINsshop_order_tags_lpin all listing queries and filters bytag_idwhen tag search is active.
Code Flow Diagrams
Tag CRUD (Admin)
Adv_order_tags_admin::add() / edit()
|
+--> form_validation: title required, max_length[25]
+--> Check titleExists() -- prevents duplicate names
+--> Generate slug: createSlug(strip_quotes($title))
+--> order_tags_model->addRecord() or updateRecord()Tag Assignment to Orders
Adv_orders_admin (bulk action) or order detail view
|
+--> order_tags_model->assignTagsToOrders($orderIds, $tagIds)
| (inserts into shop_order_tags_lp, skips existing pairs)
|
+--> order_tags_model->unassignTagsFromOrders($orderIds, $tagIds)
| (removes from shop_order_tags_lp)
|
+--> order_tags_model->resetAndAssignTagsToOrders($orderIds, $tagIds)
(delete-all-then-insert pattern, transactional)REST CRUD
OrderTag Controller (HandlesRestfulActions + HandlesWriteActions)
|
+--> index(): Service->all(ListRequest) → Collection
+--> show($id): Service->get($id) → Resource
+--> store(): WriteService->create(data) → Resource (201)
+--> update($id): WriteService->update($id, data) → Resource
+--> destroy($id): WriteService->delete($id) → 200Architecture Diagram
Admin UI (Adv_order_tags_admin) REST API (OrderTag controller)
| |
+--> Adv_order_tags_model +--> OrderTag\Service
(legacy CI model) OrderTag\WriteService
| |
v v
shop_order_tags <----------> shop_order_tags
shop_order_tags_lp shop_order_tags_lpBoth layers operate on the same database tables. The legacy admin provides the UI for tag CRUD and bulk order assignment. The modern REST layer provides programmatic CRUD and read access with relation loading (orders).
Modern Domain Layer
Namespace: Advisable\Domains\Order\OrderTag
| Class | Path | Purpose |
|---|---|---|
Entity | Repository/Entity.php | Properties: id (int), slug (string), title (string). Extends BaseEntity. |
Repository | Repository/Repository.php | Table shop_order_tags. Specification pattern. |
RepositoryConfigurator | Repository/RepositoryConfigurator.php | Defines orders relation: MANY_TO_MANY via shop_order_tags_lp (tag_id / order_id). |
WriteRepository | Repository/WriteRepository.php | Insert/update/delete on shop_order_tags. |
Service | Service.php | Read operations: all() (paginated), item() (single by filter), get() (by ID). Implements ReadService, RendersPagination. |
WriteService | WriteService.php | create(), update(), delete(). Validates via Validator, maps via WriteData. Transactional. |
Validator | Validator.php | Stub -- validateForCreate() and validateForUpdate() with empty rule sets (extensible). |
WriteData | WriteData.php | DTO with slug and title. OpenAPI schema OrderTag. fromArray() factory, toArray() with optional null exclusion. |
ListRequest | ListRequest.php | Allowed filters: id (exact), slug (exact), title (partial). Allowed sorts: id, slug, title. |
Bidirectional relation: The Order\Order\Repository\RepositoryConfigurator also defines a tags relation (MANY_TO_MANY to OrderTagRepository via shop_order_tags_lp), so orders can be fetched with their tags via ?with=tags.
DI container: Registered in src/Domains/Order/container.php (Repository, RepositoryConfigurator, Service, WriteRepository, Validator, WriteService).
REST API Layer
Controller: Advisable\Rest\Order\Controllers\OrderTag -- extends HandlesRestfulActions, uses HandlesWriteActions.
| Method | HTTP | Path | Description |
|---|---|---|---|
index() | GET | /rest/order/order-tag | Collection with filters, sorts, pagination |
item() | GET | /rest/order/order-tag/item | Single entity by filter criteria |
show($id) | GET | /rest/order/order-tag/{id} | Single entity by ID |
store() | POST | /rest/order/order-tag | Create new tag |
update($id) | POST | /rest/order/order-tag/{id} | Update existing tag |
destroy($id) | DELETE | /rest/order/order-tag/{id} | Delete tag |
All routes support language prefix (/{lang}/rest/...).
Query parameters (GET):
| Parameter | Description |
|---|---|
filter[id] | Exact match on tag ID(s) |
filter[slug] | Exact match on slug |
filter[title] | Partial (LIKE) match on title |
sort | Sort by id, slug, or title |
page | Page number |
limit | Items per page |
with | Include relations (e.g. orders) |
Resource (Resources/OrderTag/Resource.php): Returns {id, slug, title}. Conditionally includes orders collection when relation is loaded.
Collection (Resources/OrderTag/Collection.php): Array of OrderTagResource items.
Auth policy (rest_policies.php): Backend-only, roles AUTH_ROLE_ADMIN and AUTH_ROLE_ORDERS.
DI container: Registered in src/Rest/Order/container.php with explicit constructor args for Service, Resource, Collection, ListRequest, and WriteService.
Admin Menu
Configured in application/config/admin_menu.php:
php
[
'route' => 'order_tags',
'icon' => t('admin.menu.order_tags.listing.icon'),
'label' => t('admin.menu.order_tags.listing.label'),
'roles' => [AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_ORDERS],
'group' => 'ORDERS'
]Dynamically excluded by AdminMenu::parseOrderTags() when ORDER_TAGS_ENABLED is falsy.
Relation Configuration
The RepositoryConfigurator defines a MANY_TO_MANY relation:
- Join table:
shop_order_tags_lp - Local key:
tag_id - Foreign key:
order_id - Target:
Order\Repository
This allows loading associated orders when requesting tags with ?with=orders.
Client Extension Points
- Custom tag logic: Override
Adv_order_tags_admininapplication/controllers/for custom behavior - REST extensions: The modern domain can be extended via
Custom\Domains\Order\OrderTag\in client repos - Additional tag fields: Would require migration + entity update in both legacy model and domain entity
Business Rules
| Rule | Description |
|---|---|
| Title uniqueness | Enforced in legacy controller via titleExists(). Not enforced in REST layer Validator. |
| Title max length | 25 characters -- enforced by form validation (max_length[25]) and DB column varchar(25). |
| Slug auto-generation | Legacy controller generates slug from title via createSlug(). REST layer accepts slug directly. |
| Tag deletion cascades | Legacy controller deletes link-table entries before the tag in a transaction. REST WriteService only deletes from shop_order_tags (no cascade to link table). |
| Bulk assign is additive | set_order_tags batch action adds tags without removing existing ones. |
| Bulk unassign is subtractive | unset_order_tags batch action removes only the selected tags. |
| Per-order assign is replace | assign_tags replaces all tags for the order (delete-then-insert). |
| Feature gate | All UI and filtering requires ORDER_TAGS_ENABLED registry flag. REST API is not gated by this flag. |
Test Coverage
Unit tests:
tests/Unit/Domains/Order/OrderTag/ServiceTest.php-- Testsall(),item(),get()with mocked repository. Validates pagination metadata (current_page, total, has_next, has_prev).tests/Unit/Rest/Order/Resources/OrderTag/ResourceTest.php-- Tests resource transformation, relation inclusion/exclusion, null handling.tests/Unit/Rest/Order/Resources/OrderTag/CollectionTest.php-- Tests collection transformation, empty input, field types.
Integration tests:
tests/Integration/Domains/Order/OrderTag/RepositoryTest.php-- Tests get, match (exact/IN filter), matchOne, count, sort (asc/desc), pagination, and the MANY_TO_MANYordersrelation loading via link table.tests/Integration/Domains/Order/OrderTag/ServiceTest.php-- Full round-trip: create, update (title and slug independently), delete. Validates all read operations against real DB with seeded data.
Related Flows
- AD-03 Order Management -- Order listing, batch actions, per-order tag assignment
- AD-37 Metadata / Registry System -- Feature flag via Registry
OTHER.ORDER_TAGS_ENABLED