PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / DataAccess / LogQueryBuilder / JoinTables.php
matomo / app / core / DataAccess / LogQueryBuilder Last commit date
JoinGenerator.php 3 months ago JoinTables.php 3 months ago
JoinTables.php
272 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\DataAccess\LogQueryBuilder;
10
11 use Exception;
12 use Piwik\DataAccess\LogAggregator;
13 use Piwik\Plugin\LogTablesProvider;
14 class JoinTables extends \ArrayObject
15 {
16 /**
17 * @var LogTablesProvider
18 */
19 private $logTableProvider;
20 // NOTE: joins can be specified explicitly as arrays w/ 'joinOn' keys or implicitly as table names. when
21 // table names are used, the joins dependencies are assumed based on how we want to order those joins.
22 // the below table list the possible dependencies of each table, and is specifically designed to enforce
23 // the following order:
24 // log_link_visit_action, log_action, log_visit, log_conversion, log_conversion_item
25 // which means if an array is supplied where log_visit comes before log_link_visitAction, it will
26 // be moved to after it.
27 private $implicitTableDependencies = ['log_link_visit_action' => [], 'log_action' => ['log_link_visit_action', 'log_conversion', 'log_conversion_item', 'log_visit'], 'log_visit' => ['log_link_visit_action', 'log_action'], 'log_conversion' => ['log_link_visit_action', 'log_action', 'log_visit'], 'log_conversion_item' => ['log_link_visit_action', 'log_action', 'log_visit', 'log_conversion']];
28 /**
29 * Tables constructor.
30 * @param array $tables
31 */
32 public function __construct(LogTablesProvider $logTablesProvider, $tables)
33 {
34 $this->logTableProvider = $logTablesProvider;
35 foreach ($tables as $table) {
36 $this->checkTableCanBeUsedForSegmentation($table);
37 }
38 $this->exchangeArray(array_values($tables));
39 }
40 public function getTables()
41 {
42 return $this->getArrayCopy();
43 }
44 public function addTableToJoin($tableName)
45 {
46 $this->checkTableCanBeUsedForSegmentation($tableName);
47 $this->append($tableName);
48 }
49 public function hasJoinedTable($tableName)
50 {
51 $tables = in_array($tableName, $this->getTables());
52 if ($tables) {
53 return \true;
54 }
55 foreach ($this as $table) {
56 if (is_array($table)) {
57 if (!isset($table['tableAlias']) && $table['table'] === $table) {
58 return \true;
59 } elseif (isset($table['tableAlias']) && $table['tableAlias'] === $table) {
60 return \true;
61 }
62 }
63 }
64 return \false;
65 }
66 public function hasJoinedTableManually($tableToFind, $joinToFind)
67 {
68 foreach ($this as $table) {
69 if (is_array($table) && !empty($table['table']) && $table['table'] === $tableToFind && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableToFind) && (!isset($table['join']) || strtolower($table['join']) === 'left join') && isset($table['joinOn']) && $table['joinOn'] === $joinToFind) {
70 return \true;
71 }
72 }
73 return \false;
74 }
75 public function getLogTable($tableName)
76 {
77 return $this->logTableProvider->getLogTable($tableName);
78 }
79 public function findIndexOfManuallyAddedTable($tableNameToFind)
80 {
81 foreach ($this as $index => $table) {
82 if (is_array($table) && !empty($table['table']) && $table['table'] === $tableNameToFind && (!isset($table['join']) || strtolower($table['join']) === 'left join') && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableNameToFind)) {
83 return $index;
84 }
85 }
86 }
87 public function hasAddedTableManually($tableToFind)
88 {
89 $table = $this->findIndexOfManuallyAddedTable($tableToFind);
90 return isset($table);
91 }
92 public function sort()
93 {
94 // we do not use $this->uasort as we do not want to maintain keys
95 $tables = $this->getTables();
96 // the first entry is always the FROM table
97 $firstTable = array_shift($tables);
98 $sorted = [$firstTable];
99 // With the ability to specify which index to use, the $firstTable var may be an array
100 if (is_array($firstTable) && !empty($firstTable['table'])) {
101 $firstTable = $firstTable['table'];
102 }
103 if (strpos($firstTable, LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0) {
104 // the first table might be a temporary segment table in which case we need to keep the next one as well
105 $sorted[] = array_shift($tables);
106 }
107 $dependencies = $this->parseDependencies($tables);
108 $this->visitTableListDfs($tables, $dependencies, function ($tableInfo) use(&$sorted) {
109 $sorted[] = $tableInfo;
110 });
111 $this->exchangeArray($sorted);
112 }
113 public function isTableJoinableOnVisit($tableToCheck)
114 {
115 $table = $this->getLogTable($tableToCheck);
116 if (empty($table)) {
117 return \false;
118 }
119 if ($table->getColumnToJoinOnIdVisit()) {
120 return \true;
121 }
122 if ($table->getLinkTableToBeAbleToJoinOnVisit()) {
123 return \true;
124 }
125 $otherWays = $table->getWaysToJoinToOtherLogTables();
126 if (empty($otherWays)) {
127 return \false;
128 }
129 foreach ($otherWays as $logTable => $column) {
130 if ($logTable == 'log_visit' || $this->isTableJoinableOnVisit($logTable)) {
131 return \true;
132 }
133 }
134 return \false;
135 }
136 public function isTableJoinableOnAction($tableToCheck)
137 {
138 $table = $this->getLogTable($tableToCheck);
139 if (empty($table)) {
140 return \false;
141 }
142 if ($table->getColumnToJoinOnIdAction()) {
143 return \true;
144 }
145 $otherWays = $table->getWaysToJoinToOtherLogTables();
146 if (empty($otherWays)) {
147 return \false;
148 }
149 foreach ($otherWays as $logTable => $column) {
150 if ($logTable == 'log_action' || $this->isTableJoinableOnAction($logTable)) {
151 return \true;
152 }
153 }
154 return \false;
155 }
156 public function addTableDependency($table, $dependentTable)
157 {
158 if (!empty($this->implicitTableDependencies[$table])) {
159 return;
160 }
161 $this->implicitTableDependencies[$table] = [$dependentTable];
162 }
163 private function checkTableCanBeUsedForSegmentation($tableName)
164 {
165 if (!is_array($tableName) && !$this->getLogTable($tableName)) {
166 throw new Exception("Table '{$tableName}' can't be used for segmentation");
167 }
168 }
169 private function parseDependencies(array $tables)
170 {
171 $dependencies = [];
172 foreach ($tables as $key => &$fromInfo) {
173 if (is_string($fromInfo)) {
174 $dependencies[$key] = $this->assumeImplicitJoinDependencies($tables, $fromInfo);
175 continue;
176 }
177 if (empty($fromInfo['joinOn'])) {
178 continue;
179 }
180 $table = isset($fromInfo['tableAlias']) ? $fromInfo['tableAlias'] : $fromInfo['table'];
181 $tablesInExpr = $this->parseSqlTables($fromInfo['joinOn'], $table);
182 $dependencies[$key] = $tablesInExpr;
183 }
184 return $dependencies;
185 }
186 private function assumeImplicitJoinDependencies($allTablesToQuery, $table)
187 {
188 $implicitTableDependencies = $this->implicitTableDependencies;
189 $result = [];
190 if (isset($implicitTableDependencies[$table])) {
191 $result = $implicitTableDependencies[$table];
192 // only include dependencies that are in the list of requested tables (ie, if we want to
193 // query from log_conversion joining on log_link_visit_action, we don't want to add log_visit
194 // to the sql statement)
195 $result = array_filter($result, function ($table) use($allTablesToQuery) {
196 return $this->isInTableArray($allTablesToQuery, $table);
197 });
198 }
199 return $result;
200 }
201 private function isInTableArray($tables, $table)
202 {
203 foreach ($tables as $entry) {
204 if (is_string($entry) && $entry == $table) {
205 return \true;
206 }
207 if (is_array($entry) && $entry['table'] == $table) {
208 return \true;
209 }
210 }
211 return \false;
212 }
213 private function parseSqlTables($joinOn, $self)
214 {
215 preg_match_all('/\\b([a-zA-Z0-9_`]+)\\.[a-zA-Z0-9_`]+\\b/', $joinOn, $matches);
216 $tables = [];
217 foreach ($matches[1] as $table) {
218 if ($table === $self) {
219 continue;
220 }
221 $tables[] = $table;
222 }
223 return $tables;
224 }
225 private function visitTableListDfs($tables, $dependencies, $visitor)
226 {
227 $visited = [];
228 foreach ($tables as $index => $tableInfo) {
229 if (empty($visited[$index])) {
230 $this->visitTableListDfsSingle($tables, $dependencies, $visitor, $index, $visited);
231 }
232 }
233 }
234 private function visitTableListDfsSingle($tables, $dependencies, $visitor, $tableToVisitIndex, &$visited)
235 {
236 $visited[$tableToVisitIndex] = \true;
237 $tableToVisit = $tables[$tableToVisitIndex];
238 if (!empty($dependencies[$tableToVisitIndex])) {
239 foreach ($dependencies[$tableToVisitIndex] as $dependencyTableName) {
240 $dependentTableToVisit = $this->findTableIndex($tables, $dependencyTableName);
241 if ($dependentTableToVisit === null) {
242 // sanity check, in case the dependent table is not in the list of tables to query
243 continue;
244 }
245 if (!empty($visited[$dependentTableToVisit])) {
246 // skip if already visited
247 continue;
248 }
249 // visit dependent table...
250 $this->visitTableListDfsSingle($tables, $dependencies, $visitor, $dependentTableToVisit, $visited);
251 }
252 }
253 // ...then visit current table
254 $visitor($tableToVisit);
255 }
256 private function findTableIndex($tables, $tableToSearchFor)
257 {
258 foreach ($tables as $key => $info) {
259 $tableName = null;
260 if (is_string($info)) {
261 $tableName = $info;
262 } elseif (is_array($info)) {
263 $tableName = isset($info['tableAlias']) ? $info['tableAlias'] : $info['table'];
264 }
265 if ($tableName == $tableToSearchFor) {
266 return $key;
267 }
268 }
269 return null;
270 }
271 }
272