width = $width; } /** * Calculate our padding widths from the specified table style. * @param TableStyle $style */ public function setPaddingFromStyle(TableStyle $style) { $verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $style->getVerticalBorderChar())); $paddingLen = strlen($style->getPaddingChar()); $this->extraPaddingAtBeginningOfLine = 0; $this->extraPaddingAtEndOfLine = $verticalBorderLen; $this->paddingInEachCell = $verticalBorderLen + $paddingLen + 1; } /** * If columns have minimum widths, then set them here. * @param array $minimumWidths */ public function setMinimumWidths($minimumWidths) { $this->minimumWidths = $minimumWidths; } /** * Wrap the cells in each part of the provided data table * @param array $rows * @return array */ public function wrap($rows, $widths = []) { // If the width was not set, then disable wordwrap. if (!$this->width) { return $rows; } // Calculate the column widths to use based on the content. $auto_widths = $this->columnAutowidth($rows, $widths); // Do wordwrap on all cells. $newrows = array(); foreach ($rows as $rowkey => $row) { foreach ($row as $colkey => $cell) { $newrows[$rowkey][$colkey] = $this->wrapCell($cell, $auto_widths[$colkey]); } } return $newrows; } /** * Wrap one cell. Guard against modifying non-strings and * then call through to wordwrap(). * * @param mixed $cell * @param string $cellWidth * @return mixed */ protected function wrapCell($cell, $cellWidth) { if (!is_string($cell)) { return $cell; } return wordwrap($cell, $cellWidth, "\n", true); } /** * Determine the best fit for column widths. Ported from Drush. * * @param array $rows The rows to use for calculations. * @param array $widths Manually specified widths of each column * (in characters) - these will be left as is. */ protected function columnAutowidth($rows, $widths) { $auto_widths = $widths; // First we determine the distribution of row lengths in each column. // This is an array of descending character length keys (i.e. starting at // the rightmost character column), with the value indicating the number // of rows where that character column is present. $col_dist = []; // We will also calculate the longest word in each column $max_word_lens = []; foreach ($rows as $rowkey => $row) { foreach ($row as $col_id => $cell) { $longest_word_len = static::longestWordLength($cell); if ((!isset($max_word_lens[$col_id]) || ($max_word_lens[$col_id] < $longest_word_len))) { $max_word_lens[$col_id] = $longest_word_len; } if (empty($widths[$col_id])) { $length = strlen($cell); if ($length == 0) { $col_dist[$col_id][0] = 0; } while ($length > 0) { if (!isset($col_dist[$col_id][$length])) { $col_dist[$col_id][$length] = 0; } $col_dist[$col_id][$length]++; $length--; } } } } foreach ($col_dist as $col_id => $count) { // Sort the distribution in decending key order. krsort($col_dist[$col_id]); // Initially we set all columns to their "ideal" longest width // - i.e. the width of their longest column. $auto_widths[$col_id] = max(array_keys($col_dist[$col_id])); } // We determine what width we have available to use, and what width the // above "ideal" columns take up. $available_width = $this->width - ($this->extraPaddingAtBeginningOfLine + $this->extraPaddingAtEndOfLine + (count($auto_widths) * $this->paddingInEachCell)); $auto_width_current = array_sum($auto_widths); // If we cannot fit into the minimum width anyway, then just return // the max word length of each column as the 'ideal' $minimumIdealLength = array_sum($this->minimumWidths); if ($minimumIdealLength && ($available_width < $minimumIdealLength)) { return $max_word_lens; } // If we need to reduce a column so that we can fit the space we use this // loop to figure out which column will cause the "least wrapping", // (relative to the other columns) and reduce the width of that column. while ($auto_width_current > $available_width) { list($column, $width) = $this->selectColumnToReduce($col_dist, $auto_widths, $max_word_lens); if (!$column || $width <= 1) { // If we have reached a width of 1 then give up, so wordwrap can still progress. break; } // Reduce the width of the selected column. $auto_widths[$column]--; // Reduce our overall table width counter. $auto_width_current--; // Remove the corresponding data from the disctribution, so next time // around we use the data for the row to the left. unset($col_dist[$column][$width]); } return $auto_widths; } protected function selectColumnToReduce($col_dist, $auto_widths, $max_word_lens) { $column = false; $count = 0; $width = 0; foreach ($col_dist as $col_id => $counts) { // Of the columns whose length is still > than the the lenght // of their maximum word length if ($auto_widths[$col_id] > $max_word_lens[$col_id]) { if ($this->shouldSelectThisColumn($count, $counts, $width)) { $column = $col_id; $count = current($counts); $width = key($counts); } } } if ($column !== false) { return [$column, $width]; } foreach ($col_dist as $col_id => $counts) { if (empty($this->minimumWidths) || ($auto_widths[$col_id] > $this->minimumWidths[$col_id])) { if ($this->shouldSelectThisColumn($count, $counts, $width)) { $column = $col_id; $count = current($counts); $width = key($counts); } } } return [$column, $width]; } protected function shouldSelectThisColumn($count, $counts, $width) { return // If we are just starting out, select the first column. ($count == 0) || // OR: if this column would cause less wrapping than the currently // selected column, then select it. (current($counts) < $count) || // OR: if this column would cause the same amount of wrapping, but is // longer, then we choose to wrap the longer column (proportionally // less wrapping, and helps avoid triple line wraps). (current($counts) == $count && key($counts) > $width); } /** * Return the length of the longest word in the string. * @param string $str * @return int */ protected static function longestWordLength($str) { $words = preg_split('/[ -]/', $str); $lengths = array_map(function ($s) { return strlen($s); }, $words); return max($lengths); } }