• 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

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

3
declare(strict_types=1);
4

5
namespace Brick\Geo\Engine;
6

7
use Brick\Geo\Curve;
8
use Brick\Geo\Engine\Internal\TypeChecker;
9
use Brick\Geo\Exception\GeometryEngineException;
10
use Brick\Geo\Geometry;
11
use Brick\Geo\LineString;
12
use Brick\Geo\MultiCurve;
13
use Brick\Geo\MultiPoint;
14
use Brick\Geo\MultiSurface;
15
use Brick\Geo\Point;
16
use Brick\Geo\Proxy;
17
use Brick\Geo\Surface;
18
use Override;
19

20
use function assert;
21
use function implode;
22
use function is_resource;
23
use function preg_replace;
24
use function sprintf;
25
use function stream_get_contents;
26
use function strtoupper;
27

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

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

42
    #[Override]
43
    public function contains(Geometry $a, Geometry $b): bool
44
    {
45
        return $this->queryBoolean('ST_Contains', $a, $b);
76✔
46
    }
47

48
    #[Override]
49
    public function intersects(Geometry $a, Geometry $b): bool
50
    {
51
        return $this->queryBoolean('ST_Intersects', $a, $b);
36✔
52
    }
53

54
    #[Override]
55
    public function union(Geometry $a, Geometry $b): Geometry
56
    {
57
        return $this->queryGeometry('ST_Union', $a, $b);
22✔
58
    }
59

60
    #[Override]
61
    public function intersection(Geometry $a, Geometry $b): Geometry
62
    {
63
        return $this->queryGeometry('ST_Intersection', $a, $b);
12✔
64
    }
65

66
    #[Override]
67
    public function difference(Geometry $a, Geometry $b): Geometry
68
    {
69
        return $this->queryGeometry('ST_Difference', $a, $b);
12✔
70
    }
71

72
    #[Override]
73
    public function envelope(Geometry $g): Geometry
74
    {
75
        return $this->queryGeometry('ST_Envelope', $g);
18✔
76
    }
77

78
    #[Override]
79
    public function centroid(Geometry $g): Point
80
    {
81
        $centroid = $this->queryGeometry('ST_Centroid', $g);
45✔
82
        TypeChecker::check($centroid, Point::class);
45✔
83

84
        return $centroid;
45✔
85
    }
86

87
    #[Override]
88
    public function pointOnSurface(Surface|MultiSurface $g): Point
89
    {
90
        $pointOnSurface = $this->queryGeometry('ST_PointOnSurface', $g);
42✔
91
        TypeChecker::check($pointOnSurface, Point::class);
28✔
92

93
        return $pointOnSurface;
28✔
94
    }
95

96
    #[Override]
97
    public function length(Curve|MultiCurve $g): float
98
    {
99
        return $this->queryFloat('ST_Length', $g);
95✔
100
    }
101

102
    #[Override]
103
    public function area(Surface|MultiSurface $g): float
104
    {
105
        return $this->queryFloat('ST_Area', $g);
66✔
106
    }
107

108
    #[Override]
109
    public function azimuth(Point $observer, Point $subject): float
110
    {
111
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
36✔
112
    }
113

114
    #[Override]
115
    public function boundary(Geometry $g): Geometry
116
    {
117
        return $this->queryGeometry('ST_Boundary', $g);
36✔
118
    }
119

120
    #[Override]
121
    public function isValid(Geometry $g): bool
122
    {
123
        return $this->queryBoolean('ST_IsValid', $g);
46✔
124
    }
125

126
    #[Override]
127
    public function isClosed(Geometry $g): bool
128
    {
129
        return $this->queryBoolean('ST_IsClosed', $g);
276✔
130
    }
131

132
    #[Override]
133
    public function isSimple(Geometry $g): bool
134
    {
135
        return $this->queryBoolean('ST_IsSimple', $g);
102✔
136
    }
137

138
    #[Override]
139
    public function isRing(Curve $curve): bool
140
    {
141
        try {
142
            return $this->queryBoolean('ST_IsRing', $curve);
58✔
143
        } catch (GeometryEngineException) {
10✔
144
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
145
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
146
        }
147
    }
148

149
    #[Override]
150
    public function makeValid(Geometry $g): Geometry
151
    {
152
        return $this->queryGeometry('ST_MakeValid', $g);
36✔
153
    }
154

155
    #[Override]
156
    public function equals(Geometry $a, Geometry $b): bool
157
    {
158
        return $this->queryBoolean('ST_Equals', $a, $b);
192✔
159
    }
