HtmlCache   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 681
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 2.57%

Importance

Changes 0
Metric Value
wmc 90
lcom 1
cbo 5
dl 0
loc 681
rs 1.919
c 0
b 0
f 0
ccs 8
cts 311
cp 0.0257

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getExtension() 0 4 1
A setExtension() 0 4 1
A setOptions() 0 8 2
A getOptions() 0 7 2
A flush() 0 30 4
B clearExpired() 0 44 6
B clearByTags() 0 42 10
A getIterator() 0 10 2
A internalGetItem() 0 25 4
B internalGetItems() 0 37 6
A internalHasItem() 0 27 5
A getMetadata() 0 9 3
A getMetadatas() 0 9 3
A internalGetMetadata() 0 25 4
A internalGetMetadatas() 0 27 4
A internalSetItem() 0 24 2
B internalSetItems() 0 32 6
A checkAndSetItem() 0 9 3
A internalCheckAndSetItem() 0 18 3
A touchItem() 0 9 3
A touchItems() 0 9 3
A internalTouchItem() 0 21 3
A removeItem() 0 9 3
A removeItems() 0 9 3
A internalRemoveItem() 0 11 2
A getFileSpec() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like HtmlCache 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 HtmlCache, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * To change this license header, choose License Headers in Project Properties.
4
 * To change this template file, choose Tools | Templates
5
 * and open the template in the editor.
6
 */
7
8
namespace Columnis\Model;
9
10
use Exception as BaseException;
11
use GlobIterator;
12
use Zend\Cache\Exception;
13
use Zend\Cache\Storage\Adapter\Filesystem;
14
use Zend\Cache\Storage\Adapter\FilesystemOptions;
15
use Zend\Cache\Storage\Adapter\FilesystemIterator;
16
use Zend\Stdlib\ErrorHandler;
17
use ArrayObject;
18
19
class HtmlCache extends Filesystem
20
{
21
    protected $extension = '.html';
22
23
    public function getExtension()
24
    {
25
        return $this->extension;
26
    }
27
28
    public function setExtension($extension)
29
    {
30
        $this->extension = $extension;
31
    }
32
33
    /**
34
     * Set options.
35
     *
36
     * @param  FilesystemOptions $options
37
     * @return Filesystem
38
     * @see    getOptions()
39
     */
40 1
    public function setOptions($options)
41
    {
42 1
        if (!$options instanceof FilesystemOptions) {
43 1
            $options = new FilesystemOptions($options);
44 1
        }
45
46 1
        return parent::setOptions($options);
47
    }
48
49
    /**
50
     * Get options.
51
     *
52
     * @return FilesystemOptions
53
     * @see setOptions()
54
     */
55 1
    public function getOptions()
56
    {
57 1
        if (!$this->options) {
58
            $this->setOptions(new FilesystemOptions());
59
        }
60 1
        return $this->options;
61
    }
62
    /* FlushableInterface */
63
64
    /**
65
     * Flush the whole storage
66
     *
67
     * @throws Exception\RuntimeException
68
     * @return bool
69
     */
70
    public function flush()
71
    {
72
        $flags       = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
73
        $dir         = $this->getOptions()->getCacheDir();
74
        $clearFolder = null;
75
        $clearFolder = function ($dir) use (& $clearFolder, $flags) {
76
            $it = new GlobIterator($dir.DIRECTORY_SEPARATOR.'*', $flags);
77
            foreach ($it as $pathname) {
78
                if ($it->isDir()) {
79
                    $clearFolder($pathname);
80
                    rmdir($pathname);
81
                } else {
82
                    unlink($pathname);
83
                }
84
            }
85
        };
86
87
        ErrorHandler::start();
88
        $clearFolder($dir);
89
        $error = ErrorHandler::stop();
90
        if ($error) {
91
            throw new Exception\RuntimeException(
92
                "Flushing directory '{$dir}' failed",
93
                0,
94
                $error
95
            );
96
        }
97
98
        return true;
99
    }
100
    /* ClearExpiredInterface */
101
102
    /**
103
     * Remove expired items
104
     *
105
     * @return bool
106
     *
107
     * @triggers clearExpired.exception(ExceptionEvent)
108
     */
109
    public function clearExpired()
110
    {
111
        $options   = $this->getOptions();
112
        $namespace = $options->getNamespace();
113
        $prefix    = ($namespace === '') ? '' : $namespace.$options->getNamespaceSeparator();
114
115
        $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_FILEINFO;
116
        $path  = $options->getCacheDir()
117
            .str_repeat(DIRECTORY_SEPARATOR.$prefix.'*', $options->getDirLevel())
118
            .DIRECTORY_SEPARATOR.$prefix.'*'.$this->getExtension();
119
        $glob  = new GlobIterator($path, $flags);
120
        $time  = time();
121
        $ttl   = $options->getTtl();
122
123
        ErrorHandler::start();
124
        foreach ($glob as $entry) {
125
            $mtime = $entry->getMTime();
126
            if ($time >= $mtime + $ttl) {
127
                $pathname = $entry->getPathname();
128
                unlink($pathname);
129
130
                $tagPathname = substr($pathname, 0, -4).'.tag';
131
                if (file_exists($tagPathname)) {
132
                    unlink($tagPathname);
133
                }
134
            }
135
        }
136
        $error = ErrorHandler::stop();
137
        if ($error) {
138
            $result = false;
139
            return $this->triggerException(
140
                __FUNCTION__,
141
                new ArrayObject(),
142
                $result,
143
                new Exception\RuntimeException(
144
                    'Failed to clear expired items',
145
                    0,
146
                    $error
147
                )
148
            );
149
        }
150
151
        return true;
152
    }
153
154
    /**
155
     * Remove items matching given tags.
156
     *
157
     * If $disjunction only one of the given tags must match
158
     * else all given tags must match.
159
     *
160
     * @param string[] $tags
161
     * @param  bool  $disjunction
162
     * @return bool
163
     */
164
    public function clearByTags(array $tags, $disjunction = false)
165
    {
166
        if (!$tags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tags of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
167
            return true;
168
        }
169
170
        $tagCount  = count($tags);
171
        $options   = $this->getOptions();
172
        $namespace = $options->getNamespace();
173
        $prefix    = ($namespace === '') ? '' : $namespace.$options->getNamespaceSeparator();
174
175
        $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
176
        $path  = $options->getCacheDir()
177
            .str_repeat(DIRECTORY_SEPARATOR.$prefix.'*', $options->getDirLevel())
178
            .DIRECTORY_SEPARATOR.$prefix.'*.tag';
179
        $glob  = new GlobIterator($path, $flags);
180
181
        foreach ($glob as $pathname) {
182
            $diff = array_diff(
183
                $tags,
184
                explode("\n", $this->getFileContent($pathname))
185
            );
186
187
            $rem = false;
188
            if ($disjunction && count($diff) < $tagCount) {
189
                $rem = true;
190
            } elseif (!$disjunction && !$diff) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $diff of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
191
                $rem = true;
192
            }
193
194
            if ($rem) {
195
                unlink($pathname);
196
197
                $datPathname = substr($pathname, 0, -4).$this->getExtension();
198
                if (file_exists($datPathname)) {
199
                    unlink($datPathname);
200
                }
201
            }
202
        }
203
204
        return true;
205
    }
