Passed
Push — v3 ( 5261ad...9137a8 )
by Andrew
32:26 queued 04:55
created

Statistics::incrementStatistics()   B

Complexity

Conditions 8
Paths 40

Size

Total Lines 63
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 48
c 5
b 0
f 0
dl 0
loc 63
rs 7.8901
cc 8
nc 40
nop 2

How to fix   Long Method   

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
 * Retour plugin for Craft CMS 3.x
4
 *
5
 * Retour allows you to intelligently redirect legacy URLs, so that you don't
6
 * lose SEO value when rebuilding & restructuring a website
7
 *
8
 * @link      https://nystudio107.com/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
11
12
namespace nystudio107\retour\services;
13
14
use nystudio107\retour\Retour;
15
16
use nystudio107\retour\models\Stats as StatsModel;
17
18
use Craft;
19
use craft\base\Component;
20
use craft\db\Query;
21
use craft\helpers\Db;
22
use craft\helpers\UrlHelper;
23
24
use yii\db\Exception;
25
use yii\web\HttpException;
26
27
/** @noinspection MissingPropertyAnnotationsInspection */
28
29
/**
30
 * @author    nystudio107
31
 * @package   Retour
32
 * @since     3.0.0
33
 */
34
class Statistics extends Component
35
{
36
    // Constants
37
    // =========================================================================
38
39
    const LAST_STATISTICS_TRIM_CACHE_KEY = 'retour-last-statistics-trim';
40
41
    // Protected Properties
42
    // =========================================================================
43
44
    /**
45
     * @var null|array
46
     */
47
    protected $cachedStatistics;
48
49
    // Public Methods
50
    // =========================================================================
51
52
    /**
53
     * @return array All of the statistics
54
     */
55
    public function getAllStatistics(): array
56
    {
57
        // Cache it in our class; no need to fetch it more than once
58
        if ($this->cachedStatistics !== null) {
59
            return $this->cachedStatistics;
60
        }
61
        // Query the db table
62
        $stats = (new Query())
63
            ->from(['{{%retour_stats}}'])
64
            ->orderBy('hitCount DESC')
65
            ->limit(Retour::$settings->statsDisplayLimit)
66
            ->all();
67
        // Cache for future accesses
68
        $this->cachedStatistics = $stats;
69
70
        return $stats;
71
    }
72
73
    /**
74
     * @param int  $days The number of days to get
75
     * @param bool $handled
76
     *
77
     * @return array Recent statistics
78
     */
79
    public function getRecentStatistics($days = 1, $handled = false): array
80
    {
81
        // Ensure is an int
82
        $handledInt = (int)$handled;
83
        $stats = [];
84
        $db = Craft::$app->getDb();
85
        if ($db->getIsMysql()) {
86
            // Query the db table
87
            $stats = (new Query())
88
                ->from(['{{%retour_stats}}'])
89
                ->where("hitLastTime >= ( CURDATE() - INTERVAL '{$days}' DAY )")
90
                ->andWhere("handledByRetour = {$handledInt}")
91
                ->orderBy('hitLastTime DESC')
92
                ->all();
93
        }
94
        if ($db->getIsPgsql()) {
95
            // Query the db table
96
            $stats = (new Query())
97
                ->from(['{{%retour_stats}}'])
98
                ->where("\"hitLastTime\" >= ( CURRENT_TIMESTAMP - INTERVAL '{$days} days' )")
99
                ->andWhere(['handledByRetour' => $handledInt])
100
                ->orderBy('hitLastTime DESC')
101
                ->all();
102
        }
103
104
        return $stats;
105
    }
106
107
    /**
108
     * @return int
109
     */
110
    public function clearStatistics(): int
111
    {
112
        $db = Craft::$app->getDb();
113
        try {
114
            $result = $db->createCommand()
115
                ->truncateTable('{{%retour_stats}}')
116
                ->execute();
117
        } catch (Exception $e) {
118
            $result = -1;
119
            Craft::error($e->getMessage(), __METHOD__);
120
        }
121
122
        return $result;
123
    }
124
125
    /**
126
     * Delete a statistic by id
127
     *
128
     * @param int $id
129
     *
130
     * @return int The result
131
     */
132
    public function deleteStatisticById(int $id): int
133
    {
134
        $db = Craft::$app->getDb();
135
        // Delete a row from the db table
136
        try {
137
            $result = $db->createCommand()->delete(
138
                '{{%retour_stats}}',
139
                [
140
                    'id' => $id,
141
                ]
142
            )->execute();
143
        } catch (Exception $e) {
144
            Craft::error($e->getMessage(), __METHOD__);
145
            $result = 0;
146
        }
147
148
        return $result;
149
    }
150
151
    /**
152
     * Increment the retour_stats record
153
     *
154
     * @param string             $url The 404 url
155
     * @param bool               $handled
156
     */
157
    public function incrementStatistics(string $url, $handled = false)
158
    {
159
        $referrer = $remoteIp = null;
160
        $request = Craft::$app->getRequest();
161
        $siteId = Craft::$app->getSites()->currentSite->id;
162
        if (!$request->isConsoleRequest) {
163
            $referrer = $request->getReferrer();
164
            if (Retour::$settings->recordRemoteIp) {
165
                $remoteIp = $request->getUserIP();
166
            }
167
            $userAgent = $request->getUserAgent();
168
            if (Retour::$currentException !== null) {
169
                $exceptionMessage = Retour::$currentException->getMessage();
170
                $exceptionFilePath = Retour::$currentException->getFile();
171
                $exceptionFileLine = Retour::$currentException->getLine();
172
            }
173
        }
174
        $referrer = $referrer ?? '';
175
        $remoteIp = $remoteIp ?? '';
176
        $userAgent = $userAgent ?? '';
177
        $exceptionMessage = $exceptionMessage ?? '';
178
        $exceptionFilePath = $exceptionFilePath ?? '';
179
        $exceptionFileLine = $exceptionFileLine ?? 0;
180
        // Strip the query string if `stripQueryStringFromStats` is set
181
        if (Retour::$settings->stripQueryStringFromStats) {
182
            $url = UrlHelper::stripQueryString($url);
183
            $referrer = UrlHelper::stripQueryString($referrer);
0 ignored issues
show
Bug introduced by
It seems like $referrer can also be of type null; however, parameter $url of craft\helpers\UrlHelper::stripQueryString() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

183
            $referrer = UrlHelper::stripQueryString(/** @scrutinizer ignore-type */ $referrer);
Loading history...
184
        }
185
        // Normalize the $url via the validator
186
        $stats = new StatsModel([
187
            'redirectSrcUrl' => $url,
188
        ]);
189
        $stats->validate();
190
        // Find any existing retour_stats record
191
        $statsConfig = (new Query())
192
            ->from(['{{%retour_stats}}'])
193
            ->where(['redirectSrcUrl' => $stats->redirectSrcUrl])
194
            ->one();
195
        // If no record is found, initialize some values
196
        if ($statsConfig === null) {
197
            $stats->id = 0;
198
            $stats->hitCount = 0;
199
        } else {
200
            $stats->id = $statsConfig['id'];
201
            $stats->hitCount = $statsConfig['hitCount'];
202
        }
203
        // Merge in the updated info
204
        $stats->siteId = $siteId;
205
        $stats->referrerUrl = $referrer;
206
        $stats->remoteIp = $remoteIp;
207
        $stats->userAgent = $userAgent;
208
        $stats->exceptionMessage = $exceptionMessage;
209
        $stats->exceptionFilePath = $exceptionFilePath;
210
        $stats->exceptionFileLine = (int)$exceptionFileLine;
211
        $stats->hitLastTime = Db::prepareDateForDb(new \DateTime());
212
        $stats->handledByRetour = (int)$handled;
213
        $stats->hitCount++;
214
        $statsConfig = $stats->getAttributes();
215
        // Record the updated statistics
216
        $this->saveStatistics($statsConfig);
217
        // After incrementing a statistic, trim the retour_stats db table
218
        if (Retour::$settings->automaticallyTrimStatistics && !$this->rateLimited()) {
219
            $this->trimStatistics();
220
        }
221
    }
222
223
    /**
224
     * Trim the retour_stats db table based on the statsStoredLimit config.php
225
     * setting
226
     *
227
     * @param int|null $limit
228
     *
229
     * @return int
230
     */
231
    public function trimStatistics(int $limit = null): int
232
    {
233
        $affectedRows = 0;
234
        $db = Craft::$app->getDb();
235
        $quotedTable = $db->quoteTableName('{{%retour_stats}}');
236
        $limit = $limit ?? Retour::$settings->statsStoredLimit;
237
238
        if ($limit !== null) {
0 ignored issues
show
introduced by
The condition $limit !== null is always true.
Loading history...
239
            //  https://stackoverflow.com/questions/578867/sql-query-delete-all-records-from-the-table-except-latest-n
240
            try {
241
                if ($db->getIsMysql()) {
242
                    // Handle MySQL
243
                    $affectedRows = $db->createCommand(/** @lang mysql */
244
                        "
245
                        DELETE FROM {$quotedTable}
246
                        WHERE id NOT IN (
247
                          SELECT id
248
                          FROM (
249
                            SELECT id
250
                            FROM {$quotedTable}
251
                            ORDER BY hitLastTime DESC
252
                            LIMIT {$limit}
253
                          ) foo
254
                        )
255
                        "
256
                    )->execute();
257
                }
258
                if ($db->getIsPgsql()) {
259
                    // Handle Postgres
260
                    $affectedRows = $db->createCommand(/** @lang mysql */
261
                        "
262
                        DELETE FROM {$quotedTable}
263
                        WHERE id NOT IN (
264
                          SELECT id
265
                          FROM (
266
                            SELECT id
267
                            FROM {$quotedTable}
268
                            ORDER BY \"hitLastTime\" DESC
269
                            LIMIT {$limit}
270
                          ) foo
271
                        )
272
                        "
273
                    )->execute();
274
                }
275
            } catch (Exception $e) {
276
                Craft::error($e->getMessage(), __METHOD__);
277
            }
278
            Craft::info(
279
                Craft::t(
280
                    'retour',
281
                    'Trimmed {rows} from retour_stats table',
282
                    ['rows' => $affectedRows]
283
                ),
284
                __METHOD__
285
            );
286
        }
287
288
        return $affectedRows;
289
    }
290
291
    /**
292
     * @param array $statsConfig
293
     */
294
    public function saveStatistics(array $statsConfig)
295
    {
296
        // Validate the model before saving it to the db
297
        $stats = new StatsModel($statsConfig);
298
        if ($stats->validate() === false) {
299
            Craft::error(
300
                Craft::t(
301
                    'retour',
302
                    'Error validating statistics {id}: {errors}',
303
                    ['id' => $stats->id, 'errors' => print_r($stats->getErrors(), true)]
304
                ),
305
                __METHOD__
306
            );
307
308
            return;
309
        }
310
        // Get the validated model attributes and save them to the db
311
        $statsConfig = $stats->getAttributes();
312
        $db = Craft::$app->getDb();
313
        if ($statsConfig['id'] !== 0) {
314
            // Update the existing record
315
            try {
316
                $result = $db->createCommand()->update(
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
317
                    '{{%retour_stats}}',
318
                    $statsConfig,
319
                    [
320
                        'id' => $statsConfig['id'],
321
                    ]
322
                )->execute();
323
            } catch (Exception $e) {
324
                // We don't log this error on purpose, because it's just a stats
325
                // update, and deadlock errors can potentially occur
326
                // Craft::error($e->getMessage(), __METHOD__);
327
            }
328
        } else {
329
            unset($statsConfig['id']);
330
            // Create a new record
331
            try {
332
                $db->createCommand()->insert(
333
                    '{{%retour_stats}}',
334
                    $statsConfig
335
                )->execute();
336
            } catch (Exception $e) {
337
                Craft::error($e->getMessage(), __METHOD__);
338
            }
339
        }
340
    }
341
342
    // Protected Methods
343
    // =========================================================================
344
345
    /**
346
     * Don't trim more than a given interval, so that performance is not affected
347
     *
348
     * @return bool
349
     */
350
    protected function rateLimited(): bool
351
    {
352
        $limited = false;
353
        $now = round(microtime(true) * 1000);
354
        $cache = Craft::$app->getCache();
355
        $then = $cache->get(self::LAST_STATISTICS_TRIM_CACHE_KEY);
356
        if (($then !== false) && ($now - (int)$then < Retour::$settings->statisticsRateLimitMs)) {
357
            $limited = true;
358
        }
359
        $cache->set(self::LAST_STATISTICS_TRIM_CACHE_KEY, $now, 0);
360
361
        return $limited;
362
    }
363
}
364