1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
|
4
|
|
|
/** |
5
|
|
|
* @description: cleans up old (abandonned) carts... |
6
|
|
|
* |
7
|
|
|
* |
8
|
|
|
* @authors: Nicolaas [at] Sunny Side Up .co.nz |
9
|
|
|
* @package: ecommerce |
10
|
|
|
* @sub-package: tasks |
11
|
|
|
* @inspiration: Silverstripe Ltd, Jeremy |
12
|
|
|
**/ |
13
|
|
|
class EcommerceTaskCartCleanup extends BuildTask |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Standard SS Variable |
17
|
|
|
* TODO: either remove or add to all tasks. |
18
|
|
|
*/ |
19
|
|
|
private static $allowed_actions = array( |
20
|
|
|
'*' => 'SHOPADMIN', |
21
|
|
|
); |
22
|
|
|
|
23
|
|
|
protected $title = 'Clear old carts'; |
24
|
|
|
|
25
|
|
|
protected $description = 'Deletes abandonned carts (add ?limit=xxxx to the end of the URL to set the number of records (xxx = number of records) to be deleted in one load).'; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Output feedback about task? |
29
|
|
|
* |
30
|
|
|
* @var bool |
31
|
|
|
*/ |
32
|
|
|
public $verbose = false; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* run in verbose mode. |
36
|
|
|
*/ |
37
|
|
|
public static function run_on_demand() |
38
|
|
|
{ |
39
|
|
|
$obj = new self(); |
40
|
|
|
$obj->verbose = true; |
41
|
|
|
$obj->run(null); |
|
|
|
|
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* runs the task without output. |
46
|
|
|
*/ |
47
|
|
|
public function runSilently() |
48
|
|
|
{ |
49
|
|
|
$this->verbose = false; |
50
|
|
|
|
51
|
|
|
return $this->run(null); |
|
|
|
|
52
|
|
|
} |
53
|
|
|
/** |
54
|
|
|
*@return int - number of carts destroyed |
|
|
|
|
55
|
|
|
**/ |
56
|
|
|
public function run($request) |
57
|
|
|
{ |
58
|
|
|
if ($this->verbose || (isset($_GET['verbose']) && Permission::check('ADMIN'))) { |
59
|
|
|
$this->verbose = true; |
60
|
|
|
$this->flush(); |
61
|
|
|
$countAll = DB::query('SELECT COUNT("ID") FROM "Order"')->value(); |
62
|
|
|
DB::alteration_message("<h2>deleting empty and abandonned carts (total cart count = $countAll)</h2>."); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
$neverDeleteIfLinkedToMember = EcommerceConfig::get('EcommerceTaskCartCleanup', 'never_delete_if_linked_to_member'); |
66
|
|
|
$maximumNumberOfObjectsDeleted = EcommerceConfig::get('EcommerceTaskCartCleanup', 'maximum_number_of_objects_deleted'); |
67
|
|
|
|
68
|
|
|
//LIMITS ... |
69
|
|
|
if ($request) { |
70
|
|
|
$limitFromGetVar = $request->getVar('limit'); |
71
|
|
|
if ($limitFromGetVar && Permission::check('ADMIN')) { |
72
|
|
|
$maximumNumberOfObjectsDeleted = intval($limitFromGetVar); |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
$limit = '0, '.$maximumNumberOfObjectsDeleted; |
|
|
|
|
76
|
|
|
|
77
|
|
|
//sort |
78
|
|
|
$sort = '"Order"."Created" ASC'; |
79
|
|
|
|
80
|
|
|
//join |
81
|
|
|
$leftMemberJoin = 'LEFT JOIN Member ON "Member"."ID" = "Order"."MemberID"'; |
82
|
|
|
$joinShort = '"Member"."ID" = "Order"."MemberID"'; |
83
|
|
|
|
84
|
|
|
//ABANDONNED CARTS |
85
|
|
|
$clearMinutes = EcommerceConfig::get('EcommerceTaskCartCleanup', 'clear_minutes'); |
86
|
|
|
$createdStepID = OrderStep::get_status_id_from_code('CREATED'); |
87
|
|
|
$time = strtotime('-'.$clearMinutes.' minutes'); |
88
|
|
|
$where = '"StatusID" = '.$createdStepID." AND UNIX_TIMESTAMP(\"Order\".\"LastEdited\") < $time "; |
89
|
|
|
if ($neverDeleteIfLinkedToMember) { |
90
|
|
|
$userStatement = 'or have a user associated with it'; |
91
|
|
|
$withoutMemberWhere = ' AND "Member"."ID" IS NULL '; |
92
|
|
|
$withMemberWhere = ' OR "Member"."ID" IS NOT NULL '; |
93
|
|
|
$memberDeleteNote = '(Carts linked to a member will NEVER be deleted)'; |
94
|
|
|
} else { |
95
|
|
|
$userStatement = ''; |
96
|
|
|
$withoutMemberWhere = ' '; |
97
|
|
|
$withMemberWhere = ''; |
98
|
|
|
$memberDeleteNote = '(We will also delete carts in this category that are linked to a member)'; |
99
|
|
|
} |
100
|
|
|
$oldCarts = Order::get() |
101
|
|
|
->where($where.$withoutMemberWhere) |
102
|
|
|
->sort($sort) |
103
|
|
|
->limit($maximumNumberOfObjectsDeleted); |
104
|
|
|
$oldCarts = $oldCarts->leftJoin('Member', $joinShort); |
105
|
|
|
if ($oldCarts->count()) { |
106
|
|
|
$count = 0; |
107
|
|
|
if ($this->verbose) { |
108
|
|
|
$this->flush(); |
109
|
|
|
$totalToDeleteSQLObject = DB::query( |
110
|
|
|
' |
111
|
|
|
SELECT COUNT(*) |
112
|
|
|
FROM "Order" |
113
|
|
|
'.$leftMemberJoin.' |
114
|
|
|
WHERE ' |
115
|
|
|
.$where |
116
|
|
|
.$withoutMemberWhere |
117
|
|
|
.';' |
118
|
|
|
); |
119
|
|
|
$totalToDelete = $totalToDeleteSQLObject->value(); |
120
|
|
|
DB::alteration_message(' |
121
|
|
|
<h2>Total number of abandonned carts: '.$totalToDelete.'</h2> |
122
|
|
|
<br /><b>number of records deleted at one time:</b> '.$maximumNumberOfObjectsDeleted.' |
123
|
|
|
<br /><b>Criteria:</b> last edited '.$clearMinutes.' (~'.round($clearMinutes / 60 / 24, 2)." days) |
124
|
|
|
minutes ago or more $memberDeleteNote", 'created'); |
125
|
|
|
} |
126
|
|
|
foreach ($oldCarts as $oldCart) { |
127
|
|
|
$count++; |
128
|
|
|
if ($this->verbose) { |
129
|
|
|
$this->flush(); |
130
|
|
|
DB::alteration_message("$count ... deleting abandonned order #".$oldCart->ID, 'deleted'); |
131
|
|
|
} |
132
|
|
|
$this->deleteObject($oldCart); |
133
|
|
|
} |
134
|
|
|
} else { |
135
|
|
|
if ($this->verbose) { |
136
|
|
|
$this->flush(); |
137
|
|
|
DB::alteration_message('There are no old carts', 'created'); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
if ($this->verbose) { |
141
|
|
|
$this->flush(); |
142
|
|
|
$timeLegible = date('Y-m-d H:i:s', $time); |
143
|
|
|
$countCart = DB::query('SELECT COUNT("ID") FROM "Order" WHERE "StatusID" = '.$createdStepID.' ')->value(); |
144
|
|
|
$countCartWithinTimeLimit = DB::query(' |
145
|
|
|
SELECT COUNT("Order"."ID") |
146
|
|
|
FROM "Order" |
147
|
|
|
'.$leftMemberJoin.' |
148
|
|
|
WHERE "StatusID" = '.$createdStepID.' |
149
|
|
|
AND |
150
|
|
|
( |
151
|
|
|
UNIX_TIMESTAMP("Order"."LastEdited") >= '.$time .' |
152
|
|
|
'.$withMemberWhere.' |
153
|
|
|
); |
154
|
|
|
')->value(); |
155
|
|
|
DB::alteration_message( |
156
|
|
|
" |
157
|
|
|
$countCart Orders are still in the CREATED cart state (not submitted), |
158
|
|
|
$countCartWithinTimeLimit of them are within the time limit (last edited after $timeLegible) |
159
|
|
|
".$userStatement." so they are not deleted.", |
160
|
|
|
'created' |
161
|
|
|
); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
//EMPTY ORDERS |
165
|
|
|
$clearMinutes = EcommerceConfig::get('EcommerceTaskCartCleanup', 'clear_minutes_empty_carts'); |
166
|
|
|
$time = strtotime('-'.$clearMinutes.' minutes'); |
167
|
|
|
$where = "\"StatusID\" = 0 AND UNIX_TIMESTAMP(\"Order\".\"LastEdited\") < $time "; |
168
|
|
|
$oldCarts = Order::get() |
169
|
|
|
->where($where) |
170
|
|
|
->sort($sort) |
171
|
|
|
->limit($maximumNumberOfObjectsDeleted); |
172
|
|
|
$oldCarts = $oldCarts->leftJoin('Member', $joinShort); |
173
|
|
|
if ($oldCarts->count()) { |
174
|
|
|
$count = 0; |
175
|
|
|
if ($this->verbose) { |
176
|
|
|
$this->flush(); |
177
|
|
|
$totalToDelete = DB::query( |
178
|
|
|
' |
179
|
|
|
SELECT COUNT(*) |
180
|
|
|
FROM "Order" |
181
|
|
|
'.$leftMemberJoin.' |
182
|
|
|
WHERE ' |
183
|
|
|
.$where |
184
|
|
|
.$withoutMemberWhere |
185
|
|
|
.';' |
186
|
|
|
)->value(); |
187
|
|
|
DB::alteration_message(' |
188
|
|
|
<h2>Total number of empty carts: '.$totalToDelete.'</h2> |
189
|
|
|
<br /><b>number of records deleted at one time:</b> '.$maximumNumberOfObjectsDeleted." |
190
|
|
|
<br /><b>Criteria:</b> there are no order items and |
191
|
|
|
the order was last edited $clearMinutes minutes ago $memberDeleteNote", 'created'); |
192
|
|
|
} |
193
|
|
|
foreach ($oldCarts as $oldCart) { |
194
|
|
|
++$count; |
195
|
|
|
if ($this->verbose) { |
196
|
|
|
$this->flush(); |
197
|
|
|
DB::alteration_message("$count ... deleting empty order #".$oldCart->ID, 'deleted'); |
198
|
|
|
} |
199
|
|
|
$this->deleteObject($oldCart); |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
if ($this->verbose) { |
203
|
|
|
$this->flush(); |
204
|
|
|
$timeLegible = date('Y-m-d H:i:s', $time); |
205
|
|
|
$countCart = DB::query( |
206
|
|
|
' |
207
|
|
|
SELECT COUNT("Order"."ID") |
208
|
|
|
FROM "Order" |
209
|
|
|
'.$leftMemberJoin.' |
210
|
|
|
WHERE "StatusID" = 0 ' |
211
|
|
|
)->value(); |
212
|
|
|
$countCartWithinTimeLimit = DB::query( |
213
|
|
|
' |
214
|
|
|
SELECT COUNT("Order"."ID") |
215
|
|
|
FROM "Order" |
216
|
|
|
'.$leftMemberJoin.' |
217
|
|
|
WHERE "StatusID" = 0 AND |
218
|
|
|
( |
219
|
|
|
UNIX_TIMESTAMP("Order"."LastEdited") >= '.$time.' |
220
|
|
|
'.$withMemberWhere.' |
221
|
|
|
)' |
222
|
|
|
)->value(); |
223
|
|
|
DB::alteration_message( |
224
|
|
|
" |
225
|
|
|
$countCart Orders are without status at all, |
226
|
|
|
$countCartWithinTimeLimit are within the time limit (last edited after $timeLegible) |
227
|
|
|
".$userStatement."so they are not deleted yet.", |
228
|
|
|
'created' |
229
|
|
|
); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$oneToMany = EcommerceConfig::get('EcommerceTaskCartCleanup', 'one_to_many_classes'); |
233
|
|
|
$oneToOne = EcommerceConfig::get('EcommerceTaskCartCleanup', 'one_to_one_classes'); |
234
|
|
|
$manyToMany = EcommerceConfig::get('EcommerceTaskCartCleanup', 'many_to_many_classes'); |
235
|
|
|
if (!is_array($oneToOne)) { |
236
|
|
|
$oneToOne = array(); |
237
|
|
|
} |
238
|
|
|
if (!is_array($oneToMany)) { |
239
|
|
|
$oneToMany = array(); |
240
|
|
|
} |
241
|
|
|
if (!is_array($manyToMany)) { |
242
|
|
|
$manyToMany = array(); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/*********************************************** |
246
|
|
|
//CLEANING ONE-TO-ONES |
247
|
|
|
************************************************/ |
248
|
|
|
if ($this->verbose) { |
249
|
|
|
$this->flush(); |
250
|
|
|
DB::alteration_message('<h2>Checking one-to-one relationships</h2>.'); |
251
|
|
|
} |
252
|
|
|
if (count($oneToOne)) { |
253
|
|
|
foreach ($oneToOne as $orderFieldName => $className) { |
254
|
|
|
if (!in_array($className, $oneToMany) && !in_array($className, $manyToMany)) { |
255
|
|
|
if ($this->verbose) { |
256
|
|
|
$this->flush(); |
257
|
|
|
DB::alteration_message("looking for $className objects without link to order."); |
258
|
|
|
} |
259
|
|
|
$rows = DB::query(" |
260
|
|
|
SELECT \"$className\".\"ID\" |
261
|
|
|
FROM \"$className\" |
262
|
|
|
LEFT JOIN \"Order\" |
263
|
|
|
ON \"Order\".\"$orderFieldName\" = \"$className\".\"ID\" |
264
|
|
|
WHERE \"Order\".\"ID\" IS NULL |
265
|
|
|
LIMIT 0, ".$maximumNumberOfObjectsDeleted); |
266
|
|
|
//the code below is a bit of a hack, but because of the one-to-one relationship we |
267
|
|
|
//want to check both sides.... |
268
|
|
|
$oneToOneIDArray = array(); |
269
|
|
|
if ($rows) { |
270
|
|
|
foreach ($rows as $row) { |
271
|
|
|
$oneToOneIDArray[$row['ID']] = $row['ID']; |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
if (count($oneToOneIDArray)) { |
275
|
|
|
$unlinkedObjects = $className::get() |
276
|
|
|
->filter(array('ID' => $oneToOneIDArray)); |
277
|
|
|
if ($unlinkedObjects->count()) { |
278
|
|
|
foreach ($unlinkedObjects as $unlinkedObject) { |
279
|
|
|
if ($this->verbose) { |
280
|
|
|
$this->flush(); |
281
|
|
|
DB::alteration_message('Deleting '.$unlinkedObject->ClassName.' with ID #'.$unlinkedObject->ID.' because it does not appear to link to an order.', 'deleted'); |
282
|
|
|
} |
283
|
|
|
$this->deleteObject($unlinkedObject); |
284
|
|
|
} |
285
|
|
|
} else { |
286
|
|
|
if ($this->verbose) { |
287
|
|
|
$this->flush(); |
288
|
|
|
DB::alteration_message("No objects where found for $className even though there appear to be missing links.", 'created'); |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
} elseif ($this->verbose) { |
292
|
|
|
$this->flush(); |
293
|
|
|
DB::alteration_message("All references in Order to $className are valid.", 'created'); |
294
|
|
|
} |
295
|
|
|
if ($this->verbose) { |
296
|
|
|
$this->flush(); |
297
|
|
|
$countAll = DB::query("SELECT COUNT(\"ID\") FROM \"$className\"")->value(); |
298
|
|
|
$countUnlinkedOnes = DB::query("SELECT COUNT(\"$className\".\"ID\") FROM \"$className\" LEFT JOIN \"Order\" ON \"$className\".\"ID\" = \"Order\".\"$orderFieldName\" WHERE \"Order\".\"ID\" IS NULL")->value(); |
299
|
|
|
DB::alteration_message("In total there are $countAll $className ($orderFieldName), of which there are $countUnlinkedOnes not linked to an order. ", 'created'); |
300
|
|
|
if ($countUnlinkedOnes) { |
301
|
|
|
DB::alteration_message("There should be NO $orderFieldName ($className) without link to Order - un error is suspected", 'deleted'); |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/*********************************************** |
309
|
|
|
//CLEANING ONE-TO-MANY |
310
|
|
|
*************************************************/ |
311
|
|
|
|
312
|
|
|
//one order has many other things so we increase the ability to delete stuff |
313
|
|
|
$maximumNumberOfObjectsDeleted = $maximumNumberOfObjectsDeleted * 25; |
314
|
|
|
if ($this->verbose) { |
315
|
|
|
$this->flush(); |
316
|
|
|
DB::alteration_message('<h2>Checking one-to-many relationships</h2>.'); |
317
|
|
|
} |
318
|
|
|
if (count($oneToMany)) { |
319
|
|
|
foreach ($oneToMany as $classWithOrderID => $classWithLastEdited) { |
320
|
|
|
if (!in_array($classWithLastEdited, $oneToOne) && !in_array($classWithLastEdited, $manyToMany)) { |
321
|
|
|
if ($this->verbose) { |
322
|
|
|
$this->flush(); |
323
|
|
|
DB::alteration_message("looking for $classWithOrderID objects without link to order."); |
324
|
|
|
} |
325
|
|
|
$rows = DB::query(" |
326
|
|
|
SELECT \"$classWithOrderID\".\"ID\" |
327
|
|
|
FROM \"$classWithOrderID\" |
328
|
|
|
LEFT JOIN \"Order\" |
329
|
|
|
ON \"Order\".\"ID\" = \"$classWithOrderID\".\"OrderID\" |
330
|
|
|
WHERE \"Order\".\"ID\" IS NULL |
331
|
|
|
LIMIT 0, ".$maximumNumberOfObjectsDeleted); |
332
|
|
|
$oneToManyIDArray = array(); |
333
|
|
|
if ($rows) { |
334
|
|
|
foreach ($rows as $row) { |
335
|
|
|
$oneToManyIDArray[$row['ID']] = $row['ID']; |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
if (count($oneToManyIDArray)) { |
339
|
|
|
$unlinkedObjects = $classWithLastEdited::get() |
340
|
|
|
->filter(array('ID' => $oneToManyIDArray)); |
341
|
|
|
if ($unlinkedObjects->count()) { |
342
|
|
|
foreach ($unlinkedObjects as $unlinkedObject) { |
343
|
|
|
if ($this->verbose) { |
344
|
|
|
DB::alteration_message('Deleting '.$unlinkedObject->ClassName.' with ID #'.$unlinkedObject->ID.' because it does not appear to link to an order.', 'deleted'); |
345
|
|
|
} |
346
|
|
|
$this->deleteObject($unlinkedObject); |
347
|
|
|
} |
348
|
|
|
} elseif ($this->verbose) { |
349
|
|
|
$this->flush(); |
350
|
|
|
DB::alteration_message("$classWithLastEdited objects could not be found even though they were referenced.", 'deleted'); |
351
|
|
|
} |
352
|
|
|
} elseif ($this->verbose) { |
353
|
|
|
$this->flush(); |
354
|
|
|
DB::alteration_message("All $classWithLastEdited objects have a reference to a valid order.", 'created'); |
355
|
|
|
} |
356
|
|
|
if ($this->verbose) { |
357
|
|
|
$this->flush(); |
358
|
|
|
$countAll = DB::query("SELECT COUNT(\"ID\") FROM \"$classWithLastEdited\"")->value(); |
359
|
|
|
$countUnlinkedOnes = DB::query("SELECT COUNT(\"$classWithOrderID\".\"ID\") FROM \"$classWithOrderID\" LEFT JOIN \"Order\" ON \"$classWithOrderID\".\"OrderID\" = \"Order\".\"ID\" WHERE \"Order\".\"ID\" IS NULL")->value(); |
360
|
|
|
DB::alteration_message("In total there are $countAll $classWithOrderID ($classWithLastEdited), of which there are $countUnlinkedOnes not linked to an order. ", 'created'); |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
if ($this->verbose) { |
366
|
|
|
$this->flush(); |
367
|
|
|
DB::alteration_message('---------------- DONE --------------------'); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
private function flush() |
372
|
|
|
{ |
373
|
|
|
if ((php_sapi_name() === 'cli')) { |
374
|
|
|
echo "\n"; |
375
|
|
|
} else { |
376
|
|
|
ob_flush(); |
377
|
|
|
flush(); |
378
|
|
|
} |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* delete an object |
383
|
|
|
* @param DataObject |
384
|
|
|
* @return null |
385
|
|
|
*/ |
386
|
|
|
private function deleteObject($objectToDelete) |
387
|
|
|
{ |
388
|
|
|
$objectToDelete->delete(); |
389
|
|
|
$objectToDelete->destroy(); |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
It seems like the type of the argument is not accepted by the function/method which you are calling.
In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.
We suggest to add an explicit type cast like in the following example: