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 Option 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 Option, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
63 | class Option extends AbstractOption |
||
64 | { |
||
65 | /** |
||
66 | * Flag: The option has no value. |
||
67 | */ |
||
68 | const NO_VALUE = 4; |
||
69 | |||
70 | /** |
||
71 | * Flag: The option has a required value. |
||
72 | */ |
||
73 | const REQUIRED_VALUE = 8; |
||
74 | |||
75 | /** |
||
76 | * Flag: The option has an optional value. |
||
77 | */ |
||
78 | const OPTIONAL_VALUE = 16; |
||
79 | |||
80 | /** |
||
81 | * Flag: The option can be stated multiple times with different values. |
||
82 | */ |
||
83 | const MULTI_VALUED = 32; |
||
84 | |||
85 | /** |
||
86 | * Flag: The option value is parsed as string. |
||
87 | */ |
||
88 | const STRING = 128; |
||
89 | |||
90 | /** |
||
91 | * Flag: The option value is parsed as boolean. |
||
92 | */ |
||
93 | const BOOLEAN = 256; |
||
94 | |||
95 | /** |
||
96 | * Flag: The option value is parsed as integer. |
||
97 | */ |
||
98 | const INTEGER = 512; |
||
99 | |||
100 | /** |
||
101 | * Flag: The option value is parsed as float. |
||
102 | */ |
||
103 | const FLOAT = 1024; |
||
104 | |||
105 | /** |
||
106 | * Flag: The option value "null" should be parsed as `null`. |
||
107 | */ |
||
108 | const NULLABLE = 2048; |
||
109 | |||
110 | /** |
||
111 | * @var mixed |
||
112 | */ |
||
113 | private $defaultValue; |
||
114 | |||
115 | /** |
||
116 | * @var string |
||
117 | */ |
||
118 | private $valueName; |
||
119 | |||
120 | /** |
||
121 | * Creates a new option. |
||
122 | * |
||
123 | * @param string $longName The long option name. |
||
124 | * @param string|null $shortName The short option name. |
||
125 | * @param int $flags A bitwise combination of the option flag |
||
126 | * constants. |
||
127 | * @param string $description A human-readable description of the option. |
||
|
|||
128 | * @param mixed $defaultValue The default value (must be null for |
||
129 | * {@link VALUE_REQUIRED} or |
||
130 | * {@link VALUE_NONE}). |
||
131 | * @param string $valueName The name of the value to be used in |
||
132 | * usage examples of the option. |
||
133 | * |
||
134 | * @throws InvalidValueException If the default value is invalid. |
||
135 | */ |
||
136 | 276 | public function __construct($longName, $shortName = null, $flags = 0, $description = null, $defaultValue = null, $valueName = '...') |
|
137 | { |
||
138 | 276 | Assert::string($valueName, 'The option value name must be a string. Got: %s'); |
|
139 | 274 | Assert::notEmpty($valueName, 'The option value name must not be empty.'); |
|
140 | |||
141 | 273 | $this->assertFlagsValid($flags); |
|
142 | 262 | $this->addDefaultFlags($flags); |
|
143 | |||
144 | 262 | parent::__construct($longName, $shortName, $flags, $description); |
|
145 | |||
146 | 247 | $this->valueName = $valueName; |
|
147 | |||
148 | 247 | if ($this->acceptsValue() || null !== $defaultValue) { |
|
149 | 136 | $this->setDefaultValue($defaultValue); |
|
150 | } |
||
151 | 245 | } |
|
152 | |||
153 | /** |
||
154 | * Returns whether the option accepts a value. |
||
155 | * |
||
156 | * @return bool Returns `true` if a value flag other than {@link VALUE_NONE} |
||
157 | * was passed to the constructor. |
||
158 | */ |
||
159 | 397 | public function acceptsValue() |
|
163 | |||
164 | /** |
||
165 | * Parses an option value. |
||
166 | * |
||
167 | * Pass one of the flags {@link STRING}, {@link BOOLEAN}, {@link INTEGER} |
||
168 | * and {@link FLOAT} to the constructor to configure the result of this |
||
169 | * method. You can optionally combine the flags with {@link NULLABLE} to |
||
170 | * support the conversion of "null" to `null`. |
||
171 | * |
||
172 | * @param mixed $value The value to parse. |
||
173 | * |
||
174 | * @return mixed The parsed value. |
||
175 | * |
||
176 | * @throws InvalidValueException |
||
177 | */ |
||
178 | 157 | View Code Duplication | public function parseValue($value) |
196 | |||
197 | /** |
||
198 | * Returns whether the option requires a value. |
||
199 | * |
||
200 | * @return bool Returns `true` if the flag {@link VALUE_REQUIRED} was |
||
201 | * passed to the constructor. |
||
202 | */ |
||
203 | 293 | public function isValueRequired() |
|
207 | |||
208 | /** |
||
209 | * Returns whether the option takes an optional value. |
||
210 | * |
||
211 | * @return bool Returns `true` if the flag {@link VALUE_OPTIONAL} was |
||
212 | * passed to the constructor. |
||
213 | */ |
||
214 | 292 | public function isValueOptional() |
|
218 | |||
219 | /** |
||
220 | * Returns whether the option accepts multiple values. |
||
221 | * |
||
222 | * @return bool Returns `true` if the flag {@link MULTI_VALUED} was |
||
223 | * passed to the constructor. |
||
224 | */ |
||
225 | 333 | public function isMultiValued() |
|
229 | |||
230 | /** |
||
231 | * Sets the default value of the option. |
||
232 | * |
||
233 | * If the option does not accept a value, this method throws an exception. |
||
234 | * |
||
235 | * If the option is multi-valued, the passed value must be an array or |
||
236 | * `null`. |
||
237 | * |
||
238 | * @param mixed $defaultValue The default value. |
||
239 | * |
||
240 | * @throws InvalidValueException If the default value is invalid. |
||
241 | */ |
||
242 | 136 | View Code Duplication | public function setDefaultValue($defaultValue = null) |
243 | { |
||
244 | 136 | if (!$this->acceptsValue()) { |
|
245 | 1 | throw new InvalidValueException('Cannot set a default value when using the flag VALUE_NONE.'); |
|
246 | } |
||
247 | |||
248 | 135 | if ($this->isMultiValued()) { |
|
249 | 6 | if (null === $defaultValue) { |
|
250 | 4 | $defaultValue = array(); |
|
251 | 2 | } elseif (!is_array($defaultValue)) { |
|
252 | 1 | throw new InvalidValueException(sprintf( |
|
253 | 'The default value of a multi-valued option must be an '. |
||
254 | 1 | 'array. Got: %s', |
|
255 | 1 | is_object($defaultValue) ? get_class($defaultValue) : gettype($defaultValue) |
|
256 | )); |
||
257 | } |
||
258 | } |
||
259 | |||
260 | 134 | $this->defaultValue = $defaultValue; |
|
261 | 134 | } |
|
262 | |||
263 | /** |
||
264 | * Returns the default value of the option. |
||
265 | * |
||
266 | * @return mixed The default value. |
||
267 | */ |
||
268 | 298 | public function getDefaultValue() |
|
272 | |||
273 | /** |
||
274 | * Returns the name of the option value. |
||
275 | * |
||
276 | * This name can be used as placeholder of the value when displaying the |
||
277 | * option's usage. |
||
278 | * |
||
279 | * @return string The name of the option value. |
||
280 | */ |
||
281 | 25 | public function getValueName() |
|
285 | |||
286 | 273 | private function assertFlagsValid($flags) |
|
287 | { |
||
334 | |||
335 | 262 | private function addDefaultFlags(&$flags) |
|
349 | } |
||
350 |
This check looks for
@param
annotations where the type inferred by our type inference engine differs from the declared type.It makes a suggestion as to what type it considers more descriptive.
Most often this is a case of a parameter that can be null in addition to its declared types.