Passed
Push — v1 ( ac24ef...a419d0 )
by Andrew
03:03
created

DataSamples   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 30
eloc 152
dl 0
loc 331
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
B addDataSample() 0 61 8
A deleteDataSamplesByUrl() 0 19 3
A deleteAllDataSamples() 0 19 3
A deleteSampleById() 0 17 2
A trimOrphanedSamples() 0 16 2
A trimDataSamples() 0 59 5
A trimOutlierSamples() 0 22 3
A pageTitle() 0 17 2
A totalSamples() 0 11 2
1
<?php
2
/**
3
 * Webperf plugin for Craft CMS 3.x
4
 *
5
 * Monitor the performance of your webpages through real-world user timing data
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2019 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\webperf\services;
12
13
use craft\db\Query;
0 ignored issues
show
Bug introduced by
The type craft\db\Query was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use nystudio107\webperf\Webperf;
15
use nystudio107\webperf\models\DataSample;
16
17
use Craft;
0 ignored issues
show
Bug introduced by
The type Craft was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use craft\base\Component;
0 ignored issues
show
Bug introduced by
The type craft\base\Component was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
20
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
21
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 2 spaces but found 4
Loading history...
22
 * @package   Webperf
0 ignored issues
show
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 3
Loading history...
23
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 3 spaces but found 5
Loading history...
24
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
25
class DataSamples extends Component
26
{
27
    // Constants
28
    // =========================================================================
29
30
    const OUTLIER_PAGELOAD_MULTIPLIER = 10;
31
32
    // Public Methods
33
    // =========================================================================
34
35
    /**
36
     * Get the total number of data samples optionally limited by siteId
37
     *
38
     * @param int    $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
39
     * @param string $column
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
40
     *
41
     * @return int|string
42
     */
43
    public function totalSamples(int $siteId, string $column)
44
    {
45
        // Get the total number of data samples
46
        $query = (new Query())
47
            ->from(['{{%webperf_data_samples}}'])
48
            ->where(['not', [$column => null]])
0 ignored issues
show
Coding Style introduced by
Space after closing parenthesis of function call prohibited
Loading history...
49
            ;
50
        if ((int)$siteId !== 0) {
51
            $query->andWhere(['siteId' => $siteId]);
52
        }
53
        return $query->count();
54
    }
55
56
    /**
57
     * Get the page title from data samples by URL and optionally siteId
58
     *
59
     * @param string   $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 1 spaces after parameter type; 3 found
Loading history...
60
     * @param int $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 4 spaces after parameter type; 1 found
Loading history...
61
     *
62
     * @return string
63
     */
64
    public function pageTitle(string $url, int $siteId = 0): string
65
    {
66
        // Get the page title from a URL
67
        $query = (new Query())
68
            ->select(['title'])
69
            ->from(['{{%webperf_data_samples}}'])
70
            ->where([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
71
                'and', ['url' => $url],
72
                ['not', ['title' => '']],
73
            ])
0 ignored issues
show
Coding Style introduced by
Space after closing parenthesis of function call prohibited
Loading history...
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
74
        ;
75
        if ((int)$siteId !== 0) {
76
            $query->andWhere(['siteId' => $siteId]);
77
        }
78
        $result = $query->one();
79
80
        return $result['title'] ?? '';
81
    }
