Issues (22)

src/Model.php (1 issue)

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) {
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) {
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 {
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