Completed
Branch FET-10486-add-timestamp-checki... (611b15)
by
unknown
136:24 queued 121:17
created

TransientCacheStorage   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 361
Duplicated Lines 4.16 %

Coupling/Cohesion

Components 2
Dependencies 0

Importance

Changes 0
Metric Value
dl 15
loc 361
rs 8.3999
c 0
b 0
f 0
wmc 46
lcom 2
cbo 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 4
B setTransientCleanupFrequency() 0 25 2
C roundTimestamp() 15 47 8
A add() 0 9 3
B get() 0 20 5
A delete() 0 4 1
B deleteMany() 0 14 5
A updateTransients() 0 8 1
A scheduleTransientCleanup() 0 10 2
A checkTransientCleanupSchedule() 0 15 4
C cleanupExpiredTransients() 0 30 7
A deleteTransientKeys() 0 16 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like TransientCacheStorage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TransientCacheStorage, and based on these observations, apply Extract Interface, too.

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();
65
        // round current time down to closest 5 minutes to simplify scheduling
66
        $this->current_time = $this->roundTimestamp(time(), '5-minutes', false);
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( 'FHEE__TransientCacheStorage__clearExpiredTransients__end', $this);
352
        }
353
        return $update;
354
    }
355
356
357
358
    /**
359
     * calls delete_transient() on each transient key provided, up to the specified limit
360
     *
361
     * @param array $transient_keys [required]
362
     * @param int   $limit
363
     * @return bool
364
     */
365
    private function deleteTransientKeys(array $transient_keys, $limit = 50)
366
    {
367
        if (empty($transient_keys)) {
368
            return false;
369
        }
370
        $counter = 0;
371
        foreach ($transient_keys as $transient_key) {
372
            if($counter === $limit){
373
                break;
374
            }
375
            delete_transient($transient_key);
376
            unset($this->transients[$transient_key]);
377
            $counter++;
378
        }
379
        return $counter > 0;
380
    }
381
382
383
}
384
// End of file TransientCacheStorage.php
385
// Location: EventEspresso\core\services\cache/TransientCacheStorage.php