206
    /* IterableInterface */
207
208
    /**
209
     * Get the storage iterator
210
     *
211
     * @return FilesystemIterator
212
     */
213
    public function getIterator()
214
    {
215
        $options   = $this->getOptions();
216
        $namespace = $options->getNamespace();
217
        $prefix    = ($namespace === '') ? '' : $namespace.$options->getNamespaceSeparator();
218
        $path      = $options->getCacheDir()
219
            .str_repeat(DIRECTORY_SEPARATOR.$prefix.'*', $options->getDirLevel())
220
            .DIRECTORY_SEPARATOR.$prefix.'*'.$this->getExtension();
221
        return new FilesystemIterator($this, $path, $prefix);
222
    }
223
224
    /**
225
     * Internal method to get an item.
226
     *
227
     * @param  string  $normalizedKey
228
     * @param  bool $success
229
     * @param  mixed   $casToken
230
     * @return null|string Data on success, null on failure
231
     * @throws Exception\ExceptionInterface
232
     */
233
    protected function internalGetItem(
234
        & $normalizedKey,
235
        & $success = null,
236
        & $casToken = null
237
    ) {
238
        if (!$this->internalHasItem($normalizedKey)) {
239
            $success = false;
240
            return null;
241
        }
242
243
        try {
244
            $filespec = $this->getFileSpec($normalizedKey);
245
            $data     = $this->getFileContent($filespec.$this->getExtension());
246
247
            // use filemtime + filesize as CAS token
248
            if (func_num_args() > 2) {
249
                $casToken = filemtime($filespec.$this->getExtension()).filesize($filespec.$this->getExtension());
250
            }
251
            $success = true;
252
            return $data;
253
        } catch (BaseException $e) {
254
            $success = false;
255
            throw $e;
256
        }
257
    }
