Completed
Pull Request — master (#186)
by Vladimir
10:46
created

QueryBuilderFlex::active()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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