Passed
Push — master ( a8ec29...5f73fa )
by Chauncey
08:25
created

ClearCacheTemplate::hasStats()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Admin\Template\System;
4
5
use APCUIterator;
6
use APCIterator;
7
use DateInterval;
8
use DateTimeInterface;
9
use DateTime;
10
use RuntimeException;
11
12
use Stash\Driver\Apc;
13
use Stash\Driver\Ephemeral;
14
use Stash\Driver\Memcache;
15
16
use Pimple\Container;
17
18
// From 'charcoal-admin'
19
use Charcoal\Admin\AdminTemplate;
20
21
/**
22
 * Cache information.
23
 */
24
class ClearCacheTemplate extends AdminTemplate
25
{
26
    /**
27
     * Cache service.
28
     *
29
     * @var \Stash\Pool
30
     */
31
    private $cache;
32
33
    /**
34
     * Summary of cache.
35
     *
36
     * @var array
37
     */
38
    private $cacheInfo;
39
40
    /**
41
     * Cache service config.
42
     *
43
     * @var \Charcoal\App\Config\CacheConfig
0 ignored issues
show
Bug introduced by
The type Charcoal\App\Config\CacheConfig 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...
44
     */
45
    private $cacheConfig;
46
47
    /**
48
     * Driver Name => Class Name.
49
     *
50
     * @var array
51
     */
52
    private $availableCacheDrivers;
53
54
    /**
55
     * Regular expression pattern to match a Stash / APC cache key.
56
     *
57
     * @var string
58
     */
59
    private $apcCacheKeyPattern;
60
61
    /**
62
     * Retrieve the title of the page.
63
     *
64
     * @return \Charcoal\Translator\Translation|string|null
65
     */
66
    public function title()
67
    {
68
        if ($this->title === null) {
69
            $this->setTitle($this->translator()->translation('Cache information'));
70
        }
71
72
        return $this->title;
73
    }
74
75
    /**
76
     * @return \Charcoal\Admin\Widget\SidemenuWidgetInterface|null
77
     */
78
    public function sidemenu()
79
    {
80
        if ($this->sidemenu === null) {
81
            $this->sidemenu = $this->createSidemenu('system');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->createSidemenu('system') can also be of type Charcoal\Admin\Widget\SidemenuWidgetInterface. However, the property $sidemenu is declared as type Charcoal\Admin\SideMenuWidgetInterface. 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...
82
        }
83
84
        return $this->sidemenu;
85
    }
86
87
    /**
88
     * @param  boolean $force Whether to reload cache information.
89
     * @return array
90
     */
91
    public function cacheInfo($force = false)
92
    {
93
        if ($this->cacheInfo === null || $force === true) {
94
            $flip      = array_flip($this->availableCacheDrivers);
95
            $driver    = get_class($this->cache->getDriver());
96
            $cacheType = isset($flip['\\'.$driver]) ? $flip['\\'.$driver] : $driver;
97
98
            $globalItems = $this->globalCacheItems();
99
            $this->cacheInfo = [
100
                'type'              => $cacheType,
101
                'active'            => $this->cacheConfig['active'],
102
                'namespace'         => $this->getCacheNamespace(),
103
                'global'            => $this->globalCacheInfo(),
104
                'pages'             => $this->pagesCacheInfo(),
105
                'objects'           => $this->objectsCacheInfo(),
106
                'global_items'      => $globalItems,
107
                'has_global_items'  => !empty($globalItems),
108
            ];
109
        }
110
111
        return $this->cacheInfo;
112
    }
113
114
    /**
115
     * @return string
116
     */
117
    private function getCacheNamespace()
118
    {
119
        return $this->cache->getNamespace();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->cache->getNamespace() could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
120
    }
121
122
    /**
123
     * @return string
124
     */
125
    private function getApcNamespace()
126
    {
127
        return $this->cacheConfig['prefix'];
128
    }
129
130
    /**
131
     * @return string
132
     */
133
    private function getGlobalCacheKey()
134
    {
135
        return '/::'.$this->getCacheNamespace().'::/';
136
    }
137
138
    /**
139
     * @return array
140
     */
141
    private function globalCacheInfo()
142
    {
143
        if ($this->isApc()) {
144
            $cacheKey = $this->getGlobalCacheKey();
145
            return $this->apcCacheInfo($cacheKey);
146
        } else {
147
            return [
148
                'num_entries'  => 0,
149
                'total_size'   => 0,
150
                'average_size' => 0,
151
                'total_hits'   => 0,
152
                'average_hits' => 0,
153
            ];
154
        }
155
    }
156
157
    /**
158
     * @return array
159
     */
160
    private function globalCacheItems()
161
    {
162
        if ($this->isApc()) {
163
            $cacheKey = $this->getGlobalCacheKey();
164
            return $this->apcCacheItems($cacheKey);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->apcCacheItems($cacheKey) returns the type Generator which is incompatible with the documented return type array.
Loading history...
165
        } else {
166
            return [];
167
        }
168
    }
169
170
    /**
171
     * @return string
172
     */
173
    private function getPagesCacheKey()
174
    {
175
        return '/::'.$this->getCacheNamespace().'::request::|::'.$this->getCacheNamespace().'::template::/';
176
    }
177
178
    /**
179
     * @return array
180
     */
181
    private function pagesCacheInfo()
182
    {
183
        if ($this->isApc()) {
184
            $cacheKey = $this->getPagesCacheKey();
185
            return $this->apcCacheInfo($cacheKey);
186
        } else {
187
            return [
188
                'num_entries'  => 0,
189
                'total_size'   => 0,
190
                'average_size' => 0,
191
                'total_hits'   => 0,
192
                'average_hits' => 0,
193
            ];
194
        }
195
    }
196
197
    /**
198
     * @return array
199
     */
200
    private function pagesCacheItems()
0 ignored issues
show
Unused Code introduced by
The method pagesCacheItems() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
201
    {
202
        if ($this->isApc()) {
203
            $cacheKey = $this->getPagesCacheKey();
204
            return $this->apcCacheItems($cacheKey);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->apcCacheItems($cacheKey) returns the type Generator which is incompatible with the documented return type array.
Loading history...
205
        } else {
206
            return [];
207
        }
208
    }
209
210
    /**
211
     * @return string
212
     */
213
    private function getObjectsCacheKey()
214
    {
215
        return '/::'.$this->getCacheNamespace().'::object::|::'.$this->getCacheNamespace().'::metadata::/';
216
    }
217
218
    /**
219
     * @return array
220
     */
221
    private function objectsCacheInfo()
222
    {
223
        if ($this->isApc()) {
224
            $cacheKey = $this->getObjectsCacheKey();
225
            return $this->apcCacheInfo($cacheKey);
226
        } else {
227
            return [
228
                'num_entries'  => 0,
229
                'total_size'   => 0,
230
                'average_size' => 0,
231
                'total_hits'   => 0,
232
                'average_hits' => 0,
233
            ];
234
        }
235
    }
236
237
    /**
238
     * @return array
239
     */
240
    private function objectsCacheItems()
0 ignored issues
show
Unused Code introduced by
The method objectsCacheItems() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
241
    {
242
        if ($this->isApc()) {
243
            $cacheKey = $this->getObjectsCacheKey();
244
            return $this->apcCacheItems($cacheKey);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->apcCacheItems($cacheKey) returns the type Generator which is incompatible with the documented return type array.
Loading history...
245
        } else {
246
            return [];
247
        }
248
    }
249
250
    /**
251
     * @param  string $key The cache key to look at.
252
     * @return array
253
     */
254
    private function apcCacheInfo($key)
255
    {
256
        $iter = $this->createApcIterator($key);
257
258
        $numEntries = 0;
259
        $sizeTotal  = 0;
260
        $hitsTotal  = 0;
261
        $ttlTotal   = 0;
262
        foreach ($iter as $item) {
263
            $numEntries++;
264
            $sizeTotal += $item['mem_size'];
265
            $hitsTotal += $item['num_hits'];
266
            $ttlTotal  += $item['ttl'];
267
        }
268
        $sizeAvg = $numEntries ? ($sizeTotal / $numEntries) : 0;
269
        $hitsAvg = $numEntries ? ($hitsTotal / $numEntries) : 0;
270
        return [
271
            'num_entries'  => $numEntries,
272
            'total_size'   => $this->formatBytes($sizeTotal),
273
            'average_size' => $this->formatBytes($sizeAvg),
274
            'total_hits'   => $hitsTotal,
275
            'average_hits' => $hitsAvg,
276
        ];
277
    }
278
279
    /**
280
     * @param  string $key The cache key to look at.
281
     * @return array|\Generator
282
     */
283
    private function apcCacheItems($key)
284
    {
285
        $iter = $this->createApcIterator($key);
286
287
        foreach ($iter as $item) {
288
            $item['ident'] = $this->formatApcCacheKey($item['key']);
289
            $item['size']  = $this->formatBytes($item['mem_size']);
290
291
            $item['expiration_time'] = ($item['creation_time'] + $item['ttl']);
292
293
            $date1 = new DateTime('@'.$item['creation_time']);
294
            $date2 = new DateTime('@'.$item['expiration_time']);
295
296
            $item['created'] = $date1->format('Y-m-d H:i:s');
297
            $item['expiry']  = $date2->format('Y-m-d H:i:s');
298
            $item['timeout'] = $this->formatTimeDiff($date1, $date2);
299
            yield $item;
300
        }
301
    }
302
303
    /**
304
     * @param  string $key The cache item key to load.
305
     * @throws RuntimeException If the APC Iterator class is missing.
306
     * @return \APCIterator|\APCUIterator|null
307
     */
308
    private function createApcIterator($key)
309
    {
310
        if (class_exists('\\APCUIterator', false)) {
311
            return new \APCUIterator($key);
312
        } elseif (class_exists('\\APCIterator', false)) {
313
            return new \APCIterator('user', $key);
314
        } else {
315
            throw new RuntimeException('Cache uses APC but no iterator could be found.');
316
        }
317
    }
318
319
    /**
320
     * Determine if Charcoal has cache statistics.
321
     *
322
     * @return boolean
323
     */
324
    public function hasStats()
325
    {
326
        return $this->isApc();
327
    }
328
329
    /**
330
     * Determine if Charcoal is using the APC driver.
331
     *
332
     * @return boolean
333
     */
334
    public function isApc()
335
    {
336
        return is_a($this->cache->getDriver(), Apc::class);
337
    }
338
339
    /**
340
     * Determine if Charcoal is using the Memcache driver.
341
     *
342
     * @return boolean
343
     */
344
    public function isMemcache()
345
    {
346
        return is_a($this->cache->getDriver(), Memcache::class);
347
    }
348
349
    /**
350
     * Determine if Charcoal is using the Ephemeral driver.
351
     *
352
     * @return boolean
353
     */
354
    public function isMemory()
355
    {
356
        return is_a($this->cache->getDriver(), Ephemeral::class);
357
    }
358
359
    /**
360
     * Get the RegExp pattern to match a Stash / APC cache key.
361
     *
362
     * Breakdown:
363
     * - `apcID`: Installation ID
364
     * - `apcNS`: Optional. Application Key or Installation ID
365
     * - `stashNS`: Stash Segment
366
     * - `poolNS`: Optional. Application Key
367
     * - `appKey`: Data Segment
368
     *
369
     * @return string
370
     */
371
    private function getApcCacheKeyPattern()
372
    {
373
        if ($this->apcCacheKeyPattern === null) {
374
            $pattern  = '/^(?<apcID>[a-f0-9]{32})::(?:(?<apcNS>';
375
            $pattern .= preg_quote($this->getApcNamespace());
376
            $pattern .= '|[a-f0-9]{32})::)?(?<stashNS>cache|sp)::(?:(?<poolNS>';
377
            $pattern .= preg_quote($this->getCacheNamespace());
378
            $pattern .= ')::)?(?<itemID>.+)$/i';
379
380
            $this->apcCacheKeyPattern = $pattern;
381
        }
382
383
        return $this->apcCacheKeyPattern;
384
    }
385
386
    /**
387
     * Human-readable identifier format.
388
     *
389
     * @param  string $key The cache item key to format.
390
     * @return string
391
     */
392
    private function formatApcCacheKey($key)
393
    {
394
        $pattern = $this->getApcCacheKeyPattern();
395
        if (preg_match($pattern, $key, $matches)) {
396
            $sns = $matches['stashNS'];
0 ignored issues
show
Unused Code introduced by
The assignment to $sns is dead and can be removed.
Loading history...
397
            $iid = trim($matches['itemID'], ':');
398
            $iid = preg_replace([ '/:+/', '/\.+/' ], [ '⇒', '/' ], $iid);
399
            $key = $matches['stashNS'] . '⇒' . $iid;
400
        }
401
402
        return $key;
403
    }
404
405
    /**
406
     * Human-readable bytes format.
407
     *
408
     * @param  integer $bytes The number of bytes to format.
409
     * @return string
410
     */
411
    private function formatBytes($bytes)
412
    {
413
        if ($bytes === 0) {
414
            return 0;
415
        }
416
417
        $units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
418
        $base  = log($bytes, 1024);
419
        $floor = floor($base);
420
        $unit  = $units[$floor];
421
        $size  = round(pow(1024, ($base - $floor)), 2);
422
423
        $locale = localeconv();
424
        $size   = number_format($size, 2, $locale['decimal_point'], $locale['thousands_sep']);
425
426
        return rtrim($size, '.0').' '.$unit;
427
    }
428
429
    /**
430
     * Human-readable time difference.
431
     *
432
     * Note: Adapted from CakePHP\Chronos.
433
     *
434
     * @see https://github.com/cakephp/chronos/blob/1.1.4/LICENSE
435
     *
436
     * @param DateTimeInterface      $date1 The datetime to start with.
437
     * @param DateTimeInterface|null $date2 The datetime to compare against.
438
     * @return string
439
     */
440
    private function formatTimeDiff(DateTimeInterface $date1, DateTimeInterface $date2 = null)
441
    {
442
        $isNow = $date2 === null;
443
        if ($isNow) {
444
            $date2 = new DateTime('now', $date1->getTimezone());
445
        }
446
        $interval = $date1->diff($date2);
447
448
        $translator = $this->translator();
449
450
        switch (true) {
451
            case ($interval->y > 0):
452
                $unit  = 'time.year';
453
                $count = $interval->y;
454
                break;
455
            case ($interval->m > 0):
456
                $unit  = 'time.month';
457
                $count = $interval->m;
458
                break;
459
            case ($interval->d > 0):
460
                $unit  = 'time.day';
461
                $count = $interval->d;
462
                if ($count >= 7) {
463
                    $unit  = 'time.week';
464
                    $count = (int)($count / 7);
465
                }
466
                break;
467
            case ($interval->h > 0):
468
                $unit  = 'time.hour';
469
                $count = $interval->h;
470
                break;
471
            case ($interval->i > 0):
472
                $unit  = 'time.minute';
473
                $count = $interval->i;
474
                break;
475
            default:
476
                $count = $interval->s;
477
                $unit  = 'time.second';
478
                break;
479
        }
480
481
        $time = $translator->transChoice($unit, $count, [ '{{ count }}' => $count ]);
482
483
        return $time;
484
    }
485
486
    /**
487
     * @param Container $container Pimple DI Container.
488
     * @return void
489
     */
490
    protected function setDependencies(Container $container)
491
    {
492
        parent::setDependencies($container);
493
494
        $this->availableCacheDrivers = $container['cache/available-drivers'];
495
        $this->cache                 = $container['cache'];
496
        $this->cacheConfig           = $container['cache/config'];
497
    }
498
}
499