QueryGrammar::isJsonSelector()   A
last analyzed

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 1
1
<?php
2
namespace Childish\query;
3
4
use Childish\query\Builder;
5
use Childish\support\Collection;
6
use Childish\support\Tools;
7
8
/**
9
 * QueryGrammar
10
 *
11
 * @author    Pu ShaoWei <[email protected]>
12
 * @date      2017/12/7
13
 * @package   Childish
14
 * @version   1.0
15
 */
16
class QueryGrammar extends Grammar
17
{
18
    /**
19
     * The components that make up a select clause.
20
     *
21
     * @var array
22
     */
23
    protected $selectComponents = [
24
        'aggregate',
25
        'columns',
26
        'from',
27
        'joins',
28
        'wheres',
29
        'groups',
30
        'havings',
31
        'orders',
32
        'limit',
33
        'offset',
34
        'lock',
35
    ];
36
37
    /**
38
     * Compile a select query into SQL.
39
     *
40
     * @param  \Childish\query\Builder  $query
41
     * @return string
42
     */
43
    public function compileSelect(Builder $query)
44
    {
45
        $sql = parent::compileSelect($query);
46
47
        if ($query->unions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query->unions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
48
            $sql = '('.$sql.') '.$this->compileUnions($query);
49
        }
50
51
        return $sql;
52
    }
53
54
    /**
55
     * Compile the random statement into SQL.
56
     *
57
     * @param  string  $seed
58
     * @return string
59
     */
60
    public function compileRandom($seed)
61
    {
62
        return 'RAND('.$seed.')';
63
    }
64
65
    /**
66
     * Compile the lock into SQL.
67
     *
68
     * @param  \Childish\query\Builder  $query
69
     * @param  bool|string  $value
70
     * @return string
71
     */
72
    protected function compileLock(Builder $query, $value)
73
    {
74
        if (! is_string($value)) {
75
            return $value ? 'for update' : 'lock in share mode';
76
        }
77
78
        return $value;
79
    }
80
81
    /**
82
     * Compile an update statement into SQL.
83
     *
84
     * @param  \Childish\query\Builder  $query
85
     * @param  array  $values
86
     * @return string
87
     */
88
    public function compileUpdate(Builder $query, $values)
89
    {
90
        $table = $this->wrapTable($query->from);
91
92
        // Each one of the columns in the update statements needs to be wrapped in the
93
        // keyword identifiers, also a place-holder needs to be created for each of
94
        // the values in the list of bindings so we can make the sets statements.
95
        $columns = $this->compileUpdateColumns($values);
96
97
        // If the query has any "join" clauses, we will setup the joins on the builder
98
        // and compile them so we can attach them to this update, as update queries
99
        // can get join statements to attach to other tables when they're needed.
100
        $joins = '';
101
102
        if (isset($query->joins)) {
103
            $joins = ' '.$this->compileJoins($query, $query->joins);
104
        }
105
106
        // Of course, update queries may also be constrained by where clauses so we'll
107
        // need to compile the where clauses and attach it to the query so only the
108
        // intended records are updated by the SQL statements we generate to run.
109
        $where = $this->compileWheres($query);
110
111
        $sql = rtrim("update {$table}{$joins} set $columns $where");
112
113
        // If the query has an order by clause we will compile it since MySQL supports
114
        // order bys on update statements. We'll compile them using the typical way
115
        // of compiling order bys. Then they will be appended to the SQL queries.
116
        if (! empty($query->orders)) {
117
            $sql .= ' '.$this->compileOrders($query, $query->orders);
118
        }
119
120
        // Updates on MySQL also supports "limits", which allow you to easily update a
121
        // single record very easily. This is not supported by all database engines
122
        // so we have customized this update compiler here in order to add it in.
123
        if (isset($query->limit)) {
124
            $sql .= ' '.$this->compileLimit($query, $query->limit);
125
        }
126
127
        return rtrim($sql);
128
    }
129
130
    /**
131
     * Compile all of the columns for an update statement.
132
     *
133
     * @param  array  $values
134
     * @return string
135
     */
136
    protected function compileUpdateColumns($values)
137
    {
138
        return  (new Collection($values))->map(function ($value, $key) {
139
            if ($this->isJsonSelector($key)) {
140
                return $this->compileJsonUpdateColumn($key, new JsonExpression($value));
141
            } else {
142
                return $this->wrap($key).' = ? ';
143
            }
144
        })->implode(', ');
145
    }
146
147
    /**
148
     * Prepares a JSON column being updated using the JSON_SET function.
149
     *
150
     * @param  string  $key
151
     * @param  \Illuminate\Database\Query\JsonExpression  $value
152
     * @return string
153
     */
154
    protected function compileJsonUpdateColumn($key, JsonExpression $value)
155
    {
156
        $path = explode('->', $key);
157
158
        $field = $this->wrapValue(array_shift($path));
159
160
        $accessor = '"$.'.implode('.', $path).'"';
161
162
        return "{$field} = json_set({$field}, {$accessor}, {$value->getValue()})";
163
    }
164
165
    /**
166
     * Prepare the bindings for an update statement.
167
     *
168
     * Booleans, integers, and doubles are inserted into JSON updates as raw values.
169
     *
170
     * @param  array  $bindings
171
     * @param  array  $values
172
     * @return array
173
     */
174
    public function prepareBindingsForUpdate(array $bindings, array $values)
175
    {
176
        $values = (new Collection($values))->reject(function ($value, $column) {
177
            return $this->isJsonSelector($column) &&
178
                   in_array(gettype($value), ['boolean', 'integer', 'double']);
179
        })->all();
180
181
        return parent::prepareBindingsForUpdate($bindings, $values);
182
    }
183
184
    /**
185
     * Compile a delete statement into SQL.
186
     *
187
     * @param  \Childish\query\Builder  $query
188
     * @return string
189
     */
190
    public function compileDelete(Builder $query)
191
    {
192
        $table = $this->wrapTable($query->from);
193
194
        $where = is_array($query->wheres) ? $this->compileWheres($query) : '';
195
196
        return isset($query->joins)
197
            ? $this->compileDeleteWithJoins($query, $table, $where)
0 ignored issues
show
Documentation introduced by
$where is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
198
            : $this->compileDeleteWithoutJoins($query, $table, $where);
0 ignored issues
show
Documentation introduced by
$where is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
199
    }
200
201
    /**
202
     * Compile a delete query that does not use joins.
203
     *
204
     * @param  \Childish\query\Builder  $query
205
     * @param  string  $table
206
     * @param  array  $where
207
     * @return string
208
     */
209
    protected function compileDeleteWithoutJoins($query, $table, $where)
210
    {
211
        $sql = trim("delete from {$table} {$where}");
212
213
        // When using MySQL, delete statements may contain order by statements and limits
214
        // so we will compile both of those here. Once we have finished compiling this
215
        // we will return the completed SQL statement so it will be executed for us.
216
        if (! empty($query->orders)) {
217
            $sql .= ' '.$this->compileOrders($query, $query->orders);
218
        }
219
220
        if (isset($query->limit)) {
221
            $sql .= ' '.$this->compileLimit($query, $query->limit);
222
        }
223
224
        return $sql;
225
    }
226
227
    /**
228
     * Compile a delete query that uses joins.
229
     *
230
     * @param  \Childish\query\Builder  $query
231
     * @param  string  $table
232
     * @param  array  $where
233
     * @return string
234
     */
235
    protected function compileDeleteWithJoins($query, $table, $where)
236
    {
237
        $joins = ' '.$this->compileJoins($query, $query->joins);
238
239
        $alias = strpos(strtolower($table), ' as ') !== false
240
            ? explode(' as ', $table)[1] : $table;
241
242
        return trim("delete {$alias} from {$table}{$joins} {$where}");
243
    }
244
245
    /**
246
     * Wrap a single string in keyword identifiers.
247
     *
248
     * @param  string  $value
249
     * @return string
250
     */
251
    protected function wrapValue($value)
252
    {
253
        if ($value === '*') {
254
            return $value;
255
        }
256
257
        // If the given value is a JSON selector we will wrap it differently than a
258
        // traditional value. We will need to split this path and wrap each part
259
        // wrapped, etc. Otherwise, we will simply wrap the value as a string.
260
        if ($this->isJsonSelector($value)) {
261
            return $this->wrapJsonSelector($value);
262
        }
263
264
        return '`'.str_replace('`', '``', $value).'`';
265
    }
266
267
    /**
268
     * Wrap the given JSON selector.
269
     *
270
     * @param  string  $value
271
     * @return string
272
     */
273
    protected function wrapJsonSelector($value)
274
    {
275
        $path = explode('->', $value);
276
277
        $field = $this->wrapValue(array_shift($path));
278
279
        return sprintf('%s->\'$.%s\'', $field, collect($path)->map(function ($part) {
280
            return '"'.$part.'"';
281
        })->implode('.'));
282
    }
283
284
    /**
285
     * Determine if the given string is a JSON selector.
286
     *
287
     * @param  string  $value
288
     * @return bool
289
     */
290
    protected function isJsonSelector($value)
291
    {
292
        return Tools::contains($value, '->');
293
    }
294
}
295