Completed
Push — master ( 767c34...79d01e )
by Todd
02:06
created

Model::unserialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
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
12
class Model {
13
    use Utils\FetchModeTrait { setFetchMode as private; }
14
15
    const DEFAULT_LIMIT = 30;
16
17
    /**
18
     * @var string The name of the table.
19
     */
20
    private $name;
21
22
    /**
23
     * @var Db
24
     */
25
    private $db;
26
27
    /**
28
     * @var array
29
     */
30
    private $primaryKey;
31
32
    /**
33
     * @var Schema
34
     */
35
    private $schema;
36
37
    /**
38
     * @var int
39
     */
40
    private $defaultLimit = Model::DEFAULT_LIMIT;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
41
42
    /**
43
     * @var string[]
44
     */
45
    private $defaultOrder = [];
46
47 16
    public function __construct($name, Db $db, $rowType = null) {
48 16
        $this->name = $name;
49 16
        $this->db = $db;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
50
51 16
        $fetchMode = $rowType !== null ? $rowType : $db->getFetchMode();
52 16
        if (!empty($fetchMode)) {
53 16
            $this->setFetchMode(...(array)$fetchMode);
54
        }
55 16
    }
56
57
    /**
58
     * Get the name.
59
     *
60
     * @return string Returns the name.
61
     */
62 1
    public function getName() {
63 1
        return $this->name;
64
    }
65
66
    /**
67
     * Get the primaryKey.
68
     *
69
     * @return array Returns the primaryKey.
70
     */
71 1
    public function getPrimaryKey() {
72 1
        if ($this->primaryKey === null) {
73 1
            $schema = $this->getSchema();
74
75 1
            $pk = [];
76 1
            foreach ($schema->getSchemaArray()['properties'] as $column => $property) {
77 1
                if (!empty($property['primary'])) {
78 1
                    $pk[] = $column;
79
                }
80
            }
81 1
            $this->primaryKey = $pk;
82
        }
83 1
        return $this->primaryKey;
84
    }
85
86
    /**
87
     * Set the primaryKey.
88
     *
89
     * @param array $primaryKey The names of the columns in the primary key.
90
     * @return $this
91
     */
92
    protected function setPrimaryKey(...$primaryKey) {
93
        $this->primaryKey = $primaryKey;
94
        return $this;
95
    }
96
97
    /**
98
     * Get the db.
99
     *
100
     * @return Db Returns the db.
101
     */
102 1
    public function getDb() {
103 1
        return $this->db;
104
    }
105
106
    /**
107
     * Set the db.
108
     *
109
     * @param Db $db
110
     * @return $this
111
     */
112
    public function setDb($db) {
113
        $this->db = $db;
114
        return $this;
115
    }
116
117
    /**
118
     * Map primary key values to the primary key name.
119
     *
120
     * @param mixed $id An ID value or an array of ID values. If an array is passed and the model has a mult-column
121
     * primary key then all of the values must be in order.
122
     * @return array Returns an associative array mapping column names to values.
123
     */
124 1
    protected function mapID($id) {
125 1
        $idArray = (array)$id;
126
127 1
        $result = [];
128 1
        foreach ($this->getPrimaryKey() as $i => $column) {
129 1
            if (isset($idArray[$i])) {
130 1
                $result[$column] = $idArray[$i];
131
            } elseif (isset($idArray[$column])) {
132
                $result[$column] = $idArray[$column];
133
            } else {
134
                $result[$column] = null;
135
            }
136
        }
137
138 1
        return $result;
139
    }
140
141
    /**
142
     * Gets the row schema for this model.
143
     *
144
     * @return Schema Returns a schema.
145
     */
146 1
    final public function getSchema() {
147 1
        if ($this->schema === null) {
148 1
            $this->schema = $this->fetchSchema();
149
        }
150 1
        return $this->schema;
151
    }
152
153
    /**
154
     * Fetch the row schema from the database meta info.
155
     *
156
     * This method works fine as-is, but can also be overridden to provide more specific schema information for the model.
157
     * This method is called only once for the object and then is cached in a property so you don't need to implement
158
     * caching of your own.
159
     *
160
     * If you are going to override this method we recommend you still call the parent method and add its result to your schema.
161
     * Here is an example:
162
     *
163
     * ```php
164
     * protected function fetchSchema() {
165
     *     $schema = Schema::parse([
166
     *         'body:s', // make the column required even if it isn't in the db.
167
     *         'attributes:o?' // accept an object instead of string
168
     *     ]);
169
     *
170
     *     $dbSchema = parent::fetchSchema();
171
     *     $schema->add($dbSchema, true);
172
     *
173
     *     return $schema;
174
     * }
175
     * ```
176
     *
177
     * @return Schema Returns the row schema.
178
     */
179 1
    protected function fetchSchema() {
180 1
        $columns = $this->getDb()->fetchColumnDefs($this->name);
181 1
        if ($columns === null) {
182
            throw new \InvalidArgumentException("Cannot fetch schema foor {$this->name}.");
183
        }
184
185
        $schema = [
186 1
            'type' => 'object',
187 1
            'dbtype' => 'table',
188 1
            'properties' => $columns
189
        ];
190
191 1
        $required = $this->requiredFields($columns);
192 1
        if (!empty($required)) {
193 1
            $schema['required'] = $required;
194
        }
195
196 1
        return new Schema($schema);
197
    }
198
199
    /**
200
     * Figure out the schema required fields from a list of columns.
201
     *
202
     * A column is required if it meets all of the following criteria.
203
     *
204
     * - The column does not have an auto increment.
205
     * - The column does not have a default value.
206
     * - The column does not allow null.
207
     *
208
     * @param array $columns An array of column schemas.
209
     */
210 1
    private function requiredFields(array $columns) {
211 1
        $required = [];
212
213 1
        foreach ($columns as $name => $column) {
214 1
            if (empty($column['autoIncrement']) && !isset($column['default']) && empty($column['allowNull'])) {
215 1
                $required[] = $name;
216
            }
217
        }
218
219 1
        return $required;
220
    }
221
222
    /**
223
     * Query the model.
224
     *
225
     * @param array $where A where clause to filter the data.
226
     * @return DatasetInterface
227
     */
228 16
    public function get(array $where) {
229
        $options = [
230 16
            Db::OPTION_FETCH_MODE => $this->getFetchArgs(),
231 16
            'rowCallback' => [$this, 'unserialize']
232
        ];
233
234 16
        $qry = new TableQuery($this->name, $where, $this->db, $options);
235 16
        $qry->setLimit($this->getDefaultLimit())
236 16
            ->setOrder(...$this->getDefaultOrder());
237
238 16
        return $qry;
239
    }
240
241
    /**
242
     * Query the database directly.
243
     *
244
     * @param array $where A where clause to filter the data.
245
     * @param array $options Options to pass to the database. See {@link Db::get()}.
246
     * @return \PDOStatement Returns a statement from the query.
247
     */
248 11
    public function query(array $where, array $options = []) {
249
        $options += [
250 11
            'order' => $this->getDefaultOrder(),
251 11
            'limit' => $this->getDefaultLimit(),
252 11
            Db::OPTION_FETCH_MODE => $this->getFetchArgs()
253
        ];
254
255 11
        $stmt = $this->db->get($this->name, $where, $options);
256 11
        return $stmt;
257
    }
258
259
    /**
260
     * @param mixed $id A primary key value for the model.
261
     * @return mixed|null
262
     */
263 1
    public function getID($id) {
264 1
        $r = $this->get($this->mapID($id));
265 1
        return $r->firstRow();
266
    }
267
268 1
    public function insert($row, array $options = []) {
269 1
        $valid = $this->validate($row, false);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
270 1
        $serialized = $this->serialize($valid);
271
272 1
        $r = $this->db->insert($this->name, $serialized, $options);
273 1
        return $r;
274
    }
275
276
    public function update(array $set, array $where, array $options = []) {
277
        $valid = $this->validate($set, true);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
278
        $serialized = $this->serialize($valid);
279
280
        $r = $this->db->update($this->name, $serialized, $where, $options);
281
        return $r;
282
    }
283
284
    public function updateID($id, $set) {
285
        $r = $this->update($set, $this->mapID($id));
286
        return $r;
287
    }
288
289
    /**
290
     * Validate a row of data.
291
     *
292
     * @param array|\ArrayAccess $row The row to validate.
293
     * @param bool $sparse Whether or not the validation should be sparse (during update).
294
     * @return array Returns valid data.
295
     */
296 1
    public function validate($row, $sparse = false) {
297 1
        $schema = $this->getSchema();
298 1
        $valid = $schema->validate($row, $sparse);
1 ignored issue
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
299
300 1
        return $valid;
301
    }
302
303
    /**
304
     * Serialize a row of data into a format that can be native to the database.
305
     *
306
     * This method should always take an array of data, even if your model is meant to use objects of some sort. This is
307
     * possible because the row that gets passed into this method is the output of {@link validate()}.
308
     *
309
     * @param array $row The row to serialize.
310
     * @return array Returns a row of serialized data.
311
     */
312 1
    public function serialize(array $row) {
313 1
        return $row;
314
    }
315
316
    /**
317
     * Unserialize a row from the database and make it ready for use by the user of this model.
318
     *
319
     * The base model doesn't do anything in this method which is intentional for speed.
320
     *
321
     * @param mixed $row
322
     * @return mixed
323
     */
324 15
    public function unserialize($row) {
325 15
        return $row;
326
    }
327
328
    /**
329
     * Get the defaultLimit.
330
     *
331
     * @return int Returns the defaultLimit.
332
     */
333 16
    public function getDefaultLimit() {
334 16
        return $this->defaultLimit;
335
    }
336
337
    /**
338
     * Set the defaultLimit.
339
     *
340
     * @param int $defaultLimit
341
     * @return $this
342
     */
343 1
    public function setDefaultLimit($defaultLimit) {
344 1
        $this->defaultLimit = $defaultLimit;
345 1
        return $this;
346
    }
347
348
    /**
349
     * Get the defaultOrder.
350
     *
351
     * @return array Returns the defaultOrder.
352
     */
353 16
    public function getDefaultOrder() {
354 16
        return $this->defaultOrder;
355
    }
356
357
    /**
358
     * Set the defaultOrder.
359
     *
360
     * @param string[] $columns
0 ignored issues
show
Documentation introduced by
Should the type for parameter $columns not be string[][]?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
361
     * @return $this
362
     */
363 1
    public function setDefaultOrder(...$columns) {
364 1
        $this->defaultOrder = $columns;
0 ignored issues
show
Documentation Bug introduced by
It seems like $columns of type array<integer,array<integer,string>> is incompatible with the declared type array<integer,string> of property $defaultOrder.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
365 1
        return $this;
366
    }
367
}
368