Passed
Push — master ( 40f09e...9b3c0e )
by Christopher
02:06
created

FilterBuilder   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 95
dl 0
loc 226
rs 9.0399
c 0
b 0
f 0
wmc 42

6 Methods

Rating   Name   Duplication   Size   Complexity  
A buildSimpleCondition() 0 12 3
B buildAndCondition() 0 16 7
A build() 0 19 5
A buildHashCondition() 0 16 6
B buildLikeCondition() 0 35 11
B buildInCondition() 0 30 10

How to fix   Complexity   

Complex Class

Complex classes like FilterBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FilterBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link      https://github.com/chrmorandi/yii2-ldap for the source repository
4
 * @package   yii2-ldap
5
 * @author    Christopher Mota <[email protected]>
6
 * @license   MIT License - view the LICENSE file that was distributed with this source code.
7
 * @since     1.0.0
8
 */
9
10
namespace chrmorandi\ldap;
11
12
use yii\base\InvalidParamException;
13
use yii\base\BaseObject;
0 ignored issues
show
Bug introduced by
The type yii\base\BaseObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
15
/**
16
 * FilterBuilder builds a Filter for search in LDAP.
17
 *
18
 * FilterBuilder is also used by [[Query]] to build Filters.
19
 *
20
 * @author Christopher Mota <[email protected]>
21
 * @since 1.0.0
22
 */
23
class FilterBuilder extends BaseObject
24
{
25
    /**
26
     * @var string the separator between different fragments of a SQL statement.
27
     * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
28
     */
29
    public $separator = ' ';
30
31
    /**
32
     * @var array map of query condition to builder methods.
33
     * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
34
     */
35
    protected $conditionBuilders = [
36
        'NOT'         => 'buildAndCondition',
37
        'AND'         => 'buildAndCondition',
38
        'OR'          => 'buildAndCondition',
39
        'IN'          => 'buildInCondition',
40
        'NOT IN'      => 'buildInCondition',
41
        'LIKE'        => 'buildLikeCondition',
42
        'NOT LIKE'    => 'buildLikeCondition',
43
        'OR LIKE'     => 'buildLikeCondition',
44
        'OR NOT LIKE' => 'buildLikeCondition',
45
    ];
46
47
    /**
48
     * @var array map of operator for builder methods.
49
     */
50
    protected $operator = [
51
        'NOT' => '!',
52
        'AND' => '&',
53
        'OR'  => '|',
54
    ];
55
56
    /**
57
     * Parses the condition specification and generates the corresponding filters.
58
     * @param string|array $condition the condition specification. Please refer to [[Query::where()]]
59
     * on how to specify a condition.
60
     * @return string the generated
61
     */
62
    public function build($condition)
63
    {
64
        if (!is_array($condition)) {
65
            return (string) $condition;
66
        } elseif (empty($condition)) {
67
            return '';
68
        }
69
70
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
71
            $operator = strtoupper($condition[0]);
72
            if (isset($this->conditionBuilders[$operator])) {
73
                $method = $this->conditionBuilders[$operator];
74
            } else {
75
                $method = 'buildSimpleCondition';
76
            }
77
            array_shift($condition);
78
            return $this->$method($operator, $condition);
79
        } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
80
            return $this->buildHashCondition($condition);
81
        }
82
    }
83
84
    /**
85
     * Creates a condition based on column-value pairs.
86
     * @param array $condition the condition specification.
87
     * @return string the generated
88
     */
89
    public function buildHashCondition($condition)
90
    {
91
        $parts = [];
92
        foreach ($condition as $attribute => $value) {
93
            if (is_array($value)) {
94
                // IN condition
95
                $parts[] = $this->buildInCondition('IN', [$attribute, $value]);
96
            } elseif ($value === null) {
97
                $parts[] = "(!($attribute=*))";
98
            } elseif ($attribute === 'dn') {
99
                $parts[] = '(' . LdapHelper::getRdnFromDn($value) . ')';
100
            } else {
101
                $parts[] = "($attribute=$value)";
102
            }
103
        }
104
        return count($parts) === 1 ? $parts[0] : '(&' . implode('', $parts) . ')';
105
    }
106
107
    /**
108
     * Connects two or more filters expressions with the `AND`(&) or `OR`(|) operator.
109
     * @param string $operator The operator to use for connecting the given operands
110
     * @param array $operands The filter expressions to connect.
111
     * @return string The generated filter
112
     */
113
    public function buildAndCondition($operator, $operands)
114
    {
115
        if ($operator === 'NOT' && count($operands) !== 1) {
116
            throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
117
        }
118
119
        $parts = [];
120
        foreach ($operands as $operand) {
121
            if (is_array($operand)) {
122
                $parts[] = $this->build($operand);
123
            } elseif ($operand !== '') {
124
                $parts[] = $operand;
125
            }
126
        }
127
128
        return empty($parts) ? '' : '(' . $this->operator[$operator] . implode('', $parts) . ')';
129
    }
