1 | <?php |
||||
2 | /** |
||||
3 | * This file is part of the Divergence package. |
||||
4 | * |
||||
5 | * (c) Henry Paradiz <[email protected]> |
||||
6 | * |
||||
7 | * For the full copyright and license information, please view the LICENSE |
||||
8 | * file that was distributed with this source code. |
||||
9 | */ |
||||
10 | |||||
11 | namespace Divergence\Models; |
||||
12 | |||||
13 | use Exception; |
||||
14 | use Divergence\Helpers\Util; |
||||
15 | use Divergence\Models\ActiveRecord; |
||||
16 | use Divergence\IO\Database\MySQL as DB; |
||||
17 | use Divergence\IO\Database\Query\Select; |
||||
18 | |||||
19 | /** |
||||
20 | * @property string $handleField Defined in the model |
||||
21 | * @property string $primaryKey Defined in the model |
||||
22 | * @property string $tableName Defined in the model |
||||
23 | */ |
||||
24 | trait Getters |
||||
25 | { |
||||
26 | /** |
||||
27 | * Converts database record array to a model. Will attempt to use the record's Class field value to as the class to instantiate as or the name of this class if none is provided. |
||||
28 | * |
||||
29 | * @param array $record Database row as an array. |
||||
30 | * @return static|null An instantiated ActiveRecord model from the provided data. |
||||
31 | */ |
||||
32 | 78 | public static function instantiateRecord($record) |
|||
33 | { |
||||
34 | 78 | $className = static::_getRecordClass($record); |
|||
35 | 78 | return $record ? new $className($record) : null; |
|||
36 | } |
||||
37 | |||||
38 | /** |
||||
39 | * Converts an array of database records to a model corresponding to each record. Will attempt to use the record's Class field value to as the class to instantiate as or the name of this class if none is provided. |
||||
40 | * |
||||
41 | * @param array $record An array of database rows. |
||||
42 | * @return array<static>|null An array of instantiated ActiveRecord models from the provided data. |
||||
43 | */ |
||||
44 | 29 | public static function instantiateRecords($records) |
|||
45 | { |
||||
46 | 29 | foreach ($records as &$record) { |
|||
47 | 29 | $className = static::_getRecordClass($record); |
|||
48 | 29 | $record = new $className($record); |
|||
49 | } |
||||
50 | |||||
51 | 29 | return $records; |
|||
52 | } |
||||
53 | |||||
54 | /** |
||||
55 | * Uses ContextClass and ContextID to get an object. |
||||
56 | * Quick way to attach things to other objects in a one-to-one relationship |
||||
57 | * |
||||
58 | * @param array $record An array of database rows. |
||||
59 | * @return static|null An array of instantiated ActiveRecord models from the provided data. |
||||
60 | */ |
||||
61 | 1 | public static function getByContextObject(ActiveRecord $Record, $options = []) |
|||
62 | { |
||||
63 | 1 | return static::getByContext($Record::$rootClass, $Record->getPrimaryKeyValue(), $options); |
|||
64 | } |
||||
65 | |||||
66 | /** |
||||
67 | * Same as getByContextObject but this method lets you specify the ContextClass manually. |
||||
68 | * |
||||
69 | * @param array $record An array of database rows. |
||||
70 | * @return static|null An array of instantiated ActiveRecord models from the provided data. |
||||
71 | */ |
||||
72 | 2 | public static function getByContext($contextClass, $contextID, $options = []) |
|||
73 | { |
||||
74 | 2 | if (!static::fieldExists('ContextClass')) { |
|||
75 | 1 | throw new Exception('getByContext requires the field ContextClass to be defined'); |
|||
76 | } |
||||
77 | |||||
78 | 1 | $options = Util::prepareOptions($options, [ |
|||
79 | 1 | 'conditions' => [], |
|||
80 | 1 | 'order' => false, |
|||
81 | 1 | ]); |
|||
82 | |||||
83 | 1 | $options['conditions']['ContextClass'] = $contextClass; |
|||
84 | 1 | $options['conditions']['ContextID'] = $contextID; |
|||
85 | |||||
86 | 1 | $record = static::getRecordByWhere($options['conditions'], $options); |
|||
87 | |||||
88 | 1 | $className = static::_getRecordClass($record); |
|||
89 | |||||
90 | 1 | return $record ? new $className($record) : null; |
|||
91 | } |
||||
92 | |||||
93 | /** |
||||
94 | * Get model object by configurable static::$handleField value |
||||
95 | * |
||||
96 | * @param int $id |
||||
97 | * @return static|null |
||||
98 | */ |
||||
99 | 14 | public static function getByHandle($handle) |
|||
100 | { |
||||
101 | 14 | if (static::fieldExists(static::$handleField)) { |
|||
102 | 9 | if ($Record = static::getByField(static::$handleField, $handle)) { |
|||
103 | 1 | return $Record; |
|||
104 | } |
||||
105 | } |
||||
106 | 13 | return static::getByID($handle); |
|||
107 | } |
||||
108 | |||||
109 | /** |
||||
110 | * Get model object by primary key. |
||||
111 | * |
||||
112 | * @param int $id |
||||
113 | * @return static|null |
||||
114 | */ |
||||
115 | 56 | public static function getByID($id) |
|||
116 | { |
||||
117 | 56 | $record = static::getRecordByField(static::$primaryKey ? static::$primaryKey : 'ID', $id, true); |
|||
118 | |||||
119 | 56 | return static::instantiateRecord($record); |
|||
120 | } |
||||
121 | |||||
122 | /** |
||||
123 | * Get model object by field. |
||||
124 | * |
||||
125 | * @param string $field Field name |
||||
126 | * @param string $value Field value |
||||
127 | * @param boolean $cacheIndex Optional. If we should cache the result or not. Default is false. |
||||
128 | * @return static|null |
||||
129 | */ |
||||
130 | 18 | public static function getByField($field, $value, $cacheIndex = false) |
|||
131 | { |
||||
132 | 18 | $record = static::getRecordByField($field, $value, $cacheIndex); |
|||
133 | |||||
134 | 18 | return static::instantiateRecord($record); |
|||
135 | } |
||||
136 | |||||
137 | /** |
||||
138 | * Get record by field. |
||||
139 | * |
||||
140 | * @param string $field Field name |
||||
141 | * @param string $value Field value |
||||
142 | * @param boolean $cacheIndex Optional. If we should cache the result or not. Default is false. |
||||
143 | * @return array<static>|null First database result. |
||||
144 | */ |
||||
145 | 63 | public static function getRecordByField($field, $value, $cacheIndex = false) |
|||
146 | { |
||||
147 | 63 | return static::getRecordByWhere([static::_cn($field) => DB::escape($value)], $cacheIndex); |
|||
148 | } |
||||
149 | |||||
150 | /** |
||||
151 | * Get the first result instantiated as a model from a simple select query with a where clause you can provide. |
||||
152 | * |
||||
153 | * @param array|string $conditions If passed as a string a database Where clause. If an array of field/value pairs will convert to a series of `field`='value' conditions joined with an AND operator. |
||||
154 | * @param array|string $options Only takes 'order' option. A raw database string that will be inserted into the OR clause of the query or an array of field/direction pairs. |
||||
155 | * @return static|null Single model instantiated from the first database result |
||||
156 | */ |
||||
157 | 23 | public static function getByWhere($conditions, $options = []) |
|||
158 | { |
||||
159 | 23 | $record = static::getRecordByWhere($conditions, $options); |
|||
160 | |||||
161 | 23 | return static::instantiateRecord($record); |
|||
162 | } |
||||
163 | |||||
164 | /** |
||||
165 | * Get the first result as an array from a simple select query with a where clause you can provide. |
||||
166 | * |
||||
167 | * @param array|string $conditions If passed as a string a database Where clause. If an array of field/value pairs will convert to a series of `field`='value' conditions joined with an AND operator. |
||||
168 | * @param array|string $options Only takes 'order' option. A raw database string that will be inserted into the OR clause of the query or an array of field/direction pairs. |
||||
169 | * @return array<static>|null First database result. |
||||
170 | */ |
||||
171 | 77 | public static function getRecordByWhere($conditions, $options = []) |
|||
172 | { |
||||
173 | 77 | if (!is_array($conditions)) { |
|||
174 | 1 | $conditions = [$conditions]; |
|||
175 | } |
||||
176 | |||||
177 | 77 | $options = Util::prepareOptions($options, [ |
|||
178 | 77 | 'order' => false, |
|||
179 | 77 | ]); |
|||
180 | |||||
181 | // initialize conditions and order |
||||
182 | 77 | $conditions = static::_mapConditions($conditions); |
|||
183 | 77 | $order = $options['order'] ? static::_mapFieldOrder($options['order']) : []; |
|||
184 | |||||
185 | 77 | return DB::oneRecord( |
|||
186 | 77 | (new Select())->setTable(static::$tableName)->where(join(') AND (', $conditions))->order($order ? join(',', $order) : '')->limit('1'), |
|||
187 | 77 | null, |
|||
188 | 77 | [static::class,'handleException'] |
|||
189 | 77 | ); |
|||
190 | } |
||||
191 | |||||
192 | /** |
||||
193 | * Get the first result instantiated as a model from a simple select query you can provide. |
||||
194 | * |
||||
195 | * @param string $query Database query. The passed in string will be passed through vsprintf or sprintf with $params. |
||||
196 | * @param array|string $params If an array will be passed through vsprintf as the second parameter with the query as the first. If a string will be used with sprintf instead. If nothing provided you must provide your own query. |
||||
197 | * @return static|null Single model instantiated from the first database result |
||||
198 | */ |
||||
199 | 1 | public static function getByQuery($query, $params = []) |
|||
200 | { |
||||
201 | 1 | return static::instantiateRecord(DB::oneRecord($query, $params, [static::class,'handleException'])); |
|||
202 | } |
||||
203 | |||||
204 | /** |
||||
205 | * Get all models in the database by class name. This is a subclass utility method. Requires a Class field on the model. |
||||
206 | * |
||||
207 | * @param boolean $className The full name of the class including namespace. Optional. Will use the name of the current class if none provided. |
||||
208 | * @param array $options |
||||
209 | * @return array<static>|null Array of instantiated ActiveRecord models returned from the database result. |
||||
210 | */ |
||||
211 | 1 | public static function getAllByClass($className = false, $options = []) |
|||
212 | { |
||||
213 | 1 | return static::getAllByField('Class', $className ? $className : get_called_class(), $options); |
|||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
214 | } |
||||
215 | |||||
216 | /** |
||||
217 | * Get all models in the database by passing in an ActiveRecord model which has a 'ContextClass' field by the passed in records primary key. |
||||
218 | * |
||||
219 | * @param ActiveRecord $Record |
||||
220 | * @param array $options |
||||
221 | * @return array<static>|null Array of instantiated ActiveRecord models returned from the database result. |
||||
222 | */ |
||||
223 | 1 | public static function getAllByContextObject(ActiveRecord $Record, $options = []) |
|||
224 | { |
||||
225 | 1 | return static::getAllByContext($Record::$rootClass, $Record->getPrimaryKeyValue(), $options); |
|||
226 | } |
||||
227 | |||||
228 | /** |
||||
229 | * @param string $contextClass |
||||
230 | * @param mixed $contextID |
||||
231 | * @param array $options |
||||
232 | * @return array<static>|null Array of instantiated ActiveRecord models returned from the database result. |
||||
233 | */ |
||||
234 | 2 | public static function getAllByContext($contextClass, $contextID, $options = []) |
|||
235 | { |
||||
236 | 2 | if (!static::fieldExists('ContextClass')) { |
|||
237 | 1 | throw new Exception('getByContext requires the field ContextClass to be defined'); |
|||
238 | } |
||||
239 | |||||
240 | 1 | $options = Util::prepareOptions($options, [ |
|||
241 | 1 | 'conditions' => [], |
|||
242 | 1 | ]); |
|||
243 | |||||
244 | 1 | $options['conditions']['ContextClass'] = $contextClass; |
|||
245 | 1 | $options['conditions']['ContextID'] = $contextID; |
|||
246 | |||||
247 | 1 | return static::instantiateRecords(static::getAllRecordsByWhere($options['conditions'], $options)); |
|||
248 | } |
||||
249 | |||||
250 | /** |
||||
251 | * Get model objects by field and value. |
||||
252 | * |
||||
253 | * @param string $field Field name |
||||
254 | * @param string $value Field value |
||||
255 | * @param array $options |
||||
256 | * @return array<static>|null Array of models instantiated from the database result. |
||||
257 | */ |
||||
258 | 6 | public static function getAllByField($field, $value, $options = []) |
|||
259 | { |
||||
260 | 6 | return static::getAllByWhere([$field => $value], $options); |
|||
261 | } |
||||
262 | |||||
263 | /** |
||||
264 | * Gets instantiated models as an array from a simple select query with a where clause you can provide. |
||||
265 | * |
||||
266 | * @param array|string $conditions If passed as a string a database Where clause. If an array of field/value pairs will convert to a series of `field`='value' conditions joined with an AND operator. |
||||
267 | * @param array|string $options |
||||
268 | * @return array<static>|null Array of models instantiated from the database result. |
||||
269 | */ |
||||
270 | 16 | public static function getAllByWhere($conditions = [], $options = []) |
|||
271 | { |
||||
272 | 16 | return static::instantiateRecords(static::getAllRecordsByWhere($conditions, $options)); |
|||
273 | } |
||||
274 | |||||
275 | /** |
||||
276 | * Attempts to get all database records for this class and return them as an array of instantiated models. |
||||
277 | * |
||||
278 | * @param array $options |
||||
279 | * @return array<static>|null |
||||
280 | */ |
||||
281 | 10 | public static function getAll($options = []) |
|||
282 | { |
||||
283 | 10 | return static::instantiateRecords(static::getAllRecords($options)); |
|||
284 | } |
||||
285 | |||||
286 | /** |
||||
287 | * Attempts to get all database records for this class and returns them as is from the database. |
||||
288 | * |
||||
289 | * @param array $options |
||||
290 | * @return array<static>|null |
||||
291 | */ |
||||
292 | 10 | public static function getAllRecords($options = []) |
|||
293 | { |
||||
294 | 10 | $options = Util::prepareOptions($options, [ |
|||
295 | 10 | 'indexField' => false, |
|||
296 | 10 | 'order' => false, |
|||
297 | 10 | 'limit' => false, |
|||
298 | 10 | 'calcFoundRows' => false, |
|||
299 | 10 | 'offset' => 0, |
|||
300 | 10 | ]); |
|||
301 | |||||
302 | 10 | $select = (new Select())->setTable(static::$tableName)->calcFoundRows(); |
|||
303 | |||||
304 | 10 | if ($options['order']) { |
|||
305 | 6 | $select->order(join(',', static::_mapFieldOrder($options['order']))); |
|||
306 | } |
||||
307 | |||||
308 | 10 | if ($options['limit']) { |
|||
309 | 7 | $select->limit(sprintf('%u,%u', $options['offset'], $options['limit'])); |
|||
310 | } |
||||
311 | 10 | if ($options['indexField']) { |
|||
312 | return DB::table(static::_cn($options['indexField']), $select, null, null, [static::class,'handleException']); |
||||
313 | } else { |
||||
314 | 10 | return DB::allRecords($select, null, [static::class,'handleException']); |
|||
315 | } |
||||
316 | } |
||||
317 | |||||
318 | /** |
||||
319 | * Gets all records by a query you provide and then instantiates the results as an array of models. |
||||
320 | * |
||||
321 | * @param string $query Database query. The passed in string will be passed through vsprintf or sprintf with $params. |
||||
322 | * @param array|string $params If an array will be passed through vsprintf as the second parameter with the query as the first. If a string will be used with sprintf instead. If nothing provided you must provide your own query. |
||||
323 | * @return array<static>|null Array of models instantiated from the first database result |
||||
324 | */ |
||||
325 | 3 | public static function getAllByQuery($query, $params = []) |
|||
326 | { |
||||
327 | 3 | return static::instantiateRecords(DB::allRecords($query, $params, [static::class,'handleException'])); |
|||
328 | } |
||||
329 | |||||
330 | /** |
||||
331 | * Loops over the data returned from the raw query and writes a new array where the key uses the $keyField parameter instead. |
||||
332 | * |
||||
333 | * @param string $keyField |
||||
334 | * @param string $query |
||||
335 | * @param array $params |
||||
336 | * @return array<static>|null |
||||
337 | */ |
||||
338 | 1 | public static function getTableByQuery($keyField, $query, $params = []) |
|||
339 | { |
||||
340 | 1 | return static::instantiateRecords(DB::table($keyField, $query, $params, [static::class,'handleException'])); |
|||
0 ignored issues
–
show
array(static::class, 'handleException') of type array<integer,string> is incompatible with the type string expected by parameter $nullKey of Divergence\IO\Database\MySQL::table() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
341 | } |
||||
342 | |||||
343 | /** |
||||
344 | * Gets database results as array from a simple select query with a where clause you can provide. |
||||
345 | * |
||||
346 | * @param array|string $conditions If passed as a string a database Where clause. If an array of field/value pairs will convert to a series of `field`='value' conditions joined with an AND operator. |
||||
347 | * @param array|string $options |
||||
348 | * @return array<static>|null Array of records from the database result. |
||||
349 | */ |
||||
350 | 17 | public static function getAllRecordsByWhere($conditions = [], $options = []) |
|||
351 | { |
||||
352 | 17 | $className = get_called_class(); |
|||
353 | |||||
354 | 17 | $options = Util::prepareOptions($options, [ |
|||
355 | 17 | 'indexField' => false, |
|||
356 | 17 | 'order' => false, |
|||
357 | 17 | 'limit' => false, |
|||
358 | 17 | 'offset' => 0, |
|||
359 | 17 | 'calcFoundRows' => !empty($options['limit']), |
|||
360 | 17 | 'extraColumns' => false, |
|||
361 | 17 | 'having' => false, |
|||
362 | 17 | ]); |
|||
363 | |||||
364 | // initialize conditions |
||||
365 | 17 | if ($conditions) { |
|||
366 | 11 | if (is_string($conditions)) { |
|||
367 | 1 | $conditions = [$conditions]; |
|||
368 | } |
||||
369 | |||||
370 | 11 | $conditions = static::_mapConditions($conditions); |
|||
371 | } |
||||
372 | |||||
373 | 17 | $select = (new Select())->setTable(static::$tableName)->setTableAlias($className::$rootClass); |
|||
0 ignored issues
–
show
|
|||||
374 | 17 | if ($options['calcFoundRows']) { |
|||
375 | 5 | $select->calcFoundRows(); |
|||
376 | } |
||||
377 | |||||
378 | 17 | $expression = sprintf('`%s`.*', $className::$rootClass); |
|||
379 | 17 | $select->expression($expression.static::buildExtraColumns($options['extraColumns'])); |
|||
380 | |||||
381 | 17 | if ($conditions) { |
|||
382 | 11 | $select->where(join(') AND (', $conditions)); |
|||
383 | } |
||||
384 | |||||
385 | 17 | if ($options['having']) { |
|||
386 | 1 | $select->having(static::buildHaving($options['having'])); |
|||
0 ignored issues
–
show
It seems like
static::buildHaving($options['having']) can also be of type null ; however, parameter $having of Divergence\IO\Database\Query\Select::having() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
387 | } |
||||
388 | |||||
389 | 17 | if ($options['order']) { |
|||
390 | 5 | $select->order(join(',', static::_mapFieldOrder($options['order']))); |
|||
391 | } |
||||
392 | |||||
393 | 17 | if ($options['limit']) { |
|||
394 | 3 | $select->limit(sprintf('%u,%u', $options['offset'], $options['limit'])); |
|||
395 | } |
||||
396 | |||||
397 | 17 | if ($options['indexField']) { |
|||
398 | return DB::table(static::_cn($options['indexField']), $select, null, null, [static::class,'handleException']); |
||||
399 | } else { |
||||
400 | 17 | return DB::allRecords($select, null, [static::class,'handleException']); |
|||
401 | } |
||||
402 | } |
||||
403 | |||||
404 | /** |
||||
405 | * Generates a unique string based on the provided text making sure that nothing it returns already exists in the database for the given handleField option. If none is provided the static config $handleField will be used. |
||||
406 | * |
||||
407 | * @param string $text |
||||
408 | * @param array $options |
||||
409 | * @return string A unique handle. |
||||
410 | */ |
||||
411 | 22 | public static function getUniqueHandle($text, $options = []) |
|||
412 | { |
||||
413 | // apply default options |
||||
414 | 22 | $options = Util::prepareOptions($options, [ |
|||
415 | 22 | 'handleField' => static::$handleField, |
|||
416 | 22 | 'domainConstraints' => [], |
|||
417 | 22 | 'alwaysSuffix' => false, |
|||
418 | 22 | 'format' => '%s:%u', |
|||
419 | 22 | ]); |
|||
420 | |||||
421 | // transliterate accented characters |
||||
422 | 22 | $text = iconv('UTF-8', 'ASCII//TRANSLIT', $text); |
|||
423 | |||||
424 | // strip bad characters |
||||
425 | 22 | $handle = $strippedText = preg_replace( |
|||
426 | 22 | ['/\s+/', '/_*[^a-zA-Z0-9\-_:]+_*/', '/:[-_]/', '/^[-_]+/', '/[-_]+$/'], |
|||
427 | 22 | ['_', '-', ':', '', ''], |
|||
428 | 22 | trim($text) |
|||
429 | 22 | ); |
|||
430 | |||||
431 | 22 | $handle = trim($handle, '-_'); |
|||
432 | |||||
433 | 22 | $incarnation = 0; |
|||
434 | do { |
||||
435 | // TODO: check for repeat posting here? |
||||
436 | 22 | $incarnation++; |
|||
437 | |||||
438 | 22 | if ($options['alwaysSuffix'] || $incarnation > 1) { |
|||
439 | 1 | $handle = sprintf($options['format'], $strippedText, $incarnation); |
|||
440 | } |
||||
441 | 22 | } while (static::getByWhere(array_merge($options['domainConstraints'], [$options['handleField']=>$handle]))); |
|||
442 | |||||
443 | 22 | return $handle; |
|||
444 | } |
||||
445 | |||||
446 | // TODO: make the handleField |
||||
447 | 1 | public static function generateRandomHandle($length = 32) |
|||
448 | { |
||||
449 | do { |
||||
450 | 1 | $handle = substr(md5(mt_rand(0, mt_getrandmax())), 0, $length); |
|||
451 | 1 | } while (static::getByField(static::$handleField, $handle)); |
|||
452 | |||||
453 | 1 | return $handle; |
|||
454 | } |
||||
455 | |||||
456 | /** |
||||
457 | * Builds the extra columns you might want to add to a database select query after the initial list of model fields. |
||||
458 | * |
||||
459 | * @param array|string $columns An array of keys and values or a string which will be added to a list of fields after the query's SELECT clause. |
||||
460 | * @return string|null Extra columns to add after a SELECT clause in a query. Always starts with a comma. |
||||
461 | */ |
||||
462 | 17 | public static function buildExtraColumns($columns) |
|||
463 | { |
||||
464 | 17 | if (!empty($columns)) { |
|||
465 | 1 | if (is_array($columns)) { |
|||
466 | 1 | foreach ($columns as $key => $value) { |
|||
467 | 1 | return ', '.$value.' AS '.$key; |
|||
468 | } |
||||
469 | } else { |
||||
470 | 1 | return ', ' . $columns; |
|||
471 | } |
||||
472 | } |
||||
473 | } |
||||
474 | |||||
475 | /** |
||||
476 | * Builds the HAVING clause of a MySQL database query. |
||||
477 | * |
||||
478 | * @param array|string $having Same as conditions. Can provide a string to use or an array of field/value pairs which will be joined by the AND operator. |
||||
479 | * @return string|null |
||||
480 | */ |
||||
481 | 1 | public static function buildHaving($having) |
|||
482 | { |
||||
483 | 1 | if (!empty($having)) { |
|||
484 | 1 | return ' (' . (is_array($having) ? join(') AND (', static::_mapConditions($having)) : $having) . ')'; |
|||
485 | } |
||||
486 | } |
||||
487 | } |
||||
488 |