Passed
Push — develop ( 21544e...adfafd )
by Andrew
03:21
created

Recommendations::totalSamples()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 11
rs 10
c 0
b 0
f 0
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 Recommendations extends Component
26
{
27
    // Constants
28
    // =========================================================================
29
30
    // Public Methods
31
    // =========================================================================
32
33
    /**
34
     * Get the total number of data samples optionally limited by siteId
35
     *
36
     * @param int    $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
37
     * @param string $column
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
38
     *
39
     * @return int|string
40
     */
41
    public function totalSamples(int $siteId, string $column)
42
    {
43
        // Get the total number of data samples
44
        $query = (new Query())
45
            ->from(['{{%webperf_data_samples}}'])
46
            ->where(['not', [$column => null]])
0 ignored issues
show
Coding Style introduced by
Space after closing parenthesis of function call prohibited
Loading history...
47
            ;
48
        if ((int)$siteId !== 0) {
49
            $query->andWhere(['siteId' => $siteId]);
50
        }
51
        return $query->count();
52
    }
53
54
    /**
55
     * Get the page title from data samples by URL and optionally siteId
56
     *
57
     * @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...
58
     * @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...
59
     *
60
     * @return string
61
     */
62
    public function pageTitle(string $url, int $siteId = 0): string
63
    {
64
        // Get the page title from a URL
65
        $query = (new Query())
66
            ->select(['title'])
67
            ->from(['{{%webperf_data_samples}}'])
68
            ->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...
69
                'and', ['url' => $url],
70
                ['not', ['title' => '']],
71
            ])
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...
72
        ;
73
        if ((int)$siteId !== 0) {
74
            $query->andWhere(['siteId' => $siteId]);
75
        }
76
        $result = $query->one();
77
        // Decode any emojis in the title
78
        if (!empty($result['title'])) {
79
            $result['title'] = html_entity_decode($result['title'], ENT_NOQUOTES, 'UTF-8');
80
        }
81
82
        return $result['title'] ?? '';
83
    }
84
85
    /**
86
     * Add a data sample to the webperf_data_samples table
87
     *
88
     * @param DataSample $dataSample
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
89
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
90
    public function addDataSample(DataSample $dataSample)
91
    {
92
        // Validate the model before saving it to the db
93
        if ($dataSample->validate() === false) {
94
            Craft::error(
95
                Craft::t(
96
                    'webperf',
97
                    'Error validating data sample: {errors}',
98
                    ['errors' => print_r($dataSample->getErrors(), true)]
99
                ),
100
                __METHOD__
101
            );
102
103
            return;
104
        }
105
        $isNew = true;
106
        if (!empty($dataSample->requestId)) {
107
            // See if a data sample exists with the same requestId already
108
            $testSample = (new Query())
109
                ->from(['{{%webperf_data_samples}}'])
110
                ->where(['requestId' => $dataSample->requestId])
111
                ->one();
112
            // If it exists, update it rather than having duplicates
113
            if (!empty($testSample)) {
114
                $isNew = false;
115
            }
116
        }
117
        // Get the validated model attributes and save them to the db
118
        $dataSampleConfig = $dataSample->getAttributes($dataSample->fields());
119
        $db = Craft::$app->getDb();
120
        if ($isNew) {
121
            Craft::debug('Creating new data sample', __METHOD__);
122
            // Create a new record
123
            try {
124
                $result = $db->createCommand()->insert(
125
                    '{{%webperf_data_samples}}',
126
                    $dataSampleConfig
127
                )->execute();
128
                Craft::debug($result, __METHOD__);
129
            } catch (\Exception $e) {
130
                Craft::error($e->getMessage(), __METHOD__);
131
            }
132
        } else {
133
            Craft::debug('Updating existing data sample', __METHOD__);
134
            // Update the existing record
135
            try {
136
                $result = $db->createCommand()->update(
137
                    '{{%webperf_data_samples}}',
138
                    $dataSampleConfig,
139
                    [
140
                        'requestId' => $dataSample->requestId,
141
                    ]
142
                )->execute();
143
                Craft::debug($result, __METHOD__);
144
            } catch (\Exception $e) {
145
                Craft::error($e->getMessage(), __METHOD__);
146
            }
147
        }
148
        // Trim orphaned samples
149
        $this->trimOrphanedSamples($dataSample->requestId);
150
        // After adding the DataSample, trim the webperf_data_samples db table
151
        if (Webperf::$settings->automaticallyTrimDataSamples) {
152
            $this->trimDataSamples();
153
        }
154
    }
155
156
    /**
157
     * Delete a data sample by id
158
     *
159
     * @param int $id
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
160
     *
161
     * @return int The result
162
     */
163
    public function deleteSampleById(int $id): int
164
    {
165
        $db = Craft::$app->getDb();
166
        // Delete a row from the db table
167
        try {
168
            $result = $db->createCommand()->delete(
169
                '{{%webperf_data_samples}}',
170
                [
171
                    'id' => $id,
172
                ]
173
            )->execute();
174
        } catch (\Exception $e) {
175
            Craft::error($e->getMessage(), __METHOD__);
176
            $result = 0;
177
        }
178
179
        return $result;
180
    }
181
182
    /**
183
     * Delete data samples by URL and optionally siteId
184
     *
185
     * @param string   $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
186
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
187
     *
188
     * @return int
189
     */
190
    public function deleteDataSamplesByUrl(string $url, int $siteId = null): int
