Completed
Pull Request — master (#186)
by Vladimir
06:00 queued 03:00
created

QueryBuilderFlex::whereHandler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 4
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
    private $modelNameColumn;
17
18
    /** @var Model|string The FQN of the model object this QueryBuilder instance is for */
19
    private $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
    public static function createBuilder(): QueryBuilderFlex
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 QueryBuilderFlex
52
     */
53
    public static function createForTable(string $tableName): QueryBuilderFlex
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 QueryBuilderFlex
68
     */
69
    public static function createForModel(string $modelType): QueryBuilderFlex
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
        $this->select($type::getEagerColumnsList());
264
265
        $queryObject = $this->getQuery();
266
        $debug = new DatabaseQuery($queryObject->getSql(), $queryObject->getBindings());
267
268
        /** @var array $results */
269
        $results = $this->get();
270
271
        $debug->finish($results);
272
273
        if ($fastFetch) {
274
            return $type::createFromDatabaseResults($results);
275
        }
276
277
        return $type::arrayIdToModel(array_column($results, 'id'));
278
    }
279
280
    /**
281
     * Perform the query and get back the results in an array of names.
282
     *
283
     * @throws \Pecee\Pixie\Exception
284
     * @throws UnexpectedValueException When no name column has been specified
285
     *
286
     * @return string[] An array of the type $id => $name
287
     */
288
    public function getNames(): array
289
    {
290
        if (!$this->modelNameColumn) {
291
            throw new UnexpectedValueException(sprintf('The name column has not been specified for this query builder. Use %s::setNameColumn().', get_called_class()));
292
        }
293
294
        $this->select(['id', $this->modelNameColumn]);
295
296
        $queryObject = $this->getQuery();
297
        $debug = new DatabaseQuery($queryObject->getSql(), $queryObject->getBindings());
298
299
        /** @var array $results */
300
        $results = $this->get();
301
302
        $debug->finish($results);
303
304
        return array_column($results, $this->modelNameColumn, 'id');
305
    }
306
307
    /**
308
     * Set the model this QueryBuilder will be working this.
309
     *
310
     * This information is used for automatically retrieving table names, eager columns, and lazy columns for these
311
     * models.
312
     *
313
     * @param  string $modelType The FQN of the model this QueryBuilder will be working with
314
     *
315
     * @return $this
316
     */
317
    public function setModelType(string $modelType = null): QueryBuilderFlex
318
    {
319
        $this->modelType = $modelType;
320
321
        return $this;
322
    }
323
324
    /**
325
     * Set the column that'll be used as the human-friendly name of the model.
326
     *
327
     * @param string $columnName
328
     *
329
     * @return static
330
     */
331
    public function setNameColumn(string $columnName): QueryBuilderFlex
332
    {
333
        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...
334
            throw new LogicException(sprintf('Setting name columns is only supported in models implementing the "%s" interface.', NamedModel::class));
335
        }
336
337
        $this->modelNameColumn = $columnName;
338
339
        return $this;
340
    }
341
342
    /**
343
     * Make sure that Models invisible to a player are not returned.
344
     *
345
     * Note that this method does not take PermissionModel::canBeSeenBy() into
346
     * consideration for performance purposes, so you will have to override this
347
     * in your query builder if necessary.
348
     *
349
     * @param  Player $player      The player in question
350
     * @param  bool   $showDeleted Use false to hide deleted models even from admins
351
     *
352
     * @return static
353
     */
354
    public function visibleTo(Player $player, bool $showDeleted = false): QueryBuilderFlex
355
    {
356
        $type = $this->modelType;
357
358
        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...
359
            $player->hasPermission(constant("$type::EDIT_PERMISSION"))
360
        ) {
361
            // The player is an admin who can see the hidden models
362
            if (!$showDeleted) {
363
                $col = constant("$type::DELETED_COLUMN");
364
365
                if ($col !== null) {
366
                    $this->whereNot($col, '=', constant("$type::DELETED_VALUE"));
367
                }
368
            }
369
        } else {
370
            return $this->active();
371
        }
372
373
        return $this;
374
    }
375
}
376