aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/StudipPDOStatement.php
blob: 5aab1cc807e25b87810c097db5f85dd1e8d5d12c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
<?php
/**
 * This is a "fake" PDOStatement implementation that behaves mostly like
 * a real statement object, but has some additional features:
 *
 * - Parameters passed to execute() are quoted according to their PHP type.
 * - A PHP NULL value will result in an actual SQL NULL value in the query.
 * - Array types are supported for all placeholders ("WHERE value IN (?)").
 * - Positional and named parameters can be mixed in the same query.
 *
 * @author      Elmar Ludwig
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */
class StudipPDOStatement implements IteratorAggregate
{
    protected $db;
    protected $query;
    protected $options;
    protected $columns;
    protected $params;
    protected $count;
    protected $stmt;

    /**
     * Initializes a new StudipPDOStatement instance.
     */
    public function __construct($db, $query, $options)
    {
        $this->db = $db;
        $this->query = $query;
        $this->options = $options;
        $this->params = [];
    }

    /**
     * Injects a PDOStatement
     */
    public function setStatement(PDOStatement $statement)
    {
        $this->stmt = $statement;
    }

    /**
     * Arranges to have a particular variable bound to a given column in
     * the result-set from a query. Each call to fetch() or fetchAll()
     * will update all the variables that are bound to columns.
     */
    public function bindColumn($column, &$param/*, ...*/)
    {
        $args = func_get_args();
        $args[1] = &$param;
        $this->columns[] = $args;
        return true;
    }

    /**
     * Binds a PHP variable to a corresponding named or question mark place-
     * holder in the SQL statement that was used to prepare the statement.
     * Unlike bindValue(), the variable is bound as a reference and will
     * only be evaluated at the time that execute() is called.
     */
    public function bindParam($parameter, &$variable, $data_type = null)
    {
        if (is_string($parameter) && $parameter[0] !== ':') {
            $parameter = ':' . $parameter;
        }

        $this->params[$parameter] = ['value' => &$variable, 'type' => $data_type];
        return true;
    }

    /**
     * Binds a value to a corresponding named or question mark placeholder
     * in the SQL statement that was used to prepare the statement.
     */
    public function bindValue($parameter, $value, $data_type = null)
    {
        if (is_string($parameter) && $parameter[0] !== ':') {
            $parameter = ':' . $parameter;
        }

        $this->params[$parameter] = ['value' => $value, 'type' => $data_type];
        return true;
    }

    /**
     * Forwards all unknown methods to the actual statement object.
     */
    public function __call($name, array $arguments)
    {
        $callable = [$this->stmt, $name];
        if (!is_callable($callable)) {
            throw new BadMethodCallException();
        }

        return call_user_func_array($callable, $arguments);
    }

    /**
     * Forwards all Iterator methods to the actual statement object.
     */
    public function getIterator()
    {
        return $this->stmt;
    }

    /**
     * Executes the prepared statement and returns a PDOStatement object.
     */
    public function execute($input_parameters = NULL)
    {
        // bind additional parameters from execute()
        if (isset($input_parameters)) {
            foreach ($input_parameters as $key => $value) {
                $this->bindValue(is_int($key) ? $key + 1 : $key, $value, NULL);
            }
        }

        // emulate prepared statement if necessary
        $emulate_prepare = false;

        foreach ($this->params as $key => $param) {
            if ($param['type'] === StudipPDO::PARAM_ARRAY ||
                $param['type'] === StudipPDO::PARAM_COLUMN ||
                $param['type'] === NULL && !is_string($param['value'])) {
                $emulate_prepare = true;
                break;
            }
        }

        // build the actual query string and prepared statement
        if ($emulate_prepare) {
            $this->count = 1;
            $query = preg_replace_callback('/\?|:\w+/', [$this, 'replaceParam'], $this->query);
        } else {
            $query = $this->query;
        }

        $this->stmt = $this->db->prepareStatement($query, $this->options);

        // bind query parameters on the actual statement
        if (!$emulate_prepare) {
            foreach ($this->params as $key => $param) {
                $this->stmt->bindValue($key, $param['value'], $param['type'] ?: PDO::PARAM_STR);
            }
        }

        // set up column bindings on the actual statement
        if (isset($this->columns)) {
            foreach ($this->columns as $args) {
                call_user_func_array([$this->stmt, 'bindColumn'], $args);
            }
        }

        return $this->stmt->execute();
    }

    /**
     * Replaces a placeholder with the corresponding parameter value.
     * Throws an exception if there is no corresponding value.
     */
    protected function replaceParam($matches)
    {
        $name = $matches[0];

        if ($name == '?') {
            $key = $this->count++;
        } else {
            $key = $name;
        }

        if (!isset($this->params[$key])) {
            throw new PDOException('missing parameter in query: ' . $key);
        }

        return $this->db->quote($this->params[$key]['value'], $this->params[$key]['type']);
    }

    /**
     * Returns the result set rows as a grouped associative array. The first field
     * of each row is used as the array's keys.
     * optionally apply given callable on each grouped row to aggregate results
     * if no callable is given, 'current' is used, to return the first entry of the grouped row
     *
     * @param int   $fetch_style    Either PDO::FETCH_ASSOC or PDO::FETCH_COLUMN
     * @param callable $group_func  function to aggregate grouped rows
     * @return array grouped result set
     */
    public function fetchGrouped($fetch_style = PDO::FETCH_ASSOC, $group_func = 'current') {
        if (!($fetch_style & (PDO::FETCH_ASSOC | PDO::FETCH_COLUMN))) {
            throw new PDOException('Fetch style not supported, try FETCH_ASSOC or FETCH_COLUMN');
        }

        $fetch_style |= PDO::FETCH_GROUP;
        $rows = $this->fetchAll($fetch_style);

        return is_callable($group_func) ? array_map($group_func, $rows) : $rows;
    }

    /**
     * Returns the result set rows as a grouped associative array. The first field
     * of each row is used as the array's keys, the other one is grouped
     * use only when selecting 2 columns
     * optionally apply given callable on each grouped row to aggregate results
     *
     * @param callable $group_func  function to aggregate grouped rows
     * @return array grouped result set
     */
    public function fetchGroupedPairs($group_func = null)
    {
        return $this->fetchGrouped(PDO::FETCH_COLUMN, $group_func);
    }

    /**
     * Returns result rows as associative array, first colum as key,
     * second as value. Use only when selecting 2 columns
     *
     * @return array result set
     */
    public function fetchPairs()
    {
        return $this->fetchAll(PDO::FETCH_KEY_PAIR);
    }

    /**
     * Returns sequential array with values from first colum
     *
     * @return array first row result set
     */
    public function fetchFirst()
    {
        return $this->fetchAll(PDO::FETCH_COLUMN);
    }

    /**
     * Returns only first row of result set as associative array
     *
     * @return array first row result set
     */
    public function fetchOne()
    {
        $data = $this->fetch(PDO::FETCH_ASSOC);
        return $data ?: [];
    }
}