vanilla /
garden-db
| 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
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 |