130
131
    /**
132
     * Creates an filter expressions with the `IN` operator.
133
     * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
134
     * @param array $operands the first operand is the column name. If it is an array
135
     * a composite IN condition will be generated.
136
     * The second operand is an array of values that column value should be among.
137
     * If it is an empty array the generated expression will be a `false` value if
138
     * operator is `IN` and empty if operator is `NOT IN`.
139
     * @return string the generated SQL expression
140
     * @throws Exception if wrong number of operands have been given.
141
     */
142
    public function buildInCondition($operator, $operands)
143
    {
144
        if (!isset($operands[0], $operands[1])) {
145
            throw new InvalidParamException("Operator '$operator' requires two operands.");
146
        }
147
148
        list($attribute, $values) = $operands;
149
150
        if (!is_array($values)) {
151
            $values = [$values];
152
        }
153
154
        $parts = [];
155
        foreach ($values as $value) {
156
            if (is_array($value)) {
157
                $value = isset($value[$attribute]) ? $value[$attribute] : null;
158
            }
159
            if ($value === null) {
160
                $parts[] = "(!$attribute=*)";
161
            } else {
162
                $parts[] = "($attribute=$value)";
163
            }
164
        }
165
166
        if (empty($parts)) {
167
            return '';
168
        } elseif ($operator === 'NOT IN') {
169
            return '(!' . implode('', $parts) . ')';
170
        }
171
        return count($parts) === 1 ? $parts[0] : '(|' . implode('', $parts) . ')';
172
    }
173
174
    /**
175
     * Creates an SQL expressions with the `LIKE` operator.
176
     * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
177
     * @param array $operands an array of two or three operands
178
     *
179
     * - The first operand is the column name.
180
     * - The second operand is a single value or an array of values that column value
181
     *   should be compared with. If it is an empty array the generated expression will
182
     *   be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
183
     *   is `NOT LIKE` or `OR NOT LIKE`.
184
     * - An optional third operand can also be provided to specify how to escape special characters
185
     *   in the value(s). The operand should be an array of mappings from the special characters to their
186
     *   escaped counterparts. If this operand is not provided, a default escape mapping will be used.
187
     *   You may use `false` or an empty array to indicate the values are already escaped and no escape
188
     *   should be applied. Note that when using an escape mapping (or the third operand is not provided),
189
     *   the values will be automatically enclosed within a pair of percentage characters.
190
     * @return string the generated SQL expression
191
     * @throws InvalidParamException if wrong number of operands have been given.
192
     */
193
    public function buildLikeCondition($operator, $operands)
194
    {
195
        if (!isset($operands[0], $operands[1])) {
196
            throw new InvalidParamException("Operator '$operator' requires two operands.");
197
        }
198
199
        $escape = isset($operands[2]) ? $operands[2] : ['*' => '\*', '_' => '\_', '\\' => '\\\\'];
200
        unset($operands[2]);
201
202
        if (!preg_match('/^(OR|)(((NOT|))LIKE)/', $operator, $matches)) {
203
            throw new InvalidParamException("Invalid operator '$operator'.");
204
        }
205
        $andor    = (!empty($matches[1]) ? $matches[1] : 'AND');
206
        $not      = !empty($matches[3]);
0 ignored issues
show
Unused Code introduced by
The assignment to $not is dead and can be removed.
Loading history...
207
        $operator = $matches[2];
208
209
        list($attribute, $values) = $operands;
210
211
        if (!is_array($values)) {
212
            $values = [$values];
213
        }
214
215
        if (empty($values)) {
216
            return '';
217
        }
218
219
        $not = ($operator == 'NOT LIKE') ? '(' . $this->operator['NOT'] : false;
220
221
        $parts = [];
222
        foreach ($values as $value) {
223
            $value   = empty($escape) ? $value : strtr($value, $escape);
224
            $parts[] = $not . '(' . $attribute . '=*' . $value . '*)' . ($not ? ')' : '');
0 ignored issues
show
Bug introduced by
Are you sure $not of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
            $parts[] = /** @scrutinizer ignore-type */ $not . '(' . $attribute . '=*' . $value . '*)' . ($not ? ')' : '');
Loading history...
225
        }
226
227
        return '(' . $this->operator[trim($andor)] . implode($parts) . ')';
228
    }
229
230
    /**
231
     * Creates an LDAP filter expressions like `(attribute operator value)`.
232
     * @param string $operator the operator to use. A valid list could be used e.g. `=`, `>=`, `<=`, `~<`.
233
     * @param array $operands contains two column names.
234
     * @return string the generated LDAP filter expression
235
     * @throws InvalidParamException if wrong number of operands have been given.
236
     */
237
    public function buildSimpleCondition($operator, $operands)
238
    {
239
        if (count($operands) !== 2) {
240
            throw new InvalidParamException("Operator '$operator' requires two operands.");
241
        }
242
243
        list($attribute, $value) = $operands;
244
245
        if ($value === null) {
246
            return "(!($attribute = *))";
247
        } else {
248
            return "($attribute $operator $value)";
249
        }
250
    }
251
252
}
253