Completed
Push — 2.0 ( 7b1d29...a1f3e1 )
by Christopher
02:43
created

WhereScope::_driverClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
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();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::alias() has been deprecated with message: 3.4.0 Use setAlias()/getAlias() instead.

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.

Loading history...
251
        $pk = $this->_table->primaryKey();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::primaryKey() has been deprecated with message: 3.4.0 Use setPrimaryKey()/getPrimaryKey() instead.

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.

Loading history...
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