Skip to content

Subcontent / Embeddable Blocks

Flow ID: AD-49 Module(s): subcontent (legacy), Cms/Subcontent (modern) Complexity: Low Last Updated: 2026-04-04

Business Overview

Subcontent blocks are slug-keyed, multi-language rich text fragments that can be embedded into any storefront page. They provide a lightweight mechanism for store administrators to manage reusable content snippets -- promotional headers, offer banners, informational sections, legal notices, delivery notices -- without creating full CMS pages. Each block is identified by a unique slug and carries per-language name/content pairs.

Unlike CMS pages, subcontent blocks have no hierarchy, no SEO routing, no banner images, and no page view templates. They are purely data containers fetched by slug in storefront controllers and rendered inline. Approximately 34 blocks exist in a typical production database.

Storefront controllers load subcontent by slug via the subcontent_model::getContent() or getContentFieldArrayBySlugs() methods (with pscache TTL caching). Common consumers include the homepage, product pages, product category pages, vendor pages, and offer pages.


API Reference

REST Endpoints

MethodPathActionAuth
GET/rest/cms/subcontentList subcontent blocks (paginated, filterable)JWT
GET/rest/cms/subcontent/itemGet single block by filtersJWT
GET/rest/cms/subcontent/{id}Get block by IDJWT
POST/rest/cms/subcontentCreate blockJWT
POST/rest/cms/subcontent/{id}Update blockJWT
DELETE/rest/cms/subcontent/{id}Delete blockJWT

All REST routes support locale-prefixed variants (/{locale}/rest/cms/subcontent).

Filters (REST)

FilterTypeDescription
idExactBlock ID(s)
slugExactBlock slug(s)
name.{locale}PartialBlock name by locale

Relations

RelationTypeDescription
translationsOne-to-ManyMUI translation rows

Sorts

Root: id, slug Translation: name.{locale}

Admin Panel Endpoints (Legacy HMVC)

URLMethodAction
subcontent/subcontent_adminindex()List all blocks
subcontent/subcontent_admin/addadd()Create block form + handler
subcontent/subcontent_admin/edit/{id}edit($id)Edit block form + handler
subcontent/subcontent_admin/delete/{id}delete($id)Delete block

Code Flow

Storefront Consumption

Subcontent blocks are consumed inline by storefront controllers. They are not routed to directly -- they are fetched by slug and injected into the template render data.

Storefront controller (e.g., Adv_home, Adv_products, Adv_product_categories)
       |
       v
$this->load->model('subcontent/subcontent_model')
       |
       v
$this->pscache->model('subcontent_model', 'getContentFieldArrayBySlugs', [$slugs], $ttl)
       |  Fetches multiple blocks by slug array
       |  Returns keyed array: slug => content field value
       v
Merged into $this->render['{contextSubContent}']
       |
       v
View template renders subcontent HTML inline

Single-block fetch:

subcontent_model::getContent($slug, $lang)
       |  Joins subcontent + subcontent_mui
       |  Filters by slug and language
       v
Returns single row with id, slug, name, fulltext, lang

Admin Create Flow

Admin: subcontent/subcontent_admin/add
       |
       v
Adv_subcontent_admin::add()
       |  Validates: slug required (ADVISABLE only), name/fulltext per language
       |  Generates: slug via createSlug() helper
       |
       v
subcontent_model::addRecord($data, $dataMui)
       |  Inserts master: { slug }
       |  Inserts MUI rows: { subcontent_id, name, fulltext, lang }
       |
       +-- afterAdd($id)  [client hook]
       |
       v
redirect -> subcontent/subcontent_admin

REST Create Flow

POST /rest/cms/subcontent
       |
       v
Subcontent::store() -> doStore()
       |  Parses JSON body
       v
WriteService::create($data)
       |  Builds WriteData: { slug }
       |  Validates: slug required, slug unique
       |  Parses translations array -> MuiWriteData per language
       |  Validates: lang required per translation
       |
       v  (transactional)