258
259
    /**
260
     * Internal method to get multiple items.
261
     *
262
     * @param  array $normalizedKeys
263
     * @return array Associative array of keys and values
264
     * @throws Exception\ExceptionInterface
265
     */
266
    protected function internalGetItems(array & $normalizedKeys)
267
    {
268
        $keys   = $normalizedKeys; // Don't change argument passed by reference
269
        $result = array();
270
        while ($keys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
271
            // LOCK_NB if more than one items have to read
272
            $nonBlocking = count($keys) > 1;
273
            $wouldblock  = null;
274
275
            // read items
276
            foreach ($keys as $i => $key) {
277
                if (!$this->internalHasItem($key)) {
278
                    unset($keys[$i]);
279
                    continue;
280
                }
281
282
                $filespec = $this->getFileSpec($key);
283
                $data     = $this->getFileContent(
284
                    $filespec.$this->getExtension(),
285
                    $nonBlocking,
286
                    $wouldblock
287
                );
288
                if ($nonBlocking && $wouldblock) {
289
                    continue;
290
                } else {
291
                    unset($keys[$i]);
292
                }
293
294
                $result[$key] = $data;
295
            }
296
297
            // TODO: Don't check ttl after first iteration
298
            // $options['ttl'] = 0;
299
        }
300
301
        return $result;
302
    }
303
304
    protected function internalHasItem(& $normalizedKey)
305
    {
306
        $file = $this->getFileSpec($normalizedKey).$this->getExtension();
307
        if (!file_exists($file)) {
308
            return false;
309
        }
310
311
        $ttl = $this->getOptions()->getTtl();
312
        if ($ttl) {
313
            ErrorHandler::start();
314
            $mtime = filemtime($file);
315
            $error = ErrorHandler::stop();
316
            if (!$mtime) {
317
                throw new Exception\RuntimeException(
318
                    "Error getting mtime of file '{$file}'",
319
                    0,
320
                    $error
321
                );
322
            }
323
324
            if (time() >= ($mtime + $ttl)) {
325
                return false;
326
            }
327
        }
328
329
        return true;
330
    }
331
332
    /**
333
     * Get metadata
334
     *
335
     * @param string $key
336
     * @return array|bool Metadata on success, false on failure
337
     */
338
    public function getMetadata($key)
339
    {
340
        $options = $this->getOptions();
341
        if ($options->getReadable() && $options->getClearStatCache()) {
342
            clearstatcache();
343
        }
344
345
        return parent::getMetadata($key);
346
    }
347
348
    /**
349
     * Get metadatas
350
     *
351
     * @param array $keys
352
     * @param array $options
353
     * @return array Associative array of keys and metadata
354
     */
355
    public function getMetadatas(array $keys, array $options = array())
356
    {
357
        $options = $this->getOptions();
358
        if ($options->getReadable() && $options->getClearStatCache()) {
359
            clearstatcache();
360
        }
361
362
        return parent::getMetadatas($keys);
363
    }
364
365
    /**
366
     * Get info by key
367
     *
368
     * @param string $normalizedKey
369
     * @return array|bool Metadata on success, false on failure
370
     */
371
    protected function internalGetMetadata(& $normalizedKey)
372
    {
373
        if (!$this->internalHasItem($normalizedKey)) {
374
            return false;
375
        }
376
377
        $options  = $this->getOptions();
378
        $filespec = $this->getFileSpec($normalizedKey);
379
        $file     = $filespec.$this->getExtension();
380
381
        $metadata = array(
382
            'filespec' => $filespec,
383
            'mtime' => filemtime($file)
384
        );
385
386
        if (!$options->getNoCtime()) {
387
            $metadata['ctime'] = filectime($file);
388
        }
389
390
        if (!$options->getNoAtime()) {
391
            $metadata['atime'] = fileatime($file);
392
        }
393
394
        return $metadata;
395
    }
396
397
    /**
398
     * Internal method to get multiple metadata
399
     *
400
     * @param  array $normalizedKeys
401
     * @return array Associative array of keys and metadata
402
     * @throws Exception\ExceptionInterface
403
     */
404
    protected function internalGetMetadatas(array & $normalizedKeys)
