EcommerceTaskCartCleanup::run()   F
last analyzed

Complexity

Conditions 54
Paths > 20000

Size

Total Lines 314

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 54
dl 0
loc 314
rs 0
c 0
b 0
f 0
nc 147456
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SS_HTTPRequest>.

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:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SS_HTTPRequest>.

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:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
52
    }
53
    /**
54
     *@return int - number of carts destroyed
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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;
0 ignored issues
show
Unused Code introduced by
$limit is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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