82
83
    /**
84
     * Add a data sample to the webperf_data_samples table
85
     *
86
     * @param DataSample $dataSample
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
87
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
88
    public function addDataSample(DataSample $dataSample)
89
    {
90
        // Validate the model before saving it to the db
91
        if ($dataSample->validate() === false) {
92
            Craft::error(
93
                Craft::t(
94
                    'webperf',
95
                    'Error validating data sample: {errors}',
96
                    ['errors' => print_r($dataSample->getErrors(), true)]
97
                ),
98
                __METHOD__
99
            );
100
101
            return;
102
        }
103
        $isNew = true;
104
        if (!empty($dataSample->requestId)) {
105
            // See if a data sample exists with the same requestId already
106
            $testSample = (new Query())
107
                ->from(['{{%webperf_data_samples}}'])
108
                ->where(['requestId' => $dataSample->requestId])
109
                ->one();
110
            // If it exists, update it rather than having duplicates
111
            if (!empty($testSample)) {
112
                $isNew = false;
113
            }
114
        }
115
        // Get the validated model attributes and save them to the db
116
        $dataSampleConfig = $dataSample->getAttributes($dataSample->fields());
117
        $db = Craft::$app->getDb();
118
        if ($isNew) {
119
            Craft::debug('Creating new data sample', __METHOD__);
120
            // Create a new record
121
            try {
122
                $db->createCommand()->insert(
123
                    '{{%webperf_data_samples}}',
124
                    $dataSampleConfig
125
                )->execute();
126
            } catch (\Exception $e) {
127
                Craft::error($e->getMessage(), __METHOD__);
128
            }
129
        } else {
130
            Craft::debug('Updating existing data sample', __METHOD__);
131
            // Update the existing record
132
            try {
133
                $db->createCommand()->update(
134
                    '{{%webperf_data_samples}}',
135
                    $dataSampleConfig,
136
                    [
137
                        'requestId' => $dataSample->requestId,
138
                    ]
139
                )->execute();
140
            } catch (\Exception $e) {
141
                Craft::error($e->getMessage(), __METHOD__);
142
            }
143
        }
144
        // Trim orphaned samples
145
        $this->trimOrphanedSamples($dataSample->requestId);
146
        // After adding the DataSample, trim the webperf_data_samples db table
147
        if (Webperf::$settings->automaticallyTrimDataSamples) {
148
            $this->trimDataSamples();
149
        }
150
    }
151
152
    /**
153
     * Delete a data sample by id
154
     *
155
     * @param int $id
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
156
     *
157
     * @return int The result
158
     */
159
    public function deleteSampleById(int $id): int
160
    {
161
        $db = Craft::$app->getDb();
162
        // Delete a row from the db table
163
        try {
164
            $result = $db->createCommand()->delete(
165
                '{{%webperf_data_samples}}',
166
                [
167
                    'id' => $id,
168
                ]
169
            )->execute();
170
        } catch (\Exception $e) {
171
            Craft::error($e->getMessage(), __METHOD__);
172
            $result = 0;
173
        }
174
175
        return $result;
176
    }
177
178
    /**
179
     * Delete data samples by URL and optionally siteId
180
     *
181
     * @param string   $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
182
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
183
     *
184
     * @return int
185
     */
186
    public function deleteDataSamplesByUrl(string $url, int $siteId = null): int
187
    {
188
        $db = Craft::$app->getDb();
189
        // Delete a row from the db table
190
        try {
191
            $conditions = ['url' => $url];
192
            if ($siteId !== null) {
193
                $conditions['siteId'] = $siteId;
194
            }
195
            $result = $db->createCommand()->delete(
196
                '{{%webperf_data_samples}}',
197
                $conditions
198
            )->execute();
199
        } catch (\Exception $e) {
200
            Craft::error($e->getMessage(), __METHOD__);
201
            $result = 0;
202
        }
203
204
        return $result;
205
    }
206
207
    /**
208
     * Delete data all samples optionally siteId
209
     *
210
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
211
     *
212
     * @return int
213
     */
214
    public function deleteAllDataSamples(int $siteId = null): int
215
    {
216
        $db = Craft::$app->getDb();
217
        // Delete a row from the db table
218
        try {
219
            $conditions = [];
220
            if ($siteId !== null) {
221
                $conditions['siteId'] = $siteId;
222
            }
223
            $result = $db->createCommand()->delete(
224
                '{{%webperf_data_samples}}',
225
                $conditions
226
            )->execute();
227
        } catch (\Exception $e) {
228
            Craft::error($e->getMessage(), __METHOD__);
229
            $result = 0;
230
        }
231
232
        return $result;
233
    }
