Completed
Push — master ( 0ce6cd...798843 )
by Oscar
02:40
created

Select::run()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 18
nc 12
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 $leftJoin = [];
23
    protected $orderBy = [];
24
    protected $statement;
25
    protected $mode = 2;
26
    protected $page;
27
28
    /**
29
     * Change the mode to returns just the first row.
30
     * 
31
     * @return self
32
     */
33
    public function one()
34
    {
35
        $this->mode = self::MODE_ONE;
36
37
        return $this->limit(1);
38
    }
39
40
    /**
41
     * Change the mode to returns all rows.
42
     * 
43
     * @return self
44
     */
45
    public function all()
46
    {
47
        $this->mode = self::MODE_ALL;
48
49
        return $this;
50
    }
51
52
    /**
53
     * Paginate the results
54
     *
55
     * @param int|string $page
56
     * @param int|null   $limit
57
     *
58
     * @return self
59
     */
60
    public function page($page, $limit = null) {
61
        $this->page = (int) $page;
62
63
        if ($this->page < 1) {
64
            $this->page = 1;
65
        }
66
67
        if ($limit !== null) {
68
            $this->limit($limit);
69
        }
70
71
        return $this;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     *
77
     * @return Row|RowCollection|null
78
     */
79
    public function run()
80
    {
81
        $statement = $this->__invoke();
82
83
        if ($this->mode === self::MODE_ONE) {
84
            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); (array) is incompatible with the return type of the parent method SimpleCrud\Queries\Query::run of type SimpleCrud\Queries\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...
85
        }
86
87
        $result = $this->table->createCollection();
88
89
        while (($row = $statement->fetch())) {
90
            $result[] = $this->createRow($row);
91
        }
92
93
        if ($this->page !== null) {
94
            $current = $this->page;
95
            $next = $result->count() < $this->limit ? null : $current + 1;
96
            $prev = $current > 1 ? $current - 1 : null;
97
98
            $result->setMethod('getPage', function () use ($current) {
99
                return $current;
100
            });
101
102
            $result->setMethod('getNextPage', function () use ($next) {
103
                return $next;
104
            });
105
106
            $result->setMethod('getPrevPage', function () use ($prev) {
107
                return $prev;
108
            });
109
        }
110
111
        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 SimpleCrud\Queries\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...
112
    }
113
114
    /**
115
     * Create a row and insert the joined rows if exist.
116
     *
117
     * @param array $data
118
     * 
119
     * @return Row
120
     */
121
    public function createRow(array $data)
122
    {
123
        $row = $this->table->createFromDatabase($data);
124
125
        if (empty($this->leftJoin)) {
126
            return $row;
127
        }
128
129
        foreach ($this->leftJoin as $join) {
130
            $table = $this->table->getDatabase()->{$join['table']};
131
            $values = [];
132
133
            foreach (array_keys($table->fields) as $name) {
134
                $field = sprintf('%s.%s', $join['table'], $name);
135
                $values[$name] = $data[$field];
136
            }
137
138
            $row->{$join['table']} = empty($values['id']) ? null : $table->createFromDatabase($values);
139
        }
140
141
        return $row;
142
    }
143
144
    /**
145
     * Adds an ORDER BY clause.
146
     *
147
     * @param string      $orderBy
148
     * @param string|null $direction
149
     *
150
     * @return self
151
     */
152
    public function orderBy($orderBy, $direction = null)
153
    {
154
        if (!empty($direction)) {
155
            $orderBy .= ' '.$direction;
156
        }
157
158
        $this->orderBy[] = $orderBy;
159
160
        return $this;
161
    }
162
163
    /**
164
     * Adds a LEFT JOIN clause.
165
     *
166
     * @param string     $table
167
     * @param string     $on
168
     * @param array|null $marks
169
     *
170
     * @return self
171
     */
172
    public function leftJoin($table, $on = null, $marks = null)
173
    {
174
        $scheme = $this->table->getScheme();
175
176 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...
177
            throw new SimpleCrudException(sprintf("The tables '%s' and '%s' are not related", $this->table->name, $table));
178
        }
179
180 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...
181
            throw new SimpleCrudException(sprintf("Invalid LEFT JOIN between the tables '%s' and '%s'", $this->table->name, $table));
182
        }
183
184
        $this->leftJoin[] = [
185
            'table' => $table,
186
            'on' => $on,
187
        ];
188
189
        if ($marks) {
190
            $this->marks += $marks;
191
        }
192
193
        return $this;
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 View Code Duplication
    public function __invoke()
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...
200
    {
201
        $statement = $this->table->getDatabase()->execute((string) $this, $this->marks);
202
        $statement->setFetchMode(PDO::FETCH_ASSOC);
203
204
        return $statement;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $statement; (PDOStatement) is incompatible with the return type declared by the abstract method SimpleCrud\Queries\Query::__invoke of type SimpleCrud\Queries\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...
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function __toString()
211
    {
212
        if ($this->page !== null) {
213
            $this->offset = ($this->page * $this->limit) - $this->limit;
214
        }
215
216
        $query = 'SELECT';
217
        $query .= ' '.static::buildFields($this->table->name, array_keys($this->table->fields));
218
219
        foreach ($this->leftJoin as $join) {
220
            $query .= ', '.static::buildFields($join['table'], array_keys($this->table->getDatabase()->{$join['table']}->fields), true);
221
        }
222
223
        $query .= $this->fieldsToString();
224
        $query .= sprintf(' FROM `%s`', $this->table->name);
225
        $query .= $this->fromToString();
226
227
        foreach ($this->leftJoin as $join) {
228
            $relation = $this->table->getScheme()['relations'][$join['table']];
229
230
            $query .= sprintf(
231
                ' LEFT JOIN `%s` ON (`%s`.`id` = `%s`.`%s`%s)',
232
                $join['table'],
233
                $join['table'],
234
                $this->table->name,
235
                $relation[1],
236
                empty($join['on']) ? '' : sprintf(' AND (%s)', $join['on'])
237
            );
238
        }
239
240
        $query .= $this->whereToString();
241
242
        if (!empty($this->orderBy)) {
243
            $query .= ' ORDER BY '.implode(', ', $this->orderBy);
244
        }
245
246
        $query .= $this->limitToString();
247
248
        return $query;
249
    }
250
251
    /**
252
     * Generates the fields/tables part of a SELECT query.
253
     *
254
     * @param string $table
255
     * @param array  $fields
256
     * @param bool   $rename
257
     *
258
     * @return string
259
     */
260
    protected static function buildFields($table, array $fields, $rename = false)
261
    {
262
        $query = [];
263
264
        foreach ($fields as $field) {
265
            if ($rename) {
266
                $query[] = "`{$table}`.`{$field}` as `{$table}.{$field}`";
267
            } else {
268
                $query[] = "`{$table}`.`{$field}`";
269
            }
270
        }
271
272
        return implode(', ', $query);
273
    }
274
}
275