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}