Passed
Push — master ( 382b98...cf0bb6 )
by Gilles
08:40 queued 01:24
created

Seeker::checkKey()   C

Complexity

Conditions 12
Paths 8

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 18.3287

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 19
c 1
b 0
f 0
nc 8
nop 2
dl 0
loc 33
ccs 11
cts 17
cp 0.6471
crap 18.3287
rs 6.9666

How to fix   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
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 336
    public function seek(array $nodes, RuleDTO $rule, array $options): array
25
    {
26
        // XPath index
27 336
        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 333
        $options = $this->flattenOptions($options);
46
47 333
        $return = [];
48 333
        foreach ($nodes as $node) {
49
            // check if we are a leaf
50 330
            if ($node instanceof LeafNode || !$node->hasChildren()
51
            ) {
52 12
                continue;
53
            }
54
55 330
            $children = [];
56 330
            $child = $node->firstChild();
57 330
            while (!\is_null($child)) {
58
                // wild card, grab all
59 330
                if ($rule->getTag() == '*' && \is_null($rule->getKey())) {
60 15
                    $return[] = $child;
61 15
                    $child = $this->getNextChild($node, $child);
62 15
                    continue;
63
                }
64
65 330
                $pass = $this->checkTag($rule, $child);
66 330
                if ($pass && $rule->getKey() !== null) {
67 120
                    $pass = $this->checkKey($rule, $child);
68
                }
69 330
                if ($pass &&
70 330
                    $rule->getKey() !== null &&
71 330
                    $rule->getValue() !== null &&
72 330
                    $rule->getValue() != '*'
73
                ) {
74 111
                    $pass = $this->checkComparison($rule, $child);
75
                }
76
77 330
                if ($pass) {
78
                    // it passed all checks
79 264
                    $return[] = $child;
80
                }
81
                // this child failed to be matched
82 330
                if ($child instanceof InnerNode && $child->hasChildren()
83
                ) {
84 282
                    if (!isset($options['checkGrandChildren'])
85 282
                        || $options['checkGrandChildren']
86
                    ) {
87
                        // we have a child that failed but are not leaves.
88 282
                        $matches = $this->seek([$child], $rule, $options);
89 282
                        foreach ($matches as $match) {
90 144
                            $return[] = $match;
91
                        }
92
                    }
93
                }
94
95 330
                $child = $this->getNextChild($node, $child);
96
            }
97
98 330
            if ((!isset($options['checkGrandChildren'])
99 330
                    || $options['checkGrandChildren'])
100 330
                && \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 333
        return $return;
111
    }
112
113
    /**
114
     * Checks comparison condition from rules against node.
115
     */
116 111
    private function checkComparison(RuleDTO $rule, AbstractNode $node): bool
117
    {
118 111
        if ($rule->getKey() == 'plaintext') {
119
            // plaintext search
120
            $nodeValue = $node->text();
121
            $result = $this->checkNodeValue($nodeValue, $rule, $node);
122
        } else {
123
            // normal search
124 111
            if (!\is_array($rule->getKey())) {
125 108
                $nodeValue = $node->getAttribute($rule->getKey());
126 108
                $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 111
        return $result;
138
    }
139
140
    /**
141
     * Flattens the option array.
142
     *
143
     * @return array
144
     */
145 333
    private function flattenOptions(array $optionsArray)
146
    {
147 333
        $options = [];
148 333
        foreach ($optionsArray as $optionArray) {
149 3
            foreach ($optionArray as $key => $option) {
150 3
                $options[$key] = $option;
151
            }
152
        }
153
154 333
        return $options;
155
    }
156
157
    /**
158
     * Returns the next child or null if no more children.
159
     *
160
     * @return AbstractNode|null
161
     */
162 330
    private function getNextChild(
163
        AbstractNode $node,
164
        AbstractNode $currentChild
165
    ) {
166
        try {
167 330
            $child = null;
168 330
            if ($node instanceof InnerNode) {
169
                // get next child
170 330
                $child = $node->nextChild($currentChild->id());
171
            }
172 330
        } catch (ChildNotFoundException $e) {
173
            // no more children
174 330
            unset($e);
175 330
            $child = null;
176
        }
177
178 330
        return $child;
179
    }
180
181
    /**
182
     * Checks tag condition from rules against node.
183
     */
184 330
    private function checkTag(RuleDTO $rule, AbstractNode $node): bool
185
    {
186 330
        if (!empty($rule->getTag()) && $rule->getTag() != $node->getTag()->name()
187 330
            && $rule->getTag() != '*'
188
        ) {
189 315
            return false;
190
        }
191
192 264
        return true;
193
    }
194
195
    /**
196
     * Checks key condition from rules against node.
197
     */
198 120
    private function checkKey(RuleDTO $rule, AbstractNode $node): bool
199
    {
200 120
        if (!\is_array($rule->getKey())) {
201 117
            if ($rule->isNoKey()) {
202
                if ($node->getAttribute($rule->getKey()) !== null) {
203
                    return false;
204
                }
205
            } else {
206 117
                if ($rule->getKey() != 'plaintext'
207 117
                    && !$node->hasAttribute($rule->getKey())
208
                ) {
209 117
                    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 120
        return true;
231
    }
232
233 111
    private function checkNodeValue(
234
        ?string $nodeValue,
235
        RuleDTO $rule,
236
        AbstractNode $node,
237
        ?int $index = null
238
    ): bool {
239 111
        $check = false;
240
        if (
241 111
            $rule->getValue() !== null &&
242 111
            \is_string($rule->getValue()) &&
243 111
            $nodeValue !== null
244
        ) {
245 66
            $check = $this->match($rule->getOperator(), $rule->getValue(), $nodeValue);
246
        }
247
248
        // handle multiple classes
249 111
        $key = $rule->getKey();
250
        if (
251 111
            !$check &&
252 111
            $key == 'class' &&
253 111
            \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 69
            !$check &&
274 69
            \is_array($key) &&
275 69
            !\is_null($nodeValue) &&
276 69
            \is_string($rule->getOperator()) &&
277 69
            \is_string($rule->getValue()[$index])
278
        ) {
279 3
            $check = $this->match($rule->getOperator(), $rule->getValue()[$index], $nodeValue);
280
        }
281
282 111
        return $check;
283
    }
284
285
    /**
286
     * Attempts to match the given arguments with the given operator.
287
     */
288 111
    private function match(
289
        string $operator,
290
        string $pattern,
291
        string $value
292
    ): bool {
293 111
        $value = \strtolower($value);
294 111
        $pattern = \strtolower($pattern);
295 111
        switch ($operator) {
296 111
            case '=':
297 99
                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