WriteRepository::insert({ slug })
MuiWriteRepository::insertForEntity($id, $translations)
       |
       v
Repository::get($id, ['translations'])
       |  Returns created entity with translations
       v
Resource -> JSON response (201)

REST Update Flow

POST /rest/cms/subcontent/{id}
       |
       v
Subcontent::update($id) -> doUpdate($id)
       |
       v
WriteService::update($id, $data)
       |  Validates: slug not empty if provided, slug unique (excluding self)
       |  On slug change: checks for duplicates
       |
       v  (transactional)
WriteRepository::update($id, $data)       -- only non-null fields
MuiWriteRepository::replaceForEntity($id, $translations)  -- full replace
       |
       v
Repository::get($id, ['translations'])
       v
Resource -> JSON response (200)

Domain Layer

File Structure

src/Domains/Cms/Subcontent/
  Repository/
    Entity.php                    -- id, slug
    MuiEntity.php                 -- id, subcontent_id, name, fulltext, lang
    Repository.php                -- table: subcontent
    RepositoryConfigurator.php    -- translations (ONE_TO_MANY via subcontent_id)
    MuiRepository.php             -- table: subcontent_mui
    WriteRepository.php
    MuiWriteRepository.php
    Specification/
      FilterByTranslation.php
      SortByTranslation.php
  Service.php                     -- ReadService: all(), item(), get()
  WriteService.php                -- create(), update(), delete() with transactional writes
  ListRequest.php                 -- Allowed filters (id, slug, name.{locale}), sorts (id, slug, name.{locale})
  WriteData.php                   -- slug only
  MuiWriteData.php                -- lang, name, fulltext (content)
  Validator.php                   -- Create: slug required + unique. Update: slug unique if changed. Translations: lang required.

REST Layer

src/Rest/Cms/Controllers/Subcontent.php     -- HandlesRestfulActions + HandlesWriteActions; full CRUD
src/Rest/Cms/Resources/Subcontent/
  Resource.php         -- id, slug; includes translations relation
  Collection.php       -- Collects Resource
  MuiResource.php      -- id, subcontentId, name, content, lang
  MuiCollection.php    -- Collects MuiResource

Legacy Layer

ecommercen/subcontent/
  controllers/
    Adv_subcontent_admin.php      -- Admin CRUD: index, add, edit, delete
  models/
    Adv_subcontent_model.php      -- All DB operations: CRUD, content lookup by slug, batch slug fetch

Data Model

subcontent (Master Table)

ColumnTypeDescription
idint, PK, AIBlock ID
slugvarchar, uniqueIdentifier slug (e.g., homepage-banner, delivery-notice)

subcontent_mui (MUI Table)

ColumnTypeDescription
idint, PK, AIMUI row ID
subcontent_idint, FKReferences subcontent.id
langvarchar(5)Language code (e.g., el, en)
namevarcharBlock label/title (admin display)
fulltexttextRich HTML content body

Entity Relationships

subcontent
  |-- 1:N --> subcontent_mui (translations, FK: subcontent_id)

Configuration

Subcontent has minimal configuration. No config files, registry keys, or feature gates control its behavior. The feature is always available.

DI Container Registration

Domain services (src/Domains/Cms/container.php):

php
// Repository, RepositoryConfigurator, MuiRepository (with NullRelationConfigurator),
// Service, WriteRepository, MuiWriteRepository, Validator, WriteService

REST controller (src/Rest/Cms/container.php):

php
$services->set(Controllers\Subcontent::class)
    ->arg('$service', new Reference(Subcontent\Service::class))
    ->arg('$resourceClass', Resources\Subcontent\Resource::class)
    ->arg('$collectionClass', Resources\Subcontent\Collection::class)
    ->arg('$listRequestClass', Subcontent\ListRequest::class)
    ->arg('$writeService', new Reference(Subcontent\WriteService::class));

