Passed
Push — v1 ( 63c526...da15fc )
by Andrew
08:26 queued 05:04
created

src/services/ErrorSamples.php (2 issues)

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
8
 * @copyright Copyright (c) 2019 nystudio107
9
 */
10
11
namespace nystudio107\webperf\services;
12
13
use nystudio107\webperf\Webperf;
14
use nystudio107\webperf\base\DbErrorSampleInterface;
15
use nystudio107\webperf\events\ErrorSampleEvent;
16
17
use Craft;
18
use craft\base\Component;
19
use craft\db\Query;
20
use craft\helpers\Json;
21
22
/**
23
 * @author    nystudio107
24
 * @package   Webperf
25
 * @since     1.0.0
26
 */
27
class ErrorSamples extends Component
28
{
29
    // Constants
30
    // =========================================================================
31
32
    /**
33
     * @event ErrorSampleEvent The event that is triggered before the error sample is saved
34
     * You may set [[ErrorSampleEvent::isValid]] to `false` to prevent the error sample from getting saved.
35
     *
36
     * ```php
37
     * use nystudio107\webperf\services\ErrorSamples;
38
     * use nystudio107\retour\events\ErrorSampleEvent;
39
     *
40
     * Event::on(ErrorSamples::class,
41
     *     ErrorSamples::EVENT_BEFORE_SAVE_ERROR_SAMPLE,
42
     *     function(ErrorSampleEvent $event) {
43
     *         // potentially set $event->isValid;
44
     *     }
45
     * );
46
     * ```
47
     */
48
    const EVENT_BEFORE_SAVE_ERROR_SAMPLE = 'beforeSaveErrorSample';
49
50
    /**
51
     * @event ErrorSampleEvent The event that is triggered after the redirect is saved
52
     *
53
     * ```php
54
     * use nystudio107\webperf\services\ErrorSamples;
55
     * use nystudio107\webperf\events\ErrorSampleEvent;
56
     *
57
     * Event::on(ErrorSamples::class,
58
     *     ErrorSamples::EVENT_AFTER_SAVE_ERROR_SAMPLE,
59
     *     function(ErrorSampleEvent $event) {
60
     *         // the error sample was saved
61
     *     }
62
     * );
63
     * ```
64
     */
65
    const EVENT_AFTER_SAVE_ERROR_SAMPLE = 'afterSaveErrorSample';
66
67
    // Public Methods
68
    // =========================================================================
69
70
    /**
71
     * Get the total number of errors optionally limited by siteId
72
     *
73
     * @param int    $siteId
74
     * @param string $column
75
     *
76
     * @return int|string
77
     */
78
    public function totalErrorSamples(int $siteId, string $column)
79
    {
80
        // Get the total number of errors
81
        $query = (new Query())
82
            ->from(['{{%webperf_error_samples}}'])
83
            ->where(['not', [$column => null]])
84
            ;
85
        if ((int)$siteId !== 0) {
86
            $query->andWhere(['siteId' => $siteId]);
87
        }
88
89
        return $query->count();
90
    }
91
92
    /**
93
     * Get the total number of errors optionally limited by siteId, between
94
     * $start and $end
95
     *
96
     * @param int         $siteId
97
     * @param string      $start
98
     * @param string      $end
99
     * @param string|null $pageUrl
0 ignored issues
show
Missing parameter comment
Loading history...
100
     * @param string|null $type
101
     *
102
     * @return int
103
     */
104
    public function totalErrorSamplesRange(int $siteId, string $start, string $end, $pageUrl = null, $type = null): int
105
    {
106
        // Add a day since YYYY-MM-DD is really YYYY-MM-DD 00:00:00
107
        $end = date('Y-m-d', strtotime($end.'+1 day'));
108
        // Get the total number of errors
109
        $query = (new Query())
110
            ->from(['{{%webperf_error_samples}}'])
111
            ->where(['between', 'dateCreated', $start, $end])
112
        ;
113
        if ($type !== null) {
114
            $query
115
                ->andWhere(['type' => $type])
116
            ;
117
        }
118
        if ($pageUrl !== null) {
119
            $query
120
                ->andWhere(['url' => $pageUrl])
0 ignored issues
show
Space after closing parenthesis of function call prohibited
Loading history...
121
            ;
122
        }
123
        if ((int)$siteId !== 0) {
124
            $query->andWhere(['siteId' => $siteId]);
125
        }
126
127
        return $query->count();
128
    }
129
130
    /**
131
     * Get the page title from errors by URL and optionally siteId
132
     *
133
     * @param string   $url
134
     * @param int $siteId
135
     *
136
     * @return string
137
     */
138
    public function pageTitle(string $url, int $siteId = 0): string
139
    {
140
        // Get the page title from a URL
141
        $query = (new Query())
142
            ->select(['title'])
143
            ->from(['{{%webperf_error_samples}}'])
144
            ->where([
145
                'and', ['url' => $url],
146
                ['not', ['title' => '']],
147
            ])
148
        ;
149
        if ((int)$siteId !== 0) {
150
            $query->andWhere(['siteId' => $siteId]);
151
        }
152
        $result = $query->one();
153
        // Decode any emojis in the title
154
        if (!empty($result['title'])) {
155
            $result['title'] = html_entity_decode($result['title'], ENT_NOQUOTES, 'UTF-8');
156
        }
157
158
        return $result['title'] ?? '';
159
    }
160
161
    /**
162
     * Add an error to the webperf_error_samples table
163
     *
164
     * @param DbErrorSampleInterface $errorSample
165
     */
166
    public function addErrorSample(DbErrorSampleInterface $errorSample)
167
    {
168
        // Validate the model before saving it to the db
169
        if ($errorSample->validate() === false) {
170
            Craft::error(
171
                Craft::t(
172
                    'webperf',
173
                    'Error validating error sample: {errors}',
174
                    ['errors' => print_r($errorSample->getErrors(), true)]
175
                ),
176
                __METHOD__
177
            );
178
179
            return;
180
        }
181
        // Trigger a 'beforeSaveErrorSample' event
182
        $event = new ErrorSampleEvent([
183
            'errorSample' => $errorSample,
184
        ]);
185
        $this->trigger(self::EVENT_BEFORE_SAVE_ERROR_SAMPLE, $event);
186
        if (!$event->isValid) {
187
            return;
188
        }
189
        // Get the validated model attributes and save them to the db
190
        $errorSampleConfig = $errorSample->getAttributes();
191
        if (!empty($errorSampleConfig['pageErrors'])) {
192
            if (\is_array($errorSampleConfig['pageErrors'])) {
193
                $errorSampleConfig['pageErrors'] = Json::encode($errorSampleConfig['pageErrors']);
194
            }
195
            $db = Craft::$app->getDb();
196
            Craft::debug('Creating new error sample', __METHOD__);
197
            // Create a new record
198
            try {
199
                $result = $db->createCommand()->insert(
200
                    '{{%webperf_error_samples}}',
201
                    $errorSampleConfig
202
                )->execute();
203
                Craft::debug($result, __METHOD__);
204
            } catch (\Exception $e) {
205
                Craft::error($e->getMessage(), __METHOD__);
206
            }
207
            // Trigger a 'afterSaveErrorSample' event
208
            $this->trigger(self::EVENT_AFTER_SAVE_ERROR_SAMPLE, $event);
209
            // After adding the ErrorSample, trim the webperf_error_samples db table
210
            if (Webperf::$settings->automaticallyTrimErrorSamples) {
211
                $this->trimErrorSamples();
212
            }
213
        }
214
    }
215
216
    /**
217
     * Delete a error sample by id
218
     *
219
     * @param int $id
220
     *
221
     * @return int The result
222
     */
223
    public function deleteErrorSampleById(int $id): int
224
    {
225
        $db = Craft::$app->getDb();
226
        // Delete a row from the db table
227
        try {
228
            $result = $db->createCommand()->delete(
229
                '{{%webperf_error_samples}}',
230
                [
231
                    'id' => $id,
232
                ]
233
            )->execute();
234
        } catch (\Exception $e) {
235
            Craft::error($e->getMessage(), __METHOD__);
236
            $result = 0;
237
        }
238
239
        return $result;
240
    }
241
242
    /**
243
     * Delete error samples by URL and optionally siteId
244
     *
245
     * @param string   $url
246
     * @param int|null $siteId
247
     *
248
     * @return int
249
     */
250
    public function deleteErrorSamplesByUrl(string $url, int $siteId = null): int
251
    {
252
        $db = Craft::$app->getDb();
253
        // Delete a row from the db table
254
        try {
255
            $conditions = ['url' => $url];
256
            if ($siteId !== null) {
257
                $conditions['siteId'] = $siteId;
258
            }
259
            $result = $db->createCommand()->delete(
260
                '{{%webperf_error_samples}}',
261
                $conditions
262
            )->execute();
263
        } catch (\Exception $e) {
264
            Craft::error($e->getMessage(), __METHOD__);
265
            $result = 0;
266
        }
267
268
        return $result;
269
    }
270
271
    /**
272
     * Delete all error samples optionally siteId
273
     *
274
     * @param int|null $siteId
275
     *
276
     * @return int
277
     */
278
    public function deleteAllErrorSamples(int $siteId = null): int
279
    {
280
        $db = Craft::$app->getDb();
281
        // Delete a row from the db table
282
        try {
283
            $conditions = [];
284
            if ($siteId !== null) {
285
                $conditions['siteId'] = $siteId;
286
            }
287
            $result = $db->createCommand()->delete(
288
                '{{%webperf_error_samples}}',
289
                $conditions
290
            )->execute();
291
        } catch (\Exception $e) {
292
            Craft::error($e->getMessage(), __METHOD__);
293
            $result = 0;
294
        }
295
296
        return $result;
297
    }
298
299
    /**
300
     * Trim the webperf_error_samples db table based on the errorSamplesStoredLimit
301
     * config.php setting
302
     *
303
     * @param int|null $limit
304
     *
305
     * @return int
306
     */
307
    public function trimErrorSamples(int $limit = null): int
308
    {
309
        $affectedRows = 0;
310
        $db = Craft::$app->getDb();
311
        $quotedTable = $db->quoteTableName('{{%webperf_error_samples}}');
312
        $limit = $limit ?? Webperf::$settings->errorSamplesStoredLimit;
313
314
        if ($limit !== null) {
315
            //  https://stackoverflow.com/questions/578867/sql-query-delete-all-records-from-the-table-except-latest-n
316
            try {
317
                if ($db->getIsMysql()) {
318
                    // Handle MySQL
319
                    $affectedRows = $db->createCommand(/** @lang mysql */
320
                        "
321
                        DELETE FROM {$quotedTable}
322
                        WHERE id NOT IN (
323
                          SELECT id
324
                          FROM (
325
                            SELECT id
326
                            FROM {$quotedTable}
327
                            ORDER BY dateCreated DESC
328
                            LIMIT {$limit}
329
                          ) foo
330
                        )
331
                        "
332
                    )->execute();
333
                }
334
                if ($db->getIsPgsql()) {
335
                    // Handle Postgres
336
                    $affectedRows = $db->createCommand(/** @lang mysql */
337
                        "
338
                        DELETE FROM {$quotedTable}
339
                        WHERE id NOT IN (
340
                          SELECT id
341
                          FROM (
342
                            SELECT id
343
                            FROM {$quotedTable}
344
                            ORDER BY \"dateCreated\" DESC
345
                            LIMIT {$limit}
346
                          ) foo
347
                        )
348
                        "
349
                    )->execute();
350
                }
351
            } catch (\Exception $e) {
352
                Craft::error($e->getMessage(), __METHOD__);
353
            }
354
            Craft::info(
355
                Craft::t(
356
                    'webperf',
357
                    'Trimmed {rows} from webperf_error_samples table',
358
                    ['rows' => $affectedRows]
359
                ),
360
                __METHOD__
361
            );
362
        }
363
364
        return $affectedRows;
365
    }
366
}
367