Concerns: pushword/newsletter
Two breaking changes in one release, both confined to the newsletter bundle: content triggers are gone as a separate thing, and the newsletter entities expose their state as properties instead of accessors.
An automation used to watch contacts and a content trigger used to watch the site. They are one entity: an automation has a trigger source — contact, page, or one a bundle of yours registers — and a sequence of steps. A page trigger is an automation whose source is page, and it gains what only drips had: several steps, each after its own delay.
What that unlocks, and the reason for the merge: recipientWhen is resolved when each step's campaign is armed, so a publication can mail a state of the reader — every article, but only to subscribers we have not seen in a month:
{
"source": "page",
"triggerWhen": [{"field": "ancestor", "op": "=", "value": "blog"}],
"recipientWhen": [{"field": "prop.lastSeenAt", "op": "olderThan", "value": "30d"}]
}
See Newsletter.
php bin/console doctrine:schema:update --force
Then drop what the merge left behind:
DROP TABLE newsletter_content_trigger_log;
DROP TABLE newsletter_content_trigger;
Existing rows are not migrated. No converter ships with this release: content triggers were young enough that writing one would have been guesswork. Re-create each of them as an automation with source: page, its pageWhen as triggerWhen, its segment as recipientWhen, and its subject/body/delay as a single step. Set activeFrom to the old triggerFrom so the back catalogue stays unmailed.
Automations keep their rows, but not all of their columns. newsletter_automation gains source (defaults to contact), trigger_when, hosts and recipient_when; enroll_when and enroll_from are dropped. doctrine:schema:update drops and adds — it does not rename, so an existing automation comes back with an empty rule and an active_from of today. Rename them yourself before running it if you have rows worth keeping:
ALTER TABLE newsletter_automation RENAME COLUMN enroll_when TO trigger_when;
ALTER TABLE newsletter_automation RENAME COLUMN enroll_from TO active_from;
Then run the schema update, which adds what is left.
| Was | Is |
|---|---|
Automation::enrollWhen | Automation::triggerWhen |
Automation::enrollFrom | Automation::activeFrom |
ContentTrigger::pageWhen | Automation::triggerWhen (with source: page) |
ContentTrigger::segment | Automation::recipientWhen |
ContentTrigger::triggerFrom | Automation::activeFrom |
ContentTrigger::subjectTemplate / bodyTemplate / delayMinutes | one AutomationStep |
The /api/newsletter/content-trigger endpoints are removed; everything moves under /api/newsletter/automation with the new source, triggerWhen, hosts and recipientWhen fields. A GET reports waiting, handled and matchingContacts in place of waitingPages and campaignsCreated.
ContentTriggerRunner and PageMatcher are gone; AutomationRunner does both jobs (trigger(), cancelStale(), advance()). NewsletterMailer::sendStep() now takes the rendered subject and body, because what a step quotes comes from the occurrence that enrolled the contact.
pw:newsletter:tick reports enrolled, scheduled, cancelled, armed, campaignMails and dripMails; triggered and automationMails are gone.
prop.<key> accepts olderThan and newerThan on a contact, alongside =, !=, isSet and isNotSet:
[{"field": "prop.lastSeenAt", "op": "olderThan", "value": "30d"}]
The comparison is lexical, which is what lets it work without declaring a type: store the value as an ISO-8601 instant, always in the same offset — write UTC. Two spellings of the same instant do not compare as one. A property nobody ever wrote is on neither side of the operator; say "dormant or never seen" with {"any": [older-than, isNotSet]}.
Implement Pushword\Newsletter\Trigger\TriggerSource, tag it pushword.newsletter.trigger_source, and anything your application watches — orders, bookings, customers — can start a mail sequence, with the admin screen, the steps and the reporting every other automation uses. See Custom trigger sources.
Following core in rc799, the newsletter entities drop their getter/setter pairs for PHP 8.4 property hooks. Only code that reads or writes these entities directly is affected — templates using campaign.subject already worked.
-$campaign->getSubject();
-$campaign->setSubject('Janvier');
+$campaign->subject;
+$campaign->subject = 'Janvier';
Fluent construction goes with them; assign instead:
-$campaign = new Campaign()
- ->setAudience($audience)
- ->setSubject('Janvier');
+$campaign = new Campaign();
+$campaign->audience = $audience;
+$campaign->subject = 'Janvier';
What a send writes about itself is private(set) — status, sentAt, the counters, Campaign::$automation — and still changes only through the methods that name the event: schedule(), markSending(), incrementSent(), triggeredBy(). Behaviour that was never a plain accessor stays a method: isSubscribed(), optIn(), unsubscribe(), getEffectiveRateSeconds(), getStatusLabel(), Audience::filterInterests().
Two normalising setters kept their old shape as hooks rather than methods, so they still normalise on assignment: Audience::$slug lowercases, Audience::$utmSource slugifies, Contact::$email lowercases and trims, Campaign::$rateSeconds floors at one second.