1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Spatie\EmailCampaigns\Models; |
4
|
|
|
|
5
|
|
|
use Carbon\Carbon; |
6
|
|
|
use Illuminate\Database\Eloquent\Model; |
7
|
|
|
use Illuminate\Database\Eloquent\Builder; |
8
|
|
|
use Spatie\EmailCampaigns\Enums\CampaignStatus; |
9
|
|
|
use Spatie\EmailCampaigns\Jobs\SendCampaignJob; |
10
|
|
|
use Spatie\EmailCampaigns\Jobs\SendTestMailJob; |
11
|
|
|
use Spatie\EmailCampaigns\Mails\CampaignMailable; |
12
|
|
|
use Spatie\EmailCampaigns\Models\Concerns\HasUuid; |
13
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany; |
14
|
|
|
use Spatie\EmailCampaigns\Support\Segments\Segment; |
15
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo; |
16
|
|
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough; |
17
|
|
|
use Spatie\EmailCampaigns\Exceptions\CouldNotSendCampaign; |
18
|
|
|
use Spatie\EmailCampaigns\Support\Segments\EverySubscriber; |
19
|
|
|
use Spatie\EmailCampaigns\Exceptions\CampaignCouldNotUpdate; |
20
|
|
|
use Spatie\EmailCampaigns\Http\Controllers\CampaignWebviewController; |
21
|
|
|
|
22
|
|
|
class Campaign extends Model |
23
|
|
|
{ |
24
|
|
|
use HasUuid; |
25
|
|
|
|
26
|
|
|
public $table = 'email_campaigns'; |
27
|
|
|
|
28
|
|
|
protected $guarded = []; |
29
|
|
|
|
30
|
|
|
public $casts = [ |
31
|
|
|
'track_opens' => 'boolean', |
32
|
|
|
'track_clicks' => 'boolean', |
33
|
|
|
'open_rate' => 'integer', |
34
|
|
|
'click_rate' => 'integer', |
35
|
|
|
'send_to_number_of_subscribers' => 'integer', |
36
|
|
|
'sent_at' => 'datetime', |
37
|
|
|
'requires_double_opt_in' => 'boolean', |
38
|
|
|
'statistics_calculated_at' => 'datetime', |
39
|
|
|
]; |
40
|
|
|
|
41
|
|
|
public static function boot() |
42
|
|
|
{ |
43
|
|
|
parent::boot(); |
44
|
|
|
|
45
|
|
|
static::creating(function (Campaign $campaign) { |
46
|
|
|
if (! $campaign->status) { |
47
|
|
|
$campaign->status = CampaignStatus::DRAFT; |
48
|
|
|
} |
49
|
|
|
}); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
public static function scopeSentBetween(Builder $query, Carbon $periodStart, Carbon $periodEnd): void |
53
|
|
|
{ |
54
|
|
|
$query |
55
|
|
|
->where('sent_at', '>=', $periodStart) |
56
|
|
|
->where('sent_at', '<', $periodEnd); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
public function emailList(): BelongsTo |
60
|
|
|
{ |
61
|
|
|
return $this->belongsTo(EmailList::class); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
public function links(): HasMany |
65
|
|
|
{ |
66
|
|
|
return $this->hasMany(CampaignLink::class, 'email_campaign_id'); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
public function clicks(): HasManyThrough |
70
|
|
|
{ |
71
|
|
|
return $this->hasManyThrough(CampaignClick::class, CampaignLink::class, 'email_campaign_id'); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
public function opens(): HasMany |
75
|
|
|
{ |
76
|
|
|
return $this->hasMany(CampaignOpen::class, 'email_campaign_id'); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
public function sends(): HasMany |
80
|
|
|
{ |
81
|
|
|
return $this->hasMany(CampaignSend::class, 'email_campaign_id'); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
public function unsubscribes(): HasMany |
85
|
|
|
{ |
86
|
|
|
return $this->hasMany(CampaignUnsubscribe::class, 'email_campaign_id'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
public function bounces(): HasManyThrough |
90
|
|
|
{ |
91
|
|
|
return $this->hasManyThrough(CampaignSendBounce::class, CampaignSend::class, 'email_campaign_id'); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
public function subject(string $subject) |
95
|
|
|
{ |
96
|
|
|
$this->ensureUpdatable(); |
97
|
|
|
|
98
|
|
|
$this->update(compact('subject')); |
99
|
|
|
|
100
|
|
|
return $this; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
public function trackOpens(bool $bool = true) |
104
|
|
|
{ |
105
|
|
|
$this->ensureUpdatable(); |
106
|
|
|
|
107
|
|
|
$this->update(['track_opens' => $bool]); |
108
|
|
|
|
109
|
|
|
return $this; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
public function trackClicks(bool $bool = true) |
113
|
|
|
{ |
114
|
|
|
$this->ensureUpdatable(); |
115
|
|
|
|
116
|
|
|
$this->update(['track_clicks' => $bool]); |
117
|
|
|
|
118
|
|
|
return $this; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
public function useMailable(string $mailableClass) |
122
|
|
|
{ |
123
|
|
|
$this->ensureUpdatable(); |
124
|
|
|
|
125
|
|
|
if (! is_a($mailableClass, CampaignMailable::class, true)) { |
126
|
|
|
throw CouldNotSendCampaign::invalidMailableClass($this, $mailableClass); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$this->update(['mailable_class' => $mailableClass]); |
130
|
|
|
|
131
|
|
|
return $this; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
public function useSegment(string $segmentClass) |
135
|
|
|
{ |
136
|
|
|
$this->ensureUpdatable(); |
137
|
|
|
|
138
|
|
|
if (! is_a($segmentClass, Segment::class, true)) { |
139
|
|
|
throw CouldNotSendCampaign::invalidSegmentClass($this, $segmentClass); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
$this->update(['segment_class' => $segmentClass]); |
143
|
|
|
|
144
|
|
|
return $this; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
public function to(EmailList $emailList) |
148
|
|
|
{ |
149
|
|
|
$this->ensureUpdatable(); |
150
|
|
|
|
151
|
|
|
$this->update(['email_list_id' => $emailList->id]); |
152
|
|
|
|
153
|
|
|
return $this; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
public function content(string $html) |
157
|
|
|
{ |
158
|
|
|
$this->ensureUpdatable(); |
159
|
|
|
|
160
|
|
|
$this->update(compact('html')); |
161
|
|
|
|
162
|
|
|
return $this; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
public function send() |
166
|
|
|
{ |
167
|
|
|
$this->ensureSendable(); |
168
|
|
|
|
169
|
|
|
$this->markAsSending(); |
170
|
|
|
|
171
|
|
|
dispatch(new SendCampaignJob($this, $this->emailList)); |
|
|
|
|
172
|
|
|
|
173
|
|
|
return $this; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
public function sendTo(EmailList $emailList) |
177
|
|
|
{ |
178
|
|
|
return $this->to($emailList)->send(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
protected function ensureSendable() |
182
|
|
|
{ |
183
|
|
|
if ($this->status === CampaignStatus::SENDING) { |
184
|
|
|
throw CouldNotSendCampaign::beingSent($this); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
if ($this->status === CampaignStatus::SENT) { |
188
|
|
|
throw CouldNotSendCampaign::alreadySent($this); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
if (is_null($this->emailList)) { |
192
|
|
|
throw CouldNotSendCampaign::noListSet($this); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
if (! is_null($this->mailable)) { |
196
|
|
|
return; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
if (empty($this->subject)) { |
200
|
|
|
throw CouldNotSendCampaign::noSubjectSet($this); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if (empty($this->html)) { |
204
|
|
|
throw CouldNotSendCampaign::noContent($this); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
protected function ensureUpdatable(): void |
209
|
|
|
{ |
210
|
|
|
if ($this->status === CampaignStatus::SENDING) { |
211
|
|
|
throw CampaignCouldNotUpdate::beingSent($this); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
if ($this->status === CampaignStatus::SENT) { |
215
|
|
|
throw CouldNotSendCampaign::alreadySent($this); |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
private function markAsSending() |
220
|
|
|
{ |
221
|
|
|
$this->update(['status' => CampaignStatus::SENDING]); |
222
|
|
|
|
223
|
|
|
return $this; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
public function markAsSent(int $numberOfSubscribers) |
227
|
|
|
{ |
228
|
|
|
$this->update([ |
229
|
|
|
'status' => CampaignStatus::SENT, |
230
|
|
|
'sent_at' => now(), |
231
|
|
|
'statistics_calculated_at' => now(), |
232
|
|
|
'sent_to_number_of_subscribers' => $numberOfSubscribers, |
233
|
|
|
]); |
234
|
|
|
|
235
|
|
|
return $this; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
public function wasAlreadySent(): bool |
239
|
|
|
{ |
240
|
|
|
return $this->status === CampaignStatus::SENT; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* @param $email string|array|\Illuminate\Support\Collection |
245
|
|
|
*/ |
246
|
|
|
public function sendTestMail($emails) |
247
|
|
|
{ |
248
|
|
|
collect($emails)->each(function (string $email) { |
249
|
|
|
dispatch(new SendTestMailJob($this, $email)); |
250
|
|
|
}); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
public function webViewUrl(): string |
254
|
|
|
{ |
255
|
|
|
return url(action(CampaignWebviewController::class, $this->uuid)); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
public function getMailable(): CampaignMailable |
259
|
|
|
{ |
260
|
|
|
$mailableClass = $this->mailable_class ?? CampaignMailable::class; |
261
|
|
|
|
262
|
|
|
return app($mailableClass); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
public function getSegment(): Segment |
266
|
|
|
{ |
267
|
|
|
$segmentClass = $this->segment_class ?? EverySubscriber::class; |
268
|
|
|
|
269
|
|
|
return app($segmentClass); |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.