contenir / contenir-preview-laminas-mvc
Laminas MVC renderer for the Contenir preview protocol: answers the CMS's signed previews at /api/resource/preview by rendering the record's page with its unsaved values.
Package info
github.com/contenir/contenir-preview-laminas-mvc
pkg:composer/contenir/contenir-preview-laminas-mvc
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- contenir/contenir-preview: 0.1.x-dev
- laminas/laminas-eventmanager: ^3.13
- laminas/laminas-http: ^2.19
- laminas/laminas-mvc: ^3.8
- laminas/laminas-router: ^3.13
- laminas/laminas-servicemanager: ^3.22
- laminas/laminas-stdlib: ^3.21
- laminas/laminas-view: ^2.30
- psr/clock: ^1.0
- psr/container: ^1.1 || ^2.0
- psr/simple-cache: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- contenir/contenir-qa-tools: 0.1.x-dev
- infection/infection: ^0.35.6
- laminas/laminas-modulemanager: ^2.15
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-18 02:15:51 UTC
README
Renders the Contenir CMS's previews in a Laminas MVC application: the
preview protocol from
contenir/contenir-preview, wired into the MVC's events.
The CMS posts a signed token for the record being edited to /api/resource/preview on the site.
This module verifies it before the router runs, works out which page shows the record, and lets the
application render that page as it always would, with the token's values in place of the stored
record's. The answer carries the protocol's headers (never cached, indexed, or framed by anyone but
the CMS) and a small "Preview" badge.
Install
composer require contenir/contenir-preview-laminas-mvc
Add Contenir\Preview\Laminas\Mvc to the application's modules. Until the application has a
contenir_preview configuration, the module registers its services and does nothing else.
Configure
// config/autoload/preview.global.php (the secret in preview.local.php, out of version control) return [ 'contenir_preview' => [ 'issuer' => 'https://admin.example.com', 'audience' => 'https://www.example.com', 'secrets' => ['2026-10' => getenv('CONTENIR_PREVIEW_SECRET')], 'sign_with' => '2026-10', 'resolvers' => [ 'site.page.home' => '/{slug}', 'library.news.articles' => ['route' => 'news/article', 'params' => ['slug' => '{slug}']], 'site.page.landing' => App\Preview\LandingResolver::class, ], ], ];
issuer, audience, secrets and sign_with are the same on the CMS and the site.
The module registers the services contenir-preview's ConfigProvider builds from these settings,
and its own listener, view helper and controller plugin.
The CMS chooses which sections offer a Preview; the site says, per section, which page shows one of its records. Each resolver is one of:
| Resolver | The page |
|---|---|
A path, beginning / |
That path, with each {field} filled from the record's values. |
['route' => name, 'params' => [...]] |
The named route, assembled with the parameters filled the same way. |
The name of a PreviewResolverInterface service |
Whatever it answers: a path, a route, or a handler (below). |
A section the CMS previews but the site has no resolver for is answered 404, "This section has no
preview on this site".
Handlers: drafts, new records and workflow sites
A path or route reaches only the pages the router knows. A resolver service can instead answer
PreviewTarget::handler(SomeController::class, ['action' => 'show', ...]), and the module dispatches
that controller with those parameters without routing. That is how to preview a record not saved
yet, a draft at an address not live, or a site whose routes are built from its published records
(contenir-mvc-workflow).
Other settings
| Setting | Default | |
|---|---|---|
replay.cache |
none | A PSR-16 cache service; use one when the site runs on several servers. |
replay.directory |
the system's temporary directory | Where accepted tokens are remembered otherwise; it must exist. |
clock |
the system's | A PSR-20 clock service. |
marker |
true |
The visible "Preview" badge on HTML pages. |
Use the preview's values
Wherever the site loads a previewable section's record, lay the preview over it. In a controller:
$page = $this->preview()->overlay('site.page.home', ['resource_id' => $row['resource_id']], $row);
overlay() returns the stored record unchanged outside a preview, or for any other record. For a
record not saved yet, $this->preview()->unsaved('site.page.home') gives its values (or null).
In a template, $this->preview() gives the same context, so a page can tell a preview from a visit:
<?php if ($this->preview()->isActive()): ?>…<?php endif ?>
How it runs
EVENT_ROUTEat priority 100, before the router: a request to{base}/api/resource/previewis opened. Anything but a validPOSTis answered there and then with the protocol's status and a short page saying why.- The resolved page becomes the request, as a
GETwith the token's post cleared. A path or route moves the request to that address, where the router must have a page; a handler, which no route reaches, gets a route of its own at the preview endpoint for this request only (TargetMatcher::HANDLER_ROUTE). The route event then carries on: the router matches the page and every other route listener runs as it would on a visit, the site's own among them (such as contenir-resource-laminas-mvc's, which loads the routed resource). - Page caches are asked to stand aside, because a cache that keys on a
GETto a path would otherwise see the preview as a visit to the page: it could serve the stored page in place of the preview, or store the editor's unsaved page for visitors. The application triggerspagecache.disable(PreviewListener::CACHE_DISABLE_EVENT), which a Laminas MVC page cache such ascontenir/cache-laminas-mvclistens for, and the request carriesCache-Control: no-cache, no-storein place of the browser's own, so a cache that follows HTTP's rules neither serves a stored copy nor stores the answer. A refusal is answered before this and never reaches a cache. - The application routes, dispatches and renders as normal.
EVENT_FINISH: a preview's response gets the protocol's headers, replacing the page's own, and the badge before</body>.
Quality
composer check # Mago format, lint and analyze, then the unit and integration tests composer mutation-test # Infection