Skip to content

Product Lines Management (Admin)

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

Business Context

Product lines (also called "series") are merchandising groupings that sit beneath a vendor (brand). Each vendor can have multiple lines, and each line can contain multiple products. Lines provide a curated, brand-specific product collection with dedicated landing pages accessible at /vendors/{vendor_slug}/{line_slug}. They support promotional flagging, custom imagery, rich-text descriptions, per-language SEO metadata, and drag-and-drop ordering.

Lines are distinct from categories and product lists: a line is always scoped to exactly one vendor, while categories and product lists are vendor-agnostic. Products can belong to multiple lines simultaneously (many-to-many via shop_line_products).

Database Schema

Tables: shop_line, shop_line_mui, shop_line_products

TableColumnTypeNotes
shop_lineidINT PKAuto-increment
vendor_idINT NOT NULLFK to shop_vendor.id
line_imageVARCHARDetail/banner image, stored in files/lines/
line_front_imageVARCHARListing thumbnail image, stored in files/lines/
is_promoTINYINTPromotional flag (1=yes); promo lines sort first in storefront
orderINTDrag-and-drop sort position
shop_line_muiidINT PKAuto-increment
line_idINTFK to shop_line.id
langVARCHARLanguage code (e.g. el, en)
nameVARCHARLine name (required)
slugVARCHARURL-safe slug, auto-generated from name
descriptionTEXTRich-text description (TinyMCE)
meta_titleVARCHARSEO title (restricted to ADVISABLE role on create)
meta_keywordsVARCHARSEO keywords (restricted to ADVISABLE role on create)
meta_descriptionVARCHARSEO description (restricted to ADVISABLE role on create)
shop_line_productsidINT PKAuto-increment
line_idINTFK to shop_line.id
product_idINTFK to shop_product.id
orderINTSort position within line

Architecture

The lines feature spans both the legacy CI layer and the modern domain/REST layer.

Legacy Layer (admin UI):

  • Controller: ecommercen/eshop/controllers/Adv_lines_admin.php (384 lines)
  • Model: ecommercen/eshop/models/Adv_lines_model.php (416 lines)
  • Views: application/views/admin/lines/ -- list.php, create.php, update.php, products.php

Modern Domain Layer (src/Domains/Product/Line/):

  • Entity: Repository/Entity.php -- extends BaseEntity, implements FilterTranslation
  • MuiEntity: Repository/MuiEntity.php -- translation fields
  • Repository: Repository/Repository.php -- table shop_line, Specification pattern
  • MuiRepository: Repository/MuiRepository.php -- table shop_line_mui
  • RepositoryConfigurator: Repository/RepositoryConfigurator.php -- defines translations (ONE_TO_MANY), vendor (BELONGS_TO), products (MANY_TO_MANY via shop_line_products)
  • WriteRepository: Repository/WriteRepository.php -- insert/update/delete on shop_line
  • MuiWriteRepository: Repository/MuiWriteRepository.php -- insertForEntity(), replaceForEntity(), deleteForEntity() on shop_line_mui
  • Service: Service.php -- read service with all(), item(), get() using Specification pattern
  • WriteService: WriteService.php -- create(), update(), delete() with transactional MUI handling
  • WriteData: WriteData.php -- DTO for line master fields (vendorId, lineImage, lineFrontImage, isPromo, order)
  • MuiWriteData: MuiWriteData.php -- DTO for translation fields (lang, metaTitle, metaKeywords, metaDescription, slug, name, description)
  • Validator: Validator.php -- validateForCreate(), validateForUpdate(), validateTranslations() (requires non-empty lang)
  • ListRequest: ListRequest.php -- allowed filters: id, priority, isPromo, vendorId, name.{locale} (partial), slug.{locale} (exact); sorts: id, priority, name.{locale}

REST Layer (src/Rest/Product/Controllers/Line.php):

  • Extends HandlesRestfulActions with HandlesUploadActions trait
  • Full CRUD: index(), show($id), item(), store(), update($id), destroy($id)
  • File upload fields: line_image and line_front_image both stored in files/lines/
  • Resources: Resource.php, Collection.php, MuiResource.php, MuiCollection.php

DI Registration:

  • Domain services registered in src/Domains/Product/container.php (Line section: Repository, RepositoryConfigurator, MuiRepository, Service, WriteRepository, MuiWriteRepository, Validator, WriteService)
  • REST controller registered in src/Rest/Product/container.php with explicit $service, $resourceClass, $collectionClass, $listRequestClass, $writeService, $uploadService arguments
  • Both containers loaded via application/config/container/modules.php

Admin UI Flow

Authorization: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS

Admin Menu: Products section, route lines_admin, icon from admin.menu.shop.products.lines.icon

