Passed
Push — master ( 7f833c...0c217f )
by Chauncey
08:06
created

ClearCacheTemplate::getApcNamespace()   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 RuntimeException;
8
9
use Stash\Driver\Apc;
10
use Stash\Driver\Memcache;
11
12
use Pimple\Container;
13
14
// From 'charcoal-admin'
15
use Charcoal\Admin\AdminTemplate;
16
17
/**
18
 * Cache information.
19
 */
20
class ClearCacheTemplate extends AdminTemplate
21
{
22
    /**
23
     * Cache service.
24
     *
25
     * @var \Stash\Pool
26
     */
27
    private $cache;
28
29
    /**
30
     * Summary of cache.
31
     *
32
     * @var array
33
     */
34
    private $cacheInfo;
35
36
    /**
37
     * Cache service config.
38
     *
39
     * @var \Charcoal\App\Config\CacheConfig
40
     */
41
    private $cacheConfig;
42
43
    /**
44
     * Driver Name => Class Name.
45
     *
46
     * @var \Stash\Interfaces\DriverInterface
47
     */
48
    private $cacheDriver;
49
50
    /**
51
     * Driver Name => Class Name.
52
     *
53
     * @var array
54
     */
55
    private $availableCacheDrivers;
56
57
    /**
58
     * Regular expression pattern to match a Stash / APC cache key.
59
     *
60
     * @var string
61
     */
62
    private $apcCacheKeyPattern;
63
64
    /**
65
     * Retrieve the title of the page.
66
     *
67
     * @return \Charcoal\Translator\Translation|string|null
68
     */
69
    public function title()
70
    {
71
        if ($this->title === null) {
72
            $this->setTitle($this->translator()->translation('Cache information'));
73
        }
74
75
        return $this->title;
76
    }
77
78
    /**
79
     * @return \Charcoal\Admin\Widget\SidemenuWidgetInterface|null
80
     */
81
    public function sidemenu()
82
    {
83
        if ($this->sidemenu === null) {
84
            $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...
85
        }
86
87
        return $this->sidemenu;
88
    }
89
90
    /**
91
     * @param  boolean $force Whether to reload cache information.
92
     * @return array
93
     */
94
    public function cacheInfo($force = false)
95
    {
96
        if ($this->cacheInfo === null || $force === true) {
97
            $flip      = array_flip($this->availableCacheDrivers);
98
            $driver    = get_class($this->cache->getDriver());
99
            $cacheType = isset($flip['\\'.$driver]) ? $flip['\\'.$driver] : $driver;
100
101
            $globalItems = $this->globalCacheItems();
102
            # $pageItems   = $this->pagesCacheItems();
103
            # $objectItems = $this->objectsCacheItems();
104
            $this->cacheInfo = [
105
                'type'              => $cacheType,
106
                'active'            => $this->cacheConfig['active'],
107
                'namespace'         => $this->getCacheNamespace(),
108
                'global'            => $this->globalCacheInfo(),
109
                'pages'             => $this->pagesCacheInfo(),
110
                'objects'           => $this->objectsCacheInfo(),
111
                'global_items'      => $globalItems,
112
                # 'pages_items'       => $pageItems,
113
                # 'objects_items'     => $objectItems,
114
                'has_global_items'  => !empty($globalItems),
115
                # 'has_pages_items'   => !empty($pageItems),
116
                # 'has_objects_items' => !empty($objectItems),
117
            ];
118
        }
119
120
        return $this->cacheInfo;
121
    }
122
123
    /**
124
     * @return string
125
     */
126
    private function getCacheNamespace()
127
    {
128
        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...
129
    }
130
131
    /**
132
     * @return string
133
     */
134
    private function getApcNamespace()
135
    {
136
        return $this->cacheConfig['prefix'];
137
    }
138
139
    /**
140
     * @return string
141
     */
142
    private function getGlobalCacheKey()
143
    {
144
        return '/::'.$this->getCacheNamespace().'::/';
145
    }
146
147
    /**
148
     * @return array
149
     */
150
    private function globalCacheInfo()
151
    {
152
        if ($this->isApc()) {
153
            $cacheKey = $this->getGlobalCacheKey();
154
            return $this->apcCacheInfo($cacheKey);
155
        } else {
156
            return [
157
                'num_entries'  => 0,
158
                'total_size'   => 0,
159
                'average_size' => 0,
160
                'total_hits'   => 0,
161
                'average_hits' => 0,
162
            ];
163
        }
164
    }
165
166
    /**
167
     * @return array
168
     */
169
    private function globalCacheItems()
170
    {
171
        if ($this->isApc()) {
172
            $cacheKey = $this->getGlobalCacheKey();
173
            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...
174
        } else {
175
            return [];
176
        }
177
    }
178
179
    /**
180
     * @return string
181
     */
182
    private function getPagesCacheKey()
183
    {
184
        return '/::'.$this->getCacheNamespace().'::request::|::'.$this->getCacheNamespace().'::template::/';
185
    }
186
187
    /**
188
     * @return array
189
     */
190
    private function pagesCacheInfo()
191
    {
192
        if ($this->isApc()) {
193
            $cacheKey = $this->getPagesCacheKey();
194
            return $this->apcCacheInfo($cacheKey);
195
        } else {
196
            return [
197
                'num_entries'  => 0,
198
                'total_size'   => 0,
199
                'average_size' => 0,
200
                'total_hits'   => 0,
201
                'average_hits' => 0,
202
            ];
203
        }
204
    }
205
206
    /**
207
     * @return array
208
     */
209
    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...
210
    {
211
        if ($this->isApc()) {
212
            $cacheKey = $this->getPagesCacheKey();
213
            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...
214
        } else {
215
            return [];
216
        }
217
    }
218
219
    /**
220
     * @return string
221
     */
222
    private function getObjectsCacheKey()
223
    {
224
        return '/::'.$this->getCacheNamespace().'::object::|::'.$this->getCacheNamespace().'::metadata::/';
225
    }
226
227
    /**
228
     * @return array
229
     */
230
    private function objectsCacheInfo()
231
    {
232
        if ($this->isApc()) {
233
            $cacheKey = $this->getObjectsCacheKey();
234
            return $this->apcCacheInfo($cacheKey);
235
        } else {
236
            return [
237
                'num_entries'  => 0,
238
                'total_size'   => 0,
239
                'average_size' => 0,
240
                'total_hits'   => 0,
241
                'average_hits' => 0,
242
            ];
243
        }
244
    }
245
246
    /**
247
     * @return array
248
     */
249
    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...
250
    {
251
        if ($this->isApc()) {
252
            $cacheKey = $this->getObjectsCacheKey();
253
            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...
254
        } else {
255
            return [];
256
        }
257
    }
258
259
    /**
260
     * @param  string $key The cache key to look at.
261
     * @return array
262
     */
263
    private function apcCacheInfo($key)
264
    {
265
        $iter = $this->createApcIterator($key);
266
267
        $numEntries = 0;
268
        $sizeTotal  = 0;
269
        $hitsTotal  = 0;
270
        $ttlTotal   = 0;
271
        foreach ($iter as $item) {
272
            $numEntries++;
273
            $sizeTotal += $item['mem_size'];
274
            $hitsTotal += $item['num_hits'];
275
            $ttlTotal  += $item['ttl'];
276
        }
277
        $sizeAvg = $numEntries ? ($sizeTotal / $numEntries) : 0;
278
        $hitsAvg = $numEntries ? ($hitsTotal / $numEntries) : 0;
279
        return [
280
            'num_entries'  => $numEntries,
281
            'total_size'   => $this->formatBytes($sizeTotal),
282
            'average_size' => $this->formatBytes($sizeAvg),
283
            'total_hits'   => $hitsTotal,
284
            'average_hits' => $hitsAvg,
285
        ];
286
    }
287
288
    /**
289
     * @param  string $key The cache key to look at.
290
     * @return array|\Generator
291
     */
292
    private function apcCacheItems($key)
293
    {
294
        $iter = $this->createApcIterator($key);
295
296
        foreach ($iter as $item) {
297
            $item['ident']   = $this->formatApcCacheKey($item['key']);
298
            $item['size']    = $this->formatBytes($item['mem_size']);
299
            $item['created'] = date('Y-m-d H:i:s', $item['creation_time']);
300
            $item['expiry']  = date('Y-m-d H:i:s', ($item['creation_time']+$item['ttl']));
301
            yield $item;
302
        }
303
    }
304
305
    /**
306
     * @param  string $key The cache item key to load.
307
     * @throws RuntimeException If the APC Iterator class is missing.
308
     * @return \APCIterator|\APCUIterator|null
309
     */
310
    private function createApcIterator($key)
311
    {
312
        if (class_exists('\\APCUIterator', false)) {
313
            return new \APCUIterator($key);
314
        } elseif (class_exists('\\APCIterator', false)) {
315
            return new \APCIterator('user', $key);
316
        } else {
317
            throw new RuntimeException('Cache uses APC but no iterator could be found.');
318
        }
319
    }
320
321
    /**
322
     * @return boolean
323
     */
324
    private function isApc()
325
    {
326
        return is_a($this->cache->getDriver(), Apc::class);
327
    }
328
329
    /**
330
     * @return boolean
331
     */
332
    private function isMemcache()
0 ignored issues
show
Unused Code introduced by
The method isMemcache() 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...
333
    {
334
        return is_a($this->cache->getDriver(), Memcache::class);
335
    }
336
337
    /**
338
     * Get the RegExp pattern to match a Stash / APC cache key.
339
     *
340
     * Breakdown:
341
     * - `apcID`: Installation ID
342
     * - `apcNS`: Optional. Application Key or Installation ID
343
     * - `stashNS`: Stash Segment
344
     * - `poolNS`: Optional. Application Key
345
     * - `appKey`: Data Segment
346
     *
347
     * @return string
348
     */
349
    private function getApcCacheKeyPattern()
350
    {
351
        if ($this->apcCacheKeyPattern === null) {
352
            $pattern  = '/^(?<apcID>[a-f0-9]{32})::(?:(?<apcNS>';
353
            $pattern .= preg_quote($this->getApcNamespace());
354
            $pattern .= '|[a-f0-9]{32})::)?(?<stashNS>cache|sp)::(?:(?<poolNS>';
355
            $pattern .= preg_quote($this->getCacheNamespace());
356
            $pattern .= ')::)?(?<itemID>.+)$/i';
357
358
            $this->apcCacheKeyPattern = $pattern;
359
        }
360
361
        return $this->apcCacheKeyPattern;
362
    }
363
364
    /**
365
     * Human-readable identifier format.
366
     *
367
     * @param  string $key The cache item key to format.
368
     * @return string
369
     */
370
    private function formatApcCacheKey($key)
371
    {
372
        $pattern = $this->getApcCacheKeyPattern();
373
        if (preg_match($pattern, $key, $matches)) {
374
            $sns = $matches['stashNS'];
0 ignored issues
show
Unused Code introduced by
The assignment to $sns is dead and can be removed.
Loading history...
375
            $iid = trim($matches['itemID'], ':');
376
            $iid = preg_replace([ '/:+/', '/\.+/' ], [ '⇒', '/' ], $iid);
377
            $key = $matches['stashNS'] . '⇒' . $iid;
378
        }
379
380
        return $key;
381
    }
382
383
    /**
384
     * Human-readable bytes format.
385
     *
386
     * @param  integer $bytes The number of bytes to format.
387
     * @return string
388
     */
389
    private function formatBytes($bytes)
390
    {
391
        if ($bytes === 0) {
392
            return 0;
393
        }
394
395
        $units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
396
        $base  = log($bytes, 1024);
397
        $floor = floor($base);
398
        $unit  = $units[$floor];
399
        $size  = round(pow(1024, ($base - $floor)), 2);
400
401
        $locale = localeconv();
402
        $size   = number_format($size, 2, $locale['decimal_point'], $locale['thousands_sep']);
403
404
        return rtrim($size, '.0').' '.$unit;
405
    }
406
407
    /**
408
     * @param Container $container Pimple DI Container.
409
     * @return void
410
     */
411
    protected function setDependencies(Container $container)
412
    {
413
        parent::setDependencies($container);
414
415
        $this->availableCacheDrivers = $container['cache/available-drivers'];
416
        $this->cacheDriver           = $container['cache/driver'];
417
        $this->cache                 = $container['cache'];
418
        $this->cacheConfig           = $container['cache/config'];
419
    }
420
}
421