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

QueryBuilderFlex   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 351
Duplicated Lines 6.55 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 0
Metric Value
wmc 31
lcom 2
cbo 7
dl 23
loc 351
rs 9.8
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A createForTable() 11 11 1
A createForModel() 12 12 1
A __construct() 0 6 1
A limit() 0 6 1
A whereHandler() 0 8 2
B active() 0 29 4
A addToCache() 0 4 1
A countPages() 0 4 1
A except() 0 10 2
A findModel() 0 13 2
A fromPage() 0 15 3
A getModels() 0 21 2
A getNames() 0 18 2
A setModelType() 0 6 1
A setNameColumn() 0 10 2
B visibleTo() 0 21 5

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
use BZIon\Debug\DatabaseQuery;
4
use Pixie\QueryBuilder\QueryBuilderHandler;
5
6
/**
7
 * The core query builder used across BZiON for creating and modifying queries for all of our entities.
8
 *
9
 * @since 0.11.0
10
 */
11
class QueryBuilderFlex extends QueryBuilderHandler
12
{
13
    /** @var string The column name of the column dedicated to storing the name of the model */
14
    private $modelNameColumn;
15
16
    /** @var Model|string The FQN of the model object this QueryBuilder instance is for */
17
    private $modelType = null;
18
19
    /** @var int The amount of results per page with regards to result pagination */
20
    private $resultsPerPage;
21
22
    //
23
    // Factories
24
    //
25
26
    /**
27
     * Create a QueryBuilder instance for a specific table.
28
     *
29
     * @param  string $tableName
30
     *
31
     * @throws Exception If there is no database connection configured.
32
     *
33
     * @return QueryBuilderFlex
34
     */
35 View Code Duplication
    public static function createForTable($tableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
36
    {
37
        Database::getInstance();
38
39
        $connection = Service::getQueryBuilderConnection();
40
        $qbBase = new QueryBuilderFlex($connection);
41
42
        $queryBuilder = $qbBase->table($tableName);
43
44
        return $queryBuilder;
45
    }
46
47
    /**
48
     * Creeate a QueryBuilder instance to work with a Model.
49
     *
50
     * @param  string $modelType The FQN for the model that
51
     *
52
     * @throws Exception If there is no database connection configured.
53
     *
54
     * @return QueryBuilderFlex
55
     */
56 View Code Duplication
    public static function createForModel($modelType)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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