Completed
Pull Request — master (#186)
by Vladimir
16:33 queued 13:26
created

QueryBuilderFlex::visibleTo()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 11
nc 4
nop 2
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
     * @return static
124
     */
125
    public function limit($limit): IQueryBuilderHandler
126
    {
127
        $this->resultsPerPage = $limit;
128
129
        return parent::limit($limit);
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler
136
    {
137
        // For certain type of objects, we convert them into something the query builder can handle correctly
138
        if ($value instanceof BaseModel) {
139
            $value = $value->getId();
140
        }
141
        elseif ($value instanceof DateTime) {
142
            $value = (string)$value;
143
        }
144
145
        return parent::whereHandler($key, $operator, $value, $joiner);
146
    }
147
148
    //
149
    // QueryBuilderFlex unique functions
150
    //
151
152
    /**
153
     * Request that only non-deleted Models should be returned.
154
     *
155
     * @return static
156
     */
157
    public function active(): QueryBuilderFlex
158
    {
159
        $type = $this->modelType;
160
161
        // Since it's a system model, values are always handled by BZiON core meaning there will always only be "active"
162
        // values in the database.
163
        if ($type::SYSTEM_MODEL) {
164
            return $this;
165
        }
166
167
        $column = $type::DELETED_COLUMN;
168
169
        if ($column === null) {
170
            @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...
171
                sprintf('The use of the status column is deprecated. Update the %s model to use the DELETED_* constants.', get_called_class()),
172
                E_USER_DEPRECATED
173
            );
174
175
            return $this->whereIn('status', $type::getActiveStatuses());
176
        }
177
178
        $stopPropagation = $type::getActiveModels($this);
179
180
        if ($stopPropagation) {
181
            return $this;
182
        }
183
184
        return $this->whereNot($column, '=', $type::DELETED_VALUE);
185
    }
186
187
    /**
188
     * An alias for QueryBuilder::getModels(), with fast fetching on by default and no return of results.
189
     *
190
     * @param  bool $fastFetch Whether to perform one query to load all the model data instead of fetching them one by
191
     *              one
192
     *
193
     * @throws \Pecee\Pixie\Exception
194
     *
195
     * @return void
196
     */
197
    public function addToCache(bool $fastFetch = true): void
198
    {
199
        $this->getModels($fastFetch);
200
    }
201
202
    /**
203
     * Get the amount of pages this query would have.
204
     *
205
     * @throws \Pecee\Pixie\Exception
206
     *
207
     * @return int
208
     */
209
    public function countPages(): int
210
    {
211
        return (int)ceil($this->count() / $this->resultsPerPage);
212
    }
213
214
    /**
215
     * Request that a specific model is not returned.
216
     *
217
     * @param  Model|int $model The ID or model you don't want to get
218
     *
219
     * @return static
220
     */
221
    public function except($model): QueryBuilderFlex
222
    {
223
        if ($model instanceof Model) {
224
            $model = $model->getId();
225
        }
226
227
        $this->whereNot('id', '=', $model);
228
229
        return $this;
230
    }
231
232
    /**
233
     * Find the first matching model in the database or return an invalid model.
234
     *
235
     * @param mixed  $value      The value to search for
236
     * @param string $columnName The column name we'll be checking
237
     *
238
     * @throws \Pecee\Pixie\Exception
239
     *
240
     * @return Model
241
     */
242
    public function findModel($value, string $columnName = 'id'): Model
243
    {
244
        $type = $this->modelType;
245
246
        /** @var array $result */
247
        $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...
248
249
        if ($result === null) {
250
            return $type::get(0);
251
        }
252
253
        return $type::createFromDatabaseResult($result);
254
    }
255
256
    /**
257
     * Only show results from a specific page.
258
     *
259
     * This method will automatically take care of the calculations for a correct OFFSET.
260
     *
261
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
262
     *
263
     * @throws \Pecee\Pixie\Exception
264
     *
265
     * @return static
266
     */
267
    public function fromPage(int $page = null): QueryBuilderFlex
268
    {
269
        if ($page === null) {
270
            $this->offset($page);
271
272
            return $this;
273
        }
274
275
        $page = intval($page);
276
        $page = ($page <= 0) ? 1 : $page;
277
278
        $this->offset((min($page, $this->countPages()) - 1) * $this->resultsPerPage);
279
280
        return $this;
281
    }
282
283
    /**
284
     * Get the results of query as an array.
285
     *
286
     * @param array|string $columns
287
     *
288
     * @throws \Pecee\Pixie\Exception
289
     *
290
     * @return array
291
     */
292
    public function getArray($columns): array
293
    {
294
        $this->select($columns);
295
296
        return $this->get();
297
    }
298
299
    /**
300
     * Perform the query and get the results as Models.
301
     *
302
     * @param  bool $fastFetch Whether to perform one query to load all the model data instead of fetching them one by
303
     *                         one (ignores cache)
304
     *
305
     * @throws \Pecee\Pixie\Exception
306
     *
307
     * @return Model[]
308
     */
309
    public function getModels(bool $fastFetch = true): array
310
    {
311
        /** @var Model $type */
312
        $type = $this->modelType;
313
314
        $modelColumnsToSelect = $type::getEagerColumnsList();
315
316
        if (isset($this->statements['joins'])) {
317
            $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...
318
                return sprintf('%s.%s', $type::TABLE, $value);
319
            });
320
        }
