Upgrade to 1.0.0-rc823

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.

Content triggers are automations now

An automation used to watch contacts and a content trigger used to watch the site. They are one entity: an automation has a trigger sourcecontact, 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.

Run the schema update

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.

Renamed fields

WasIs
Automation::enrollWhenAutomation::triggerWhen
Automation::enrollFromAutomation::activeFrom
ContentTrigger::pageWhenAutomation::triggerWhen (with source: page)
ContentTrigger::segmentAutomation::recipientWhen
ContentTrigger::triggerFromAutomation::activeFrom
ContentTrigger::subjectTemplate / bodyTemplate / delayMinutesone AutomationStep

API

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.

Services

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.

Segments read dates stored in custom properties

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]}.

A bundle can register its own trigger source

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.

Newsletter entities expose properties, not accessors

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.