Appearance
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
| Method | Path | Action | Description |
|---|---|---|---|
| GET | /rest/product/related | index | List related product entries (paginated) |
| GET | /rest/product/related/item | item | Get single entry by filter |
| GET | /rest/product/related/{id} | show | Get single entry by ID |
| POST | /rest/product/related | store | Create a relation |
| POST | /rest/product/related/{id} | update | Update a relation |
| DELETE | /rest/product/related/{id} | destroy | Delete a relation |
| GET | /rest/product/related-group | index | List related groups (paginated) |
| GET | /rest/product/related-group/item | item | Get single group by filter |
| GET | /rest/product/related-group/{id} | show | Get single group by ID |
| POST | /rest/product/related-group | store | Create a group |
| POST | /rest/product/related-group/{id} | update | Update a group |
| DELETE | /rest/product/related-group/{id} | destroy | Delete 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
| Route | Controller | Method | Description |
|---|---|---|---|
product_relations | AdvProductRelations | index | Relations list page (Vue SPA) |
product_relations/add | AdvProductRelations | add | Add relations page (Vue SPA) |
product_relations/post | AdvProductRelations | post | Bulk insert relations (AJAX JSON) |
product_relations/apiRelatedGroups | AdvProductRelations | apiRelatedGroups | Get groups for dropdown (AJAX JSON) |
product_relations/groupRelations/{groupId} | AdvProductRelations | groupRelations | Get relations within a group (AJAX JSON) |
product_relations/groupsRelations/{ids...} | AdvProductRelations | groupsRelations | Get relations across multiple groups (AJAX JSON) |
product_relations/removeProductFromGroup | AdvProductRelations | removeProductFromGroup | Remove all of a product's relations from a group (AJAX JSON) |
product_relations/removeProductFromAllGroups | AdvProductRelations | removeProductFromAllGroups | Remove a product from all groups (AJAX JSON) |
product_relations/removeRelation | AdvProductRelations | removeRelation | Remove a single relation by ID (AJAX JSON) |
settings/related_groups | AdvRelatedGroupsAdmin | index | Groups list page |
settings/related_groups/add | AdvRelatedGroupsAdmin | add | Create group form |
settings/related_groups/edit/{id} | AdvRelatedGroupsAdmin | edit | Edit group form |
settings/related_groups/delete/{id} | AdvRelatedGroupsAdmin | delete | Delete group |
Code Flow
Legacy Admin: Adding Relations
- Admin navigates to
product_relations/add--AdvProductRelations::add()renders the Vue componentuseAddProductRelationsComponent. - The Vue form loads available groups via
product_relations/apiRelatedGroupswhich returns JSON fromrelated_product_model::getRelatedProductGroups(). - Admin selects a group, picks "left" products and "right" products.
- POST to
product_relations/postwith{ groupId, left: [...ids], right: [...ids] }. formatPostData()validates product IDs exist inshop_productviakeepExistingProductIds().validatePost()confirms the group exists viarelated_product_model::getGroupMaster().processPost()callsrelated_product_model::insertOrUpdateGroupRelations()which generates a cross-product of left x right IDs and inserts them asINSERT ... ON DUPLICATE KEY UPDATE.
Legacy Admin: Viewing Relations
- Admin navigates to
product_relations--AdvProductRelations::index()renders the Vue componentuseProductRelationsListComponent. - Vue loads groups and their relations via
groupRelations/{groupId}orgroupsRelations/{ids}. - Controller fetches raw relations via
related_product_model::relatedGroupProducts(), collects all product IDs, fetches product details viaproduct_model::getProductsByIdsOrderedFromInput(), and returns{ liveData, relations }. - Relations are grouped by
product_idwith each relation showing{ groupId, product (relatedProductId), relationId }.
Legacy Admin: Managing Groups
AdvRelatedGroupsAdmin::index()lists all groups with MUI names.add()/edit()show forms with a relation type dropdown (1/2/3) and MUI name fields per language.delete()checkscanDeleteGroup()-- blocked if anyshop_related_productsrows reference the group.
Storefront Resolution
The related_product_model provides several resolution strategies:
leftRelationProductsFromGroup(): Type 1 -- find where this product is theproduct_id.rightRelationProductsFromGroup(): Type 2 -- find where this product is therelated_product_id.leftRightRelationProductsFromGroup(): Type 3 -- find where this product appears on either side.relatedProductsFromGroups(): Dispatches to the correct strategy based on the group'srelationfield.- Fallback chain in
getRelatedProductsFront()(deprecated): LP relations -> Line -> Category -> Vendor.
Domain Layer
Related (src/Domains/Product/Related/)
| Component | Class | Description |
|---|---|---|
| Entity | Repository\Entity | Maps shop_related_products -- id, product_id, related_product_id, group_id |
| Repository | Repository\Repository | BaseRepository, table shop_related_products |
| Configurator | Repository\RepositoryConfigurator | product -> ProductRepository, relatedProduct -> ProductRepository, group -> GroupRepository |
| Service | Service | Read service with filter/sort/pagination |
| WriteService | WriteService | Create/update/delete with Validator |
| WriteData | WriteData | productId, relatedProductId, groupId |
| WriteRepository | Repository\WriteRepository | Insert/update/delete operations |
| Validator | Validator | Stub validator (no custom rules currently) |
| ListRequest | ListRequest | Filter/sort parameter definitions |
Related Group (src/Domains/Product/Related/Group/)
| Component | Class | Description |
|---|---|---|
| Entity | Repository\Entity | Maps related_groups -- id, relation |
| MuiEntity | Repository\MuiEntity | Maps related_groups_mui -- group_id, name, lang |
| Repository | Repository\Repository | BaseRepository, table related_groups |
| MuiRepository | Repository\MuiRepository | MUI repository for translations |
| Service | Service | Read service |
| WriteService | WriteService | CRUD with MUI handling |
| WriteData | WriteData | relation (type integer) |
| MuiWriteData | MuiWriteData | Translation write data |
REST Layer (src/Rest/Product/)
| Component | Class | Description |
|---|---|---|
| Controller | Controllers\Related | Full CRUD with HandlesWriteActions |
| Controller | Controllers\RelatedGroup | Full CRUD with HandlesWriteActions |
| Resource | Resources\Related\Resource | Outputs id, productId, relatedProductId, groupId; includes product, relatedProduct, group relations |
| Collection | Resources\Related\Collection | Paginated collection |
| Resource | Resources\RelatedGroup\Resource | Outputs id, relation; includes translations |
| Collection | Resources\RelatedGroup\Collection | Paginated collection |
| MuiResource | Resources\RelatedGroup\MuiResource | Translation resource |
| MuiCollection | Resources\RelatedGroup\MuiCollection | Translation collection |
Architecture
| Component | Path | Purpose |
|---|---|---|
AdvProductRelations | ecommercen/eshop/controllers/AdvProductRelations.php | Admin Vue SPA controller for relation management |
AdvRelatedGroupsAdmin | ecommercen/eshop/controllers/AdvRelatedGroupsAdmin.php | Admin CRUD for relation groups |
Adv_related_product_model | ecommercen/eshop/models/Adv_related_product_model.php | Legacy model (878 lines) with all query logic |
Related_product_model | application/modules/eshop/models/Related_product_model.php | Thin app-level wrapper |
| Domain layer | src/Domains/Product/Related/ | Modern DDD entities, repositories, services |
| REST controllers | src/Rest/Product/Controllers/Related.php, RelatedGroup.php | OpenAPI-annotated REST endpoints |
| REST routes | application/config/rest_routes.php:511-524, 1517-1530 | Route definitions |
| Admin routes | application/config/routes.php:671-672, 197-198 | Legacy route definitions |
| Admin menu | application/config/admin_menu.php:542-562 | Menu entries under settings |
Data Model
shop_related_products
| Column | Type | Description |
|---|---|---|
id | int (PK) | Auto-increment primary key |
product_id | int (FK) | Source product ID |
related_product_id | int (FK) | Target related product ID |
group_id | int (FK) | Related group ID |
Unique constraint on (group_id, product_id, related_product_id) (enforced by ON DUPLICATE KEY UPDATE).
related_groups
| Column | Type | Description |
|---|---|---|
id | int (PK) | Auto-increment primary key |
relation | int | Relation type: 1=left, 2=right, 3=bidirectional |
related_groups_mui
| Column | Type | Description |
|---|---|---|
group_id | int (FK) | Related group ID |
name | varchar | Localized group name |
lang | varchar | Language 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
| Source | Key | Description |
|---|---|---|
| Admin menu | admin.menu.related_groups.* | Menu icon, label, and sub-items |
| Translations | eshop.admin.related_products.title.* | Page titles |
| Translations | eshop.admin.related_groups.* | Group CRUD labels and messages |
| DB defaults | db_default_values.php related_groups | Seed data for default group |
Client Extension Points
- Override model: Extend
Related_product_modelinapplication/modules/eshop/models/to add custom fallback strategies. - Override controller: Extend
Product_relationsinapplication/modules/eshop/controllers/to customize admin behavior. - Custom REST overrides: Create
Custom\Domains\Product\Related\classes and DI aliases incustom/Domains/container.php. - Hook methods:
AdvProductRelationsusesprocessPost()as a protected method that can be overridden for custom post-processing.
Business Rules
- Cross-product insertion: When adding relations, every left product is paired with every right product (N x M pairs).
- Duplicate prevention:
INSERT ... ON DUPLICATE KEY UPDATEprevents duplicate(group_id, product_id, related_product_id)entries. - Product validation: Product IDs are validated against
shop_productbefore insertion. - Group validation: The group ID must exist in
related_groupsbefore relations can be added. - Delete guard: A group cannot be deleted if any
shop_related_productsrows reference it. - Directionality: The
relationfield on the group determines storefront resolution direction (1=left only, 2=right only, 3=bidirectional). - Fallback chain (deprecated
getRelatedProductsFront): LP relations -> Product line -> Category -> Vendor. Client repos should implement their own fallback logic.
Related Flows
- AD-02 Product Management Admin -- Product admin page includes a related products tab using
getAdminProductPageRelatedProductsWithGroups() - CF-02 Product Detail -- Storefront renders related product widgets using the resolution strategies
- CF-12 Promotions Front -- Cross-sell/upsell groups can be used as promotional widgets
- AD-56 Recommendation Algorithm Priority -- algorithm priority that references related product groups
- SY-23 MUI Translation Pattern — related_groups_mui companion table