234
235
    /**
236
     * Trim any samples that our outliers
237
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
238
    public function trimOutlierSamples()
239
    {
240
        $db = Craft::$app->getDb();
241
        Craft::debug('Trimming outlier samples', __METHOD__);
242
        // Get the average pageload time
243
        $stats = (new Query())
244
            ->from('{{%webperf_data_samples}}')
245
            ->select([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
246
                'AVG(pageload) AS avg',
247
            ])
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
248
            ->one();
249
        if (!empty($stats['avg'])) {
250
            $threshold = $stats['avg'] * self::OUTLIER_PAGELOAD_MULTIPLIER;
251
            // Delete any samples that are far above average
252
            try {
253
                $result = $db->createCommand()->delete(
254
                    '{{%webperf_data_samples}}',
255
                    ['>', 'pageLoad', $threshold]
256
                )->execute();
257
                Craft::debug($result, __METHOD__);
258
            } catch (\Exception $e) {
259
                Craft::error($e->getMessage(), __METHOD__);
260
            }
261
        }
262
    }
263
264
    /**
265
     * Trim samples that have the placeholder in the URL, aka they never
266
     * received the Boomerang beacon
267
     *
268
     * @param int $requestId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
269
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
270
    public function trimOrphanedSamples($requestId)
271
    {
272
        $db = Craft::$app->getDb();
273
        Craft::debug('Trimming orphaned samples', __METHOD__);
274
        // Update the existing record
275
        try {
276
            $result = $db->createCommand()->delete(
277
                '{{%webperf_data_samples}}',
278
                [
279
                    'and', ['url' => DataSample::PLACEHOLDER_URL],
280
                    ['not', ['requestId' => $requestId]],
281
                ]
282
            )->execute();
283
            Craft::debug($result, __METHOD__);
284
        } catch (\Exception $e) {
285
            Craft::error($e->getMessage(), __METHOD__);
286
        }
287
    }
288
289
    /**
290
     * Trim the webperf_data_samples db table based on the dataSamplesStoredLimit
291
     * config.php setting
292
     *
293
     * @param int|null $limit
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
294
     *
295
     * @return int
296
     */
297
    public function trimDataSamples(int $limit = null): int
298
    {
299
        $this->trimOutlierSamples();
300
        $affectedRows = 0;
301
        $db = Craft::$app->getDb();
302
        $quotedTable = $db->quoteTableName('{{%webperf_data_samples}}');
303
        $limit = $limit ?? Webperf::$settings->dataSamplesStoredLimit;
304
305
        if ($limit !== null) {
0 ignored issues
show
introduced by
The condition $limit !== null is always true.
Loading history...
306
            //  https://stackoverflow.com/questions/578867/sql-query-delete-all-records-from-the-table-except-latest-n
307
            try {
308
                if ($db->getIsMysql()) {
309
                    // Handle MySQL
310
                    $affectedRows = $db->createCommand(/** @lang mysql */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
311
                        "
312
                        DELETE FROM {$quotedTable}
313
                        WHERE id NOT IN (
314
                          SELECT id
315
                          FROM (
316
                            SELECT id
317
                            FROM {$quotedTable}
318
                            ORDER BY dateUpdated DESC
319
                            LIMIT {$limit}
320
                          ) foo
321
                        )
322
                        "
323
                    )->execute();
324
                }
325
                if ($db->getIsPgsql()) {
326
                    // Handle Postgres
327
                    $affectedRows = $db->createCommand(/** @lang mysql */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
328
                        "
329
                        DELETE FROM {$quotedTable}
330
                        WHERE id NOT IN (
331
                          SELECT id
332
                          FROM (
333
                            SELECT id
334
                            FROM {$quotedTable}
335
                            ORDER BY \"dateUpdated\" DESC
336
                            LIMIT {$limit}
337
                          ) foo
338
                        )
339
                        "
340
                    )->execute();
341
                }
342
            } catch (\Exception $e) {
343
                Craft::error($e->getMessage(), __METHOD__);
344
            }
345
            Craft::info(
346
                Craft::t(
347
                    'webperf',
348
                    'Trimmed {rows} from webperf_data_samples table',
349
                    ['rows' => $affectedRows]
350
                ),
351
                __METHOD__
352
            );
353
        }
354
355
        return $affectedRows;
356
    }
357
}
358