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

brick / geo / 13655507172

04 Mar 2025 02:07PM UTC coverage: 84.44% (+0.04%) from 84.402%
13655507172

push

github

BenMorel
Restrict CompoundCurve to contain LineString|CircularString

It looks like nested CompoundCurves are, in fact, forbidden.

3 of 5 new or added lines in 3 files covered. (60.0%)

88 existing lines in 4 files now uncovered.

1552 of 1838 relevant lines covered (84.44%)

1985.26 hits per line

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

95.54
/src/Engine/DatabaseEngine.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Geo\Engine;
6

7
use Brick\Geo\CircularString;
8
use Brick\Geo\CompoundCurve;
9
use Brick\Geo\Curve;
10
use Brick\Geo\CurvePolygon;
11
use Brick\Geo\Exception\GeometryEngineException;
12
use Brick\Geo\Geometry;
13
use Brick\Geo\GeometryCollection;
14
use Brick\Geo\LineString;
15
use Brick\Geo\MultiCurve;
16
use Brick\Geo\MultiLineString;
17
use Brick\Geo\MultiPoint;
18
use Brick\Geo\MultiSurface;
19
use Brick\Geo\MultiPolygon;
20
use Brick\Geo\Point;
21
use Brick\Geo\Polygon;
22
use Brick\Geo\PolyhedralSurface;
23
use Brick\Geo\Proxy\ProxyFactory;
24
use Brick\Geo\Surface;
25
use Brick\Geo\TIN;
26
use Brick\Geo\Triangle;
27
use Override;
28

29
/**
30
 * Database implementation of the GeometryEngine.
31
 *
32
 * The target database must have support for GIS functions.
33
 */
