1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Reallyli\AB; |
4
|
|
|
|
5
|
|
|
use Illuminate\Http\Request; |
6
|
|
|
use Reallyli\AB\Models\Goal; |
7
|
|
|
use Reallyli\AB\Models\Experiment; |
8
|
|
|
use Illuminate\Support\Facades\Route; |
9
|
|
|
use Illuminate\Support\Facades\Config; |
10
|
|
|
use Reallyli\AB\Session\SessionInterface; |
11
|
|
|
|
12
|
|
|
class Tester |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* The Session instance. |
16
|
|
|
* |
17
|
|
|
* @var SessionInterface |
18
|
|
|
*/ |
19
|
|
|
protected $session; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Constructor. |
23
|
|
|
* |
24
|
|
|
* @param SessionInterface $session |
25
|
|
|
*/ |
26
|
|
|
public function __construct(SessionInterface $session) |
27
|
|
|
{ |
28
|
|
|
$this->session = $session; |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Track clicked links and form submissions. |
33
|
|
|
* |
34
|
|
|
* @param Request $request |
35
|
|
|
* @return void |
36
|
|
|
*/ |
37
|
|
|
public function track(Request $request) |
38
|
|
|
{ |
39
|
|
|
// Don't track if there is no active experiment. |
40
|
|
|
if (! $this->session->get('experiment')) { |
41
|
|
|
return; |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
// Since there is an ongoing experiment, increase the pageviews. |
45
|
|
|
// This will only be incremented once during the whole experiment. |
46
|
|
|
$this->pageview(); |
47
|
|
|
|
48
|
|
|
// Check current and previous urls. |
49
|
|
|
$root = $request->root(); |
50
|
|
|
$from = ltrim(str_replace($root, '', $request->headers->get('referer')), '/'); |
51
|
|
|
$to = ltrim(str_replace($root, '', $request->getPathInfo()), '/'); |
52
|
|
|
// Don't track refreshes. |
53
|
|
|
if ($from == $to) { |
54
|
|
|
return; |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
// Because the visitor is viewing a new page, trigger engagement. |
58
|
|
|
// This will only be incremented once during the whole experiment. |
59
|
|
|
$this->interact(); |
60
|
|
|
|
61
|
|
|
$goals = $this->getGoals(); |
62
|
|
|
|
63
|
|
|
// Detect goal completion based on the current url. |
64
|
|
|
if (in_array($to, $goals) or in_array('/'.$to, $goals)) { |
65
|
|
|
$this->complete($to); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
// Detect goal completion based on the current route name. |
69
|
|
|
if ($route = Route::currentRouteName() and in_array($route, $goals)) { |
70
|
|
|
$this->complete($route); |
71
|
|
|
} |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Get or compare the current experiment for this session. |
76
|
|
|
* |
77
|
|
|
* @param string $target |
78
|
|
|
* @return bool|string |
79
|
|
|
*/ |
80
|
|
|
public function experiment($target = null) |
81
|
|
|
{ |
82
|
|
|
// Get the existing or new experiment. |
83
|
|
|
try { |
84
|
|
|
$experiment = $this->session->get('experiment') ?: $this->nextExperiment(); |
85
|
|
|
|
86
|
|
|
if (is_null($target)) { |
87
|
|
|
return $experiment; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
return $experiment == $target; |
91
|
|
|
} catch (\Exception $e) { |
92
|
|
|
\Log::error('Experiments on front may be deleted'); |
93
|
|
|
|
94
|
|
|
return false; |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Increment the pageviews for the current experiment. |
100
|
|
|
* |
101
|
|
|
* @return void |
102
|
|
|
*/ |
103
|
|
View Code Duplication |
public function pageview() |
|
|
|
|
104
|
|
|
{ |
105
|
|
|
// Only interact once per experiment. #reallyli update |
106
|
|
|
if (config('ab.unique_pageview')) { |
107
|
|
|
if ($this->session->get('pageview')) { |
108
|
|
|
return; |
109
|
|
|
} |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
$experiment = Experiment::firstOrNew(['name' => $this->experiment()]); |
113
|
|
|
$experiment->visitors++; |
|
|
|
|
114
|
|
|
$experiment->save(); |
115
|
|
|
|
116
|
|
|
// Mark current experiment as interacted. |
117
|
|
|
$this->session->set('pageview', 1); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Increment the engagement for the current experiment. |
122
|
|
|
* |
123
|
|
|
* @return void |
124
|
|
|
*/ |
125
|
|
View Code Duplication |
public function interact() |
|
|
|
|
126
|
|
|
{ |
127
|
|
|
// Only interact once per experiment. |
128
|
|
|
if ($this->session->get('interacted')) { |
129
|
|
|
return; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$experiment = Experiment::firstOrNew(['name' => $this->experiment()]); |
133
|
|
|
$experiment->engagement++; |
|
|
|
|
134
|
|
|
$experiment->save(); |
135
|
|
|
|
136
|
|
|
// Mark current experiment as interacted. |
137
|
|
|
$this->session->set('interacted', 1); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Mark a goal as completed for the current experiment. |
142
|
|
|
* |
143
|
|
|
* @return void |
144
|
|
|
*/ |
145
|
|
|
public function complete($name) |
146
|
|
|
{ |
147
|
|
|
// Only complete once per experiment. |
148
|
|
|
if ($this->session->get("completed_$name")) { |
149
|
|
|
return; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$goal = Goal::firstOrCreate(['name' => $name, 'experiment' => $this->experiment()]); |
153
|
|
|
Goal::where('name', $name)->where('experiment', $this->experiment())->update(['count' => ($goal->count + 1)]); |
|
|
|
|
154
|
|
|
|
155
|
|
|
// Mark current experiment as completed. |
156
|
|
|
$this->session->set("completed_$name", 1); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Set the current experiment for this session manually. |
161
|
|
|
* |
162
|
|
|
* @param string $experiment |
163
|
|
|
*/ |
164
|
|
|
public function setExperiment($experiment) |
165
|
|
|
{ |
166
|
|
|
if ($this->session->get('experiment') != $experiment) { |
167
|
|
|
$this->session->set('experiment', $experiment); |
168
|
|
|
|
169
|
|
|
// Increase pageviews for new experiment. |
170
|
|
|
$this->nextExperiment($experiment); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Get all experiments. |
176
|
|
|
* |
177
|
|
|
* @return array |
178
|
|
|
*/ |
179
|
|
|
public function getExperiments() |
180
|
|
|
{ |
181
|
|
|
return Config::get('ab', [])['experiments']; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Get all goals. |
186
|
|
|
* |
187
|
|
|
* @return array |
188
|
|
|
*/ |
189
|
|
|
public function getGoals() |
190
|
|
|
{ |
191
|
|
|
return Config::get('ab', [])['goals']; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Get the session instance. |
196
|
|
|
* |
197
|
|
|
* @return SessionInterface |
198
|
|
|
*/ |
199
|
|
|
public function getSession() |
200
|
|
|
{ |
201
|
|
|
return $this->session; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Set the session instance. |
206
|
|
|
* |
207
|
|
|
* @param $session SessionInterface |
208
|
|
|
*/ |
209
|
|
|
public function setSession(SessionInterface $session) |
210
|
|
|
{ |
211
|
|
|
$this->session = $session; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* If an experiment has initialized get his string. |
216
|
|
|
* |
217
|
|
|
* @return string |
218
|
|
|
*/ |
219
|
|
|
public function currentExperiment() |
220
|
|
|
{ |
221
|
|
|
// Verify that the experiments are in the database. |
222
|
|
|
$this->checkExperiments(); |
223
|
|
|
if ($this->session->get('experiment') != '') { |
224
|
|
|
$experiment = $this->session->get('experiment'); |
225
|
|
|
} else { |
226
|
|
|
$experiment = Experiment::active()->orderBy('updated_at', 'asc')->firstOrFail(); |
|
|
|
|
227
|
|
|
$experiment = $experiment->name; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $experiment; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Prepare an experiment for this session. |
235
|
|
|
* |
236
|
|
|
* @return string |
237
|
|
|
*/ |
238
|
|
|
protected function nextExperiment($experiment = null) |
239
|
|
|
{ |
240
|
|
|
// Verify that the experiments are in the database. |
241
|
|
|
$this->checkExperiments(); |
242
|
|
|
|
243
|
|
|
if ($experiment) { |
244
|
|
|
$experiment = Experiment::active()->findOrfail($experiment); |
|
|
|
|
245
|
|
|
} else { |
246
|
|
|
$experiment = Experiment::active()->orderBy('visitors', 'asc')->firstOrFail(); |
|
|
|
|
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
$this->session->set('experiment', $experiment->name); |
250
|
|
|
|
251
|
|
|
// Since there is an ongoing experiment, increase the pageviews. |
252
|
|
|
// This will only be incremented once during the whole experiment. |
253
|
|
|
$this->pageview(); |
254
|
|
|
|
255
|
|
|
return $experiment->name; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Add experiments to the database. |
260
|
|
|
* |
261
|
|
|
* @return void |
262
|
|
|
*/ |
263
|
|
|
protected function checkExperiments() |
264
|
|
|
{ |
265
|
|
|
// Check if the database contains all experiments. |
266
|
|
|
if (Experiment::active()->count() != count($this->getExperiments())) { |
|
|
|
|
267
|
|
|
// Insert all experiments. |
268
|
|
|
foreach ($this->getExperiments() as $experiment) { |
269
|
|
|
Experiment::firstOrCreate(['name' => $experiment]); |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Check if there are active experiments. |
276
|
|
|
* |
277
|
|
|
* @return string |
278
|
|
|
*/ |
279
|
|
|
public function hasExperiments() |
280
|
|
|
{ |
281
|
|
|
$count = Experiment::count(); |
282
|
|
|
|
283
|
|
|
return $count > 1; |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.