Line Listing (index)

  1. Loads all lines joined with vendor MUI names, ordered by shop_line.order ASC
  2. Displays table: ID, thumbnail image (resized to w96), line name, vendor name
  3. Vendor filter dropdown at top of listing
  4. Drag-and-drop sortable rows -- AJAX POST to lines_admin/update_order updates shop_line.order for each row
  5. Action buttons per row: live preview (opens storefront URL /vendors/{vendor_slug}/{line_slug}), edit, manage products, delete (with confirmation dialog)

Create Line (add)

  1. Multipart form with language tabs (via mixed_crud_lang_tabs partial)
  2. General Information section:
    • Name (per language, required) -- auto-generates slug on insert via createSlug()
    • Vendor (required, dropdown from vendors_model->vendorsCombo(), validated greater_than[0])
    • Is Promo (toggle switch checkbox)
    • Line Image (file upload to files/lines/)
    • Line Front Image / listing thumbnail (file upload to files/lines/)
  3. SEO section (collapsible, restricted to users with metatags access):
    • Meta Title, Meta Description, Meta Keywords (per language)
  4. Series Details section:
    • Description (per language, TinyMCE rich-text editor)
  5. On submit: validates form, uploads files via advuploader->uploadFilesSameFolder(), inserts master record + MUI records via lines_model->add_record()

Edit Line (edit/$id)

  1. Same form layout as create, pre-populated from lines_model->getAdminRecord($id)
  2. Image fields show current image link + delete checkbox (del_image, del_image2) to clear
  3. Slug editing (available to users with urlFields access): shown as readonly field with a "change" checkbox per language that unlocks editing. ADVISABLE role can force slug regeneration via change_slug checkbox.
  4. Slug auto-generation: if no slug exists and no manual slug provided, regenerated from name. Existing slugs preserved by default.
  5. On submit: updates master record + upserts MUI records via lines_model->update_record() using updateOrInsertMui() pattern

Delete Line (delete/$id)

  1. Deletes product associations from shop_line_products
  2. Deletes MUI records from shop_line_mui
  3. Deletes master record from shop_line
  4. No deletion guard -- line can be deleted even with products assigned (associations are cleaned up)

Manage Line Products (products/$lineId)

  1. Lists products assigned to the line, joined with product MUI names and thumbnail images
  2. Ordered by shop_line_products.order ASC
  3. Drag-and-drop sortable -- AJAX POST to lines_admin/update_products_order
  4. Select-all checkbox for bulk operations
  5. Batch remove: form POSTs selected product IDs to batchRemoveFromLine/$lineId -- removes entries from shop_line_products
  6. Per-product actions: edit product (redirects to products_admin/edit/{product_id}), delete association (single product)

Product-to-Line Assignment

Lines are also managed from the product edit screen:

  • Product edit/clone form (Adv_products_admin.php): Multi-select dropdown (line_ids[]) populated from lines_model->dropdown(). On save, calls manageProductLines($id) which delegates to lines_model->updateProductLines($productId, $lineIds) -- deletes all existing shop_line_products rows for the product, then batch-inserts the new selection.

  • Batch action from product listing: The "put in lines" batch action posts line_id and calls lines_model->update_batch_product_line($products, $lineId) -- inserts into shop_line_products only if the product-line pair does not already exist (de-duplicated).

  • Product clone: manageCloneProductLines() copies all line associations from the source product to the clone via getLineIdsByProductId() + updateProductLines().

REST API Endpoints

All endpoints require JWT authentication.

MethodPathActionDescription
GET/rest/product/lineindexPaginated collection with filters and sorts
GET/rest/product/line/itemitemSingle resource by filter criteria
GET/rest/product/line/{id}showSingle resource by ID
POST/rest/product/linestoreCreate new line (JSON or multipart with file uploads)
POST/rest/product/line/{id}updateUpdate existing line (JSON or multipart)
DELETE/rest/product/line/{id}destroyDelete line + translations

Locale-prefixed variants (/{lang}/rest/product/line/...) also registered.

Available filters: id (exact), priority (exact), isPromo (exact), vendorId (exact), name.{locale} (partial/LIKE), slug.{locale} (exact)

Available sorts: id, priority, name.{locale}

Available relations (via ?with=): translations, vendor, products

Resource output (ProductLineResource):

json
{
  "id": 1,
  "vendorId": 42,
  "image": "https://cdn.example.com/files/lines/banner.jpg",
  "frontImage": "https://cdn.example.com/files/lines/thumb.jpg",
  "isPromo": false,
  "priority": 0,
  "translations": [...],
  "vendor": {...},
  "products": [...]
}

Feed Integration

