Skip to content

Product Relations Admin

Flow ID: AD-38 | Module(s): eshop | Complexity: High Last Updated: 2026-04-04

Business Context

Product relations allow administrators to define links between products for cross-selling, upselling, and "related products" widgets on the storefront. Relations are organized into groups, each with a configurable directionality type that controls how the storefront resolves which products to display.

The system supports three relation types per group:

  • Type 1 (Left/One-way): Only the "left" product shows the "right" product as related.
  • Type 2 (Right/Reverse): Only the "right" product shows the "left" product as related.
  • Type 3 (Bidirectional): Both products in the pair see each other as related.

The platform provides both a legacy admin panel (Vue-powered AJAX interface) for managing relations and a full modern REST API with CRUD endpoints for both Related records and Related Groups.


API Reference

REST Endpoints

MethodPathActionDescription
GET/rest/product/relatedindexList related product entries (paginated)
GET/rest/product/related/itemitemGet single entry by filter
GET/rest/product/related/{id}showGet single entry by ID
POST/rest/product/relatedstoreCreate a relation
POST/rest/product/related/{id}updateUpdate a relation
DELETE/rest/product/related/{id}destroyDelete a relation
GET/rest/product/related-groupindexList related groups (paginated)
GET/rest/product/related-group/itemitemGet single group by filter
GET/rest/product/related-group/{id}showGet single group by ID
POST/rest/product/related-groupstoreCreate a group
POST/rest/product/related-group/{id}updateUpdate a group
DELETE/rest/product/related-group/{id}destroyDelete a group

Filters (Related): id (exact), productId (exact), relatedProductId (exact), groupId (exact) Relations (Related): product, relatedProduct, groupFilters (RelatedGroup): id (exact), relation (exact), name (partial) Relations (RelatedGroup): translations

Legacy Admin Routes

RouteControllerMethodDescription
product_relationsAdvProductRelationsindexRelations list page (Vue SPA)
product_relations/addAdvProductRelationsaddAdd relations page (Vue SPA)
product_relations/postAdvProductRelationspostBulk insert relations (AJAX JSON)
product_relations/apiRelatedGroupsAdvProductRelationsapiRelatedGroupsGet groups for dropdown (AJAX JSON)
product_relations/groupRelations/{groupId}AdvProductRelationsgroupRelationsGet relations within a group (AJAX JSON)
product_relations/groupsRelations/{ids...}AdvProductRelationsgroupsRelationsGet relations across multiple groups (AJAX JSON)
product_relations/removeProductFromGroupAdvProductRelationsremoveProductFromGroupRemove all of a product's relations from a group (AJAX JSON)
product_relations/removeProductFromAllGroupsAdvProductRelationsremoveProductFromAllGroupsRemove a product from all groups (AJAX JSON)
product_relations/removeRelationAdvProductRelationsremoveRelationRemove a single relation by ID (AJAX JSON)
settings/related_groupsAdvRelatedGroupsAdminindexGroups list page
settings/related_groups/addAdvRelatedGroupsAdminaddCreate group form
settings/related_groups/edit/{id}AdvRelatedGroupsAdmineditEdit group form
settings/related_groups/delete/{id}AdvRelatedGroupsAdmindeleteDelete group

Code Flow

Legacy Admin: Adding Relations

  1. Admin navigates to product_relations/add -- AdvProductRelations::add() renders the Vue component useAddProductRelationsComponent.
  2. The Vue form loads available groups via product_relations/apiRelatedGroups which returns JSON from related_product_model::getRelatedProductGroups().
  3. Admin selects a group, picks "left" products and "right" products.
  4. POST to product_relations/post with { groupId, left: [...ids], right: [...ids] }.
  5. formatPostData() validates product IDs exist in shop_product via keepExistingProductIds().
  6. validatePost() confirms the group exists via related_product_model::getGroupMaster().
  7. processPost() calls related_product_model::insertOrUpdateGroupRelations() which generates a cross-product of left x right IDs and inserts them as INSERT ... ON DUPLICATE KEY UPDATE.

Legacy Admin: Viewing Relations

  1. Admin navigates to product_relations -- AdvProductRelations::index() renders the Vue component useProductRelationsListComponent.
  2. Vue loads groups and their relations via groupRelations/{groupId} or groupsRelations/{ids}.
  3. Controller fetches raw relations via related_product_model::relatedGroupProducts(), collects all product IDs, fetches product details via product_model::getProductsByIdsOrderedFromInput(), and returns { liveData, relations }.
  4. Relations are grouped by product_id with each relation showing { groupId, product (relatedProductId), relationId }.

Legacy Admin: Managing Groups

  1. AdvRelatedGroupsAdmin::index() lists all groups with MUI names.
  2. add() / edit() show forms with a relation type dropdown (1/2/3) and MUI name fields per language.
  3. delete() checks canDeleteGroup() -- blocked if any shop_related_products rows reference the group.

Storefront Resolution

The related_product_model provides several resolution strategies:

  • leftRelationProductsFromGroup(): Type 1 -- find where this product is the product_id.
  • rightRelationProductsFromGroup(): Type 2 -- find where this product is the related_product_id.
  • leftRightRelationProductsFromGroup(): Type 3 -- find where this product appears on either side.
  • relatedProductsFromGroups(): Dispatches to the correct strategy based on the group's relation field.
  • Fallback chain in getRelatedProductsFront() (deprecated): LP relations -> Line -> Category -> Vendor.