405
    {
406
        $options = $this->getOptions();
407
        $result  = array();
408
409
        foreach ($normalizedKeys as $normalizedKey) {
410
            $filespec = $this->getFileSpec($normalizedKey);
411
            $file     = $filespec.$this->getExtension();
412
413
            $metadata = array(
414
                'filespec' => $filespec,
415
                'mtime' => filemtime($file),
416
            );
417
418
            if (!$options->getNoCtime()) {
419
                $metadata['ctime'] = filectime($file);
420
            }
421
422
            if (!$options->getNoAtime()) {
423
                $metadata['atime'] = fileatime($file);
424
            }
425
426
            $result[$normalizedKey] = $metadata;
427
        }
428
429
        return $result;
430
    }
431
432
    /**
433
     * Internal method to store an item.
434
     *
435
     * @param  string $normalizedKey
436
     * @param  mixed  $value
437
     * @return bool
438
     * @throws Exception\ExceptionInterface
439
     */
440
    protected function internalSetItem(& $normalizedKey, & $value)
441
    {
442
        $filespec = $this->getFileSpec($normalizedKey);
443
        $this->prepareDirectoryStructure($filespec);
444
445
        // write data in non-blocking mode
446
        $wouldblock = null;
447
        $this->putFileContent(
448
            $filespec.$this->getExtension(),
449
            $value,
450
            true,
451
            $wouldblock
452
        );
453
454
        // delete related tag file (if present)
455
        $this->unlink($filespec.'.tag');
456
457
        // Retry writing data in blocking mode if it was blocked before
458
        if ($wouldblock) {
459
            $this->putFileContent($filespec.$this->getExtension(), $value);
460
        }
461
462
        return true;
463
    }
464
465
    /**
466
     * Internal method to store multiple items.
467
     *
468
     * @param  array $normalizedKeyValuePairs
469
     * @return array Array of not stored keys
470
     * @throws Exception\ExceptionInterface
471
     */
472
    protected function internalSetItems(array & $normalizedKeyValuePairs)
473
    {
474
475
        // create an associated array of files and contents to write
476
        $contents = array();
477
        foreach ($normalizedKeyValuePairs as $key => & $value) {
478
            $filespec = $this->getFileSpec($key);
479
            $this->prepareDirectoryStructure($filespec);
480
481
            // *.ext file
482
            $contents[$filespec.$this->getExtension()] = & $value;
483
484
            // *.tag file
485
            $this->unlink($filespec.'.tag');
486
        }
487
488
        // write to disk
489
        while ($contents) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $contents of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
490
            $nonBlocking = count($contents) > 1;
491
            $wouldblock  = null;
492
493
            foreach ($contents as $file => & $content) {
494
                $this->putFileContent($file, $content, $nonBlocking, $wouldblock);
495
                if (!$nonBlocking || !$wouldblock) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $wouldblock of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
496
                    unset($contents[$file]);
497
                }
498
            }
499
        }
500
501
        // return OK
502
        return array();
503
    }
504
505
    /**
506
     * Set an item only if token matches
507
     *
508
     * It uses the token received from getItem() to check if the item has
509
     * changed before overwriting it.
510
     *
511
     * @param  mixed  $token
512
     * @param  string $key
513
     * @param  mixed  $value
514
     * @return bool
515
     * @throws Exception\ExceptionInterface
516
     * @see    getItem()
517
     * @see    setItem()
518
     */
519
    public function checkAndSetItem($token, $key, $value)
520
    {
521
        $options = $this->getOptions();
522
        if ($options->getWritable() && $options->getClearStatCache()) {
523
            clearstatcache();
524
        }
525
526
        return parent::checkAndSetItem($token, $key, $value);
527
    }
528
529
    /**
530
     * Internal method to set an item only if token matches
531
     *
532
     * @param  mixed  $token
533
     * @param  string $normalizedKey
534
     * @param  mixed  $value
535
     * @return bool
536
     * @throws Exception\ExceptionInterface
537
     * @see    getItem()
538
     * @see    setItem()
539
     */
540
    protected function internalCheckAndSetItem(
541
        & $token,
542
        & $normalizedKey,
543
        & $value
544
    ) {
545
        if (!$this->internalHasItem($normalizedKey)) {
546
            return false;
547
        }
548
549
        // use filemtime + filesize as CAS token
550
        $file  = $this->getFileSpec($normalizedKey).$this->getExtension();
551
        $check = filemtime($file).filesize($file);
552
        if ($token !== $check) {
553
            return false;
554
        }
555
556
        return $this->internalSetItem($normalizedKey, $value);
557
    }
