DrawCache   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 9
dl 0
loc 236
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B save() 0 40 8
B load() 0 54 7
A nodeToCache() 0 8 2
A createCacheKey() 0 23 3
A cacheKeyTrigger() 0 15 3
B createOnReplaceHandler() 0 24 6
1
<?php
2
/**
3
 * Webino (http://webino.sk)
4
 *
5
 * @link        https://github.com/webino/WebinoDraw for the canonical source repository
6
 * @copyright   Copyright (c) 2012-2017 Webino, s. r. o. (http://webino.sk)
7
 * @author      Peter Bačinský <[email protected]>
8
 * @license     BSD-3-Clause
9
 */
10
11
namespace WebinoDraw\Cache;
12
13
use ArrayObject;
14
use DOMNode;
15
use WebinoDraw\Dom\Element;
16
use WebinoDraw\Dom\Text;
17
use WebinoDraw\Event\DrawEvent;
18
use Zend\Cache\Storage\StorageInterface;
19
use Zend\EventManager\EventManagerAwareInterface;
20
use Zend\EventManager\EventManagerAwareTrait;
21
22
/**
23
 * Class DrawCache
24
 */
25
class DrawCache implements EventManagerAwareInterface
26
{
27
    use EventManagerAwareTrait;
28
29
    /**
30
     * Cache storage service name
31
     */
32
    const STORAGE = 'WebinoDrawCache';
33
34
    /**
35
     * @var StorageInterface
36
     */
37
    protected $cache;
38
39
    /**
40
     * @var string
41
     */
42
    protected $eventIdentifier = 'WebinoDraw';
43
44
    /**
45
     * @var Element[]|Text[]
46
     */
47
    private $nodes;
48
49
    /**
50
     * @param StorageInterface|object $cache
51
     */
52
    public function __construct(StorageInterface $cache)
53
    {
54
        $this->cache = $cache;
55
    }
56
57
    /**
58
     * Save nodes XHTML to the cache
59
     *
60
     * @todo refactor
61
     * @param DrawEvent $event
62
     * @return self
63
     */
64
    public function save(DrawEvent $event)
65
    {
66
        $spec = $event->getSpec();
67
        if (empty($spec['cache'])) {
68
            return $this;
69
        }
70
71
        foreach ($this->nodes as $cacheKey => $node) {
72
            if (empty($node->ownerDocument)) {
73
                continue;
74
            }
75
76
            $doc = $node->getOwnerDocument();
77
            $cachedNode = $doc->getXpath()->query('//*[@__cacheKey="' . $cacheKey . '"]')->item(0);
78
79
            if (empty($cachedNode) || !($cachedNode instanceof Element)) {
80
                // TODO logger should be saved to cache
81
                //echo 'SHOULD SAVE: ' . print_r($spec['locator'], true);echo  '<br />';
82
                continue;
83
            }
84
85
            $cachedNode->removeAttribute('__cacheKey');
86
87
            // TODO redesign
88
            if ($node instanceof Text || 'text' === $cachedNode->getAttribute('__cache')) {
89
                $cachedNode->removeAttribute('__cache');
90
                $xhtml = $doc->saveXML($cachedNode->firstChild);
91
            } else {
92
                $xhtml = $doc->saveXML($cachedNode);
93
            }
94
95
            // TODO logger saving to cache
96
            //echo '<br />SAVE: ' . print_r($spec['cache'], true) . '<br />' . htmlspecialchars($xhtml) . '<br />';
97
            $this->cache->setItem($cacheKey, $xhtml);
98
            $this->cache->setTags($cacheKey, (array) $spec['cache']);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Zend\Cache\Storage\StorageInterface as the method setTags() does only exist in the following implementations of said interface: Zend\Cache\Storage\Adapter\BlackHole, Zend\Cache\Storage\Adapter\Filesystem, Zend\Cache\Storage\Adapter\Memory.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
99
        }
100
101
        $this->nodes = [];
102
        return $this;
103
    }
104
105
    /**
106
     * Load nodes XHTML from the cache
107
     *
108
     * @todo refactor
109
     * @param DrawEvent $event
110
     * @return bool true = loaded
111
     */
112
    public function load(DrawEvent $event)
113
    {
114
        $spec = $event->getSpec();
115
        if (empty($spec['cache'])) {
116
            return false;
117
        }
118
119
        $cached   = true;
120
        $nodes    = $event->getNodes();
121
        $newNodes = [];
122
123
        foreach ($nodes->toArray() as $_node) {
124
            $node     = $this->nodeToCache($spec, $_node);
125
            $cacheKey = $this->createCacheKey($node, $event);
126
            $xhtml    = $this->cache->getItem($cacheKey);
127
128
            if (empty($xhtml)) {
129
                $cached     = false;
130
                $newNodes[] = $_node;
131
132
                // TODO logger queued to cache
133
                //echo 'CANNOT LOAD: ' . print_r($spec['locator'], true);echo  '<br />';
134
                $this->nodes[$cacheKey] = $node;
135
136
                if ($node instanceof Text) {
137
                    $node->getParentNode()->setAttribute('__cacheKey', $cacheKey);
138
                    $onReplace = function ($newNode) use ($cacheKey) {
139
                        $newNode->parentNode->setAttribute('__cacheKey', $cacheKey);
140
                    };
141
142
                } elseif ($node instanceof Element) {
143
                    $node->setAttribute('__cacheKey', $cacheKey);
144
                    $onReplace = function (Element $newNode) use ($cacheKey) {
145
                        $newNode->setAttribute('__cacheKey', $cacheKey);
146
                    };
147
                }
148
149
                if (isset($onReplace)) {
150
                    $node->setOnReplace($this->createOnReplaceHandler());
151
                    $node->setOnReplace($onReplace);
152
                }
153
                continue;
154
            }
155
156
            // TODO logger loading cached
157
            //echo '<br />LOAD: ' . print_r($spec['cache'], true) . '<br />' . htmlspecialchars($xhtml) . '<br />';
158
            $frag = $node->ownerDocument->createDocumentFragment();
159
            $frag->appendXml($xhtml);
160
            $node->parentNode->replaceChild($frag, $node);
161
        }
162
163
        $nodes->setNodes($newNodes);
164
        return $cached;
165
    }
166
167
    /**
168
     * Select node to cache
169
     *
170
     * @param ArrayObject $spec
171
     * @param Element|Text $node
172
     * @return DomNode
173
     */
174
    private function nodeToCache(ArrayObject $spec, $node)
175
    {
176
        if (isset($spec['cache_node_xpath'])) {
177
            // TODO cache node not found exception
178
            return $node->getOwnerDocument()->getXpath()->query($spec['cache_node_xpath'], $node)->item(0);
179
        }
180
        return $node;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $node; (WebinoDraw\Dom\Element|WebinoDraw\Dom\Text) is incompatible with the return type documented by WebinoDraw\Cache\DrawCache::nodeToCache of type WebinoDraw\Cache\DomNode.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
181
    }
182
183
    /**
184
     * @param DOMNode $node
185
     * @param DrawEvent $event
186
     * @return string
187
     */
188
    protected function createCacheKey(DOMNode $node, DrawEvent $event)
189
    {
190
        $spec     = $event->getSpec();
191
        $cacheKey = $node->getNodePath();
192
193
        if (!empty($spec['cache_key'])) {
194
            // replace vars in the cache key settings
195
            $specCacheKey = $spec['cache_key'];
196
            $helper       = $event->getHelper();
197
198
            /** @var \WebinoDraw\VarTranslator\Translation $translation */
199
            $translation = clone $helper->getVarTranslator()->getTranslation();
200
            $translation->merge($helper->getVars());
201
            $translation->getVarTranslation()->translate($specCacheKey);
202
203
            $cacheKey .= join('', $specCacheKey);
204
        }
205
206
        empty($spec['cache_key_trigger'])
207
            or $cacheKey .= $this->cacheKeyTrigger((array) $spec['cache_key_trigger'], $event);
208
209
        return md5($cacheKey);
210
    }
211
212
    /**
213
     * @param array $triggers
214
     * @param DrawEvent $event
215
     * @return string
216
     */
217
    protected function cacheKeyTrigger(array $triggers, DrawEvent $event)
218
    {
219
        $spec     = $event->getSpec();
220
        $helper   = $event->getHelper();
221
        $cacheKey = '';
222
223
        foreach ($triggers as $eventName) {
224
            $results = $this->getEventManager()->trigger($eventName, $helper, ['spec' => $spec]);
225
            foreach ($results as $result) {
226
                $cacheKey .= $result;
227
            }
228
        }
229
230
        return $cacheKey;
231
    }
232
233
    /**
234
     * @return \Closure
235
     */
236
    private function createOnReplaceHandler()
237
    {
238
        return function ($newNode, $oldNode, $frag) {
239
            $self = ($oldNode instanceof Text) ? $oldNode->getParentNode() : $oldNode;
240
            if ($self->hasAttribute('__cacheKey')) {
241
                $cacheKey = $self->getAttributeNode('__cacheKey');
242
243
                if ($newNode instanceof Text) {
244
                    $newParentNode = $newNode->getParentNode();
245
                    $newParentNode->setAttributeNode($cacheKey);
246
                    $newParentNode->setAttribute('__cache', 'text');
247
248
                } elseif ($newNode instanceof Element) {
249
                    if ($frag->childNodes->length <= 1) {
250
                        // has wrapper
251
                        $newNode->setAttributeNode($cacheKey);
252
                    } else {
253
                        $newNode->parentNode->setAttributeNode($cacheKey);
254
                    }
255
                }
256
                $self->removeAttribute('__cacheKey');
257
            }
258
        };
259
    }
260
}
261