| 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 |
|