Model::setPrimaryKey()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Db;
9
10
use Garden\Schema\Schema;
11
use Garden\Schema\ValidationException;
12
13
class Model {
14
    use Utils\FetchModeTrait { setFetchMode as private; }
15
16
    const DEFAULT_LIMIT = 30;
17
18
    /**
19
     * @var string The name of the table.
20
     */
21
    private $name;
22
23
    /**
24
     * @var Db
25
     */
26
    private $db;
27
28
    /**
29
     * @var array
30
     */
31
    private $primaryKey;
32
33
    /**
34
     * @var Schema
35
     */
36
    private $schema;
37
38
    /**
39
     * @var int
40
     */
41
    private $defaultLimit = Model::DEFAULT_LIMIT;
42
43
    /**
44
     * @var string[]
45
     */
46
    private $defaultOrder = [];
47
48 16
    public function __construct($name, Db $db, $rowType = null) {
49 16
        $this->name = $name;
50 16
        $this->db = $db;
51
52 16
        $fetchMode = $rowType !== null ? $rowType : $db->getFetchMode();
53 16
        if (!empty($fetchMode)) {
54 16
            $this->setFetchMode(...(array)$fetchMode);
55
        }
56 16
    }
57
58
    /**
59
     * Get the name.
60
     *
61
     * @return string Returns the name.
62
     */
63 1
    public function getName() {
64 1
        return $this->name;
65
    }
66
67
    /**
68
     * Get the primaryKey.
69
     *
70
     * @return array Returns the primaryKey.
71
     */
72 1
    public function getPrimaryKey() {
73 1
        if ($this->primaryKey === null) {
74 1
            $schema = $this->getSchema();
75
76 1
            $pk = [];
77 1
            foreach ($schema->getSchemaArray()['properties'] as $column => $property) {
78 1
                if (!empty($property['primary'])) {
79 1
                    $pk[] = $column;
80
                }
81
            }
82 1
            $this->primaryKey = $pk;
83
        }
84 1
        return $this->primaryKey;
85
    }
86
87
    /**
88
     * Set the primaryKey.
89
     *
90
     * @param string ...$primaryKey The names of the columns in the primary key.
91
     * @return $this
92
     */
93
    protected function setPrimaryKey(...$primaryKey) {
94
        $this->primaryKey = $primaryKey;
95
        return $this;
96
    }
97
98
    /**
99
     * Get the db.
100
     *
101
     * @return Db Returns the db.
102
     */
103 1
    public function getDb() {
104 1
        return $this->db;
105
    }
106
107
    /**
108
     * Set the db.
109
     *
110
     * @param Db $db
111
     * @return $this
112
     */
113
    public function setDb($db) {
114
        $this->db = $db;
115
        return $this;
116
    }
117
118
    /**
119
     * Map primary key values to the primary key name.
120
     *
121
     * @param mixed $id An ID value or an array of ID values. If an array is passed and the model has a mult-column
122
     * primary key then all of the values must be in order.
123
     * @return array Returns an associative array mapping column names to values.
124
     */
125 1
    protected function mapID($id) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
126 1
        $idArray = (array)$id;
127
128 1
        $result = [];
129 1
        foreach ($this->getPrimaryKey() as $i => $column) {
130 1
            if (isset($idArray[$i])) {
131 1
                $result[$column] = $idArray[$i];
132
            } elseif (isset($idArray[$column])) {
133
                $result[$column] = $idArray[$column];
134
            } else {
135
                $result[$column] = null;
136
            }
137
        }
138
139 1
        return $result;
140
    }
141
142
    /**
143
     * Gets the row schema for this model.
144
     *
145
     * @return Schema Returns a schema.
146
     */
147 1
    final public function getSchema() {
148 1
        if ($this->schema === null) {
149 1
            $this->schema = $this->fetchSchema();
150
        }
151 1
        return $this->schema;
152
    }
153
154
    /**
155
     * Fetch the row schema from the database meta info.
156
     *
157
     * This method works fine as-is, but can also be overridden to provide more specific schema information for the model.
158
     * This method is called only once for the object and then is cached in a property so you don't need to implement
159
     * caching of your own.
160
     *
161
     * If you are going to override this method we recommend you still call the parent method and add its result to your schema.
162
     * Here is an example:
163
     *
164
     * ```php
165
     * protected function fetchSchema() {
166
     *     $schema = Schema::parse([
167
     *         'body:s', // make the column required even if it isn't in the db.
168
     *         'attributes:o?' // accept an object instead of string
169
     *     ]);
170
     *
171
     *     $dbSchema = parent::fetchSchema();
172
     *     $schema->add($dbSchema, true);
173
     *
174
     *     return $schema;
175
     * }
176
     * ```
177
     *
178
     * @return Schema Returns the row schema.
179
     */
180 1
    protected function fetchSchema() {
181 1
        $columns = $this->getDb()->fetchColumnDefs($this->name);
182 1
        if ($columns === null) {
183
            throw new \InvalidArgumentException("Cannot fetch schema foor {$this->name}.");
184
        }
185
186
        $schema = [
187 1
            'type' => 'object',
188 1
            'dbtype' => 'table',
189 1
            'properties' => $columns
190
        ];
191
192 1
        $required = $this->requiredFields($columns);
193 1
        if (!empty($required)) {
194 1
            $schema['required'] = $required;
195
        }
196
197 1
        return new Schema($schema);
198
    }
199
200
    /**
201
     * Figure out the schema required fields from a list of columns.
202
     *
203
     * A column is required if it meets all of the following criteria.
204
     *
205
     * - The column does not have an auto increment.
206
     * - The column does not have a default value.
207
     * - The column does not allow null.
208
     *
209
     * @param array $columns An array of column schemas.
210
     */
211 1
    private function requiredFields(array $columns) {
212 1
        $required = [];
213
214 1
        foreach ($columns as $name => $column) {
215 1
            if (empty($column['autoIncrement']) && !isset($column['default']) && empty($column['allowNull'])) {
216 1
                $required[] = $name;
217
            }
218
        }
219
220 1
        return $required;
221
    }
222
223
    /**
224
     * Query the model.
225
     *
226
     * @param array $where A where clause to filter the data.
227
     * @return DatasetInterface
228
     */
229 16
    public function get(array $where) {
230
        $options = [
231 16
            Db::OPTION_FETCH_MODE => $this->getFetchArgs(),
232 16
            'rowCallback' => [$this, 'unserialize']
233
        ];
234
235 16
        $qry = new TableQuery($this->name, $where, $this->db, $options);
236 16
        $qry->setLimit($this->getDefaultLimit())
237 16
            ->setOrder(...$this->getDefaultOrder());
238
239 16
        return $qry;
240
    }
241
242
    /**
243
     * Query the database directly.
244
     *
245
     * @param array $where A where clause to filter the data.
246
     * @param array $options Options to pass to the database. See {@link Db::get()}.
247
     * @return \PDOStatement Returns a statement from the query.
248
     */
249 11
    public function query(array $where, array $options = []) {
250
        $options += [
251 11
            'order' => $this->getDefaultOrder(),
252 11
            'limit' => $this->getDefaultLimit(),
253 11
            Db::OPTION_FETCH_MODE => $this->getFetchArgs()
254
        ];
255
256 11
        $stmt = $this->db->get($this->name, $where, $options);
257 11
        return $stmt;
258
    }
259
260
    /**
261
     * @param mixed $id A primary key value for the model.
262
     * @return mixed|null
263
     */
264 1
    public function getID($id) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
265 1
        $r = $this->get($this->mapID($id));
266 1
        return $r->firstRow();
267
    }
268
269
    /**
270
     * Insert a row into the database.
271
     *
272
     * @param array|\ArrayAccess $row The row to insert.
273
     * @param array $options Options to control the insert.
274
     * @return mixed
275
     * @throws ValidationException Throws an exception if the row doesn't validate.
276
     * @throws \Garden\Schema\RefNotFoundException Throws an exception if the schema configured incorrectly.
277
     */
278 1
    public function insert($row, array $options = []) {
279 1
        $valid = $this->validate($row, false);
280 1
        $serialized = $this->serialize($valid);
281
282 1
        $r = $this->db->insert($this->name, $serialized, $options);
283 1
        return $r;
284
    }
