Completed
Pull Request — master (#186)
by Vladimir
28:51 queued 13:52
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 string The column name of the column dedicated to storing the name of the model */
16
    protected $modelNameColumn;
17
18
    /** @var Model|string The FQN of the model object this QueryBuilder instance is for */
19
    protected $modelType = null;
20
21
    /** @var int The amount of results per page with regards to result pagination */
22
    private $resultsPerPage;
23
24
    //
25
    // Factories
26
    //
27
28
    /**
29
     * Create a bare QueryBuilder instance.
30
     *
31
     * @throws Exception
32
     *
33
     * @return static
34
     */
35
    final public static function createBuilder()
36
    {
37
        Database::getInstance();
38
39
        $connect = Service::getQueryBuilderConnection();
40
41
        return (new static($connect));
42
    }
43
44
    /**
45
     * Create a QueryBuilder instance for a specific table.
46
     *
47
     * @param  string $tableName
48
     *
49
     * @throws Exception If there is no database connection configured.
50
     *
51
     * @return static
52
     */
53
    final public static function createForTable(string $tableName)
54
    {
55
        return self::createBuilder()
56
            ->table($tableName)
57
        ;
58
    }
59
60
    /**
61
     * Creeate a QueryBuilder instance to work with a Model.
62
     *
63
     * @param  string $modelType The FQN for the model that
64
     *
65
     * @throws Exception If there is no database connection configured.
66
     *
67
     * @return static
68
     */
69
    final public static function createForModel(string $modelType)
70
    {
71
        return self::createBuilder()
72
            ->table(constant("$modelType::TABLE"))
73
            ->setModelType($modelType)
74
        ;
75
    }
76
77
    //
78
    // Overridden QueryBuilder Functions
79
    //
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function __construct(Connection $connection = null)
85
    {
86
        parent::__construct($connection);
87
88
        $this->setFetchMode(PDO::FETCH_ASSOC);
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     *
94
     * @internal Use one of the QueryBuilderFlex get*() methods instead.
95
     *
96
     * @see self::getArray()
97
     * @see self::getModels()
98
     * @see self::getNames()
99
     */
100
    public function get(): array
101
    {
102
        return parent::get();
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function limit($limit): IQueryBuilderHandler
109
    {
110
        $this->resultsPerPage = $limit;
111
112
        return parent::limit($limit);
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    protected function whereHandler($key, string $operator = null, $value = null, $joiner = 'AND'): IQueryBuilderHandler
119
    {
120
        if ($value instanceof BaseModel) {
121
            $value = $value->getId();
122
        }
123
124
        return parent::whereHandler($key, $operator, $value, $joiner);
125
    }
126
127
    //
128
    // QueryBuilderFlex unique functions
129
    //
130
131
    /**
132
     * Request that only non-deleted Models should be returned.
133
     *
134
     * @return static
135
     */
136
    public function active(): QueryBuilderFlex
137
    {
138
        $type = $this->modelType;
139
140
        // Since it's a system model, values are always handled by BZiON core meaning there will always only be "active"
141
        // values in the database.
142
        if ($type::SYSTEM_MODEL) {
143
            return $this;
144
        }
145
146
        $column = $type::DELETED_COLUMN;
147
148
        if ($column === null) {
149
            @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...
150
                sprintf('The use of the status column is deprecated. Update the %s model to use the DELETED_* constants.', get_called_class()),
151
                E_USER_DEPRECATED
152
            );
153
154
            return $this->whereIn('status', $type::getActiveStatuses());
155
        }
156
157
        $stopPropagation = $type::getActiveModels($this);
158
159
        if ($stopPropagation) {
160
            return $this;
161
        }
162
163
        return $this->whereNot($column, '=', $type::DELETED_VALUE);
164
    }
165
166
    /**
167
     * An alias for QueryBuilder::getModels(), with fast fetching on by default and no return of results.
168
     *
169
     * @param  bool $fastFetch Whether to perform one query to load all the model data instead of fetching them one by
170
     *              one
171
     *
172
     * @throws \Pecee\Pixie\Exception
173
     *
174
     * @return void
175
     */
176
    public function addToCache(bool $fastFetch = true): void
177
    {
178
        $this->getModels($fastFetch);
179
    }
180
181
    /**
182
     * Get the amount of pages this query would have.
183
     *
184
     * @throws \Pecee\Pixie\Exception
185
     *
186
     * @return int
187
     */
188
    public function countPages(): int
189
    {
190
        return (int)ceil($this->count() / $this->resultsPerPage);
191
    }
192
193
    /**
194
     * Request that a specific model is not returned.
195
     *
196
     * @param  Model|int $model The ID or model you don't want to get
197
     *
198
     * @return static
199
     */
200
    public function except($model): QueryBuilderFlex
201
    {
202
        if ($model instanceof Model) {
203
            $model = $model->getId();
204
        }
205
206
        $this->whereNot('id', '=', $model);
207
208
        return $this;
209
    }
210
211
    /**
212
     * Find the first matching model in the database or return an invalid model.
213
     *
214
     * @param mixed  $value      The value to search for
215
     * @param string $columnName The column name we'll be checking
216
     *
217
     * @throws \Pecee\Pixie\Exception
218
     *
219
     * @return Model
220
     */
221
    public function findModel($value, string $columnName = 'id'): Model
222
    {
223
        $type = $this->modelType;
224
225
        /** @var array $result */
226
        $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...
227
228
        if ($result === null) {
229
            return $type::get(0);
230
        }
231
232
        return $type::createFromDatabaseResult($result);
233
    }
234
235
    /**
236
     * Only show results from a specific page.
237
     *
238
     * This method will automatically take care of the calculations for a correct OFFSET.
239
     *
240
     * @param  int|null $page The page number (or null to show all pages - counting starts from 0)
241
     *
242
     * @throws \Pecee\Pixie\Exception
243
     *
244
     * @return static
245
     */
246
    public function fromPage(int $page = null): QueryBuilderFlex
247
    {
248
        if ($page === null) {
249
            $this->offset($page);
250
251
            return $this;
252
        }
253
254
        $page = intval($page);
255
        $page = ($page <= 0) ? 1 : $page;
256
257
        $this->offset((min($page, $this->countPages()) - 1) * $this->resultsPerPage);
258
259
        return $this;
260
    }
261
262
    /**
263
     * Get the results of query as an array.
264
     *
265
     * @param array|string $columns
266
     *
267
     * @throws \Pecee\Pixie\Exception
268
     *
269
     * @return array
270
     */
271
    public function getArray($columns): array
272
    {
273
        $this->select($columns);
274
275
        $queryObject = $this->getQuery();
276
        $debug = new DatabaseQuery($queryObject->getSql(), $queryObject->getBindings());
277
278
        $results = $this->get();
279
280
        $debug->finish($results);
281
282
        return $results;
283
    }
284
285
    /**
286
     * Perform the query and get the results as Models.
287
     *
288
     * @param  bool $fastFetch Whether to perform one query to load all the model data instead of fetching them one by
289
     *                         one (ignores cache)
290
     *
291
     * @throws \Pecee\Pixie\Exception
292
     *
293
     * @return Model[]
294
     */
295
    public function getModels(bool $fastFetch = true): array
296
    {
297
        /** @var Model $type */
298
        $type = $this->modelType;
299
300
        $modelColumnsToSelect = $type::getEagerColumnsList();
301
302
        if (isset($this->statements['joins'])) {
303
            $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...
304
                return sprintf('%s.%s', $type::TABLE, $value);
305
            });
306
        }
307
308
        $this->select($modelColumnsToSelect);
309
310
        $queryObject = $this->getQuery();
311
        $debug = new DatabaseQuery($queryObject->getSql(), $queryObject->getBindings());
312
313
        /** @var array $results */
314
        $results = $this->get();
315
316
        $debug->finish($results);
317
318
        if ($fastFetch) {
319
            return $type::createFromDatabaseResults($results);
320
        }
321
322
        return $type::arrayIdToModel(array_column($results, 'id'));
323
    }
324
325
    /**
326
     * Perform the query and get back the results in an array of names.
327
     *
328
     * @throws \Pecee\Pixie\Exception
329
     * @throws UnexpectedValueException When no name column has been specified
330
     *
331
     * @return string[] An array of the type $id => $name
332
     */
333
    public function getNames(): array
334
    {
335
        if (!$this->modelNameColumn) {
336
            throw new UnexpectedValueException(sprintf('The name column has not been specified for this query builder. Use %s::setNameColumn().', get_called_class()));
337
        }
338
339
        $this->select(['id', $this->modelNameColumn]);
340
341
        $queryObject = $this->getQuery();
342
        $debug = new DatabaseQuery($queryObject->getSql(), $queryObject->getBindings());
343
344
        /** @var array $results */
345
        $results = $this->get();
346
347
        $debug->finish($results);
348
349
        return array_column($results, $this->modelNameColumn, 'id');
350
    }
351
352
    /**
353
     * Set the model this QueryBuilder will be working this.
354
     *
355
     * This information is used for automatically retrieving table names, eager columns, and lazy columns for these
356
     * models.
357
     *
358
     * @param  string $modelType The FQN of the model this QueryBuilder will be working with
359
     *
360
     * @return $this
361
     */
362
    public function setModelType(string $modelType = null): QueryBuilderFlex
363
    {
364
        $this->modelType = $modelType;
365
366
        return $this;
367
    }
368
369
    /**
370
     * Set the column that'll be used as the human-friendly name of the model.
371
     *
372
     * @param string $columnName
373
     *
374
     * @return static
375
     */
376
    public function setNameColumn(string $columnName): QueryBuilderFlex
377
    {
378
        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...
379
            throw new LogicException(sprintf('Setting name columns is only supported in models implementing the "%s" interface.', NamedModel::class));
380
        }
381
382
        $this->modelNameColumn = $columnName;
383
384
        return $this;
385
    }
386
387
    /**
388
     * Make sure that Models invisible to a player are not returned.
389
     *
390
     * Note that this method does not take PermissionModel::canBeSeenBy() into
391
     * consideration for performance purposes, so you will have to override this
392
     * in your query builder if necessary.
393
     *
394
     * @param  Player $player      The player in question
395
     * @param  bool   $showDeleted Use false to hide deleted models even from admins
396
     *
397
     * @return static
398
     */
399
    public function visibleTo(Player $player, bool $showDeleted = false): QueryBuilderFlex
400
    {
401
        $type = $this->modelType;
402
403
        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...
404
            $player->hasPermission(constant("$type::EDIT_PERMISSION"))
405
        ) {
406
            // The player is an admin who can see the hidden models
407
            if (!$showDeleted) {
408
                $col = constant("$type::DELETED_COLUMN");
409
410
                if ($col !== null) {
411
                    $this->whereNot($col, '=', constant("$type::DELETED_VALUE"));
412
                }
413
            }
414
        } else {
415
            return $this->active();
416
        }
417
418
        return $this;
419
    }
420
}
421