Complex classes like DropdownField 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 DropdownField, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
84 | class DropdownField extends FormField { |
||
85 | |||
86 | /** |
||
87 | * @var array|ArrayAccess $source Associative or numeric array of all dropdown items, |
||
88 | * with array key as the submitted field value, and the array value as a |
||
89 | * natural language description shown in the interface element. |
||
90 | */ |
||
91 | protected $source; |
||
92 | |||
93 | /** |
||
94 | * @var boolean $isSelected Determines if the field was selected |
||
95 | * at the time it was rendered, so if {@link $value} matches on of the array |
||
96 | * values specified in {@link $source} |
||
97 | */ |
||
98 | protected $isSelected; |
||
99 | |||
100 | /** |
||
101 | * @var boolean $hasEmptyDefault Show the first <option> element as |
||
102 | * empty (not having a value), with an optional label defined through |
||
103 | * {@link $emptyString}. By default, the <select> element will be |
||
104 | * rendered with the first option from {@link $source} selected. |
||
105 | */ |
||
106 | protected $hasEmptyDefault = false; |
||
107 | |||
108 | /** |
||
109 | * @var string $emptyString The title shown for an empty default selection, |
||
110 | * e.g. "Select...". |
||
111 | */ |
||
112 | protected $emptyString = ''; |
||
113 | |||
114 | /** |
||
115 | * @var array $disabledItems The keys for items that should be disabled (greyed out) in the dropdown |
||
116 | */ |
||
117 | protected $disabledItems = array(); |
||
118 | |||
119 | /** |
||
120 | * @param string $name The field name |
||
121 | * @param string $title The field title |
||
122 | * @param array|ArrayAccess $source A map of the dropdown items |
||
123 | * @param string $value The current value |
||
124 | * @param Form $form The parent form |
||
125 | */ |
||
126 | public function __construct($name, $title=null, $source=array(), $value='', $form=null, $emptyString=null) { |
||
127 | $this->setSource($source); |
||
128 | |||
129 | if($emptyString === true) { |
||
130 | Deprecation::notice('4.0', |
||
131 | 'Please use setHasEmptyDefault(true) instead of passing a boolean true $emptyString argument', |
||
132 | Deprecation::SCOPE_GLOBAL); |
||
133 | } |
||
134 | if(is_string($emptyString)) { |
||
135 | Deprecation::notice('4.0', 'Please use setEmptyString() instead of passing a string emptyString argument.', |
||
136 | Deprecation::SCOPE_GLOBAL); |
||
137 | } |
||
138 | |||
139 | if($emptyString) $this->setHasEmptyDefault(true); |
||
140 | if(is_string($emptyString)) $this->setEmptyString($emptyString); |
||
141 | |||
142 | parent::__construct($name, ($title===null) ? $name : $title, $value, $form); |
||
|
|||
143 | } |
||
144 | |||
145 | /** |
||
146 | * @param array $properties |
||
147 | * @return HTMLText |
||
148 | */ |
||
149 | public function Field($properties = array()) { |
||
150 | $source = $this->getSource(); |
||
151 | $options = array(); |
||
152 | |||
153 | if ($this->getHasEmptyDefault()) { |
||
154 | $selected = ($this->value === '' || $this->value === null); |
||
155 | $disabled = (in_array('', $this->disabledItems, true)) ? 'disabled' : false; |
||
156 | |||
157 | $options[] = new ArrayData(array( |
||
158 | 'Value' => '', |
||
159 | 'Title' => $this->getEmptyString(), |
||
160 | 'Selected' => $selected, |
||
161 | 'Disabled' => $disabled |
||
162 | )); |
||
163 | } |
||
164 | |||
165 | if ($source) { |
||
166 | foreach($source as $value => $title) { |
||
167 | $selected = false; |
||
168 | if($value === '' && ($this->value === '' || $this->value === null)) { |
||
169 | $selected = true; |
||
170 | } else { |
||
171 | // check against value, fallback to a type check comparison when !value |
||
172 | if($value) { |
||
173 | $selected = ($value == $this->value); |
||
174 | } else { |
||
175 | $selected = ($value === $this->value) || (((string) $value) === ((string) $this->value)); |
||
176 | } |
||
177 | |||
178 | $this->isSelected = $selected; |
||
179 | } |
||
180 | |||
181 | $disabled = false; |
||
182 | if(in_array($value, $this->disabledItems) && $title != $this->emptyString ){ |
||
183 | $disabled = 'disabled'; |
||
184 | } |
||
185 | |||
186 | $options[] = new ArrayData(array( |
||
187 | 'Title' => $title, |
||
188 | 'Value' => $value, |
||
189 | 'Selected' => $selected, |
||
190 | 'Disabled' => $disabled, |
||
191 | )); |
||
192 | } |
||
193 | } |
||
194 | |||
195 | $properties = array_merge($properties, array( |
||
196 | 'Options' => new ArrayList($options) |
||
197 | )); |
||
198 | |||
199 | return parent::Field($properties); |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Mark certain elements as disabled, regardless of the |
||
204 | * {@link setDisabled()} settings. |
||
205 | * |
||
206 | * @param array $items Collection of array keys, as defined in the $source array |
||
207 | */ |
||
208 | public function setDisabledItems($items) { |
||
209 | $this->disabledItems = $items; |
||
210 | |||
211 | return $this; |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * @return array |
||
216 | */ |
||
217 | public function getDisabledItems() { |
||
220 | |||
221 | /** |
||
222 | * @return array |
||
223 | */ |
||
224 | public function getAttributes() { |
||
225 | return array_merge( |
||
226 | parent::getAttributes(), |
||
227 | array( |
||
228 | 'type' => null, |
||
229 | 'value' => null |
||
230 | ) |
||
231 | ); |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * @return boolean |
||
236 | */ |
||
237 | public function isSelected() { |
||
240 | |||
241 | /** |
||
242 | * Gets the source array including any empty default values. |
||
243 | * |
||
244 | * @return array|ArrayAccess |
||
245 | */ |
||
246 | public function getSource() { |
||
249 | |||
250 | /** |
||
251 | * @param array|ArrayAccess $source |
||
252 | */ |
||
253 | public function setSource($source) { |
||
254 | $this->source = $source; |
||
255 | |||
256 | return $this; |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @param boolean $bool |
||
261 | */ |
||
262 | public function setHasEmptyDefault($bool) { |
||
263 | $this->hasEmptyDefault = $bool; |
||
264 | |||
265 | return $this; |
||
266 | } |
||
267 | |||
268 | /** |
||
269 | * @return boolean |
||
270 | */ |
||
271 | public function getHasEmptyDefault() { |
||
274 | |||
275 | /** |
||
276 | * Set the default selection label, e.g. "select...". |
||
277 | * |
||
278 | * Defaults to an empty string. Automatically sets {@link $hasEmptyDefault} |
||
279 | * to true. |
||
280 | * |
||
281 | * @param string $str |
||
282 | */ |
||
283 | public function setEmptyString($str) { |
||
284 | $this->setHasEmptyDefault(true); |
||
285 | $this->emptyString = $str; |
||
286 | |||
287 | return $this; |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * @return string |
||
292 | */ |
||
293 | public function getEmptyString() { |
||
296 | |||
297 | /** |
||
298 | * @return LookupField |
||
299 | */ |
||
300 | public function performReadonlyTransformation() { |
||
301 | $field = $this->castedCopy('LookupField'); |
||
302 | $field->setSource($this->getSource()); |
||
303 | $field->setReadonly(true); |
||
307 | |||
308 | /** |
||
309 | * Get the source of this field as an array |
||
310 | * |
||
311 | * @return array |
||
312 | */ |
||
313 | public function getSourceAsArray() |
||
326 | |||
327 | /** |
||
328 | * Validate this field |
||
329 | * |
||
330 | * @param Validator $validator |
||
331 | * @return bool |
||
332 | */ |
||
333 | public function validate($validator) { |
||
354 | |||
355 | /** |
||
356 | * Returns another instance of this field, but "cast" to a different class. |
||
357 | * |
||
358 | * @see FormField::castedCopy() |
||
359 | * |
||
360 | * @param String $classOrCopy |
||
361 | * @return FormField |
||
362 | */ |
||
363 | public function castedCopy($classOrCopy) { |
||
370 | } |
||
371 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.