Issues (45)

src/Models/Getters.php (4 issues)

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
It seems like $className ? $className : get_called_class() can also be of type true; however, parameter $value of Divergence\Models\Getters::getAllByField() 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 ignore-type  annotation

213
        return static::getAllByField('Class', /** @scrutinizer ignore-type */ $className ? $className : get_called_class(), $options);
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 ignore-type  annotation

340
        return static::instantiateRecords(DB::table($keyField, $query, $params, /** @scrutinizer ignore-type */ [static::class,'handleException']));
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
The property rootClass does not exist on string.
Loading history...
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 ignore-type  annotation

386
            $select->having(/** @scrutinizer ignore-type */ static::buildHaving($options['having']));
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