Completed
Branch BUG-10381-asset-loading (afb6d5)
by
unknown
112:48 queued 102:01
created

TransientCacheStorage::deleteTransientKeys()   C

Complexity

Conditions 11
Paths 9

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 23
nc 9
nop 2
dl 0
loc 38
rs 5.2653
c 0
b 0
f 0

How to fix   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
namespace EventEspresso\core\services\cache;
3
4
use EE_Error;
5
use WP_Error;
6
use wpdb;
7
8
defined('EVENT_ESPRESSO_VERSION') || exit;
9
10
11
12
/**
13
 * Class TransientCacheStorage
14
 * Manages the creation and cleanup of transients
15
 * by tracking transient keys and their corresponding expiration.
16
 * The transient cleanup schedule is filterable
17
 * to control how often cleanup occurs
18
 *
19
 * @package       Event Espresso
20
 * @author        Brent Christensen
21
 * @since         4.9.31
22
 */
23
class TransientCacheStorage implements CacheStorageInterface
24
{
25
26
    /**
27
     * wp-option option_name for tracking transients
28
     *
29
     * @type string
30
     */
31
    const TRANSIENT_SCHEDULE_OPTIONS_KEY = 'ee_transient_schedule';
32
33
    /**
34
     * @var int $current_time
35
     */
36
    private $current_time = 0;
37
38
    /**
39
     * how often to perform transient cleanup
40
     *
41
     * @var string $transient_cleanup_frequency
42
     */
43
    private $transient_cleanup_frequency = 'hour';
44
45
    /**
46
     * options for how often to perform transient cleanup
47
     *
48
     * @var array $transient_cleanup_frequency_options
49
     */
50
    private $transient_cleanup_frequency_options = array();
51
52
    /**
53
     * @var array $transients
54
     */
55
    private $transients = array();
56
57
58
59
    /**
60
     * TransientCacheStorage constructor.
61
     */
62
    public function __construct()
63
    {
64
        $this->transient_cleanup_frequency = $this->setTransientCleanupFrequency();
0 ignored issues
show
Documentation Bug introduced by
The property $transient_cleanup_frequency was declared of type string, but $this->setTransientCleanupFrequency() is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
65
        // round current time down to closest 5 minutes to simplify scheduling
66
        $this->current_time = $this->roundTimestamp(time(), '5-minutes', false);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->roundTimestamp(time(), '5-minutes', false) can also be of type false. However, the property $current_time is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
67
        $this->transients = (array)get_option(TransientCacheStorage::TRANSIENT_SCHEDULE_OPTIONS_KEY, array());
68
        if ( ! (defined('DOING_AJAX') && DOING_AJAX) && $this->transient_cleanup_frequency !== 'off') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $this->transient_cleanup_frequency (integer) and 'off' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
69
            add_action('shutdown', array($this, 'checkTransientCleanupSchedule'), 999);
70
        }
71
    }
72
73
74
75
    /**
76
     * Sets how often transient cleanup occurs
77
     *
78
     * @return int
79
     */
80
    private function setTransientCleanupFrequency()
81
    {
82
        // sets how often transients are cleaned up
83
        $this->transient_cleanup_frequency_options = apply_filters(
84
            'FHEE__TransientCacheStorage__transient_cleanup_schedule_options',
85
            array(
86
                'off',
87
                '15-minutes',
88
                'hour',
89
                '12-hours',
90
                'day',
91
            )
92
        );
93
        $transient_cleanup_frequency = apply_filters(
94
            'FHEE__TransientCacheStorage__transient_cleanup_schedule',
95
            'hour'
96
        );
97
        return in_array(
98
            $transient_cleanup_frequency,
99
            $this->transient_cleanup_frequency_options,
100
            true
101
        )
102
            ? $transient_cleanup_frequency
103
            : 'hour';
104
    }
105
106
107
108
    /**
109
     * we need to be able to round timestamps off to match the set transient cleanup frequency
110
     * so if a transient is set to expire at 1:17 pm for example, and our cleanup schedule is every hour,
111
     * then that timestamp needs to be rounded up to 2:00 pm so that it is removed
112
     * during the next scheduled cleanup after its expiration.
113
     * We also round off the current time timestamp to the closest 5 minutes
114
     * just to make the timestamps a little easier to round which helps with debugging.
115
     *
116
     * @param int    $timestamp [required]
117
     * @param string $cleanup_frequency
118
     * @param bool   $round_up
119
     * @return false|int
120
     */
121
    private function roundTimestamp($timestamp, $cleanup_frequency = 'hour', $round_up = true)