558
559
    /**
560
     * Reset lifetime of an item
561
     *
562
     * @param  string $key
563
     * @return bool
564
     * @throws Exception\ExceptionInterface
565
     *
566
     * @triggers touchItem.pre(PreEvent)
567
     * @triggers touchItem.post(PostEvent)
568
     * @triggers touchItem.exception(ExceptionEvent)
569
     */
570
    public function touchItem($key)
571
    {
572
        $options = $this->getOptions();
573
        if ($options->getWritable() && $options->getClearStatCache()) {
574
            clearstatcache();
575
        }
576
577
        return parent::touchItem($key);
578
    }
579
580
    /**
581
     * Reset lifetime of multiple items.
582
     *
583
     * @param  array $keys
584
     * @return array Array of not updated keys
585
     * @throws Exception\ExceptionInterface
586
     *
587
     * @triggers touchItems.pre(PreEvent)
588
     * @triggers touchItems.post(PostEvent)
589
     * @triggers touchItems.exception(ExceptionEvent)
590
     */
591
    public function touchItems(array $keys)
592
    {
593
        $options = $this->getOptions();
594
        if ($options->getWritable() && $options->getClearStatCache()) {
595
            clearstatcache();
596
        }
597
598
        return parent::touchItems($keys);
599
    }
600
601
    /**
602
     * Internal method to reset lifetime of an item
603
     *
604
     * @param  string $normalizedKey
605
     * @return bool
606
     * @throws Exception\ExceptionInterface
607
     */
608
    protected function internalTouchItem(& $normalizedKey)
609
    {
610
        if (!$this->internalHasItem($normalizedKey)) {
611
            return false;
612
        }
613
614
        $filespec = $this->getFileSpec($normalizedKey);
615
616
        ErrorHandler::start();
617
        $touch = touch($filespec.$this->getExtension());
618
        $error = ErrorHandler::stop();
619
        if (!$touch) {
620
            throw new Exception\RuntimeException(
621
                "Error touching file '{$filespec}.'".$this->getExtension(),
622
                0,
623
                $error
624
            );
625
        }
626
627
        return true;
628
    }
629
630
    /**
631
     * Remove an item.
632
     *
633
     * @param  string $key
634
     * @return bool
635
     * @throws Exception\ExceptionInterface
636
     *
637
     * @triggers removeItem.pre(PreEvent)
638
     * @triggers removeItem.post(PostEvent)
639
     * @triggers removeItem.exception(ExceptionEvent)
640
     */
641
    public function removeItem($key)
642
    {
643
        $options = $this->getOptions();
644
        if ($options->getWritable() && $options->getClearStatCache()) {
645
            clearstatcache();
646
        }
647
648
        return parent::removeItem($key);
649
    }
650
651
    /**
652
     * Remove multiple items.
653
     *
654
     * @param  array $keys
655
     * @return array Array of not removed keys
656
     * @throws Exception\ExceptionInterface
657
     *
658
     * @triggers removeItems.pre(PreEvent)
659
     * @triggers removeItems.post(PostEvent)
660
     * @triggers removeItems.exception(ExceptionEvent)
661
     */
662
    public function removeItems(array $keys)
663
    {
664
        $options = $this->getOptions();
665
        if ($options->getWritable() && $options->getClearStatCache()) {
666
            clearstatcache();
667
        }
668
669
        return parent::removeItems($keys);
670
    }
671
672
    /**
673
     * Internal method to remove an item.
674
     *
675
     * @param  string $normalizedKey
676
     * @return bool
677
     * @throws Exception\ExceptionInterface
678
     */
679
    protected function internalRemoveItem(& $normalizedKey)
680
    {
681
        $filespec = $this->getFileSpec($normalizedKey);
682
        if (!file_exists($filespec.$this->getExtension())) {
683
            return false;
684
        } else {
685
            $this->unlink($filespec.$this->getExtension());
686
            $this->unlink($filespec.'.tag');
687
        }
688
        return true;
689
    }
690
691
    protected function getFileSpec($normalizedKey)
692
    {
693
        $options   = $this->getOptions();
694
        $namespace = $options->getNamespace();
695
        $prefix    = ($namespace === '') ? '' : $namespace;
696
        $path      = $options->getCacheDir().DIRECTORY_SEPARATOR;
697
        return $path.$prefix.DIRECTORY_SEPARATOR.$normalizedKey;
698
    }
699
}
700