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