• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / geo / 17456208570

04 Sep 2025 07:10AM UTC coverage: 50.432%. Remained the same
17456208570

push

github

BenMorel
Use @extends and @implements instead of @template-* variants

For consistency with the rest of the project.

1867 of 3702 relevant lines covered (50.43%)

1140.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.27
/src/Io/Internal/WktParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Geo\Io\Internal;
6

7
use Brick\Geo\Exception\GeometryIoException;
8

9
use function array_unshift;
10
use function implode;
11
use function preg_match_all;
12

13
use const PREG_SET_ORDER;
14

15
/**
16
 * Well-Known Text parser, with support for EWKT.
17
 *
18
 * @internal
19
 */
20
final class WktParser
21
{
22
    private const REGEX_CAPTURE_WORD = '([a-z]+)';
23
    private const REGEX_CAPTURE_NUMBER = '(\-?[0-9]+(?:\.[0-9]+)?(?:e[\+\-]?[0-9]+)?)';
24
    private const REGEX_WHITESPACE = '\s+';
25
    private const REGEX_CAPTURE_OTHER = '(.+?)';
26
    private const REGEX_CAPTURE_SRID = 'SRID\=([0-9]+)\s*;'; // EWKT
27

28
    /**
29
     * The list of tokens.
30
     *
31
     * The first element of each token is the token type, the second element is the token value.
32
     *
33
     * @var list<array{WktTokenType, string}>
34
     */
35
    private array $tokens = [];
36

37
    /**
38
     * The current token pointer.
39
     */
40
    private int $current = 0;
41

42
    public function __construct(
43
        string $wkt,
44
        private bool $ewkt = false,
45
    ) {
46
        $this->scan($wkt);
17,639✔
47
    }
48

49
    /**
50
     * @throws GeometryIoException
51
     */
52
    public function matchOpener(): void
53
    {
54
        $token = $this->nextToken();
11,604✔
55

56
        if ($token === null) {
11,604✔
57
            throw new GeometryIoException("Expected '(' but encountered end of stream");
×
58
        }
59
        if ($token[1] !== '(') {
11,604✔
60
            throw new GeometryIoException("Expected '(' but encountered '" . $token[1] . "'");
×
61
        }
62
    }
63

64
    /**
65
     * @throws GeometryIoException
66
     */
67
    public function matchCloser(): void
68
    {
69
        $token = $this->nextToken();
2,142✔
70

71
        if ($token === null) {
2,142✔
72
            throw new GeometryIoException("Expected ')' but encountered end of stream");
×
73
        }
74
        if ($token[1] !== ')') {
2,142✔
75
            throw new GeometryIoException("Expected ')' but encountered '" . $token[1] . "'");
×
76
        }
77
    }
78

79
    /**
80
     * @throws GeometryIoException
81
     */
82
    public function getNextWord(): string
83
    {
84
        $token = $this->nextToken();
17,639✔
85

86
        if ($token === null) {
17,639✔
87
            throw new GeometryIoException('Expected word but encountered end of stream');
×
88
        }
89
        if ($token[0] !== WktTokenType::Word) {
17,639✔
90
            throw new GeometryIoException("Expected word but encountered '" . $token[1] . "'");
×
91
        }
92

93
        return $token[1];
17,639✔
94
    }
95

96
    /**
97
     * @return string|null The next word, or NULL if the next token is not a word, or there are no more tokens.
98
     */
99
    public function getOptionalNextWord(): ?string
100
    {
101
        $token = $this->tokens[$this->current] ?? null;
17,639✔
102

103
        if ($token === null) {
17,639✔
104
            return null;
×
105
        }
106

107
        if ($token[0] !== WktTokenType::Word) {
17,639✔
108
            return null;
11,604✔
109
        }
110

111
        $this->current++;
12,775✔
112

113
        return $token[1];
12,775✔
114
    }
115

116
    public function matchOptionalOpener(): bool
117
    {
118
        $token = $this->peekToken();
10,333✔
119

120
        $isOpener = ($token !== null && $token[1] === '(');
10,333✔
121

122
        if ($isOpener) {
10,333✔
123
            $this->current++;
192✔
124
        }
125

126
        return $isOpener;
10,333✔
127
    }
128

129
    /**
130
     * Returns whether the next token is an opener or a word.
131
     *
132
     * @return bool True if the next token is an opener, false if it is a word.
133
     *
134
     * @throws GeometryIoException If the next token is not an opener or a word, or if there is no next token.
135
     */
136
    public function isNextOpenerOrWord(): bool
137
    {
138
        $token = $this->tokens[$this->current] ?? null;
1,608✔
139

140
        if ($token === null) {
1,608✔
141
            throw new GeometryIoException("Expected '(' or word but encountered end of stream");
×
142
        }
143

144
        if ($token[1] === '(') {
1,608✔
145
            return true;
1,576✔
146
        }
147

148
        if ($token[0] === WktTokenType::Word) {
1,384✔
149
            return false;
1,384✔
150
        }
151

152
        throw new GeometryIoException("Expected '(' or word but encountered '" . $token[1] . "'");
×
153
    }
154

155
    /**
156
     * @throws GeometryIoException
157
     */
158
    public function getNextNumber(): float
159
    {
160
        $token = $this->nextToken();
11,516✔
161

162
        if ($token === null) {
11,516✔
163
            throw new GeometryIoException('Expected number but encountered end of stream');
×
164
        }
165

166
        if ($token[0] !== WktTokenType::Number) {
11,516✔
167
            throw new GeometryIoException("Expected number but encountered '" . $token[1] . "'");
×
168
        }
169

170
        return (float) $token[1];
11,516✔
171
    }
172

173
    /**
174
     * @throws GeometryIoException
175
     */
176
    public function getNextCloserOrComma(): string
177
    {
178
        $token = $this->nextToken();
10,461✔
179

180
        if ($token === null) {
10,461✔
181
            throw new GeometryIoException("Expected ')' or ',' but encountered end of stream");
×
182
        }
183
        if ($token[1] !== ')' && $token[1] !== ',') {
10,461✔
184
            throw new GeometryIoException("Expected ')' or ',' but encountered '" . $token[1] . "'");
×
185
        }
186

187
        return $token[1];
10,461✔
188
    }
189

190
    public function getOptionalSrid(): int
191
    {
192
        $token = $this->tokens[$this->current] ?? null;
5,632✔
193

194
        if ($token === null) {
5,632✔
195
            return 0;
×
196
        }
197

198
        if ($token[0] !== WktTokenType::Srid) {
5,632✔
199
            return 0;
2,816✔
200
        }
201

202
        $this->current++;
2,816✔
203

204
        return (int) $token[1];
2,816✔
205
    }
206

207
    public function isEndOfStream(): bool
208
    {
209
        return $this->nextToken() === null;
17,583✔
210
    }
211

212
    private function scan(string $wkt): void
213
    {
214
        $regexPatterns = [
17,639✔
215
            self::REGEX_CAPTURE_WORD,
17,639✔
216
            self::REGEX_CAPTURE_NUMBER,
17,639✔
217
            self::REGEX_WHITESPACE,
17,639✔
218
            self::REGEX_CAPTURE_OTHER,
17,639✔
219
        ];
17,639✔
220

221
        if ($this->ewkt) {
17,639✔
222
            array_unshift($regexPatterns, self::REGEX_CAPTURE_SRID);
5,632✔
223
        }
224

225
        $matchKeyToType = $this->ewkt ? [
17,639✔
226
            1 => WktTokenType::Srid,
17,639✔
227
            2 => WktTokenType::Word,
17,639✔
228
            3 => WktTokenType::Number,
17,639✔
229
            4 => WktTokenType::Other,
17,639✔
230
        ] : [
12,007✔
231
            1 => WktTokenType::Word,
12,007✔
232
            2 => WktTokenType::Number,
12,007✔
233
            3 => WktTokenType::Other,
12,007✔
234
        ];
12,007✔
235

236
        $regex = '/' . implode('|', $regexPatterns) . '/i';
17,639✔
237

238
        preg_match_all($regex, $wkt, $matches, PREG_SET_ORDER);
17,639✔
239

240
        foreach ($matches as $match) {
17,639✔
241
            foreach ($match as $key => $value) {
17,639✔
242
                /** @var int $key */
243

244
                if ($key === 0) {
17,639✔
245
                    continue;
17,639✔
246
                }
247

248
                if ($value !== '') {
17,639✔
249
                    $this->tokens[] = [$matchKeyToType[$key], $value];
17,639✔
250
                }
251
            }
252
        }
253
    }
254

255
    /**
256
     * @return array{WktTokenType, string}|null The next token, or null if there are no more tokens.
257
     */
258
    private function peekToken(): ?array
259
    {
260
        return $this->tokens[$this->current] ?? null;
17,639✔
261
    }
262

263
    /**
264
     * @return array{WktTokenType, string}|null The next token, or null if there are no more tokens.
265
     */
266
    private function nextToken(): ?array
267
    {
268
        $token = $this->peekToken();
17,639✔
269

270
        if ($token === null) {
17,639✔
271
            return null;
17,583✔
272
        }
273

274
        $this->current++;
17,639✔
275

276
        return $token;
17,639✔
277
    }
278
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc