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