Completed
Push — master ( d65f51...5b82a8 )
by Oscar
02:16
created

Select::getFoundRows()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace SimpleCrud\Queries\Mysql;
4
5
use SimpleCrud\SimpleCrudException;
6
use SimpleCrud\Queries\Query;
7
use SimpleCrud\Scheme\Scheme;
8
use SimpleCrud\RowCollection;
9
use SimpleCrud\Table;
10
use PDO;
11
12
/**
13
 * Manages a database select query.
14
 */
15
class Select extends Query
16
{
17
    const MODE_ONE = 1;
18
    const MODE_ALL = 2;
19
20
    use ExtendedSelectionTrait;
21
22
    protected $join = [];
23
    protected $orderBy = [];
24
    protected $statement;
25
    protected $mode = 2;
26
    protected $page;
27
    protected $calcFoundRows = false;
28
29
    /**
30
     * Change the mode to returns just the first row.
31
     * 
32
     * @return self
33
     */
34
    public function one()
35
    {
36
        $this->mode = self::MODE_ONE;
37
38
        return $this->limit(1);
39
    }
40
41
    /**
42
     * Change the mode to returns all rows.
43
     * 
44
     * @return self
45
     */
46
    public function all()
47
    {
48
        $this->mode = self::MODE_ALL;
49
50
        return $this;
51
    }
52
53
    /**
54
     * Adds a LIMIT clause.
55
     *
56
     * @param int  $limit
57
     * @param bool $calcFoundRows
58
     *
59
     * @return self
60
     */
61
    public function limit($limit, $calcFoundRows = false)
62
    {
63
        $this->limit = $limit;
64
        $this->calcFoundRows = (bool) $calcFoundRows;
65
66
        return $this;
67
    }
68
69
    /**
70
     * Paginate the results
71
     *
72
     * @param int|string $page
73
     * @param int|null   $limit
74
     *
75
     * @return self
76
     */
77
    public function page($page, $limit = null) {
78
        $this->page = (int) $page;
79
80
        if ($this->page < 1) {
81
            $this->page = 1;
82
        }
83
84
        if ($limit !== null) {
85
            $this->limit($limit);
86
        }
87
88
        return $this;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     *
94
     * @return Row|RowCollection|null
95
     */
96
    public function run()
97
    {
98
        $statement = $this->__invoke();
99
100
        if ($this->mode === self::MODE_ONE) {
101
            return ($row = $statement->fetch()) === false ? null : $this->createRow($row);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return ($row = $statemen...$this->createRow($row); (SimpleCrud\Row) is incompatible with the return type of the parent method SimpleCrud\Queries\Query::run of type PDOStatement.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
102
        }
103
104
        $result = $this->table->createCollection();
105
106
        $total = null;
107
108
        if ($this->calcFoundRows) {
109
            $total = $this->getFoundRows();
110
111
            $result->setMethod('getTotal', function () use ($total) {
112
                return $total;
113
            });
114
        }
115
116
        while (($row = $statement->fetch())) {
117
            $result[] = $this->createRow($row);
118
        }
119
120
        if ($this->page !== null) {
121
            $current = $this->page;
122
            // $next = $result->count() < $this->limit ? null : $current + 1;
123
            $next = ($current * $this->limit) < $total ? $current + 1 : null;
124
            $prev = $current > 1 ? $current - 1 : null;
125
126
            $result->setMethod('getPage', function () use ($current) {
127
                return $current;
128
            });
129
130
            $result->setMethod('getNextPage', function () use ($next) {
131
                return $next;
132
            });
133
134
            $result->setMethod('getPrevPage', function () use ($prev) {
135
                return $prev;
136
            });
137
        }
138
139
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (SimpleCrud\RowCollection) is incompatible with the return type of the parent method SimpleCrud\Queries\Query::run of type PDOStatement.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
140
    }
141
142
    /**
143
     * Create a row and insert the joined rows if exist.
144
     *
145
     * @param array $data
146
     * 
147
     * @return Row
148
     */
149
    public function createRow(array $data)
150
    {
151
        $row = $this->table->createFromDatabase($data);
152
153
        if (empty($this->join)) {
154
            return $row;
155
        }
156
157
        foreach ($this->join as $joins) {
158
            foreach ($joins as $join) {
159
                $table = $this->table->getDatabase()->{$join['table']};
160
                $values = [];
161
162
                foreach (array_keys($table->getScheme()['fields']) as $name) {
163
                    $field = sprintf('%s.%s', $join['table'], $name);
164
                    $values[$name] = $data[$field];
165
                }
166
167
                $row->{$join['table']} = empty($values['id']) ? null : $table->createFromDatabase($values);
168
            }
169
        }
170
171
        return $row;
172
    }
173
174
    /**
175
     * Adds an ORDER BY clause.
176
     *
177
     * @param string      $orderBy
178
     * @param string|null $direction
179
     *
180
     * @return self
181
     */
182
    public function orderBy($orderBy, $direction = null)
183
    {
184
        if (!empty($direction)) {
185
            $orderBy .= ' '.$direction;
186
        }
187
188
        $this->orderBy[] = $orderBy;
189
190
        return $this;
191
    }
192
193
    /**
194
     * Adds a LEFT JOIN clause.
195
     *
196
     * @param string     $table
197
     * @param string     $on
198
     * @param array|null $marks
199
     *
200
     * @return self
201
     */
202
    public function leftJoin($table, $on = null, $marks = null)
203
    {
204
        return $this->join('LEFT', $table, $on, $marks);
205
    }
206
207
    /**
208
     * Adds a JOIN clause.
209
     *
210
     * @param string     $join
211
     * @param string     $table
212
     * @param string     $on
213
     * @param array|null $marks
214
     *
215
     * @return self
216
     */
217
    public function join($join, $table, $on = null, $marks = null)
218
    {
219
        $join = strtoupper($join);
220
        $scheme = $this->table->getScheme();
221
222 View Code Duplication
        if (!isset($scheme['relations'][$table])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
223
            throw new SimpleCrudException(sprintf("The tables '%s' and '%s' are not related", $this->table->getName(), $table));
224
        }
225
226 View Code Duplication
        if ($scheme['relations'][$table][0] !== Scheme::HAS_ONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
227
            throw new SimpleCrudException(sprintf("Invalid %s JOIN between the tables '%s' and '%s'", $join, $this->table->getName(), $table));
228
        }
229
230
        if (!isset($this->join[$join])) {
231
            $this->join[$join] = [];
232
        }
233
234
        $this->join[$join][] = [
235
            'table' => $table,
236
            'on' => $on,
237
        ];
238
239
        if ($marks) {
240
            $this->marks += $marks;
241
        }
242
243
        return $this;
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249 View Code Duplication
    public function __invoke()
250
    {
251
        $statement = $this->table->getDatabase()->execute((string) $this, $this->marks);
252
        $statement->setFetchMode(PDO::FETCH_ASSOC);
253
254
        return $statement;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function __toString()
261
    {
262
        if ($this->page !== null) {
263
            $this->offset = ($this->page * $this->limit) - $this->limit;
264
        }
265
266
        $query = 'SELECT';
267
268
        if ($this->calcFoundRows) {
269
            $query .= ' '.static::buildFoundRows();
270
        }
271
272
        $query .= ' '.static::buildFields($this->table);
273
274
        foreach ($this->join as $joins) {
275
            foreach ($joins as $join) {
276
                $query .= ', '.static::buildFields($this->table->getDatabase()->{$join['table']}, true);
277
            }
278
        }
279
280
        $query .= $this->fieldsToString();
281
        $query .= sprintf(' FROM `%s`', $this->table->getName());
282
        $query .= $this->fromToString();
283
284
        foreach ($this->join as $type => $joins) {
285
            foreach ($joins as $join) {
286
                $relation = $this->table->getScheme()['relations'][$join['table']];
287
288
                $query .= sprintf(
289
                    ' %s JOIN `%s` ON (`%s`.`id` = `%s`.`%s`%s)',
290
                    $type,
291
                    $join['table'],
292
                    $join['table'],
293
                    $this->table->getName(),
294
                    $relation[1],
295
                    empty($join['on']) ? '' : sprintf(' AND (%s)', $join['on'])
296
                );
297
            }
298
        }
299
300
        $query .= $this->whereToString();
301
302
        if (!empty($this->orderBy)) {
303
            $query .= ' ORDER BY '.implode(', ', $this->orderBy);
304
        }
305
306
        $query .= $this->limitToString();
307
308
        return $query;
309
    }
310
311
    /**
312
     * Generates the fields/tables part of a SELECT query.
313
     *
314
     * @param Table $table
315
     * @param bool  $rename
316
     *
317
     * @return string
318
     */
319
    protected static function buildFields(Table $table, $rename = false)
320
    {
321
        $tableName = $table->getName();
322
        $query = [];
323
324
        foreach ($table->getFields() as $fieldName => $field) {
325
            $query[] = $field->getSelectExpression($rename ? "{$tableName}.{$fieldName}" : null);
326
        }
327
328
        return implode(', ', $query);
329
    }
330
331
    /**
332
     * Generates the SQL_CALC_FOUND_ROWS part of a SELECT query.
333
     *
334
     * @return string
335
     */
336
    protected static function buildFoundRows()
337
    {
338
        return 'SQL_CALC_FOUND_ROWS';
339
    }
340
341
    /**
342
     * Returns the total rows found
343
     * 
344
     * @return int
345
     */
346
    protected function getFoundRows() {
347
        $total = $this->table->getDatabase()->execute('SELECT FOUND_ROWS()')->fetch();
348
        return (int) $total[0];
349
    }
350
}
351