285
286
    /**
287
     * Update a row in the database.
288
     *
289
     * @param array $set The columns to update.
290
     * @param array $where The column update filter.
291
     * @param array $options Options to control the update.
292
     * @return int Returns the number of affected rows.
293
     * @throws ValidationException Throws an exception if the row doesn't validate.
294
     * @throws \Garden\Schema\RefNotFoundException Throws an exception if the schema isn't configured correctly.
295
     */
296
    public function update(array $set, array $where, array $options = []): int {
297
        $valid = $this->validate($set, true);
298
        $serialized = $this->serialize($valid);
299
300
        $r = $this->db->update($this->name, $serialized, $where, $options);
301
        return $r;
302
    }
303
304
    /**
305
     * Update a row in the database with a known primary key.
306
     *
307
     * @param mixed $id A single ID value or an array for multi-column primary keys.
308
     * @param array $set The columns to update.
309
     * @return int Returns the number of affected rows.
310
     * @throws ValidationException Throws an exception if the row doesn't validate.
311
     * @throws \Garden\Schema\RefNotFoundException Throws an exception if you have an invalid schema reference.
312
     */
313
    public function updateID($id, array $set): int {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
314
        $r = $this->update($set, $this->mapID($id));
315
        return $r;
316
    }
317
318
    /**
319
     * Validate a row of data.
320
     *
321
     * @param array|\ArrayAccess $row The row to validate.
322
     * @param bool $sparse Whether or not the validation should be sparse (during update).
323
     * @return array Returns valid data.
324
     * @throws ValidationException Throws an exception if the row doesn't validate.
325
     * @throws \Garden\Schema\RefNotFoundException Throws an exception if you have an invalid schema reference.
326
     */
327 1
    public function validate($row, $sparse = false) {
328 1
        $schema = $this->getSchema();
329 1
        $valid = $schema->validate($row, ['sparse' => $sparse]);
330
331 1
        return $valid;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $valid also could return the type Garden\Schema\Invalid which is incompatible with the documented return type array.
Loading history...
332
    }
333
334
    /**
335
     * Serialize a row of data into a format that can be native to the database.
336
     *
337
     * This method should always take an array of data, even if your model is meant to use objects of some sort. This is
338
     * possible because the row that gets passed into this method is the output of {@link validate()}.
339
     *
340
     * @param array $row The row to serialize.
341
     * @return array Returns a row of serialized data.
342
     */
343 1
    public function serialize(array $row) {
344 1
        return $row;
345
    }
346
347
    /**
348
     * Unserialize a row from the database and make it ready for use by the user of this model.
349
     *
350
     * The base model doesn't do anything in this method which is intentional for speed.
351
     *
352
     * @param mixed $row
353
     * @return mixed
354
     */
355 15
    public function unserialize($row) {
356 15
        return $row;
357
    }
358
359
    /**
360
     * Get the defaultLimit.
361
     *
362
     * @return int Returns the defaultLimit.
363
     */
364 16
    public function getDefaultLimit() {
365 16
        return $this->defaultLimit;
366
    }
367
368
    /**
369
     * Set the defaultLimit.
370
     *
371
     * @param int $defaultLimit
372
     * @return $this
373
     */
374 1
    public function setDefaultLimit($defaultLimit) {
375 1
        $this->defaultLimit = $defaultLimit;
376 1
        return $this;
377
    }
378
379
    /**
380
     * Get the default sort order.
381
     *
382
     * The default sort order will be passed to all queries, but can be overridden in the {@link DatasetInterface}.
383
     *
384
     * @return string[] Returns an array of column names, optionally prefixed with "-" to denote descending order.
385
     */
386 16
    public function getDefaultOrder() {
387 16
        return $this->defaultOrder;
388
    }
389
390
    /**
391
     * Set the default sort order.
392
     *
393
     * The default sort order will be passed to all queries, but can be overridden in the {@link DatasetInterface}.
394
     *
395
     * @param string ...$columns The column names to sort by, optionally prefixed with "-" to denote descending order.
396
     * @return $this
397
     */
398 1
    public function setDefaultOrder(...$columns) {
399 1
        $this->defaultOrder = $columns;
400 1
        return $this;
401
    }
402
}
403