contenir / contenir-audience-mezzio
The site half of contenir/contenir-audience for Mezzio: a Subscriptions service a sign-up handler calls, which writes the sign-up to the add-on's outbox in the site's database, wired from the same bindings the CMS add-on reads.
Package info
github.com/contenir/contenir-audience-mezzio
pkg:composer/contenir/contenir-audience-mezzio
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- contenir/contenir-audience: ^0.1
- php-db/phpdb: ^0.6.0
- psr/clock: ^1.0
- psr/container: ^1.1 || ^2.0
Requires (Dev)
- contenir/contenir-qa-tools: 0.1.x-dev
- infection/infection: ^0.34.1
- laminas/laminas-servicemanager: ^3.22 || ^4.0
- php-db/phpdb-mysql: 0.5.x-dev
- php-db/phpdb-sqlite: 0.3.x-dev
- phpunit/phpunit: ^11.5.42
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 05:56:19 UTC
README
The site half of contenir/contenir-audience for a Mezzio site. A
sign-up handler hands its form to Subscriptions, which writes it to the newsletter add-on's outbox in the site's
own database. The CMS's queue worker sends it to Campaign Monitor or Mailchimp later, so a slow or failing service
never reaches the visitor: the only thing that can fail on the page is writing that one row.
- One binding definition. The site reads the add-on's bindings from
extensions.<handle>.bindings, the key the CMS add-on reads, so the site and the CMS share one definition of which list a form feeds and which fields go where. - Consent goes with the sign-up. Each message carries the wording the visitor agreed to, the time and their address. The worker records it in the add-on's consent ledger once the person is on the list, and applies the library's consent policy and suppression as it sends.
- Nobody who complained or was erased is queued again. Their sign-up is withheld before any row is written, so their address never sits in the outbox. Someone who unsubscribed or bounced is queued, and the form's consent counts as fresh.
- The visitor is always thanked.
subscribe()says whether it queued or withheld a sign-up, but the page should say the same either way, or the form would tell anyone which addresses once complained.
There's no laminas-mvc variant yet: one follows when a Laminas site needs it.
Requirements
- PHP 8.3, 8.4 or 8.5
- php-db/phpdb 0.6, with the site's database as a php-db adapter service
- The newsletter add-on enabled in the CMS, which creates its tables in the site's database
Install
composer require contenir/contenir-audience-mezzio
Neither this package nor contenir/contenir-audience is on Packagist yet, so the site's composer.json lists both
repositories:
"repositories": [ {"type": "vcs", "url": "https://github.com/contenir/contenir-audience"}, {"type": "vcs", "url": "https://github.com/contenir/contenir-audience-mezzio"} ]
php-db 0.6 has no stable release yet, so the site also needs "minimum-stability": "dev" with
"prefer-stable": true. With laminas-component-installer, the ConfigProvider is added for you; otherwise add
Contenir\Audience\Mezzio\ConfigProvider::class to config/config.php.
Configure
The CMS add-on keeps its bindings and pepper in config/autoload/extensions/<handle>.global.php, with
<handle>.local.php laid over it. The site reads the same two files, so there is one definition. Mezzio's default
glob doesn't reach that folder, so add a provider for it after the site's own autoload one in
config/config.php:
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), new PhpFileProvider(realpath(__DIR__) . '/autoload/extensions/{{,*.}global,{,*.}local}.php'),
The add-on's file holds its bindings:
// config/autoload/extensions/campaign-monitor.global.php return ['extensions' => ['campaign-monitor' => ['bindings' => [ 'newsletter' => [ 'list' => 'a1b2c3', 'fields' => ['Name' => 'name'], 'consent' => 'Signing up to the newsletter', 'options' => ['consent_to_track' => 'Yes'], ], ]]]];
// config/autoload/extensions/campaign-monitor.local.php, not committed return ['extensions' => ['campaign-monitor' => ['pepper' => '…the add-on\'s pepper…']]];
And the site names the add-on it signs people up through:
// config/autoload/audience.global.php return [ 'contenir_audience' => [ 'handle' => 'campaign-monitor', // the add-on: its tables are ext_campaign_monitor_* 'adapter' => PhpDb\Adapter\Adapter::class, // the site's database; default AdapterInterface::class 'clock' => null, // a PSR-20 clock service; default the system's time ], ];
The pepper must be the one the CMS add-on uses, at least 32 bytes. Everything kept after a message is sent is keyed by a peppered hash of the email, so with another pepper the site's hashes would never match the CMS's. That would let a sign-up through for someone suppressed for good.
A binding's keys are the library's (see BindingReader). An unknown key is refused, so a typo can't pass unnoticed.
Use
Call subscribe() from the site's own handler once its form is valid:
use Contenir\Audience\Exception\StorageException; use Contenir\Audience\Mezzio\Subscriptions; final class SubscribeHandler implements RequestHandlerInterface { public function __construct(private Subscriptions $subscriptions, /* … */) {} public function handle(ServerRequestInterface $request): ResponseInterface { // … validate the form … try { $this->subscriptions->subscribe( 'newsletter', $data['email'], ['name' => $data['name'] ?? ''], ip: $request->getServerParams()['REMOTE_ADDR'] ?? null, ); } catch (StorageException) { // the outbox row couldn't be written: the one failure to show the visitor } // … render "thanks" … } }
$valuesholds the form's other fields. The binding'sfieldsdecide which are sent and how each is changed on the way. The email given replaces any field of the same name.wordingis the text the form showed beside its button. If it's left out, the binding'sconsentis used. A sign-up with neither is refused, as the policy would treat it as an update.subscribe()throwsUnknownBindingwhen the site has no binding by that name, andInvalidBindingwhen no wording can be found. Both are mistakes in the site's set-up. It throwsInvalidEmailfor an address the form should already have refused, andStorageExceptionwhen the row can't be written.
Development
composer check
runs formatting, lint and static analysis (Mago), the unit and integration suites, and mutation testing (Infection).
The integration suite runs on SQLite, then on MySQL when CONTENIR_TEST_DB_* names one.