PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.37.0
MailPoet – Newsletters, Email Marketing, and Automation v5.37.0
5.37.0 5.36.1 5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Entities / SubscriberEntity.php
mailpoet / lib / Entities Last commit date
CustomFieldEntity.php 3 months ago DynamicSegmentFilterData.php 3 months ago DynamicSegmentFilterEntity.php 3 years ago FeatureFlagEntity.php 3 years ago FormEntity.php 1 month ago LogEntity.php 3 years ago NewsletterEntity.php 4 months ago NewsletterLinkEntity.php 3 years ago NewsletterOptionEntity.php 3 years ago NewsletterOptionFieldEntity.php 3 months ago NewsletterPostEntity.php 3 years ago NewsletterSegmentEntity.php 3 years ago NewsletterTemplateEntity.php 3 years ago ScheduledTaskEntity.php 2 months ago ScheduledTaskSubscriberEntity.php 1 year ago SegmentEntity.php 3 months ago SendingQueueEntity.php 4 months ago SettingEntity.php 3 years ago StatisticsBounceEntity.php 3 years ago StatisticsClickEntity.php 3 years ago StatisticsFormEntity.php 3 years ago StatisticsNewsletterEntity.php 1 year ago StatisticsOpenEntity.php 3 years ago StatisticsUnsubscribeEntity.php 3 months ago StatisticsWooCommercePurchaseEntity.php 2 years ago StatsNotificationEntity.php 3 years ago SubscriberCustomFieldEntity.php 3 years ago SubscriberEntity.php 2 days ago SubscriberIPEntity.php 3 years ago SubscriberSegmentEntity.php 3 years ago SubscriberTagEntity.php 4 years ago TagEntity.php 4 years ago UserAgentEntity.php 3 years ago UserFlagEntity.php 3 years ago WpPostEntity.php 2 years ago index.php 3 years ago
SubscriberEntity.php
883 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Entities;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use DateTimeInterface;
9 use MailPoet\Doctrine\EntityTraits\AutoincrementedIdTrait;
10 use MailPoet\Doctrine\EntityTraits\CreatedAtTrait;
11 use MailPoet\Doctrine\EntityTraits\DeletedAtTrait;
12 use MailPoet\Doctrine\EntityTraits\UpdatedAtTrait;
13 use MailPoet\Doctrine\EntityTraits\ValidationGroupsTrait;
14 use MailPoetVendor\Doctrine\Common\Collections\ArrayCollection;
15 use MailPoetVendor\Doctrine\Common\Collections\Collection;
16 use MailPoetVendor\Doctrine\Common\Collections\Criteria;
17 use MailPoetVendor\Doctrine\ORM\Mapping as ORM;
18 use MailPoetVendor\Symfony\Component\Validator\Constraints as Assert;
19
20 /**
21 * @ORM\Entity()
22 * @ORM\Table(name="subscribers")
23 * @ORM\HasLifecycleCallbacks
24 * @ORM\EntityListeners({"\MailPoet\Doctrine\EventListeners\SubscriberListener"})
25 */
26 class SubscriberEntity {
27 // hook names
28 public const HOOK_SUBSCRIBER_CREATED = 'mailpoet_subscriber_created';
29 public const HOOK_SUBSCRIBER_DELETED = 'mailpoet_subscriber_deleted';
30 public const HOOK_SUBSCRIBER_UPDATED = 'mailpoet_subscriber_updated';
31 public const HOOK_SUBSCRIBER_STATUS_CHANGED = 'mailpoet_subscriber_status_changed';
32 public const HOOK_MULTIPLE_SUBSCRIBERS_CREATED = 'mailpoet_multiple_subscribers_created';
33 public const HOOK_MULTIPLE_SUBSCRIBERS_DELETED = 'mailpoet_multiple_subscribers_deleted';
34 public const HOOK_MULTIPLE_SUBSCRIBERS_UPDATED = 'mailpoet_multiple_subscribers_updated';
35 public const HOOK_SUBSCRIBERS_COUNT_CHANGED = 'mailpoet_subscribers_count_changed';
36
37 // statuses
38 const STATUS_BOUNCED = 'bounced';
39 const STATUS_INACTIVE = 'inactive';
40 const STATUS_SUBSCRIBED = 'subscribed';
41 const STATUS_UNCONFIRMED = 'unconfirmed';
42 const STATUS_UNSUBSCRIBED = 'unsubscribed';
43
44 // tracking consent
45 const TRACKING_CONSENT_UNKNOWN = 'unknown';
46 const TRACKING_CONSENT_GRANTED = 'granted';
47 const TRACKING_CONSENT_DENIED = 'denied';
48
49 const TRACKING_CONSENT_VALUES = [
50 self::TRACKING_CONSENT_UNKNOWN,
51 self::TRACKING_CONSENT_GRANTED,
52 self::TRACKING_CONSENT_DENIED,
53 ];
54
55 const TRACKING_CONSENT_METHOD_FOOTER_LINK = 'footer_link';
56 const TRACKING_CONSENT_METHOD_MANAGE_PAGE = 'manage_page';
57 const TRACKING_CONSENT_METHOD_FORM = 'form';
58 const TRACKING_CONSENT_METHOD_ADMIN = 'admin';
59 const TRACKING_CONSENT_METHOD_IMPORT = 'import';
60 const TRACKING_CONSENT_METHOD_WOOCOMMERCE_CHECKOUT = 'woocommerce_checkout';
61 const TRACKING_CONSENT_METHOD_REGISTRATION = 'registration';
62 const TRACKING_CONSENT_METHOD_COMMENT = 'comment';
63 /**
64 * A caller of the public PHP API is itself the collection point: it rendered
65 * its own consent control, so only it knows what the subscriber was shown.
66 * Kept separate from METHOD_ADMIN so a consent record can tell "recorded by
67 * an integration" apart from "changed by a person in wp-admin".
68 */
69 const TRACKING_CONSENT_METHOD_API = 'api';
70
71 public const OBSOLETE_LINK_TOKEN_LENGTH = 6;
72 public const LINK_TOKEN_LENGTH = 32;
73 public const TIME_ZONE_FIELD_NAME = 'mailpoet_subscriber_timezone';
74 public const TIME_ZONE_SOURCE_FORM = 'form';
75 public const TIME_ZONE_SOURCE_MANUAL = 'manual';
76 public const TIME_ZONE_SOURCE_SITE_FALLBACK = 'site_fallback';
77 public const TIME_ZONE_CONFIDENCE_BROWSER = 90;
78 public const TIME_ZONE_CONFIDENCE_MANUAL = 100;
79
80 /** @var array<string,bool>|null */
81 private static $validTimeZones = null;
82
83 use AutoincrementedIdTrait;
84 use CreatedAtTrait;
85 use UpdatedAtTrait;
86 use DeletedAtTrait;
87 use ValidationGroupsTrait;
88
89 /**
90 * @ORM\Column(type="bigint", nullable=true)
91 * @var string|null
92 */
93 private $wpUserId;
94
95 /**
96 * @ORM\Column(type="boolean")
97 * @var bool
98 */
99 private $isWoocommerceUser = false;
100
101 /**
102 * CNIL/Garante: three states are legally distinct. `unknown` means we never
103 * asked — it is NOT consent, and under the opt-in regime it must not be
104 * treated as consent. How `unknown` is handled is a site setting; see
105 * TrackingConsentController.
106 *
107 * Validated in setTrackingConsent(), not by a validation constraint on this
108 * property. A constraint runs at flush, so a row that already holds a bad value —
109 * hand-edited column, incomplete migration, restored backup — would throw the moment
110 * anything flushed it, even an unrelated field. That took the whole mailpoet/v1
111 * namespace down for one customer. A caller trying to WRITE a bad value still fails,
112 * immediately, in the setter.
113 *
114 * @ORM\Column(type="string", length=20)
115 * @var string
116 */
117 private $trackingConsent = self::TRACKING_CONSENT_UNKNOWN;
118
119 /**
120 * @ORM\Column(type="datetimetz", nullable=true)
121 * @var DateTimeInterface|null
122 */
123 private $trackingConsentUpdatedAt;
124
125 /**
126 * @ORM\Column(type="string", length=40, nullable=true)
127 * @var string|null
128 */
129 private $trackingConsentMethod;
130
131 /**
132 * The exact wording shown when the choice was made. Required for proof of
133 * consent (CNIL §6: a record of each person's consent "as well as the
134 * conditions under which that consent was obtained").
135 *
136 * @ORM\Column(type="text", nullable=true)
137 * @var string|null
138 */
139 private $trackingConsentCopy;
140
141 /**
142 * @ORM\Column(type="string")
143 * @var string
144 */
145 private $firstName = '';
146
147 /**
148 * @ORM\Column(type="string")
149 * @var string
150 */
151 private $lastName = '';
152
153 /**
154 * @ORM\Column(type="string")
155 * @Assert\Email(groups={"Saving"})
156 * @Assert\NotBlank()
157 * @var string
158 */
159 private $email;
160
161 /**
162 * @ORM\Column(type="string")
163 * @var string
164 */
165 private $status = self::STATUS_UNCONFIRMED;
166
167 /**
168 * @ORM\Column(type="string", nullable=true)
169 * @var string|null
170 */
171 private $subscribedIp;
172
173 /**
174 * @ORM\Column(type="string", nullable=true)
175 * @var string|null
176 */
177 private $confirmedIp;
178
179 /**
180 * @ORM\Column(type="string", nullable=true)
181 * @var string|null
182 */
183 private $timeZone;
184
185 /**
186 * @ORM\Column(type="string", nullable=true)
187 * @var string|null
188 */
189 private $timeZoneSource;
190
191 /**
192 * @ORM\Column(type="integer", nullable=true)
193 * @var int|null
194 */
195 private $timeZoneConfidence;
196
197 /**
198 * @ORM\Column(type="datetimetz", nullable=true)
199 * @var DateTimeInterface|null
200 */
201 private $timeZoneUpdatedAt;
202
203 /**
204 * @ORM\Column(type="datetimetz", nullable=true)
205 * @var DateTimeInterface|null
206 */
207 private $confirmedAt;
208
209 /**
210 * @ORM\Column(type="datetimetz", nullable=true)
211 * @var DateTimeInterface|null
212 */
213 private $lastSubscribedAt;
214
215 /**
216 * @ORM\Column(type="datetimetz", nullable=true)
217 * @var DateTimeInterface|null
218 */
219 private $lastConfirmationEmailSentAt;
220
221 /**
222 * @ORM\Column(type="text", nullable=true)
223 * @var string|null
224 */
225 private $unconfirmedData;
226
227 /**
228 * @ORM\Column(type="string")
229 * @var string
230 */
231 private $source = 'unknown';
232
233 /**
234 * @ORM\Column(type="integer")
235 * @var int
236 */
237 private $countConfirmations = 0;
238
239 /**
240 * Denormalized number of subscribed memberships in non-deleted segments.
241 * Maintained by SegmentsCountRecalculator; used to quickly find subscribers
242 * without a list (segments_count = 0).
243 * @ORM\Column(type="integer", options={"unsigned":true})
244 * @var int
245 */
246 private $segmentsCount = 0;
247
248 /**
249 * @ORM\Column(type="string", nullable=true)
250 * @var string|null
251 */
252 private $unsubscribeToken;
253
254 /**
255 * @ORM\Column(type="string", nullable=true)
256 * @var string|null
257 */
258 private $linkToken;
259
260 /**
261 * @ORM\Column(type="float", nullable=true)
262 * @var float|null
263 */
264 private $engagementScore;
265
266 /**
267 * @ORM\Column(type="datetimetz", nullable=true)
268 * @var DateTimeInterface|null
269 */
270 private $engagementScoreUpdatedAt;
271
272 /**
273 * @ORM\Column(type="datetimetz", nullable=true)
274 * @var DateTimeInterface|null
275 */
276 private $lastEngagementAt;
277
278 /**
279 * @ORM\Column(type="datetimetz", nullable=true)
280 * @var DateTimeInterface|null
281 */
282 private $lastSendingAt;
283
284 /**
285 * @ORM\Column(type="datetimetz", nullable=true)
286 * @var DateTimeInterface|null
287 */
288 private $lastOpenAt;
289
290 /**
291 * @ORM\Column(type="datetimetz", nullable=true)
292 * @var DateTimeInterface|null
293 */
294 private $lastClickAt;
295
296 /**
297 * @ORM\Column(type="datetimetz", nullable=true)
298 * @var DateTimeInterface|null
299 */
300 private $lastPurchaseAt;
301
302 /**
303 * @ORM\Column(type="datetimetz", nullable=true)
304 * @var DateTimeInterface|null
305 */
306 private $lastPageViewAt;
307
308 /**
309 * @ORM\Column(type="datetimetz", nullable=true)
310 * @var DateTimeInterface|null
311 */
312 private $woocommerceSyncedAt;
313
314 /**
315 * @ORM\Column(type="integer")
316 * @var int
317 */
318 private $emailCount = 0;
319
320 /**
321 * @ORM\OneToMany(targetEntity="MailPoet\Entities\SubscriberSegmentEntity", mappedBy="subscriber", orphanRemoval=true)
322 * @var Collection<int, SubscriberSegmentEntity>
323 */
324 private $subscriberSegments;
325
326 /**
327 * @ORM\OneToMany(targetEntity="MailPoet\Entities\SubscriberCustomFieldEntity", mappedBy="subscriber", orphanRemoval=true)
328 * @var Collection<int, SubscriberCustomFieldEntity>
329 */
330 private $subscriberCustomFields;
331
332 /**
333 * @ORM\OneToMany(targetEntity="MailPoet\Entities\SubscriberTagEntity", mappedBy="subscriber", orphanRemoval=true)
334 * @var Collection<int, SubscriberTagEntity>
335 */
336 private $subscriberTags;
337
338 /**
339 * @ORM\OneToMany(targetEntity="MailPoet\Entities\ScheduledTaskSubscriberEntity", mappedBy="subscriber", orphanRemoval=true)
340 * @var Collection<int, ScheduledTaskSubscriberEntity>
341 */
342 private $scheduledTaskSubscribers;
343
344 public function __construct() {
345 $this->subscriberSegments = new ArrayCollection();
346 $this->subscriberCustomFields = new ArrayCollection();
347 $this->subscriberTags = new ArrayCollection();
348 $this->scheduledTaskSubscribers = new ArrayCollection();
349 }
350
351 /**
352 * @return int|null
353 */
354 public function getWpUserId() {
355 return $this->wpUserId ? (int)$this->wpUserId : null;
356 }
357
358 /**
359 * @param int|null $wpUserId
360 */
361 public function setWpUserId($wpUserId) {
362 $this->wpUserId = $wpUserId ? (string)$wpUserId : null;
363 }
364
365 public function isWPUser(): bool {
366 return $this->getWpUserId() > 0;
367 }
368
369 /**
370 * @return bool
371 */
372 public function getIsWoocommerceUser() {
373 return $this->isWoocommerceUser;
374 }
375
376 /**
377 * @param bool $isWoocommerceUser
378 */
379 public function setIsWoocommerceUser($isWoocommerceUser) {
380 $this->isWoocommerceUser = $isWoocommerceUser;
381 }
382
383 public function getTrackingConsent(): string {
384 return $this->trackingConsent;
385 }
386
387 /**
388 * Setting the state also stamps when, how, and against what wording it
389 * changed (CNIL/Garante record-keeping).
390 */
391
392 /**
393 * @throws \InvalidArgumentException if $consent is not one of the three
394 * TRACKING_CONSENT_* constants. This is the only place the value is validated;
395 * see the property docblock for why it is not a flush-time constraint.
396 */
397 public function setTrackingConsent(string $consent, ?string $method = null, ?string $copy = null): void {
398 if (!in_array($consent, [self::TRACKING_CONSENT_UNKNOWN, self::TRACKING_CONSENT_GRANTED, self::TRACKING_CONSENT_DENIED], true)) {
399 throw new \InvalidArgumentException("Invalid tracking consent value: {$consent}");
400 }
401 if ($this->trackingConsent === $consent) {
402 return;
403 }
404 $this->trackingConsent = $consent;
405 $this->trackingConsentUpdatedAt = new \DateTimeImmutable();
406 $this->trackingConsentMethod = $method;
407 $this->trackingConsentCopy = $copy;
408 }
409
410 public function getTrackingConsentUpdatedAt(): ?DateTimeInterface {
411 return $this->trackingConsentUpdatedAt;
412 }
413
414 public function getTrackingConsentMethod(): ?string {
415 return $this->trackingConsentMethod;
416 }
417
418 public function getTrackingConsentCopy(): ?string {
419 return $this->trackingConsentCopy;
420 }
421
422 /**
423 * @return string
424 */
425 public function getFirstName() {
426 return $this->firstName;
427 }
428
429 /**
430 * @param string $firstName
431 */
432 public function setFirstName($firstName) {
433 $this->firstName = $firstName;
434 }
435
436 /**
437 * @return string
438 */
439 public function getLastName() {
440 return $this->lastName;
441 }
442
443 /**
444 * @param string $lastName
445 */
446 public function setLastName($lastName) {
447 $this->lastName = $lastName;
448 }
449
450 /**
451 * @return string
452 */
453 public function getEmail() {
454 return $this->email;
455 }
456
457 /**
458 * @param string $email
459 */
460 public function setEmail($email) {
461 $this->email = $email;
462 }
463
464 /**
465 * @return string
466 */
467 public function getStatus() {
468 return $this->status;
469 }
470
471 /**
472 * @param string $status
473 */
474 public function setStatus($status) {
475 if (
476 !in_array($status, [
477 self::STATUS_BOUNCED,
478 self::STATUS_INACTIVE,
479 self::STATUS_SUBSCRIBED,
480 self::STATUS_UNCONFIRMED,
481 self::STATUS_UNSUBSCRIBED,
482 ])
483 ) {
484 throw new \InvalidArgumentException("Invalid status '{$status}' given to subscriber!");
485 }
486 $this->status = $status;
487 }
488
489 /**
490 * @return string|null
491 */
492 public function getSubscribedIp() {
493 return $this->subscribedIp;
494 }
495
496 /**
497 * @param string $subscribedIp
498 */
499 public function setSubscribedIp($subscribedIp) {
500 $this->subscribedIp = $subscribedIp;
501 }
502
503 /**
504 * @return string|null
505 */
506 public function getConfirmedIp() {
507 return $this->confirmedIp;
508 }
509
510 /**
511 * @param string|null $confirmedIp
512 */
513 public function setConfirmedIp($confirmedIp) {
514 $this->confirmedIp = $confirmedIp;
515 }
516
517 public function getTimeZone(): ?string {
518 return $this->timeZone;
519 }
520
521 public function setTimeZone(?string $timeZone): void {
522 $this->timeZone = $timeZone;
523 }
524
525 public function getTimeZoneSource(): ?string {
526 return $this->timeZoneSource;
527 }
528
529 public function setTimeZoneSource(?string $timeZoneSource): void {
530 $this->timeZoneSource = $timeZoneSource;
531 }
532
533 public function getTimeZoneConfidence(): ?int {
534 return $this->timeZoneConfidence;
535 }
536
537 public function setTimeZoneConfidence(?int $timeZoneConfidence): void {
538 $this->timeZoneConfidence = $timeZoneConfidence;
539 }
540
541 public function getTimeZoneUpdatedAt(): ?DateTimeInterface {
542 return $this->timeZoneUpdatedAt;
543 }
544
545 public function setTimeZoneUpdatedAt(?DateTimeInterface $timeZoneUpdatedAt): void {
546 $this->timeZoneUpdatedAt = $timeZoneUpdatedAt;
547 }
548
549 /**
550 * @param mixed $timeZone
551 */
552 public static function sanitizeTimeZone($timeZone): ?string {
553 if (!is_string($timeZone)) {
554 return null;
555 }
556 $timeZone = trim($timeZone);
557 if ($timeZone === '' || strlen($timeZone) > 64) {
558 return null;
559 }
560 return self::isValidTimeZone($timeZone) ? $timeZone : null;
561 }
562
563 /**
564 * @param mixed $timeZone
565 */
566 public static function isValidTimeZone($timeZone): bool {
567 if (!is_string($timeZone) || $timeZone === '') {
568 return false;
569 }
570 if (self::$validTimeZones === null) {
571 self::$validTimeZones = array_fill_keys(\DateTimeZone::listIdentifiers(), true);
572 }
573 return isset(self::$validTimeZones[$timeZone]);
574 }
575
576 /**
577 * @return DateTimeInterface|null
578 */
579 public function getConfirmedAt() {
580 return $this->confirmedAt;
581 }
582
583 /**
584 * @param DateTimeInterface|null $confirmedAt
585 */
586 public function setConfirmedAt($confirmedAt) {
587 $this->confirmedAt = $confirmedAt;
588 }
589
590 /**
591 * @return DateTimeInterface|null
592 */
593 public function getLastSubscribedAt() {
594 return $this->lastSubscribedAt;
595 }
596
597 /**
598 * @param DateTimeInterface|null $lastSubscribedAt
599 */
600 public function setLastSubscribedAt($lastSubscribedAt) {
601 $this->lastSubscribedAt = $lastSubscribedAt;
602 }
603
604 /**
605 * @return DateTimeInterface|null
606 */
607 public function getLastConfirmationEmailSentAt() {
608 return $this->lastConfirmationEmailSentAt;
609 }
610
611 /**
612 * @param DateTimeInterface|null $lastConfirmationEmailSentAt
613 */
614 public function setLastConfirmationEmailSentAt($lastConfirmationEmailSentAt) {
615 $this->lastConfirmationEmailSentAt = $lastConfirmationEmailSentAt;
616 }
617
618 /**
619 * @return string|null
620 */
621 public function getUnconfirmedData() {
622 return $this->unconfirmedData;
623 }
624
625 /**
626 * @param string|null $unconfirmedData
627 */
628 public function setUnconfirmedData($unconfirmedData) {
629 $this->unconfirmedData = $unconfirmedData;
630 }
631
632 /**
633 * @return string
634 */
635 public function getSource() {
636 return $this->source;
637 }
638
639 /**
640 * @param string $source
641 */
642 public function setSource($source) {
643 if (
644 !in_array($source, [
645 'api',
646 'form',
647 'unknown',
648 'imported',
649 'administrator',
650 'wordpress_user',
651 'wordpress_user_deleted',
652 'woocommerce_user',
653 'woocommerce_checkout',
654 ])
655 ) {
656 throw new \InvalidArgumentException("Invalid source '{$source}' given to subscriber!");
657 }
658 $this->source = $source;
659 }
660
661 /**
662 * @return int
663 */
664 public function getConfirmationsCount() {
665 return $this->countConfirmations;
666 }
667
668 /**
669 * @param int $countConfirmations
670 */
671 public function setConfirmationsCount($countConfirmations) {
672 $this->countConfirmations = $countConfirmations;
673 }
674
675 public function getSegmentsCount(): int {
676 return $this->segmentsCount;
677 }
678
679 public function setSegmentsCount(int $segmentsCount): void {
680 $this->segmentsCount = $segmentsCount;
681 }
682
683 /**
684 * @return string|null
685 */
686 public function getUnsubscribeToken() {
687 return $this->unsubscribeToken;
688 }
689
690 /**
691 * @param string|null $unsubscribeToken
692 */
693 public function setUnsubscribeToken($unsubscribeToken) {
694 $this->unsubscribeToken = $unsubscribeToken;
695 }
696
697 /**
698 * @return string|null
699 */
700 public function getLinkToken() {
701 return $this->linkToken;
702 }
703
704 /**
705 * @param string|null $linkToken
706 */
707 public function setLinkToken($linkToken) {
708 $this->linkToken = $linkToken;
709 }
710
711 /**
712 * @return Collection<int, SubscriberSegmentEntity>
713 */
714 public function getSubscriberSegments(?string $status = null) {
715 if (!is_null($status)) {
716 $criteria = Criteria::create()
717 ->where(Criteria::expr()->eq('status', $status));
718 $subscriberSegments = $this->subscriberSegments->matching($criteria);
719 } else {
720 $subscriberSegments = $this->subscriberSegments;
721 }
722
723 return $subscriberSegments;
724 }
725
726 /** * @return Collection<int, SegmentEntity> */
727 public function getSegments() {
728 return $this->subscriberSegments->map(function (?SubscriberSegmentEntity $subscriberSegment = null) {
729 if (!$subscriberSegment) return null;
730 return $subscriberSegment->getSegment();
731 })->filter(function (?SegmentEntity $segment = null) {
732 return $segment !== null;
733 });
734 }
735
736 /**
737 * @return Collection<int, SubscriberCustomFieldEntity>
738 */
739 public function getSubscriberCustomFields() {
740 return $this->subscriberCustomFields;
741 }
742
743 public function getSubscriberCustomField(CustomFieldEntity $customField): ?SubscriberCustomFieldEntity {
744 $criteria = Criteria::create()
745 ->where(Criteria::expr()->eq('customField', $customField))
746 ->setMaxResults(1);
747 return $this->getSubscriberCustomFields()->matching($criteria)->first() ?: null;
748 }
749
750 /**
751 * @return Collection<int, SubscriberTagEntity>
752 */
753 public function getSubscriberTags() {
754 return $this->subscriberTags;
755 }
756
757 public function getSubscriberTag(TagEntity $tag): ?SubscriberTagEntity {
758 $criteria = Criteria::create()
759 ->where(Criteria::expr()->eq('tag', $tag))
760 ->setMaxResults(1);
761 return $this->getSubscriberTags()->matching($criteria)->first() ?: null;
762 }
763
764 /**
765 * @return float|null
766 */
767 public function getEngagementScore(): ?float {
768 return $this->engagementScore;
769 }
770
771 /**
772 * @param float|null $engagementScore
773 */
774 public function setEngagementScore(?float $engagementScore): void {
775 $this->engagementScore = $engagementScore;
776 }
777
778 /**
779 * @return DateTimeInterface|null
780 */
781 public function getEngagementScoreUpdatedAt(): ?DateTimeInterface {
782 return $this->engagementScoreUpdatedAt;
783 }
784
785 /**
786 * @param DateTimeInterface|null $engagementScoreUpdatedAt
787 */
788 public function setEngagementScoreUpdatedAt(?DateTimeInterface $engagementScoreUpdatedAt): void {
789 $this->engagementScoreUpdatedAt = $engagementScoreUpdatedAt;
790 }
791
792 public function getLastEngagementAt(): ?DateTimeInterface {
793 return $this->lastEngagementAt;
794 }
795
796 /**
797 * Sets the raw engagement timestamp without touching status. Prefer markEngaged() when
798 * recording a real engagement event (open, click, purchase, page view) so inactive
799 * subscribers are reactivated immediately instead of waiting for the maintenance cron.
800 */
801 public function setLastEngagementAt(DateTimeInterface $lastEngagementAt): void {
802 $this->lastEngagementAt = $lastEngagementAt;
803 }
804
805 /**
806 * Records engagement and immediately reactivates the subscriber if they were inactive,
807 * so they don't wait for the InactiveSubscribersMaintenance cron to be reactivated.
808 */
809 public function markEngaged(DateTimeInterface $engagedAt): void {
810 $this->setLastEngagementAt($engagedAt);
811 if ($this->getStatus() === self::STATUS_INACTIVE) {
812 $this->setStatus(self::STATUS_SUBSCRIBED);
813 }
814 }
815
816 public function getLastSendingAt(): ?DateTimeInterface {
817 return $this->lastSendingAt;
818 }
819
820 public function setLastSendingAt(?DateTimeInterface $dateTime): void {
821 $this->lastSendingAt = $dateTime;
822 }
823
824 public function getLastOpenAt(): ?DateTimeInterface {
825 return $this->lastOpenAt;
826 }
827
828 public function setLastOpenAt(?DateTimeInterface $dateTime): void {
829 $this->lastOpenAt = $dateTime;
830 }
831
832 public function getLastClickAt(): ?DateTimeInterface {
833 return $this->lastClickAt;
834 }
835
836 public function setLastClickAt(?DateTimeInterface $dateTime): void {
837 $this->lastClickAt = $dateTime;
838 }
839
840 public function getLastPurchaseAt(): ?DateTimeInterface {
841 return $this->lastPurchaseAt;
842 }
843
844 public function setLastPurchaseAt(?DateTimeInterface $dateTime): void {
845 $this->lastPurchaseAt = $dateTime;
846 }
847
848 public function getLastPageViewAt(): ?DateTimeInterface {
849 return $this->lastPageViewAt;
850 }
851
852 public function setLastPageViewAt(?DateTimeInterface $dateTime): void {
853 $this->lastPageViewAt = $dateTime;
854 }
855
856 public function setWoocommerceSyncedAt(?DateTimeInterface $woocommerceSyncedAt): void {
857 $this->woocommerceSyncedAt = $woocommerceSyncedAt;
858 }
859
860 public function getWoocommerceSyncedAt(): ?DateTimeInterface {
861 return $this->woocommerceSyncedAt;
862 }
863
864 public function getEmailCount(): int {
865 return $this->emailCount;
866 }
867
868 public function setEmailCount(int $emailCount): void {
869 $this->emailCount = $emailCount;
870 }
871
872 /** @ORM\PreFlush */
873 public function cleanupSubscriberSegments(): void {
874 // Delete old orphan SubscriberSegments to avoid errors on update
875 $this->subscriberSegments->map(function (?SubscriberSegmentEntity $subscriberSegment = null) {
876 if (!$subscriberSegment) return null;
877 if ($subscriberSegment->getSegment() === null) {
878 $this->subscriberSegments->removeElement($subscriberSegment);
879 }
880 });
881 }
882 }
883