PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / import / ForeignSourceQueryGateway.php

ForeignSourceQueryGateway.php in 404 Solution trunk, at includes/import/ForeignSourceQueryGateway.php

172 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Executes queries against a foreign redirect plugin's storage through the
9 * centralized {@see ABJ_404_Solution_DatabaseQueryInterface} pipeline and
10 * translates driver-level outcomes (missing table, query error, timeout)
11 * into simple pass/fail results.
12 *
13 * This class holds zero per-source schema knowledge (no table names, no
14 * column names, no WHERE-clause filters): it only knows how to ask "does
15 * this table exist" and "run this SQL and hand back rows or nothing."
16 * {@see ABJ_404_Solution_ForeignRedirectSourceReader} owns the per-source
17 * knowledge and is the primary caller today; a future 6th foreign-plugin
18 * source would reuse this gateway unchanged.
19 */
20 class ABJ_404_Solution_ForeignSourceQueryGateway {
21
22 /** @var ABJ_404_Solution_Logging */
23 private $logger;
24
25 /** @var ABJ_404_Solution_DatabaseQueryInterface|null */
26 private $dbQuery;
27
28 /**
29 * @param mixed $redirectsRepository Used only to resolve a database query
30 * service when one is not supplied directly.
31 * @param ABJ_404_Solution_Logging $logger
32 * @param ABJ_404_Solution_DatabaseQueryInterface|null $dbQuery
33 */
34 public function __construct($redirectsRepository, $logger, $dbQuery = null) {
35 $this->logger = $logger;
36 $this->dbQuery = $this->resolveDatabaseQuery($redirectsRepository, $dbQuery);
37 }
38
39 /**
40 * Check whether a table exists using SHOW TABLES LIKE.
41 *
42 * @param string $tableName Fully-prefixed table name
43 * @return bool
44 */
45 public function tableExists(string $tableName): bool {
46 if (!$this->dbQuery instanceof ABJ_404_Solution_DatabaseQueryInterface) {
47 $this->logger->warn(
48 'CrossPluginImporter: cannot check source table "' . $tableName . '" because no database query service is available.'
49 );
50 return false;
51 }
52
53 $result = $this->dbQuery->queryAndGetResults(
54 'SHOW TABLES LIKE %s',
55 array(
56 'query_params' => array($tableName),
57 'result_type' => defined('ARRAY_A') ? ARRAY_A : 'ARRAY_A',
58 'log_errors' => false,
59 'skip_repair' => true,
60 )
61 );
62
63 if ($this->queryFailed($result)) {
64 $this->logger->warn(
65 'CrossPluginImporter: source table probe failed for "' . $tableName . '". Error: ' .
66 $this->queryErrorMessage($result)
67 );
68 return false;
69 }
70
71 return !empty($result['rows']) && is_array($result['rows']);
72 }
73
74 /**
75 * Run a SQL statement against foreign source-plugin storage through the
76 * centralized query pipeline and return its rows. Used for both full row
77 * reads (import/preview) and COUNT(*) queries (preview count) -- the
78 * shape of the row(s) returned is up to the caller's SQL, this method
79 * only handles execution and error interpretation.
80 *
81 * @param string $sql
82 * @return array<int, array<string, mixed>>
83 */
84 public function queryRows(string $sql): array {
85 if (!$this->dbQuery instanceof ABJ_404_Solution_DatabaseQueryInterface) {
86 $this->logger->warn('CrossPluginImporter: cannot query source storage because no database query service is available.');
87 return array();
88 }
89
90 $result = $this->dbQuery->queryAndGetResults(
91 $sql,
92 array('result_type' => defined('ARRAY_A') ? ARRAY_A : 'ARRAY_A')
93 );
94
95 if ($this->queryFailed($result)) {
96 $this->logger->warn(
97 'CrossPluginImporter: source query failed. Error: ' . $this->queryErrorMessage($result)
98 );
99 return array();
100 }
101
102 $rows = $result['rows'] ?? array();
103 if (!is_array($rows)) {
104 return array();
105 }
106
107 $normalizedRows = array();
108 foreach ($rows as $row) {
109 if (is_array($row)) {
110 $normalizedRows[] = $row;
111 }
112 }
113 return $normalizedRows;
114 }
115
116 /**
117 * @param array<string, mixed> $result
118 * @return bool
119 */
120 private function queryFailed(array $result): bool {
121 if (($result['timed_out'] ?? false) === true) {
122 return true;
123 }
124 return $this->queryErrorMessage($result) !== '';
125 }
126
127 /**
128 * @param array<string, mixed> $result
129 * @return string
130 */
131 private function queryErrorMessage(array $result): string {
132 if (($result['timed_out'] ?? false) === true) {
133 return 'query timed out';
134 }
135
136 $error = $result['last_error'] ?? '';
137 if ($error === '') {
138 return '';
139 }
140 if (is_scalar($error)) {
141 return (string)$error;
142 }
143 if (is_object($error) && method_exists($error, '__toString')) {
144 return (string)$error;
145 }
146 return 'non-scalar database error of type ' . gettype($error);
147 }
148
149 /**
150 * Resolve the database query service without requiring existing callers
151 * to pass the optional constructor argument.
152 *
153 * @param mixed $redirectsRepository
154 * @param mixed $dbQuery
155 * @return ABJ_404_Solution_DatabaseQueryInterface|null
156 */
157 private function resolveDatabaseQuery($redirectsRepository, $dbQuery) {
158 if ($dbQuery instanceof ABJ_404_Solution_DatabaseQueryInterface) {
159 return $dbQuery;
160 }
161
162 if (is_object($redirectsRepository) && method_exists($redirectsRepository, 'getDbCore')) {
163 $candidate = $redirectsRepository->getDbCore();
164 if ($candidate instanceof ABJ_404_Solution_DatabaseQueryInterface) {
165 return $candidate;
166 }
167 }
168
169 return null;
170 }
171 }
172