1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 */ 8namespace Piwik\Metrics; 9 10use Piwik\DataTable; 11use Piwik\DataTable\Row; 12use Piwik\Metrics; 13use Piwik\Plugin\Metric; 14 15class Sorter 16{ 17 /** 18 * @var Sorter\Config 19 */ 20 private $config; 21 22 public function __construct(Sorter\Config $config) 23 { 24 $this->config = $config; 25 } 26 27 /** 28 * Sorts the DataTable rows using the supplied callback function. 29 * 30 * @param DataTable $table The table to sort. 31 */ 32 public function sort(DataTable $table) 33 { 34 // all that code is in here and not in separate methods for best performance. It does make a difference once 35 // php has to copy many (eg 50k) rows otherwise. 36 37 $table->setTableSortedBy($this->config->primaryColumnToSort); 38 39 $rows = $table->getRowsWithoutSummaryRow(); 40 41 // we need to sort rows that have a value separately from rows that do not have a value since we always want 42 // to append rows that do not have a value at the end. 43 $rowsWithValues = array(); 44 $rowsWithoutValues = array(); 45 46 $valuesToSort = array(); 47 foreach ($rows as $key => $row) { 48 $value = $this->getColumnValue($row); 49 if (isset($value)) { 50 $valuesToSort[] = $value; 51 $rowsWithValues[] = $row; 52 } else { 53 $rowsWithoutValues[] = $row; 54 } 55 } 56 57 unset($rows); 58 59 if ($this->config->isSecondaryColumnSortEnabled && $this->config->secondaryColumnToSort) { 60 $secondaryValues = array(); 61 foreach ($rowsWithValues as $key => $row) { 62 $secondaryValues[$key] = $row->getColumn($this->config->secondaryColumnToSort); 63 } 64 65 array_multisort($valuesToSort, $this->config->primarySortOrder, $this->config->primarySortFlags, $secondaryValues, $this->config->secondarySortOrder, $this->config->secondarySortFlags, $rowsWithValues); 66 67 } else { 68 array_multisort($valuesToSort, $this->config->primarySortOrder, $this->config->primarySortFlags, $rowsWithValues); 69 } 70 71 if (!empty($rowsWithoutValues) && $this->config->secondaryColumnToSort) { 72 $secondaryValues = array(); 73 foreach ($rowsWithoutValues as $key => $row) { 74 $secondaryValues[$key] = $row->getColumn($this->config->secondaryColumnToSort); 75 } 76 77 array_multisort($secondaryValues, $this->config->secondarySortOrder, $this->config->secondarySortFlags, $rowsWithoutValues); 78 } 79 80 unset($secondaryValues); 81 82 foreach ($rowsWithoutValues as $row) { 83 $rowsWithValues[] = $row; 84 } 85 86 $table->setRows(array_values($rowsWithValues)); 87 } 88 89 private function getColumnValue(Row $row) 90 { 91 $value = $row->getColumn($this->config->primaryColumnToSort); 92 93 if ($value === false || is_array($value)) { 94 return null; 95 } 96 97 return $value; 98 } 99 100 /** 101 * @param string $order 'asc' or 'desc' 102 * @return int 103 */ 104 public function getPrimarySortOrder($order) 105 { 106 if ($order === 'asc') { 107 return SORT_ASC; 108 } 109 110 return SORT_DESC; 111 } 112 113 /** 114 * @param string $order 'asc' or 'desc' 115 * @param string|int $secondarySortColumn column name or column id 116 * @return int 117 */ 118 public function getSecondarySortOrder($order, $secondarySortColumn) 119 { 120 if ($secondarySortColumn === 'label') { 121 122 $secondaryOrder = SORT_ASC; 123 if ($order === 'asc') { 124 $secondaryOrder = SORT_DESC; 125 } 126 127 return $secondaryOrder; 128 } 129 130 return $this->getPrimarySortOrder($order); 131 } 132 133 /** 134 * Detect the column to be used for sorting 135 * 136 * @param DataTable $table 137 * @param string|int $columnToSort column name or column id 138 * @return int 139 */ 140 public function getPrimaryColumnToSort(DataTable $table, $columnToSort) 141 { 142 // we fallback to nb_visits in case columnToSort does not exist 143 $columnsToCheck = array($columnToSort, 'nb_visits'); 144 145 $row = $table->getFirstRow(); 146 147 foreach ($columnsToCheck as $column) { 148 $column = Metric::getActualMetricColumn($table, $column); 149 150 if ($row->hasColumn($column)) { 151 // since getActualMetricColumn() returns a default value, we need to make sure it actually has that column 152 return $column; 153 } 154 } 155 156 return $columnToSort; 157 } 158 159 /** 160 * Detect the secondary sort column to be used for sorting 161 * 162 * @param Row $row 163 * @param int|string $primaryColumnToSort 164 * @return int 165 */ 166 public function getSecondaryColumnToSort(Row $row, $primaryColumnToSort) 167 { 168 $defaultSecondaryColumn = array(Metrics::INDEX_NB_VISITS, 'nb_visits'); 169 170 if (in_array($primaryColumnToSort, $defaultSecondaryColumn)) { 171 // if sorted by visits, then sort by label as a secondary column 172 $column = 'label'; 173 $value = $row->hasColumn($column); 174 if ($value !== false) { 175 return $column; 176 } 177 178 return null; 179 } 180 181 if ($primaryColumnToSort !== 'label') { 182 // we do not add this by default to make sure we do not sort by label as a first and secondary column 183 $defaultSecondaryColumn[] = 'label'; 184 } 185 186 foreach ($defaultSecondaryColumn as $column) { 187 $value = $row->hasColumn($column); 188 if ($value !== false) { 189 return $column; 190 } 191 } 192 } 193 194 /** 195 * @param DataTable $table 196 * @param string|int $columnToSort A column name or column id. Make sure that column actually exists in the row. 197 * You might want to get a valid column via {@link getPrimaryColumnToSort()} or 198 * {@link getSecondaryColumnToSort()} 199 * @return int 200 */ 201 public function getBestSortFlags(DataTable $table, $columnToSort) 202 { 203 // when column is label we always to sort by string or natural 204 if (isset($columnToSort) && $columnToSort !== 'label') { 205 foreach ($table->getRowsWithoutSummaryRow() as $row) { 206 $value = $row->getColumn($columnToSort); 207 208 if ($value !== false && $value !== null && !is_array($value)) { 209 210 if (is_numeric($value)) { 211 $sortFlags = SORT_NUMERIC; 212 } else { 213 $sortFlags = $this->getStringSortFlags(); 214 } 215 216 return $sortFlags; 217 } 218 } 219 } 220 221 return $this->getStringSortFlags(); 222 } 223 224 private function getStringSortFlags() 225 { 226 if ($this->config->naturalSort) { 227 $sortFlags = SORT_NATURAL | SORT_FLAG_CASE; 228 } else { 229 $sortFlags = SORT_STRING | SORT_FLAG_CASE; 230 } 231 232 return $sortFlags; 233 } 234 235 236}