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
|
|||
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
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 ![]() |
|||
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
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 ![]() |
|||
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
|
|||
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 |
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
.