160

161
    #[Override]
162
    public function disjoint(Geometry $a, Geometry $b): bool
163
    {
164
        return $this->queryBoolean('ST_Disjoint', $a, $b);
36✔
165
    }
166

167
    #[Override]
168
    public function touches(Geometry $a, Geometry $b): bool
169
    {
170
        return $this->queryBoolean('ST_Touches', $a, $b);
48✔
171
    }
172

173
    #[Override]
174
    public function crosses(Geometry $a, Geometry $b): bool
175
    {
176
        return $this->queryBoolean('ST_Crosses', $a, $b);
48✔
177
    }
178

179
    #[Override]
180
    public function within(Geometry $a, Geometry $b): bool
181
    {
182
        return $this->queryBoolean('ST_Within', $a, $b);
30✔
183
    }
184

185
    #[Override]
186
    public function overlaps(Geometry $a, Geometry $b): bool
187
    {
188
        return $this->queryBoolean('ST_Overlaps', $a, $b);
12✔
189
    }
190

191
    #[Override]
192
    public function relate(Geometry $a, Geometry $b, string $matrix): bool
193
    {
194
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
24✔
195
    }
196

197
    #[Override]
198
    public function locateAlong(Geometry $g, float $mValue): Geometry
199
    {
200
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
12✔
201
    }
202

203
    #[Override]
204
    public function locateBetween(Geometry $g, float $mStart, float $mEnd): Geometry
205
    {
206
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
12✔
207
    }
208

209
    #[Override]
210
    public function distance(Geometry $a, Geometry $b): float
211
    {
212
        return $this->queryFloat('ST_Distance', $a, $b);
30✔
213
    }
214

215
    #[Override]
216
    public function buffer(Geometry $g, float $distance): Geometry
217
    {
218
        return $this->queryGeometry('ST_Buffer', $g, $distance);
18✔
219
    }
220

221
    #[Override]
222
    public function convexHull(Geometry $g): Geometry
223
    {
224
        return $this->queryGeometry('ST_ConvexHull', $g);
18✔
225
    }
226

227
    #[Override]
228
    public function concaveHull(Geometry $g, float $convexity, bool $allowHoles): Geometry
229
    {
230
        return $this->queryGeometry('ST_ConcaveHull', $g, $convexity, $allowHoles);
30✔
231
    }
232

233
    #[Override]
234
    public function symDifference(Geometry $a, Geometry $b): Geometry
235
    {
236
        return $this->queryGeometry('ST_SymDifference', $a, $b);
6✔
237
    }
238

239
    #[Override]
240
    public function snapToGrid(Geometry $g, float $size): Geometry
241
    {
242
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
24✔
243
    }
244

245
    #[Override]
246
    public function simplify(Geometry $g, float $tolerance): Geometry
247
    {
248
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
12✔
249
    }
250

251
    #[Override]
252
    public function maxDistance(Geometry $a, Geometry $b): float
253
    {
254
        return $this->queryFloat('ST_MaxDistance', $a, $b);
18✔
255
    }
256

257
    #[Override]
258
    public function transform(Geometry $g, int $srid): Geometry
259
    {
260
        return $this->queryGeometry('ST_Transform', $g, $srid);
42✔
261
    }
262

263
    #[Override]
264
    public function split(Geometry $g, Geometry $blade): Geometry
265
    {
266
        return $this->queryGeometry('ST_Split', $g, $blade);
24✔
267
    }
268

269
    #[Override]
270
    public function lineInterpolatePoint(LineString $lineString, float $fraction): Point
271
    {
272
        $result = $this->queryGeometry('ST_LineInterpolatePoint', $lineString, $fraction);
105✔
273
        TypeChecker::check($result, Point::class);
63✔
274

275
        return $result;
63✔
276
    }
277

278
    #[Override]
279
    public function lineInterpolatePoints(LineString $lineString, float $fraction): MultiPoint
280
    {
281
        $result = $this->queryGeometry('ST_LineInterpolatePoints', $lineString, $fraction);
179✔
282

283
        if ($result instanceof MultiPoint) {
88✔
284
            return $result;
70✔
285
        }
286

287
        TypeChecker::check($result, Point::class);
18✔
288

289
        // POINT EMPTY
290
        if ($result->isEmpty()) {
18✔
291
            return new MultiPoint($result->coordinateSystem());
1✔
292
        }
293

294
        // POINT
295
        return MultiPoint::of($result);
17✔
296
    }
297

