Moderation (PRO)
A queue of objects waiting for review: unpublished rows of tables marked Moderation. A moderator sees them as one list — a badge in the manager's top bar and a widget on the dashboard.
It takes two switches
- The
pageblocks_moderationsystem setting — the switch for the whole branch. Off by default. - The «Moderation» checkbox on the table — in the table constructor.
The order matters: while the setting is off, the second step does not exist at all.
This is behind almost every "I don't have it in the manager"
Turning the setting off does not hide one button — it removes the whole branch: no checkbox on the table form, no badge in the top bar, no dashboard widget, no stamps, an empty queue. Nothing breaks and nothing is reported, so from the outside it looks like a missing feature. See System settings.
The moderation_at column
A marked table gets a moderation_at column (datetime, NULL, right after published_at) when the constructor is saved — the component adds it itself, no migration needed. Unchecking the box does not drop the column: the component will not delete data quietly.
There is no index on it, and that is a decision
The column is almost entirely NULL, the queue selects on published_at, and on an imported table of several hundred thousand rows building an index would stretch saving the form into minutes.
What ends up in the queue
A row qualifies when all of this holds at once:
| Condition | Where it lives |
|---|---|
| The table is marked Moderation | pb_tables.moderated |
| The table has its own model | Rows of the shared pb_table_data never queue |
| The table is published and not deleted | Its own published_at / deleted_at |
| The row is not published | published_at IS NULL |
| The row carries a stamp | moderation_at is not empty |
| The row is not deleted | deleted_at IS NULL |
Read the "own model" line carefully: a table that keeps rows in the shared pb_table_data is not moderated even with the box checked. The queue works on tables with their own model and migration.
Your code applies the stamp
The component does not decide what goes to review — it never stamps rows on its own. The stamp is applied by application code when the object is created, through ModerationQueue::stamp():
use Boshnik\PageBlocks\Support\ModerationQueue;
$city = PbCity::create(ModerationQueue::stamp((new PbCity)->getTable()) + [
'name' => $request->get('name'),
'country_id' => (int) $request->get('country_id'),
]);stamp() returns ['moderation_at' => now()] — or an empty array when moderation is off or the table has no such column. The same controller therefore runs on a site with moderation and on one without: no stray field reaches the insert, no Unknown column.
Approving means publishing
There are no "approve" and "reject" buttons in the queue, and that is not an omission. Clicking an entry opens the object in the same window the global search uses: the moderator reads the row, edits it and publishes it. A filled published_at removes it from the queue — that is exactly what the queue selects on.
Why the stamp and publishing are kept apart
moderation_at answers "when was this sent for review", published_at answers "is it visible on the site". The split is deliberate: an edit to an already approved row can go back for review without disappearing from the site meanwhile.
The same coin's other side: moderation hides nothing by itself. A stamped row is an ordinary row, and if your template lists unpublished rows, visitors will see it. "Not visible yet" is published_at, not the queue.
Where the queue lives
- A badge in the manager's top bar — with the number waiting (over a hundred shows as
99+). - A dashboard widget — "Objects awaiting moderation", the same list.
Two endpoints feed it — mgr/pb/moderation for the list and mgr/pb/moderation/count for the badge; both require a manager session and the paid edition.
An entry names the table, the title, the parent, the author and a short description built from the fields, so a moderator does not have to open a row to know what it is:
| Shown | Taken from |
|---|---|
| Title | First non-empty of pagetitle, name, title, subject; with no such column — the first text field of the constructor |
| Description | Up to five field: value pairs; files, galleries, nested tables and aliases are skipped, list values are replaced by their labels |
| Author | First non-empty of author_id, user_id, created_by, author, user. Deleted user — #id is shown; no author recorded at all — the slot stays empty |
| Date | The moment it entered the queue, not the moment it was created |
Sorting follows the queueing date: an ad from two years ago sent for review today comes first.
Volume and age
Each table contributes its 50 most recent queued rows.
The days parameter caps the list by queueing date: 0 (the default) means no cap, 90 is the maximum. Capping is rarely useful — stamps are applied deliberately, and an object waiting its second week should not vanish from the queue.
A table without the column: a three-day window
If the table has no moderation_at — the database predates moderation and the constructor has not been saved since — the component does not fail. It falls back to a different rule: unpublished rows created within the last three days. A days value replaces that window.
Which is why the queue looks odd on such a table
Without the column there is no record of when a row was sent for review — only of when it was created. Rows drop out of the queue by age rather than by a moderator's decision. This is a guard against listing the entire unpublished archive at once, not a mode of operation: save the table in the constructor with the setting on, the column appears, and the selection becomes exact.
The queue is empty although everything is on
The pb_tables.moderated column arrives with the core migration 10000000000019_add_moderated_to_pb_tables, and core migrations are applied by hand. Until it is there the queue is always empty — and a warning naming the migration goes to the MODX log. Nothing crashes: taking a page down over an unapplied migration is a bad trade.