Completed
Pull Request — 2.0 (#150)
by Christopher
02:42
created

WhereScope::_inspectComparisonExpression()   C

Complexity

Conditions 11
Paths 22

Size

Total Lines 61
Code Lines 43

Duplication

Lines 21
Ratio 34.43 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 43
c 1
b 0
f 0
nc 22
nop 3
dl 21
loc 61
rs 6.2318

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
135
        // some variables
136
        $pk = $this->_tablePrimaryKey();
137
        $driverClass = $this->_driverClass($query);
138
139 View Code Duplication
        switch ($driverClass) {
140
            case 'sqlite':
141
                $concat = implode(' || ', $pk);
142
                $field = "({$concat} || '')";
143
                break;
144
            case 'mysql':
145
            case 'postgres':
146
            case 'sqlserver':
147
            default:
148
                $concat = implode(', ', $pk);
149
                $field = "CONCAT({$concat}, '')";
150
                break;
151
        }
152
153
        // compile query, faster than raw subquery in most cases
154
        $ids = $subQuery->all()->extract('entity_id')->toArray();
155
        $ids = empty($ids) ? ['-1'] : $ids;
156
        $expression->setField($field);
157
        $expression->setValue($ids);
158
        $expression->setOperator('IN');
159
160
        $class = new \ReflectionClass($expression);
161
        $property = $class->getProperty('_type');
162
        $property->setAccessible(true);
163
        $property->setValue($expression, 'string[]');
164
165
        return $expression;
166
    }
167
168
    /**
169
     * Analyzes the given unary expression and alters it according.
170
     *
171
     * @param \Cake\Database\Expression\UnaryExpression $expression Unary expression
172
     * @param string $bundle Consider attributes only for a specific bundle
173
     * @param \Cake\ORM\Query $query The query instance this expression comes from
174
     * @return \Cake\Database\Expression\UnaryExpression Scoped expression (or not)
175
     */
176
    protected function _inspectUnaryExpression(UnaryExpression $expression, $bundle, Query $query)
177
    {
178
        $class = new \ReflectionClass($expression);
179
        $property = $class->getProperty('_value');
180
        $property->setAccessible(true);
181
        $value = $property->getValue($expression);
182
183
        if ($value instanceof IdentifierExpression) {
184
            $field = $value->getIdentifier();
185
            $column = is_string($field) ? $this->_toolbox->columnName($field) : '';
186
187 View Code Duplication
            if (empty($column) ||
188
                in_array($column, (array)$this->_table->schema()->columns()) || // ignore real columns
189
                !in_array($column, $this->_toolbox->getAttributeNames($bundle)) ||
190
                !$this->_toolbox->isSearchable($column) // ignore no searchable virtual columns
191
            ) {
192
                // nothing to alter
193
                return $expression;
194
            }
195
196
            $pk = $this->_tablePrimaryKey();
197
            $driverClass = $this->_driverClass($query);
198
199 View Code Duplication
            switch ($driverClass) {
200
                case 'sqlite':
201
                    $concat = implode(' || ', $pk);
202
                    $field = "({$concat} || '')";
203
                    break;
204
                case 'mysql':
205
                case 'postgres':
206
                case 'sqlserver':
207
                default:
208
                    $concat = implode(', ', $pk);
209
                    $field = "CONCAT({$concat}, '')";
210
                    break;
211
            }
212
213
            $attr = $this->_toolbox->attributes($bundle)[$column];
214
            $type = $this->_toolbox->getType($column);
215
            $subQuery = TableRegistry::get('Eav.EavValues')
216
                ->find()
217
                ->select("EavValues.value_{$type}")
218
                ->where([
219
                    'EavValues.entity_id' => $field,
220
                    'EavValues.eav_attribute_id' => $attr['id']
221
                ])
222
                ->sql();
223
            $subQuery = str_replace([':c0', ':c1'], [$field, $attr['id']], $subQuery);
224
            $property->setValue($expression, "({$subQuery})");
225
        }
226
227
        return $expression;
228
    }
229
230
    /**
231
     * Gets table's PK as an array.
232
     *
233
     * @return array
234
     */
235
    protected function _tablePrimaryKey()
236
    {
237
        $alias = $this->_table->alias();
238
        $pk = $this->_table->primaryKey();
239
240
        if (!is_array($pk)) {
241
            $pk = [$pk];
242
        }
243
244
        $pk = array_map(function ($key) use ($alias) {
245
            return "{$alias}.{$key}";
246
        }, $pk);
247
248
        return $pk;
249
    }
250
251
    /**
252
     * Gets the name of the class driver used by the given $query to access the DB.
253
     *
254
     * @param \Cake\ORM\Query $query The query to inspect
255
     * @return string Lowercased drive name. e.g. `mysql`
256
     */
257
    protected function _driverClass(Query $query)
258
    {
259
        $conn = $query->connection(null);
260
        list(, $driverClass) = namespaceSplit(strtolower(get_class($conn->driver())));
261
        return $driverClass;
262
    }
263
}
264