Completed
Push — feature/pixie-port ( 132a2b...ea34e6 )
by Vladimir
13:20
created

QueryBuilderFlex::injectResultValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
use BZIon\Debug\DatabaseQuery;
4
use Pecee\Pixie\Connection;
5
use Pecee\Pixie\QueryBuilder\IQueryBuilderHandler;
6
use Pecee\Pixie\QueryBuilder\QueryBuilderHandler;
7
8
/**
9
 * The core query builder used across BZiON for creating and modifying queries for all of our entities.
10
 *
11
 * @since 0.11.0
12
 */
13
class QueryBuilderFlex extends QueryBuilderHandler
14
{
15
    /** @var array An array of values that'll be injected into returned database results */
16
    protected $injectedValues = [];
17
18
    /** @var string The column name of the column dedicated to storing the name of the model */
19
    protected $modelNameColumn;
20
21
    /** @var Model|string The FQN of the model object this QueryBuilder instance is for */
22
    protected $modelType = null;
23
24
    /** @var int The amount of results per page with regards to result pagination */
25
    private $resultsPerPage;
26
27
    //
28
    // Factories
29
    //
30
31
    /**
32
     * Create a bare QueryBuilder instance.
33
     *
34
     * @throws Exception
35
     *
36
     * @return static
37
     */
38
    final public static function createBuilder()
39
    {
40
        Database::getInstance();
41
42
        $connect = Service::getQueryBuilderConnection();
43
44
        return (new static($connect));
45
    }
46
47
    /**
48
     * Create a QueryBuilder instance for a specific table.
49
     *
50
     * @param  string $tableName
51
     *
52
     * @throws Exception If there is no database connection configured.
53
     *
54
     * @return static
55
     */
56
    final public static function createForTable(string $tableName)
57
    {
58
        return self::createBuilder()
59
            ->table($tableName)
60
        ;
61
    }
62
63
    /**
64
     * Creeate a QueryBuilder instance to work with a Model.
65
     *
66
     * @param  string $modelType The FQN for the model that
67
     *
68
     * @throws Exception If there is no database connection configured.
69
     *
70
     * @return static
71
     */
72
    final public static function createForModel(string $modelType)
73
    {
74
        return self::createBuilder()
75
            ->table(constant("$modelType::TABLE"))
76
            ->setModelType($modelType)
77
        ;
78
    }
79
80
    //
81
    // Overridden QueryBuilder Functions
82
    //
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function __construct(Connection $connection = null)
88
    {
89
        parent::__construct($connection);
90
91
        $this->setFetchMode(PDO::FETCH_ASSOC);
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     *
97
     * @internal Use one of the QueryBuilderFlex get*() methods instead.
98
     *
99
     * @see self::getArray()
100
     * @see self::getModels()
101
     * @see self::getNames()
102
     */
103
    public function get(): array
104
    {
105
        $queryObject = $this->getQuery();
106
        $debug = new DatabaseQuery($queryObject->getSql(), $queryObject->getBindings());
107
108
        /** @var array $results */
109
        $results = parent::get();
110
111
        $debug->finish($results);
112
113
        foreach ($results as &$result) {
114
            $result = array_merge($this->injectedValues, $result);
115
        }
116
117
        return $results;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function limit($limit): IQueryBuilderHandler
124
    {
125
        $this->resultsPerPage = $limit;
126
127
        return parent::limit($limit);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler
134
    {
135
        if ($value instanceof BaseModel) {
136
            $value = $value->getId();
137
        }
138
139
        return parent::whereHandler($key, $operator, $value, $joiner);
140
    }
141
142
    //
143
    // QueryBuilderFlex unique functions
144
    //
145
146
    /**
147
     * Request that only non-deleted Models should be returned.
148
     *
149
     * @return static
150
     */
151
    public function active(): QueryBuilderFlex
152
    {
153
        $type = $this->modelType;
154
155
        // Since it's a system model, values are always handled by BZiON core meaning there will always only be "active"
156
        // values in the database.
157
        if ($type::SYSTEM_MODEL) {
158
            return $this;
159
        }
160
161
        $column = $type::DELETED_COLUMN;
162
163
        if ($column === null) {
164
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
165
                sprintf('The use of the status column is deprecated. Update the %s model to use the DELETED_* constants.', get_called_class()),
166
                E_USER_DEPRECATED
167
            );
168
169
            return $this->whereIn('status', $type::getActiveStatuses());
170
        }
171
172
        $stopPropagation = $type::getActiveModels($this);
173
174
        if ($stopPropagation) {
175
            return $this;
176
        }
177
178
        return $this->whereNot($column, '=', $type::DELETED_VALUE);
179
    }
180
181
    /**
182
     * An alias for QueryBuilder::getModels(), with fast fetching on by default and no return of results.
183
     *
184
     * @param  bool $fastFetch Whether to perform one query to load all the model data instead of fetching them one by
185
     *              one
186
     *
187
     * @throws \Pecee\Pixie\Exception
188
     *
189
     * @return void
190
     */
191
    public function addToCache(bool $fastFetch = true): void
192
    {
193
        $this->getModels($fastFetch);
194
    }
195
196
    /**
197
     * Get the amount of pages this query would have.
198
     *
199
     * @throws \Pecee\Pixie\Exception
200
     *
201
     * @return int
202
     */
203
    public function countPages(): int
204
    {
205
        return (int)ceil($this->count() / $this->resultsPerPage);
206
    }
207
208
    /**
209
     * Request that a specific model is not returned.
210
     *
211
     * @param  Model|int $model The ID or model you don't want to get
212
     *
213
     * @return static
214
     */
215
    public function except($model): QueryBuilderFlex
216
    {
217
        if ($model instanceof Model) {
218
            $model = $model->getId();
219
        }
220
221
        $this->whereNot('id', '=', $model);
222
223
        return $this;
224
    }
225
226
    /**
227
     * Find the first matching model in the database or return an invalid model.
228
     *
229
     * @param mixed  $value      The value to search for
230
     * @param string $columnName The column name we'll be checking
231
     *
232
     * @throws \Pecee\Pixie\Exception
233
     *
234
     * @return Model
235
     */
236
    public function findModel($value, string $columnName = 'id'): Model
237
    {
238
        $type = $this->modelType;
239
240
        /** @var array $result */
241
        $result = parent::find($value, $columnName);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (find() instead of findModel()). Are you sure this is correct? If so, you might want to change this to $this->find().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
242
243
        if ($result === null) {
244
            return $type::get(0);
245
        }
246
247
        return $type::createFromDatabaseResult($result);
248
    }
249
250
    /**
251
     * Only show results from a specific page.
252
     *
253
     * This method will automatically take care of the calculations for a correct OFFSET.
254
     *
255
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
256
     *
257
     * @throws \Pecee\Pixie\Exception
258
     *
259
     * @return static
260
     */
261
    public function fromPage(int $page = null): QueryBuilderFlex
262
    {
263
        if ($page === null) {
264
            $this->offset($page);
265
266
            return $this;
267
        }
268
269
        $page = intval($page);
270
        $page = ($page <= 0) ? 1 : $page;
271
272
        $this->offset((min($page, $this->countPages()) - 1) * $this->resultsPerPage);
273
274
        return $this;
275
    }
276
277
    /**
278
     * Get the results of query as an array.
279
     *
280
     * @param array|string $columns
281
     *
282
     * @throws \Pecee\Pixie\Exception
283
     *
284
     * @return array
285
     */
286
    public function getArray($columns): array
287
    {
288
        $this->select($columns);
289
290
        return $this->get();
291
    }
292
293
    /**
294
     * Perform the query and get the results as Models.
295
     *
296
     * @param  bool $fastFetch Whether to perform one query to load all the model data instead of fetching them one by
297
     *                         one (ignores cache)
298
     *
299
     * @throws \Pecee\Pixie\Exception
300
     *
301
     * @return Model[]
302
     */
303
    public function getModels(bool $fastFetch = true): array
304
    {
305
        /** @var Model $type */
306
        $type = $this->modelType;
307
308
        $modelColumnsToSelect = $type::getEagerColumnsList();
309
310
        if (isset($this->statements['joins'])) {
311
            $modelColumnsToSelect = __::mapValues($modelColumnsToSelect, function ($value, $key, $array) use ($type) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $array is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
312
                return sprintf('%s.%s', $type::TABLE, $value);
313
            });
314
        }
315
316
        $this->select($modelColumnsToSelect);
317
318
        $results = $this->get();
319
320
        if ($fastFetch) {
321
            return $type::createFromDatabaseResults($results);
322
        }
323
324
        return $type::arrayIdToModel(array_column($results, 'id'));
325
    }
326
327
    /**
328
     * Perform the query and get back the results in an array of names.
329
     *
330
     * @throws \Pecee\Pixie\Exception
331
     * @throws UnexpectedValueException When no name column has been specified
332
     *
333
     * @return string[] An array of the type $id => $name
334
     */
335
    public function getNames(): array
336
    {
337
        if (!$this->modelNameColumn) {
338
            throw new UnexpectedValueException(sprintf('The name column has not been specified for this query builder. Use %s::setNameColumn().', get_called_class()));
339
        }
340
341
        $this->select(['id', $this->modelNameColumn]);
342
343
        $results = $this->get();
344
345
        return array_column($results, $this->modelNameColumn, 'id');
346
    }
347
348
    /**
349
     * Inject variables into the returned database results.
350
     *
351
     * These values will be merged in with values returned from database results. Database results will override any
352
     * injected values.
353
     *
354
     * @param array $injection
355
     *
356
     * @return QueryBuilderFlex
357
     */
358
    public function injectResultValues(array $injection): QueryBuilderFlex
359
    {
360
        $this->injectedValues = $injection;
361
362
        return $this;
363
    }
364
365
    /**
366
     * Set the model this QueryBuilder will be working this.
367
     *
368
     * This information is used for automatically retrieving table names, eager columns, and lazy columns for these
369
     * models.
370
     *
371
     * @param  string $modelType The FQN of the model this QueryBuilder will be working with
372
     *
373
     * @return $this
374
     */
375
    public function setModelType(string $modelType = null): QueryBuilderFlex
376
    {
377
        $this->modelType = $modelType;
378
379
        return $this;
380
    }
381
382
    /**
383
     * Set the column that'll be used as the human-friendly name of the model.
384
     *
385
     * @param string $columnName
386
     *
387
     * @return static
388
     */
389
    public function setNameColumn(string $columnName): QueryBuilderFlex
390
    {
391
        if (!is_subclass_of($this->modelType, NamedModel::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \NamedModel::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
392
            throw new LogicException(sprintf('Setting name columns is only supported in models implementing the "%s" interface.', NamedModel::class));
393
        }
394
395
        $this->modelNameColumn = $columnName;
396
397
        return $this;
398
    }
399
400
    /**
401
     * Make sure that Models invisible to a player are not returned.
402
     *
403
     * Note that this method does not take PermissionModel::canBeSeenBy() into
404
     * consideration for performance purposes, so you will have to override this
405
     * in your query builder if necessary.
406
     *
407
     * @param  Player $player      The player in question
408
     * @param  bool   $showDeleted Use false to hide deleted models even from admins
409
     *
410
     * @return static
411
     */
412
    public function visibleTo(Player $player, bool $showDeleted = false): QueryBuilderFlex
413
    {
414
        $type = $this->modelType;
415
416
        if (is_subclass_of($this->modelType, PermissionModel::class) &&
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \PermissionModel::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
417
            $player->hasPermission(constant("$type::EDIT_PERMISSION"))
418
        ) {
419
            // The player is an admin who can see the hidden models
420
            if (!$showDeleted) {
421
                $col = constant("$type::DELETED_COLUMN");
422
423
                if ($col !== null) {
424
                    $this->whereNot($col, '=', constant("$type::DELETED_VALUE"));
425
                }
426
            }
427
        } else {
428
            return $this->active();
429
        }
430
431
        return $this;
432
    }
433
}
434