Completed
Push — master ( bd03b7...2e6f46 )
by Ondřej
03:14
created

StatementExecution::setupRecipe()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
c 0
b 0
f 0
rs 9.2
cc 4
eloc 10
nc 4
nop 2
1
<?php
2
namespace Ivory\Connection;
3
4
use Ivory\Exception\StatementException;
5
use Ivory\Exception\ConnectionException;
6
use Ivory\Exception\StatementExceptionFactory;
7
use Ivory\Ivory;
8
use Ivory\Query\ICommandRecipe;
9
use Ivory\Query\IRelationRecipe;
10
use Ivory\Query\ISqlPatternRecipe;
11
use Ivory\Query\SqlCommandRecipe;
12
use Ivory\Query\SqlRelationRecipe;
13
use Ivory\Result\CommandResult;
14
use Ivory\Result\CopyInResult;
15
use Ivory\Result\CopyOutResult;
16
use Ivory\Result\ICommandResult;
17
use Ivory\Result\IQueryResult;
18
use Ivory\Result\IResult;
19
use Ivory\Result\QueryResult;
20
21
class StatementExecution implements IStatementExecution
22
{
23
    private $connCtl;
24
    private $typeCtl;
25
    private $stmtExFactory;
26
27
    public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl)
28
    {
29
        $this->connCtl = $connCtl;
30
        $this->typeCtl = $typeCtl;
31
        $this->stmtExFactory = new StatementExceptionFactory();
32
    }
33
34
    public function query($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap): IQueryResult
35
    {
36
        if ($sqlFragmentPatternOrRecipe instanceof IRelationRecipe) {
37
            $recipe = $this->setupRecipe($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap);
38
        }
39
        else {
40
            $recipe = SqlRelationRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap);
41
        }
42
43
        $sql = $recipe->toSql($this->typeCtl->getTypeDictionary());
44
        return $this->rawQuery($sql);
45
    }
46
47
    public function command($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap): ICommandResult
48
    {
49
        if ($sqlFragmentPatternOrRecipe instanceof ICommandRecipe) {
50
            $recipe = $this->setupRecipe($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap);
51
        }
52
        else {
53
            $recipe = SqlCommandRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap);
54
        }
55
56
        $sql = $recipe->toSql($this->typeCtl->getTypeDictionary());
57
        return $this->rawCommand($sql);
58
    }
59
60
    private function setupRecipe($recipe, ...$args)
61
    {
62
        if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
63
            if ($recipe instanceof ISqlPatternRecipe) {
64
                if (count($args) > 1) {
65
                    throw new \InvalidArgumentException('Too many arguments given.');
66
                }
67
                $namedParamsMap = $args[0];
68
                $recipe->setParams($namedParamsMap);
69
            }
70
            else {
71
                throw new \InvalidArgumentException('Too many arguments given.');
72
            }
73
        }
74
75
        return $recipe;
76
    }
77
78
    public function rawQuery(string $sqlQuery): IQueryResult
79
    {
80
        $result = $this->executeRawStatement($sqlQuery, $resultHandler);
81
        if ($result instanceof IQueryResult) {
82
            return $result;
83
        }
84
        else {
85
            trigger_error(
86
                'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' .
87
                'Returning an empty relation. Consider calling command() or rawCommand() instead. ' .
88
                "SQL statement: $sqlQuery",
89
                E_USER_WARNING
90
            );
91
            return new QueryResult($resultHandler, $this->typeCtl->getTypeDictionary(), $result->getLastNotice());
92
        }
93
    }
94
95
    public function rawCommand(string $sqlCommand) : ICommandResult
96
    {
97
        $result = $this->executeRawStatement($sqlCommand, $resultHandler);
98
        if ($result instanceof ICommandResult) {
99
            return $result;
100
        }
101
        else {
102
            trigger_error(
103
                'The supplied SQL statement was supposed to be a command, but it returned a result set. ' .
104
                'Returning an empty result. Consider calling query() or rawQuery() instead. ' .
105
                "SQL statement: $sqlCommand",
106
                E_USER_WARNING
107
            );
108
            return new CommandResult($resultHandler, $result->getLastNotice());
109
        }
110
    }
