Completed
Push — master ( ea729d...2dd925 )
by David
03:38 queued 47s
created

CacheControlListener   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 226
Duplicated Lines 11.5 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 43
c 0
b 0
f 0
lcom 1
cbo 4
dl 26
loc 226
ccs 76
cts 76
cp 1
rs 8.96

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getSubscribedEvents() 0 6 1
A setSkip() 0 4 1
D onKernelResponse() 0 53 18
A addRule() 0 6 1
A matchRule() 10 10 3
B setCache() 0 29 9
B setExtraCacheDirectives() 16 23 9

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
/*
4
 * This file is part of the FOSHttpCacheBundle package.
5
 *
6
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace FOS\HttpCacheBundle\EventListener;
13
14
use FOS\HttpCacheBundle\Http\RuleMatcherInterface;
15
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\HttpFoundation\Response;
18
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
19
use Symfony\Component\HttpKernel\KernelEvents;
20
21
/**
22
 * Set caching settings on matching response according to the configurations.
23
 *
24
 * The first matching ruleset is applied.
25
 *
26
 * @author Lea Haensenberger <[email protected]>
27
 * @author David Buchmann <[email protected]>
28
 */
29
class CacheControlListener implements EventSubscriberInterface
30
{
31
    /**
32
     * Whether to skip this response and not set any cache headers.
33
     *
34
     * @var bool
35
     */
36
    private $skip = false;
37
38
    /**
39
     * Cache control directives directly supported by Response.
40
     *
41
     * @var array
42
     */
43
    private $supportedDirectives = [
44
        'max_age' => true,
45
        's_maxage' => true,
46
        'private' => true,
47
        'public' => true,
48
    ];
49
50
    /**
51
     * @var array List of arrays with RuleMatcherInterface, settings array
52
     */
53
    private $rulesMap = [];
54
55
    /**
56
     * If not empty, add a debug header with that name to all responses,
57
     * telling the cache proxy to add debug output.
58
     *
59
     * @var string|bool Name of the header or false to add no header
60
     */
61
    private $debugHeader;
62
63
    /**
64
     * @param string|bool $debugHeader Header to set to trigger debugging, or false to send no header
65
     */
66 36
    public function __construct($debugHeader = false)
67
    {
68 36
        $this->debugHeader = $debugHeader;
69 36
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 2
    public static function getSubscribedEvents()
75
    {
76
        return [
77 2
            KernelEvents::RESPONSE => ['onKernelResponse', 10],
78
        ];
79
    }
80
81
    /**
82
     * Set whether to skip this response completely.
83
     *
84
     * This can be called when other parts of the application took care of all
85
     * cache headers. No attempt to merge cache headers is made anymore.
86
     *
87
     * The debug header is still added if configured.
88
     *
89
     * @param bool $skip
90
     */
91 1
    public function setSkip($skip = true)
92
    {
93 1
        $this->skip = $skip;
94 1
    }
95
96
    /**
97
     * Apply the header rules if the request matches.
98
     *
99
     * @param FilterResponseEvent $event
100
     */
101 35
    public function onKernelResponse(FilterResponseEvent $event)
102
    {
103 35
        $request = $event->getRequest();
104 35
        $response = $event->getResponse();
105
106 35
        if ($this->debugHeader) {
107 19
            $response->headers->set($this->debugHeader, 1, false);
108
        }
109
110
        // do not change cache directives on non-cacheable requests.
111 35
        if ($this->skip || !$request->isMethodCacheable()) {
112 10
            return;
113
        }
114
115 25
        $options = $this->matchRule($request, $response);
116
117 25
        if (false === $options) {
118 10
            return;
119
        }
120
121 16
        if (!empty($options['cache_control'])) {
122 12
            $directives = array_intersect_key($options['cache_control'], $this->supportedDirectives);
123 12
            $extraDirectives = array_diff_key($options['cache_control'], $directives);
124 12
            if (!empty($directives)) {
125 9
                $this->setCache($response, $directives, $options['overwrite']);
126
            }
127 12
            if (!empty($extraDirectives)) {
128 7
                $this->setExtraCacheDirectives($response, $extraDirectives, $options['overwrite']);
129
            }
130
        }
131
132 16
        if (isset($options['reverse_proxy_ttl'])
133 16
            && null !== $options['reverse_proxy_ttl']
134 16
            && !$response->headers->has('X-Reverse-Proxy-TTL')
135
        ) {
136 1
            $response->headers->set('X-Reverse-Proxy-TTL', (int) $options['reverse_proxy_ttl'], false);
137
        }
138
139 16
        if (!empty($options['vary'])) {
140 3
            $response->setVary($options['vary'], $options['overwrite']);
141
        }
142
143 16
        if (!empty($options['etag'])
144 16
            && ($options['overwrite'] || null === $response->getEtag())
145
        ) {
146 3
            $response->setEtag(md5($response->getContent()), 'weak' === $options['etag']);
147
        }
148 16
        if (isset($options['last_modified'])
149 16
            && ($options['overwrite'] || null === $response->getLastModified())
150
        ) {
151 3
            $response->setLastModified(new \DateTime($options['last_modified']));
152
        }
153 16
    }
154
155
    /**
156
     * Add a rule matcher with a list of header directives to apply if the
157
     * request and response are matched.
158
     *
159
     * @param RuleMatcherInterface $ruleMatcher The headers apply to request and response matched by this matcher
160
     * @param array                $settings    An array of header configuration
161
     */
162 35
    public function addRule(
163
        RuleMatcherInterface $ruleMatcher,
164
        array $settings = []
165
    ) {
166 35
        $this->rulesMap[] = [$ruleMatcher, $settings];
167 35
    }
168
169
    /**
170
     * Return the settings for the current request if any rule matches.
171
     *
172
     * @param Request $request
173
     *
174
     * @return array|false Settings to apply or false if no rule matched
175
     */
176 25 View Code Duplication
    private function matchRule(Request $request, Response $response)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
177
    {
178 25
        foreach ($this->rulesMap as $elements) {
179 25
            if ($elements[0]->matches($request, $response)) {
180 25
                return $elements[1];
181
            }
182
        }
183
184 10
        return false;
185
    }
186
187
    /**
188
     * Set cache headers on response.
189
     *
190
     * @param Response $response
191
     * @param array    $directives
192
     * @param bool     $overwrite  Whether to keep existing cache headers or to overwrite them
193
     */
194 9
    private function setCache(Response $response, array $directives, $overwrite)
195
    {
196 9
        if ($overwrite) {
197 1
            $response->setCache($directives);
198
199 1
            return;
200
        }
201
202 8
        if (false !== strpos($response->headers->get('Cache-Control'), 'no-cache')) {
203
            // this single header is set by default. if its the only thing, we override it.
204 6
            $response->setCache($directives);
205
206 6
            return;
207
        }
208
209 2
        foreach (array_keys($this->supportedDirectives) as $key) {
210 2
            $directive = str_replace('_', '-', $key);
211 2
            if ($response->headers->hasCacheControlDirective($directive)) {
212 2
                $directives[$key] = $response->headers->getCacheControlDirective($directive);
213
            }
214 2
            if ('public' === $directive && $response->headers->hasCacheControlDirective('private')
215 2
                || 'private' === $directive && $response->headers->hasCacheControlDirective('public')
216
            ) {
217 2
                unset($directives[$key]);
218
            }
219
        }
220
221 2
        $response->setCache($directives);
222 2
    }
223
224
    /**
225
     * Add extra cache control directives on response.
226
     *
227
     * @param Response $response
228
     * @param array    $controls
229
     * @param bool     $overwrite Whether to keep existing cache headers or to overwrite them
230
     */
231 7
    private function setExtraCacheDirectives(Response $response, array $controls, $overwrite)
232
    {
233 7
        $flags = ['must_revalidate', 'proxy_revalidate', 'no_transform', 'no_cache', 'no_store'];
234 7
        $options = ['stale_if_error', 'stale_while_revalidate'];
235
236 7 View Code Duplication
        foreach ($flags as $key) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
237 7
            $flag = str_replace('_', '-', $key);
238 7
            if (!empty($controls[$key])
239 7
                && ($overwrite || !$response->headers->hasCacheControlDirective($flag))
240
            ) {
241 7
                $response->headers->addCacheControlDirective($flag);
242
            }
243
        }
244
245 7 View Code Duplication
        foreach ($options as $key) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246 7
            $option = str_replace('_', '-', $key);
247 7
            if (isset($controls[$key])
248 7
                && ($overwrite || !$response->headers->hasCacheControlDirective($option))
249
            ) {
250 7
                $response->headers->addCacheControlDirective($option, $controls[$key]);
251
            }
252
        }
253 7
    }
254
}
255