Completed
Branch develop (6af1b6)
by Henry
08:27
created

Getters::generateRandomHandle()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 10
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
namespace Divergence\Models;
11
12
use Exception;
13
use Divergence\Helpers\Util;
14
use Divergence\Models\ActiveRecord;
15
use Divergence\IO\Database\MySQL as DB;
16
use Divergence\IO\Database\Query\Select;
17
18
/**
19
 * @property string $handleField Defined in the model
20
 * @property string $primaryKey Defined in the model
21
 * @property string $tableName Defined in the model
22
 */
23
trait Getters
24
{
25
    /**
26
     * 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.
27
     *
28
     * @param array $record Database row as an array.
29
     * @return static|null An instantiated ActiveRecord model from the provided data.
30
     */
31
    public static function instantiateRecord($record)
32
    {
33
        $className = static::_getRecordClass($record);
34
        return $record ? new $className($record) : null;
35
    }
36
37
    /**
38
     * 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.
39
     *
40
     * @param array $record An array of database rows.
41
     * @return static|null An array of instantiated ActiveRecord models from the provided data.
42
     */
43
    public static function instantiateRecords($records)
44
    {
45
        foreach ($records as &$record) {
46
            $className = static::_getRecordClass($record);
47
            $record = new $className($record);
48
        }
49
50
        return $records;
51
    }
52
53
    public static function getByContextObject(ActiveRecord $Record, $options = [])
54
    {
55
        return static::getByContext($Record::$rootClass, $Record->getPrimaryKeyValue(), $options);
56
    }
57
58
    public static function getByContext($contextClass, $contextID, $options = [])
59
    {
60
        if (!static::fieldExists('ContextClass')) {
61
            throw new Exception('getByContext requires the field ContextClass to be defined');
62
        }
63
64
        $options = Util::prepareOptions($options, [
65
            'conditions' => [],
66
            'order' => false,
67
        ]);
68
69
        $options['conditions']['ContextClass'] = $contextClass;
70
        $options['conditions']['ContextID'] = $contextID;
71
72
        $record = static::getRecordByWhere($options['conditions'], $options);
73
74
        $className = static::_getRecordClass($record);
75
76
        return $record ? new $className($record) : null;
77
    }
78
79
    public static function getByHandle($handle)
80
    {
81
        if (static::fieldExists(static::$handleField)) {
82
            if ($Record = static::getByField(static::$handleField, $handle)) {
83
                return $Record;
84
            }
85
        }
86
        return static::getByID($handle);
87
    }
88
89
    /**
90
     * Get model object by primary key.
91
     *
92
     * @param int $id
93
     * @return static|null
94
     */
95
    public static function getByID($id)
96
    {
97
        $record = static::getRecordByField(static::$primaryKey ? static::$primaryKey : 'ID', $id, true);
98
99
        return static::instantiateRecord($record);
100
    }
101
102
    /**
103
     * Get model object by field.
104
     *
105
     * @param string $field Field name
106
     * @param string $value Field value
107
     * @param boolean $cacheIndex Optional. If we should cache the result or not. Default is false.
108
     * @return static|null
109
     */
110
    public static function getByField($field, $value, $cacheIndex = false)
111
    {
112
        $record = static::getRecordByField($field, $value, $cacheIndex);
113
114
        return static::instantiateRecord($record);
115
    }
116
117
    /**
118
     * Get record by field.
119
     *
120
     * @param string $field Field name
121
     * @param string $value Field value
122
     * @param boolean $cacheIndex Optional. If we should cache the result or not. Default is false.
123
     * @return array|null First database result.
124
     */
125
    public static function getRecordByField($field, $value, $cacheIndex = false)
126
    {
127
        return static::getRecordByWhere([static::_cn($field) => DB::escape($value)], $cacheIndex);
128
    }
129
130
    /**
131
     * Get the first result instantiated as a model from a simple select query with a where clause you can provide.
132
     *
133
     * @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.
134
     * @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.
135
     * @return static|null Single model instantiated from the first database result
136
     */
137
    public static function getByWhere($conditions, $options = [])
138
    {
139
        $record = static::getRecordByWhere($conditions, $options);
140
141
        return static::instantiateRecord($record);
142
    }
143
144
    /**
145
     * Get the first result as an array from a simple select query with a where clause you can provide.
146
     *
147
     * @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.
148
     * @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.
149
     * @return array|null First database result.
150
     */
151
    public static function getRecordByWhere($conditions, $options = [])
152
    {
153
        if (!is_array($conditions)) {
154
            $conditions = [$conditions];
155
        }
156
157
        $options = Util::prepareOptions($options, [
158
            'order' => false,
159
        ]);
160
161
        // initialize conditions and order
162
        $conditions = static::_mapConditions($conditions);
163
        $order = $options['order'] ? static::_mapFieldOrder($options['order']) : [];
164
165
        return DB::oneRecord(
166
            (new Select())->setTable(static::$tableName)->where(join(') AND (', $conditions))->order($order ? join(',', $order) : '')->limit('1'),
167
            null,
168
            [static::class,'handleError']
169
        );
170
    }
171
172
    /**
173
     * Get the first result instantiated as a model from a simple select query you can provide.
174
     *
175
     * @param string $query Database query. The passed in string will be passed through vsprintf or sprintf with $params.
176
     * @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.
177
     * @return static|null Single model instantiated from the first database result
178
     */
179
    public static function getByQuery($query, $params = [])
180
    {
181
        return static::instantiateRecord(DB::oneRecord($query, $params, [static::class,'handleError']));
182
    }
183
184
    /**
185
     * Get all models in the database by class name. This is a subclass utility method. Requires a Class field on the model.
186
     *
187
     * @param boolean $className The full name of the class including namespace. Optional. Will use the name of the current class if none provided.
188
     * @param array $options
189
     * @return static[]|null Array of instantiated ActiveRecord models returned from the database result.
190
     */
191
    public static function getAllByClass($className = false, $options = [])
192
    {
193
        return static::getAllByField('Class', $className ? $className : get_called_class(), $options);
0 ignored issues
show
Bug introduced by
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

193
        return static::getAllByField('Class', /** @scrutinizer ignore-type */ $className ? $className : get_called_class(), $options);
Loading history...
194
    }
195
196
    /**
197
     * Get all models in the database by passing in an ActiveRecord model which has a 'ContextClass' field by the passed in records primary key.
198
     *
199
     * @param ActiveRecord $Record
200
     * @param array $options
201
     * @return static[]|null Array of instantiated ActiveRecord models returned from the database result.
202
     */
203
    public static function getAllByContextObject(ActiveRecord $Record, $options = [])
204
    {
205
        return static::getAllByContext($Record::$rootClass, $Record->getPrimaryKeyValue(), $options);
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::getAllByC...ryKeyValue(), $options) also could return the type Divergence\Models\Getters which is incompatible with the documented return type array<mixed,Divergence\Models\Getters>|null.
Loading history...
206
    }
