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 tar 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 tar, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
91 | class tar |
||
92 | { |
||
93 | /** |
||
94 | * *#@+ |
||
95 | * Unprocessed Archive Information |
||
96 | */ |
||
97 | public $filename; |
||
98 | public $isGzipped; |
||
99 | public $tar_file; |
||
100 | /** |
||
101 | * *#@- |
||
102 | */ |
||
103 | |||
104 | /** |
||
105 | * *#@+ |
||
106 | * Processed Archive Information |
||
107 | */ |
||
108 | public $files; |
||
109 | public $directories; |
||
110 | public $numFiles; |
||
111 | public $numDirectories; |
||
112 | |||
113 | /** |
||
114 | * *#@- |
||
115 | */ |
||
116 | |||
117 | /** |
||
118 | * Computes the unsigned Checksum of a file's header |
||
119 | * to try to ensure valid file |
||
120 | * |
||
121 | * @param string $bytestring |
||
122 | * @return int|string |
||
123 | * @access private |
||
124 | */ |
||
125 | private function __computeUnsignedChecksum($bytestring) |
||
126 | { |
||
127 | $unsigned_chksum = ''; |
||
128 | for ($i = 0; $i < 512; ++$i) { |
||
129 | $unsigned_chksum += ord($bytestring[$i]); |
||
130 | } |
||
131 | for ($i = 0; $i < 8; ++$i) { |
||
132 | $unsigned_chksum -= ord($bytestring[148 + $i]); |
||
133 | $unsigned_chksum += ord(' ') * 8; |
||
134 | } |
||
135 | return $unsigned_chksum; |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * Converts a NULL padded string to a non-NULL padded string |
||
140 | * |
||
141 | * @param string $string |
||
142 | * @return string |
||
143 | * @access private |
||
144 | */ |
||
145 | private function __parseNullPaddedString($string) |
||
146 | { |
||
147 | $position = strpos($string, chr(0)); |
||
148 | return substr($string, 0, $position); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * This function parses the current TAR file |
||
153 | * |
||
154 | * @return bool always TRUE |
||
155 | * @access private |
||
156 | */ |
||
157 | private function __parseTar() |
||
158 | { |
||
159 | // Read Files from archive |
||
160 | $tar_length = strlen($this->tar_file); |
||
161 | $main_offset = 0; |
||
162 | $this->numFiles = 0; |
||
163 | while ($main_offset < $tar_length) { |
||
164 | // If we read a block of 512 nulls, we are at the end of the archive |
||
165 | if (substr($this->tar_file, $main_offset, 512) == str_repeat(chr(0), 512)) { |
||
166 | break; |
||
167 | } |
||
168 | // Parse file name |
||
169 | $file_name = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset, 100)); |
||
170 | // Parse the file mode |
||
171 | $file_mode = substr($this->tar_file, $main_offset + 100, 8); |
||
172 | // Parse the file user ID |
||
173 | $file_uid = octdec(substr($this->tar_file, $main_offset + 108, 8)); |
||
174 | // Parse the file group ID |
||
175 | $file_gid = octdec(substr($this->tar_file, $main_offset + 116, 8)); |
||
176 | // Parse the file size |
||
177 | $file_size = octdec(substr($this->tar_file, $main_offset + 124, 12)); |
||
178 | // Parse the file update time - unix timestamp format |
||
179 | $file_time = octdec(substr($this->tar_file, $main_offset + 136, 12)); |
||
180 | // Parse Checksum |
||
181 | $file_chksum = octdec(substr($this->tar_file, $main_offset + 148, 6)); |
||
182 | // Parse user name |
||
183 | $file_uname = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset + 265, 32)); |
||
184 | // Parse Group name |
||
185 | $file_gname = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset + 297, 32)); |
||
186 | // Make sure our file is valid |
||
187 | if ($this->__computeUnsignedChecksum(substr($this->tar_file, $main_offset, 512)) != $file_chksum) { |
||
188 | return false; |
||
189 | } |
||
190 | // Parse File Contents |
||
191 | $file_contents = substr($this->tar_file, $main_offset + 512, $file_size); |
||
192 | |||
193 | /** |
||
194 | * ### Unused Header Information ### |
||
195 | * $activeFile["typeflag"] = substr($this->tar_file,$main_offset + 156,1); |
||
196 | * $activeFile["linkname"] = substr($this->tar_file,$main_offset + 157,100); |
||
197 | * $activeFile["magic"] = substr($this->tar_file,$main_offset + 257,6); |
||
198 | * $activeFile["version"] = substr($this->tar_file,$main_offset + 263,2); |
||
199 | * $activeFile["devmajor"] = substr($this->tar_file,$main_offset + 329,8); |
||
200 | * $activeFile["devminor"] = substr($this->tar_file,$main_offset + 337,8); |
||
201 | * $activeFile["prefix"] = substr($this->tar_file,$main_offset + 345,155); |
||
202 | * $activeFile["endheader"] = substr($this->tar_file,$main_offset + 500,12); |
||
203 | */ |
||
204 | |||
205 | if ($file_size > 0) { |
||
206 | // Increment number of files |
||
207 | $this->numFiles++; |
||
208 | // Create us a new file in our array |
||
209 | $activeFile =& $this->files[]; |
||
210 | // Asign Values |
||
211 | $activeFile["name"] = $file_name; |
||
212 | $activeFile["mode"] = $file_mode; |
||
213 | $activeFile["size"] = $file_size; |
||
214 | $activeFile["time"] = $file_time; |
||
215 | $activeFile["user_id"] = $file_uid; |
||
216 | $activeFile["group_id"] = $file_gid; |
||
217 | $activeFile["user_name"] = $file_uname; |
||
218 | $activeFile["group_name"] = $file_gname; |
||
219 | $activeFile["checksum"] = $file_chksum; |
||
220 | $activeFile["file"] = $file_contents; |
||
221 | } else { |
||
222 | // Increment number of directories |
||
223 | $this->numDirectories++; |
||
224 | // Create a new directory in our array |
||
225 | $activeDir =& $this->directories[]; |
||
226 | // Assign values |
||
227 | $activeDir["name"] = $file_name; |
||
228 | $activeDir["mode"] = $file_mode; |
||
229 | $activeDir["time"] = $file_time; |
||
230 | $activeDir["user_id"] = $file_uid; |
||
231 | $activeDir["group_id"] = $file_gid; |
||
232 | $activeDir["user_name"] = $file_uname; |
||
233 | $activeDir["group_name"] = $file_gname; |
||
234 | $activeDir["checksum"] = $file_chksum; |
||
235 | } |
||
236 | // Move our offset the number of blocks we have processed |
||
237 | $main_offset += 512 + (ceil($file_size / 512) * 512); |
||
238 | } |
||
239 | |||
240 | return true; |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * Read a non gzipped tar file in for processing. |
||
245 | * |
||
246 | * @param string $filename full filename |
||
247 | * @return bool always TRUE |
||
248 | * @access private |
||
249 | */ |
||
250 | private function __readTar($filename = '') |
||
251 | { |
||
252 | // Set the filename to load |
||
253 | if (!$filename) { |
||
254 | $filename = $this->filename; |
||
255 | } |
||
256 | // Read in the TAR file |
||
257 | $fp = fopen($filename, 'rb'); |
||
258 | $this->tar_file = fread($fp, filesize($filename)); |
||
259 | fclose($fp); |
||
260 | |||
261 | if ($this->tar_file[0] == chr(31) && $this->tar_file[1] == chr(139) && $this->tar_file[2] == chr(8)) { |
||
262 | if (!function_exists('gzinflate')) { |
||
263 | return false; |
||
264 | } |
||
265 | $this->isGzipped = true; |
||
266 | $this->tar_file = gzinflate(substr($this->tar_file, 10, -4)); |
||
267 | } |
||
268 | // Parse the TAR file |
||
269 | $this->__parseTar(); |
||
270 | |||
271 | return true; |
||
272 | } |
||
273 | |||
274 | /** |
||
275 | * Generates a TAR file from the processed data |
||
276 | * |
||
277 | * @return bool always TRUE |
||
278 | * @access private |
||
279 | */ |
||
280 | private function __generateTar() |
||
281 | { |
||
282 | // Clear any data currently in $this->tar_file |
||
283 | unset($this->tar_file); |
||
284 | // Generate Records for each directory, if we have directories |
||
285 | if ($this->numDirectories > 0) { |
||
286 | foreach ($this->directories as $key => $information) { |
||
287 | $header = ''; |
||
288 | // Generate tar header for this directory |
||
289 | // Filename, Permissions, UID, GID, size, Time, checksum, typeflag, linkname, magic, version, user name, group name, devmajor, devminor, prefix, end |
||
290 | $header .= str_pad($information["name"], 100, chr(0)); |
||
291 | $header .= str_pad(decoct($information["mode"]), 7, "0", STR_PAD_LEFT) . chr(0); |
||
292 | $header .= str_pad(decoct($information["user_id"]), 7, "0", STR_PAD_LEFT) . chr(0); |
||
293 | $header .= str_pad(decoct($information["group_id"]), 7, "0", STR_PAD_LEFT) . chr(0); |
||
294 | $header .= str_pad(decoct(0), 11, "0", STR_PAD_LEFT) . chr(0); |
||
295 | $header .= str_pad(decoct($information["time"]), 11, "0", STR_PAD_LEFT) . chr(0); |
||
296 | $header .= str_repeat(" ", 8); |
||
297 | $header .= "5"; |
||
298 | $header .= str_repeat(chr(0), 100); |
||
299 | $header .= str_pad("ustar", 6, chr(32)); |
||
300 | $header .= chr(32) . chr(0); |
||
301 | $header .= str_pad("", 32, chr(0)); |
||
302 | $header .= str_pad("", 32, chr(0)); |
||
303 | $header .= str_repeat(chr(0), 8); |
||
304 | $header .= str_repeat(chr(0), 8); |
||
305 | $header .= str_repeat(chr(0), 155); |
||
306 | $header .= str_repeat(chr(0), 12); |
||
307 | // Compute header checksum |
||
308 | $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT); |
||
309 | View Code Duplication | for ($i = 0; $i < 6; ++$i) { |
|
310 | $header[(148 + $i)] = substr($checksum, $i, 1); |
||
311 | } |
||
312 | $header[154] = chr(0); |
||
313 | $header[155] = chr(32); |
||
314 | // Add new tar formatted data to tar file contents |
||
315 | $this->tar_file .= $header; |
||
316 | } |
||
317 | } |
||
318 | // Generate Records for each file, if we have files (We should...) |
||
319 | if ($this->numFiles > 0) { |
||
320 | $this->tar_file = ''; |
||
321 | foreach ($this->files as $key => $information) { |
||
322 | $header = ''; |
||
323 | // Generate the TAR header for this file |
||
324 | // Filename, Permissions, UID, GID, size, Time, checksum, typeflag, linkname, magic, version, user name, group name, devmajor, devminor, prefix, end |
||
325 | $header .= str_pad($information["name"], 100, chr(0)); |
||
326 | $header .= str_pad(decoct($information["mode"]), 7, "0", STR_PAD_LEFT) . chr(0); |
||
327 | $header .= str_pad(decoct($information["user_id"]), 7, "0", STR_PAD_LEFT) . chr(0); |
||
328 | $header .= str_pad(decoct($information["group_id"]), 7, "0", STR_PAD_LEFT) . chr(0); |
||
329 | $header .= str_pad(decoct($information["size"]), 11, "0", STR_PAD_LEFT) . chr(0); |
||
330 | $header .= str_pad(decoct($information["time"]), 11, "0", STR_PAD_LEFT) . chr(0); |
||
331 | $header .= str_repeat(" ", 8); |
||
332 | $header .= "0"; |
||
333 | $header .= str_repeat(chr(0), 100); |
||
334 | $header .= str_pad("ustar", 6, chr(32)); |
||
335 | $header .= chr(32) . chr(0); |
||
336 | $header .= str_pad($information["user_name"], 32, chr(0)); // How do I get a file's user name from PHP? |
||
337 | $header .= str_pad($information["group_name"], 32, chr(0)); // How do I get a file's group name from PHP? |
||
338 | $header .= str_repeat(chr(0), 8); |
||
339 | $header .= str_repeat(chr(0), 8); |
||
340 | $header .= str_repeat(chr(0), 155); |
||
341 | $header .= str_repeat(chr(0), 12); |
||
342 | // Compute header checksum |
||
343 | $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT); |
||
344 | View Code Duplication | for ($i = 0; $i < 6; ++$i) { |
|
345 | $header[(148 + $i)] = substr($checksum, $i, 1); |
||
346 | } |
||
347 | $header[154] = chr(0); |
||
348 | $header[155] = chr(32); |
||
349 | // Pad file contents to byte count divisible by 512 |
||
350 | $file_contents = str_pad($information["file"], (ceil($information["size"] / 512) * 512), chr(0)); |
||
351 | // Add new tar formatted data to tar file contents |
||
352 | $this->tar_file .= $header . $file_contents; |
||
353 | } |
||
354 | } |
||
355 | // Add 512 bytes of NULLs to designate EOF |
||
356 | $this->tar_file .= str_repeat(chr(0), 512); |
||
357 | return true; |
||
358 | } |
||
359 | |||
360 | /** |
||
361 | * Open a TAR file |
||
362 | * |
||
363 | * @param string $filename |
||
364 | * @return bool |
||
365 | */ |
||
366 | public function openTAR($filename) |
||
367 | { |
||
368 | // Clear any values from previous tar archives |
||
369 | unset($this->filename); |
||
370 | unset($this->isGzipped); |
||
371 | unset($this->tar_file); |
||
372 | unset($this->files); |
||
373 | unset($this->directories); |
||
374 | unset($this->numFiles); |
||
375 | unset($this->numDirectories); |
||
376 | // If the tar file doesn't exist... |
||
377 | if (!XoopsLoad::fileExists($filename)) { |
||
378 | return false; |
||
379 | } |
||
380 | |||
381 | $this->filename = $filename; |
||
382 | // Parse this file |
||
383 | $this->__readTar(); |
||
384 | return true; |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * Appends a tar file to the end of the currently opened tar file. |
||
389 | * |
||
390 | * @param string $filename |
||
391 | * @return bool |
||
392 | */ |
||
393 | public function appendTar($filename) |
||
394 | { |
||
395 | // If the tar file doesn't exist... |
||
396 | if (!XoopsLoad::fileExists($filename)) { |
||
397 | return false; |
||
398 | } |
||
399 | $this->__readTar($filename); |
||
400 | return true; |
||
401 | } |
||
402 | |||
403 | /** |
||
404 | * Retrieves information about a file in the current tar archive |
||
405 | * |
||
406 | * @param string $filename |
||
407 | * @return string FALSE on fail |
||
408 | */ |
||
409 | View Code Duplication | public function getFile($filename) |
|
420 | |||
421 | /** |
||
422 | * Retrieves information about a directory in the current tar archive |
||
423 | * |
||
424 | * @param string $dirname |
||
425 | * @return string FALSE on fail |
||
426 | */ |
||
427 | View Code Duplication | public function getDirectory($dirname) |
|
438 | |||
439 | /** |
||
440 | * Check if this tar archive contains a specific file |
||
441 | * |
||
442 | * @param string $filename |
||
443 | * @return bool |
||
444 | */ |
||
445 | View Code Duplication | public function containsFile($filename) |
|
456 | |||
457 | /** |
||
458 | * Check if this tar archive contains a specific directory |
||
459 | * |
||
460 | * @param string $dirname |
||
461 | * @return bool |
||
462 | */ |
||
463 | View Code Duplication | public function containsDirectory($dirname) |
|
474 | |||
475 | /** |
||
476 | * Add a directory to this tar archive |
||
477 | * |
||
478 | * @param string $dirname |
||
479 | * @return bool |
||
480 | */ |
||
481 | public function addDirectory($dirname) |
||
482 | { |
||
483 | if (!XoopsLoad::fileExists($dirname)) { |
||
484 | return false; |
||
485 | } |
||
486 | // Get directory information |
||
487 | $file_information = stat($dirname); |
||
488 | // Add directory to processed data |
||
489 | $this->numDirectories++; |
||
490 | $activeDir =& $this->directories[]; |
||
491 | $activeDir['name'] = $dirname; |
||
492 | $activeDir['mode'] = $file_information['mode']; |
||
493 | $activeDir['time'] = $file_information['time']; |
||
494 | $activeDir['user_id'] = $file_information['uid']; |
||
495 | $activeDir['group_id'] = $file_information['gid']; |
||
496 | $activeDir['checksum'] = isset($checksum) ? $checksum : ''; |
||
497 | |||
498 | return true; |
||
499 | } |
||
500 | |||
501 | /** |
||
502 | * Add a file to the tar archive |
||
503 | * |
||
504 | * @param string $filename |
||
505 | * @param boolean $binary Binary file? |
||
506 | * @return bool |
||
507 | */ |
||
508 | public function addFile($filename, $binary = false) |
||
509 | { |
||
510 | // Make sure the file we are adding exists! |
||
511 | if (!XoopsLoad::fileExists($filename)) { |
||
512 | return false; |
||
513 | } |
||
514 | // Make sure there are no other files in the archive that have this same filename |
||
515 | if ($this->containsFile($filename)) { |
||
516 | return false; |
||
517 | } |
||
518 | // Get file information |
||
519 | $file_information = stat($filename); |
||
520 | // Read in the file's contents |
||
521 | if (!$binary) { |
||
522 | $fp = fopen($filename, 'r'); |
||
523 | } else { |
||
524 | $fp = fopen($filename, 'rb'); |
||
525 | } |
||
526 | $file_contents = fread($fp, filesize($filename)); |
||
527 | fclose($fp); |
||
528 | // Add file to processed data |
||
529 | $this->numFiles++; |
||
530 | $activeFile =& $this->files[]; |
||
531 | $activeFile['name'] = $filename; |
||
532 | $activeFile['mode'] = $file_information['mode']; |
||
533 | $activeFile['user_id'] = $file_information['uid']; |
||
534 | $activeFile['group_id'] = $file_information['gid']; |
||
535 | $activeFile['size'] = $file_information['size']; |
||
536 | $activeFile['time'] = $file_information['mtime']; |
||
537 | $activeFile['checksum'] = isset($checksum) ? $checksum : ''; |
||
538 | $activeFile['user_name'] = ''; |
||
539 | $activeFile['group_name'] = ''; |
||
540 | $activeFile['file'] = trim($file_contents); |
||
541 | return true; |
||
542 | } |
||
543 | |||
544 | /** |
||
545 | * Remove a file from the tar archive |
||
546 | * |
||
547 | * @param string $filename |
||
548 | * @return bool |
||
549 | */ |
||
550 | public function removeFile($filename) |
||
563 | |||
564 | /** |
||
565 | * Remove a directory from the tar archive |
||
566 | * |
||
567 | * @param string $dirname |
||
568 | * @return bool |
||
569 | */ |
||
570 | public function removeDirectory($dirname) |
||
583 | |||
584 | /** |
||
585 | * Write the currently loaded tar archive to disk |
||
586 | * |
||
587 | * @return bool |
||
588 | */ |
||
589 | public function saveTar() |
||
590 | { |
||
591 | if (!$this->filename) { |
||
592 | return false; |
||
593 | } |
||
594 | // Write tar to current file using specified gzip compression |
||
595 | $this->toTar($this->filename, $this->isGzipped); |
||
596 | return true; |
||
597 | } |
||
598 | |||
599 | /** |
||
600 | * Saves tar archive to a different file than the current file |
||
601 | * |
||
602 | * @param string $filename |
||
603 | * @param bool $useGzip Use GZ compression? |
||
604 | * @return bool |
||
605 | */ |
||
606 | public function toTar($filename, $useGzip) |
||
607 | { |
||
608 | if (!$filename) { |
||
609 | return false; |
||
610 | } |
||
611 | // Encode processed files into TAR file format |
||
612 | $this->__generateTar(); |
||
613 | // GZ Compress the data if we need to |
||
614 | View Code Duplication | if ($useGzip) { |
|
615 | // Make sure we have gzip support |
||
616 | if (!function_exists('gzencode')) { |
||
617 | return false; |
||
618 | } |
||
619 | $file = gzencode($this->tar_file); |
||
620 | } else { |
||
621 | $file = $this->tar_file; |
||
622 | } |
||
623 | // Write the TAR file |
||
624 | $fp = fopen($filename, 'wb'); |
||
625 | fwrite($fp, $file); |
||
626 | fclose($fp); |
||
627 | return true; |
||
628 | } |
||
629 | |||
630 | /** |
||
631 | * Sends tar archive to stdout |
||
632 | * |
||
633 | * @param string $filename |
||
634 | * @param bool $useGzip Use GZ compression? |
||
635 | * @return string |
||
636 | */ |
||
637 | public function toTarOutput($filename, $useGzip) |
||
656 | } |
||
657 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.