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

brick / geo / 13715715561

06 Mar 2025 10:47PM UTC coverage: 44.086% (-40.4%) from 84.507%
13715715561

push

github

BenMorel
Remove Psalm-specific annotations

1543 of 3500 relevant lines covered (44.09%)

270.91 hits per line

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

83.61
/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\Exception\GeometryEngineException;
9
use Brick\Geo\Geometry;
10
use Brick\Geo\LineString;
11
use Brick\Geo\MultiCurve;
12
use Brick\Geo\MultiPoint;
13
use Brick\Geo\MultiSurface;
14
use Brick\Geo\MultiPolygon;
15
use Brick\Geo\Point;
16
use Brick\Geo\Polygon;
17
use Brick\Geo\Proxy;
18
use Brick\Geo\Proxy\ProxyInterface;
19
use Brick\Geo\Surface;
20
use Override;
21

22
/**
23
 * Database implementation of the GeometryEngine.
24
 *
25
 * The target database must have support for GIS functions.
26
 */
27
abstract class DatabaseEngine implements GeometryEngine
28
{
29
    private readonly bool $useProxy;
30

31
    public function __construct(bool $useProxy)
32
    {
33
        $this->useProxy = $useProxy;
×
34
    }
35

36
    /**
37
     * Executes a SQL query.
38
     *
39
     * @param string                                   $query      The SQL query to execute.
40
     * @param list<GeometryParameter|string|float|int> $parameters The geometry data or scalar values to pass as parameters.
41
     *
42
     * @return list<mixed> A numeric result array.
43
     *
44
     * @throws GeometryEngineException
45
     */
46
    abstract protected function executeQuery(string $query, array $parameters) : array;
47

48
    /**
49
     * Returns the syntax required to perform a ST_GeomFromText(), together with placeholders.
50
     *
51
     * This method may be overridden if necessary.
52
     */
53
    protected function getGeomFromTextSyntax(): string
54
    {
55
        return 'ST_GeomFromText(?, ?)';
50✔
56
    }
57

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

68
    /**
69
     * Returns the placeholder syntax for the given parameter.
70
     *
71
     * This method may be overridden to perform explicit type casts if necessary.
72
     */
73
    protected function getParameterPlaceholder(string|float|int $parameter): string
74
    {
75
        return '?';
52✔
76
    }
77

78
    /**
79
     * Builds and executes a SQL query for a GIS function.
80
     *
81
     * @param string                           $function        The SQL GIS function to execute.
82
     * @param array<Geometry|string|float|int> $parameters     The Geometry objects or scalar values to pass as parameters.
83
     * @param bool                             $returnsGeometry Whether the GIS function returns a Geometry.
84
     *
85
     * @return list<mixed> A numeric result array.
86
     *
87
     * @throws GeometryEngineException
88
     */
89
    private function query(string $function, array $parameters, bool $returnsGeometry) : array
90
    {
91
        $queryParameters = [];
436✔
92
        $queryValues = [];
436✔
93

94
        foreach ($parameters as $parameter) {
436✔
95
            if ($parameter instanceof Geometry) {
436✔
96
                if ($parameter instanceof Proxy\ProxyInterface) {
436✔
97
                    $sendAsBinary = $parameter->isProxyBinary();
30✔
98
                } else {
99
                    $sendAsBinary = ! $parameter->isEmpty();
436✔
100
                }
101

102
                $queryParameters[] = $sendAsBinary
436✔
103
                    ? $this->getGeomFromWKBSyntax()
390✔
104
                    : $this->getGeomFromTextSyntax();
50✔
105

106
                $queryValues[] = new GeometryParameter($parameter, $sendAsBinary);
436✔
107
            } else {
108
                $queryParameters[] = $this->getParameterPlaceholder($parameter);
52✔
109
                $queryValues[] = $parameter;
52✔
110
            }
111
        }
112

113
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
436✔
114

115
        if ($returnsGeometry) {
436✔
116
            $query = sprintf('
116✔
117
                SELECT
118
                    CASE WHEN ST_IsEmpty(g) THEN ST_AsText(g) ELSE NULL END,
119
                    CASE WHEN ST_IsEmpty(g) THEN NULL ELSE ST_AsBinary(g) END,
120
                    ST_GeometryType(g),
121
                    ST_SRID(g)
122
                FROM (%s AS g) AS q
123
            ', $query);
116✔
124
        }
125

126
        return $this->executeQuery($query, $queryValues);
436✔
127
    }
128

129
    /**
130
     * Queries a GIS function returning a boolean value.
131
     *
132
     * @param string                       $function   The SQL GIS function to execute.
133
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
134
     *
135
     * @throws GeometryEngineException
136
     */
137
    private function queryBoolean(string $function, Geometry|string|float|int ...$parameters) : bool
138
    {
139
        /** @var array{scalar|null} $result */
140
        $result = $this->query($function, $parameters, false);
286✔
141

142
        $value = $result[0];
182✔
143

144
        // SQLite3 returns -1 when calling a boolean GIS function on a NULL result,
145
        // MariaDB returns -1 when an unsupported operation is performed on a Z/M geometry.
146
        if ($value === null || $value === -1 || $value === '-1') {
182✔
147
            throw GeometryEngineException::operationYieldedNoResult();
×
148
        }
149

150
        return (bool) $value;
182✔
151
    }
152

153
    /**
154
     * Queries a GIS function returning a floating point value.
155
     *
156
     * @param string                       $function   The SQL GIS function to execute.
157
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
158
     *
159
     * @throws GeometryEngineException
160
     */
161
    private function queryFloat(string $function, Geometry|string|float|int ...$parameters) : float
162
    {
163
        /** @var array{scalar|null} $result */
164
        $result = $this->query($function, $parameters, false);
70✔
165

166
        $value = $result[0];
46✔
167

168
        if ($value === null) {
46✔
169
            throw GeometryEngineException::operationYieldedNoResult();
×
170
        }
171

172
        return (float) $value;
46✔
173
    }
174

175
    /**
176
     * Queries a GIS function returning a Geometry object.
177
     *
178
     * @param string                       $function   The SQL GIS function to execute.
179
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
180
     *
181
     * @throws GeometryEngineException
182
     */
183
    protected function queryGeometry(string $function, Geometry|string|float|int ...$parameters) : Geometry
184
    {
185
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
186
        $result = $this->query($function, $parameters, true);
116✔
187

188
        [$wkt, $wkb, $geometryType, $srid] = $result;
68✔
189

190
        $srid = (int) $srid;
68✔
191

192
        if ($wkt !== null) {
68✔
193
            if ($this->useProxy) {
×
194
                $proxyClassName = $this->getProxyClassName($geometryType);
×
195

196
                return new $proxyClassName($wkt, false, $srid);
×
197
            }
198

199
            return Geometry::fromText($wkt, $srid);
×
200
        }
201

202
        if ($wkb !== null) {
68✔
203
            if (is_resource($wkb)) {
68✔
204
                $wkb = stream_get_contents($wkb);
×
205
            }
206

207
            if ($this->useProxy) {
68✔
208
                $proxyClassName = $this->getProxyClassName($geometryType);
68✔
209

210
                return new $proxyClassName($wkb, true, $srid);
68✔
211
            }
212

213
            return Geometry::fromBinary($wkb, $srid);
×
214
        }
215

216
        throw GeometryEngineException::operationYieldedNoResult();
×
217
    }
218

219
    /**
220
     * @return class-string<Proxy\ProxyInterface&Geometry>
221
     *
222
     * @throws GeometryEngineException
223
     */
224
    private function getProxyClassName(string $geometryType) : string
225
    {
226
        $proxyClasses = [
68✔
227
            'CIRCULARSTRING'     => Proxy\CircularStringProxy::class,
68✔
228
            'COMPOUNDCURVE'      => Proxy\CompoundCurveProxy::class,
68✔
229
            'CURVE'              => Proxy\CurveProxy::class,
68✔
230
            'CURVEPOLYGON'       => Proxy\CurvePolygonProxy::class,
68✔
231
            'GEOMCOLLECTION'     => Proxy\GeometryCollectionProxy::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
68✔
232
            'GEOMETRY'           => Proxy\GeometryProxy::class,
68✔
233
            'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
68✔
234
            'LINESTRING'         => Proxy\LineStringProxy::class,
68✔
235
            'MULTICURVE'         => Proxy\MultiCurveProxy::class,
68✔
236
            'MULTILINESTRING'    => Proxy\MultiLineStringProxy::class,
68✔
237
            'MULTIPOINT'         => Proxy\MultiPointProxy::class,
68✔
238
            'MULTIPOLYGON'       => Proxy\MultiPolygonProxy::class,
68✔
239
            'MULTISURFACE'       => Proxy\MultiSurfaceProxy::class,
68✔
240
            'POINT'              => Proxy\PointProxy::class,
68✔
241
            'POLYGON'            => Proxy\PolygonProxy::class,
68✔
242
            'POLYHEDRALSURFACE'  => Proxy\PolyhedralSurfaceProxy::class,
68✔
243
            'SURFACE'            => Proxy\SurfaceProxy::class,
68✔
244
            'TIN'                => Proxy\TINProxy::class,
68✔
245
            'TRIANGLE'           => Proxy\TriangleProxy::class
68✔
246
        ];
68✔
247

248
        $geometryType = strtoupper($geometryType);
68✔
249
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
68✔
250
        assert($geometryType !== null);
251
        $geometryType = preg_replace('/ .*/', '', $geometryType);
68✔
252
        assert($geometryType !== null);
253

254
        if (! isset($proxyClasses[$geometryType])) {
68✔
255
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
256
        }
257

258
        return $proxyClasses[$geometryType];
68✔
259
    }
260

261
    #[Override]
262
    public function contains(Geometry $a, Geometry $b) : bool
263
    {
264
        return $this->queryBoolean('ST_Contains', $a, $b);
16✔
265
    }
266

267
    #[Override]
268
    public function intersects(Geometry $a, Geometry $b) : bool
269
    {
270
        return $this->queryBoolean('ST_Intersects', $a, $b);
12✔
271
    }
272

273
    #[Override]
274
    public function union(Geometry $a, Geometry $b) : Geometry
275
    {
276
        return $this->queryGeometry('ST_Union', $a, $b);
8✔
277
    }
278

279
    #[Override]
280
    public function intersection(Geometry $a, Geometry $b) : Geometry
281
    {
282
        return $this->queryGeometry('ST_Intersection', $a, $b);
4✔
283
    }
284

285
    #[Override]
286
    public function difference(Geometry $a, Geometry $b) : Geometry
287
    {
288
        return $this->queryGeometry('ST_Difference', $a, $b);
4✔
289
    }
290

291
    #[Override]
292
    public function envelope(Geometry $g) : Geometry
293
    {
294
        return $this->queryGeometry('ST_Envelope', $g);
6✔
295
    }
296

297
    #[Override]
298
    public function centroid(Geometry $g) : Point
299
    {
300
        /** @var Point */
301
        return $this->queryGeometry('ST_Centroid', $g);
16✔
302
    }
303

304
    #[Override]
305
    public function pointOnSurface(Surface|MultiSurface $g) : Point
306
    {
307
        /** @var Point */
308
        return $this->queryGeometry('ST_PointOnSurface', $g);
14✔
309
    }
310

311
    #[Override]
312
    public function length(Curve|MultiCurve $g) : float
313
    {
314
        return $this->queryFloat('ST_Length', $g);
32✔
315
    }
316

317
    #[Override]
318
    public function area(Surface|MultiSurface $g) : float
319
    {
320
        return $this->queryFloat('ST_Area', $g);
22✔
321
    }
322

323
    #[Override]
324
    public function azimuth(Point $observer, Point $subject) : float
325
    {
326
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
×
327
    }
328

329
    #[Override]
330
    public function boundary(Geometry $g) : Geometry
331
    {
332
        return $this->queryGeometry('ST_Boundary', $g);
12✔
333
    }
334

335
    #[Override]
336
    public function isValid(Geometry $g) : bool
337
    {
338
        return $this->queryBoolean('ST_IsValid', $g);
12✔
339
    }
340

341
    #[Override]
342
    public function isClosed(Geometry $g) : bool
343
    {
344
        return $this->queryBoolean('ST_IsClosed', $g);
98✔
345
    }
346

347
    #[Override]
348
    public function isSimple(Geometry $g) : bool
349
    {
350
        return $this->queryBoolean('ST_IsSimple', $g);
38✔
351
    }
352

353
    #[Override]
354
    public function isRing(Curve $curve) : bool
355
    {
356
        try {
357
            return $this->queryBoolean('ST_IsRing', $curve);
10✔
358
        } catch (GeometryEngineException) {
10✔
359
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
360
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
361
        }
362
    }
363

364
    #[Override]
365
    public function makeValid(Geometry $g) : Geometry
366
    {
367
        return $this->queryGeometry('ST_MakeValid', $g);
×
368
    }
369

370
    #[Override]
371
    public function equals(Geometry $a, Geometry $b) : bool
372
    {
373
        return $this->queryBoolean('ST_Equals', $a, $b);
50✔
374
    }
375

376
    #[Override]
377
    public function disjoint(Geometry $a, Geometry $b) : bool
378
    {
379
        return $this->queryBoolean('ST_Disjoint', $a, $b);
12✔
380
    }
381

382
    #[Override]
383
    public function touches(Geometry $a, Geometry $b) : bool
384
    {
385
        return $this->queryBoolean('ST_Touches', $a, $b);
16✔
386
    }
387

388
    #[Override]
389
    public function crosses(Geometry $a, Geometry $b) : bool
390
    {
391
        return $this->queryBoolean('ST_Crosses', $a, $b);
16✔
392
    }
393

394
    #[Override]
395
    public function within(Geometry $a, Geometry $b) : bool
396
    {
397
        return $this->queryBoolean('ST_Within', $a, $b);
10✔
398
    }
399

400
    #[Override]
401
    public function overlaps(Geometry $a, Geometry $b) : bool
402
    {
403
        return $this->queryBoolean('ST_Overlaps', $a, $b);
4✔
404
    }
405

406
    #[Override]
407
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
408
    {
409
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
8✔
410
    }
411

412
    #[Override]
413
    public function locateAlong(Geometry $g, float $mValue) : Geometry
414
    {
415
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
4✔
416
    }
417

418
    #[Override]
419
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
420
    {
421
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
4✔
422
    }
423

424
    #[Override]
425
    public function distance(Geometry $a, Geometry $b) : float
426
    {
427
        return $this->queryFloat('ST_Distance', $a, $b);
10✔
428
    }
429

430
    #[Override]
431
    public function buffer(Geometry $g, float $distance) : Geometry
432
    {
433
        return $this->queryGeometry('ST_Buffer', $g, $distance);
6✔
434
    }
435

436
    #[Override]
437
    public function convexHull(Geometry $g) : Geometry
438
    {
439
        return $this->queryGeometry('ST_ConvexHull', $g);
6✔
440
    }
441

442
    #[Override]
443
    public function symDifference(Geometry $a, Geometry $b) : Geometry
444
    {
445
        return $this->queryGeometry('ST_SymDifference', $a, $b);
2✔
446
    }
447

448
    #[Override]
449
    public function snapToGrid(Geometry $g, float $size) : Geometry
450
    {
451
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
8✔
452
    }
453

454
    #[Override]
455
    public function simplify(Geometry $g, float $tolerance) : Geometry
456
    {
457
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
4✔
458
    }
459

460
    #[Override]
461
    public function maxDistance(Geometry $a, Geometry $b) : float
462
    {
463
        return $this->queryFloat('ST_MaxDistance', $a, $b);
6✔
464
    }
465

466
    #[Override]
467
    public function transform(Geometry $g, int $srid) : Geometry
468
    {
469
        return $this->queryGeometry('ST_Transform', $g, $srid);
×
470
    }
471

472
    #[Override]
473
    public function split(Geometry $g, Geometry $blade) : Geometry
474
    {
475
        return $this->queryGeometry('ST_Split', $g, $blade);
×
476
    }
477

478
    /**
479
     * @throws GeometryEngineException
480
     */
481
    #[Override]
482
    public function lineInterpolatePoint(LineString $linestring, float $fraction) : Point
483
    {
484
        $result = $this->queryGeometry('ST_LineInterpolatePoint', $linestring, $fraction);
8✔
485
        if (! $result instanceof Point) {
8✔
486
            throw new GeometryEngineException('This operation yielded wrong type: ' . $result::class);
×
487
        }
488

489
        return $result;
8✔
490
    }
491

492
    #[Override]
493
    public function lineInterpolatePoints(LineString $linestring, float $fraction) : MultiPoint
494
    {
495
        $result = $this->queryGeometry('ST_LineInterpolatePoints', $linestring, $fraction);
10✔
496

497
        if ($result instanceof MultiPoint) {
8✔
498
            return $result;
8✔
499
        }
500

501
        if ($result->isEmpty()) {
×
502
            return new MultiPoint($result->coordinateSystem());
×
503
        }
504

505
        return MultiPoint::of($result);
×
506
    }
507
}
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