122
    {
123
        $cleanup_frequency = $cleanup_frequency ? $cleanup_frequency : $this->transient_cleanup_frequency;
124
        // in order to round the time to the closest xx minutes (or hours),
125
        // we take the minutes (or hours) portion of the timestamp and divide it by xx,
126
        // round down to a whole number, then multiply by xx to bring us almost back up to where we were
127
        // why round down ? so the minutes (or hours) don't go over 60 (or 24)
128
        // and bump the hour, which could bump the day, which could bump the month, etc,
129
        // which would be bad because we don't always want to round up,
130
        // but when we do we can easily achieve that by simply adding the desired offset,
131
        $minutes = '00';
132
        $hours = 'H';
133
        switch ($cleanup_frequency) {
134 View Code Duplication
            case '5-minutes' :
135
                $minutes = floor((int)date('i', $timestamp) / 5) * 5;
136
                $minutes = str_pad($minutes, 2, '0', STR_PAD_LEFT);
137
                $offset = MINUTE_IN_SECONDS * 5;
138
                break;
139 View Code Duplication
            case '15-minutes' :
140
                $minutes = floor((int)date('i', $timestamp) / 15) * 15;
141
                $minutes = str_pad($minutes, 2, '0', STR_PAD_LEFT);
142
                $offset = MINUTE_IN_SECONDS * 15;
143
                break;
144 View Code Duplication
            case '12-hours' :
145
                $hours = floor((int)date('H', $timestamp) / 12) * 12;
146
                $hours = str_pad($hours, 2, '0', STR_PAD_LEFT);
147
                $offset = HOUR_IN_SECONDS * 12;
148
                break;
149
            case 'day' :
150
                $hours = '03'; // run cleanup at 3:00 am (or first site hit after that)
151
                $offset = DAY_IN_SECONDS;
152
                break;
153
            case 'hour' :
154
            default :
155
                $offset = HOUR_IN_SECONDS;
156
                break;
157
        }
158
        $rounded_timestamp = strtotime(date("Y-m-d {$hours}:{$minutes}:00", $timestamp));
159
        $rounded_timestamp += $round_up ? $offset : 0;
160
        return apply_filters(
161
            'FHEE__TransientCacheStorage__roundTimestamp__timestamp',
162
            $rounded_timestamp,
163
            $timestamp,
164
            $cleanup_frequency,
165
            $round_up
166
        );
167
    }
168
169
170
171
    /**
172
     * Saves supplied data to a transient
173
     * if an expiration is set, then it automatically schedules the transient for cleanup
174
     *
175
     * @param string $transient_key [required]
176
     * @param string $data          [required]
177
     * @param int    $expiration    number of seconds until the cache expires
178
     * @return bool
179
     */
180
    public function add($transient_key, $data, $expiration = 0)
181
    {
182
        $expiration = (int)abs($expiration);
183
        $saved = set_transient($transient_key, $data, $expiration);
184
        if ($saved && $expiration) {
185
            $this->scheduleTransientCleanup($transient_key, $expiration);
186
        }
187
        return $saved;
188
    }
189
190
191
192
    /**
193
     * retrieves transient data
194
     * automatically triggers early cache refresh for standard cache items
195
     * in order to avoid cache stampedes on busy sites.
196
     * For non-standard cache items like PHP Session data where early refreshing is not wanted,
197
     * the $standard_cache parameter should be set to false when retrieving data
198
     *
199
     * @param string $transient_key [required]
200
     * @param bool   $standard_cache
201
     * @return mixed|null
202
     */
203
    public function get($transient_key, $standard_cache = true)
204
    {
205
        // to avoid cache stampedes (AKA:dogpiles) for standard cache items,
206
        // check if known cache expires within the next minute,
207
        // and if so, remove it from our tracking and and return nothing.
208
        // this should trigger the cache content to be regenerated during this request,
209
        // while allowing any following requests to still access the existing cache
210
        // until it gets replaced with the refreshed content
211
        if (
212
            $standard_cache
213
            && isset($this->transients[$transient_key])
214
            && $this->transients[$transient_key] - time() <= MINUTE_IN_SECONDS
215
        ) {
216
            unset($this->transients[$transient_key]);
217
            $this->updateTransients();
218
            return null;
219
        }
220
        $content = get_transient($transient_key);
221
        return $content !== false ? $content : null;
222
    }
223
224
225
226
    /**
227
     * delete a single transient and remove tracking
228
     *
229
     * @param string $transient_key [required] full or partial transient key to be deleted
230
     */
231
    public function delete($transient_key)
232
    {
233
        $this->deleteMany(array($transient_key));
234
    }
235
236
237
238
    /**
239
     * delete multiple transients and remove tracking
240
     *
241
     * @param array $transient_keys [required] array of full or partial transient keys to be deleted
242
     */
243
    public function deleteMany(array $transient_keys)
244
    {
245
        $full_transient_keys = array();
246
        foreach ($this->transients as $transient_key => $expiration) {
247
            foreach ($transient_keys as $transient_key_to_delete) {
248
                if (strpos($transient_key, $transient_key_to_delete) !== false) {
249
                    $full_transient_keys[] = $transient_key;
250
                }
251
            }
252
        }
253
        if ($this->deleteTransientKeys($full_transient_keys)) {
254
            $this->updateTransients();
255
        }
256
    }
257
258
259
260
    /**
261
     * sorts transients numerically by timestamp
262
     * then saves the transient schedule to a WP option
263
     */
264
    private function updateTransients()
265
    {
266
        asort($this->transients, SORT_NUMERIC);
267
        update_option(
268
            TransientCacheStorage::TRANSIENT_SCHEDULE_OPTIONS_KEY,
269
            $this->transients
270
        );
271
    }
