Seeker::checkKey()   C
last analyzed

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 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