Lines Catalog XML (ecommercen/feeds/controllers/AdvLinesCatalog.php):

  • Route: /xml/lines
  • Uses OutputLinesXml feed output class (src/Feeds/Output/OutputLinesXml.php)
  • Actually outputs vendor data (not individual lines) via vendors_model->getVendorsForXml()
  • XML structure: <lines> root, <line> items with id, title, url, discount, items

Business Rules

RuleDescription
Vendor requiredEvery line must belong to exactly one vendor (vendor_id > 0 validated)
Many-to-many productsProducts can belong to multiple lines; lines can contain multiple products
Promo sortingLines with is_promo = 1 sort before non-promo lines on storefront (shop_line.is_promo, shop_line.order asc)
Auto-slug generationOn create, slug generated from line name per language. On edit, existing slugs preserved unless explicitly changed.
Slug editing restrictedOnly users with urlFields access can manually edit slugs. ADVISABLE role has additional slug regeneration control.
SEO fields restrictedMeta title/keywords/description editable only for ADVISABLE role on create; on edit accessible to users with metatags permission
Image storageBoth images stored in files/lines/ directory
Cascade deleteDeleting a line removes all product associations and MUI records
No deletion guardLines can be deleted regardless of product count (unlike vendors)
Product orderingProducts within a line have independent sort order via shop_line_products.order
Product assignment from product editProduct can be assigned to lines via multi-select on the product edit form; delete-and-reinsert pattern
Product cloneLine associations copied when cloning a product

Storefront Behavior

Lines are displayed within vendor pages at /vendors/{vendor_slug}/{line_slug}:

  • The Adv_vendors controller (ecommercen/eshop/controllers/Adv_vendors.php) checks if the path after the vendor slug matches a line slug
  • If a line record is found for the given slug and vendor, it renders the line listing page with products
  • Cached via pscache for performance

Sitemap Integration

lines_model->getLinesForSiteMap() returns all line URLs (vendor_slug + line slug) for sitemap generation.

Test Coverage

Unit tests: tests/Unit/Domains/Product/Line/ServiceTest.php -- 8 tests covering all(), item(), get() with mocked repository, pagination assertions

Integration tests:

  • tests/Integration/Domains/Product/Line/RepositoryTest.php -- 14 tests covering get(), match(), matchOne(), count(), sorting, pagination, relation loading (translations, vendor), MUI repository queries
  • tests/Integration/Domains/Product/Line/ServiceTest.php -- 10 tests covering full read/write round-trip: create() with/without translations, update() field modification and translation replacement, delete() cascade, validation error on empty lang, and a complete create-update-delete round-trip test

Key File Paths

ComponentPath
Admin Controllerecommercen/eshop/controllers/Adv_lines_admin.php
Legacy Modelecommercen/eshop/models/Adv_lines_model.php
Admin Viewsapplication/views/admin/lines/{list,create,update,products}.php
Entitysrc/Domains/Product/Line/Repository/Entity.php
MuiEntitysrc/Domains/Product/Line/Repository/MuiEntity.php
Repositorysrc/Domains/Product/Line/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Product/Line/Repository/RepositoryConfigurator.php
WriteRepositorysrc/Domains/Product/Line/Repository/WriteRepository.php
MuiWriteRepositorysrc/Domains/Product/Line/Repository/MuiWriteRepository.php
Servicesrc/Domains/Product/Line/Service.php
WriteServicesrc/Domains/Product/Line/WriteService.php
WriteDatasrc/Domains/Product/Line/WriteData.php
MuiWriteDatasrc/Domains/Product/Line/MuiWriteData.php
Validatorsrc/Domains/Product/Line/Validator.php
ListRequestsrc/Domains/Product/Line/ListRequest.php
REST Controllersrc/Rest/Product/Controllers/Line.php
REST Resourcesrc/Rest/Product/Resources/Line/Resource.php
REST Collectionsrc/Rest/Product/Resources/Line/Collection.php
REST MuiResourcesrc/Rest/Product/Resources/Line/MuiResource.php
REST MuiCollectionsrc/Rest/Product/Resources/Line/MuiCollection.php
Domain DI Containersrc/Domains/Product/container.php (Line section)
REST DI Containersrc/Rest/Product/container.php (Line entry)
REST Routesapplication/config/rest_routes.php (lines 448-453, 1444-1450)
Admin Routesapplication/config/routes.php (lines 323-326)
Admin Menuapplication/config/admin_menu.php (line 444)
Feed Controllerecommercen/feeds/controllers/AdvLinesCatalog.php
Feed Outputsrc/Feeds/Output/OutputLinesXml.php
Unit Teststests/Unit/Domains/Product/Line/ServiceTest.php
Integration Teststests/Integration/Domains/Product/Line/{ServiceTest,RepositoryTest}.php