298
    /**
299
     * Executes a SQL query.
300
     *
301
     * @param string                         $query      The SQL query to execute.
302
     * @param list<GeometryParameter|scalar> $parameters The geometry data or scalar values to pass as parameters.
303
     *
304
     * @return list<mixed> A numeric result array.
305
     *
306
     * @throws GeometryEngineException
307
     */
308
    abstract protected function executeQuery(string $query, array $parameters): array;
309

310
    /**
311
     * Returns the syntax required to perform a ST_GeomFromText(), together with placeholders.
312
     *
313
     * This method may be overridden if necessary.
314
     */
315
    protected function getGeomFromTextSyntax(): string
316
    {
317
        return 'ST_GeomFromText(?, ?)';
125✔
318
    }
319

320
    /**
321
     * Returns the syntax required to perform a ST_GeomFromWKB(), together with placeholders.
322
     *
323
     * This method may be overridden if necessary.
324
     */
325
    protected function getGeomFromWkbSyntax(): string
326
    {
327
        return 'ST_GeomFromWKB(?, ?)';
551✔
328
    }
329

330
    /**
331
     * Returns the placeholder syntax for the given parameter.
332
     *
333
     * This method may be overridden to perform explicit type casts if necessary.
334
     */
335
    protected function getParameterPlaceholder(string|float|int|bool $parameter): string
336
    {
337
        return '?';
472✔
338
    }
339

340
    /**
341
     * Queries a GIS function returning a Geometry object.
342
     *
343
     * @param string          $function      The SQL GIS function to execute.
344
     * @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
345
     *
346
     * @throws GeometryEngineException
347
     */
348
    final protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters): Geometry
349
    {
350
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
351
        $result = $this->query($function, $parameters, true);
726✔
352

353
        [$wkt, $wkb, $geometryType, $srid] = $result;
453✔
354

355
        $srid = (int) $srid;
453✔
356

357
        if ($wkt !== null) {
453✔
358
            if ($this->useProxy) {
6✔
359
                $proxyClassName = $this->getProxyClassName($geometryType);
6✔
360

361
                return new $proxyClassName($wkt, false, $srid);
6✔
362
            }
363

364
            return Geometry::fromText($wkt, $srid);
×
365
        }
366

367
        if ($wkb !== null) {
447✔
368
            if (is_resource($wkb)) {
428✔
369
                $wkb = stream_get_contents($wkb);
117✔
370
            }
371

372
            if ($this->useProxy) {
428✔
373
                $proxyClassName = $this->getProxyClassName($geometryType);
428✔
374

375
                return new $proxyClassName($wkb, true, $srid);
428✔
376
            }
377

378
            return Geometry::fromBinary($wkb, $srid);
×
379
        }
380

381
        throw GeometryEngineException::operationYieldedNoResult();
19✔
382
    }
383

384
    /**
385
     * Builds and executes a SQL query for a GIS function.
386
     *
387
     * @param string                 $function        The SQL GIS function to execute.
388
     * @param array<Geometry|scalar> $parameters      The Geometry objects or scalar values to pass as parameters.
389
     * @param bool                   $returnsGeometry Whether the GIS function returns a Geometry.
390
     *
391
     * @return list<mixed> A numeric result array.
392
     *
393
     * @throws GeometryEngineException
394
     */
395
    private function query(string $function, array $parameters, bool $returnsGeometry): array
