A lightweight, config-driven editorial flow (draft → in review → approved) built on symfony/workflow. It lets small teams move a page through explicit states and record who validated it. It is not a full multi-stage approval engine — but the states, transitions, and guards are fully customizable, and the whole feature is opt-out.
Three fields on Page:
workflowState (string, default draft) — the editorial place, backed by a symfony/workflow state machine.reviewedBy (User, nullable) — who approved the page.reviewedAt (datetime, nullable) — when it was approved.publishedAt stays the source of truth for visibility; workflowState is the editorial layer on top.
Pushword ships a page_editorial state machine:
draft, in_review, approvedsubmit (draft → in_review), approve (in_review → approved, guarded by ROLE_EDITOR), request_changes (in_review/approved → draft)When a page enters approved, a subscriber records reviewedBy (current user) and reviewedAt, mirroring the createdBy/editedBy convention.
In the admin, the page form shows the current state plus the transition buttons the current user is allowed to apply (guards are enforced). The page list gains a workflowState column and filter.
Redefine page_editorial in your app config (config/packages/workflow.yaml or framework.workflows) — add places, split states, change guards. When an app already defines a page_editorial workflow, Pushword skips its default, so there is no core fork:
framework:
workflows:
page_editorial:
type: state_machine
marking_store: { type: method, property: workflowState }
supports: [Pushword\Core\Entity\Page]
initial_marking: draft
places: [draft, in_review, approved]
transitions:
submit: { from: draft, to: in_review }
approve: { from: in_review, to: approved, guard: "is_granted('ROLE_EDITOR')" }
request_changes: { from: [in_review, approved], to: draft }
Off by default. When enabled, a page can only stay published (publishedAt) once its workflowState is approved — enforced in PageListener so it covers admin, flat-sync, and API writes uniformly:
pushword:
require_approval_before_publish: true # default: false
pushword:
editorial_workflow: false # default: true
When false, the default page_editorial workflow is not registered and the admin hides its workflow field, column, and filter. The workflowState column stays in the schema (inert, defaults to draft). Keep require_approval_before_publish at false when disabling, otherwise publishing would be blocked with no way to reach approved.
workflowState round-trips through the page frontmatter like other content. reviewedBy and reviewedAt are intentionally not written to flat files — consistent with createdBy/editedBy, git is the audit trail.
The version extension already snapshots on update, so transitions become part of the page's version history for free.