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 Renderer 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 Renderer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
15 | class Renderer extends AbstractExport |
||
16 | { |
||
17 | protected $allowedColumnTypes = [ |
||
18 | Type\DateTime::class, |
||
19 | Type\Image::class, |
||
20 | Type\Number::class, |
||
21 | Type\PhpArray::class, |
||
22 | Type\PhpString::class, |
||
23 | ]; |
||
24 | |||
25 | /** |
||
26 | * @var TCPDF |
||
27 | */ |
||
28 | protected $pdf; |
||
29 | |||
30 | /** |
||
31 | * @var Alignment |
||
32 | */ |
||
33 | protected $alignment = 'L'; |
||
34 | |||
35 | private $columnsPositionX = []; |
||
36 | |||
37 | public function getName() |
||
38 | { |
||
39 | return 'TCPDF'; |
||
40 | } |
||
41 | |||
42 | public function isExport() |
||
43 | { |
||
44 | return true; |
||
45 | } |
||
46 | |||
47 | public function isHtml() |
||
48 | { |
||
49 | return false; |
||
50 | } |
||
51 | |||
52 | public function execute() |
||
53 | { |
||
54 | $pdf = $this->getPdf(); |
||
55 | $pdf->AddPage(); |
||
56 | |||
57 | $cols = $this->getColumnsToExport(); |
||
58 | $this->calculateColumnWidth($cols); |
||
59 | |||
60 | /* |
||
61 | * Display used filters etc... |
||
62 | */ |
||
63 | // @todo |
||
64 | |||
65 | $this->printGrid(); |
||
66 | |||
67 | return $this->saveAndSend(); |
||
|
|||
68 | } |
||
69 | |||
70 | protected function printGrid() |
||
71 | { |
||
72 | $pdf = $this->getPdf(); |
||
73 | |||
74 | /* |
||
75 | * Print the header |
||
76 | */ |
||
77 | $this->printTableHeader(); |
||
78 | |||
79 | /* |
||
80 | * Write data |
||
81 | */ |
||
82 | $pageHeight = $pdf->getPageHeight(); |
||
83 | $pageHeight -= 10; |
||
84 | |||
85 | foreach ($this->getData() as $row) { |
||
86 | $rowHeight = $this->getRowHeight($row); |
||
87 | $y = $pdf->GetY(); |
||
88 | |||
89 | $usedHeight = $y + $rowHeight; |
||
90 | |||
91 | if ($usedHeight > $pageHeight) { |
||
92 | // Height is more than the pageHeight -> create a new page |
||
93 | if ($rowHeight < $pageHeight) { |
||
94 | // If the row height is more than the page height, than we would have a problem, if we add a new page |
||
95 | // because it will overflow anyway... |
||
96 | $pdf->AddPage(); |
||
97 | |||
98 | $this->printTableHeader(); |
||
99 | } |
||
100 | } |
||
101 | |||
102 | $this->printTableRow($row, $rowHeight); |
||
103 | } |
||
104 | } |
||
105 | |||
106 | protected function saveAndSend() |
||
107 | { |
||
108 | $pdf = $this->getPdf(); |
||
109 | |||
110 | $options = $this->getOptions(); |
||
111 | $optionsExport = $options['settings']['export']; |
||
112 | |||
113 | $path = $optionsExport['path']; |
||
114 | $saveFilename = date('Y-m-d_H-i-s').$this->getCacheId().'.pdf'; |
||
115 | $pdf->Output($path.'/'.$saveFilename, 'F'); |
||
116 | |||
117 | $response = new ResponseStream(); |
||
118 | $response->setStream(fopen($path.'/'.$saveFilename, 'r')); |
||
119 | |||
120 | $headers = new Headers(); |
||
121 | $headers->addHeaders([ |
||
122 | 'Content-Type' => [ |
||
123 | 'application/force-download', |
||
124 | 'application/octet-stream', |
||
125 | 'application/download', |
||
126 | ], |
||
127 | 'Content-Length' => filesize($path.'/'.$saveFilename), |
||
128 | 'Content-Disposition' => 'attachment;filename='.$this->getFilename().'.pdf', |
||
129 | 'Cache-Control' => 'must-revalidate', |
||
130 | 'Pragma' => 'no-cache', |
||
131 | 'Expires' => 'Thu, 1 Jan 1970 00:00:00 GMT', |
||
132 | ]); |
||
133 | |||
134 | $response->setHeaders($headers); |
||
135 | |||
136 | return $response; |
||
137 | } |
||
138 | |||
139 | protected function initPdf() |
||
140 | { |
||
141 | $optionsRenderer = $this->getOptionsRenderer(); |
||
142 | |||
143 | $papersize = $optionsRenderer['papersize']; |
||
144 | $orientation = $optionsRenderer['orientation']; |
||
145 | if ('landscape' == $orientation) { |
||
146 | $orientation = 'L'; |
||
147 | } else { |
||
148 | $orientation = 'P'; |
||
149 | } |
||
150 | |||
151 | $pdf = new TCPDF($orientation, 'mm', $papersize); |
||
152 | |||
153 | $margins = $optionsRenderer['margins']; |
||
154 | $pdf->SetMargins($margins['left'], $margins['top'], $margins['right']); |
||
155 | $pdf->SetAutoPageBreak(true, $margins['bottom']); |
||
156 | $pdf->setHeaderMargin($margins['header']); |
||
157 | $pdf->setFooterMargin($margins['footer']); |
||
158 | |||
159 | $header = $optionsRenderer['header']; |
||
160 | $pdf->setHeaderFont([ |
||
161 | 'Helvetica', |
||
162 | '', |
||
163 | 13, |
||
164 | ]); |
||
165 | |||
166 | $pdf->setHeaderData($header['logo'], $header['logoWidth'], $this->getTitle()); |
||
167 | |||
168 | $this->pdf = $pdf; |
||
169 | } |
||
170 | |||
171 | /** |
||
172 | * @return TCPDF |
||
173 | */ |
||
174 | public function getPdf() |
||
175 | { |
||
176 | if (null === $this->pdf) { |
||
177 | $this->initPdf(); |
||
178 | } |
||
179 | |||
180 | return $this->pdf; |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Calculates the column width, based on the papersize and orientation. |
||
185 | * |
||
186 | * @param array $cols |
||
187 | */ |
||
188 | protected function calculateColumnWidth(array $cols) |
||
189 | { |
||
190 | // First make sure the columns width is 100 "percent" |
||
191 | $this->calculateColumnWidthPercent($cols); |
||
192 | |||
193 | $pdf = $this->getPdf(); |
||
194 | $margins = $pdf->getMargins(); |
||
195 | |||
196 | $paperWidth = $this->getPaperWidth(); |
||
197 | $paperWidth -= ($margins['left'] + $margins['right']); |
||
198 | |||
199 | $factor = $paperWidth / 100; |
||
200 | foreach ($cols as $col) { |
||
201 | /* @var $col \ZfcDatagrid\Column\AbstractColumn */ |
||
202 | $col->setWidth($col->getWidth() * $factor); |
||
203 | } |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * @param array $row |
||
208 | * |
||
209 | * @return number |
||
210 | */ |
||
211 | protected function getRowHeight(array $row) |
||
212 | { |
||
213 | $optionsRenderer = $this->getOptionsRenderer(); |
||
214 | $sizePoint = $optionsRenderer['style']['data']['size']; |
||
215 | $padding = $optionsRenderer['style']['data']['padding']; |
||
216 | $contentPadding = $optionsRenderer['style']['data']['contentPadding']; |
||
217 | |||
218 | // Points to MM |
||
219 | $size = $sizePoint / 2.83464566929134; |
||
220 | |||
221 | $pdf = $this->getPdf(); |
||
222 | |||
223 | $rowHeight = $size + $padding; |
||
224 | foreach ($this->getColumnsToExport() as $col) { |
||
225 | /* @var $col \ZfcDatagrid\Column\AbstractColumn */ |
||
226 | |||
227 | switch (get_class($col->getType())) { |
||
228 | |||
229 | case Type\Image::class: |
||
230 | // "min" height for such a column |
||
231 | $height = $col->getType()->getResizeHeight() + $contentPadding; |
||
232 | break; |
||
233 | |||
234 | default: |
||
235 | $value = $row[$col->getUniqueId()]; |
||
236 | if (is_array($value)) { |
||
237 | $value = implode(PHP_EOL, $value); |
||
238 | } |
||
239 | |||
240 | foreach ($col->getStyles() as $style) { |
||
241 | if ($style instanceof Style\Html) { |
||
242 | $value = str_replace(['<br>', '<br />', '<br/>'], [PHP_EOL, PHP_EOL, PHP_EOL], $value); |
||
243 | $value = strip_tags($value); |
||
244 | } |
||
245 | } |
||
246 | |||
247 | $height = $pdf->getStringHeight($col->getWidth(), $value); |
||
248 | |||
249 | // include borders top/bottom |
||
250 | $height += $contentPadding; |
||
251 | break; |
||
252 | } |
||
253 | |||
254 | if ($height > $rowHeight) { |
||
255 | $rowHeight = $height; |
||
256 | } |
||
257 | } |
||
258 | |||
259 | return $rowHeight; |
||
260 | } |
||
261 | |||
262 | protected function printTableHeader() |
||
263 | { |
||
264 | $optionsRenderer = $this->getOptionsRenderer(); |
||
265 | $height = $optionsRenderer['style']['header']['height']; |
||
266 | $this->setFontHeader(); |
||
267 | |||
268 | $pdf = $this->getPdf(); |
||
269 | $currentPage = $pdf->getPage(); |
||
270 | $y = $pdf->GetY(); |
||
271 | foreach ($this->getColumnsToExport() as $col) { |
||
272 | /* @var $col \ZfcDatagrid\Column\AbstractColumn */ |
||
273 | $x = $pdf->GetX(); |
||
274 | $pdf->setPage($currentPage); |
||
275 | |||
276 | $this->columnsPositionX[$col->getUniqueId()] = $x; |
||
277 | |||
278 | $label = $this->translate($col->getLabel()); |
||
279 | |||
280 | // Do not wrap header labels, it will look very ugly, that's why max height is set to 7! |
||
281 | $pdf->MultiCell($col->getWidth(), $height, $label, 1, $this->getTextAlignment(), true, 2, $x, $y, true, 0, false, true, 7); |
||
282 | } |
||
283 | } |
||
284 | |||
285 | protected function printTableRow(array $row, $rowHeight) |
||
286 | { |
||
287 | $pdf = $this->getPdf(); |
||
288 | |||
289 | $currentPage = $pdf->getPage(); |
||
290 | $y = $pdf->GetY(); |
||
291 | foreach ($this->getColumnsToExport() as $col) { |
||
292 | /* @var $col \ZfcDatagrid\Column\AbstractColumn */ |
||
293 | |||
294 | $pdf->setPage($currentPage); |
||
295 | $x = $this->columnsPositionX[$col->getUniqueId()]; |
||
296 | |||
297 | switch (get_class($col->getType())) { |
||
298 | |||
299 | case 'ZfcDatagrid\Column\Type\Image': |
||
300 | $text = ''; |
||
301 | |||
302 | $link = K_BLANK_IMAGE; |
||
303 | if ($row[$col->getUniqueId()] != '') { |
||
304 | $link = $row[$col->getUniqueId()]; |
||
305 | if (is_array($link)) { |
||
306 | $link = array_shift($link); |
||
307 | } |
||
308 | } |
||
309 | |||
310 | try { |
||
311 | $resizeType = $col->getType()->getResizeType(); |
||
312 | $resizeHeight = $col->getType()->getResizeHeight(); |
||
313 | if ('dynamic' === $resizeType) { |
||
314 | // resizing properly to width + height (and keeping the ratio) |
||
315 | $file = file_get_contents($link); |
||
316 | if ($file !== false) { |
||
317 | list($width, $height) = $this->calcImageSize($file, $col->getWidth() - 2, $rowHeight - 2); |
||
318 | |||
319 | $pdf->Image('@' . $file, $x + 1, $y + 1, $width, $height, '', '', 'L', true); |
||
320 | } |
||
321 | } else { |
||
322 | $pdf->Image($link, $x + 1, $y + 1, 0, $resizeHeight, '', '', 'L', true); |
||
323 | } |
||
324 | } catch (\Exception $e) { |
||
325 | // if tcpdf couldnt find a image, continue and log it |
||
326 | trigger_error($e->getMessage()); |
||
327 | } |
||
328 | break; |
||
329 | |||
330 | default: |
||
331 | $text = $row[$col->getUniqueId()]; |
||
332 | break; |
||
333 | } |
||
334 | |||
335 | if (is_array($text)) { |
||
336 | $text = implode(PHP_EOL, $text); |
||
337 | } |
||
338 | |||
339 | /* |
||
340 | * Styles |
||
341 | */ |
||
342 | $this->setFontData(); |
||
343 | |||
344 | $isHtml = false; |
||
345 | $backgroundColor = false; |
||
346 | |||
347 | $styles = array_merge($this->getRowStyles(), $col->getStyles()); |
||
348 | foreach ($styles as $style) { |
||
349 | /* @var $style Style\AbstractStyle */ |
||
350 | if ($style->isApply($row) === true) { |
||
351 | switch (get_class($style)) { |
||
352 | |||
353 | case Style\Bold::class: |
||
354 | $this->setBold(); |
||
355 | break; |
||
356 | |||
357 | case Style\Italic::class: |
||
358 | $this->setItalic(); |
||
359 | break; |
||
360 | |||
361 | case Style\Color::class: |
||
362 | $this->setColor($style->getRgbArray()); |
||
363 | break; |
||
364 | |||
365 | case Style\BackgroundColor::class: |
||
366 | $this->setBackgroundColor($style->getRgbArray()); |
||
367 | $backgroundColor = true; |
||
368 | break; |
||
369 | |||
370 | case Style\Strikethrough::class: |
||
371 | $text = '<del>'.$text.'</del>'; |
||
372 | $isHtml = true; |
||
373 | break; |
||
374 | |||
375 | case Style\Html::class: |
||
376 | $isHtml = true; |
||
377 | break; |
||
378 | |||
379 | case Style\Align::class: |
||
380 | switch ($style->getAlignment()) { |
||
381 | case Style\Align::$RIGHT: |
||
382 | $this->setTextAlignment('R'); |
||
383 | break; |
||
384 | case Style\Align::$LEFT: |
||
385 | $this->setTextAlignment('L'); |
||
386 | break; |
||
387 | case Style\Align::$CENTER: |
||
388 | $this->setTextAlignment('C'); |
||
389 | break; |
||
390 | case Style\Align::$JUSTIFY: |
||
391 | $this->setTextAlignment('J'); |
||
392 | break; |
||
393 | default: |
||
394 | //throw new \Exception('Not defined yet: "'.get_class($style->getAlignment()).'"'); |
||
395 | break; |
||
396 | } |
||
397 | break; |
||
398 | |||
399 | default: |
||
400 | throw new \Exception('Not defined yet: "'.get_class($style).'"'); |
||
401 | break; |
||
402 | } |
||
403 | } |
||
404 | } |
||
405 | |||
406 | // MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false, $ln=1, $x='', $y='', $reseth=true, $stretch=0, $ishtml=false, $autopadding=true, $maxh=0, $valign='T', $fitcell=false) |
||
407 | $pdf->MultiCell($col->getWidth(), $rowHeight, $text, 1, $this->getTextAlignment(), $backgroundColor, 1, $x, $y, true, 0, $isHtml); |
||
408 | } |
||
409 | } |
||
410 | |||
411 | /** |
||
412 | * @param string $imageData |
||
413 | * @param number $maxWidth |
||
414 | * @param number $maxHeight |
||
415 | * |
||
416 | * @return array |
||
417 | */ |
||
418 | protected function calcImageSize($imageData, $maxWidth, $maxHeight) |
||
419 | { |
||
420 | $pdf = $this->getPdf(); |
||
421 | |||
422 | list($width, $height) = getimagesizefromstring($imageData); |
||
423 | $width = $pdf->pixelsToUnits($width); |
||
424 | $height = $pdf->pixelsToUnits($height); |
||
425 | |||
426 | list($newWidth, $newHeight) = ImageResize::getCalculatedSize($width, $height, $maxWidth, $maxHeight); |
||
427 | |||
428 | return [ |
||
429 | $newWidth, |
||
430 | $newHeight, |
||
431 | ]; |
||
432 | } |
||
433 | |||
434 | View Code Duplication | protected function setFontHeader() |
|
435 | { |
||
436 | $optionsRenderer = $this->getOptionsRenderer(); |
||
437 | $style = $optionsRenderer['style']['header']; |
||
438 | |||
439 | $font = $style['font']; |
||
440 | $size = $style['size']; |
||
441 | $color = $style['color']; |
||
442 | $background = $style['background-color']; |
||
443 | |||
444 | $pdf = $this->getPdf(); |
||
445 | $pdf->SetFont($font, '', $size); |
||
446 | $pdf->SetTextColor($color[0], $color[1], $color[2]); |
||
447 | $pdf->SetFillColor($background[0], $background[1], $background[2]); |
||
448 | // "BOLD" fake |
||
449 | $pdf->setTextRenderingMode(0.15, true, false); |
||
450 | } |
||
451 | |||
452 | View Code Duplication | protected function setFontData() |
|
453 | { |
||
454 | $optionsRenderer = $this->getOptionsRenderer(); |
||
455 | $style = $optionsRenderer['style']['data']; |
||
456 | |||
457 | $font = $style['font']; |
||
458 | $size = $style['size']; |
||
459 | $color = $style['color']; |
||
460 | $background = $style['background-color']; |
||
461 | |||
462 | $pdf = $this->getPdf(); |
||
463 | $pdf->SetFont($font, '', $size); |
||
464 | $pdf->SetTextColor($color[0], $color[1], $color[2]); |
||
465 | $pdf->SetFillColor($background[0], $background[1], $background[2]); |
||
466 | $pdf->setTextRenderingMode(); |
||
467 | } |
||
468 | |||
469 | protected function setBold() |
||
470 | { |
||
471 | $pdf = $this->getPdf(); |
||
472 | $pdf->setTextRenderingMode(0.15, true, false); |
||
473 | } |
||
474 | |||
475 | protected function setItalic() |
||
476 | { |
||
477 | $optionsRenderer = $this->getOptionsRenderer(); |
||
478 | $style = $optionsRenderer['style']['data']; |
||
479 | $font = $style['font']; |
||
480 | $size = $style['size']; |
||
481 | |||
482 | $pdf = $this->getPdf(); |
||
483 | $pdf->SetFont($font.'I', '', $size); |
||
484 | } |
||
485 | |||
486 | /** |
||
487 | * @param array $rgb |
||
488 | */ |
||
489 | protected function setColor(array $rgb) |
||
490 | { |
||
491 | $pdf = $this->getPdf(); |
||
492 | $pdf->SetTextColor($rgb['red'], $rgb['green'], $rgb['blue']); |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * @param array $rgb |
||
497 | */ |
||
498 | protected function setBackgroundColor(array $rgb) |
||
503 | |||
504 | /** |
||
505 | * @param string $alignment |
||
506 | */ |
||
507 | public function setTextAlignment($alignment) |
||
511 | |||
512 | /** |
||
513 | * @return string |
||
514 | */ |
||
515 | public function getTextAlignment() |
||
516 | { |
||
517 | return $this->alignment; |
||
518 | } |
||
519 | } |
||
520 |
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.