Total Complexity | 43 |
Total Lines | 361 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like Table 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.
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 Table, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
46 | class Table extends Widget |
||
47 | { |
||
48 | const DEFAULT_CONSOLE_SCREEN_WIDTH = 120; |
||
49 | const CONSOLE_SCROLLBAR_OFFSET = 3; |
||
50 | const CHAR_TOP = 'top'; |
||
51 | const CHAR_TOP_MID = 'top-mid'; |
||
52 | const CHAR_TOP_LEFT = 'top-left'; |
||
53 | const CHAR_TOP_RIGHT = 'top-right'; |
||
54 | const CHAR_BOTTOM = 'bottom'; |
||
55 | const CHAR_BOTTOM_MID = 'bottom-mid'; |
||
56 | const CHAR_BOTTOM_LEFT = 'bottom-left'; |
||
57 | const CHAR_BOTTOM_RIGHT = 'bottom-right'; |
||
58 | const CHAR_LEFT = 'left'; |
||
59 | const CHAR_LEFT_MID = 'left-mid'; |
||
60 | const CHAR_MID = 'mid'; |
||
61 | const CHAR_MID_MID = 'mid-mid'; |
||
62 | const CHAR_RIGHT = 'right'; |
||
63 | const CHAR_RIGHT_MID = 'right-mid'; |
||
64 | const CHAR_MIDDLE = 'middle'; |
||
65 | |||
66 | /** |
||
67 | * @var array table headers |
||
68 | * @since 2.0.19 |
||
69 | */ |
||
70 | protected $headers = []; |
||
71 | /** |
||
72 | * @var array table rows |
||
73 | * @since 2.0.19 |
||
74 | */ |
||
75 | protected $rows = []; |
||
76 | /** |
||
77 | * @var array table chars |
||
78 | * @since 2.0.19 |
||
79 | */ |
||
80 | protected $chars = [ |
||
81 | self::CHAR_TOP => '═', |
||
82 | self::CHAR_TOP_MID => '╤', |
||
83 | self::CHAR_TOP_LEFT => '╔', |
||
84 | self::CHAR_TOP_RIGHT => '╗', |
||
85 | self::CHAR_BOTTOM => '═', |
||
86 | self::CHAR_BOTTOM_MID => '╧', |
||
87 | self::CHAR_BOTTOM_LEFT => '╚', |
||
88 | self::CHAR_BOTTOM_RIGHT => '╝', |
||
89 | self::CHAR_LEFT => '║', |
||
90 | self::CHAR_LEFT_MID => '╟', |
||
91 | self::CHAR_MID => '─', |
||
92 | self::CHAR_MID_MID => '┼', |
||
93 | self::CHAR_RIGHT => '║', |
||
94 | self::CHAR_RIGHT_MID => '╢', |
||
95 | self::CHAR_MIDDLE => '│', |
||
96 | ]; |
||
97 | /** |
||
98 | * @var array table column widths |
||
99 | * @since 2.0.19 |
||
100 | */ |
||
101 | protected $columnWidths = []; |
||
102 | /** |
||
103 | * @var int screen width |
||
104 | * @since 2.0.19 |
||
105 | */ |
||
106 | protected $screenWidth; |
||
107 | /** |
||
108 | * @var string list prefix |
||
109 | * @since 2.0.19 |
||
110 | */ |
||
111 | protected $listPrefix = '• '; |
||
112 | |||
113 | |||
114 | /** |
||
115 | * Set table headers. |
||
116 | * |
||
117 | * @param array $headers table headers |
||
118 | * @return $this |
||
119 | */ |
||
120 | public function setHeaders(array $headers) |
||
124 | } |
||
125 | |||
126 | /** |
||
127 | * Set table rows. |
||
128 | * |
||
129 | * @param array $rows table rows |
||
130 | * @return $this |
||
131 | */ |
||
132 | public function setRows(array $rows) |
||
133 | { |
||
134 | $this->rows = array_map(function($row) { |
||
135 | return array_map(function($value) { |
||
136 | return empty($value) && !is_numeric($value) ? ' ' : $value; |
||
137 | }, array_values($row)); |
||
138 | }, $rows); |
||
139 | return $this; |
||
140 | } |
||
141 | |||
142 | /** |
||
143 | * Set table chars. |
||
144 | * |
||
145 | * @param array $chars table chars |
||
146 | * @return $this |
||
147 | */ |
||
148 | public function setChars(array $chars) |
||
149 | { |
||
150 | $this->chars = $chars; |
||
151 | return $this; |
||
152 | } |
||
153 | |||
154 | /** |
||
155 | * Set screen width. |
||
156 | * |
||
157 | * @param int $width screen width |
||
158 | * @return $this |
||
159 | */ |
||
160 | public function setScreenWidth($width) |
||
161 | { |
||
162 | $this->screenWidth = $width; |
||
163 | return $this; |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Set list prefix. |
||
168 | * |
||
169 | * @param string $listPrefix list prefix |
||
170 | * @return $this |
||
171 | */ |
||
172 | public function setListPrefix($listPrefix) |
||
173 | { |
||
174 | $this->listPrefix = $listPrefix; |
||
175 | return $this; |
||
176 | } |
||
177 | |||
178 | /** |
||
179 | * @return string the rendered table |
||
180 | */ |
||
181 | public function run() |
||
182 | { |
||
183 | $this->calculateRowsSize(); |
||
184 | $headerCount = count($this->headers); |
||
185 | |||
186 | $buffer = $this->renderSeparator( |
||
187 | $this->chars[self::CHAR_TOP_LEFT], |
||
188 | $this->chars[self::CHAR_TOP_MID], |
||
189 | $this->chars[self::CHAR_TOP], |
||
190 | $this->chars[self::CHAR_TOP_RIGHT] |
||
191 | ); |
||
192 | // Header |
||
193 | if ($headerCount > 0) { |
||
194 | $buffer .= $this->renderRow($this->headers, |
||
195 | $this->chars[self::CHAR_LEFT], |
||
196 | $this->chars[self::CHAR_MIDDLE], |
||
197 | $this->chars[self::CHAR_RIGHT] |
||
198 | ); |
||
199 | } |
||
200 | |||
201 | // Content |
||
202 | foreach ($this->rows as $i => $row) { |
||
203 | if ($i > 0 || $headerCount > 0) { |
||
204 | $buffer .= $this->renderSeparator( |
||
205 | $this->chars[self::CHAR_LEFT_MID], |
||
206 | $this->chars[self::CHAR_MID_MID], |
||
207 | $this->chars[self::CHAR_MID], |
||
208 | $this->chars[self::CHAR_RIGHT_MID] |
||
209 | ); |
||
210 | } |
||
211 | $buffer .= $this->renderRow($row, |
||
212 | $this->chars[self::CHAR_LEFT], |
||
213 | $this->chars[self::CHAR_MIDDLE], |
||
214 | $this->chars[self::CHAR_RIGHT]); |
||
215 | } |
||
216 | |||
217 | $buffer .= $this->renderSeparator( |
||
218 | $this->chars[self::CHAR_BOTTOM_LEFT], |
||
219 | $this->chars[self::CHAR_BOTTOM_MID], |
||
220 | $this->chars[self::CHAR_BOTTOM], |
||
221 | $this->chars[self::CHAR_BOTTOM_RIGHT] |
||
222 | ); |
||
223 | |||
224 | return $buffer; |
||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Renders a row of data into a string. |
||
229 | * |
||
230 | * @param array $row row of data |
||
231 | * @param string $spanLeft character for left border |
||
232 | * @param string $spanMiddle character for middle border |
||
233 | * @param string $spanRight character for right border |
||
234 | * @return string |
||
235 | * @see \yii\console\widgets\Table::render() |
||
236 | */ |
||
237 | protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight) |
||
238 | { |
||
239 | $size = $this->columnWidths; |
||
240 | |||
241 | $buffer = ''; |
||
242 | $arrayPointer = []; |
||
243 | $finalChunk = []; |
||
244 | for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) { |
||
245 | $buffer .= $spanLeft . ' '; |
||
246 | foreach ($size as $index => $cellSize) { |
||
247 | $cell = isset($row[$index]) ? $row[$index] : null; |
||
248 | $prefix = ''; |
||
249 | if ($index !== 0) { |
||
250 | $buffer .= $spanMiddle . ' '; |
||
251 | } |
||
252 | if (is_array($cell)) { |
||
253 | if (empty($finalChunk[$index])) { |
||
254 | $finalChunk[$index] = ''; |
||
255 | $start = 0; |
||
256 | $prefix = $this->listPrefix; |
||
257 | if (!isset($arrayPointer[$index])) { |
||
258 | $arrayPointer[$index] = 0; |
||
259 | } |
||
260 | } else { |
||
261 | $start = mb_strwidth($finalChunk[$index], Yii::$app->charset); |
||
262 | } |
||
263 | $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset); |
||
264 | $finalChunk[$index] .= $chunk; |
||
265 | if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) { |
||
266 | $arrayPointer[$index]++; |
||
267 | $finalChunk[$index] = ''; |
||
268 | } |
||
269 | } else { |
||
270 | $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset); |
||
271 | } |
||
272 | $chunk = $prefix . $chunk; |
||
273 | $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1; |
||
274 | $buffer .= $chunk; |
||
275 | if ($repeat >= 0) { |
||
276 | $buffer .= str_repeat(' ', $repeat); |
||
277 | } |
||
278 | } |
||
279 | $buffer .= "$spanRight\n"; |
||
280 | } |
||
281 | |||
282 | return $buffer; |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | * Renders separator. |
||
287 | * |
||
288 | * @param string $spanLeft character for left border |
||
289 | * @param string $spanMid character for middle border |
||
290 | * @param string $spanMidMid character for middle-middle border |
||
291 | * @param string $spanRight character for right border |
||
292 | * @return string the generated separator row |
||
293 | * @see \yii\console\widgets\Table::render() |
||
294 | */ |
||
295 | protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight) |
||
296 | { |
||
297 | $separator = $spanLeft; |
||
298 | foreach ($this->columnWidths as $index => $rowSize) { |
||
299 | if ($index !== 0) { |
||
300 | $separator .= $spanMid; |
||
301 | } |
||
302 | $separator .= str_repeat($spanMidMid, $rowSize); |
||
303 | } |
||
304 | $separator .= $spanRight . "\n"; |
||
305 | return $separator; |
||
306 | } |
||
307 | |||
308 | /** |
||
309 | * Calculate the size of rows to draw anchor of columns in console. |
||
310 | * |
||
311 | * @see \yii\console\widgets\Table::render() |
||
312 | */ |
||
313 | protected function calculateRowsSize() |
||
314 | { |
||
315 | $this->columnWidths = $columns = []; |
||
316 | $totalWidth = 0; |
||
317 | $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET; |
||
318 | |||
319 | $headerCount = count($this->headers); |
||
320 | if (empty($this->rows)) { |
||
321 | $rowColCount = 0; |
||
322 | } else { |
||
323 | $rowColCount = max(array_map('count', $this->rows)); |
||
324 | } |
||
325 | $count = max($headerCount, $rowColCount); |
||
326 | for ($i = 0; $i < $count; $i++) { |
||
327 | $columns[] = ArrayHelper::getColumn($this->rows, $i); |
||
328 | if ($i < $headerCount) { |
||
329 | $columns[$i][] = $this->headers[$i]; |
||
330 | } |
||
331 | } |
||
332 | |||
333 | foreach ($columns as $column) { |
||
334 | $columnWidth = max(array_map(function ($val) { |
||
335 | if (is_array($val)) { |
||
336 | $encodings = array_fill(0, count($val), Yii::$app->charset); |
||
337 | return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->listPrefix, Yii::$app->charset); |
||
338 | } |
||
339 | |||
340 | return mb_strwidth($val, Yii::$app->charset); |
||
341 | }, $column)) + 2; |
||
342 | $this->columnWidths[] = $columnWidth; |
||
343 | $totalWidth += $columnWidth; |
||
344 | } |
||
345 | |||
346 | $relativeWidth = $screenWidth / $totalWidth; |
||
347 | |||
348 | if ($totalWidth > $screenWidth) { |
||
349 | foreach ($this->columnWidths as $j => $width) { |
||
350 | $this->columnWidths[$j] = (int) ($width * $relativeWidth); |
||
351 | if ($j === count($this->columnWidths)) { |
||
352 | $this->columnWidths = $totalWidth; |
||
|
|||
353 | } |
||
354 | $totalWidth -= $this->columnWidths[$j]; |
||
355 | } |
||
356 | } |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * Calculate the height of a row. |
||
361 | * |
||
362 | * @param array $row |
||
363 | * @return int maximum row per cell |
||
364 | * @see \yii\console\widgets\Table::render() |
||
365 | */ |
||
366 | protected function calculateRowHeight($row) |
||
367 | { |
||
368 | $rowsPerCell = array_map(function ($size, $columnWidth) { |
||
369 | if (is_array($columnWidth)) { |
||
370 | $rows = 0; |
||
371 | foreach ($columnWidth as $width) { |
||
372 | $rows += ceil($width / ($size - 2)); |
||
373 | } |
||
374 | |||
375 | return $rows; |
||
376 | } |
||
377 | |||
378 | return ceil($columnWidth / ($size - 2)); |
||
379 | }, $this->columnWidths, array_map(function ($val) { |
||
380 | if (is_array($val)) { |
||
381 | $encodings = array_fill(0, count($val), Yii::$app->charset); |
||
382 | return array_map('mb_strwidth', $val, $encodings); |
||
383 | } |
||
384 | |||
385 | return mb_strwidth($val, Yii::$app->charset); |
||
386 | }, $row) |
||
387 | ); |
||
388 | |||
389 | return max($rowsPerCell); |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * Getting screen width. |
||
394 | * If it is not able to determine screen width, default value `123` will be set. |
||
395 | * |
||
396 | * @return int screen width |
||
397 | */ |
||
398 | protected function getScreenWidth() |
||
407 | } |
||
408 | } |
||
409 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..