Seeker   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Test Coverage

Coverage 90.14%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 146
dl 0
loc 300
ccs 128
cts 142
cp 0.9014
rs 2
c 1
b 0
f 0
wmc 81

8 Methods

Rating   Name   Duplication   Size   Complexity  
A flattenOptions() 0 10 3
A getNextChild() 0 17 3
A checkTag() 0 9 4
A checkComparison() 0 22 5
D checkNodeValue() 0 50 18
B match() 0 26 7
C checkKey() 0 33 12
D seek() 0 87 29

How to fix   Complexity   

Complex Class

Complex classes like Seeker 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 Seeker, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PHPHtmlParser\Selector;
6
7
use PHPHtmlParser\Contracts\Selector\SeekerInterface;
8
use PHPHtmlParser\Dom\Node\AbstractNode;
9
use PHPHtmlParser\Dom\Node\InnerNode;
10
use PHPHtmlParser\Dom\Node\LeafNode;
11
use PHPHtmlParser\DTO\Selector\RuleDTO;
12
use PHPHtmlParser\Exceptions\ChildNotFoundException;
13
14
class Seeker implements SeekerInterface
15
{
16
    /**
17
     * Attempts to find all children that match the rule
18
     * given.
19
     *
20
     * @var InnerNode[]
21
     *
22
     * @throws ChildNotFoundException
23
     */
24 342
    public function seek(array $nodes, RuleDTO $rule, array $options): array
25
    {
26
        // XPath index
27 342
        if ($rule->getTag() !== null && \is_numeric($rule->getKey())) {
28 3
            $count = 0;
29 3
            foreach ($nodes as $node) {
30 3
                if ($rule->getTag() == '*'
31 3
                    || $rule->getTag() == $node->getTag()
32 3
                        ->name()
33
                ) {
34 3
                    ++$count;
35 3
                    if ($count == $rule->getKey()) {
36
                        // found the node we wanted
37 3
                        return [$node];
38
                    }
39
                }
40
            }
41
42
            return [];
43
        }
44
45 339
        $options = $this->flattenOptions($options);
46
47 339
        $return = [];
48 339
        foreach ($nodes as $node) {
49
            // check if we are a leaf
50 336
            if ($node instanceof LeafNode || !$node->hasChildren()
51
            ) {
52 12
                continue;
53
            }
54
55 336
            $children = [];
56 336
            $child = $node->firstChild();
57 336
            while (!\is_null($child)) {
58
                // wild card, grab all
59 336
                if ($rule->getTag() == '*' && \is_null($rule->getKey())) {
60 15
                    $return[] = $child;
61 15
                    $child = $this->getNextChild($node, $child);
62 15
                    continue;
63
                }
64
65 336
                $pass = $this->checkTag($rule, $child);
66 336
                if ($pass && $rule->getKey() !== null) {
67 123
                    $pass = $this->checkKey($rule, $child);
68
                }
69 336
                if ($pass &&
70 336
                    $rule->getKey() !== null &&
71 336
                    $rule->getValue() !== null &&
72 336
                    $rule->getValue() != '*'
73
                ) {
74 114
                    $pass = $this->checkComparison($rule, $child);
75
                }
76
77 336
                if ($pass) {
78
                    // it passed all checks
79 267
                    $return[] = $child;
80
                }
81
                // this child failed to be matched
82 336
                if ($child instanceof InnerNode && $child->hasChildren()
83
                ) {
84 285
                    if (!isset($options['checkGrandChildren'])
85 285
                        || $options['checkGrandChildren']
86
                    ) {
87
                        // we have a child that failed but are not leaves.
88 285
                        $matches = $this->seek([$child], $rule, $options);
89 285
                        foreach ($matches as $match) {
90 147
                            $return[] = $match;
91
                        }
92
                    }
93
                }
94
95 336
                $child = $this->getNextChild($node, $child);
96
            }
97
98 336
            if ((!isset($options['checkGrandChildren'])
99 336
                    || $options['checkGrandChildren'])
100 336
                && \count($children) > 0
101
            ) {
102
                // we have children that failed but are not leaves.
103
                $matches = $this->seek($children, $rule, $options);
104
                foreach ($matches as $match) {
105
                    $return[] = $match;
106
                }
107
            }
108
        }
109
110 339
        return $return;
111
    }
112
113
    /**
114
     * Checks comparison condition from rules against node.
115
     */
116 114
    private function checkComparison(RuleDTO $rule, AbstractNode $node): bool
117
    {
118 114
        if ($rule->getKey() == 'plaintext') {
119
            // plaintext search
120
            $nodeValue = $node->text();
121
            $result = $this->checkNodeValue($nodeValue, $rule, $node);
122
        } else {
123
            // normal search
124 114
            if (!\is_array($rule->getKey())) {
125 111
                $nodeValue = $node->getAttribute($rule->getKey());
126 111
                $result = $this->checkNodeValue($nodeValue, $rule, $node);
127
            } else {
128 3
                $result = true;
129 3
                foreach ($rule->getKey() as $index => $key) {
130 3
                    $nodeValue = $node->getAttribute($key);
131 3
                    $result = $result &&
132 3
                        $this->checkNodeValue($nodeValue, $rule, $node, $index);
133
                }
134
            }
135
        }
136
137 114
        return $result;
138
    }
139
140
    /**
141
     * Flattens the option array.
142
     *
143
     * @return array
144
     */
145 339
    private function flattenOptions(array $optionsArray)
146
    {
147 339
        $options = [];
148 339
        foreach ($optionsArray as $optionArray) {
149 3
            foreach ($optionArray as $key => $option) {
150 3
                $options[$key] = $option;
151
            }
152
        }
153
154 339
        return $options;
155
    }
156
157
    /**
158
     * Returns the next child or null if no more children.
159
     *
160
     * @return AbstractNode|null
161
     */
162 336
    private function getNextChild(
163
        AbstractNode $node,
164
        AbstractNode $currentChild
165
    ) {
166
        try {
167 336
            $child = null;
168 336
            if ($node instanceof InnerNode) {
169
                // get next child
170 336
                $child = $node->nextChild($currentChild->id());
171
            }
172 336
        } catch (ChildNotFoundException $e) {
173
            // no more children
174 336
            unset($e);
175 336
            $child = null;
176
        }
177
178 336
        return $child;
179
    }
180
181
    /**
182
     * Checks tag condition from rules against node.
183
     */
184 336
    private function checkTag(RuleDTO $rule, AbstractNode $node): bool
185
    {
186 336
        if (!empty($rule->getTag()) && $rule->getTag() != $node->getTag()->name()
187 336
            && $rule->getTag() != '*'
188
        ) {
189 321
            return false;
190
        }
191
192 267
        return true;
193
    }
194
195
    /**
196
     * Checks key condition from rules against node.
197
     */
198 123
    private function checkKey(RuleDTO $rule, AbstractNode $node): bool
199
    {
200 123
        if (!\is_array($rule->getKey())) {
201 120
            if ($rule->isNoKey()) {
202
                if ($node->getAttribute($rule->getKey()) !== null) {
203
                    return false;
204
                }
205
            } else {
206 120
                if ($rule->getKey() != 'plaintext'
207 120
                    && !$node->hasAttribute($rule->getKey())
208
                ) {
209 120
                    return false;
210
                }
211
            }
212
        } else {
213 3
            if ($rule->isNoKey()) {
214
                foreach ($rule->getKey() as $key) {
215
                    if (!\is_null($node->getAttribute($key))) {
216
                        return false;
217
                    }
218
                }
219
            } else {
220 3
                foreach ($rule->getKey() as $key) {
221 3
                    if ($key != 'plaintext'
222 3
                        && !$node->hasAttribute($key)
223
                    ) {
224
                        return false;
225
                    }
226
                }
227
            }
228
        }
229
230 123
        return true;
231
    }
232
233 114
    private function checkNodeValue(
234
        ?string $nodeValue,
235
        RuleDTO $rule,
236
        AbstractNode $node,
237
        ?int $index = null
238
    ): bool {
239 114
        $check = false;
240
        if (
241 114
            $rule->getValue() !== null &&
242 114
            \is_string($rule->getValue()) &&
243 114
            $nodeValue !== null
244
        ) {
245 69
            $check = $this->match($rule->getOperator(), $rule->getValue(), $nodeValue);
246
        }
247
248
        // handle multiple classes
249 114
        $key = $rule->getKey();
250
        if (
251 114
            !$check &&
252 114
            $key == 'class' &&
253 114
            \is_array($rule->getValue())
254
        ) {
255 54
            $nodeClasses = \explode(' ', $node->getAttribute('class') ?? '');
256 54
            foreach ($rule->getValue() as $value) {
257 54
                foreach ($nodeClasses as $class) {
258
                    if (
259 54
                        !empty($class) &&
260 54
                        \is_string($rule->getOperator())
261
                    ) {
262 54
                        $check = $this->match($rule->getOperator(), $value, $class);
263
                    }
264 54
                    if ($check) {
265 54
                        break;
266
                    }
267
                }
268 54
                if (!$check) {
269 45
                    break;
270
                }
271
            }
272
        } elseif (
273 72
            !$check &&
274 72
            \is_array($key) &&
275 72
            !\is_null($nodeValue) &&
276 72
            \is_string($rule->getOperator()) &&
277 72
            \is_string($rule->getValue()[$index])
278
        ) {
279 3
            $check = $this->match($rule->getOperator(), $rule->getValue()[$index], $nodeValue);
280
        }
281
282 114
        return $check;
283
    }
284
285
    /**
286
     * Attempts to match the given arguments with the given operator.
287
     */
288 114
    private function match(
289
        string $operator,
290
        string $pattern,
291
        string $value
292
    ): bool {
293 114
        $value = \strtolower($value);
294 114
        $pattern = \strtolower($pattern);
295 114
        switch ($operator) {
296 114
            case '=':
297 102
                return $value === $pattern;
298 12
            case '!=':
299 3
                return $value !== $pattern;
300 9
            case '^=':
301 3
                return \preg_match('/^' . \preg_quote($pattern, '/') . '/',
302 3
                        $value) == 1;
303 6
            case '$=':
304 3
                return \preg_match('/' . \preg_quote($pattern, '/') . '$/',
305 3
                        $value) == 1;
306 3
            case '*=':
307 3
                if ($pattern[0] == '/') {
308 3
                    return \preg_match($pattern, $value) == 1;
309
                }
310
311
                return \preg_match('/' . $pattern . '/i', $value) == 1;
312
            default:
313
                return false;
314
        }
315
    }
316
}
317