207
208
209
    public static function getAllByContext($contextClass, $contextID, $options = [])
210
    {
211
        if (!static::fieldExists('ContextClass')) {
212
            throw new Exception('getByContext requires the field ContextClass to be defined');
213
        }
214
215
        $options = Util::prepareOptions($options, [
216
            'conditions' => [],
217
        ]);
218
219
        $options['conditions']['ContextClass'] = $contextClass;
220
        $options['conditions']['ContextID'] = $contextID;
221
222
        return static::instantiateRecords(static::getAllRecordsByWhere($options['conditions'], $options));
223
    }
224
225
    /**
226
     * Get model objects by field and value.
227
     *
228
     * @param string $field Field name
229
     * @param string $value Field value
230
     * @param array $options
231
     * @return static[]|null Array of models instantiated from the database result.
232
     */
233
    public static function getAllByField($field, $value, $options = [])
234
    {
235
        return static::getAllByWhere([$field => $value], $options);
236
    }
237
238
    /**
239
     * Gets instantiated models as an array from a simple select query with a where clause you can provide.
240
     *
241
     * @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.
242
     * @param array|string $options
243
     * @return static[]|null Array of models instantiated from the database result.
244
     */
245
    public static function getAllByWhere($conditions = [], $options = [])
246
    {
247
        return static::instantiateRecords(static::getAllRecordsByWhere($conditions, $options));
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::instantia...$conditions, $options)) also could return the type Divergence\Models\Getters which is incompatible with the documented return type array<mixed,Divergence\Models\Getters>|null.
Loading history...
248
    }
