Total Complexity | 57 |
Total Lines | 331 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like StatementExecution often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use StatementExecution, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
33 | class StatementExecution implements IStatementExecution |
||
34 | { |
||
35 | private $connCtl; |
||
36 | private $typeCtl; |
||
37 | private $stmtExFactory; |
||
38 | |||
39 | public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl) |
||
40 | { |
||
41 | $this->connCtl = $connCtl; |
||
42 | $this->typeCtl = $typeCtl; |
||
43 | $this->stmtExFactory = Ivory::getCoreFactory()->createStatementExceptionFactory($this); |
||
44 | } |
||
45 | |||
46 | public function query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IQueryResult |
||
47 | { |
||
48 | $sql = $this->extractRawQuery($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams); |
||
49 | return $this->rawQuery($sql); |
||
50 | } |
||
51 | |||
52 | public function queryAsync($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IAsyncQueryResult |
||
59 | } |
||
60 | ); |
||
61 | } |
||
62 | |||
63 | private function extractRawQuery($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): string |
||
86 | } |
||
87 | } |
||
88 | |||
89 | public function querySingleTuple($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): ?ITuple |
||
90 | { |
||
91 | $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams); |
||
92 | switch ($rel->count()) { |
||
93 | case 1: |
||
94 | return $rel->tuple(); |
||
95 | case 0: |
||
96 | return null; |
||
97 | default: |
||
98 | throw new ResultDimensionException( |
||
99 | "The query should have resulted in at most one row, but has {$rel->count()} rows." |
||
100 | ); |
||
101 | } |
||
102 | } |
||
103 | |||
104 | public function querySingleColumn($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IColumn |
||
105 | { |
||
106 | $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams); |
||
107 | |||
108 | $colCnt = count($rel->getColumns()); |
||
109 | if ($colCnt != 1) { |
||
110 | throw new ResultDimensionException( |
||
111 | "The query should have resulted in exactly one column, but has $colCnt columns." |
||
112 | ); |
||
113 | } |
||
114 | |||
115 | return $rel->col(0); |
||
116 | } |
||
117 | |||
118 | public function querySingleValue($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams) |
||
119 | { |
||
120 | $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams); |
||
121 | |||
122 | $colCnt = count($rel->getColumns()); |
||
123 | if ($colCnt != 1) { |
||
124 | throw new ResultDimensionException( |
||
125 | "The query should have resulted in exactly one column, but has $colCnt columns." |
||
126 | ); |
||
127 | } |
||
128 | |||
129 | switch ($rel->count()) { |
||
130 | case 1: |
||
131 | return $rel->value(); |
||
132 | case 0: |
||
133 | return null; |
||
134 | default: |
||
135 | throw new ResultDimensionException( |
||
136 | "The query should have resulted in at most one row, but has {$rel->count()} rows." |
||
137 | ); |
||
138 | } |
||
139 | } |
||
140 | |||
141 | public function command($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): ICommandResult |
||
145 | } |
||
146 | |||
147 | public function commandAsync($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): IAsyncCommandResult |
||
148 | { |
||
149 | $sql = $this->extractRawCommand($sqlFragmentPatternOrCommand, ...$fragmentsAndParams); |
||
150 | $this->sendRawStatement($sql); |
||
151 | return new AsyncCommandResult( |
||
152 | function () use ($sql) { |
||
153 | return $this->fetchStatementResult($sql); |
||
154 | } |
||
155 | ); |
||
156 | } |
||
157 | |||
158 | private function extractRawCommand($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): string |
||
159 | { |
||
160 | $typeDict = $this->typeCtl->getTypeDictionary(); |
||
161 | |||
162 | try { |
||
163 | if ($sqlFragmentPatternOrCommand instanceof ISqlPatternStatement) { |
||
164 | $this->checkMaxArgs($fragmentsAndParams, 1); |
||
165 | $sqlCommand = $sqlFragmentPatternOrCommand; |
||
166 | $serializeArgs = $fragmentsAndParams; |
||
167 | return $sqlCommand->toSql($typeDict, ...$serializeArgs); |
||
168 | } elseif ($sqlFragmentPatternOrCommand instanceof ICommand) { |
||
169 | $this->checkMaxArgs($fragmentsAndParams, 0); |
||
170 | $command = $sqlFragmentPatternOrCommand; |
||
171 | return $command->toSql($typeDict); |
||
172 | } else { |
||
173 | $command = SqlCommand::fromFragments($sqlFragmentPatternOrCommand, ...$fragmentsAndParams); |
||
174 | return $command->toSql($typeDict); |
||
175 | } |
||
176 | } catch (InvalidStateException $e) { |
||
177 | throw new \InvalidArgumentException($e->getMessage()); |
||
178 | } |
||
179 | } |
||
180 | |||
181 | private function checkMaxArgs($args, int $maxCount): void |
||
182 | { |
||
183 | if (count($args) > $maxCount) { |
||
184 | throw new \InvalidArgumentException('Too many arguments given.'); |
||
185 | } |
||
186 | } |
||
187 | |||
188 | public function rawQuery(string $sqlQuery): IQueryResult |
||
189 | { |
||
190 | $result = $this->executeRawStatement($sqlQuery); |
||
191 | if ($result instanceof IQueryResult) { |
||
192 | return $result; |
||
193 | } else { |
||
194 | throw new UsageException( |
||
195 | 'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' . |
||
196 | 'Did you mean to call command() or rawCommand()?' |
||
197 | ); |
||
198 | } |
||
199 | } |
||
200 | |||
201 | public function rawCommand(string $sqlCommand): ICommandResult |
||
202 | { |
||
203 | $result = $this->executeRawStatement($sqlCommand); |
||
204 | if ($result instanceof ICommandResult) { |
||
205 | return $result; |
||
206 | } else { |
||
207 | throw new UsageException( |
||
208 | 'The supplied SQL statement was supposed to be a command, but it returned a result set. ' . |
||
209 | 'Did you mean to call query() or rawQuery()?' |
||
210 | ); |
||
211 | } |
||
212 | } |
||
213 | |||
214 | public function executeStatement($sqlStatement): IResult |
||
218 | } |
||
219 | |||
220 | public function executeStatementAsync($sqlStatement): IAsyncResult |
||
221 | { |
||
222 | $rawStatement = $this->extractRawStatement($sqlStatement); |
||
223 | $this->sendRawStatement($rawStatement); |
||
224 | return new AsyncResult( |
||
225 | function () use ($rawStatement) { |
||
226 | return $this->fetchStatementResult($rawStatement); |
||
227 | } |
||
228 | ); |
||
229 | } |
||
230 | |||
231 | private function extractRawStatement($sqlStatement): string |
||
232 | { |
||
233 | if (is_string($sqlStatement)) { |
||
234 | return $sqlStatement; |
||
235 | } elseif ($sqlStatement instanceof ISqlPatternStatement) { |
||
236 | $typeDict = $this->typeCtl->getTypeDictionary(); |
||
237 | return $sqlStatement->toSql($typeDict); |
||
238 | } else { |
||
239 | throw new \InvalidArgumentException( |
||
240 | '$sqlStatement must either by a string or ' . ISqlPatternStatement::class |
||
241 | ); |
||
242 | } |
||
243 | } |
||
244 | |||
245 | private function executeRawStatement(string $sqlStatement): IResult |
||
246 | { |
||
247 | $this->sendRawStatement($sqlStatement); |
||
248 | return $this->fetchStatementResult($sqlStatement); |
||
249 | } |
||
250 | |||
251 | private function sendRawStatement(string $sqlStatement): void |
||
264 | } |
||
265 | } |
||
266 | |||
267 | private function fetchStatementResult(string $sqlStatement): IResult |
||
268 | { |
||
269 | $connHandler = $this->connCtl->requireConnection(); |
||
270 | |||
271 | $resultHandler = pg_get_result($connHandler); |
||
272 | if ($resultHandler === false) { |
||
273 | throw new ConnectionException('No results received from the database.'); |
||
274 | } |
||
275 | /** |
||
276 | * For erroneous queries, one must call pg_get_result() once again to update the structures at the client side. |
||
277 | * Even worse, a loop might actually be needed according to |
||
278 | * {@link http://www.postgresql.org/message-id/flat/[email protected]#[email protected]}, |
||
279 | * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of |
||
280 | * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution. |
||
281 | */ |
||
282 | while (pg_get_result($connHandler) !== false) { |
||
283 | trigger_error('The database gave an unexpected result set.', E_USER_NOTICE); |
||
284 | } |
||
285 | |||
286 | return $this->processResult($connHandler, $resultHandler, $sqlStatement); |
||
287 | } |
||
288 | |||
289 | public function runScript(string $sqlScript): array |
||
290 | { |
||
291 | $connHandler = $this->connCtl->requireConnection(); |
||
292 | |||
293 | while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen |
||
294 | usleep(1); |
||
295 | } |
||
296 | |||
297 | $sent = pg_send_query($connHandler, $sqlScript); |
||
298 | if (!$sent) { |
||
299 | throw new ConnectionException('Error sending the query to the database.'); |
||
300 | } |
||
301 | |||
302 | $resHandlers = []; |
||
303 | while (($res = pg_get_result($connHandler)) !== false) { |
||
304 | /* NOTE: Cannot process the result right away - the remaining results must all be read, or they would, in |
||
305 | * case of error, block the connection from accepting further queries. |
||
306 | */ |
||
307 | $resHandlers[] = $res; |
||
308 | } |
||
309 | $results = []; |
||
310 | foreach ($resHandlers as $resHandler) { |
||
311 | $results[] = $this->processResult($connHandler, $resHandler, $sqlScript); |
||
312 | } |
||
313 | return $results; |
||
314 | } |
||
315 | |||
316 | /** |
||
317 | * @param resource $connHandler |
||
318 | * @param resource $resHandler |
||
319 | * @param string $query |
||
320 | * @return IResult |
||
321 | * @throws StatementException upon an SQL statement error |
||
322 | */ |
||
323 | private function processResult($connHandler, $resHandler, string $query): IResult |
||
324 | { |
||
325 | $notice = $this->getLastResultNotice(); |
||
326 | $stat = pg_result_status($resHandler); |
||
327 | switch ($stat) { |
||
328 | case PGSQL_COMMAND_OK: |
||
329 | return new CommandResult($resHandler, $notice); |
||
330 | case PGSQL_TUPLES_OK: |
||
331 | return new QueryResult($resHandler, $this->typeCtl, $notice); |
||
332 | case PGSQL_COPY_IN: |
||
333 | return new CopyInResult($connHandler, $resHandler, $notice); |
||
334 | case PGSQL_COPY_OUT: |
||
335 | return new CopyOutResult($connHandler, $resHandler, $notice); |
||
336 | |||
337 | case PGSQL_EMPTY_QUERY: |
||
338 | case PGSQL_BAD_RESPONSE: |
||
339 | case PGSQL_NONFATAL_ERROR: |
||
340 | // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway... |
||
341 | case PGSQL_FATAL_ERROR: |
||
342 | throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory()); |
||
343 | |||
344 | default: |
||
345 | throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat); |
||
346 | } |
||
347 | } |
||
348 | |||
349 | private function getLastResultNotice(): ?string |
||
350 | { |
||
351 | $resNotice = pg_last_notice($this->connCtl->requireConnection()); |
||
352 | $connNotice = $this->connCtl->getLastNotice(); |
||
353 | if ($resNotice !== $connNotice) { |
||
354 | $this->connCtl->setLastNotice($resNotice); |
||
355 | return $resNotice; |
||
356 | } else { |
||
357 | return null; |
||
358 | } |
||
359 | } |
||
360 | |||
361 | public function getStatementExceptionFactory(): StatementExceptionFactory |
||
364 | } |
||
365 | } |
||
366 |