34
abstract class DatabaseEngine implements GeometryEngine
35
{
36
    private readonly bool $useProxy;
37

38
    public function __construct(bool $useProxy)
39
    {
40
        $this->useProxy = $useProxy;
×
41
    }
42

43
    /**
44
     * Executes a SQL query.
45
     *
46
     * @psalm-param list<GeometryParameter|string|float|int> $parameters
47
     * @psalm-return list<mixed>
48
     *
49
     * @param string $query      The SQL query to execute.
50
     * @param array  $parameters The geometry data or scalar values to pass as parameters.
51
     *
52
     * @return list<mixed> A numeric result array.
53
     *
54
     * @throws GeometryEngineException
55
     */
56
    abstract protected function executeQuery(string $query, array $parameters) : array;
57

58
    /**
59
     * Returns the syntax required to perform an ST_GeomFromText(), together with placeholders.
60
     *
61
     * This method may be overridden if necessary.
62
     */
63
    protected function getGeomFromTextSyntax(): string
64
    {
UNCOV
65
        return 'ST_GeomFromText(?, ?)';
120✔
66
    }
67

68
    /**
69
     * Returns the syntax required to perform an ST_GeomFromWKB(), together with placeholders.
70
     *
71
     * This method may be overridden if necessary.
72
     */
73
    protected function getGeomFromWKBSyntax(): string
74
    {
75
        return 'ST_GeomFromWKB(?, ?)';
409✔
76
    }
77

78
    /**
79
     * Returns the placeholder syntax for the given parameter.
80
     *
81
     * This method may be overridden to perform explicit type casts if necessary.
82
     */
83
    protected function getParameterPlaceholder(string|float|int $parameter): string
84
    {
85
        return '?';
102✔
86
    }
87

88
    /**
89
     * Builds and executes a SQL query for a GIS function.
90
     *
91
     * @psalm-param array<Geometry|string|float|int> $parameters
92
     * @psalm-return list<mixed>
93
     *
94
     * @param string $function        The SQL GIS function to execute.
95
     * @param array  $parameters      The Geometry objects or scalar values to pass as parameters.
96
     * @param bool   $returnsGeometry Whether the GIS function returns a Geometry.
97
     *
98
     * @return array A numeric result array.
99
     *
100
     * @throws GeometryEngineException
101
     */
102
    private function query(string $function, array $parameters, bool $returnsGeometry) : array
103
    {
104
        $queryParameters = [];
1,263✔
105
        $queryValues = [];
1,263✔
106

107
        foreach ($parameters as $parameter) {
1,263✔
108
            if ($parameter instanceof Geometry) {
1,263✔
109
                $sendAsBinary = ! $parameter->isEmpty();
1,263✔
110

111
                $queryParameters[] = $sendAsBinary
1,263✔
112
                    ? $this->getGeomFromWKBSyntax()
1,153✔
UNCOV
113
                    : $this->getGeomFromTextSyntax();
120✔
114

115
                $queryValues[] = new GeometryParameter($parameter, $sendAsBinary);
1,263✔
116
            } else {
117
                $queryParameters[] = $this->getParameterPlaceholder($parameter);
106✔
118
                $queryValues[] = $parameter;
106✔
119
            }
120
        }
121

122
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
1,263✔
123

124
        if ($returnsGeometry) {
1,263✔
125
            $query = sprintf('
313✔
126
                SELECT
127
                    CASE WHEN ST_IsEmpty(g) THEN ST_AsText(g) ELSE NULL END,
128
                    CASE WHEN ST_IsEmpty(g) THEN NULL ELSE ST_AsBinary(g) END,
129
                    ST_GeometryType(g),
130
                    ST_SRID(g)
131
                FROM (%s AS g) AS q
132
            ', $query);
313✔
133
        }
134

135
        return $this->executeQuery($query, $queryValues);
1,263✔
136
    }
137

138
    /**
139
     * Queries a GIS function returning a boolean value.
140
     *
141
     * @param string                       $function   The SQL GIS function to execute.
142
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
143
     *
144
     * @throws GeometryEngineException
145
     */
146
    private function queryBoolean(string $function, Geometry|string|float|int ...$parameters) : bool
147
    {
148
        /** @var array{scalar|null} $result */
149
        $result = $this->query($function, $parameters, false);
848✔
150

151
        $value = $result[0];
732✔
152

153
        // SQLite3 returns -1 when calling a boolean GIS function on a NULL result,
154
        // MariaDB returns -1 when an unsupported operation is performed on a Z/M geometry.
155
        if ($value === null || $value === -1 || $value === '-1') {
732✔
156
            throw GeometryEngineException::operationYieldedNoResult();
96✔
157
        }
158

159
        return (bool) $value;
636✔
160
    }
161

162
    /**
163
     * Queries a GIS function returning a floating point value.
164
     *
165
     * @param string                       $function   The SQL GIS function to execute.
166
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
167
     *
168
     * @throws GeometryEngineException
169
     */
170
    private function queryFloat(string $function, Geometry|string|float|int ...$parameters) : float
171
    {
172
        /** @var array{scalar|null} $result */
173
        $result = $this->query($function, $parameters, false);
221✔
174

175
        $value = $result[0];
191✔
176

177
        if ($value === null) {
191✔
178
            throw GeometryEngineException::operationYieldedNoResult();
27✔
179
        }
180

181
        return (float) $value;
164✔
182
    }
183

184
    /**
185
     * Queries a GIS function returning a Geometry object.
186
     *
187
     * @param string                       $function   The SQL GIS function to execute.
188
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
189
     *
190
     * @throws GeometryEngineException
191
     */
192
    private function queryGeometry(string $function, Geometry|string|float|int ...$parameters) : Geometry
193
    {
194
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
195
        $result = $this->query($function, $parameters, true);
313✔
196

197
        [$wkt, $wkb, $geometryType, $srid] = $result;
247✔
198

199
        $srid = (int) $srid;
247✔
200

201
        if ($wkt !== null) {
247✔
UNCOV
202
            if ($this->useProxy) {
5✔
UNCOV
203
                $geometryClass = $this->getGeometryClass($geometryType);
5✔
204

UNCOV
205
                return ProxyFactory::createWktProxy($geometryClass, $wkt, $srid);
5✔
206
            }
207

208
            return Geometry::fromText($wkt, $srid);
×
209
        }
210

211
        if ($wkb !== null) {
242✔
212
            if (is_resource($wkb)) {
228✔
UNCOV
213
                $wkb = stream_get_contents($wkb);
59✔
214

UNCOV
215
                if ($wkb === false) {
59✔
216
                    throw new GeometryEngineException('Cannot read stream contents.');
×
217
                }
218
            }
219

220
            if ($this->useProxy) {
228✔
221
                $geometryClass = $this->getGeometryClass($geometryType);
228✔
222

223
                return ProxyFactory::createWkbProxy($geometryClass, $wkb, $srid);
228✔
224
            }
225

226
            return Geometry::fromBinary($wkb, $srid);
×
227
        }
228

229
        throw GeometryEngineException::operationYieldedNoResult();
14✔
230
    }
231

232
    /**
233
     * @return class-string<Geometry>
234
     *
235
     * @throws GeometryEngineException
236
     */
237
    private function getGeometryClass(string $geometryType) : string
238
    {
239
        $geometryClasses = [
233✔
240
            'CIRCULARSTRING'     => CircularString::class,
233✔
241
            'COMPOUNDCURVE'      => CompoundCurve::class,
233✔
242
            'CURVE'              => Curve::class,
233✔
243
            'CURVEPOLYGON'       => CurvePolygon::class,
233✔
244
            'GEOMCOLLECTION'     => GeometryCollection::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
233✔
245
            'GEOMETRY'           => Geometry::class,
233✔
246
            'GEOMETRYCOLLECTION' => GeometryCollection::class,
233✔
247
            'LINESTRING'         => LineString::class,
233✔
248
            'MULTICURVE'         => MultiCurve::class,
233✔
249
            'MULTILINESTRING'    => MultiLineString::class,
233✔
250
            'MULTIPOINT'         => MultiPoint::class,
233✔
251
            'MULTIPOLYGON'       => MultiPolygon::class,
233✔
252
            'MULTISURFACE'       => MultiSurface::class,
233✔
253
            'POINT'              => Point::class,
233✔
254
            'POLYGON'            => Polygon::class,
233✔
255
            'POLYHEDRALSURFACE'  => PolyhedralSurface::class,
233✔
256
            'SURFACE'            => Surface::class,
233✔
257
            'TIN'                => TIN::class,
233✔
258
            'TRIANGLE'           => Triangle::class
233✔
259
        ];
233✔
260

261
        $geometryType = strtoupper($geometryType);
233✔
262
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
233✔
263
        assert($geometryType !== null);
264
        $geometryType = preg_replace('/ .*/', '', $geometryType);
233✔
265
        assert($geometryType !== null);
266

267
        if (! isset($geometryClasses[$geometryType])) {
233✔
268
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
269
        }
270

271
        return $geometryClasses[$geometryType];
233✔
272
    }
273

274
    #[Override]
275
    public function contains(Geometry $a, Geometry $b) : bool
276
    {
277
        return $this->queryBoolean('ST_Contains', $a, $b);
76✔
278
    }
279

280
    #[Override]
281
    public function intersects(Geometry $a, Geometry $b) : bool
282
    {
283
        return $this->queryBoolean('ST_Intersects', $a, $b);
36✔
284
    }
285

286
    #[Override]
287
    public function union(Geometry $a, Geometry $b) : Geometry
288
    {
289
        return $this->queryGeometry('ST_Union', $a, $b);
22✔
290
    }
291

292
    #[Override]
293
    public function intersection(Geometry $a, Geometry $b) : Geometry
294
    {
295
        return $this->queryGeometry('ST_Intersection', $a, $b);
12✔
296
    }
297

298
    #[Override]
299
    public function difference(Geometry $a, Geometry $b) : Geometry
300
    {
301
        return $this->queryGeometry('ST_Difference', $a, $b);
12✔
302
    }
303

304
    #[Override]
305
    public function envelope(Geometry $g) : Geometry
306
    {
307
        return $this->queryGeometry('ST_Envelope', $g);
18✔
308
    }
309

310
    #[Override]
311
    public function centroid(Geometry $g) : Point
312
    {
313
        /** @var Point */
314
        return $this->queryGeometry('ST_Centroid', $g);
45✔
315
    }
316

317
    #[Override]
318
    public function pointOnSurface(Surface|MultiSurface $g) : Point
319
    {
320
        /** @var Point */
321
        return $this->queryGeometry('ST_PointOnSurface', $g);
42✔
322
    }
323

324
    #[Override]
325
    public function length(Curve|MultiCurve $g) : float
326
    {
327
        return $this->queryFloat('ST_Length', $g);
95✔
328
    }
329

330
    #[Override]
331
    public function area(Surface|MultiSurface $g) : float
332
    {
333
        return $this->queryFloat('ST_Area', $g);
66✔
334
    }
335

336
    #[Override]
337
    public function azimuth(Point $observer, Point $subject) : float
338
    {
339
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
12✔
340
    }
341

342
    #[Override]
343
    public function boundary(Geometry $g) : Geometry
344
    {
345
        return $this->queryGeometry('ST_Boundary', $g);
36✔
346
    }
347

348
    #[Override]
349
    public function isValid(Geometry $g) : bool
350
    {
351
        return $this->queryBoolean('ST_IsValid', $g);
46✔
352
    }
353

354
    #[Override]
355
    public function isClosed(Geometry $g) : bool
356
    {
357
        return $this->queryBoolean('ST_IsClosed', $g);
276✔
358
    }
359

360
    #[Override]
361
    public function isSimple(Geometry $g) : bool
362
    {
363
        return $this->queryBoolean('ST_IsSimple', $g);
102✔
364
    }
365

366
    #[Override]
367
    public function isRing(Curve $curve) : bool
368
    {
369
        try {
370
            return $this->queryBoolean('ST_IsRing', $curve);
58✔
371
        } catch (GeometryEngineException) {
10✔
372
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
373
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
374
        }
375
    }
376

377
    #[Override]
378
    public function makeValid(Geometry $g) : Geometry
379
    {
380
        return $this->queryGeometry('ST_MakeValid', $g);
12✔
381
    }
382

383
    #[Override]
384
    public function equals(Geometry $a, Geometry $b) : bool
385
    {
386
        return $this->queryBoolean('ST_Equals', $a, $b);
122✔
387
    }
388

389
    #[Override]
390
    public function disjoint(Geometry $a, Geometry $b) : bool
391
    {
392
        return $this->queryBoolean('ST_Disjoint', $a, $b);
36✔
393
    }
394

395
    #[Override]
396
    public function touches(Geometry $a, Geometry $b) : bool
397
    {
398
        return $this->queryBoolean('ST_Touches', $a, $b);
48✔
399
    }
400

401
    #[Override]
402
    public function crosses(Geometry $a, Geometry $b) : bool
403
    {
404
        return $this->queryBoolean('ST_Crosses', $a, $b);
48✔
405
    }
406

407
    #[Override]
408
    public function within(Geometry $a, Geometry $b) : bool
409
    {
410
        return $this->queryBoolean('ST_Within', $a, $b);
30✔
411
    }
412

413
    #[Override]
414
    public function overlaps(Geometry $a, Geometry $b) : bool
415
    {
416
        return $this->queryBoolean('ST_Overlaps', $a, $b);
12✔
417
    }
418

419
    #[Override]
420
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
421
    {
422
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
24✔
423
    }
424

425
    #[Override]
426
    public function locateAlong(Geometry $g, float $mValue) : Geometry
427
    {
428
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
12✔
429
    }
430

431
    #[Override]
432
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
433
    {
434
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
12✔
435
    }
436

437
    #[Override]
438
    public function distance(Geometry $a, Geometry $b) : float
439
    {
440
        return $this->queryFloat('ST_Distance', $a, $b);
30✔
441
    }
442

443
    #[Override]
444
    public function buffer(Geometry $g, float $distance) : Geometry
445
    {
446
        return $this->queryGeometry('ST_Buffer', $g, $distance);
18✔
447
    }
448

449
    #[Override]
450
    public function convexHull(Geometry $g) : Geometry
451
    {
452
        return $this->queryGeometry('ST_ConvexHull', $g);
18✔
453
    }
454

455
    #[Override]
456
    public function symDifference(Geometry $a, Geometry $b) : Geometry
457
    {
458
        return $this->queryGeometry('ST_SymDifference', $a, $b);
6✔
459
    }
460

461
    #[Override]
462
    public function snapToGrid(Geometry $g, float $size) : Geometry
463
    {
464
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
24✔
465
    }
466

467
    #[Override]
468
    public function simplify(Geometry $g, float $tolerance) : Geometry
469
    {
470
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
12✔
471
    }
472

473
    #[Override]
474
    public function maxDistance(Geometry $a, Geometry $b) : float
475
    {
476
        return $this->queryFloat('ST_MaxDistance', $a, $b);
18✔
477
    }
478

479
    #[Override]
480
    public function transform(Geometry $g, int $srid) : Geometry
481
    {
UNCOV
482
        return $this->queryGeometry('ST_Transform', $g, $srid);
4✔
483
    }
484

485
    #[Override]
486
    public function split(Geometry $g, Geometry $blade) : Geometry
487
    {
488
        return $this->queryGeometry('ST_Split', $g, $blade);
8✔
489
    }
490
}
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