249
250
    /**
251
     * Attempts to get all database records for this class and return them as an array of instantiated models.
252
     *
253
     * @param array $options
254
     * @return static[]|null
255
     */
256
    public static function getAll($options = [])
257
    {
258
        return static::instantiateRecords(static::getAllRecords($options));
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::instantia...etAllRecords($options)) also could return the type Divergence\Models\Getters which is incompatible with the documented return type array<mixed,Divergence\Models\Getters>|null.
Loading history...
259
    }
260
261
    /**
262
     * Attempts to get all database records for this class and returns them as is from the database.
263
     *
264
     * @param array $options
265
     * @return array[]|null
266
     */
267
    public static function getAllRecords($options = [])
268
    {
269
        $options = Util::prepareOptions($options, [
270
            'indexField' => false,
271
            'order' => false,
272
            'limit' => false,
273
            'calcFoundRows' => false,
274
            'offset' => 0,
275
        ]);
276
277
        $select = (new Select())->setTable(static::$tableName)->calcFoundRows();
278
279
        if ($options['order']) {
280
            $select->order(join(',', static::_mapFieldOrder($options['order'])));
281
        }
282
283
        if ($options['limit']) {
284
            $select->limit(sprintf('%u,%u', $options['offset'], $options['limit']));
285
        }
286
        if ($options['indexField']) {
287
            return DB::table(static::_cn($options['indexField']), $select, null, null, [static::class,'handleError']);
288
        } else {
289
            return DB::allRecords($select, null, [static::class,'handleError']);
290
        }
291
    }
292
293
    /**
294
     * Gets all records by a query you provide and then instantiates the results as an array of models.
295
     *
296
     * @param string $query Database query. The passed in string will be passed through vsprintf or sprintf with $params.
297
     * @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.
298
     * @return static[]|null Array of models instantiated from the first database result
299
     */
300
    public static function getAllByQuery($query, $params = [])
301
    {
302
        return static::instantiateRecords(DB::allRecords($query, $params, [static::class,'handleError']));
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::instantia...class, 'handleError'))) also could return the type Divergence\Models\Getters which is incompatible with the documented return type array<mixed,Divergence\Models\Getters>|null.
Loading history...
303
    }
304
305
    public static function getTableByQuery($keyField, $query, $params = [])
306
    {
307
        return static::instantiateRecords(DB::table($keyField, $query, $params, [static::class,'handleError']));
0 ignored issues
show
Bug introduced by
array(static::class, 'handleError') 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

307
        return static::instantiateRecords(DB::table($keyField, $query, $params, /** @scrutinizer ignore-type */ [static::class,'handleError']));
Loading history...
308
    }
309
310
    /**
311
     * Gets database results as array from a simple select query with a where clause you can provide.
312
     *
313
     * @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.
314
     * @param array|string $options
315
     * @return array[]|null Array of records from the database result.
316
     */
317
    public static function getAllRecordsByWhere($conditions = [], $options = [])
318
    {
319
        $className = get_called_class();
320
321
        $options = Util::prepareOptions($options, [
322
            'indexField' => false,
323
            'order' => false,
324
            'limit' => false,
325
            'offset' => 0,
326
            'calcFoundRows' => !empty($options['limit']),
327
            'extraColumns' => false,
328
            'having' => false,
329
        ]);
330
331
        // initialize conditions
332
        if ($conditions) {
333
            if (is_string($conditions)) {
334
                $conditions = [$conditions];
335
            }
336
337
            $conditions = static::_mapConditions($conditions);
338
        }
339
340
        $select = (new Select())->setTable(static::$tableName)->setTableAlias($className::$rootClass);
0 ignored issues
show
Bug introduced by
The property rootClass does not exist on string.
Loading history...
341
        if ($options['calcFoundRows']) {
342
            $select->calcFoundRows();
343
        }
344
        
345
        $expression = sprintf('`%s`.*', $className::$rootClass);
346
        $select->expression($expression.static::buildExtraColumns($options['extraColumns']));
347
348
        if ($conditions) {
349
            $select->where(join(') AND (', $conditions));
350
        }
351
352
        if ($options['having']) {
353
            $select->having(static::buildHaving($options['having']));
0 ignored issues
show
Bug introduced by
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

353
            $select->having(/** @scrutinizer ignore-type */ static::buildHaving($options['having']));
Loading history...
354
        }
355
356
        if ($options['order']) {
357
            $select->order(join(',', static::_mapFieldOrder($options['order'])));
358
        }
359
360
        if ($options['limit']) {
361
            $select->limit(sprintf('%u,%u', $options['offset'], $options['limit']));
362
        }
363
364
        if ($options['indexField']) {
365
            return DB::table(static::_cn($options['indexField']), $select, null, null, [static::class,'handleError']);
366
        } else {
367
            return DB::allRecords($select, null, [static::class,'handleError']);
368
        }
369
    }
370
371
    /**
372
     * 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.
373
     *
374
     * @param string $text
375
     * @param array $options
376
     * @return string A unique handle.
377
     */
378
    public static function getUniqueHandle($text, $options = [])
379
    {
380
        // apply default options
381
        $options = Util::prepareOptions($options, [
382
            'handleField' => static::$handleField,
383
            'domainConstraints' => [],
384
            'alwaysSuffix' => false,
385
            'format' => '%s:%u',
386
        ]);
387
388
        // transliterate accented characters
389
        $text = iconv('UTF-8', 'ASCII//TRANSLIT', $text);
390
391
        // strip bad characters
392
        $handle = $strippedText = preg_replace(
393
            ['/\s+/', '/_*[^a-zA-Z0-9\-_:]+_*/', '/:[-_]/', '/^[-_]+/', '/[-_]+$/'],
394
            ['_', '-', ':', '', ''],
395
            trim($text)
396
        );
397
398
        $handle = trim($handle, '-_');
399
400
        $incarnation = 0;
401
        do {
402
            // TODO: check for repeat posting here?
403
            $incarnation++;
404
405
            if ($options['alwaysSuffix'] || $incarnation > 1) {
406
                $handle = sprintf($options['format'], $strippedText, $incarnation);
407
            }
408
        } while (static::getByWhere(array_merge($options['domainConstraints'], [$options['handleField']=>$handle])));
409
410
        return $handle;
411
    }
412
413
    // TODO: make the handleField
414
    public static function generateRandomHandle($length = 32)
415
    {
416
        do {
417
            $handle = substr(md5(mt_rand(0, mt_getrandmax())), 0, $length);
418
        } while (static::getByField(static::$handleField, $handle));
419
420
        return $handle;
421
    }
422
423
    /**
424
     * Builds the extra columns you might want to add to a database select query after the initial list of model fields.
425
     *
426
     * @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.
427
     * @return string|null Extra columns to add after a SELECT clause in a query. Always starts with a comma.
428
     */
429
    public static function buildExtraColumns($columns)
430
    {
431
        if (!empty($columns)) {
432
            if (is_array($columns)) {
433
                foreach ($columns as $key => $value) {
434
                    return ', '.$value.' AS '.$key;
435
                }
436
            } else {
437
                return ', ' . $columns;
438
            }
439
        }
440
    }
441
442
    /**
443
     * Builds the HAVING clause of a MySQL database query.
444
     *
445
     * @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.
446
     * @return string|null
447
     */
448
    public static function buildHaving($having)
449
    {
450
        if (!empty($having)) {
451
            return ' (' . (is_array($having) ? join(') AND (', static::_mapConditions($having)) : $having) . ')';
452
        }
453
    }
454
}
455