Completed
Push — master ( 5fdfe9...8a6726 )
by Ondřej
03:21
created

StatementExecution::getStatementExceptionFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\SqlCommandRecipe;
9
use Ivory\Query\SqlRecipe;
10
use Ivory\Query\SqlRelationRecipe;
11
use Ivory\Result\CommandResult;
12
use Ivory\Result\CopyInResult;
13
use Ivory\Result\CopyOutResult;
14
use Ivory\Result\IResult;
15
use Ivory\Result\QueryResult;
16
17
class StatementExecution implements IStatementExecution
18
{
19
    private $connCtl;
20
    private $typeCtl;
21
    private $stmtExFactory;
22
23
    public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl)
24
    {
25
        $this->connCtl = $connCtl;
26
        $this->typeCtl = $typeCtl;
27
        $this->stmtExFactory = new StatementExceptionFactory();
28
    }
29
30 View Code Duplication
    public function query($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
31
    {
32
        if ($sqlFragmentPatternOrRecipe instanceof SqlRecipe) {
33
            $recipe = $sqlFragmentPatternOrRecipe;
34
            if ($fragmentsAndPositionalParamsAndNamedParamsMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fragmentsAndPositionalParamsAndNamedParamsMap 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...
35
                if (count($fragmentsAndPositionalParamsAndNamedParamsMap) > 1) {
36
                    throw new \InvalidArgumentException('Too many arguments given.');
37
                }
38
                $namedParamsMap = $fragmentsAndPositionalParamsAndNamedParamsMap[0];
39
                $recipe->setParams($namedParamsMap);
40
            }
41
        }
42
        else {
43
            $recipe = SqlRelationRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap);
44
        }
45
46
        $sql = $recipe->toSql($this->typeCtl->getTypeDictionary());
47
        return $this->rawQuery($sql);
48
    }
49
50 View Code Duplication
    public function command($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
51
    {
52
        if ($sqlFragmentPatternOrRecipe instanceof SqlRecipe) {
53
            $recipe = $sqlFragmentPatternOrRecipe;
54
            if ($fragmentsAndPositionalParamsAndNamedParamsMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fragmentsAndPositionalParamsAndNamedParamsMap 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...
55
                if (count($fragmentsAndPositionalParamsAndNamedParamsMap) > 1) {
56
                    throw new \InvalidArgumentException('Too many arguments given.');
57
                }
58
                $namedParamsMap = $fragmentsAndPositionalParamsAndNamedParamsMap[0];
59
                $recipe->setParams($namedParamsMap);
60
            }
61
        }
62
        else {
63
            $recipe = SqlCommandRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap);
64
        }
65
66
        $sql = $recipe->toSql($this->typeCtl->getTypeDictionary());
67
        return $this->rawQuery($sql); // FIXME: call rawCommand() instead once it is defined
68
    }
69
70
    public function rawQuery(string $sqlStatement)
71
    {
72
        $connHandler = $this->connCtl->requireConnection();
73
74
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
75
            usleep(1);
76
        }
77
78
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
79
        $sent = pg_send_query_params($connHandler, $sqlStatement, []); // TODO: consider trapping errors to get more detailed error message
80
        if (!$sent) {
81
            throw new ConnectionException('Error sending the query to the database.');
82
        }
83
84
        $res = pg_get_result($connHandler);
85
        if ($res === false) {
86
            throw new ConnectionException('No results received from the database.');
87
        }
88
        /* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
89
         * Even worse, a loop might actually be needed according to
90
         * http://www.postgresql.org/message-id/flat/[email protected]#[email protected],
91
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
92
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
93
         */
94
        while (pg_get_result($connHandler) !== false) {
95
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
96
        }
97
98
        $result = $this->processResult($connHandler, $res, $sqlStatement);
99
100
        // TODO: emit warning if ICommandResult is returned for rawQuery(), or if IQueryResult is returned for command()
101
102
        return $result;
103
    }
104
105
    public function rawMultiQuery($sqlStatements)
106
    {
107
        if (!is_array($sqlStatements) && !$sqlStatements instanceof \Traversable) {
108
            throw new \InvalidArgumentException('$sqlStatements is neither array nor \Traversable object');
109
        }
110
111
        $results = [];
112
        foreach ($sqlStatements as $stmtKey => $stmt) {
113
            $results[$stmtKey] = $this->rawQuery($stmt);
114
        }
115
        return $results;
116
    }
117
118
    public function runScript($sqlScript)
119
    {
120
        $connHandler = $this->connCtl->requireConnection();
121
122
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
123
            usleep(1);
124
        }
125
126
        $sent = pg_send_query($connHandler, $sqlScript);
127
        if (!$sent) {
128
            throw new ConnectionException('Error sending the query to the database.');
129
        }
130
131
        $resHandlers = [];
132
        while (($res = pg_get_result($connHandler)) !== false) {
133
            $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
134
        }
135
        $results = [];
136
        foreach ($resHandlers as $resHandler) {
137
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
138
        }
139
        return $results;
140
    }
141
142
    /**
143
     * @param resource $connHandler
144
     * @param resource $resHandler
145
     * @param string $query
146
     * @return IResult
147
     * @throws StatementException upon an SQL statement error
148
     */
149
    private function processResult($connHandler, $resHandler, $query)
150
    {
151
        $notice = $this->getLastResultNotice();
152
        $stat = pg_result_status($resHandler);
153
        switch ($stat) {
154
            case PGSQL_COMMAND_OK:
155
                return new CommandResult($resHandler, $notice);
156
            case PGSQL_TUPLES_OK:
157
                $typeDict = $this->typeCtl->getTypeDictionary();
158
                return new QueryResult($resHandler, $typeDict, $notice);
159
            case PGSQL_COPY_IN:
160
                return new CopyInResult($connHandler, $resHandler, $notice);
161
            case PGSQL_COPY_OUT:
162
                return new CopyOutResult($connHandler, $resHandler, $notice);
163
164
            case PGSQL_EMPTY_QUERY:
165
            case PGSQL_BAD_RESPONSE:
166
            case PGSQL_NONFATAL_ERROR:
167
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
168
            case PGSQL_FATAL_ERROR:
169
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
170
171
            default:
172
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
173
        }
174
    }
175
176
    private function getLastResultNotice()
177
    {
178
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
179
        $connNotice = $this->connCtl->getLastNotice();
180
        if ($resNotice !== $connNotice) {
181
            $this->connCtl->setLastNotice($resNotice);
182
            return $resNotice;
183
        }
184
        else {
185
            return null;
186
        }
187
    }
188
189
    public function getStatementExceptionFactory()
190
    {
191
        return $this->stmtExFactory;
192
    }
193
}
194