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#
- Create the parameter and attach it to a category (
CategoryParameterRel). - The select options are the parameter's children — the parameter is the root of a tree, its descendants are the individual options.
- Decide whether it needs a denormalised column. Only what is sorted by or range-filtered needs one.
- Display rules (
ParameterRule) decide when the field appears — a condition over another parameter. - 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.php → recountAdverts() |
| 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