Appearance
Promotions (Storefront)
Flow ID: CF-12 | Module(s): eshop, Product (domain) | Complexity: Medium
Business Overview
Promotional landing pages allow marketing teams to create curated product collections accessible via friendly URLs. Each promo page is a standalone marketing landing with its own visual identity, SEO metadata, and product selection. Promos serve multiple business purposes: seasonal campaigns, brand-specific showcases, clearance events, and featured product collections.
What customers see:
- A promo landing page at
/promo/{slug}with a product grid - Responsive banner images (separate desktop and mobile banners)
- Rich HTML description content above and below the product grid
- Optional block builder layout for complex visual arrangements
- Custom header and text color theming per promo
- Product cards with full pricing, discount, stock, and variation data
- Category sidebar navigation (unless overridden by client)
What the business configures (via admin):
- Promo master record with visual settings (view type, colors, images)
- Per-language translations: name, slug, description, footer description, SEO meta tags
- Per-language builder block associations for rich layouts
- Curated product list with manual ordering (drag-and-drop)
- SEO dofollow toggle to control search engine indexing
- Products can be batch-added or batch-removed from promos via the products admin
Additional promo touchpoints:
- Cart page: a configurable promo provides randomized product recommendations (via
cart_promo_idconfig) - Home page: promo vendors are loaded as part of the homepage rendering
API Reference
Legacy Storefront Routes
Defined in application/config/routes.php:
| URL Pattern | Controller | Method | Description |
|---|---|---|---|
/promo | Adv_promo | index() | Promo index (no slug) |
/promo/{slug} | Adv_promo | index($slug) | Render promo page by slug |
/{lang}/promo/{slug} | Adv_promo | index($slug) | Localized promo page |
REST API Endpoints (Admin/Backend)
Defined in application/config/rest_routes.php. Authentication: JWT backend, roles: AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA.
| Method | Endpoint | Controller Method | Description |
|---|---|---|---|
GET | /rest/product/promo | index() | List promos (paginated, filterable) |
GET | /rest/product/promo/item | item() | Get single promo by filters |
GET | /rest/product/promo/{id} | show($id) | Get promo by ID |
POST | /rest/product/promo | store() | Create new promo |
POST | /rest/product/promo/{id} | update($id) | Update existing promo |
DELETE | /rest/product/promo/{id} | destroy($id) | Delete promo |
All endpoints support locale prefix: /{lang}/rest/product/promo/...
Filters: id (exact), viewType (exact), name.{locale} (partial), slug.{locale} (exact)
Sorts: id, viewType, name.{locale}
Relations: translations (one-to-many), products (many-to-many)
File uploads: promo_image and promo_small_banner via multipart/form-data
Code Flow
Step 1: Route Resolution
The request to /promo/{slug} is matched by application/config/routes.php:
$route['promo/(.+)'] = 'eshop/promo/index/$1';
$route['(\w{2})/promo/(.+)'] = 'eshop/promo/index/$2';This routes to Adv_promo::index($slug) in the HMVC eshop module. The application-level controller at application/modules/eshop/controllers/Promo.php extends Adv_promo (which extends Front_c).
Step 2: Controller Construction
ecommercen/eshop/controllers/Adv_promo.php constructor:
- Loads models:
product_model,vats_model,promo_model - Loads pagination helper
- Instantiates
BlockBuilderfor optional rich layouts - Sets
$this->render['is_promo'] = trueas a view flag
Step 3: Promo Lookup (Cached)
php
$promo = $this->pscache->model(
'promo_model', 'getMuiRecord',
[['slug' => $slug, 'lang' => $this->language_abbr]],
$this->pscache_ttl
);The PSCache library wraps model calls with a PSR-6 cache layer (FileAdapter or RedisAdapter). Cache TTL comes from config('cache_l2_default_expires').
promo_model::getMuiRecord() joins shop_promo with shop_promo_mui and returns a single row with fields: id, promo_image, promo_small_banner, view_type, header_color, text_color, do_follow, promo_name, promo_slug, meta_title, meta_description, meta_keywords, description, footer_description, builder_block_id.
If the promo is not found, $this->error_404() is called and the method returns.
Step 4: Dofollow Resolution
php
$dofollow = !((isset($promo->do_follow) && $promo->do_follow === "0"));The dofollow flag is passed to the main layout view. In application/views/main.php (and default.php), when $dofollow === false, the template emits <meta name="robots" content="noindex,nofollow"/>. This gives per-promo control over search engine crawling. Additionally, Adv_front_controller::setNoFollow() sets dofollow to false when query string parameters are present.
Step 5: Product Loading (Cached)
php
$this->render['records'] = $this->pscache->model(
'product_model', 'getPromoProducts',
[['shop_promo_products.promo_id' => $promo->id,
'shop_product_mui.lang' => $this->language_abbr,
'shop_product.active' => true,
'shop_product.price >' => 0]],
$this->pscache_ttl
);product_model::getPromoProducts() (ecommercen/eshop/models/Adv_product_model.php) executes:
- Selects product fields: id, slug, name, price, pieces, soft_delete, description, video, vendor info, category slug, negative_stock
- Joins:
shop_product->shop_product_mui->shop_product_category_lp->shop_product_category_mui->shop_vendor->shop_vendor_mui->shop_promo_products - Filters by language for both product and vendor MUI tables
- Orders by
shop_promo_products.order ASC(manual ordering) - Groups by
product_idto prevent duplicates from multiple category associations - Supports optional
$limitand$randomparameters (used by cart promo, not storefront)
Product IDs are collected into $this->productIdsToParse (a UniqueCollection) for downstream enrichment.
Step 6: Block Builder Resolution
php
$promoViewFullPage = promoViewFullPage();
$this->render = $this->blockBuilder->frontRender($this->render, $promo->builder_block_id, 'promoPage');The BlockBuilder library (application/modules/builder/libraries/BlockBuilder.php) checks if the promo's MUI record has a builder_block_id. If so, it loads the block data and stores it in $this->render['blockData']['promoPage'].
View selection logic (three paths):
Builder block exists (
blockData['promoPage']is set): Uses thepromoBuilderlayout view, which renders the builder component. Template keypromoBuildermaps tolayouts/builder/promo.php.Full-page promo view (
promoViewFullPage[$view_type]istrue): Loads the view directly without the site header/footer wrapper. View file determined bypromoView($view_type).Standard promo view (default): Loads the promo view as
view_contentwithin the site's main template. View file:{client_views}/promo/{promoView($view_type)}.
Step 7: Sidebar and Default Rendering
php
$this->promoExtra();
$this->defaultRender();promoExtra() loads the category tree for sidebar navigation:
php
$this->render['categories_search'] = $this->pscache->model(
'product_category_model', 'getRecordsTree',
[['parent_id' => 0, 'published' => true]],
$this->pscache_ttl
);defaultRender() (from Adv_front_controller) adds standard storefront data: client views path, canonical URL, customer favorites, category slugs, product badges, menu, footer, push notifications, cart data, attributes, variations, and JSON state.
Step 8: Product Enrichment
php
$this->defaultProductParseWithProductCodes($this->productIdsToParse->getItems());This calls four methods sequentially:
defaultProductParse()-- loads pricing data (discounts, final prices, VAT, currency) from the prices view and attaches it to$this->render['liveData']defaultGiftsFromProductParse()-- loads gift/promotion rules for productsdefaultProductCodesParse()-- loads product codes (SKU, EAN, etc.) into$this->render['productCodes']defaultProductCodeImagesParse()-- loads product code images into$this->render['productCodeImages']
Step 9: View Rendering
The view (e.g., application/views/main/promo/promo.php) iterates over products and for each:
- Calls
mergeDataToProduct()to combine the product record with live pricing data, product codes, and code images - Calls
transformProductForJson()to add the product to the page's structured data - Optionally calls
trackProductPromotionsHelper()for analytics tracking data - Renders the
productCardcomponent view for each product
The view displays:
- Breadcrumbs (if available)
- Responsive banner image (desktop:
promo_image, mobile:promo_small_banner) - Promo name as
<h1> - Description HTML content
- Product grid (responsive columns)
- Footer description HTML content
Domain Layer
Modern Domain: Advisable\Domains\Product\Promo
Located at src/Domains/Product/Promo/. Provides the read/write service layer used by the REST API.
Entity (Repository/Entity.php):
- Properties:
id,promo_image,promo_small_banner,view_type,header_color,text_color,do_follow - Implements
FilterTranslationviaFiltersTranslationtrait - Table:
shop_promo
MUI Entity (Repository/MuiEntity.php):
- Properties:
id,promo_id,slug,meta_title,meta_keywords,meta_description,name,description,footer_description,lang,builder_block_id - Table:
shop_promo_mui
Repository (Repository/Repository.php):
- Extends
BaseRepository, tableshop_promo - Uses Specification pattern (Filter, Sort, Pagination, WithRelations)
Repository Configurator (Repository/RepositoryConfigurator.php):
translations: ONE_TO_MANY relation viaMuiRepositoryonpromo_idproducts: MANY_TO_MANY relation viashop_promo_productsjunction table, linking toProduct\Repository\Repositorywithpromo_id/product_id
MUI Repository (Repository/MuiRepository.php):
- Extends
BaseRepository, tableshop_promo_mui - Uses
NullRelationConfigurator(no nested relations)
Service (Service.php):
- Implements
ReadServiceandRendersPagination - Methods:
all()(paginated list),item()(single by filter),get()(by ID with relations) - Builds specifications from
ListRequestfilters and sorts
ListRequest (ListRequest.php):
- Allowed filters:
id(exact),viewType(exact),name.{locale}(partial),slug.{locale}(exact) - Allowed sorts:
id,viewType,name.{locale} - Relation sorts:
translations.name
WriteService (WriteService.php):
- Methods:
create(),update(),delete() - Transactional: wraps insert/update/delete in DB transactions
- Handles MUI translations: parses
translationsarray, validates each, inserts/replaces for entity - Uses
WriteDataandMuiWriteDatavalue objects - Validation via
Validatorclass (checkslangrequired on translations)
WriteData (WriteData.php):
- Fields:
promoImage,promoSmallBanner,viewType,headerColor,textColor,doFollow - Accepts both snake_case and camelCase input keys
MuiWriteData (MuiWriteData.php):
- Fields:
lang,slug,metaTitle,metaKeywords,metaDescription,name,description,footerDescription,builderBlockId
Architecture
Layer Interaction
Browser Request
|
v
[CI Router] ---> Adv_promo::index($slug)
| |
| +--> PSCache -> promo_model::getMuiRecord() [shop_promo + shop_promo_mui]
| +--> PSCache -> product_model::getPromoProducts() [shop_product + promo_products]
| +--> BlockBuilder::frontRender() [builder blocks]
| +--> promoExtra() -> product_category_model [sidebar categories]
| +--> defaultRender() [menu, footer, badges, cart]
| +--> defaultProductParseWithProductCodes() [pricing, codes, gifts]
| |
| v
| [View Layer]
| |
| +--> promo view (standard / full-page / builder)
| +--> productCard component per product
| +--> analytics tracking data
|
v
HTML ResponseREST API Layer (Admin)
JWT-Authenticated Request
|
v
[REST Router] ---> Promo::index() / show() / store() / update() / destroy()
| |
| +--> Service::all() / get() [Read operations]
| +--> WriteService::create/update/delete [Write operations]
| +--> UploadService [File handling]
| |
| v
| [Resource Layer]
| +--> Resource.php (formats entity for JSON response)
| +--> MuiResource.php (formats translations)
| +--> Collection.php / MuiCollection.php
|
v
JSON ResponseKey Dependencies
| Component | File | Role |
|---|---|---|
Adv_promo | ecommercen/eshop/controllers/Adv_promo.php | Storefront controller |
Adv_promo_admin | ecommercen/eshop/controllers/Adv_promo_admin.php | Admin CRUD controller |
Adv_promo_model | ecommercen/eshop/models/Adv_promo_model.php | Legacy promo model |
Adv_product_model | ecommercen/eshop/models/Adv_product_model.php | Product model (getPromoProducts()) |
BlockBuilder | application/modules/builder/libraries/BlockBuilder.php | Rich layout rendering |
Pscache | application/libraries/Pscache.php | PSR-6 cache wrapper |
Service | src/Domains/Product/Promo/Service.php | Modern read service |
WriteService | src/Domains/Product/Promo/WriteService.php | Modern write service |
Promo (REST) | src/Rest/Product/Controllers/Promo.php | REST controller |
Data Model
For the full shop_promo, shop_promo_mui, and shop_promo_products column schemas, see AD-07 Promotion Management.
Entity-Relationship Diagram
shop_promo (1) ---< shop_promo_mui (N) [one-to-many: translations]
shop_promo (1) ---< shop_promo_products (N) [junction table]
shop_promo_products (N) >--- shop_product (1) [many-to-one: products]Related Tables (Read-Only in Promo Context)
| Table | Usage |
|---|---|
shop_product | Product master (active, price, stock filters) |
shop_product_mui | Product translations (name, slug, description by lang) |
shop_vendor / shop_vendor_mui | Vendor data for product cards |
shop_product_category_lp / shop_product_category_mui | Category slug for product URLs |
Configuration
Application Config
| Key | File | Default | Description |
|---|---|---|---|
cart_promo_id | application/config/main.php | 64 | Promo ID used for cart page product recommendations |
cart_promo_limit | application/config/main.php | 8 | Max products shown from cart promo |
cache_l2_default_expires | application/config/cache.php | (varies) | PSCache TTL for promo and product queries |
products_list_limit | app config | (varies) | General product listing limit |
View Type Configuration
Defined in ecommercen/helpers/shopmodule_helper.php (overridable in application/helpers/main_theme_helper.php):
php
// View type labels (shown in admin dropdown)
function promoViewTypes() {
return [0 => 'Default'];
}
// View template file mapping
function promoViews() {
return [0 => 'promo']; // maps to {client_views}/promo/promo.php
}
// Full-page toggle (skip site header/footer)
function promoViewFullPage() {
return [0 => false]; // false = render inside main template
}The promoView($viewType) helper in ecommercen/helpers/theme_helper.php resolves a view type integer to its template filename.
Template Configuration
Promo builder views are registered in application/config/mainTemplate.json and application/config/template.json:
json
{
"promoBuilder": {
"view": "layouts/builder/promo.php",
"scss": ""
}
}REST API Policy
In application/config/rest_policies.php:
php
Promo::class => [
'defaults' => [
'auth' => 'backend',
'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA]
]
]DI Container Registration
Domain services in src/Domains/Product/container.php:
Promo\Repository\Repository,Promo\Repository\RepositoryConfiguratorPromo\Repository\MuiRepository(withNullRelationConfigurator)Promo\Service,Promo\Validator,Promo\WriteServicePromo\Repository\WriteRepository,Promo\Repository\MuiWriteRepository
REST controller in src/Rest/Product/container.php:
Promocontroller wired with:Service,Resource,Collection,ListRequest,WriteService,UploadService
Both registered via application/config/container/modules.php.
Client Extension Points
Storefront Controller Override
The client application controller at application/modules/eshop/controllers/Promo.php extends Adv_promo. Clients can override in their application/ directory.
promoExtra() hook: The promoExtra() method is explicitly designed as a hook point. The doc comment states it should be overridden with an empty method body (omitting parent::promoExtra()) to skip the sidebar category query. Clients can also add custom sidebar content, upsells, or promotional widgets here.
afterAdd(), afterEdit(), afterDelete(), afterProductsDelete() hooks (admin): Empty protected methods in Adv_promo_admin that clients can override for post-CRUD actions (cache clearing, sync, logging).
afterBatchActionSubmitUpdatePromo() and afterBatchActionSubmitRemovePromo() hooks (products admin): Empty protected methods in Adv_products_admin for custom logic when products are batch-added to or removed from a promo.
View Type Customization
Clients can define custom promo view types by overriding three functions in their theme helper (application/helpers/main_theme_helper.php):
php
function promoViewTypes() {
return [
0 => 'Default',
1 => 'Custom Campaign',
2 => 'Full-page Landing',
];
}
function promoViews() {
return [
0 => 'promo',
1 => 'promo-campaign',
2 => 'promo-landing',
];
}
function promoViewFullPage() {
return [
0 => false,
1 => false,
2 => true, // renders without site header/footer
];
}Each view type maps to a PHP view file at application/views/{client}/promo/{name}.php.
Builder Block Integration
By setting a builder_block_id on the promo MUI record, admins can replace the standard promo view with a fully custom block builder layout. This is per-language, allowing different layouts for different locales.
Custom REST Layer (Client Repos)
Client repos can extend the domain and REST layers in custom/:
custom/Domains/Product/Promo/...
custom/Rest/Product/Controllers/Promo.phpWith DI aliases to override upstream services.
Image Storage
Promo images are stored at files/promo/ relative to the public asset directory. Both promo_image (desktop banner) and promo_small_banner (mobile banner) are uploaded via the AdvUploader library in admin or the UploadService in the REST API. The REST resource formats file paths with $this->formatFile($field, '/files/promo/').
Business Rules
Product visibility: Only products with
active = trueANDprice > 0are shown on promo pages. Soft-deleted products are included in the query but may be filtered at the view level.Product ordering: Products are displayed in the manual
ordercolumn fromshop_promo_products(ascending). The admin panel supports drag-and-drop reordering via AJAX.No pagination: All matching promo products are loaded in a single query (no limit on storefront). The cart promo feature uses a limit and random ordering.
Language filtering: Both product and vendor MUI records are filtered by the current language. Products without a translation in the active language will not appear.
Caching: All database queries are cached via PSCache with the configured L2 cache TTL. Cache invalidation happens when the cache expires or is manually cleared.
SEO control: The
do_followflag defaults to1(follow). When set to0, the page emits<meta name="robots" content="noindex,nofollow"/>. This is also triggered in development/testing environments regardless of the flag.Slug uniqueness: Promo slugs are auto-generated from the name on creation (
createSlug(strip_quotes($name))). On update, slugs can be manually set by users withurlFieldsaccess. The validation ruleis_unique_muiensures slug uniqueness per language.Admin access control: Promo admin requires one of:
AUTH_ROLE_ADVISABLE,AUTH_ROLE_ADMIN,AUTH_ROLE_MARKETING,AUTH_ROLE_MEDIA. The REST API usesAUTH_ROLE_ADMIN,AUTH_ROLE_MARKETING,AUTH_ROLE_MEDIA.SEO meta access: The dofollow toggle and meta tag fields (title, description, keywords) are only visible to users with the
metatagsaccess level. Slug editing requiresurlFieldsaccess.Cart promo: The order page loads a specific promo (configured via
cart_promo_id) with randomized products and a configurable limit (cart_promo_limit) for cross-selling on the cart page.Batch operations: Products can be added to or removed from promos in bulk from the products admin listing, using the
put_in_promoandremove_from_promobatch actions.Builder override: When a builder block is assigned to a promo's MUI record, it completely replaces the standard product grid view with the builder-rendered layout. The view type and color settings are ignored in builder mode.
Analytics tracking: The
maintheme view callstrackProductPromotionsHelper()for each product, generating structured promotion tracking data (compatible with Google Analytics Enhanced Ecommerce).Cascading delete: Deleting a promo removes the master record, all MUI translations, and all product associations from
shop_promo_products.
Related Flows
- AD-07 Promotion Management -- Admin CRUD for promo creation and editing
- CF-01 Product Browsing -- Product listing and product card rendering shared with promos
- CF-21 Offers -- Related promotional pricing and discount mechanisms
- CF-26 Home Page -- Promo vendors displayed on homepage via
homePromoVendors() - CF-05 Cart Management -- Cart page uses promo products for recommendations via
cartPromo() - CF-03 URL Routing --
/promo/{slug}route resolution - SY-07 Cache Management
- SY-23 MUI Translation Pattern
- SY-28 Storage Abstraction
Wiki Guide: Builder block integration — see Builder Guide. Cache layer for promo queries — see Cache System.