Appearance
Vendor Management (Admin)
Flow ID: AD-19 Module(s): eshop, video Complexity: Medium Last Updated: 2026-04-04
Business Overview
Vendors (brands/manufacturers) are a core product taxonomy entity. Every product is assigned to exactly one vendor via shop_product.vendor_id. The admin vendor management module provides CRUD operations for vendors including multi-language names/descriptions, four banner images, logo upload, promotional and exclusive flags, builder block integration, slider assignment, supplier linkage, video association, and drag-and-drop ordering.
Vendors also serve as the primary navigation layer on the storefront: the vendor listing page shows all vendors with active products, and each vendor has a dedicated product listing page with optional category/line sub-navigation and a "details" page for exclusive vendors with rich content (builder blocks, sliders, videos).
API Reference
REST API (Modern Layer)
| Method | Endpoint | Action | Auth |
|---|---|---|---|
GET | /rest/product/vendor | List vendors (paginated, filterable) | JWT |
GET | /rest/product/vendor/{id} | Get single vendor by ID | JWT |
GET | /rest/product/vendor/item | Get single vendor by filter | JWT |
POST | /rest/product/vendor | Create new vendor | JWT |
POST | /rest/product/vendor/{id} | Update existing vendor | JWT |
DELETE | /rest/product/vendor/{id} | Delete vendor | JWT |
All endpoints support locale-prefixed variants: /{locale}/rest/product/vendor/...
Query Parameters (GET collection):
filter[id]-- exact match (comma-separated for multiple)filter[isPromo]-- boolean (0/1)filter[isExclusive]-- boolean (0/1)filter[order]-- exact matchfilter[sliderId]-- exact matchfilter[supplierId]-- exact matchfilter[name.{locale}]-- partial match (LIKE)filter[slug.{locale}]-- exact matchsort--id,isPromo,isExclusive,order,name.{locale},slug.{locale}with--translations,supplier,slider,videospage,limit-- pagination
File Uploads: POST endpoints accept multipart/form-data with file fields: logo, vendor_banner, vendor_medium_banner, vendor_small_banner. All uploaded to files/vendors/.
Admin Routes (Legacy Layer)
| Route | Controller Method | Description |
|---|---|---|
eshop/vendors_admin | index() | Vendor listing with drag-and-drop ordering |
eshop/vendors_admin/add | add() | Create vendor form |
eshop/vendors_admin/edit/{id} | edit($id) | Edit vendor form |
eshop/vendors_admin/delete/{id} | delete($id) | Delete vendor |
eshop/vendors_admin/update_order | update_order() | AJAX drag-and-drop reorder |
Auth: Requires roles ADVISABLE, ADMIN, or PRODUCTS.
Admin Menu: Located under Products > Vendors group in application/config/admin_menu.php. Shares a parent menu node with Lines (AD-17).
Frontend Routes
| Route | Controller Method | Description |
|---|---|---|
vendors | index() | All vendors listing (only those with active products) |
vendors/{slug} | vendors($slug) | Vendor product listing with category/line sub-nav |
vendors/{slug}/details | vendorsDetails($slug) | Exclusive vendor detail page (builder, slider, videos) |
vendors/{slug}/{lineSlug} | lines_list(...) | Vendor's line product listing |
xml/vendors | vendors_catalog/index | XML vendor catalog feed |
xml/doofinder/vendors | doofinder/vendorsXml/index | Doofinder vendor XML feed |
Code Flow
Admin CRUD Flow
Admin_c (Adv_vendors_admin)
|
+--> add()
| |-- validation() -- form_validation for MUI name (required), flags, banner URLs
| |-- advuploader->uploadFilesSameFolder() -- upload to files/vendors/
| |-- vendors_model->add_record($data, $dataMui) -- INSERT shop_vendor + shop_vendor_mui
| |-- video_model->assignMultipleVideosToVendor() -- sync shop_vendor_video_lp
| +-- afterAdd($id) -- hook (empty, overridable by client repos)
|
+--> edit($id)
| |-- validation(isUpdate: true) -- same rules
| |-- advuploader (conditional) -- only if new files posted
| |-- del_image/del_image2/del_image3/del_image4 -- null out image fields
| |-- vendors_model->update_record($data, $dataMui, $id) -- UPDATE + updateOrInsertMui
| |-- video_model->assignMultipleVideosToVendor() -- delete-and-reinsert
| |-- setupAiContentGenerationJsonState() -- AI content generation support
| +-- afterEdit($id) -- hook
|
+--> delete($id)
| |-- vendors_model->delete_record($id)
| | |-- canDelete($id) -- checks if any shop_product has this vendor_id
| | |-- IF products exist: set session error, skip deletion
| | +-- ELSE: DELETE shop_vendor + DELETE shop_vendor_mui
| +-- afterDelete($id) -- hook
|
+--> index()
| +-- vendors_model->get_vendors_list() -- JOIN vendor+mui, subquery for most common discount
| +-- jquery_on_load(js_listing_sortable()) -- sortable drag-and-drop UI
|
+--> update_order()
+-- vendors_model->updateOrder() -- batch UPDATE order columnREST API Flow
Vendor Controller (HandlesRestfulActions + HandlesUploadActions)
|
+--> index() -> Service->all(ListRequest) -> Repository->match(Filter, Sort, Pagination, WithRelations)
+--> show($id) -> Service->get($id, relations)
+--> item() -> Service->item(ListRequest)
+--> store() -> doStore()
| |-- UploadService handles file fields (logo, vendor_banner, vendor_medium_banner, vendor_small_banner)
| |-- WriteService->create(data)
| | |-- WriteData::fromArray() -- maps camelCase/snake_case input
| | |-- Validator->validateForCreate()
| | |-- parseMuiData() -> MuiWriteData::fromArray() per translation
| | |-- Validator->validateTranslations() -- lang required
| | +-- transactional: WriteRepository->insert() + MuiWriteRepository->insertForEntity()
| +-- Returns created entity with translations
|
+--> update($id) -> doUpdate($id)
| |-- UploadService handles replacement files
| |-- WriteService->update($id, data)
| | +-- transactional: WriteRepository->update() + MuiWriteRepository->replaceForEntity() (delete+reinsert)
| +-- Returns updated entity
|
+--> destroy($id) -> doDestroy($id)
|-- UploadService removes physical files from storage
+-- WriteService->delete($id)
+-- transactional: MuiWriteRepository->deleteForEntity() + WriteRepository->delete()Slug Generation
In the admin layer, vendor slugs are auto-generated per language from the name field using createSlug(strip_quotes($name)). Each language gets its own slug stored in shop_vendor_mui.vendor_slug. The REST API accepts slugs directly via the vendorSlug field.
Domain Layer
Namespace: Advisable\Domains\Product\Vendor
| File | Class | Purpose |
|---|---|---|
Repository/Entity.php | Entity | Base entity with FilterTranslation trait for MUI-aware filtering |
Repository/MuiEntity.php | MuiEntity | Translation entity (name, slug, fulltext, banner URLs) |
Repository/Repository.php | Repository | Read repository, table shop_vendor |
Repository/MuiRepository.php | MuiRepository | MUI read repository, table shop_vendor_mui |
Repository/RepositoryConfigurator.php | RepositoryConfigurator | Defines relations: translations, supplier, slider, videos |
Repository/WriteRepository.php | WriteRepository | Write repository for shop_vendor |
Repository/MuiWriteRepository.php | MuiWriteRepository | Write repository for shop_vendor_mui with insertForEntity/replaceForEntity/deleteForEntity |
Service.php | Service | Read service with all(), item(), get() |
WriteService.php | WriteService | Write service with create(), update(), delete() (transactional) |
ListRequest.php | ListRequest | Defines allowed filters/sorts with locale-aware MUI fields |
WriteData.php | WriteData | DTO for vendor write operations (OpenAPI-annotated) |
MuiWriteData.php | MuiWriteData | DTO for translation write operations (OpenAPI-annotated) |
Validator.php | Validator | Validation: validateForCreate, validateForUpdate, validateTranslations (lang required) |
Entity Properties
Entity (shop_vendor): id, logo, vendor_banner, vendor_medium_banner, vendor_small_banner, is_promo, is_exclusive, order, old_id, old_img_proceed, old_logo, slider_id, supplier_id
MuiEntity (shop_vendor_mui): id, vendor_id, name, vendor_slug, fulltext, exclusive_fulltext, lang, main_banner_url, middle_banner_url, small_banner_url
Additional MUI columns added by migrations: vendor_info (text, product detail page info), builder_block_id (int, page builder block), exclusive_builder_block_id (int, exclusive vendor builder block).
Configured Relations
| Name | Type | Target Repository | FK / Pivot |
|---|---|---|---|
translations | ONE_TO_MANY | MuiRepository | vendor_id |
supplier | BELONGS_TO | SupplierRepository | supplier_id |
slider | BELONGS_TO | SliderRepository | slider_id |
videos | MANY_TO_MANY | Video\Repository | shop_vendor_video_lp (vendor_id, video_id) |
REST Resource Layer
Namespace: Advisable\Rest\Product
| File | Class | Purpose |
|---|---|---|
Controllers/Vendor.php | Vendor | REST controller with full CRUD + file uploads |
Resources/Vendor/Resource.php | Resource | JSON resource: maps entity to camelCase, formats file URLs, adds relations |
Resources/Vendor/Collection.php | Collection | Collection wrapper |
Resources/Vendor/MuiResource.php | MuiResource | Translation resource: vendorId, name, slug, fulltext, exclusiveFulltext, banner URLs, lang |
Resources/Vendor/MuiCollection.php | MuiCollection | Translation collection wrapper |
Resource Output Fields: id, logo (full URL), vendorBanner, vendorMediumBanner, vendorSmallBanner, isPromo, isExclusive, priority (mapped from order), sliderId, supplierId, translations[], supplier{}, slider{}, videos[]
Architecture
Dual-Layer Architecture
The vendor domain operates across both legacy and modern layers:
- Legacy Admin:
Adv_vendors_adminextendsAdmin_c, usesvendors_model(Adv_base_model), direct CI form validation,advuploaderlibrary for file handling,BlockBuilderfor builder block integration,aiContentGenerationTraitfor AI-assisted content. - Modern REST API:
Vendorcontroller extendsHandlesRestfulActionswithHandlesUploadActionstrait, uses DI-injectedService/WriteService,UploadServicefor file handling,WriteData/MuiWriteDataDTOs with OpenAPI annotations. - Legacy Frontend:
Adv_vendorsextendsFront_c, complex routing via_remap()with vendor slug resolution, category/line sub-navigation, exclusive vendor details page with builder blocks and slider integration.
Key Integration Points
- BlockBuilder: Admin edit form renders builder block selector per language. Frontend exclusive vendor page renders block content via
blockBuilder->frontRender(). - Video: Many-to-many via
shop_vendor_video_lp. Admin manages via multiselect; frontend displays on vendor details page. - Slider: Optional slider assignment (
slider_id). Rendered on exclusive vendor details page viasliders_model->getFrontMasterRecord(). - Supplier: Optional many-to-one (
supplier_id). Suppliers can manage their vendor assignments bidirectionally. - AI Content: Edit form supports AI content generation via
aiContentGenerationTrait+setupAiContentGenerationJsonState(). - PSCache: Frontend vendor queries are wrapped in
pscache->model()/pscache->library()calls for page-level caching.
Data Model
shop_vendor
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | int(11) | NO | AUTO_INCREMENT | Primary key |
logo | text | NO | -- | Logo image filename |
vendor_banner | varchar(255) | YES | NULL | Large banner image |
vendor_medium_banner | varchar(254) | YES | NULL | Medium banner image |
vendor_small_banner | varchar(254) | YES | NULL | Small banner image |
is_promo | tinyint(1) | NO | 0 | Show on promo/homepage vendor carousel |
is_exclusive | int(2) | NO | 0 | Exclusive vendor flag (enables details page) |
order | int(11) | NO | 0 | Display sort order (drag-and-drop) |
old_id | int(11) | NO | 0 | Legacy migration ID |
old_img_proceed | int(1) | NO | 0 | Legacy image migration flag |
old_logo | text | YES | NULL | Legacy logo filename |
slider_id | int(11) | YES | NULL | FK to slider (for exclusive vendor page) |
supplier_id | int(11) | YES | NULL | FK to shop_supplier |
Indexes: PRIMARY(id), is_exclusive, is_promo, old_id
shop_vendor_mui
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | int(11) | NO | AUTO_INCREMENT | Primary key |
vendor_id | int(11) | NO | -- | FK to shop_vendor.id |
name | varchar(255) | NO | -- | Vendor display name |
vendor_slug | varchar(255) | NO | -- | URL-safe slug (auto-generated from name) |
fulltext | text | YES | NULL | Vendor description (HTML) |
exclusive_fulltext | text | YES | NULL | Exclusive vendor description |
vendor_info | text | YES | NULL | Vendor info text (shown on product detail page) |
lang | varchar(2) | NO | 'el' | Language code |
main_banner_url | varchar(255) | YES | NULL | Click-through URL for main banner |
middle_banner_url | varchar(255) | YES | NULL | Click-through URL for medium banner |
small_banner_url | varchar(255) | YES | NULL | Click-through URL for small banner |
builder_block_id | int(11) | YES | NULL | FK to builder block (vendor page content) |
exclusive_builder_block_id | int(11) | YES | NULL | FK to builder block (exclusive vendor page) |
Indexes: PRIMARY(id), lang, vendor_id, vendor_id_lang(vendor_id, lang), slug(vendor_slug), slug_lang(vendor_slug, lang), builder_block_id, exclusive_builder_block_id
shop_vendor_video_lp
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | int(11) | NO | AUTO_INCREMENT | Primary key |
video_id | int(11) | NO | -- | FK to video table |
vendor_id | int(11) | NO | -- | FK to shop_vendor.id |
Indexes: PRIMARY(id), video_id, vendor_id, video_vendor(video_id, vendor_id)
Foreign Key References
Tables referencing shop_vendor.id:
shop_product.vendor_id-- every product belongs to one vendorshop_line.vendor_id-- product lines (collections) belong to a vendorshop_vendor_mui.vendor_id-- MUI translationsshop_vendor_video_lp.vendor_id-- vendor-video associationscoupon_vendors.vendor_id-- coupon vendor restrictions
Configuration
Application Config
| File | Key | Description |
|---|---|---|
application/config/app.php | useCanonical | Enables canonical URL handling for vendor routes (TODO: rename to eshopHasVendors) |
application/config/languages.php | vendors_base_url_{lang} | Base URL segment per language (default: vendors for both el/en) |
Admin Menu Config
Vendor admin is registered in application/config/admin_menu.php under the PRODUCTS group:
- Parent menu: Vendors (icon + label from translation keys)
- Child:
eshop/vendors_admin-- Vendor listing - Sibling child:
lines_admin-- Lines listing (AD-17)
DI Container Registration
Domain (src/Domains/Product/container.php):
Vendor\Repository\Repository
Vendor\Repository\RepositoryConfigurator
Vendor\Repository\MuiRepository (with NullRelationConfigurator)
Vendor\Service
Vendor\Repository\WriteRepository
Vendor\Repository\MuiWriteRepository
Vendor\Validator
Vendor\WriteServiceREST (src/Rest/Product/container.php):
Rest\Product\Controllers\Vendor
-> $service: Vendor\Service
-> $resourceClass: Vendor\Resource
-> $collectionClass: Vendor\Collection
-> $listRequestClass: Vendor\ListRequest
-> $writeService: Vendor\WriteService
-> $uploadService: UploadServiceFile Upload Configuration
All vendor images upload to files/vendors/ via the advuploader library (admin) or UploadService (REST). Upload field config in the REST controller:
| Field Key | Upload Path |
|---|---|
logo | files/vendors/ |
vendor_banner | files/vendors/ |
vendor_medium_banner | files/vendors/ |
vendor_small_banner | files/vendors/ |
Reporting
Vendor-related reports accessible from admin menu under REPORTING > Vendors:
eshop/reporting/vendors-- Vendor sales reporteshop/reporting/vendor_products-- Vendor product performanceeshop/reporting/vendors_timespans-- Vendor sales over timeeshop/reporting/vendor_products_timespans-- Vendor product sales over time
Cache
- Admin: Cache cleared via
advisable/clearcache/product_categories_vendors(shared with categories) - Frontend: PSCache wraps all vendor model queries with configurable TTL
Client Extension Points
Overridable Admin Hooks
The Adv_vendors_admin controller provides three protected hook methods that client repos can override:
php
protected function afterAdd($id) {}
protected function afterEdit($id) {}
protected function afterDelete($id) {}Client repos extend Adv_vendors_admin in application/controllers/ and override these hooks for custom logic (e.g., cache invalidation, ERP sync, custom logging).
Overridable Frontend Hooks
The Adv_vendors frontend controller provides:
php
protected function indexExtras() {}
protected function vendorsDetailsExtra() {}Custom Domain Layer (Client Repos)
Client repos can override the vendor domain via custom/:
php
// custom/Domains/container.php
$services->set(\Custom\Domains\Product\Vendor\Repository\RepositoryConfigurator::class);
$services->alias(
\Advisable\Domains\Product\Vendor\Repository\RepositoryConfigurator::class,
\Custom\Domains\Product\Vendor\Repository\RepositoryConfigurator::class
);This allows adding custom relations, modifying filters, or extending the vendor entity behavior.
Business Rules
| Rule | Description | Code Location |
|---|---|---|
| Deletion blocked if products exist | canDelete($id) checks shop_product.vendor_id; sets session error if found | Adv_vendors_model::canDelete() |
| Name required per language | Form validation enforces `trim | requiredonname_{lang}` |
| Slug auto-generated | createSlug(strip_quotes($name)) per language on add/edit | Adv_vendors_admin::add(), edit() |
| Image deletion explicit | Must check del_image/del_image2/del_image3/del_image4 checkbox to remove | Adv_vendors_admin::edit() |
| Exclusive vendor enables details page | is_exclusive == 1 required for /vendors/{slug}/details route to respond (404 otherwise) | Adv_vendors::vendorsDetails() |
| Promo vendors appear on homepage | is_promo == 1 vendors returned by getPromoVendors() | Adv_vendors_model::getPromoVendors() |
| Frontend listing filters inactive | Only vendors with active, non-deleted products with price > 0 and stock > 0 (or negative_stock) | Adv_vendors_model::getVendorsWithActiveProductsAndValidPrices() |
| Video assignment is replace-all | assignMultipleVideosToVendor() deletes existing associations and reinserts | Adv_video_model::assignMultipleVideosToVendor() |
| REST translation update is replace-all | WriteService::update() uses replaceForEntity() (delete + reinsert all translations) | WriteService::update() |
| REST translation requires lang | Validator::validateTranslations() throws ValidationException if lang is empty | Validator::validateTranslations() |
| Builder blocks per language | Each language can have its own builder_block_id and exclusive_builder_block_id | shop_vendor_mui columns |
| Vendor info on PDP | vendor_info text from MUI is joined and displayed on product detail pages | Adv_products, Adv_product_parser_model |
| Order is drag-and-drop | update_order() accepts AJAX sortable list, batch updates order column | Adv_vendors_admin::update_order() |
Migrations
| Migration | Description |
|---|---|
20241031154616_add_vendor_block_ids.php | Added builder_block_id and exclusive_builder_block_id to shop_vendor_mui |
20250509145411_add_vendor_info_field.php | Added vendor_info TEXT column to shop_vendor_mui |
Test Coverage
Integration tests exist at:
tests/Integration/Domains/Product/Vendor/RepositoryTest.php-- Repository CRUD, filtering, sorting, pagination, relation loading, MUI queriestests/Integration/Domains/Product/Vendor/ServiceTest.php-- Service read operations, WriteService create/update/delete, translation management, round-trip test, validation exception on missing lang
Both use CiDatabaseTestCase with direct DB seeding of test vendors (IDs 99011+) to avoid conflicts with TestSeed data.
Related Flows
- AD-17 Lines Management -- Lines (series) belong to vendors; managed as child entities
- AD-20 Supplier Management -- Suppliers link to vendors via
supplier_id - AD-21 Slider Management -- Vendor sliders for exclusive pages
- AD-29 Builder Admin -- Builder blocks assigned per vendor per language (see Builder guide)
- AD-30 Video Management -- Videos associated to vendors via
shop_vendor_video_lp - AD-02 Product Management -- Products assigned to vendors; vendor dropdown on product form
- AD-47 AI Content Generation -- AI-assisted vendor description generation
- CF-01 Product Browsing -- Vendor as product filter facet
- CF-02 Product Detail -- Vendor name/logo/info on PDP
- CF-13 Coupons -- Coupon vendor restrictions via
coupon_vendors - AD-10 Reporting -- Vendor sales and product performance reports
- SY-23 MUI Translation Pattern --
shop_vendor_muicompanion table stores per-language names, slugs, descriptions, builder block IDs, and banner URLs - SY-25 File Upload & Storage --
advuploader->uploadFilesSameFolder()and RESTUploadServicehandle logo and four banner image uploads tofiles/vendors/