272
273
274
275
    /**
276
     * schedules a transient for cleanup by adding it to the transient tracking
277
     *
278
     * @param string $transient_key [required]
279
     * @param int    $expiration    [required]
280
     */
281
    private function scheduleTransientCleanup($transient_key, $expiration)
282
    {
283
        // make sure a valid future timestamp is set
284
        $expiration += $expiration < time() ? time() : 0;
285
        // and round to the closest 15 minutes
286
        $expiration = $this->roundTimestamp($expiration);
287
        // save transients to clear using their ID as the key to avoid duplicates
288
        $this->transients[$transient_key] = $expiration;
289
        $this->updateTransients();
290
    }
291
292
293
294
    /**
295
     * Since our tracked transients are sorted by their timestamps
296
     * we can grab the first transient and see when it is scheduled for cleanup.
297
     * If that timestamp is less than or equal to the current time,
298
     * then cleanup is triggered
299
     */
300
    public function checkTransientCleanupSchedule()
301
    {
302
        if (empty($this->transients)) {
303
            return;
304
        }
305
        // when do we run the next cleanup job?
306
        reset($this->transients);
307
        $next_scheduled_cleanup = current($this->transients);
308
        // if the next cleanup job is scheduled for the current hour
309
        if ($next_scheduled_cleanup <= $this->current_time) {
310
            if ($this->cleanupExpiredTransients()) {
311
                $this->updateTransients();
312
            }
313
        }
314
    }
315
316
317
318
    /**
319
     * loops through the array of tracked transients,
320
     * compiles a list of those that have expired, and sends that list off for deletion.
321
     * Also removes any bad records from the transients array
322
     *
323
     * @return bool
324
     */
325
    private function cleanupExpiredTransients()
326
    {
327
        $update = false;
328
        // filter the query limit. Set to 0 to turn off garbage collection
329
        $limit = (int)abs(
330
            apply_filters(
331
                'FHEE__TransientCacheStorage__clearExpiredTransients__limit',
332
                50
333
            )
334
        );
335
        // non-zero LIMIT means take out the trash
336
        if ($limit) {
337
            $transient_keys = array();
338
            foreach ($this->transients as $transient_key => $expiration) {
339
                if ($expiration > $this->current_time) {
340
                    continue;
341
                }
342
                if ( ! $expiration || ! $transient_key) {
343
                    unset($this->transients[$transient_key]);
344
                    $update = true;
345
                    continue;
346
                }
347
                $transient_keys[] = $transient_key;
348
            }
349
            // delete expired keys, but maintain value of $update if nothing is deleted
350
            $update = $this->deleteTransientKeys($transient_keys, $limit) ? true : $update;
351
            do_action(
352
                'FHEE__TransientCacheStorage__clearExpiredTransients__end',
353
                $limit
354
            );
355
        }
356
        return $update;
357
    }
358
359
360
361
    /**
362
     * given an array of transient keys to be deleted,
363
     * performs a single delete query using a regex pattern
364
     * so that both the transient data records
365
     * and their corresponding expiration timestamp records get deleted
366
     *
367
     * @param array $transient_keys [required]
368
     * @param int   $limit
369
     * @return bool
370
     */
371
    private function deleteTransientKeys(array $transient_keys, $limit = 0)
372
    {
373
        if (empty($transient_keys)) {
374
            return false;
375
        }
376
        /** @type wpdb $wpdb */
377
        global $wpdb;
378
        $regexp = implode('|_transient.*', $transient_keys);
379
        $SQL = "DELETE FROM {$wpdb->options} WHERE option_name REGEXP '_transient.*{$regexp}'";
380
        // scheduled deletions will have a limit set, but manual deletions will NOT
381
        $SQL .= $limit ? " LIMIT {$limit}" : '';
382
        $results = $wpdb->query($SQL);
383
        // if something went wrong, then notify the admin
384
        if ($results instanceof WP_Error) {
0 ignored issues
show
Bug introduced by
The class WP_Error does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
385
            if (is_admin()) {
386
                EE_Error::add_error($results->get_error_message(), __FILE__, __FUNCTION__, __LINE__);
387
            }
388
            return false;
389
        } else if ($results) {
390
            $deletions = 0;
391
            foreach ($transient_keys as $transient_key) {
392
                // don't unset more than what was deleted in the scheduled cleanup query
393
                if ($limit && $deletions >= $results) {
394
                    continue;
395
                }
396
                unset($this->transients[$transient_key]);
397
                // also need to manually remove the transients from the WP cache,
398
                // else they will continue to be returned if you use get_transient()
399
                if ( wp_cache_delete("_transient_{$transient_key}", 'options')) {
400
                    $deletions++;
401
                }
402
                if (wp_cache_delete("_transient_timeout_{$transient_key}", 'options')) {
403
                    $deletions++;
404
                }
405
            }
406
        }
407
        return true;
408
    }
409
410
411
}
412
// End of file TransientCacheStorage.php
413
// Location: EventEspresso\core\services\cache/TransientCacheStorage.php