Completed
Push — master ( 2ba6ce...b0d87c )
by Oscar
03:11
created

Select::limit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
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
        if ($this->calcFoundRows) {
107
            $total = $this->table->getDatabase()->execute((string) 'SELECT FOUND_ROWS()')->fetch();
108
            $total = (int) $total[0];
109
110
            $result->setMethod('getTotal', function () use ($total) {
111
                return $total;
112
            });
113
        }
114
115
        while (($row = $statement->fetch())) {
116
            $result[] = $this->createRow($row);
117
        }
118
119
        if ($this->page !== null) {
120
            $current = $this->page;
121
            $next = $result->count() < $this->limit ? null : $current + 1;
122
            $prev = $current > 1 ? $current - 1 : null;
123
124
            $result->setMethod('getPage', function () use ($current) {
125
                return $current;
126
            });
127
128
            $result->setMethod('getNextPage', function () use ($next) {
129
                return $next;
130
            });
131
132
            $result->setMethod('getPrevPage', function () use ($prev) {
133
                return $prev;
134
            });
135
        }
136
137
        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...
138
    }
139
140
    /**
141
     * Create a row and insert the joined rows if exist.
142
     *
143
     * @param array $data
144
     * 
145
     * @return Row
146
     */
147
    public function createRow(array $data)
148
    {
149
        $row = $this->table->createFromDatabase($data);
150
151
        if (empty($this->join)) {
152
            return $row;
153
        }
154
155
        foreach ($this->join as $joins) {
156
            foreach ($joins as $join) {
157
                $table = $this->table->getDatabase()->{$join['table']};
158
                $values = [];
159
160
                foreach (array_keys($table->getScheme()['fields']) as $name) {
161
                    $field = sprintf('%s.%s', $join['table'], $name);
162
                    $values[$name] = $data[$field];
163
                }
164
165
                $row->{$join['table']} = empty($values['id']) ? null : $table->createFromDatabase($values);
166
            }
167
        }
168
169
        return $row;
170
    }
171
172
    /**
173
     * Adds an ORDER BY clause.
174
     *
175
     * @param string      $orderBy
176
     * @param string|null $direction
177
     *
178
     * @return self
179
     */
180
    public function orderBy($orderBy, $direction = null)
181
    {
182
        if (!empty($direction)) {
183
            $orderBy .= ' '.$direction;
184
        }
185
186
        $this->orderBy[] = $orderBy;
187
188
        return $this;
189
    }
190
191
    /**
192
     * Adds a LEFT JOIN clause.
193
     *
194
     * @param string     $table
195
     * @param string     $on
196
     * @param array|null $marks
197
     *
198
     * @return self
199
     */
200
    public function leftJoin($table, $on = null, $marks = null)
201
    {
202
        return $this->join('LEFT', $table, $on, $marks);
203
    }
204
205
    /**
206
     * Adds a JOIN clause.
207
     *
208
     * @param string     $join
209
     * @param string     $table
210
     * @param string     $on
211
     * @param array|null $marks
212
     *
213
     * @return self
214
     */
215
    public function join($join, $table, $on = null, $marks = null)
216
    {
217
        $join = strtoupper($join);
218
        $scheme = $this->table->getScheme();
219
220 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...
221
            throw new SimpleCrudException(sprintf("The tables '%s' and '%s' are not related", $this->table->getName(), $table));
222
        }
223
224 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...
225
            throw new SimpleCrudException(sprintf("Invalid %s JOIN between the tables '%s' and '%s'", $join, $this->table->getName(), $table));
226
        }
227
228
        if (!isset($this->join[$join])) {
229
            $this->join[$join] = [];
230
        }
231
232
        $this->join[$join][] = [
233
            'table' => $table,
234
            'on' => $on,
235
        ];
236
237
        if ($marks) {
238
            $this->marks += $marks;
239
        }
240
241
        return $this;
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247 View Code Duplication
    public function __invoke()
248
    {
249
        $statement = $this->table->getDatabase()->execute((string) $this, $this->marks);
250
        $statement->setFetchMode(PDO::FETCH_ASSOC);
251
252
        return $statement;
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function __toString()
259
    {
260
        if ($this->page !== null) {
261
            $this->offset = ($this->page * $this->limit) - $this->limit;
262
        }
263
264
        $query = 'SELECT';
265
266
        if ($this->calcFoundRows) {
267
            $query .= ' SQL_CALC_FOUND_ROWS';
268
        }
269
270
        $query .= ' '.static::buildFields($this->table);
271
272
        foreach ($this->join as $joins) {
273
            foreach ($joins as $join) {
274
                $query .= ', '.static::buildFields($this->table->getDatabase()->{$join['table']}, true);
275
            }
276
        }
277
278
        $query .= $this->fieldsToString();
279
        $query .= sprintf(' FROM `%s`', $this->table->getName());
280
        $query .= $this->fromToString();
281
282
        foreach ($this->join as $type => $joins) {
283
            foreach ($joins as $join) {
284
                $relation = $this->table->getScheme()['relations'][$join['table']];
285
286
                $query .= sprintf(
287
                    ' %s JOIN `%s` ON (`%s`.`id` = `%s`.`%s`%s)',
288
                    $type,
289
                    $join['table'],
290
                    $join['table'],
291
                    $this->table->getName(),
292
                    $relation[1],
293
                    empty($join['on']) ? '' : sprintf(' AND (%s)', $join['on'])
294
                );
295
            }
296
        }
297
298
        $query .= $this->whereToString();
299
300
        if (!empty($this->orderBy)) {
301
            $query .= ' ORDER BY '.implode(', ', $this->orderBy);
302
        }
303
304
        $query .= $this->limitToString();
305
306
        return $query;
307
    }
308
309
    /**
310
     * Generates the fields/tables part of a SELECT query.
311
     *
312
     * @param Table $table
313
     * @param bool  $rename
314
     *
315
     * @return string
316
     */
317
    protected static function buildFields(Table $table, $rename = false)
318
    {
319
        $tableName = $table->getName();
320
        $query = [];
321
322
        foreach ($table->getFields() as $fieldName => $field) {
323
            $query[] = $field->getSelectExpression($rename ? "{$tableName}.{$fieldName}" : null);
324
        }
325
326
        return implode(', ', $query);
327
    }
328
}
329