111
112
    private function executeRawStatement(string $sqlStatement, &$resultHandler = null): IResult
113
    {
114
        $connHandler = $this->connCtl->requireConnection();
115
116
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
117
            usleep(1);
118
        }
119
120
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
121
        $sent = pg_send_query_params($connHandler, $sqlStatement, []); // TODO: consider trapping errors to get more detailed error message
122
        if (!$sent) {
123
            throw new ConnectionException('Error sending the query to the database.');
124
        }
125
126
        $resultHandler = pg_get_result($connHandler);
127
        if ($resultHandler === false) {
128
            throw new ConnectionException('No results received from the database.');
129
        }
130
        /* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
131
         * Even worse, a loop might actually be needed according to
132
         * http://www.postgresql.org/message-id/flat/[email protected]#[email protected],
133
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
134
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
135
         */
136
        while (pg_get_result($connHandler) !== false) {
137
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
138
        }
139
140
        return $this->processResult($connHandler, $resultHandler, $sqlStatement);
141
    }
142
143
    public function rawMultiStatement($sqlStatements)
144
    {
145
        if (!is_array($sqlStatements) && !$sqlStatements instanceof \Traversable) {
146
            throw new \InvalidArgumentException('$sqlStatements is neither array nor \Traversable object');
147
        }
148
149
        $results = [];
150
        foreach ($sqlStatements as $stmtKey => $stmt) {
151
            $results[$stmtKey] = $this->executeRawStatement($stmt);
152
        }
153
        return $results;
154
    }
155
156
    public function runScript(string $sqlScript)
157
    {
158
        $connHandler = $this->connCtl->requireConnection();
159
160
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
161
            usleep(1);
162
        }
163
164
        $sent = pg_send_query($connHandler, $sqlScript);
165
        if (!$sent) {
166
            throw new ConnectionException('Error sending the query to the database.');
167
        }
168
169
        $resHandlers = [];
170
        while (($res = pg_get_result($connHandler)) !== false) {
171
            $resHandlers[] = $res; // NOTE: cannot process the result right away - the remaining results must all be read, or they would, in case of error, block the connection from accepting further queries
172
        }
173
        $results = [];
174
        foreach ($resHandlers as $resHandler) {
175
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
176
        }
177
        return $results;
178
    }
179
180
    /**
181
     * @param resource $connHandler
182
     * @param resource $resHandler
183
     * @param string $query
184
     * @return IResult
185
     * @throws StatementException upon an SQL statement error
186
     */
187
    private function processResult($connHandler, $resHandler, $query)
188
    {
189
        $notice = $this->getLastResultNotice();
190
        $stat = pg_result_status($resHandler);
191
        switch ($stat) {
192
            case PGSQL_COMMAND_OK:
193
                return new CommandResult($resHandler, $notice);
194
            case PGSQL_TUPLES_OK:
195
                $typeDict = $this->typeCtl->getTypeDictionary();
196
                return new QueryResult($resHandler, $typeDict, $notice);
197
            case PGSQL_COPY_IN:
198
                return new CopyInResult($connHandler, $resHandler, $notice);
199
            case PGSQL_COPY_OUT:
200
                return new CopyOutResult($connHandler, $resHandler, $notice);
201
202
            case PGSQL_EMPTY_QUERY:
203
            case PGSQL_BAD_RESPONSE:
204
            case PGSQL_NONFATAL_ERROR:
205
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
206
            case PGSQL_FATAL_ERROR:
207
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
208
209
            default:
210
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
211
        }
212
    }
213
214
    private function getLastResultNotice()
215
    {
216
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
217
        $connNotice = $this->connCtl->getLastNotice();
218
        if ($resNotice !== $connNotice) {
219
            $this->connCtl->setLastNotice($resNotice);
220
            return $resNotice;
221
        }
222
        else {
223
            return null;
224
        }
225
    }
226
227
    public function getStatementExceptionFactory(): StatementExceptionFactory
228
    {
229
        return $this->stmtExFactory;
230
    }
231
}
232