Domain Layer

ComponentClassDescription
EntityRepository\EntityMaps shop_related_products -- id, product_id, related_product_id, group_id
RepositoryRepository\RepositoryBaseRepository, table shop_related_products
ConfiguratorRepository\RepositoryConfiguratorproduct -> ProductRepository, relatedProduct -> ProductRepository, group -> GroupRepository
ServiceServiceRead service with filter/sort/pagination
WriteServiceWriteServiceCreate/update/delete with Validator
WriteDataWriteDataproductId, relatedProductId, groupId
WriteRepositoryRepository\WriteRepositoryInsert/update/delete operations
ValidatorValidatorStub validator (no custom rules currently)
ListRequestListRequestFilter/sort parameter definitions
ComponentClassDescription
EntityRepository\EntityMaps related_groups -- id, relation
MuiEntityRepository\MuiEntityMaps related_groups_mui -- group_id, name, lang
RepositoryRepository\RepositoryBaseRepository, table related_groups
MuiRepositoryRepository\MuiRepositoryMUI repository for translations
ServiceServiceRead service
WriteServiceWriteServiceCRUD with MUI handling
WriteDataWriteDatarelation (type integer)
MuiWriteDataMuiWriteDataTranslation write data

REST Layer (src/Rest/Product/)

ComponentClassDescription
ControllerControllers\RelatedFull CRUD with HandlesWriteActions
ControllerControllers\RelatedGroupFull CRUD with HandlesWriteActions
ResourceResources\Related\ResourceOutputs id, productId, relatedProductId, groupId; includes product, relatedProduct, group relations
CollectionResources\Related\CollectionPaginated collection
ResourceResources\RelatedGroup\ResourceOutputs id, relation; includes translations
CollectionResources\RelatedGroup\CollectionPaginated collection
MuiResourceResources\RelatedGroup\MuiResourceTranslation resource
MuiCollectionResources\RelatedGroup\MuiCollectionTranslation collection

Architecture

ComponentPathPurpose
AdvProductRelationsecommercen/eshop/controllers/AdvProductRelations.phpAdmin Vue SPA controller for relation management
AdvRelatedGroupsAdminecommercen/eshop/controllers/AdvRelatedGroupsAdmin.phpAdmin CRUD for relation groups
Adv_related_product_modelecommercen/eshop/models/Adv_related_product_model.phpLegacy model (878 lines) with all query logic
Related_product_modelapplication/modules/eshop/models/Related_product_model.phpThin app-level wrapper
Domain layersrc/Domains/Product/Related/Modern DDD entities, repositories, services
REST controllerssrc/Rest/Product/Controllers/Related.php, RelatedGroup.phpOpenAPI-annotated REST endpoints
REST routesapplication/config/rest_routes.php:511-524, 1517-1530Route definitions
Admin routesapplication/config/routes.php:671-672, 197-198Legacy route definitions
Admin menuapplication/config/admin_menu.php:542-562Menu entries under settings

Data Model

ColumnTypeDescription
idint (PK)Auto-increment primary key
product_idint (FK)Source product ID
related_product_idint (FK)Target related product ID
group_idint (FK)Related group ID

Unique constraint on (group_id, product_id, related_product_id) (enforced by ON DUPLICATE KEY UPDATE).

ColumnTypeDescription
idint (PK)Auto-increment primary key
relationintRelation type: 1=left, 2=right, 3=bidirectional
ColumnTypeDescription
group_idint (FK)Related group ID
namevarcharLocalized group name
langvarcharLanguage code

Default seed data (application/config/db_default_values.php): One group (id=1, relation=3 "bidirectional") with names "Related products" (en) / "Scheta proionta" (el).


Configuration

SourceKeyDescription
Admin menuadmin.menu.related_groups.*Menu icon, label, and sub-items
Translationseshop.admin.related_products.title.*Page titles
Translationseshop.admin.related_groups.*Group CRUD labels and messages
DB defaultsdb_default_values.php related_groupsSeed data for default group

Client Extension Points

  • Override model: Extend Related_product_model in application/modules/eshop/models/ to add custom fallback strategies.
  • Override controller: Extend Product_relations in application/modules/eshop/controllers/ to customize admin behavior.
  • Custom REST overrides: Create Custom\Domains\Product\Related\ classes and DI aliases in custom/Domains/container.php.
  • Hook methods: AdvProductRelations uses processPost() as a protected method that can be overridden for custom post-processing.

Business Rules

  1. Cross-product insertion: When adding relations, every left product is paired with every right product (N x M pairs).
  2. Duplicate prevention: INSERT ... ON DUPLICATE KEY UPDATE prevents duplicate (group_id, product_id, related_product_id) entries.
  3. Product validation: Product IDs are validated against shop_product before insertion.
  4. Group validation: The group ID must exist in related_groups before relations can be added.
  5. Delete guard: A group cannot be deleted if any shop_related_products rows reference it.
  6. Directionality: The relation field on the group determines storefront resolution direction (1=left only, 2=right only, 3=bidirectional).
  7. Fallback chain (deprecated getRelatedProductsFront): LP relations -> Product line -> Category -> Vendor. Client repos should implement their own fallback logic.