396
    {
397
        $queryParameters = [];
1,766✔
398
        $queryValues = [];
1,766✔
399

400
        foreach ($parameters as $parameter) {
1,766✔
401
            if ($parameter instanceof Geometry) {
1,766✔
402
                if ($parameter instanceof Proxy\ProxyInterface) {
1,766✔
403
                    $sendAsBinary = $parameter->isProxyBinary();
95✔
404
                } else {
405
                    $sendAsBinary = ! $parameter->isEmpty();
1,766✔
406
                }
407

408
                $queryParameters[] = $sendAsBinary
1,766✔
409
                    ? $this->getGeomFromWkbSyntax()
1,651✔
410
                    : $this->getGeomFromTextSyntax();
125✔
411

412
                $queryValues[] = new GeometryParameter($parameter, $sendAsBinary);
1,766✔
413
            } else {
414
                $queryParameters[] = $this->getParameterPlaceholder($parameter);
479✔
415
                $queryValues[] = $parameter;
479✔
416
            }
417
        }
418

419
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
1,766✔
420

421
        if ($returnsGeometry) {
1,766✔
422
            $query = sprintf('
726✔
423
                SELECT
424
                    CASE WHEN ST_IsEmpty(g) THEN ST_AsText(g) ELSE NULL END,
425
                    CASE WHEN ST_IsEmpty(g) THEN NULL ELSE ST_AsBinary(g) END,
426
                    ST_GeometryType(g),
427
                    ST_SRID(g)
428
                FROM (%s AS g) AS q
429
            ', $query);
726✔
430
        }
431

432
        return $this->executeQuery($query, $queryValues);
1,766✔
433
    }
434

435
    /**
436
     * Queries a GIS function returning a boolean value.
437
     *
438
     * @param string          $function      The SQL GIS function to execute.
439
     * @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
440
     *
441
     * @throws GeometryEngineException
442
     */
443
    private function queryBoolean(string $function, Geometry|string|float|int|bool ...$parameters): bool
444
    {
445
        /** @var array{scalar|null} $result */
446
        $result = $this->query($function, $parameters, false);
916✔
447

448
        $value = $result[0];
800✔
449

450
        // SQLite3 returns -1 when calling a boolean GIS function on a NULL result,
451
        // MariaDB returns -1 when an unsupported operation is performed on a Z/M geometry.
452
        if ($value === null || $value === -1 || $value === '-1') {
800✔
453
            throw GeometryEngineException::operationYieldedNoResult();
96✔
454
        }
455

456
        return (bool) $value;
704✔
457
    }
458

459
    /**
460
     * Queries a GIS function returning a floating point value.
461
     *
462
     * @param string          $function      The SQL GIS function to execute.
463
     * @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
464
     *
465
     * @throws GeometryEngineException
466
     */
467
    private function queryFloat(string $function, Geometry|string|float|int|bool ...$parameters): float
468
    {
469
        /** @var array{scalar|null} $result */
470
        $result = $this->query($function, $parameters, false);
245✔
471

472
        $value = $result[0];
191✔
473

474
        if ($value === null) {
191✔
475
            throw GeometryEngineException::operationYieldedNoResult();
27✔
476
        }
477

478
        return (float) $value;
164✔
479
    }
480

481
    /**
482
     * @return class-string<Proxy\ProxyInterface&Geometry>
483
     *
484
     * @throws GeometryEngineException
485
     */
486
    private function getProxyClassName(string $geometryType): string
487
    {
488
        $proxyClasses = [
434✔
489
            'CIRCULARSTRING' => Proxy\CircularStringProxy::class,
434✔
490
            'COMPOUNDCURVE' => Proxy\CompoundCurveProxy::class,
434✔
491
            'CURVE' => Proxy\CurveProxy::class,
434✔
492
            'CURVEPOLYGON' => Proxy\CurvePolygonProxy::class,
434✔
493
            'GEOMCOLLECTION' => Proxy\GeometryCollectionProxy::class, // MySQL 8 - https://github.com/brick/geo/pull/33
434✔
494
            'GEOMETRY' => Proxy\GeometryProxy::class,
434✔
495
            'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
434✔
496
            'LINESTRING' => Proxy\LineStringProxy::class,
434✔
497
            'MULTICURVE' => Proxy\MultiCurveProxy::class,
434✔
498
            'MULTILINESTRING' => Proxy\MultiLineStringProxy::class,
434✔
499
            'MULTIPOINT' => Proxy\MultiPointProxy::class,
434✔
500
            'MULTIPOLYGON' => Proxy\MultiPolygonProxy::class,
434✔
501
            'MULTISURFACE' => Proxy\MultiSurfaceProxy::class,
434✔
502
            'POINT' => Proxy\PointProxy::class,
434✔
503
            'POLYGON' => Proxy\PolygonProxy::class,
434✔
504
            'POLYHEDRALSURFACE' => Proxy\PolyhedralSurfaceProxy::class,
434✔
505
            'SURFACE' => Proxy\SurfaceProxy::class,
434✔
506
            'TIN' => Proxy\TinProxy::class,
434✔
507
            'TRIANGLE' => Proxy\TriangleProxy::class,
434✔
508
        ];
434✔
509

510
        $geometryType = strtoupper($geometryType);
434✔
511
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
434✔
512
        assert($geometryType !== null);
513
        $geometryType = preg_replace('/ .*/', '', $geometryType);
434✔
514
        assert($geometryType !== null);
515

516
        if (! isset($proxyClasses[$geometryType])) {
434✔
517
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
518
        }
519

520
        return $proxyClasses[$geometryType];
434✔
521
    }
522
}
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