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

brick / geo / 13731438031

07 Mar 2025 11:54PM UTC coverage: 49.73%. Remained the same
13731438031

push

github

BenMorel
Use array_reduce() in getBoundingBox()

10 of 30 new or added lines in 6 files covered. (33.33%)

48 existing lines in 8 files now uncovered.

1748 of 3515 relevant lines covered (49.73%)

974.97 hits per line

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

96.8
/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\MultiPolygon;
16
use Brick\Geo\Point;
17
use Brick\Geo\Polygon;
18
use Brick\Geo\Proxy;
19
use Brick\Geo\Proxy\ProxyInterface;
20
use Brick\Geo\Surface;
21
use Override;
22

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

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

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

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

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

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

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

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

103
                $queryParameters[] = $sendAsBinary
1,670✔
104
                    ? $this->getGeomFromWKBSyntax()
1,555✔
105
                    : $this->getGeomFromTextSyntax();
125✔
106

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

114
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
1,670✔
115

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

127
        return $this->executeQuery($query, $queryValues);
1,670✔
128
    }
129

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

143
        $value = $result[0];
732✔
144

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

151
        return (bool) $value;
636✔
152
    }
153

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

167
        $value = $result[0];
191✔
168

169
        if ($value === null) {
191✔
170
            throw GeometryEngineException::operationYieldedNoResult();
27✔
171
        }
172

173
        return (float) $value;
164✔
174
    }
175

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

189
        [$wkt, $wkb, $geometryType, $srid] = $result;
436✔
190

191
        $srid = (int) $srid;
436✔
192

193
        if ($wkt !== null) {
436✔
194
            if ($this->useProxy) {
6✔
195
                $proxyClassName = $this->getProxyClassName($geometryType);
6✔
196

197
                return new $proxyClassName($wkt, false, $srid);
6✔
198
            }
199

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

203
        if ($wkb !== null) {
430✔
204
            if (is_resource($wkb)) {
416✔
205
                $wkb = stream_get_contents($wkb);
112✔
206
            }
207

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

211
                return new $proxyClassName($wkb, true, $srid);
416✔
212
            }
213

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

UNCOV
217
        throw GeometryEngineException::operationYieldedNoResult();
14✔
218
    }
219

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

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

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

259
        return $proxyClasses[$geometryType];
422✔
260
    }
261

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

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

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

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

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

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

298
    #[Override]
299
    public function centroid(Geometry $g) : Point
300
    {
301
        $centroid = $this->queryGeometry('ST_Centroid', $g);
45✔
302
        TypeChecker::check($centroid, Point::class);
45✔
303

304
        return $centroid;
45✔
305
    }
306

307
    #[Override]
308
    public function pointOnSurface(Surface|MultiSurface $g) : Point
309
    {
310
        $pointOnSurface = $this->queryGeometry('ST_PointOnSurface', $g);
42✔
311
        TypeChecker::check($pointOnSurface, Point::class);
28✔
312

313
        return $pointOnSurface;
28✔
314
    }
315

316
    #[Override]
317
    public function length(Curve|MultiCurve $g) : float
318
    {
319
        return $this->queryFloat('ST_Length', $g);
95✔
320
    }
321

322
    #[Override]
323
    public function area(Surface|MultiSurface $g) : float
324
    {
325
        return $this->queryFloat('ST_Area', $g);
66✔
326
    }
327

328
    #[Override]
329
    public function azimuth(Point $observer, Point $subject) : float
330
    {
331
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
36✔
332
    }
333

334
    #[Override]
335
    public function boundary(Geometry $g) : Geometry
336
    {
337
        return $this->queryGeometry('ST_Boundary', $g);
36✔
338
    }
339

340
    #[Override]
341
    public function isValid(Geometry $g) : bool
342
    {
343
        return $this->queryBoolean('ST_IsValid', $g);
46✔
344
    }
345

346
    #[Override]
347
    public function isClosed(Geometry $g) : bool
348
    {
349
        return $this->queryBoolean('ST_IsClosed', $g);
276✔
350
    }
351

352
    #[Override]
353
    public function isSimple(Geometry $g) : bool
