The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftAnnotations

Publishable

Add a draft/publish workflow to any component so admins can edit content privately before making it live.

#[Silverback\Publishable] gives your component a twin-resource draft/publish lifecycle. An admin edits the draft privately; visitors always see the published version. Publishing is atomic — the switch happens at the database level with no stale cache window.

Setup

use Silverback\ApiComponentsBundle\Annotation as Silverback;
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
use Silverback\ApiComponentsBundle\Entity\Utility\PublishableTrait;

#[Silverback\Publishable]
#[ORM\Entity]
#[ApiResource(mercure: true)]
class Title extends AbstractComponent
{
    use PublishableTrait;

    #[ORM\Column(type: 'text', nullable: true)]
    public ?string $title = null;
}

That's all that's required. The trait and annotation together wire up the full lifecycle.

What PublishableTrait Adds

PropertyTypeDescription
publishedAt?DateTimeInterfacenull = unpublished draft
publishedResource?selfPoints from draft → published twin
draftResource?selfPoints from published → draft twin

The bundle creates and maintains these twins automatically. You never create the published twin manually.

How It Works at the API Level

The bundle intercepts requests and routes based on the caller's role and the published query parameter:

CallerGets
Anonymous visitorPublished version (or 404 if never published)
Admin in edit modeDraft version
Direct ?published=truePublished version explicitly

The Nuxt module handles the ?published=false parameter automatically in edit mode — you don't add it manually.

Publishing a Resource

Send a PATCH with publishedAt set to a date string:

PATCH /component/titles/018e-...
Content-Type: application/merge-patch+json

{
    "publishedAt": "2024-01-15T10:30:00+00:00"
}

The bundle creates or updates the published twin and broadcasts a Mercure update if mercure: true is on the entity.

To unpublish: PATCH with "publishedAt": null.

Publish-Only Validation

Some fields should only be required when going live, not while saving a draft:

#[ORM\Column(type: 'text', nullable: true)]
#[Assert\NotBlank(groups: ['Title:published'])]
public ?string $title = null;

The convention for the publish validation group is ClassName:published. The bundle applies this group automatically when publishedAt is being set.

Customise the group name:

#[Silverback\Publishable(validationGroups: ['my_custom_group'])]

Configuring Who Can Publish

Set the security expression in the bundle config. Only users matching this expression can set publishedAt:

silverback_api_components:
    publishable:
        permission: "is_granted('ROLE_ADMIN')"

Non-admin users can still PATCH a resource's other fields — they just can't flip the publish switch.

Custom Property Names

The default property names (publishedAt, publishedResource, draftResource) match what PublishableTrait declares. Override them when names conflict with your own fields:

#[Silverback\Publishable(
    fieldName: 'publicationDate',
    associationName: 'original',
    reverseAssociationName: 'draft'
)]

If you customise names, don't use PublishableTrait — declare the properties yourself using the matching names.

Migration Notes

PublishableTrait adds:

  • 1 nullable datetime column (published_at)
  • 2 nullable self-referencing foreign keys (published_resource_id, draft_resource_id)

Always review the generated Doctrine migration. The self-referencing keys use ON DELETE SET NULL so deleting a draft doesn't cascade to the published version.

API Response Metadata

Every publishable component response includes a _metadata.publishable object visible to admin users:

{
  "@id": "/component/titles/018e-...",
  "title": "My Heading",
  "_metadata": {
    "publishable": {
      "published": true,
      "publishedAt": "2024-01-15T10:30:00+00:00",
      "locationCount": 3
    }
  }
}
FieldTypeDescription
publishedbooleanWhether the published twin exists
publishedAtstring | nullISO 8601 timestamp of last publish, or null if unpublished
locationCountinteger | nullTotal number of places this component appears — counts both direct ComponentPosition references and AbstractPageData instances that hold it as a typed property (resolved via pageDataProperty positions at render time). Useful for showing admins "used in N places" before deleting.

locationCount is null for component types that have no getComponentPositions() method. Anonymous visitors do not receive _metadata at all.

Interaction with Mercure

When a resource is published (or unpublished), if mercure: true is on the #[ApiResource], the API broadcasts an update to the hub. Every browser that has loaded the resource receives the new content in real time with no page reload needed.