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:
1 | <?php |
||
37 | class Configuration implements ConfigurationInterface |
||
38 | { |
||
39 | /** |
||
40 | * @var bool |
||
41 | */ |
||
42 | private $debug; |
||
43 | |||
44 | /** |
||
45 | * @param bool $debug Whether to use the debug mode |
||
46 | */ |
||
47 | 41 | public function __construct($debug) |
|
51 | |||
52 | /** |
||
53 | * {@inheritdoc} |
||
54 | */ |
||
55 | 41 | public function getConfigTreeBuilder() |
|
56 | { |
||
57 | 41 | $treeBuilder = new TreeBuilder(); |
|
58 | 41 | $rootNode = $treeBuilder->root('fos_http_cache'); |
|
59 | |||
60 | $rootNode |
||
61 | 41 | ->validate() |
|
62 | ->ifTrue(function ($v) { |
||
63 | 40 | return $v['cache_manager']['enabled'] |
|
64 | 40 | && !isset($v['proxy_client']) |
|
65 | 40 | && !isset($v['cache_manager']['custom_proxy_client']) |
|
66 | ; |
||
67 | 41 | }) |
|
68 | View Code Duplication | ->then(function ($v) { |
|
|
|||
69 | 13 | if ('auto' === $v['cache_manager']['enabled']) { |
|
70 | 12 | $v['cache_manager']['enabled'] = false; |
|
71 | |||
72 | 12 | return $v; |
|
73 | } |
||
74 | |||
75 | 1 | throw new InvalidConfigurationException('You need to configure a proxy_client or specify a custom_proxy_client to use the cache_manager.'); |
|
76 | 41 | }) |
|
77 | 41 | ->end() |
|
78 | 41 | ->validate() |
|
79 | ->ifTrue(function ($v) { |
||
80 | 39 | return $v['tags']['enabled'] && !$v['cache_manager']['enabled']; |
|
81 | 41 | }) |
|
82 | View Code Duplication | ->then(function ($v) { |
|
83 | 14 | if ('auto' === $v['tags']['enabled']) { |
|
84 | 13 | $v['tags']['enabled'] = false; |
|
85 | |||
86 | 13 | return $v; |
|
87 | } |
||
88 | |||
89 | 1 | throw new InvalidConfigurationException('You need to configure a proxy_client to get the cache_manager needed for tag handling.'); |
|
90 | 41 | }) |
|
91 | 41 | ->end() |
|
92 | 41 | ->validate() |
|
93 | ->ifTrue(function ($v) { |
||
94 | 38 | return $v['invalidation']['enabled'] && !$v['cache_manager']['enabled']; |
|
95 | 41 | }) |
|
96 | View Code Duplication | ->then(function ($v) { |
|
97 | 13 | if ('auto' === $v['invalidation']['enabled']) { |
|
98 | 12 | $v['invalidation']['enabled'] = false; |
|
99 | |||
100 | 12 | return $v; |
|
101 | } |
||
102 | |||
103 | 1 | throw new InvalidConfigurationException('You need to configure a proxy_client to get the cache_manager needed for invalidation handling.'); |
|
104 | 41 | }) |
|
105 | 41 | ->end() |
|
106 | 41 | ->validate() |
|
107 | 41 | ->ifTrue( |
|
108 | function ($v) { |
||
109 | 37 | return $v['user_context']['logout_handler']['enabled'] |
|
110 | 37 | && !isset($v['proxy_client']); |
|
111 | 41 | } |
|
112 | ) |
||
113 | View Code Duplication | ->then(function ($v) { |
|
114 | 14 | if ('auto' === $v['user_context']['logout_handler']['enabled']) { |
|
115 | 14 | $v['user_context']['logout_handler']['enabled'] = false; |
|
116 | |||
117 | 14 | return $v; |
|
118 | } |
||
119 | |||
120 | throw new InvalidConfigurationException('You need to configure a proxy_client for the logout_handler.'); |
||
121 | 41 | }) |
|
122 | ; |
||
123 | |||
124 | 41 | $this->addCacheableResponseSection($rootNode); |
|
125 | 41 | $this->addCacheControlSection($rootNode); |
|
126 | 41 | $this->addProxyClientSection($rootNode); |
|
127 | 41 | $this->addCacheManagerSection($rootNode); |
|
128 | 41 | $this->addTagSection($rootNode); |
|
129 | 41 | $this->addInvalidationSection($rootNode); |
|
130 | 41 | $this->addUserContextListenerSection($rootNode); |
|
131 | 41 | $this->addFlashMessageSection($rootNode); |
|
132 | 41 | $this->addTestSection($rootNode); |
|
133 | 41 | $this->addDebugSection($rootNode); |
|
134 | |||
135 | 41 | return $treeBuilder; |
|
136 | } |
||
137 | |||
138 | 41 | private function addCacheableResponseSection(ArrayNodeDefinition $rootNode) |
|
139 | { |
||
140 | $rootNode |
||
141 | 41 | ->children() |
|
142 | 41 | ->arrayNode('cacheable') |
|
143 | 41 | ->addDefaultsIfNotSet() |
|
144 | 41 | ->children() |
|
145 | 41 | ->arrayNode('response') |
|
146 | 41 | ->addDefaultsIfNotSet() |
|
147 | 41 | ->children() |
|
148 | 41 | ->arrayNode('additional_status') |
|
149 | 41 | ->prototype('scalar')->end() |
|
150 | 41 | ->info('Additional response HTTP status codes that will be considered cacheable.') |
|
151 | 41 | ->end() |
|
152 | 41 | ->scalarNode('expression') |
|
153 | 41 | ->defaultNull() |
|
154 | 41 | ->info('Expression to decide whether response is cacheable. Replaces the default status codes.') |
|
155 | 41 | ->end() |
|
156 | 41 | ->end() |
|
157 | |||
158 | 41 | ->validate() |
|
159 | ->ifTrue(function ($v) { |
||
160 | 4 | return !empty($v['additional_status']) && !empty($v['expression']); |
|
161 | 41 | }) |
|
162 | 41 | ->thenInvalid('You may not set both additional_status and expression.') |
|
163 | 41 | ->end() |
|
164 | 41 | ->end() |
|
165 | 41 | ->end() |
|
166 | 41 | ->end(); |
|
167 | 41 | } |
|
168 | |||
169 | /** |
||
170 | * Cache header control main section. |
||
171 | * |
||
172 | * @param ArrayNodeDefinition $rootNode |
||
173 | */ |
||
174 | 41 | private function addCacheControlSection(ArrayNodeDefinition $rootNode) |
|
175 | { |
||
176 | $rules = $rootNode |
||
177 | 41 | ->children() |
|
178 | 41 | ->arrayNode('cache_control') |
|
179 | 41 | ->fixXmlConfig('rule') |
|
180 | 41 | ->children() |
|
181 | 41 | ->arrayNode('defaults') |
|
182 | 41 | ->addDefaultsIfNotSet() |
|
183 | 41 | ->children() |
|
184 | 41 | ->booleanNode('overwrite') |
|
185 | 41 | ->info('Whether to overwrite existing cache headers') |
|
186 | 41 | ->defaultFalse() |
|
187 | 41 | ->end() |
|
188 | 41 | ->end() |
|
189 | 41 | ->end() |
|
190 | 41 | ->arrayNode('rules') |
|
191 | 41 | ->prototype('array') |
|
192 | 41 | ->children(); |
|
193 | |||
194 | 41 | $this->addMatch($rules, true); |
|
195 | $rules |
||
196 | 41 | ->arrayNode('headers') |
|
197 | 41 | ->isRequired() |
|
198 | // todo validate there is some header defined |
||
199 | 41 | ->children() |
|
200 | 41 | ->enumNode('overwrite') |
|
201 | 41 | ->info('Whether to overwrite cache headers for this rule, defaults to the cache_control.defaults.overwrite setting') |
|
202 | 41 | ->values(['default', true, false]) |
|
203 | 41 | ->defaultValue('default') |
|
204 | 41 | ->end() |
|
205 | 41 | ->arrayNode('cache_control') |
|
206 | 41 | ->info('Add the specified cache control directives.') |
|
207 | 41 | ->children() |
|
208 | 41 | ->scalarNode('max_age')->end() |
|
209 | 41 | ->scalarNode('s_maxage')->end() |
|
210 | 41 | ->booleanNode('private')->end() |
|
211 | 41 | ->booleanNode('public')->end() |
|
212 | 41 | ->booleanNode('must_revalidate')->end() |
|
213 | 41 | ->booleanNode('proxy_revalidate')->end() |
|
214 | 41 | ->booleanNode('no_transform')->end() |
|
215 | 41 | ->booleanNode('no_cache')->end() |
|
216 | 41 | ->booleanNode('no_store')->end() |
|
217 | 41 | ->scalarNode('stale_if_error')->end() |
|
218 | 41 | ->scalarNode('stale_while_revalidate')->end() |
|
219 | 41 | ->end() |
|
220 | 41 | ->end() |
|
221 | 41 | ->enumNode('etag') |
|
222 | 41 | ->defaultValue(false) |
|
223 | 41 | ->treatTrueLike('strong') |
|
224 | 41 | ->info('Set a simple ETag which is just the md5 hash of the response body. '. |
|
225 | 41 | 'You can specify which type of ETag you want by passing "strong" or "weak".') |
|
226 | 41 | ->values(['weak', 'strong', false]) |
|
227 | 41 | ->end() |
|
228 | 41 | ->scalarNode('last_modified') |
|
229 | 41 | ->validate() |
|
230 | ->ifTrue(function ($v) { |
||
231 | 2 | if (is_string($v)) { |
|
232 | 2 | new \DateTime($v); |
|
233 | } |
||
234 | |||
235 | 1 | return false; |
|
236 | 41 | }) |
|
237 | 41 | ->thenInvalid('') // this will never happen as new DateTime will throw an exception if $v is no date |
|
238 | 41 | ->end() |
|
239 | 41 | ->info('Set a default last modified timestamp if none is set yet. Value must be parseable by DateTime') |
|
240 | 41 | ->end() |
|
241 | 41 | ->scalarNode('reverse_proxy_ttl') |
|
242 | 41 | ->defaultNull() |
|
243 | 41 | ->info('Specify an X-Reverse-Proxy-TTL header with a time in seconds for a caching proxy under your control.') |
|
244 | 41 | ->end() |
|
245 | 41 | ->arrayNode('vary') |
|
246 | ->beforeNormalization()->ifString()->then(function ($v) { |
||
247 | 2 | return preg_split('/\s*,\s*/', $v); |
|
248 | 41 | })->end() |
|
249 | 41 | ->prototype('scalar')->end() |
|
250 | 41 | ->info('Define a list of additional headers on which the response varies.') |
|
251 | 41 | ->end() |
|
252 | 41 | ->end() |
|
253 | 41 | ->end() |
|
254 | ; |
||
255 | 41 | } |
|
256 | |||
257 | /** |
||
258 | * Shared configuration between cache control, tags and invalidation. |
||
259 | * |
||
260 | * @param NodeBuilder $rules |
||
261 | * @param bool $matchResponse whether to also add fields to match response |
||
262 | */ |
||
263 | 41 | private function addMatch(NodeBuilder $rules, $matchResponse = false) |
|
264 | { |
||
265 | $match = $rules |
||
266 | 41 | ->arrayNode('match') |
|
267 | 41 | ->cannotBeOverwritten() |
|
268 | 41 | ->isRequired() |
|
269 | 41 | ->fixXmlConfig('method') |
|
270 | 41 | ->fixXmlConfig('ip') |
|
271 | 41 | ->fixXmlConfig('attribute') |
|
272 | 41 | ->validate() |
|
273 | ->ifTrue(function ($v) { |
||
274 | 14 | return !empty($v['additional_response_status']) && !empty($v['match_response']); |
|
275 | 41 | }) |
|
276 | 41 | ->thenInvalid('You may not set both additional_response_status and match_response.') |
|
277 | 41 | ->end() |
|
278 | 41 | ->children() |
|
279 | 41 | ->scalarNode('path') |
|
280 | 41 | ->defaultNull() |
|
281 | 41 | ->info('Request path.') |
|
282 | 41 | ->end() |
|
283 | 41 | ->scalarNode('query_string') |
|
284 | 41 | ->defaultNull() |
|
285 | 41 | ->info('Request query string.') |
|
286 | 41 | ->end() |
|
287 | 41 | ->scalarNode('host') |
|
288 | 41 | ->defaultNull() |
|
289 | 41 | ->info('Request host name.') |
|
290 | 41 | ->end() |
|
291 | 41 | ->arrayNode('methods') |
|
292 | ->beforeNormalization()->ifString()->then(function ($v) { |
||
293 | 3 | return preg_split('/\s*,\s*/', $v); |
|
294 | 41 | })->end() |
|
295 | 41 | ->useAttributeAsKey('name') |
|
296 | 41 | ->prototype('scalar')->end() |
|
297 | 41 | ->info('Request HTTP methods.') |
|
298 | 41 | ->end() |
|
299 | 41 | ->arrayNode('ips') |
|
300 | ->beforeNormalization()->ifString()->then(function ($v) { |
||
301 | 3 | return preg_split('/\s*,\s*/', $v); |
|
302 | 41 | })->end() |
|
303 | 41 | ->useAttributeAsKey('name') |
|
304 | 41 | ->prototype('scalar')->end() |
|
305 | 41 | ->info('List of client IPs.') |
|
306 | 41 | ->end() |
|
307 | 41 | ->arrayNode('attributes') |
|
308 | 41 | ->useAttributeAsKey('name') |
|
309 | 41 | ->prototype('scalar')->end() |
|
310 | 41 | ->info('Regular expressions on request attributes.') |
|
311 | 41 | ->end() |
|
312 | ; |
||
313 | 41 | if ($matchResponse) { |
|
314 | $match |
||
315 | 41 | ->arrayNode('additional_response_status') |
|
316 | 41 | ->prototype('scalar')->end() |
|
317 | 41 | ->info('Additional response HTTP status codes that will match. Replaces cacheable configuration.') |
|
318 | 41 | ->end() |
|
319 | 41 | ->scalarNode('match_response') |
|
320 | 41 | ->defaultNull() |
|
321 | 41 | ->info('Expression to decide whether response should be matched. Replaces cacheable configuration.') |
|
322 | 41 | ->end() |
|
323 | ; |
||
324 | } |
||
325 | 41 | } |
|
326 | |||
327 | 41 | private function addProxyClientSection(ArrayNodeDefinition $rootNode) |
|
328 | { |
||
329 | $rootNode |
||
330 | 41 | ->children() |
|
331 | 41 | ->arrayNode('proxy_client') |
|
332 | 41 | ->children() |
|
333 | 41 | ->enumNode('default') |
|
334 | 41 | ->values(['varnish', 'nginx', 'symfony', 'noop']) |
|
335 | 41 | ->info('If you configure more than one proxy client, you need to specify which client is the default.') |
|
336 | 41 | ->end() |
|
337 | 41 | ->arrayNode('varnish') |
|
338 | 41 | ->fixXmlConfig('default_ban_header') |
|
339 | 41 | ->validate() |
|
340 | ->always(function ($v) { |
||
341 | 16 | if (!count($v['default_ban_headers'])) { |
|
342 | 15 | unset($v['default_ban_headers']); |
|
343 | } |
||
344 | |||
345 | 16 | return $v; |
|
346 | 41 | }) |
|
347 | 41 | ->end() |
|
348 | 41 | ->children() |
|
349 | 41 | ->scalarNode('tags_header') |
|
350 | 41 | ->defaultValue(Varnish::DEFAULT_HTTP_HEADER_CACHE_TAGS) |
|
351 | 41 | ->info('HTTP header to use when sending tag invalidation requests to Varnish') |
|
352 | 41 | ->end() |
|
353 | 41 | ->scalarNode('header_length') |
|
354 | 41 | ->info('Maximum header length when invalidating tags. If there are more tags to invalidate than fit into the header, the invalidation request is split into several requests.') |
|
355 | 41 | ->end() |
|
356 | 41 | ->arrayNode('default_ban_headers') |
|
357 | 41 | ->useAttributeAsKey('name') |
|
358 | 41 | ->info('Map of additional headers to include in each ban request.') |
|
359 | 41 | ->prototype('scalar')->end() |
|
360 | 41 | ->end() |
|
361 | 41 | ->append($this->getHttpDispatcherNode()) |
|
362 | 41 | ->end() |
|
363 | 41 | ->end() |
|
364 | |||
365 | 41 | ->arrayNode('nginx') |
|
366 | 41 | ->children() |
|
367 | 41 | ->scalarNode('purge_location') |
|
368 | 41 | ->defaultValue(false) |
|
369 | 41 | ->info('Path to trigger the purge on Nginx for different location purge.') |
|
370 | 41 | ->end() |
|
371 | 41 | ->append($this->getHttpDispatcherNode()) |
|
372 | 41 | ->end() |
|
373 | 41 | ->end() |
|
374 | |||
375 | 41 | ->arrayNode('symfony') |
|
376 | 41 | ->children() |
|
377 | 41 | ->scalarNode('tags_header') |
|
378 | 41 | ->defaultValue(PurgeTagsListener::DEFAULT_TAGS_HEADER) |
|
379 | 41 | ->info('HTTP header to use when sending tag invalidation requests to Symfony HttpCache') |
|
380 | 41 | ->end() |
|
381 | 41 | ->scalarNode('tags_method') |
|
382 | 41 | ->defaultValue(PurgeTagsListener::DEFAULT_TAGS_METHOD) |
|
383 | 41 | ->info('HTTP method for sending tag invalidation requests to Symfony HttpCache') |
|
384 | 41 | ->end() |
|
385 | 41 | ->scalarNode('header_length') |
|
386 | 41 | ->info('Maximum header length when invalidating tags. If there are more tags to invalidate than fit into the header, the invalidation request is split into several requests.') |
|
387 | 41 | ->end() |
|
388 | 41 | ->scalarNode('purge_method') |
|
389 | 41 | ->defaultValue(PurgeListener::DEFAULT_PURGE_METHOD) |
|
390 | 41 | ->info('HTTP method to use when sending purge requests to Symfony HttpCache') |
|
391 | 41 | ->end() |
|
392 | 41 | ->append($this->getHttpDispatcherNode()) |
|
393 | 41 | ->end() |
|
394 | 41 | ->end() |
|
395 | |||
396 | 41 | ->booleanNode('noop')->end() |
|
397 | |||
398 | 41 | ->end() |
|
399 | 41 | ->end() |
|
400 | 41 | ->end(); |
|
401 | 41 | } |
|
402 | |||
403 | /** |
||
404 | * Get the configuration node for a HTTP dispatcher in a proxy client. |
||
405 | * |
||
406 | * @return NodeDefinition |
||
407 | */ |
||
408 | 41 | private function getHttpDispatcherNode() |
|
437 | |||
438 | 41 | private function addTestSection(ArrayNodeDefinition $rootNode) |
|
477 | |||
478 | /** |
||
479 | * Cache manager main section. |
||
480 | * |
||
481 | * @param ArrayNodeDefinition $rootNode |
||
482 | */ |
||
483 | 41 | private function addCacheManagerSection(ArrayNodeDefinition $rootNode) |
|
484 | { |
||
485 | $rootNode |
||
486 | 41 | ->children() |
|
487 | 41 | ->arrayNode('cache_manager') |
|
488 | 41 | ->addDefaultsIfNotSet() |
|
489 | 41 | ->beforeNormalization() |
|
490 | 41 | ->ifArray() |
|
491 | ->then(function ($v) { |
||
492 | 6 | $v['enabled'] = isset($v['enabled']) ? $v['enabled'] : true; |
|
493 | |||
494 | 6 | return $v; |
|
495 | 41 | }) |
|
496 | 41 | ->end() |
|
497 | 41 | ->info('Configure the cache manager. Needs a proxy_client to be configured.') |
|
498 | 41 | ->children() |
|
499 | 41 | ->enumNode('enabled') |
|
500 | 41 | ->values([true, false, 'auto']) |
|
501 | 41 | ->defaultValue('auto') |
|
502 | 41 | ->info('Allows to disable the invalidation manager. Enabled by default if you configure a proxy client.') |
|
503 | 41 | ->end() |
|
504 | 41 | ->scalarNode('custom_proxy_client') |
|
505 | 41 | ->info('Service name of a custom proxy client to use. With a custom client, generate_url_type defaults to ABSOLUTE_URL and tag support needs to be explicitly enabled. If no custom proxy client is specified, the first proxy client you configured is used.') |
|
506 | 41 | ->cannotBeEmpty() |
|
507 | 41 | ->end() |
|
508 | 41 | ->enumNode('generate_url_type') |
|
509 | 41 | ->values([ |
|
510 | 41 | 'auto', |
|
511 | UrlGeneratorInterface::ABSOLUTE_PATH, |
||
512 | UrlGeneratorInterface::ABSOLUTE_URL, |
||
513 | UrlGeneratorInterface::NETWORK_PATH, |
||
514 | UrlGeneratorInterface::RELATIVE_PATH, |
||
515 | ]) |
||
516 | 41 | ->defaultValue('auto') |
|
517 | 41 | ->info('Set what URLs to generate on invalidate/refresh Route. Auto means path if base_url is set on the default proxy client, full URL otherwise.') |
|
518 | 41 | ->end() |
|
519 | 41 | ->end() |
|
520 | ; |
||
521 | 41 | } |
|
522 | |||
523 | 41 | private function addTagSection(ArrayNodeDefinition $rootNode) |
|
570 | |||
571 | 41 | private function addInvalidationSection(ArrayNodeDefinition $rootNode) |
|
608 | |||
609 | /** |
||
610 | * User context main section. |
||
611 | * |
||
612 | * @param ArrayNodeDefinition $rootNode |
||
613 | */ |
||
614 | 41 | private function addUserContextListenerSection(ArrayNodeDefinition $rootNode) |
|
678 | |||
679 | 41 | private function addFlashMessageSection(ArrayNodeDefinition $rootNode) |
|
708 | |||
709 | 41 | private function addDebugSection(ArrayNodeDefinition $rootNode) |
|
729 | } |
||
730 |
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.