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 classes like Configuration 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 Configuration, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
36 | class Configuration implements ConfigurationInterface |
||
37 | { |
||
38 | /** |
||
39 | * @var bool |
||
40 | */ |
||
41 | private $debug; |
||
42 | |||
43 | /** |
||
44 | * @param bool $debug Whether to use the debug mode |
||
45 | */ |
||
46 | 58 | public function __construct($debug) |
|
50 | |||
51 | /** |
||
52 | * {@inheritdoc} |
||
53 | */ |
||
54 | 58 | public function getConfigTreeBuilder() |
|
55 | { |
||
56 | 58 | View Code Duplication | if (method_exists(TreeBuilder::class, 'getRootNode')) { |
|
|||
57 | $treeBuilder = new TreeBuilder('fos_http_cache'); |
||
58 | $rootNode = $treeBuilder->getRootNode(); |
||
59 | } else { |
||
60 | 58 | $treeBuilder = new TreeBuilder(); |
|
61 | 58 | $rootNode = $treeBuilder->root('fos_http_cache'); |
|
62 | } |
||
63 | |||
64 | $rootNode |
||
65 | 58 | ->validate() |
|
66 | 58 | ->ifTrue(function ($v) { |
|
67 | 56 | return $v['cache_manager']['enabled'] |
|
68 | 56 | && !isset($v['proxy_client']) |
|
69 | 56 | && !isset($v['cache_manager']['custom_proxy_client']) |
|
70 | ; |
||
71 | 58 | }) |
|
72 | 58 | View Code Duplication | ->then(function ($v) { |
73 | 17 | if ('auto' === $v['cache_manager']['enabled']) { |
|
74 | 16 | $v['cache_manager']['enabled'] = false; |
|
75 | |||
76 | 16 | return $v; |
|
77 | } |
||
78 | |||
79 | 1 | throw new InvalidConfigurationException('You need to configure a proxy_client or specify a custom_proxy_client to use the cache_manager.'); |
|
80 | 58 | }) |
|
81 | 58 | ->end() |
|
82 | 58 | ->validate() |
|
83 | 58 | ->ifTrue(function ($v) { |
|
84 | 55 | return $v['tags']['enabled'] && !$v['cache_manager']['enabled']; |
|
85 | 58 | }) |
|
86 | 58 | View Code Duplication | ->then(function ($v) { |
87 | 18 | if ('auto' === $v['tags']['enabled']) { |
|
88 | 17 | $v['tags']['enabled'] = false; |
|
89 | |||
90 | 17 | return $v; |
|
91 | } |
||
92 | |||
93 | 1 | throw new InvalidConfigurationException('You need to configure a proxy_client to get the cache_manager needed for tag handling.'); |
|
94 | 58 | }) |
|
95 | 58 | ->end() |
|
96 | 58 | ->validate() |
|
97 | 58 | ->ifTrue(function ($v) { |
|
98 | 54 | return $v['invalidation']['enabled'] && !$v['cache_manager']['enabled']; |
|
99 | 58 | }) |
|
100 | 58 | View Code Duplication | ->then(function ($v) { |
101 | 17 | if ('auto' === $v['invalidation']['enabled']) { |
|
102 | 16 | $v['invalidation']['enabled'] = false; |
|
103 | |||
104 | 16 | return $v; |
|
105 | } |
||
106 | |||
107 | 1 | throw new InvalidConfigurationException('You need to configure a proxy_client to get the cache_manager needed for invalidation handling.'); |
|
108 | 58 | }) |
|
109 | 58 | ->end() |
|
110 | 58 | ->validate() |
|
111 | 58 | ->ifTrue( |
|
112 | 58 | function ($v) { |
|
113 | 53 | return false !== $v['user_context']['logout_handler']['enabled']; |
|
114 | 58 | } |
|
115 | ) |
||
116 | 58 | ->then(function ($v) { |
|
117 | 52 | if (isset($v['cache_manager']['custom_proxy_client'])) { |
|
118 | 5 | $v['user_context']['logout_handler']['enabled'] = true; |
|
119 | |||
120 | 5 | return $v; |
|
121 | } |
||
122 | |||
123 | 47 | if (isset($v['proxy_client']['default']) && in_array($v['proxy_client']['default'], ['varnish', 'noop'])) { |
|
124 | $v['user_context']['logout_handler']['enabled'] = true; |
||
125 | |||
126 | return $v; |
||
127 | } |
||
128 | 47 | View Code Duplication | if (isset($v['proxy_client']['varnish']) || isset($v['proxy_client']['noop'])) { |
129 | 23 | $v['user_context']['logout_handler']['enabled'] = true; |
|
130 | |||
131 | 23 | return $v; |
|
132 | } |
||
133 | |||
134 | 24 | View Code Duplication | if ('auto' === $v['user_context']['logout_handler']['enabled']) { |
135 | 22 | $v['user_context']['logout_handler']['enabled'] = false; |
|
136 | |||
137 | 22 | return $v; |
|
138 | } |
||
139 | |||
140 | 2 | throw new InvalidConfigurationException('To enable the user context logout handler, you need to configure a ban capable proxy_client.'); |
|
141 | 58 | }) |
|
142 | 58 | ->end() |
|
143 | // Determine the default tags header for the varnish client, depending on whether we use BAN or xkey |
||
144 | 58 | ->validate() |
|
145 | 58 | ->ifTrue( |
|
146 | 58 | View Code Duplication | function ($v) { |
147 | return |
||
148 | 51 | array_key_exists('proxy_client', $v) |
|
149 | 51 | && array_key_exists('varnish', $v['proxy_client']) |
|
150 | 51 | && empty($v['proxy_client']['varnish']['tags_header']) |
|
151 | ; |
||
152 | 58 | } |
|
153 | ) |
||
154 | 58 | ->then(function ($v) { |
|
155 | 18 | $v['proxy_client']['varnish']['tags_header'] = |
|
156 | 18 | (Varnish::TAG_XKEY === $v['proxy_client']['varnish']['tag_mode']) |
|
157 | 1 | ? Varnish::DEFAULT_HTTP_HEADER_CACHE_XKEY |
|
158 | 17 | : Varnish::DEFAULT_HTTP_HEADER_CACHE_TAGS; |
|
159 | |||
160 | 18 | return $v; |
|
161 | 58 | }) |
|
162 | 58 | ->end() |
|
163 | // Determine the default tag response header, depending on whether we use BAN or xkey |
||
164 | 58 | ->validate() |
|
165 | 58 | ->ifTrue( |
|
166 | 58 | function ($v) { |
|
167 | 51 | return empty($v['tags']['response_header']); |
|
168 | 58 | } |
|
169 | ) |
||
170 | 58 | ->then(function ($v) { |
|
171 | 47 | $v['tags']['response_header'] = $this->isVarnishXkey($v) ? 'xkey' : TagHeaderFormatter::DEFAULT_HEADER_NAME; |
|
172 | |||
173 | 47 | return $v; |
|
174 | 58 | }) |
|
175 | 58 | ->end() |
|
176 | // Determine the default separator for the tags header, depending on whether we use BAN or xkey |
||
177 | 58 | ->validate() |
|
178 | 58 | ->ifTrue( |
|
179 | 58 | function ($v) { |
|
180 | 51 | return empty($v['tags']['separator']); |
|
181 | 58 | } |
|
182 | ) |
||
183 | 58 | ->then(function ($v) { |
|
184 | 48 | $v['tags']['separator'] = $this->isVarnishXkey($v) ? ' ' : ','; |
|
185 | |||
186 | 48 | return $v; |
|
187 | 58 | }) |
|
188 | ; |
||
189 | |||
190 | 58 | $this->addCacheableResponseSection($rootNode); |
|
191 | 58 | $this->addCacheControlSection($rootNode); |
|
192 | 58 | $this->addProxyClientSection($rootNode); |
|
193 | 58 | $this->addCacheManagerSection($rootNode); |
|
194 | 58 | $this->addTagSection($rootNode); |
|
195 | 58 | $this->addInvalidationSection($rootNode); |
|
196 | 58 | $this->addUserContextListenerSection($rootNode); |
|
197 | 58 | $this->addFlashMessageSection($rootNode); |
|
198 | 58 | $this->addTestSection($rootNode); |
|
199 | 58 | $this->addDebugSection($rootNode); |
|
200 | |||
201 | 58 | return $treeBuilder; |
|
202 | } |
||
203 | |||
204 | 48 | View Code Duplication | private function isVarnishXkey(array $v): bool |
211 | |||
212 | 58 | private function addCacheableResponseSection(ArrayNodeDefinition $rootNode) |
|
242 | |||
243 | /** |
||
244 | * Cache header control main section. |
||
245 | * |
||
246 | * @param ArrayNodeDefinition $rootNode |
||
247 | */ |
||
248 | 58 | private function addCacheControlSection(ArrayNodeDefinition $rootNode) |
|
249 | { |
||
250 | $rules = $rootNode |
||
251 | 58 | ->children() |
|
252 | 58 | ->arrayNode('cache_control') |
|
253 | 58 | ->fixXmlConfig('rule') |
|
254 | 58 | ->children() |
|
255 | 58 | ->arrayNode('defaults') |
|
256 | 58 | ->addDefaultsIfNotSet() |
|
257 | 58 | ->children() |
|
258 | 58 | ->booleanNode('overwrite') |
|
259 | 58 | ->info('Whether to overwrite existing cache headers') |
|
260 | 58 | ->defaultFalse() |
|
261 | 58 | ->end() |
|
262 | 58 | ->end() |
|
263 | 58 | ->end() |
|
264 | 58 | ->arrayNode('rules') |
|
265 | 58 | ->prototype('array') |
|
266 | 58 | ->children(); |
|
267 | |||
268 | 58 | $this->addMatch($rules, true); |
|
269 | $rules |
||
270 | 58 | ->arrayNode('headers') |
|
271 | 58 | ->isRequired() |
|
272 | // todo validate there is some header defined |
||
273 | 58 | ->children() |
|
274 | 58 | ->enumNode('overwrite') |
|
275 | 58 | ->info('Whether to overwrite cache headers for this rule, defaults to the cache_control.defaults.overwrite setting') |
|
276 | 58 | ->values(['default', true, false]) |
|
277 | 58 | ->defaultValue('default') |
|
278 | 58 | ->end() |
|
279 | 58 | ->arrayNode('cache_control') |
|
280 | 58 | ->info('Add the specified cache control directives.') |
|
281 | 58 | ->children() |
|
282 | 58 | ->scalarNode('max_age')->end() |
|
283 | 58 | ->scalarNode('s_maxage')->end() |
|
284 | 58 | ->booleanNode('private')->end() |
|
285 | 58 | ->booleanNode('public')->end() |
|
286 | 58 | ->booleanNode('must_revalidate')->end() |
|
287 | 58 | ->booleanNode('proxy_revalidate')->end() |
|
288 | 58 | ->booleanNode('no_transform')->end() |
|
289 | 58 | ->booleanNode('no_cache')->end() |
|
290 | 58 | ->booleanNode('no_store')->end() |
|
291 | 58 | ->scalarNode('stale_if_error')->end() |
|
292 | 58 | ->scalarNode('stale_while_revalidate')->end() |
|
293 | 58 | ->end() |
|
294 | 58 | ->end() |
|
295 | 58 | ->enumNode('etag') |
|
296 | 58 | ->defaultValue(false) |
|
297 | 58 | ->treatTrueLike('strong') |
|
298 | 58 | ->info('Set a simple ETag which is just the md5 hash of the response body. '. |
|
299 | 58 | 'You can specify which type of ETag you want by passing "strong" or "weak".') |
|
300 | 58 | ->values(['weak', 'strong', false]) |
|
301 | 58 | ->end() |
|
302 | 58 | ->scalarNode('last_modified') |
|
303 | 58 | ->validate() |
|
304 | 58 | ->ifTrue(function ($v) { |
|
305 | 2 | if (is_string($v)) { |
|
306 | 2 | new \DateTime($v); |
|
307 | } |
||
308 | |||
309 | 1 | return false; |
|
310 | 58 | }) |
|
311 | 58 | ->thenInvalid('') // this will never happen as new DateTime will throw an exception if $v is no date |
|
312 | 58 | ->end() |
|
313 | 58 | ->info('Set a default last modified timestamp if none is set yet. Value must be parseable by DateTime') |
|
314 | 58 | ->end() |
|
315 | 58 | ->scalarNode('reverse_proxy_ttl') |
|
316 | 58 | ->defaultNull() |
|
317 | 58 | ->info('Specify an X-Reverse-Proxy-TTL header with a time in seconds for a caching proxy under your control.') |
|
318 | 58 | ->end() |
|
319 | 58 | ->arrayNode('vary') |
|
320 | 58 | ->beforeNormalization()->ifString()->then(function ($v) { |
|
321 | 2 | return preg_split('/\s*,\s*/', $v); |
|
322 | 58 | })->end() |
|
323 | 58 | ->prototype('scalar')->end() |
|
324 | 58 | ->info('Define a list of additional headers on which the response varies.') |
|
325 | 58 | ->end() |
|
326 | 58 | ->end() |
|
327 | 58 | ->end() |
|
328 | ; |
||
329 | 58 | } |
|
330 | |||
331 | /** |
||
332 | * Shared configuration between cache control, tags and invalidation. |
||
333 | * |
||
334 | * @param NodeBuilder $rules |
||
335 | * @param bool $matchResponse whether to also add fields to match response |
||
336 | */ |
||
337 | 58 | private function addMatch(NodeBuilder $rules, $matchResponse = false) |
|
400 | |||
401 | 58 | private function addProxyClientSection(ArrayNodeDefinition $rootNode) |
|
402 | { |
||
403 | $rootNode |
||
404 | 58 | ->children() |
|
405 | 58 | ->arrayNode('proxy_client') |
|
406 | 58 | ->children() |
|
407 | 58 | ->enumNode('default') |
|
408 | 58 | ->values(['varnish', 'nginx', 'symfony', 'noop']) |
|
409 | 58 | ->info('If you configure more than one proxy client, you need to specify which client is the default.') |
|
410 | 58 | ->end() |
|
411 | 58 | ->arrayNode('varnish') |
|
412 | 58 | ->fixXmlConfig('default_ban_header') |
|
413 | 58 | ->validate() |
|
414 | 58 | ->always(function ($v) { |
|
415 | 22 | if (!count($v['default_ban_headers'])) { |
|
416 | 21 | unset($v['default_ban_headers']); |
|
417 | } |
||
418 | |||
419 | 22 | return $v; |
|
420 | 58 | }) |
|
421 | 58 | ->end() |
|
422 | 58 | ->children() |
|
423 | 58 | ->scalarNode('tags_header') |
|
424 | 58 | ->info('HTTP header to use when sending tag invalidation requests to Varnish') |
|
425 | 58 | ->end() |
|
426 | 58 | ->scalarNode('header_length') |
|
427 | 58 | ->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.') |
|
428 | 58 | ->end() |
|
429 | 58 | ->arrayNode('default_ban_headers') |
|
430 | 58 | ->useAttributeAsKey('name') |
|
431 | 58 | ->info('Map of additional headers to include in each ban request.') |
|
432 | 58 | ->prototype('scalar')->end() |
|
433 | 58 | ->end() |
|
434 | 58 | ->enumNode('tag_mode') |
|
435 | 58 | ->info('If you can enable the xkey module in Varnish, use the purgekeys mode for more efficient tag handling') |
|
436 | 58 | ->values(['ban', 'purgekeys']) |
|
437 | 58 | ->defaultValue('ban') |
|
438 | 58 | ->end() |
|
439 | 58 | ->append($this->getHttpDispatcherNode()) |
|
440 | 58 | ->end() |
|
441 | 58 | ->end() |
|
442 | |||
443 | 58 | ->arrayNode('nginx') |
|
444 | 58 | ->children() |
|
445 | 58 | ->scalarNode('purge_location') |
|
446 | 58 | ->defaultValue(false) |
|
447 | 58 | ->info('Path to trigger the purge on Nginx for different location purge.') |
|
448 | 58 | ->end() |
|
449 | 58 | ->append($this->getHttpDispatcherNode()) |
|
450 | 58 | ->end() |
|
451 | 58 | ->end() |
|
452 | |||
453 | 58 | ->arrayNode('symfony') |
|
454 | 58 | ->children() |
|
455 | 58 | ->scalarNode('tags_header') |
|
456 | 58 | ->defaultValue(PurgeTagsListener::DEFAULT_TAGS_HEADER) |
|
457 | 58 | ->info('HTTP header to use when sending tag invalidation requests to Symfony HttpCache') |
|
458 | 58 | ->end() |
|
459 | 58 | ->scalarNode('tags_method') |
|
460 | 58 | ->defaultValue(PurgeTagsListener::DEFAULT_TAGS_METHOD) |
|
461 | 58 | ->info('HTTP method for sending tag invalidation requests to Symfony HttpCache') |
|
462 | 58 | ->end() |
|
463 | 58 | ->scalarNode('header_length') |
|
464 | 58 | ->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.') |
|
465 | 58 | ->end() |
|
466 | 58 | ->scalarNode('purge_method') |
|
467 | 58 | ->defaultValue(PurgeListener::DEFAULT_PURGE_METHOD) |
|
468 | 58 | ->info('HTTP method to use when sending purge requests to Symfony HttpCache') |
|
469 | 58 | ->end() |
|
470 | 58 | ->booleanNode('use_kernel_dispatcher') |
|
471 | 58 | ->defaultFalse() |
|
472 | 58 | ->info('Dispatches invalidation requests to the kernel directly instead of executing real HTTP requests. Requires special kernel setup! Refer to the documentation for more information.') |
|
473 | 58 | ->end() |
|
474 | 58 | ->append($this->getHttpDispatcherNode()) |
|
475 | 58 | ->end() |
|
476 | 58 | ->end() |
|
477 | |||
478 | 58 | ->booleanNode('noop')->end() |
|
479 | 58 | ->end() |
|
480 | 58 | ->validate() |
|
481 | 58 | ->always() |
|
482 | 58 | ->then(function ($config) { |
|
483 | 34 | foreach ($config as $proxyName => $proxyConfig) { |
|
484 | 34 | $serversConfigured = isset($proxyConfig['http']) && isset($proxyConfig['http']['servers']) && \is_array($proxyConfig['http']['servers']); |
|
485 | |||
486 | 34 | if (!\in_array($proxyName, ['noop', 'default', 'symfony'])) { |
|
487 | 27 | if (!$serversConfigured) { |
|
488 | throw new \InvalidArgumentException(sprintf('The "http.servers" section must be defined for the proxy "%s"', $proxyName)); |
||
489 | } |
||
490 | |||
491 | 27 | return $config; |
|
492 | } |
||
493 | |||
494 | 7 | if ('symfony' === $proxyName) { |
|
495 | 4 | if (!$serversConfigured && false === $proxyConfig['use_kernel_dispatcher']) { |
|
496 | 7 | throw new \InvalidArgumentException('Either configure the "http.servers" section or enable "proxy_client.symfony.use_kernel_dispatcher"'); |
|
497 | } |
||
498 | } |
||
499 | } |
||
500 | |||
501 | 6 | return $config; |
|
502 | 58 | }) |
|
503 | 58 | ->end() |
|
504 | 58 | ->end() |
|
505 | 58 | ->end(); |
|
506 | 58 | } |
|
507 | |||
508 | /** |
||
509 | * Get the configuration node for a HTTP dispatcher in a proxy client. |
||
510 | * |
||
511 | * @return NodeDefinition |
||
512 | */ |
||
513 | 58 | private function getHttpDispatcherNode() |
|
546 | |||
547 | 58 | private function addTestSection(ArrayNodeDefinition $rootNode) |
|
586 | |||
587 | /** |
||
588 | * Cache manager main section. |
||
589 | * |
||
590 | * @param ArrayNodeDefinition $rootNode |
||
591 | */ |
||
592 | 58 | private function addCacheManagerSection(ArrayNodeDefinition $rootNode) |
|
631 | |||
632 | 58 | private function addTagSection(ArrayNodeDefinition $rootNode) |
|
633 | { |
||
634 | $rules = $rootNode |
||
635 | 58 | ->children() |
|
636 | 58 | ->arrayNode('tags') |
|
637 | 58 | ->addDefaultsIfNotSet() |
|
638 | 58 | ->fixXmlConfig('rule') |
|
639 | 58 | ->children() |
|
640 | 58 | ->enumNode('enabled') |
|
641 | 58 | ->values([true, false, 'auto']) |
|
642 | 58 | ->defaultValue('auto') |
|
643 | 58 | ->info('Allows to disable the event subscriber for tag configuration and annotations when your project does not use the annotations. Enabled by default if you configured the cache manager.') |
|
644 | 58 | ->end() |
|
645 | 58 | ->booleanNode('strict')->defaultFalse()->end() |
|
646 | 58 | ->scalarNode('expression_language') |
|
647 | 58 | ->defaultNull() |
|
648 | 58 | ->info('Service name of a custom ExpressionLanugage to use.') |
|
649 | 58 | ->end() |
|
650 | 58 | ->scalarNode('response_header') |
|
651 | 58 | ->defaultNull() |
|
652 | 58 | ->info('HTTP header that contains cache tags. Defaults to xkey-softpurge for Varnish xkey or X-Cache-Tags otherwise') |
|
653 | 58 | ->end() |
|
654 | 58 | ->scalarNode('separator') |
|
655 | 58 | ->defaultNull() |
|
656 | 58 | ->info('Character(s) to use to separate multiple tags. Defaults to " " for Varnish xkey or "," otherwise') |
|
657 | 58 | ->end() |
|
658 | 58 | ->arrayNode('rules') |
|
659 | 58 | ->prototype('array') |
|
660 | 58 | ->fixXmlConfig('tag') |
|
661 | 58 | ->fixXmlConfig('tag_expression') |
|
662 | 58 | ->validate() |
|
663 | 58 | ->ifTrue(function ($v) { |
|
664 | 4 | return !empty($v['tag_expressions']) && !class_exists(ExpressionLanguage::class); |
|
665 | 58 | }) |
|
666 | 58 | ->thenInvalid('Configured a tag_expression but ExpressionLanugage is not available') |
|
667 | 58 | ->end() |
|
668 | 58 | ->children() |
|
669 | ; |
||
670 | 58 | $this->addMatch($rules); |
|
671 | |||
672 | $rules |
||
673 | 58 | ->arrayNode('tags') |
|
674 | 58 | ->prototype('scalar') |
|
675 | 58 | ->info('Tags to add to the response on safe requests, to invalidate on unsafe requests') |
|
676 | 58 | ->end()->end() |
|
677 | 58 | ->arrayNode('tag_expressions') |
|
678 | 58 | ->prototype('scalar') |
|
679 | 58 | ->info('Tags to add to the response on safe requests, to invalidate on unsafe requests') |
|
680 | 58 | ->end() |
|
681 | ; |
||
682 | 58 | } |
|
683 | |||
684 | 58 | private function addInvalidationSection(ArrayNodeDefinition $rootNode) |
|
721 | |||
722 | /** |
||
723 | * User context main section. |
||
724 | * |
||
725 | * @param ArrayNodeDefinition $rootNode |
||
726 | */ |
||
727 | 58 | private function addUserContextListenerSection(ArrayNodeDefinition $rootNode) |
|
795 | |||
796 | 58 | private function addFlashMessageSection(ArrayNodeDefinition $rootNode) |
|
825 | |||
826 | 58 | private function addDebugSection(ArrayNodeDefinition $rootNode) |
|
846 | } |
||
847 |
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.