191
    {
192
        $db = Craft::$app->getDb();
193
        // Delete a row from the db table
194
        try {
195
            $conditions = ['url' => $url];
196
            if ($siteId !== null) {
197
                $conditions['siteId'] = $siteId;
198
            }
199
            $result = $db->createCommand()->delete(
200
                '{{%webperf_data_samples}}',
201
                $conditions
202
            )->execute();
203
        } catch (\Exception $e) {
204
            Craft::error($e->getMessage(), __METHOD__);
205
            $result = 0;
206
        }
207
208
        return $result;
209
    }
210
211
    /**
212
     * Delete data all samples optionally siteId
213
     *
214
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
215
     *
216
     * @return int
217
     */
218
    public function deleteAllDataSamples(int $siteId = null): int
219
    {
220
        $db = Craft::$app->getDb();
221
        // Delete a row from the db table
222
        try {
223
            $conditions = [];
224
            if ($siteId !== null) {
225
                $conditions['siteId'] = $siteId;
226
            }
227
            $result = $db->createCommand()->delete(
228
                '{{%webperf_data_samples}}',
229
                $conditions
230
            )->execute();
231
        } catch (\Exception $e) {
232
            Craft::error($e->getMessage(), __METHOD__);
233
            $result = 0;
234
        }
235
236
        return $result;
237
    }
238
239
    /**
240
     * Trim any samples that our outliers
241
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
242
    public function trimOutlierSamples()
243
    {
244
        if (Webperf::$settings->trimOutlierDataSamples) {
245
            $db = Craft::$app->getDb();
246
            Craft::debug('Trimming outlier samples', __METHOD__);
247
            // Get the average pageload time
248
            $stats = (new Query())
249
                ->from('{{%webperf_data_samples}}')
250
                ->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...
251
                    'AVG(pageload) AS avg',
252
                ])
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...
253
                ->one();
254
            if (!empty($stats['avg'])) {
255
                $threshold = $stats['avg'] * self::OUTLIER_PAGELOAD_MULTIPLIER;
256
                // Delete any samples that are far above average
257
                try {
258
                    $result = $db->createCommand()->delete(
259
                        '{{%webperf_data_samples}}',
260
                        ['>', 'pageLoad', $threshold]
261
                    )->execute();
262
                    Craft::debug($result, __METHOD__);
263
                } catch (\Exception $e) {
264
                    Craft::error($e->getMessage(), __METHOD__);
265
                }
266
            }
267
        }
268
    }
269
270
    /**
271
     * Trim samples that have the placeholder in the URL, aka they never
272
     * received the Boomerang beacon
273
     *
274
     * @param int $requestId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
275
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
276
    public function trimOrphanedSamples($requestId)
277
    {
278
        $db = Craft::$app->getDb();
279
        Craft::debug('Trimming orphaned samples', __METHOD__);
280
        // Update the existing record
281
        try {
282
            $result = $db->createCommand()->delete(
283
                '{{%webperf_data_samples}}',
284
                [
285
                    'and', ['url' => DataSample::PLACEHOLDER_URL],
286
                    ['not', ['requestId' => $requestId]],
287
                ]
288
            )->execute();
289
            Craft::debug($result, __METHOD__);
290
        } catch (\Exception $e) {
291
            Craft::error($e->getMessage(), __METHOD__);
292
        }
293
    }
294
295
    /**
296
     * Trim the webperf_data_samples db table based on the dataSamplesStoredLimit
297
     * config.php setting
298
     *
299
     * @param int|null $limit
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
300
     *
301
     * @return int
302
     */
303
    public function trimDataSamples(int $limit = null): int
304
    {
305
        $this->trimOutlierSamples();
306
        $affectedRows = 0;
307
        $db = Craft::$app->getDb();
308
        $quotedTable = $db->quoteTableName('{{%webperf_data_samples}}');
309
        $limit = $limit ?? Webperf::$settings->dataSamplesStoredLimit;
310
311
        if ($limit !== null) {
0 ignored issues
show
introduced by
The condition $limit !== null is always true.
Loading history...
312
            //  https://stackoverflow.com/questions/578867/sql-query-delete-all-records-from-the-table-except-latest-n
313
            try {
314
                if ($db->getIsMysql()) {
315
                    // Handle MySQL
316
                    $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...
317
                        "
318
                        DELETE FROM {$quotedTable}
319
                        WHERE id NOT IN (
320
                          SELECT id
321
                          FROM (
322
                            SELECT id
323
                            FROM {$quotedTable}
324
                            ORDER BY dateUpdated DESC
325
                            LIMIT {$limit}
326
                          ) foo
327
                        )
328
                        "
329
                    )->execute();
330
                }
331
                if ($db->getIsPgsql()) {
332
                    // Handle Postgres
333
                    $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...
334
                        "
335
                        DELETE FROM {$quotedTable}
336
                        WHERE id NOT IN (
337
                          SELECT id
338
                          FROM (
339
                            SELECT id
340
                            FROM {$quotedTable}
341
                            ORDER BY \"dateUpdated\" DESC
342
                            LIMIT {$limit}
343
                          ) foo
344
                        )
345
                        "
346
                    )->execute();
347
                }
348
            } catch (\Exception $e) {
349
                Craft::error($e->getMessage(), __METHOD__);
350
            }
351
            Craft::info(
352
                Craft::t(
353
                    'webperf',
354
                    'Trimmed {rows} from webperf_data_samples table',
355
                    ['rows' => $affectedRows]
356
                ),
357
                __METHOD__
358
            );
359
        }
360
361
        return $affectedRows;
362
    }
363
}
364