Completed
Pull Request — master (#3)
by Rémy
01:22
created

Model::unsetConnectionResolver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
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
    }
39
40
    public static function unsetConnectionResolver(): void
41
    {
42
        static::$connectionResolver = null;
43
    }
44
45
    public function getTable(): string
46
    {
47
        if ($this->_table !== null) {
48
            return $this->_table;
49
        }
50
51
        return strtolower(str_replace('\\', '_', static::class));
52
    }
53
54
    public function getConnection(): \PDO
55
    {
56
        return static::$connectionResolver->connection($this->_connection);
57
    }
58
59
    /**
60
     * @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
        return $query->findOne();
68
    }
69
70
    /**
71
     * @return static[]
72
     */
73
    public static function findAll(): array
74
    {
75
        return (new static)->query()->findAll();
76
    }
77
78
    public function query(): QueryBuilder
79
    {
80
        // maybe pass $this?
81
        return new QueryBuilder(
82
            $this->getConnection(),
83
            new ModelBuilder(),
84
            static::class,
85
            new Helper(),
86
            $this->getTable(),
87
            $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
        $ormQuery->setQuery($query);
96
        if (!empty($params)) {
97
            $ormQuery->setParams($params);
98
        }
99
        return $ormQuery;
100
    }
101
102
    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
            foreach ($columns as $key) {
109
                $result[$key] = $this->get($key);
110
            }
111
            return $result;
112
        }
113
114
        return $this->get($this->_idColumn);
115
    }
116
117
    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
                if (empty($value)) {
125
                    return false;
126
                }
127
            }
128
            return true;
129
        }
130
131
        $value = $this->get($this->_idColumn);
132
        return !empty($value);
133
    }
134
135
    /**
136
     * @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
146
        $idColumns = (array)$this->_idColumn;
147
        $doMerge = $this->hasId();
148
149
        // ignore fields
150
        $notSetFields = $this->_ignoreColumns;
151
152
        /**
153
         * Different queries are built for each driver
154
         *
155
         * IDEA: probably split into methods (saveMySQL, saveSQLite)
156
         */
157
        if ($helper->isMySQL($dbh)) {
158
            /**
159
             * MySQL
160
             * Instead of REPLACE INTO we use INSERT INTO ON DUPLICATE KEY UPDATE.
161
             * Because REPLACE INTO does DELETE and INSERT,
162
             * which does not play nice with TRIGGERs and FOREIGN KEY CONSTRAINTS
163
             */
164
            $sql = 'INSERT INTO ' . $quotedTable . ' ';
165
166
            $insertData = [];
167
            $updateData = [];
168
169
            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
177
                if ($doMerge && !in_array($column, $idColumns, true)) {
178
                    $updateData[] = $quotedColumn . ' = VALUES(' . $quotedColumn . ')';
179
                }
180
            }
181
            unset($column, $value, $quotedColumn);
182
183
            // insert
184
            $sql .=
185
                '(' . implode(', ', array_keys($insertData)) . ')' .
186
                ' VALUES ' .
187
                '(' . implode(', ', $insertData) . ')';
188
189
            if ($doMerge && count($updateData) > 0) {
190
                // update
191
                $sql .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updateData);
192
            }
193
194
            // execute (most likely throws PDOException if there is an error)
195
            if ($dbh->exec($sql) === false) {
196
                return false; // @codeCoverageIgnore
197
            }
198
199
            // update generated id
200
            if ($this->_autoId && !$doMerge) {
201
                // last insert id
202
                $this->set($this->_idColumn, $dbh->lastInsertId());
203
            }
204
205
            return true;
206
        }
207
208
        /**
209
         * SQLite
210
         */
211
        if ($doMerge) {
212
            $sql = 'INSERT OR REPLACE INTO ' . $quotedTable . ' ';
213
        } else {
214
            $sql = 'INSERT INTO ' . $quotedTable . ' ';
215
        }
216
217
        // build (column) VALUES (values)
218
        $quotedData = [];
219
        foreach ($this->_data as $column => $value) {
220
            if (in_array($column, $notSetFields, true)) {
221
                continue;
222
            }
223
224
            $quotedData[$quoteIdentifier($column)] = $helper->quote($dbh, $value);
225
        }
226
        unset($column, $value);
227
228
        $sql .= '(' . implode(', ', array_keys($quotedData)) . ') VALUES (' . implode(', ', $quotedData) . ')';
229
230
        // execute (most likely throws PDOException if there is an error)
231
        if ($dbh->exec($sql) === false) {
232
            return false; // @codeCoverageIgnore
233
        }
234
235
        // update generated id
236
        if ($this->_autoId && !$this->hasId()) {
237
            // last insert id
238
            $this->set($this->_idColumn, $dbh->lastInsertId());
239
        }
240
241
        return true;
242
    }
243
244
    public function delete(): bool
245
    {
246
        $dbh = $this->getConnection();
247
        $helper = new Helper();
248
        $quoteIdentifier = $helper->getIdentifierQuoter($dbh);
249
250
        $idColumns = (array)$this->_idColumn;
251
252
        $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
        return $dbh->exec($sql) > 0;
260
    }
261
262
    // data access
263
    public function getData(): array
264
    {
265
        return $this->_data;
266
    }
267
268
    public function setData(array $data): void
269
    {
270
        $this->_data = $data;
271
    }
272
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
    {
275
        if (array_key_exists($name, $this->_data)) {
276
            return $this->_data[$name];
277
        }
278
        return null;
279
    }
280
281
    public function set(string $name, $value): Model
282
    {
283
        $this->_data[$name] = $value;
284
        return $this;
285
    }
286
287
    public function has(string $name): bool
288
    {
289
        return isset($this->_data[$name]);
290
    }
291
292
    /**
293
     * Remove data from the model
294
     */
295
    public function remove(string $name): void
296
    {
297
        $this->_data[$name] = null;
298
    }
299
300
    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
        return $this->get($name);
303
    }
304
305
    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
    }
314
315
    public function __unset(string $name): void
316
    {
317
        $this->remove($name);
318
    }
319
320
    public function copyDataFrom($object, array $except = []): void
321
    {
322
        foreach ($object as $key => $value) {
323
            if (!in_array($key, $except, true)) {
324
                $this->set($key, $value);
325
            }
326
        }
327
    }
328
329
    // Iterator
330
    public function rewind(): void
331
    {
332
        reset($this->_data);
333
    }
334
335
    public function current()
336
    {
337
        return current($this->_data);
338
    }
339
340
    public function key()
341
    {
342
        return key($this->_data);
343
    }
344
345
    public function next(): void
346
    {
347
        next($this->_data);
348
    }
349
350
    public function valid(): bool
351
    {
352
        return key($this->_data) !== null;
353
    }
354
355
    // JsonSerializable
356
    public function jsonSerialize()
357
    {
358
        return $this->_data;
359
    }
360
}
361