Completed
Pull Request — master (#3)
by Rémy
08:33
created

Model::setConnectionResolver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 1
cts 1
cp 1
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * @author Rémy M. Böhler <[email protected]>
4
 */
5
declare(strict_types=1);
6
7
namespace Rorm;
8
9
use Iterator;
10
use JsonSerializable;
11
12
abstract class Model implements Iterator, JsonSerializable
13
{
14
    /** @var string|null */
15
    protected $_table;
16
17
    /** @var string|array */
18
    protected $_idColumn = 'id';
19
20
    /** @var bool */
21
    protected $_autoId = true;
22
23
    /** @var array */
24
    protected $_ignoreColumns = [];
25
26
    /** @var string */
27
    protected $_connection;
28
29
    /** @var array */
30
    protected $_data = [];
31
32
    /** @var ConnectionResolver */
33
    protected static $connectionResolver;
34
35
    public static function setConnectionResolver(ConnectionResolver $resolver): void
36
    {
37
        static::$connectionResolver = $resolver;
38 27
    }
39
40 27
    public static function unsetConnectionResolver(): void
41 18
    {
42
        static::$connectionResolver = null;
43
    }
44 11
45
    public function getTable(): string
46
    {
47
        if ($this->_table !== null) {
48
            return $this->_table;
49
        }
50 29
51
        return strtolower(str_replace('\\', '_', static::class));
52 29
    }
53
54
    public function getConnection(): \PDO
55
    {
56
        return static::$connectionResolver->connection($this->_connection);
57
    }
58 19
59
    /**
60 19
     * @return static
0 ignored issues
show
Documentation introduced by
Should the return type not be Model|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
61
     */
62
    public static function find($id): ?Model
63
    {
64
        $query = (new static)->query();
65
        $query->whereId(...func_get_args());
66
67 5
        return $query->findOne();
68
    }
69 5
70 5
    /**
71 5
     * @return static[]
72
     */
73
    public static function findAll(): array
74
    {
75
        return (new static)->query()->findAll();
76
    }
77 2
78
    public function query(): QueryBuilder
79 2
    {
80
        // maybe pass $this?
81
        return new QueryBuilder(
82
            $this->getConnection(),
83
            new ModelBuilder(),
84
            static::class,
85 24
            new Helper(),
86
            $this->getTable(),
87 24
            $this->_idColumn
88
        );
89
    }
90
91
    public function customQuery(string $query, array $params = []): Query
92
    {
93
        $model = new static;
94
        $ormQuery = new Query($model->getConnection(), new ModelBuilder(), static::class);
95 3
        $ormQuery->setQuery($query);
96
        if (!empty($params)) {
97 3
            $ormQuery->setParams($params);
98 3
        }
99 3
        return $ormQuery;
100 1
    }
101 1
102 3
    public function getId()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
103
    {
104
        if (is_array($this->_idColumn)) {
105
            $result = [];
106
            /** @var string[] $columns */
107
            $columns = $this->_idColumn;
108 3
            foreach ($columns as $key) {
109
                $result[$key] = $this->get($key);
110 3
            }
111 1
            return $result;
112 1
        }
113 1
114 1
        return $this->get($this->_idColumn);
115 1
    }
116
117 3
    public function hasId(): bool
118
    {
119
        if (is_array($this->_idColumn)) {
120
            /** @var string[] $columns */
121
            $columns = $this->_idColumn;
122
            foreach ($columns as $key) {
123
                $value = $this->get($key);
124 10
                if (empty($value)) {
125
                    return false;
126 10
                }
127 3
            }
128 3
            return true;
129 3
        }
130 1
131
        $value = $this->get($this->_idColumn);
132 3
        return !empty($value);
133 3
    }
134
135 8
    /**
136 8
     * @throws QueryException
137
     * @throws \PDOException
138
     */
139
    public function save(): bool
140
    {
141
        $dbh = $this->getConnection();
142
        $helper = new Helper();
143
        $quoteIdentifier = $helper->getIdentifierQuoter($dbh);
144
        $quotedTable = $quoteIdentifier($this->getTable());
145 10
146
        $idColumns = (array)$this->_idColumn;
147 10
        $doMerge = $this->hasId();
148 1
149
        // ignore fields
150
        $notSetFields = $this->_ignoreColumns;
151 9
152 9
        /**
153 9
         * Different queries are built for each driver
154
         *
155 9
         * IDEA: probably split into methods (saveMySQL, saveSQLite)
156 9
         */
157 7
        if ($helper->isMySQL($dbh)) {
158 7
            /**
159 9
             * MySQL
160
             * Instead of REPLACE INTO we use INSERT INTO ON DUPLICATE KEY UPDATE.
161
             * Because REPLACE INTO does DELETE and INSERT,
162 9
             * which does not play nice with TRIGGERs and FOREIGN KEY CONSTRAINTS
163
             */
164
            $sql = 'INSERT INTO ' . $quotedTable . ' ';
165
166
            $insertData = [];
167
            $updateData = [];
168
169 9
            foreach ($this->_data as $column => $value) {
170
                if (in_array($column, $notSetFields, true)) {
171
                    continue;
172
                }
173
174
                $quotedColumn = $quoteIdentifier($column);
175
                $insertData[$quotedColumn] = $helper->quote($dbh, $value);
176 5
177
                if ($doMerge && !in_array($column, $idColumns, true)) {
178 5
                    $updateData[] = $quotedColumn . ' = VALUES(' . $quotedColumn . ')';
179 5
                }
180
            }
181 5
            unset($column, $value, $quotedColumn);
182 5
183 1
            // insert
184
            $sql .=
185
                '(' . implode(', ', array_keys($insertData)) . ')' .
186 5
                ' VALUES ' .
187 5
                '(' . implode(', ', $insertData) . ')';
188
189 5
            if ($doMerge && count($updateData) > 0) {
190 2
                // update
191 2
                $sql .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updateData);
192 5
            }
193 5
194
            // execute (most likely throws PDOException if there is an error)
195
            if ($dbh->exec($sql) === false) {
196
                return false; // @codeCoverageIgnore
197 5
            }
198 5
199 5
            // update generated id
200
            if ($this->_autoId && !$doMerge) {
201 5
                // last insert id
202
                $this->set($this->_idColumn, $dbh->lastInsertId());
203 2
            }
204 2
205
            return true;
206
        }
207 5
208
        /**
209
         * SQLite
210
         */
211
        if ($doMerge) {
212 5
            $sql = 'INSERT OR REPLACE INTO ' . $quotedTable . ' ';
213
        } else {
214 4
            $sql = 'INSERT INTO ' . $quotedTable . ' ';
215 4
        }
216
217 5
        // build (column) VALUES (values)
218
        $quotedData = [];
219
        foreach ($this->_data as $column => $value) {
220
            if (in_array($column, $notSetFields, true)) {
221
                continue;
222 4
            }
223 2
224 2
            $quotedData[$quoteIdentifier($column)] = $helper->quote($dbh, $value);
225 3
        }
226
        unset($column, $value);
227
228
        $sql .= '(' . implode(', ', array_keys($quotedData)) . ') VALUES (' . implode(', ', $quotedData) . ')';
229 4
230 4
        // execute (most likely throws PDOException if there is an error)
231 4
        if ($dbh->exec($sql) === false) {
232
            return false; // @codeCoverageIgnore
233
        }
234
235 4
        // update generated id
236 4
        if ($this->_autoId && !$this->hasId()) {
237 4
            // last insert id
238
            $this->set($this->_idColumn, $dbh->lastInsertId());
239 4
        }
240
241
        return true;
242 4
    }
243
244
    public function delete(): bool
245
    {
246
        $dbh = $this->getConnection();
247 4
        $helper = new Helper();
248
        $quoteIdentifier = $helper->getIdentifierQuoter($dbh);
249 3
250 3
        $idColumns = (array)$this->_idColumn;
251
252 4
        $where = [];
253
        foreach ($idColumns as $columnName) {
254
            $where[] = $quoteIdentifier($columnName) . ' = ' . $helper->quote($dbh, $this->$columnName);
255
        }
256
257
        $sql = 'DELETE FROM ' . $quoteIdentifier($this->getTable()) . ' WHERE ' . implode(' AND ', $where);
258
259 4
        return $dbh->exec($sql) > 0;
260
    }
261 4
262 4
    // data access
263
    public function getData(): array
264 4
    {
265 4
        return $this->_data;
266 2
    }
267 2
268
    public function setData(array $data): void
269 4
    {
270 4
        $this->_data = $data;
271 4
    }
272 4
273
    public function get(string $name)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
274 4
    {
275
        if (array_key_exists($name, $this->_data)) {
276 4
            return $this->_data[$name];
277
        }
278
        return null;
279
    }
280
281
    public function set(string $name, $value): Model
282
    {
283 2
        $this->_data[$name] = $value;
284
        return $this;
285 2
    }
286
287
    public function has(string $name): bool
288
    {
289
        return isset($this->_data[$name]);
290
    }
291 15
292
    /**
293 15
     * Remove data from the model
294 15
     */
295
    public function remove(string $name): void
296
    {
297
        $this->_data[$name] = null;
298
    }
299
300 15
    public function __get(string $name)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
301
    {
302 15
        return $this->get($name);
303 13
    }
304
305 9
    public function __set(string $name, $value): void
306
    {
307
        $this->set($name, $value);
308
    }
309
310
    public function __isset(string $name): bool
311
    {
312
        return $this->has($name);
313 15
    }
314
315 15
    public function __unset(string $name): void
316 15
    {
317
        $this->remove($name);
318
    }
319
320
    public function copyDataFrom($object, array $except = []): void
321
    {
322
        foreach ($object as $key => $value) {
323 2
            if (!in_array($key, $except, true)) {
324
                $this->set($key, $value);
325 2
            }
326
        }
327
    }
328
329
    // Iterator
330
    public function rewind(): void
331
    {
332
        reset($this->_data);
333 2
    }
334
335 2
    public function current()
336 2
    {
337
        return current($this->_data);
338
    }
339
340
    public function key()
341
    {
342 12
        return key($this->_data);
343
    }
344 12
345
    public function next(): void
346
    {
347
        next($this->_data);
348
    }
349
350
    public function valid(): bool
351 10
    {
352
        return key($this->_data) !== null;
353 10
    }
354 10
355
    // JsonSerializable
356
    public function jsonSerialize()
357
    {
358
        return $this->_data;
359
    }
360
}
361