Completed
Push — develop ( c4d291...65fed0 )
by Peter
02:27
created

DrawCache::nodeToCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
c 2
b 1
f 1
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 2
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-2016 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
        foreach ($event->getNodes()->toArray() as $_node) {
121
            $node     = $this->nodeToCache($spec, $_node);
122
            $cacheKey = $this->createCacheKey($node, $event);
123
            $xhtml    = $this->cache->getItem($cacheKey);
124
125
            if (empty($xhtml)) {
126
                $cached = false;
127
                // TODO logger queued to cache
128
                //echo 'CANNOT LOAD: ' . print_r($spec['locator'], true);echo  '<br />';
129
                $this->nodes[$cacheKey] = $node;
130
131
                if ($node instanceof Text) {
132
                    $node->getParentNode()->setAttribute('__cacheKey', $cacheKey);
133
                    $onReplace = function ($newNode) use ($cacheKey) {
134
                        $newNode->parentNode->setAttribute('__cacheKey', $cacheKey);
135
                    };
136
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
137
                } elseif ($node instanceof Element) {
138
                    $node->setAttribute('__cacheKey', $cacheKey);
139
                    $onReplace = function (Element $newNode) use ($cacheKey) {
140
                        $newNode->setAttribute('__cacheKey', $cacheKey);
141
                    };
142
                }
143
144
                if (isset($onReplace)) {
145
                    $node->setOnReplace($this->createOnReplaceHandler());
146
                    $node->setOnReplace($onReplace);
147
                }
148
                continue;
149
            }
150
151
            // TODO logger loading cached
152
            //echo '<br />LOAD: ' . print_r($spec['cache'], true) . '<br />' . htmlspecialchars($xhtml) . '<br />';
153
            $frag = $node->ownerDocument->createDocumentFragment();
154
            $frag->appendXml($xhtml);
155
            $node->parentNode->replaceChild($frag, $node);
156
        }
157
158
        return $cached;
159
    }
160
161
    /**
162
     * Select node to cache
163
     *
164
     * @param ArrayObject $spec
165
     * @param Element|Text $node
166
     * @return DomNode
167
     */
168
    private function nodeToCache(ArrayObject $spec, $node)
169
    {
170
        if (isset($spec['cache_node_xpath'])) {
171
            // TODO cache node not found exception
172
            return $node->getOwnerDocument()->getXpath()->query($spec['cache_node_xpath'], $node)->item(0);
173
        }
174
        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...
175
    }
176
177
    /**
178
     * @param DOMNode $node
179
     * @param DrawEvent $event
180
     * @return string
181
     */
182
    protected function createCacheKey(DOMNode $node, DrawEvent $event)
183
    {
184
        $spec     = $event->getSpec();
185
        $cacheKey = $node->getNodePath();
186
187
        if (!empty($spec['cache_key'])) {
188
            // replace vars in the cache key settings
189
            $specCacheKey = $spec['cache_key'];
190
            $helper       = $event->getHelper();
191
192
            /** @var \WebinoDraw\VarTranslator\Translation $translation */
193
            $translation = clone $helper->getVarTranslator()->getTranslation();
194
            $translation->merge($helper->getVars());
195
            $translation->getVarTranslation()->translate($specCacheKey);
196
197
            $cacheKey .= join('', $specCacheKey);
198
        }
199
200
        empty($spec['cache_key_trigger'])
201
            or $cacheKey .= $this->cacheKeyTrigger((array) $spec['cache_key_trigger'], $event);
202
203
        return md5($cacheKey);
204
    }
205
206
    /**
207
     * @param array $triggers
208
     * @param DrawEvent $event
209
     * @return string
210
     */
211
    protected function cacheKeyTrigger(array $triggers, DrawEvent $event)
212
    {
213
        $spec     = $event->getSpec();
214
        $helper   = $event->getHelper();
215
        $cacheKey = '';
216
217
        foreach ($triggers as $eventName) {
218
            $results = $this->getEventManager()->trigger($eventName, $helper, ['spec' => $spec]);
219
            foreach ($results as $result) {
220
                $cacheKey .= $result;
221
            }
222
        }
223
224
        return $cacheKey;
225
    }
226
227
    /**
228
     * @return \Closure
229
     */
230
    private function createOnReplaceHandler()
231
    {
232
        return function ($newNode, $oldNode, $frag) {
233
            $self = ($oldNode instanceof Text) ? $oldNode->getParentNode() : $oldNode;
234
            if ($self->hasAttribute('__cacheKey')) {
235
                $cacheKey = $self->getAttributeNode('__cacheKey');
236
237
                if ($newNode instanceof Text) {
238
                    $newParentNode = $newNode->getParentNode();
239
                    $newParentNode->setAttributeNode($cacheKey);
240
                    $newParentNode->setAttribute('__cache', 'text');
241
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
242
                } elseif ($newNode instanceof Element) {
243
                    if ($frag->childNodes->length <= 1) {
244
                        // has wrapper
245
                        $newNode->setAttributeNode($cacheKey);
246
                    } else {
247
                        $newNode->parentNode->setAttributeNode($cacheKey);
248
                    }
249
                }
250
                $self->removeAttribute('__cacheKey');
251
            }
252
        };
253
    }
254
}
255