1<?php
2
3namespace Doctrine\DBAL\Driver\SQLAnywhere;
4
5use Doctrine\DBAL\Driver\Statement;
6use Doctrine\DBAL\Driver\StatementIterator;
7use Doctrine\DBAL\FetchMode;
8use Doctrine\DBAL\ParameterType;
9use IteratorAggregate;
10use PDO;
11use ReflectionClass;
12use ReflectionObject;
13use stdClass;
14use const SASQL_BOTH;
15use function array_key_exists;
16use function func_get_args;
17use function func_num_args;
18use function gettype;
19use function is_array;
20use function is_int;
21use function is_object;
22use function is_resource;
23use function is_string;
24use function sasql_fetch_array;
25use function sasql_fetch_assoc;
26use function sasql_fetch_object;
27use function sasql_fetch_row;
28use function sasql_prepare;
29use function sasql_stmt_affected_rows;
30use function sasql_stmt_bind_param_ex;
31use function sasql_stmt_errno;
32use function sasql_stmt_error;
33use function sasql_stmt_execute;
34use function sasql_stmt_field_count;
35use function sasql_stmt_reset;
36use function sasql_stmt_result_metadata;
37use function sprintf;
38
39/**
40 * SAP SQL Anywhere implementation of the Statement interface.
41 */
42class SQLAnywhereStatement implements IteratorAggregate, Statement
43{
44    /** @var resource The connection resource. */
45    private $conn;
46
47    /** @var string Name of the default class to instantiate when fetching class instances. */
48    private $defaultFetchClass = '\stdClass';
49
50    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
51    private $defaultFetchClassCtorArgs = [];
52
53    /** @var int Default fetch mode to use. */
54    private $defaultFetchMode = FetchMode::MIXED;
55
56    /** @var resource The result set resource to fetch. */
57    private $result;
58
59    /** @var resource The prepared SQL statement to execute. */
60    private $stmt;
61
62    /** @var mixed[] The references to bound parameter values. */
63    private $boundValues = [];
64
65    /**
66     * Prepares given statement for given connection.
67     *
68     * @param resource $conn The connection resource to use.
69     * @param string   $sql  The SQL statement to prepare.
70     *
71     * @throws SQLAnywhereException
72     */
73    public function __construct($conn, $sql)
74    {
75        if (! is_resource($conn)) {
76            throw new SQLAnywhereException('Invalid SQL Anywhere connection resource: ' . $conn);
77        }
78
79        $this->conn = $conn;
80        $this->stmt = sasql_prepare($conn, $sql);
81
82        if (! is_resource($this->stmt)) {
83            throw SQLAnywhereException::fromSQLAnywhereError($conn);
84        }
85    }
86
87    /**
88     * {@inheritdoc}
89     *
90     * @throws SQLAnywhereException
91     */
92    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null)
93    {
94        switch ($type) {
95            case ParameterType::INTEGER:
96            case ParameterType::BOOLEAN:
97                $type = 'i';
98                break;
99
100            case ParameterType::LARGE_OBJECT:
101                $type = 'b';
102                break;
103
104            case ParameterType::NULL:
105            case ParameterType::STRING:
106            case ParameterType::BINARY:
107                $type = 's';
108                break;
109
110            default:
111                throw new SQLAnywhereException('Unknown type: ' . $type);
112        }
113
114        $this->boundValues[$column] =& $variable;
115
116        if (! sasql_stmt_bind_param_ex($this->stmt, $column - 1, $variable, $type, $variable === null)) {
117            throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
118        }
119
120        return true;
121    }
122
123    /**
124     * {@inheritdoc}
125     */
126    public function bindValue($param, $value, $type = ParameterType::STRING)
127    {
128        return $this->bindParam($param, $value, $type);
129    }
130
131    /**
132     * {@inheritdoc}
133     *
134     * @throws SQLAnywhereException
135     */
136    public function closeCursor()
137    {
138        if (! sasql_stmt_reset($this->stmt)) {
139            throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
140        }
141
142        return true;
143    }
144
145    /**
146     * {@inheritdoc}
147     */
148    public function columnCount()
149    {
150        return sasql_stmt_field_count($this->stmt);
151    }
152
153    /**
154     * {@inheritdoc}
155     */
156    public function errorCode()
157    {
158        return sasql_stmt_errno($this->stmt);
159    }
160
161    /**
162     * {@inheritdoc}
163     */
164    public function errorInfo()
165    {
166        return sasql_stmt_error($this->stmt);
167    }
168
169    /**
170     * {@inheritdoc}
171     *
172     * @throws SQLAnywhereException
173     */
174    public function execute($params = null)
175    {
176        if (is_array($params)) {
177            $hasZeroIndex = array_key_exists(0, $params);
178
179            foreach ($params as $key => $val) {
180                if ($hasZeroIndex && is_int($key)) {
181                    $this->bindValue($key + 1, $val);
182                } else {
183                    $this->bindValue($key, $val);
184                }
185            }
186        }
187
188        if (! sasql_stmt_execute($this->stmt)) {
189            throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
190        }
191
192        $this->result = sasql_stmt_result_metadata($this->stmt);
193
194        return true;
195    }
196
197    /**
198     * {@inheritdoc}
199     *
200     * @throws SQLAnywhereException
201     */
202    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
203    {
204        if (! is_resource($this->result)) {
205            return false;
206        }
207
208        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
209
210        switch ($fetchMode) {
211            case FetchMode::COLUMN:
212                return $this->fetchColumn();
213
214            case FetchMode::ASSOCIATIVE:
215                return sasql_fetch_assoc($this->result);
216
217            case FetchMode::MIXED:
218                return sasql_fetch_array($this->result, SASQL_BOTH);
219
220            case FetchMode::CUSTOM_OBJECT:
221                $className = $this->defaultFetchClass;
222                $ctorArgs  = $this->defaultFetchClassCtorArgs;
223
224                if (func_num_args() >= 2) {
225                    $args      = func_get_args();
226                    $className = $args[1];
227                    $ctorArgs  = $args[2] ?? [];
228                }
229
230                $result = sasql_fetch_object($this->result);
231
232                if ($result instanceof stdClass) {
233                    $result = $this->castObject($result, $className, $ctorArgs);
234                }
235
236                return $result;
237
238            case FetchMode::NUMERIC:
239                return sasql_fetch_row($this->result);
240
241            case FetchMode::STANDARD_OBJECT:
242                return sasql_fetch_object($this->result);
243
244            default:
245                throw new SQLAnywhereException('Fetch mode is not supported: ' . $fetchMode);
246        }
247    }
248
249    /**
250     * {@inheritdoc}
251     */
252    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
253    {
254        $rows = [];
255
256        switch ($fetchMode) {
257            case FetchMode::CUSTOM_OBJECT:
258                while (($row = $this->fetch(...func_get_args())) !== false) {
259                    $rows[] = $row;
260                }
261                break;
262
263            case FetchMode::COLUMN:
264                while (($row = $this->fetchColumn()) !== false) {
265                    $rows[] = $row;
266                }
267                break;
268
269            default:
270                while (($row = $this->fetch($fetchMode)) !== false) {
271                    $rows[] = $row;
272                }
273        }
274
275        return $rows;
276    }
277
278    /**
279     * {@inheritdoc}
280     */
281    public function fetchColumn($columnIndex = 0)
282    {
283        $row = $this->fetch(FetchMode::NUMERIC);
284
285        if ($row === false) {
286            return false;
287        }
288
289        return $row[$columnIndex] ?? null;
290    }
291
292    /**
293     * {@inheritdoc}
294     */
295    public function getIterator()
296    {
297        return new StatementIterator($this);
298    }
299
300    /**
301     * {@inheritdoc}
302     */
303    public function rowCount()
304    {
305        return sasql_stmt_affected_rows($this->stmt);
306    }
307
308    /**
309     * {@inheritdoc}
310     */
311    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
312    {
313        $this->defaultFetchMode          = $fetchMode;
314        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
315        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;
316    }
317
318    /**
319     * Casts a stdClass object to the given class name mapping its' properties.
320     *
321     * @param stdClass      $sourceObject     Object to cast from.
322     * @param string|object $destinationClass Name of the class or class instance to cast to.
323     * @param mixed[]       $ctorArgs         Arguments to use for constructing the destination class instance.
324     *
325     * @return object
326     *
327     * @throws SQLAnywhereException
328     */
329    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
330    {
331        if (! is_string($destinationClass)) {
332            if (! is_object($destinationClass)) {
333                throw new SQLAnywhereException(sprintf(
334                    'Destination class has to be of type string or object, %s given.',
335                    gettype($destinationClass)
336                ));
337            }
338        } else {
339            $destinationClass = new ReflectionClass($destinationClass);
340            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
341        }
342
343        $sourceReflection           = new ReflectionObject($sourceObject);
344        $destinationClassReflection = new ReflectionObject($destinationClass);
345
346        foreach ($sourceReflection->getProperties() as $sourceProperty) {
347            $sourceProperty->setAccessible(true);
348
349            $name  = $sourceProperty->getName();
350            $value = $sourceProperty->getValue($sourceObject);
351
352            if ($destinationClassReflection->hasProperty($name)) {
353                $destinationProperty = $destinationClassReflection->getProperty($name);
354
355                $destinationProperty->setAccessible(true);
356                $destinationProperty->setValue($destinationClass, $value);
357            } else {
358                $destinationClass->$name = $value;
359            }
360        }
361
362        return $destinationClass;
363    }
364}
365