1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Licensed under The GPL-3.0 License |
4
|
|
|
* For full copyright and license information, please see the LICENSE.txt |
5
|
|
|
* Redistributions of files must retain the above copyright notice. |
6
|
|
|
* |
7
|
|
|
* @since 2.0.0 |
8
|
|
|
* @author Christopher Castro <[email protected]> |
9
|
|
|
* @link http://www.quickappscms.org |
10
|
|
|
* @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License |
11
|
|
|
*/ |
12
|
|
|
namespace Eav\Model\Behavior\QueryScope; |
13
|
|
|
|
14
|
|
|
use Cake\Database\ExpressionInterface; |
15
|
|
|
use Cake\Database\Expression\Comparison; |
16
|
|
|
use Cake\Database\Expression\IdentifierExpression; |
17
|
|
|
use Cake\Database\Expression\UnaryExpression; |
18
|
|
|
use Cake\ORM\Query; |
19
|
|
|
use Cake\ORM\Table; |
20
|
|
|
use Cake\ORM\TableRegistry; |
21
|
|
|
use Eav\Model\Behavior\EavToolbox; |
22
|
|
|
use Eav\Model\Behavior\QueryScope\QueryScopeInterface; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Used by EAV Behavior to scope WHERE statements. |
26
|
|
|
*/ |
27
|
|
|
class WhereScope implements QueryScopeInterface |
28
|
|
|
{ |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* The table being managed. |
32
|
|
|
* |
33
|
|
|
* @var \Cake\ORM\Table |
34
|
|
|
*/ |
35
|
|
|
protected $_table = null; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Instance of toolbox. |
39
|
|
|
* |
40
|
|
|
* @var \Eav\Model\Behavior\EavToolbox |
41
|
|
|
*/ |
42
|
|
|
protected $_toolbox = null; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* {@inheritDoc} |
46
|
|
|
*/ |
47
|
|
|
public function __construct(Table $table) |
48
|
|
|
{ |
49
|
|
|
$this->_table = $table; |
50
|
|
|
$this->_toolbox = new EavToolbox($table); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* {@inheritDoc} |
55
|
|
|
* |
56
|
|
|
* Look for virtual columns in query's WHERE clause. |
57
|
|
|
* |
58
|
|
|
* @param \Cake\ORM\Query $query The query to scope |
59
|
|
|
* @param string|null $bundle Consider attributes only for a specific bundle |
60
|
|
|
* @return \Cake\ORM\Query The modified query object |
61
|
|
|
*/ |
62
|
|
|
public function scope(Query $query, $bundle = null) |
63
|
|
|
{ |
64
|
|
|
$whereClause = $query->clause('where'); |
65
|
|
|
if (!$whereClause) { |
66
|
|
|
return $query; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$whereClause->traverse(function (&$expression) use ($bundle, $query) { |
70
|
|
|
if ($expression instanceof ExpressionInterface) { |
71
|
|
|
$expression = $this->_inspectExpression($expression, $bundle, $query); |
72
|
|
|
} |
73
|
|
|
}); |
74
|
|
|
|
75
|
|
|
return $query; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Analyzes the given WHERE expression, looks for virtual columns and alters |
80
|
|
|
* the expressions according. |
81
|
|
|
* |
82
|
|
|
* @param \Cake\Database\ExpressionInterface $expression Expression to scope |
83
|
|
|
* @param string $bundle Consider attributes only for a specific bundle |
84
|
|
|
* @param \Cake\ORM\Query $query The query instance this expression comes from |
85
|
|
|
* @return \Cake\Database\ExpressionInterface The altered expression (or not) |
86
|
|
|
*/ |
87
|
|
|
protected function _inspectExpression(ExpressionInterface $expression, $bundle, Query $query) |
88
|
|
|
{ |
89
|
|
|
if ($expression instanceof Comparison) { |
90
|
|
|
$expression = $this->_inspectComparisonExpression($expression, $bundle, $query); |
91
|
|
|
} elseif ($expression instanceof UnaryExpression) { |
92
|
|
|
$expression = $this->_inspectUnaryExpression($expression, $bundle, $query); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
return $expression; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Analyzes the given comparison expression and alters it according. |
100
|
|
|
* |
101
|
|
|
* @param \Cake\Database\Expression\Comparison $expression Comparison expression |
102
|
|
|
* @param string $bundle Consider attributes only for a specific bundle |
103
|
|
|
* @param \Cake\ORM\Query $query The query instance this expression comes from |
104
|
|
|
* @return \Cake\Database\Expression\Comparison Scoped expression (or not) |
105
|
|
|
*/ |
106
|
|
|
protected function _inspectComparisonExpression(Comparison $expression, $bundle, Query $query) |
107
|
|
|
{ |
108
|
|
|
$field = $expression->getField(); |
109
|
|
|
$column = is_string($field) ? $this->_toolbox->columnName($field) : ''; |
110
|
|
|
|
111
|
|
View Code Duplication |
if (empty($column) || |
112
|
|
|
in_array($column, (array)$this->_table->schema()->columns()) || // ignore real columns |
113
|
|
|
!in_array($column, $this->_toolbox->getAttributeNames()) || |
114
|
|
|
!$this->_toolbox->isSearchable($column) // ignore no searchable virtual columns |
115
|
|
|
) { |
116
|
|
|
// nothing to alter |
117
|
|
|
return $expression; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$attr = $this->_toolbox->attributes($bundle)[$column]; |
121
|
|
|
$value = $expression->getValue(); |
122
|
|
|
$type = $this->_toolbox->getType($column); |
123
|
|
|
$conjunction = $expression->getOperator(); |
124
|
|
|
$conditions = [ |
125
|
|
|
'EavValues.eav_attribute_id' => $attr['id'], |
126
|
|
|
"EavValues.value_{$type} {$conjunction}" => $value, |
127
|
|
|
]; |
128
|
|
|
|
129
|
|
|
// subquery scope |
130
|
|
|
$subQuery = TableRegistry::get('Eav.EavValues') |
131
|
|
|
->find() |
132
|
|
|
->select('EavValues.entity_id') |
133
|
|
|
->where($conditions) |
134
|
|
|
->order(['EavValues.id' => 'DESC']); |
135
|
|
|
|
136
|
|
|
// some variables |
137
|
|
|
$pk = $this->_tablePrimaryKey(); |
138
|
|
|
$driverClass = $this->_toolbox->driver($query); |
139
|
|
|
|
140
|
|
|
switch ($driverClass) { |
141
|
|
|
case 'sqlite': |
142
|
|
|
$concat = implode(' || ', $pk); |
143
|
|
|
$field = "({$concat} || '')"; |
144
|
|
|
break; |
145
|
|
|
case 'sqlserver': |
146
|
|
|
$pk = array_map(function ($keyPart) { |
147
|
|
|
return "CAST({$keyPart} AS VARCHAR)"; |
148
|
|
|
}, $pk); |
149
|
|
|
$concat = implode(' + ', $pk); |
150
|
|
|
$field = "({$concat} + '')"; |
151
|
|
|
break; |
152
|
|
|
case 'mysql': |
153
|
|
|
case 'postgres': |
154
|
|
|
default: |
155
|
|
|
$concat = implode(', ', $pk); |
156
|
|
|
$field = "CONCAT({$concat}, '')"; |
157
|
|
|
break; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
// compile query, faster than raw subquery in most cases |
161
|
|
|
$ids = $subQuery->all()->extract('entity_id')->toArray(); |
162
|
|
|
$ids = empty($ids) ? ['-1'] : $ids; |
163
|
|
|
$expression->setField($field); |
164
|
|
|
$expression->setValue($ids); |
165
|
|
|
$expression->setOperator('IN'); |
166
|
|
|
|
167
|
|
|
$class = new \ReflectionClass($expression); |
168
|
|
|
$property = $class->getProperty('_type'); |
169
|
|
|
$property->setAccessible(true); |
170
|
|
|
$property->setValue($expression, 'string'); |
171
|
|
|
|
172
|
|
|
$property = $class->getProperty('_isMultiple'); |
173
|
|
|
$property->setAccessible(true); |
174
|
|
|
$property->setValue($expression, true); |
175
|
|
|
|
176
|
|
|
return $expression; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Analyzes the given unary expression and alters it according. |
181
|
|
|
* |
182
|
|
|
* @param \Cake\Database\Expression\UnaryExpression $expression Unary expression |
183
|
|
|
* @param string $bundle Consider attributes only for a specific bundle |
184
|
|
|
* @param \Cake\ORM\Query $query The query instance this expression comes from |
185
|
|
|
* @return \Cake\Database\Expression\UnaryExpression Scoped expression (or not) |
186
|
|
|
*/ |
187
|
|
|
protected function _inspectUnaryExpression(UnaryExpression $expression, $bundle, Query $query) |
188
|
|
|
{ |
189
|
|
|
$class = new \ReflectionClass($expression); |
190
|
|
|
$property = $class->getProperty('_value'); |
191
|
|
|
$property->setAccessible(true); |
192
|
|
|
$value = $property->getValue($expression); |
193
|
|
|
|
194
|
|
|
if ($value instanceof IdentifierExpression) { |
195
|
|
|
$field = $value->getIdentifier(); |
196
|
|
|
$column = is_string($field) ? $this->_toolbox->columnName($field) : ''; |
197
|
|
|
|
198
|
|
View Code Duplication |
if (empty($column) || |
199
|
|
|
in_array($column, (array)$this->_table->schema()->columns()) || // ignore real columns |
200
|
|
|
!in_array($column, $this->_toolbox->getAttributeNames($bundle)) || |
201
|
|
|
!$this->_toolbox->isSearchable($column) // ignore no searchable virtual columns |
202
|
|
|
) { |
203
|
|
|
// nothing to alter |
204
|
|
|
return $expression; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$pk = $this->_tablePrimaryKey(); |
208
|
|
|
$driverClass = $this->_toolbox->driver($query); |
209
|
|
|
|
210
|
|
|
switch ($driverClass) { |
211
|
|
|
case 'sqlite': |
212
|
|
|
$concat = implode(' || ', $pk); |
213
|
|
|
$field = "({$concat} || '')"; |
214
|
|
|
break; |
215
|
|
|
case 'mysql': |
216
|
|
|
case 'postgres': |
217
|
|
|
case 'sqlserver': |
218
|
|
|
default: |
219
|
|
|
$concat = implode(', ', $pk); |
220
|
|
|
$field = "CONCAT({$concat}, '')"; |
221
|
|
|
break; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
$attr = $this->_toolbox->attributes($bundle)[$column]; |
225
|
|
|
$type = $this->_toolbox->getType($column); |
226
|
|
|
$subQuery = TableRegistry::get('Eav.EavValues') |
227
|
|
|
->find() |
228
|
|
|
->select("EavValues.value_{$type}") |
229
|
|
|
->where([ |
230
|
|
|
'EavValues.entity_id' => $field, |
231
|
|
|
'EavValues.eav_attribute_id' => $attr['id'] |
232
|
|
|
]) |
233
|
|
|
->order(['EavValues.id' => 'DESC']) |
234
|
|
|
->limit(1) |
235
|
|
|
->sql(); |
236
|
|
|
$subQuery = str_replace([':c0', ':c1'], [$field, $attr['id']], $subQuery); |
237
|
|
|
$property->setValue($expression, "({$subQuery})"); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
return $expression; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Gets table's PK as an array. |
245
|
|
|
* |
246
|
|
|
* @return array |
247
|
|
|
*/ |
248
|
|
|
protected function _tablePrimaryKey() |
249
|
|
|
{ |
250
|
|
|
$alias = $this->_table->alias(); |
|
|
|
|
251
|
|
|
$pk = $this->_table->primaryKey(); |
|
|
|
|
252
|
|
|
|
253
|
|
|
if (!is_array($pk)) { |
254
|
|
|
$pk = [$pk]; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$pk = array_map(function ($key) use ($alias) { |
258
|
|
|
return "{$alias}.{$key}"; |
259
|
|
|
}, $pk); |
260
|
|
|
|
261
|
|
|
return $pk; |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.