321
322
        $this->select($modelColumnsToSelect);
323
324
        $results = $this->get();
325
326
        if ($fastFetch) {
327
            return $type::createFromDatabaseResults($results);
328
        }
329
330
        return $type::arrayIdToModel(array_column($results, 'id'));
331
    }
332
333
    /**
334
     * Perform the query and get back the results in an array of names.
335
     *
336
     * @throws \Pecee\Pixie\Exception
337
     * @throws UnexpectedValueException When no name column has been specified
338
     *
339
     * @return string[] An array of the type $id => $name
340
     */
341
    public function getNames(): array
342
    {
343
        if (!$this->modelNameColumn) {
344
            throw new UnexpectedValueException(sprintf('The name column has not been specified for this query builder. Use %s::setNameColumn().', get_called_class()));
345
        }
346
347
        $this->select(['id', $this->modelNameColumn]);
348
349
        $results = $this->get();
350
351
        return array_column($results, $this->modelNameColumn, 'id');
352
    }
353
354
    /**
355
     * Inject variables into the returned database results.
356
     *
357
     * These values will be merged in with values returned from database results. Database results will override any
358
     * injected values.
359
     *
360
     * @param array $injection
361
     *
362
     * @return QueryBuilderFlex
363
     */
364
    public function injectResultValues(array $injection): QueryBuilderFlex
365
    {
366
        $this->injectedValues = $injection;
367
368
        return $this;
369
    }
370
371
    /**
372
     * Set the model this QueryBuilder will be working this.
373
     *
374
     * This information is used for automatically retrieving table names, eager columns, and lazy columns for these
375
     * models.
376
     *
377
     * @param  string $modelType The FQN of the model this QueryBuilder will be working with
378
     *
379
     * @return $this
380
     */
381
    public function setModelType(string $modelType = null): QueryBuilderFlex
382
    {
383
        $this->modelType = $modelType;
384
385
        return $this;
386
    }
387
388
    /**
389
     * Set the column that'll be used as the human-friendly name of the model.
390
     *
391
     * @param string $columnName
392
     *
393
     * @return static
394
     */
395
    public function setNameColumn(string $columnName): QueryBuilderFlex
396
    {
397
        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...
398
            throw new LogicException(sprintf('Setting name columns is only supported in models implementing the "%s" interface.', NamedModel::class));
399
        }
400
401
        $this->modelNameColumn = $columnName;
402
403
        return $this;
404
    }
405
406
    /**
407
     * Make sure that Models invisible to a player are not returned.
408
     *
409
     * Note that this method does not take PermissionModel::canBeSeenBy() into
410
     * consideration for performance purposes, so you will have to override this
411
     * in your query builder if necessary.
412
     *
413
     * @param  Player $player      The player in question
414
     * @param  bool   $showDeleted Use false to hide deleted models even from admins
415
     *
416
     * @return static
417
     */
418
    public function visibleTo(Player $player, bool $showDeleted = false): QueryBuilderFlex
419
    {
420
        $type = $this->modelType;
421
422
        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...
423
            $player->hasPermission(constant("$type::EDIT_PERMISSION"))
424
        ) {
425
            // The player is an admin who can see the hidden models
426
            if (!$showDeleted) {
427
                $col = constant("$type::DELETED_COLUMN");
428
429
                if ($col !== null) {
430
                    $this->whereNot($col, '=', constant("$type::DELETED_VALUE"));
431
                }
432
            }
433
        } else {
434
            return $this->active();
435
        }
436
437
        return $this;
438
    }
439
}
440