Skip to content
V
For developers
Architecture, conventions, the core and security
Modules / Classifieds

Classifieds#

The Bazaar module — adverts with dynamic fields, paid tariffs, saved-search alerts and replies. The code lives in app/UI/Api/Bazaar/, the front end in app/UI/Front/Bazaar/, the administration in app/UI/Admin/Bazaar/.

What the module is made of#

Area Main entities Note
the advert Advert plus the relations AdvertParameterRel, AdvertCategoryRel, AdvertAssetRel, AdvertFileRel almost all the data lives in relations, not in columns
categories Category, CategoryText, CategoryPrice, CategoryParameterRel a nested set
parameters Parameter, ParameterGroup, ParameterRule plus ParameterRuleCondition/Source/Target the rules decide which field appears when
payment Tariff, TariffPrice, TariffQuickPrice, PaymentItem
interaction Response, AdvertFollower, AdvertReport, AdvertStatistic
saved-search alerts Watchdog, WatchdogEmail plus relations a stored search
moderation BlacklistedWord

An advert has no fixed fields#

The title, description, price, email and region are all dynamic parameters (AdvertParameterRel). The columns in the table are only denormalised copies for sorting and filtering — neither can be done over a parameter's text value.

Parameter key Denormalised column Who fills it
price price (a number, for sorting and range filtering) AdvertManager::beforeSave() from meta['parameters']
email email writing the advert
regionId region_id writing the advert

Renaming a reserved parameter key silently breaks sorting

Nothing reports it — beforeSave() simply does not find what to put into the column. Sorting by price stops working and replies stop arriving, because there is nowhere to send the email. It looks like a listing bug.

Prices in different currencies are not converted

The column receives the bare number with no conversion. Sorting by price therefore mixes currencies — 100 EUR sorts before 200 CZK.

The price column has a self-heal cron

/cron/bazaar/advert/recount-prices (every 10–15 min) reconciles discrepancies after imports and manual database edits. It is a safety net, not the primary write path.

The active column is computed by the database#

cms_mod_bazaar_adverts.active is a generated column (VIRTUAL):

active = CASE WHEN approved_at IS NOT NULL
              AND paid = 1 AND banned = 0 AND deleted = 0
              AND deactivated = 0 AND sold = 0
         THEN 1 ELSE 0 END

active cannot be set from the administration or from the API

It changes only by writing one of those six flags. The entity therefore marks it insertable: false, updatable: false and additionally #[ExternalReadonly] — without the latter, setActive() would overwrite the in-memory value before the save and the response would return the client's invented state instead of the real one.

Expiry is NOT in that formula

An expired advert stays active = 1; it is removed from listings by an application filter. Anyone writing their own query on active = 1 alone gets expired adverts too.

Procedure: adding a dynamic parameter#

  1. Create the parameter and attach it to a category (CategoryParameterRel).
  2. The select options are the parameter's children — the parameter is the root of a tree, its descendants are the individual options.
  3. Decide whether it needs a denormalised column. Only what is sorted by or range-filtered needs one.
  4. Display rules (ParameterRule) decide when the field appears — a condition over another parameter.
  5. Click through creating and editing an advert in that category.

Deleting a node deletes the whole subtree

For a parameter that includes all its options — and with them the values in every advert that had that option filled in. The data cannot be recovered.

Propagating prices or parameters into a subtree is irreversible

It overwrites every subcategory without asking and without a backup. On a large tree that is hundreds of overwritten categories from one click.

Denormalised counters#

Value What reconciles it
advert.price the /cron/bazaar/advert/recount-prices cron
category.advert_count the /cron/bazaar/category/recount cron
advert.email, advert.region_id writing the advert

The count recalculation is a set-based UPDATE over the whole tree — a parent counts the active adverts of its entire subtree, not just its direct children.

The recalculation respects the automatic approval setting

1/useAutoConfirmation changes what counts as active. Anyone verifying the counts with hand-written SQL has to take it into account — otherwise the numbers will not match and they will go looking for a bug in the cron.

Saved-search alerts#

A stored search that sends an email when a matching advert appears.

It only fires when an advert is CREATED

Not on an edit and not on a move to another category. An advert that enters the category later never reaches the subscribers — and nobody misses anything until the advertiser asks.

The watchdog uses hardcoded ids of parent parameters

After a change of the parameter code lists it stops matching. That surfaces as no error, just as a silent stop.

Highlighting and expiry#

toped_until drives the highlight and the ordering in listings. An extension from a payment is monotonic — it only prolongs, never shortens. A manual change in the administration can do both.

Cron What it does Interval
/cron/bazaar/advert/notice-expiration warns N days before expiry (5/expirationNoticeDays), sets a flag so it does not repeat daily
/cron/bazaar/advert/delete-expired silently marks deleted = 1 M days after expiry (5/expirationDeleteDays) daily

Notifications#

Followers are notified ONLY when the price tag changes

The triple price, price type and currency is compared. A change of title, description or photos notifies nobody — deliberately, otherwise fixing a typo would send out hundreds of emails.

Editing in the administration sends no emails at all

Not the activation one, not to the watchdogs, not to the followers. That is intentional: an administrator is fixing someone else's advert and should not trigger communication in their name.

Elasticsearch#

Classifieds is one of the Elasticsearch sources — the reindex is /cron/bazaar/elastic/reindex, the incremental sync /cron/bazaar/elastic/sync.

Bulk operations outside the hooks are invisible to the index

A set-based UPDATE (recounts, imports, manual SQL) does not fire the hook, so it does not propagate into the index. Consistency is only restored by the nightly reindex — until then search returns stale data and nothing shows it.

Where to look#

I want Where
the advert logic and the denormalisation app/UI/Api/Bazaar/Models/Managers/AdvertManager.php
the count recalculation in categories app/UI/Api/Bazaar/Models/Managers/CategoryManager.phprecountAdverts()
the saved-search alerts app/UI/Api/Bazaar/Models/Managers/Watchdog*Manager.php
the entity definition and its flags app/Core/Base/Shared/Bazaar/Models/Entities/Advert.php
the cron tasks app/UI/Cron/Bazaar/Presenters/
the front-end advert form app/UI/Front/Bazaar/Components/AdvertFormComponent/

Follow-up chapters: Company catalogue · Elasticsearch · Filters · Scheduled tasks