Completed
Push — master ( fbba87...38916d )
by Oscar
02:14
created

Select::leftJoin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 3
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
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->join)) {
126
            return $row;
127
        }
128
129
        foreach ($this->join as $joins) {
130
            foreach ($joins as $join) {
131
                $table = $this->table->getDatabase()->{$join['table']};
132
                $values = [];
133
134
                foreach (array_keys($table->fields) as $name) {
135
                    $field = sprintf('%s.%s', $join['table'], $name);
136
                    $values[$name] = $data[$field];
137
                }
138
139
                $row->{$join['table']} = empty($values['id']) ? null : $table->createFromDatabase($values);
140
            }
141
        }
142
143
        return $row;
144
    }
145
146
    /**
147
     * Adds an ORDER BY clause.
148
     *
149
     * @param string      $orderBy
150
     * @param string|null $direction
151
     *
152
     * @return self
153
     */
154
    public function orderBy($orderBy, $direction = null)
155
    {
156
        if (!empty($direction)) {
157
            $orderBy .= ' '.$direction;
158
        }
159
160
        $this->orderBy[] = $orderBy;
161
162
        return $this;
163
    }
164
165
    /**
166
     * Adds a LEFT JOIN clause.
167
     *
168
     * @param string     $table
169
     * @param string     $on
170
     * @param array|null $marks
171
     *
172
     * @return self
173
     */
174
    public function leftJoin($table, $on = null, $marks = null)
175
    {
176
        return $this->join('LEFT', $table, $on, $marks);
177
    }
178
179
    /**
180
     * Adds a JOIN clause.
181
     *
182
     * @param string     $join
183
     * @param string     $table
184
     * @param string     $on
185
     * @param array|null $marks
186
     *
187
     * @return self
188
     */
189
    public function join($join, $table, $on = null, $marks = null)
190
    {
191
        $join = strtoupper($join);
192
        $scheme = $this->table->getScheme();
193
194 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...
195
            throw new SimpleCrudException(sprintf("The tables '%s' and '%s' are not related", $this->table->name, $table));
196
        }
197
198 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...
199
            throw new SimpleCrudException(sprintf("Invalid %s JOIN between the tables '%s' and '%s'", $join, $this->table->name, $table));
200
        }
201
202
        if (!isset($this->join[$join])) {
203
            $this->join[$join] = [];
204
        }
205
206
        $this->join[$join][] = [
207
            'table' => $table,
208
            'on' => $on,
209
        ];
210
211
        if ($marks) {
212
            $this->marks += $marks;
213
        }
214
215
        return $this;
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221 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...
222
    {
223
        $statement = $this->table->getDatabase()->execute((string) $this, $this->marks);
224
        $statement->setFetchMode(PDO::FETCH_ASSOC);
225
226
        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...
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    public function __toString()
233
    {
234
        if ($this->page !== null) {
235
            $this->offset = ($this->page * $this->limit) - $this->limit;
236
        }
237
238
        $query = 'SELECT';
239
        $query .= ' '.static::buildFields($this->table->name, array_keys($this->table->fields));
240
241
        foreach ($this->join as $joins) {
242
            foreach ($joins as $join) {
243
                $query .= ', '.static::buildFields($join['table'], array_keys($this->table->getDatabase()->{$join['table']}->fields), true);
244
            }
245
        }
246
247
        $query .= $this->fieldsToString();
248
        $query .= sprintf(' FROM `%s`', $this->table->name);
249
        $query .= $this->fromToString();
250
251
        foreach ($this->join as $type => $joins) {
252
            foreach ($joins as $join) {
253
                $relation = $this->table->getScheme()['relations'][$join['table']];
254
255
                $query .= sprintf(
256
                    ' %s JOIN `%s` ON (`%s`.`id` = `%s`.`%s`%s)',
257
                    $type,
258
                    $join['table'],
259
                    $join['table'],
260
                    $this->table->name,
261
                    $relation[1],
262
                    empty($join['on']) ? '' : sprintf(' AND (%s)', $join['on'])
263
                );
264
            }
265
        }
266
267
        $query .= $this->whereToString();
268
269
        if (!empty($this->orderBy)) {
270
            $query .= ' ORDER BY '.implode(', ', $this->orderBy);
271
        }
272
273
        $query .= $this->limitToString();
274
275
        return $query;
276
    }
277
278
    /**
279
     * Generates the fields/tables part of a SELECT query.
280
     *
281
     * @param string $table
282
     * @param array  $fields
283
     * @param bool   $rename
284
     *
285
     * @return string
286
     */
287
    protected static function buildFields($table, array $fields, $rename = false)
288
    {
289
        $query = [];
290
291
        foreach ($fields as $field) {
292
            if ($rename) {
293
                $query[] = "`{$table}`.`{$field}` as `{$table}.{$field}`";
294
            } else {
295
                $query[] = "`{$table}`.`{$field}`";
296
            }
297
        }
298
299
        return implode(', ', $query);
300
    }
301
}
302