Authorization

Admin Panel

RoleConstantAccess
AdvisableAUTH_ROLE_ADVISABLEFull CRUD + slug editing + delete
AdminAUTH_ROLE_ADMINAdd/edit (no slug editing, no delete)
CMSAUTH_ROLE_CMSAdd/edit (no slug editing, no delete)
MediaAUTH_ROLE_MEDIAAdd/edit (no slug editing, no delete)

Key restrictions:

  • Slug field: Only AUTH_ROLE_ADVISABLE can set or modify the slug in the admin panel. Non-ADVISABLE roles see a read-only slug or it is not shown.
  • Deletion: Only AUTH_ROLE_ADVISABLE can delete subcontent blocks. Other roles receive an error flash message.
  • Slug validation: On add, slug is required and generated via createSlug(). On edit, slug is only updated if the user has AUTH_ROLE_ADVISABLE.

REST API

JWT authentication required. The REST API does not differentiate role access beyond authentication -- all authenticated users can perform CRUD operations.


Client Extension Points

Admin Controller Hooks

The admin controller provides empty protected methods for client-specific logic:

HookWhen CalledSignature
afterAdd($id)After insert(int $id): void
afterEdit($id)After update(int $id): void
afterDelete($id)After delete(int $id): void

Domain Layer Override

Client repos can override the repository or validator via DI alias:

php
$services->alias(
    \Advisable\Domains\Cms\Subcontent\Repository\RepositoryConfigurator::class,
    \Custom\Domains\Cms\Subcontent\Repository\RepositoryConfigurator::class
);

Storefront Usage Pattern

Clients embed subcontent by loading the model and fetching by slug. The common patterns are:

Single block:

php
$this->load->model('subcontent/subcontent_model');
$content = $this->subcontent_model->getContent('my-block-slug');
// $content->fulltext contains the HTML

Batch fetch (multiple slugs at once):

php
$slugs = ['banner-top', 'promo-header', 'delivery-notice'];
$data = $this->pscache->model('subcontent_model', 'getContentFieldArrayBySlugs', ['fulltext', $slugs], $ttl);
// Returns: ['banner-top' => '<html>...', 'promo-header' => '<html>...', ...]

Business Rules

  1. Slug uniqueness: Each subcontent block must have a globally unique slug. The REST Validator checks for duplicates on both create and update, throwing a ValidationException if a collision is found.
  2. Slug immutability (admin): In the admin panel, only ADVISABLE role can modify slugs. This protects storefront code that references blocks by hardcoded slug strings.
  3. Deletion restriction (admin): Only ADVISABLE role can delete blocks. The canDeleteRecord() method in the legacy model enforces isAdvisableUser().
  4. No SEO routing: Subcontent blocks are not routed to on the storefront. They have no URL, no metatags, and no page view templates.
  5. No hierarchy: Subcontent blocks are flat -- no parent-child relationships. Each block is a standalone content fragment.
  6. Language fallback: The getContent() method fetches by slug + language. If no translation exists for the requested language, null is returned. There is no automatic fallback to a default language.
  7. Cache layer: Storefront controllers use pscache (page-scope cache) with a TTL when fetching subcontent. Changes made via admin are visible after cache expiry.
  8. Transactional writes (REST): Both create and update operations are wrapped in database transactions. MUI records are replaced atomically on update (delete all + re-insert).

Storefront Consumers

The following controllers load subcontent blocks for inline rendering:

ControllerSubcontent SlugsContext
Adv_homeVarious (homepage sections)Homepage promotional content
Adv_productsProduct page subcontent slugsProduct detail page extras
Adv_product_categoriesContest + category subcontentCategory listing page sections
Adv_vendorsVendor page subcontentVendor/brand page sections
Adv_offersOffer subcontentOffer page promotional blocks

Slug values are typically client-specific and hardcoded in client controller overrides or loaded from registry configuration.