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 Container 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 Container, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
63 | class Container implements IntrospectableContainerInterface |
||
64 | { |
||
65 | /** |
||
66 | * @var ParameterBagInterface |
||
67 | */ |
||
68 | protected $parameterBag; |
||
69 | |||
70 | protected $services = array(); |
||
71 | protected $methodMap = array(); |
||
72 | protected $aliases = array(); |
||
73 | protected $scopes = array(); |
||
74 | protected $scopeChildren = array(); |
||
75 | protected $scopedServices = array(); |
||
76 | protected $scopeStacks = array(); |
||
77 | protected $loading = array(); |
||
78 | |||
79 | private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_'); |
||
80 | |||
81 | /** |
||
82 | * Constructor. |
||
83 | * |
||
84 | * @param ParameterBagInterface $parameterBag A ParameterBagInterface instance |
||
85 | * |
||
86 | * @api |
||
87 | */ |
||
88 | public function __construct(ParameterBagInterface $parameterBag = null) |
||
92 | |||
93 | /** |
||
94 | * Compiles the container. |
||
95 | * |
||
96 | * This method does two things: |
||
97 | * |
||
98 | * * Parameter values are resolved; |
||
99 | * * The parameter bag is frozen. |
||
100 | * |
||
101 | * @api |
||
102 | */ |
||
103 | public function compile() |
||
104 | { |
||
105 | $this->parameterBag->resolve(); |
||
106 | |||
107 | $this->parameterBag = new FrozenParameterBag($this->parameterBag->all()); |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * Returns true if the container parameter bag are frozen. |
||
112 | * |
||
113 | * @return bool true if the container parameter bag are frozen, false otherwise |
||
114 | * |
||
115 | * @api |
||
116 | */ |
||
117 | public function isFrozen() |
||
121 | |||
122 | /** |
||
123 | * Gets the service container parameter bag. |
||
124 | * |
||
125 | * @return ParameterBagInterface A ParameterBagInterface instance |
||
126 | * |
||
127 | * @api |
||
128 | */ |
||
129 | public function getParameterBag() |
||
133 | |||
134 | /** |
||
135 | * Gets a parameter. |
||
136 | * |
||
137 | * @param string $name The parameter name |
||
138 | * |
||
139 | * @return mixed The parameter value |
||
140 | * |
||
141 | * @throws InvalidArgumentException if the parameter is not defined |
||
142 | * |
||
143 | * @api |
||
144 | */ |
||
145 | public function getParameter($name) |
||
149 | |||
150 | /** |
||
151 | * Checks if a parameter exists. |
||
152 | * |
||
153 | * @param string $name The parameter name |
||
154 | * |
||
155 | * @return bool The presence of parameter in container |
||
156 | * |
||
157 | * @api |
||
158 | */ |
||
159 | public function hasParameter($name) |
||
163 | |||
164 | /** |
||
165 | * Sets a parameter. |
||
166 | * |
||
167 | * @param string $name The parameter name |
||
168 | * @param mixed $value The parameter value |
||
169 | * |
||
170 | * @api |
||
171 | */ |
||
172 | public function setParameter($name, $value) |
||
176 | |||
177 | /** |
||
178 | * Sets a service. |
||
179 | * |
||
180 | * Setting a service to null resets the service: has() returns false and get() |
||
181 | * behaves in the same way as if the service was never created. |
||
182 | * |
||
183 | * @param string $id The service identifier |
||
184 | * @param object $service The service instance |
||
185 | * @param string $scope The scope of the service |
||
186 | * |
||
187 | * @throws RuntimeException When trying to set a service in an inactive scope |
||
188 | * @throws InvalidArgumentException When trying to set a service in the prototype scope |
||
189 | * |
||
190 | * @api |
||
191 | */ |
||
192 | public function set($id, $service, $scope = self::SCOPE_CONTAINER) |
||
193 | { |
||
194 | if (self::SCOPE_PROTOTYPE === $scope) { |
||
195 | throw new InvalidArgumentException(sprintf('You cannot set service "%s" of scope "prototype".', $id)); |
||
196 | } |
||
197 | |||
198 | $id = strtolower($id); |
||
199 | |||
200 | if ('service_container' === $id) { |
||
201 | // BC: 'service_container' is no longer a self-reference but always |
||
202 | // $this, so ignore this call. |
||
203 | // @todo Throw InvalidArgumentException in next major release. |
||
204 | return; |
||
205 | } |
||
206 | if (self::SCOPE_CONTAINER !== $scope) { |
||
207 | if (!isset($this->scopedServices[$scope])) { |
||
208 | throw new RuntimeException(sprintf('You cannot set service "%s" of inactive scope.', $id)); |
||
209 | } |
||
210 | |||
211 | $this->scopedServices[$scope][$id] = $service; |
||
212 | } |
||
213 | |||
214 | $this->services[$id] = $service; |
||
215 | |||
216 | if (method_exists($this, $method = 'synchronize'.strtr($id, $this->underscoreMap).'Service')) { |
||
217 | $this->$method(); |
||
218 | } |
||
219 | |||
220 | if (null === $service) { |
||
221 | if (self::SCOPE_CONTAINER !== $scope) { |
||
222 | unset($this->scopedServices[$scope][$id]); |
||
223 | } |
||
224 | |||
225 | unset($this->services[$id]); |
||
226 | } |
||
227 | } |
||
228 | |||
229 | /** |
||
230 | * Returns true if the given service is defined. |
||
231 | * |
||
232 | * @param string $id The service identifier |
||
233 | * |
||
234 | * @return bool true if the service is defined, false otherwise |
||
235 | * |
||
236 | * @api |
||
237 | */ |
||
238 | public function has($id) |
||
239 | { |
||
240 | for ($i = 2;;) { |
||
241 | View Code Duplication | if ('service_container' === $id |
|
|
|||
242 | || isset($this->aliases[$id]) |
||
243 | || isset($this->services[$id]) |
||
244 | || array_key_exists($id, $this->services) |
||
245 | ) { |
||
246 | return true; |
||
247 | } |
||
248 | if (--$i && $id !== $lcId = strtolower($id)) { |
||
249 | $id = $lcId; |
||
250 | } else { |
||
251 | return method_exists($this, 'get'.strtr($id, $this->underscoreMap).'Service'); |
||
252 | } |
||
253 | } |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * Gets a service. |
||
258 | * |
||
259 | * If a service is defined both through a set() method and |
||
260 | * with a get{$id}Service() method, the former has always precedence. |
||
261 | * |
||
262 | * @param string $id The service identifier |
||
263 | * @param int $invalidBehavior The behavior when the service does not exist |
||
264 | * |
||
265 | * @return object The associated service |
||
266 | * |
||
267 | * @throws ServiceCircularReferenceException When a circular reference is detected |
||
268 | * @throws ServiceNotFoundException When the service is not defined |
||
269 | * @throws \Exception if an exception has been thrown when the service has been resolved |
||
270 | * |
||
271 | * @see Reference |
||
272 | * |
||
273 | * @api |
||
274 | */ |
||
275 | public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) |
||
276 | { |
||
277 | // Attempt to retrieve the service by checking first aliases then |
||
278 | // available services. Service IDs are case insensitive, however since |
||
279 | // this method can be called thousands of times during a request, avoid |
||
280 | // calling strtolower() unless necessary. |
||
281 | for ($i = 2;;) { |
||
282 | if ('service_container' === $id) { |
||
283 | return $this; |
||
284 | } |
||
285 | if (isset($this->aliases[$id])) { |
||
286 | $id = $this->aliases[$id]; |
||
287 | } |
||
288 | // Re-use shared service instance if it exists. |
||
289 | if (isset($this->services[$id]) || array_key_exists($id, $this->services)) { |
||
290 | return $this->services[$id]; |
||
291 | } |
||
292 | |||
293 | if (isset($this->loading[$id])) { |
||
294 | throw new ServiceCircularReferenceException($id, array_keys($this->loading)); |
||
295 | } |
||
296 | |||
297 | if (isset($this->methodMap[$id])) { |
||
298 | $method = $this->methodMap[$id]; |
||
299 | } elseif (--$i && $id !== $lcId = strtolower($id)) { |
||
300 | $id = $lcId; |
||
301 | continue; |
||
302 | } elseif (method_exists($this, $method = 'get'.strtr($id, $this->underscoreMap).'Service')) { |
||
303 | // $method is set to the right value, proceed |
||
304 | View Code Duplication | } else { |
|
305 | if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) { |
||
306 | if (!$id) { |
||
307 | throw new ServiceNotFoundException($id); |
||
308 | } |
||
309 | |||
310 | $alternatives = array(); |
||
311 | foreach ($this->services as $key => $associatedService) { |
||
312 | $lev = levenshtein($id, $key); |
||
313 | if ($lev <= strlen($id) / 3 || false !== strpos($key, $id)) { |
||
314 | $alternatives[] = $key; |
||
315 | } |
||
316 | } |
||
317 | |||
318 | throw new ServiceNotFoundException($id, null, null, $alternatives); |
||
319 | } |
||
320 | |||
321 | return; |
||
322 | } |
||
323 | |||
324 | $this->loading[$id] = true; |
||
325 | |||
326 | try { |
||
327 | $service = $this->$method(); |
||
328 | } catch (\Exception $e) { |
||
329 | unset($this->loading[$id]); |
||
330 | |||
331 | if (array_key_exists($id, $this->services)) { |
||
332 | unset($this->services[$id]); |
||
333 | } |
||
334 | |||
335 | if ($e instanceof InactiveScopeException && self::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) { |
||
336 | return; |
||
337 | } |
||
338 | |||
339 | throw $e; |
||
340 | } |
||
341 | |||
342 | unset($this->loading[$id]); |
||
343 | |||
344 | return $service; |
||
345 | } |
||
346 | } |
||
347 | |||
348 | /** |
||
349 | * Returns true if the given service has actually been initialized. |
||
350 | * |
||
351 | * @param string $id The service identifier |
||
352 | * |
||
353 | * @return bool true if service has already been initialized, false otherwise |
||
354 | */ |
||
355 | public function initialized($id) |
||
371 | |||
372 | /** |
||
373 | * Gets all service ids. |
||
374 | * |
||
375 | * @return array An array of all defined service ids |
||
376 | */ |
||
377 | public function getServiceIds() |
||
378 | { |
||
379 | $ids = array(); |
||
380 | $r = new \ReflectionClass($this); |
||
381 | foreach ($r->getMethods() as $method) { |
||
382 | if (preg_match('/^get(.+)Service$/', $method->name, $match)) { |
||
383 | $ids[] = self::underscore($match[1]); |
||
384 | } |
||
385 | } |
||
386 | $ids[] = 'service_container'; |
||
387 | |||
388 | return array_unique(array_merge($ids, array_keys($this->services))); |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * This is called when you enter a scope. |
||
393 | * |
||
394 | * @param string $name |
||
395 | * |
||
396 | * @throws RuntimeException When the parent scope is inactive |
||
397 | * @throws InvalidArgumentException When the scope does not exist |
||
398 | * |
||
399 | * @api |
||
400 | */ |
||
401 | public function enterScope($name) |
||
402 | { |
||
403 | if (!isset($this->scopes[$name])) { |
||
404 | throw new InvalidArgumentException(sprintf('The scope "%s" does not exist.', $name)); |
||
405 | } |
||
406 | |||
407 | if (self::SCOPE_CONTAINER !== $this->scopes[$name] && !isset($this->scopedServices[$this->scopes[$name]])) { |
||
408 | throw new RuntimeException(sprintf('The parent scope "%s" must be active when entering this scope.', $this->scopes[$name])); |
||
409 | } |
||
410 | |||
411 | // check if a scope of this name is already active, if so we need to |
||
412 | // remove all services of this scope, and those of any of its child |
||
413 | // scopes from the global services map |
||
414 | if (isset($this->scopedServices[$name])) { |
||
415 | $services = array($this->services, $name => $this->scopedServices[$name]); |
||
416 | unset($this->scopedServices[$name]); |
||
417 | |||
418 | View Code Duplication | foreach ($this->scopeChildren[$name] as $child) { |
|
419 | if (isset($this->scopedServices[$child])) { |
||
420 | $services[$child] = $this->scopedServices[$child]; |
||
421 | unset($this->scopedServices[$child]); |
||
422 | } |
||
423 | } |
||
424 | |||
425 | // update global map |
||
426 | $this->services = call_user_func_array('array_diff_key', $services); |
||
427 | array_shift($services); |
||
428 | |||
429 | // add stack entry for this scope so we can restore the removed services later |
||
430 | if (!isset($this->scopeStacks[$name])) { |
||
431 | $this->scopeStacks[$name] = new \SplStack(); |
||
432 | } |
||
433 | $this->scopeStacks[$name]->push($services); |
||
434 | } |
||
435 | |||
436 | $this->scopedServices[$name] = array(); |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * This is called to leave the current scope, and move back to the parent |
||
441 | * scope. |
||
442 | * |
||
443 | * @param string $name The name of the scope to leave |
||
444 | * |
||
445 | * @throws InvalidArgumentException if the scope is not active |
||
446 | * |
||
447 | * @api |
||
448 | */ |
||
449 | public function leaveScope($name) |
||
450 | { |
||
451 | if (!isset($this->scopedServices[$name])) { |
||
452 | throw new InvalidArgumentException(sprintf('The scope "%s" is not active.', $name)); |
||
453 | } |
||
454 | |||
455 | // remove all services of this scope, or any of its child scopes from |
||
456 | // the global service map |
||
457 | $services = array($this->services, $this->scopedServices[$name]); |
||
458 | unset($this->scopedServices[$name]); |
||
459 | |||
460 | View Code Duplication | foreach ($this->scopeChildren[$name] as $child) { |
|
461 | if (isset($this->scopedServices[$child])) { |
||
462 | $services[] = $this->scopedServices[$child]; |
||
463 | unset($this->scopedServices[$child]); |
||
464 | } |
||
465 | } |
||
466 | |||
467 | // update global map |
||
468 | $this->services = call_user_func_array('array_diff_key', $services); |
||
469 | |||
470 | // check if we need to restore services of a previous scope of this type |
||
471 | if (isset($this->scopeStacks[$name]) && count($this->scopeStacks[$name]) > 0) { |
||
472 | $services = $this->scopeStacks[$name]->pop(); |
||
473 | $this->scopedServices += $services; |
||
474 | |||
475 | if ($this->scopeStacks[$name]->isEmpty()) { |
||
476 | unset($this->scopeStacks[$name]); |
||
477 | } |
||
478 | |||
479 | foreach ($services as $array) { |
||
480 | foreach ($array as $id => $service) { |
||
481 | $this->set($id, $service, $name); |
||
482 | } |
||
483 | } |
||
484 | } |
||
485 | } |
||
486 | |||
487 | /** |
||
488 | * Adds a scope to the container. |
||
489 | * |
||
490 | * @param ScopeInterface $scope |
||
491 | * |
||
492 | * @throws InvalidArgumentException |
||
493 | * |
||
494 | * @api |
||
495 | */ |
||
496 | public function addScope(ScopeInterface $scope) |
||
497 | { |
||
498 | $name = $scope->getName(); |
||
499 | $parentScope = $scope->getParentName(); |
||
500 | |||
501 | if (self::SCOPE_CONTAINER === $name || self::SCOPE_PROTOTYPE === $name) { |
||
502 | throw new InvalidArgumentException(sprintf('The scope "%s" is reserved.', $name)); |
||
503 | } |
||
504 | if (isset($this->scopes[$name])) { |
||
505 | throw new InvalidArgumentException(sprintf('A scope with name "%s" already exists.', $name)); |
||
506 | } |
||
507 | if (self::SCOPE_CONTAINER !== $parentScope && !isset($this->scopes[$parentScope])) { |
||
508 | throw new InvalidArgumentException(sprintf('The parent scope "%s" does not exist, or is invalid.', $parentScope)); |
||
509 | } |
||
510 | |||
511 | $this->scopes[$name] = $parentScope; |
||
512 | $this->scopeChildren[$name] = array(); |
||
513 | |||
514 | // normalize the child relations |
||
515 | while ($parentScope !== self::SCOPE_CONTAINER) { |
||
516 | $this->scopeChildren[$parentScope][] = $name; |
||
517 | $parentScope = $this->scopes[$parentScope]; |
||
518 | } |
||
519 | } |
||
520 | |||
521 | /** |
||
522 | * Returns whether this container has a certain scope. |
||
523 | * |
||
524 | * @param string $name The name of the scope |
||
525 | * |
||
526 | * @return bool |
||
527 | * |
||
528 | * @api |
||
529 | */ |
||
530 | public function hasScope($name) |
||
534 | |||
535 | /** |
||
536 | * Returns whether this scope is currently active. |
||
537 | * |
||
538 | * This does not actually check if the passed scope actually exists. |
||
539 | * |
||
540 | * @param string $name |
||
541 | * |
||
542 | * @return bool |
||
543 | * |
||
544 | * @api |
||
545 | */ |
||
546 | public function isScopeActive($name) |
||
550 | |||
551 | /** |
||
552 | * Camelizes a string. |
||
553 | * |
||
554 | * @param string $id A string to camelize |
||
555 | * |
||
556 | * @return string The camelized string |
||
557 | */ |
||
558 | public static function camelize($id) |
||
562 | |||
563 | /** |
||
564 | * A string to underscore. |
||
565 | * |
||
566 | * @param string $id The string to underscore |
||
567 | * |
||
568 | * @return string The underscored string |
||
569 | */ |
||
570 | public static function underscore($id) |
||
574 | } |
||
575 |
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.