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 FieldUpload 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 FieldUpload, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
10 | class FieldUpload extends Field implements ExportableField, ImportableField |
||
11 | { |
||
12 | protected static $imageMimeTypes = array( |
||
13 | 'image/gif', |
||
14 | 'image/jpg', |
||
15 | 'image/jpeg', |
||
16 | 'image/pjpeg', |
||
17 | 'image/png', |
||
18 | 'image/x-png' |
||
19 | ); |
||
20 | |||
21 | View Code Duplication | public function __construct() |
|
22 | { |
||
23 | parent::__construct(); |
||
24 | |||
25 | $this->_name = __('File Upload'); |
||
|
|||
26 | $this->_required = true; |
||
27 | |||
28 | $this->set('location', 'sidebar'); |
||
29 | $this->set('required', 'no'); |
||
30 | } |
||
31 | |||
32 | /*------------------------------------------------------------------------- |
||
33 | Definition: |
||
34 | -------------------------------------------------------------------------*/ |
||
35 | |||
36 | public function canFilter() |
||
37 | { |
||
38 | return true; |
||
39 | } |
||
40 | |||
41 | public function canPrePopulate() |
||
42 | { |
||
43 | return true; |
||
44 | } |
||
45 | |||
46 | public function isSortable() |
||
47 | { |
||
48 | return true; |
||
49 | } |
||
50 | |||
51 | public function fetchFilterableOperators() |
||
52 | { |
||
53 | return array( |
||
54 | array( |
||
55 | 'title' => 'is', |
||
56 | 'filter' => ' ', |
||
57 | 'help' => __('Find files that are an exact match for the given string.') |
||
58 | ), |
||
59 | array( |
||
60 | 'title' => 'contains', |
||
61 | 'filter' => 'regexp: ', |
||
62 | 'help' => __('Find files that match the given <a href="%s">MySQL regular expressions</a>.', array( |
||
63 | 'http://dev.mysql.com/doc/mysql/en/regexp.html' |
||
64 | )) |
||
65 | ), |
||
66 | array( |
||
67 | 'title' => 'does not contain', |
||
68 | 'filter' => 'not-regexp: ', |
||
69 | 'help' => __('Find files that do not match the given <a href="%s">MySQL regular expressions</a>.', array( |
||
70 | 'http://dev.mysql.com/doc/mysql/en/regexp.html' |
||
71 | )) |
||
72 | ), |
||
73 | array( |
||
74 | 'title' => 'file type is', |
||
75 | 'filter' => 'mimetype: ', |
||
76 | 'help' => __('Find files that match the given mimetype.') |
||
77 | ), |
||
78 | array( |
||
79 | 'title' => 'size is', |
||
80 | 'filter' => 'size: ', |
||
81 | 'help' => __('Find files that match the given size.') |
||
82 | ) |
||
83 | ); |
||
84 | } |
||
85 | |||
86 | /*------------------------------------------------------------------------- |
||
87 | Setup: |
||
88 | -------------------------------------------------------------------------*/ |
||
89 | |||
90 | public function createTable() |
||
91 | { |
||
92 | return Symphony::Database()->query( |
||
93 | "CREATE TABLE IF NOT EXISTS `tbl_entries_data_" . $this->get('id') . "` ( |
||
94 | `id` int(11) unsigned NOT null auto_increment, |
||
95 | `entry_id` int(11) unsigned NOT null, |
||
96 | `file` varchar(255) default null, |
||
97 | `size` int(11) unsigned null, |
||
98 | `mimetype` varchar(100) default null, |
||
99 | `meta` varchar(255) default null, |
||
100 | PRIMARY KEY (`id`), |
||
101 | UNIQUE KEY `entry_id` (`entry_id`), |
||
102 | KEY `file` (`file`), |
||
103 | KEY `mimetype` (`mimetype`) |
||
104 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;" |
||
105 | ); |
||
106 | } |
||
107 | |||
108 | /*------------------------------------------------------------------------- |
||
109 | Utilities: |
||
110 | -------------------------------------------------------------------------*/ |
||
111 | |||
112 | public function entryDataCleanup($entry_id, $data = null) |
||
113 | { |
||
114 | $file_location = $this->getFilePath($data['file']); |
||
115 | |||
116 | if (is_file($file_location)) { |
||
117 | General::deleteFile($file_location); |
||
118 | } |
||
119 | |||
120 | parent::entryDataCleanup($entry_id); |
||
121 | |||
122 | return true; |
||
123 | } |
||
124 | |||
125 | public static function getMetaInfo($file, $type) |
||
126 | { |
||
127 | $meta = array(); |
||
128 | |||
129 | if (!file_exists($file) || !is_readable($file)) { |
||
130 | return $meta; |
||
131 | } |
||
132 | |||
133 | $meta['creation'] = DateTimeObj::get('c', filemtime($file)); |
||
134 | |||
135 | if (General::in_iarray($type, fieldUpload::$imageMimeTypes) && $array = @getimagesize($file)) { |
||
136 | $meta['width'] = $array[0]; |
||
137 | $meta['height'] = $array[1]; |
||
138 | } |
||
139 | |||
140 | return $meta; |
||
141 | } |
||
142 | |||
143 | public function getFilePath($filename) |
||
144 | { |
||
145 | /** |
||
146 | * Ensure the file exists in the `WORKSPACE` directory |
||
147 | * @link http://getsymphony.com/discuss/issues/view/610/ |
||
148 | */ |
||
149 | $file = WORKSPACE . preg_replace(array('%/+%', '%(^|/)\.\./%', '%\/workspace\/%'), '/', $this->get('destination') . '/' . $filename); |
||
150 | |||
151 | return $file; |
||
152 | } |
||
153 | |||
154 | /*------------------------------------------------------------------------- |
||
155 | Settings: |
||
156 | -------------------------------------------------------------------------*/ |
||
157 | |||
158 | public function displaySettingsPanel(XMLElement &$wrapper, $errors = null) |
||
159 | { |
||
160 | parent::displaySettingsPanel($wrapper, $errors); |
||
161 | |||
162 | // Destination Folder |
||
163 | $ignore = array( |
||
164 | '/workspace/events', |
||
165 | '/workspace/data-sources', |
||
166 | '/workspace/text-formatters', |
||
167 | '/workspace/pages', |
||
168 | '/workspace/utilities' |
||
169 | ); |
||
170 | $directories = General::listDirStructure(WORKSPACE, null, true, DOCROOT, $ignore); |
||
171 | |||
172 | $label = Widget::Label(__('Destination Directory')); |
||
173 | |||
174 | $options = array(); |
||
175 | $options[] = array('/workspace', false, '/workspace'); |
||
176 | |||
177 | if (!empty($directories) && is_array($directories)) { |
||
178 | foreach ($directories as $d) { |
||
179 | $d = '/' . trim($d, '/'); |
||
180 | |||
181 | if (!in_array($d, $ignore)) { |
||
182 | $options[] = array($d, ($this->get('destination') == $d), $d); |
||
183 | } |
||
184 | } |
||
185 | } |
||
186 | |||
187 | $label->appendChild(Widget::Select('fields['.$this->get('sortorder').'][destination]', $options)); |
||
188 | |||
189 | View Code Duplication | if (isset($errors['destination'])) { |
|
190 | $wrapper->appendChild(Widget::Error($label, $errors['destination'])); |
||
191 | } else { |
||
192 | $wrapper->appendChild($label); |
||
193 | } |
||
194 | |||
195 | // Validation rule |
||
196 | $this->buildValidationSelect($wrapper, $this->get('validator'), 'fields['.$this->get('sortorder').'][validator]', 'upload', $errors); |
||
197 | |||
198 | // Requirements and table display |
||
199 | $this->appendStatusFooter($wrapper); |
||
200 | } |
||
201 | |||
202 | public function checkFields(array &$errors, $checkForDuplicates = true) |
||
203 | { |
||
204 | if (is_dir(DOCROOT . $this->get('destination') . '/') === false) { |
||
205 | $errors['destination'] = __('The destination directory, %s, does not exist.', array( |
||
206 | '<code>' . $this->get('destination') . '</code>' |
||
207 | )); |
||
208 | View Code Duplication | } elseif (is_writable(DOCROOT . $this->get('destination') . '/') === false) { |
|
209 | $errors['destination'] = __('The destination directory is not writable.') |
||
210 | . ' ' |
||
211 | . __('Please check permissions on %s.', array( |
||
212 | '<code>' . $this->get('destination') . '</code>' |
||
213 | )); |
||
214 | } |
||
215 | |||
216 | parent::checkFields($errors, $checkForDuplicates); |
||
217 | } |
||
218 | |||
219 | View Code Duplication | public function commit() |
|
220 | { |
||
221 | if (!parent::commit()) { |
||
222 | return false; |
||
223 | } |
||
224 | |||
225 | $id = $this->get('id'); |
||
226 | |||
227 | if ($id === false) { |
||
228 | return false; |
||
229 | } |
||
230 | |||
231 | $fields = array(); |
||
232 | |||
233 | $fields['destination'] = $this->get('destination'); |
||
234 | $fields['validator'] = ($fields['validator'] == 'custom' ? null : $this->get('validator')); |
||
235 | |||
236 | return FieldManager::saveSettings($id, $fields); |
||
237 | } |
||
238 | |||
239 | /*------------------------------------------------------------------------- |
||
240 | Publish: |
||
241 | -------------------------------------------------------------------------*/ |
||
242 | |||
243 | public function displayPublishPanel(XMLElement &$wrapper, $data = null, $flagWithError = null, $fieldnamePrefix = null, $fieldnamePostfix = null, $entry_id = null) |
||
244 | { |
||
245 | if (is_dir(DOCROOT . $this->get('destination') . '/') === false) { |
||
246 | $flagWithError = __('The destination directory, %s, does not exist.', array( |
||
247 | '<code>' . $this->get('destination') . '</code>' |
||
248 | )); |
||
249 | View Code Duplication | } elseif ($flagWithError && is_writable(DOCROOT . $this->get('destination') . '/') === false) { |
|
250 | $flagWithError = __('Destination folder is not writable.') |
||
251 | . ' ' |
||
252 | . __('Please check permissions on %s.', array( |
||
253 | '<code>' . $this->get('destination') . '</code>' |
||
254 | )); |
||
255 | } |
||
256 | |||
257 | $label = Widget::Label($this->get('label')); |
||
258 | $label->setAttribute('class', 'file'); |
||
259 | |||
260 | if ($this->get('required') !== 'yes') { |
||
261 | $label->appendChild(new XMLElement('i', __('Optional'))); |
||
262 | } |
||
263 | |||
264 | $span = new XMLElement('span', null, array('class' => 'frame')); |
||
265 | |||
266 | if (isset($data['file'])) { |
||
267 | $filename = $this->get('destination') . '/' . basename($data['file']); |
||
268 | $file = $this->getFilePath($data['file']); |
||
269 | if (file_exists($file) === false || !is_readable($file)) { |
||
270 | $flagWithError = __('The file uploaded is no longer available. Please check that it exists, and is readable.'); |
||
271 | } |
||
272 | |||
273 | $span->appendChild(new XMLElement('span', Widget::Anchor(preg_replace("![^a-z0-9]+!i", "$0​", $filename), URL . $filename))); |
||
274 | } else { |
||
275 | $filename = null; |
||
276 | } |
||
277 | |||
278 | $span->appendChild(Widget::Input('fields'.$fieldnamePrefix.'['.$this->get('element_name').']'.$fieldnamePostfix, $filename, ($filename ? 'hidden' : 'file'))); |
||
279 | |||
280 | $label->appendChild($span); |
||
281 | |||
282 | if ($flagWithError != null) { |
||
283 | $wrapper->appendChild(Widget::Error($label, $flagWithError)); |
||
284 | } else { |
||
285 | $wrapper->appendChild($label); |
||
286 | } |
||
287 | } |
||
288 | |||
289 | public function validateFilename($file, &$message) |
||
290 | { |
||
291 | if ($this->get('validator') != null) { |
||
292 | $rule = $this->get('validator'); |
||
293 | |||
294 | View Code Duplication | if (General::validateString($file, $rule) === false) { |
|
295 | $message = __('File chosen in ‘%s’ does not match allowable file types for that field.', array( |
||
296 | $this->get('label') |
||
297 | )); |
||
298 | |||
299 | return self::__INVALID_FIELDS__; |
||
300 | } |
||
301 | } |
||
302 | // If the developer did not specified any validator, check for the |
||
303 | // blacklisted file types instead |
||
304 | else { |
||
305 | $blacklist = Symphony::Configuration()->get('upload_blacklist', 'admin'); |
||
306 | |||
307 | View Code Duplication | if (!empty($blacklist) && General::validateString($file, $blacklist)) { |
|
308 | $message = __('File chosen in ‘%s’ is blacklisted for that field.', array( |
||
309 | $this->get('label') |
||
310 | )); |
||
311 | |||
312 | return self::__INVALID_FIELDS__; |
||
313 | } |
||
314 | } |
||
315 | |||
316 | return self::__OK__; |
||
317 | } |
||
318 | |||
319 | public function checkPostFieldData($data, &$message, $entry_id = null) |
||
403 | |||
404 | public function processRawFieldData($data, &$status, &$message = null, $simulate = false, $entry_id = null) |
||
405 | { |
||
406 | $status = self::__OK__; |
||
407 | |||
408 | // No file given, save empty data: |
||
409 | if ($data === null) { |
||
410 | return array( |
||
411 | 'file' => null, |
||
412 | 'mimetype' => null, |
||
413 | 'size' => null, |
||
414 | 'meta' => null |
||
415 | ); |
||
416 | } |
||
417 | |||
553 | |||
554 | protected function getCurrentValues($entry_id) |
||
566 | |||
567 | /*------------------------------------------------------------------------- |
||
568 | Output: |
||
569 | -------------------------------------------------------------------------*/ |
||
570 | |||
571 | public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null) |
||
600 | |||
601 | public function prepareTableValue($data, XMLElement $link = null, $entry_id = null) |
||
619 | |||
620 | public function prepareTextValue($data, $entry_id = null) |
||
627 | |||
628 | public function prepareAssociationsDrawerXMLElement(Entry $e, array $parent_association, $prepopulate = '') |
||
636 | |||
637 | /*------------------------------------------------------------------------- |
||
638 | Import: |
||
639 | -------------------------------------------------------------------------*/ |
||
640 | |||
641 | public function getImportModes() |
||
648 | |||
649 | View Code Duplication | public function prepareImportValue($data, $mode, $entry_id = null) |
|
662 | |||
663 | /*------------------------------------------------------------------------- |
||
664 | Export: |
||
665 | -------------------------------------------------------------------------*/ |
||
666 | |||
667 | /** |
||
668 | * Return a list of supported export modes for use with `prepareExportValue`. |
||
669 | * |
||
670 | * @return array |
||
671 | */ |
||
672 | public function getExportModes() |
||
680 | |||
681 | /** |
||
682 | * Give the field some data and ask it to return a value using one of many |
||
683 | * possible modes. |
||
684 | * |
||
685 | * @param mixed $data |
||
686 | * @param integer $mode |
||
687 | * @param integer $entry_id |
||
688 | * @return array|string|null |
||
689 | */ |
||
690 | public function prepareExportValue($data, $mode, $entry_id = null) |
||
720 | |||
721 | /*------------------------------------------------------------------------- |
||
722 | Filtering: |
||
723 | -------------------------------------------------------------------------*/ |
||
724 | |||
725 | public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation = false) |
||
777 | |||
778 | /*------------------------------------------------------------------------- |
||
779 | Sorting: |
||
780 | -------------------------------------------------------------------------*/ |
||
781 | |||
782 | public function buildSortingSQL(&$joins, &$where, &$sort, $order = 'ASC') |
||
799 | |||
800 | /*------------------------------------------------------------------------- |
||
801 | Events: |
||
802 | -------------------------------------------------------------------------*/ |
||
803 | |||
804 | View Code Duplication | public function getExampleFormMarkup() |
|
811 | } |
||
812 |
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: