Skip to content

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:

  1. Legacy layer -- Tag CRUD admin controller + order-tag assignment in the orders admin controller, backed by the Adv_order_tags_model legacy model.
  2. 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.

ColumnTypeConstraints
idint(10) unsignedPK, auto-increment
slugvarchar(50)UNIQUE (idx_slug)
titlevarchar(25)INDEX (idx_title)

shop_order_tags_lp -- Many-to-many link table (orders to tags).

ColumnTypeConstraints
order_idint(10) unsignedComposite PK with tag_id, INDEX (order_id)
tag_idint(10) unsignedComposite PK with order_id, INDEX (idx_tag_id)
entry_datetimestampDEFAULT 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.

ActionRouteDescription
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/addCreate 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/resetIndexClears 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).

MethodDescription
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):

  1. User selects orders in the listing, chooses "Set order tags" or "Unset order tags" from the batch action dropdown.
  2. Redirects to orders_admin/batch_action with selected order serials stored in session.
  3. Dual-listbox UI (batch_update.php) shows available tags on the left, selected on the right.
  4. On submit:
    • set_order_tags: calls assignTagsToOrders() (additive -- does not remove existing tags).
    • unset_order_tags: calls unassignTagsFromOrders() (removes only selected tags).

Per-order assignment:

  1. Hashtag button on each order row links to orders_admin/assign_tags/{orderId}.
  2. assign_tags($orderId) renders update_tags.php with dual-listbox (available vs. currently assigned).
  3. 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 #tag format.
  • Tag filter dropdown in the search form uses getAllResultsTransformed() for the multi-select.
  • The order model (Adv_order_model) LEFT JOINs shop_order_tags_lp in all listing queries and filters by tag_id when 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) → 200

Architecture 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_lp

Both 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

ClassPathPurpose
EntityRepository/Entity.phpProperties: id (int), slug (string), title (string). Extends BaseEntity.
RepositoryRepository/Repository.phpTable shop_order_tags. Specification pattern.
RepositoryConfiguratorRepository/RepositoryConfigurator.phpDefines orders relation: MANY_TO_MANY via shop_order_tags_lp (tag_id / order_id).
WriteRepositoryRepository/WriteRepository.phpInsert/update/delete on shop_order_tags.
ServiceService.phpRead operations: all() (paginated), item() (single by filter), get() (by ID). Implements ReadService, RendersPagination.
WriteServiceWriteService.phpcreate(), update(), delete(). Validates via Validator, maps via WriteData. Transactional.
ValidatorValidator.phpStub -- validateForCreate() and validateForUpdate() with empty rule sets (extensible).
WriteDataWriteData.phpDTO with slug and title. OpenAPI schema OrderTag. fromArray() factory, toArray() with optional null exclusion.
ListRequestListRequest.phpAllowed 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.

MethodHTTPPathDescription
index()GET/rest/order/order-tagCollection with filters, sorts, pagination
item()GET/rest/order/order-tag/itemSingle entity by filter criteria
show($id)GET/rest/order/order-tag/{id}Single entity by ID
store()POST/rest/order/order-tagCreate 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):

ParameterDescription
filter[id]Exact match on tag ID(s)
filter[slug]Exact match on slug
filter[title]Partial (LIKE) match on title
sortSort by id, slug, or title
pagePage number
limitItems per page
withInclude 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_admin in application/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

RuleDescription
Title uniquenessEnforced in legacy controller via titleExists(). Not enforced in REST layer Validator.
Title max length25 characters -- enforced by form validation (max_length[25]) and DB column varchar(25).
Slug auto-generationLegacy controller generates slug from title via createSlug(). REST layer accepts slug directly.
Tag deletion cascadesLegacy 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 additiveset_order_tags batch action adds tags without removing existing ones.
Bulk unassign is subtractiveunset_order_tags batch action removes only the selected tags.
Per-order assign is replaceassign_tags replaces all tags for the order (delete-then-insert).
Feature gateAll 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 -- Tests all(), 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_MANY orders relation 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.