354
    {
355
        return $this->queryBoolean('ST_IsSimple', $g);
102✔
356
    }
357

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

369
    #[Override]
370
    public function makeValid(Geometry $g) : Geometry
371
    {
372
        return $this->queryGeometry('ST_MakeValid', $g);
36✔
373
    }
374

375
    #[Override]
376
    public function equals(Geometry $a, Geometry $b) : bool
377
    {
378
        return $this->queryBoolean('ST_Equals', $a, $b);
122✔
379
    }
380

381
    #[Override]
382
    public function disjoint(Geometry $a, Geometry $b) : bool
383
    {
384
        return $this->queryBoolean('ST_Disjoint', $a, $b);
36✔
385
    }
386

387
    #[Override]
388
    public function touches(Geometry $a, Geometry $b) : bool
389
    {
390
        return $this->queryBoolean('ST_Touches', $a, $b);
48✔
391
    }
392

393
    #[Override]
394
    public function crosses(Geometry $a, Geometry $b) : bool
395
    {
396
        return $this->queryBoolean('ST_Crosses', $a, $b);
48✔
397
    }
398

399
    #[Override]
400
    public function within(Geometry $a, Geometry $b) : bool
401
    {
402
        return $this->queryBoolean('ST_Within', $a, $b);
30✔
403
    }
404

405
    #[Override]
406
    public function overlaps(Geometry $a, Geometry $b) : bool
407
    {
408
        return $this->queryBoolean('ST_Overlaps', $a, $b);
12✔
409
    }
410

411
    #[Override]
412
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
413
    {
414
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
24✔
415
    }
416

417
    #[Override]
418
    public function locateAlong(Geometry $g, float $mValue) : Geometry
419
    {
420
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
12✔
421
    }
422

423
    #[Override]
424
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
425
    {
426
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
12✔
427
    }
428

429
    #[Override]
430
    public function distance(Geometry $a, Geometry $b) : float
431
    {
432
        return $this->queryFloat('ST_Distance', $a, $b);
30✔
433
    }
434

435
    #[Override]
436
    public function buffer(Geometry $g, float $distance) : Geometry
437
    {
438
        return $this->queryGeometry('ST_Buffer', $g, $distance);
18✔
439
    }
440

441
    #[Override]
442
    public function convexHull(Geometry $g) : Geometry
443
    {
444
        return $this->queryGeometry('ST_ConvexHull', $g);
18✔
445
    }
446

447
    #[Override]
448
    public function symDifference(Geometry $a, Geometry $b) : Geometry
449
    {
450
        return $this->queryGeometry('ST_SymDifference', $a, $b);
6✔
451
    }
452

453
    #[Override]
454
    public function snapToGrid(Geometry $g, float $size) : Geometry
455
    {
456
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
24✔
457
    }
458

459
    #[Override]
460
    public function simplify(Geometry $g, float $tolerance) : Geometry
461
    {
462
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
12✔
463
    }
464

465
    #[Override]
466
    public function maxDistance(Geometry $a, Geometry $b) : float
467
    {
468
        return $this->queryFloat('ST_MaxDistance', $a, $b);
18✔
469
    }
470

471
    #[Override]
472
    public function transform(Geometry $g, int $srid) : Geometry
473
    {
474
        return $this->queryGeometry('ST_Transform', $g, $srid);
42✔
475
    }
476

477
    #[Override]
478
    public function split(Geometry $g, Geometry $blade) : Geometry
479
    {
480
        return $this->queryGeometry('ST_Split', $g, $blade);
24✔
481
    }
482

483
    #[Override]
484
    public function lineInterpolatePoint(LineString $lineString, float $fraction) : Point
485
    {
486
        $result = $this->queryGeometry('ST_LineInterpolatePoint', $lineString, $fraction);
105✔
487
        TypeChecker::check($result, Point::class);
63✔
488

489
        return $result;
63✔
490
    }
491

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

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

501
        // POINT EMPTY
502
        if ($result->isEmpty()) {
18✔
503
            return new MultiPoint($result->coordinateSystem());
1✔
504
        }
505

506
        // POINT
507
        return MultiPoint::of($result);
17✔
508
    }
509
}
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