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

brick / geo / 13399473854

18 Feb 2025 08:26PM UTC coverage: 83.425%. Remained the same
13399473854

push

github

BenMorel
BoundingBox: readonly + promoted properties

23 of 28 new or added lines in 10 files covered. (82.14%)

8 existing lines in 3 files now uncovered.

1525 of 1828 relevant lines covered (83.42%)

1858.37 hits per line

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

95.45
/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 array 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
    {
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✔
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
        [$result] = $this->query($function, $parameters, false);
848✔
149

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

156
        return (bool) $result;
636✔
157
    }
158

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

171
        if ($result === null) {
191✔
172
            throw GeometryEngineException::operationYieldedNoResult();
27✔
173
        }
174

175
        return (float) $result;
164✔
176
    }
177

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

191
        [$wkt, $wkb, $geometryType, $srid] = $result;
247✔
192

193
        $srid = (int) $srid;
247✔
194

195
        if ($wkt !== null) {
247✔
196
            if ($this->useProxy) {
5✔
197
                $geometryClass = $this->getGeometryClass($geometryType);
5✔
198

199
                return ProxyFactory::createWktProxy($geometryClass, $wkt, $srid);
5✔
200
            }
201

202
            return Geometry::fromText($wkt, $srid);
×
203
        }
204

205
        if ($wkb !== null) {
242✔
206
            if (is_resource($wkb)) {
228✔
207
                $wkb = stream_get_contents($wkb);
59✔
208

209
                if ($wkb === false) {
59✔
210
                    throw new GeometryEngineException('Cannot read stream contents.');
×
211
                }
212
            }
213

214
            if ($this->useProxy) {
228✔
215
                $geometryClass = $this->getGeometryClass($geometryType);
228✔
216

217
                return ProxyFactory::createWkbProxy($geometryClass, $wkb, $srid);
228✔
218
            }
219

220
            return Geometry::fromBinary($wkb, $srid);
×
221
        }
222

223
        throw GeometryEngineException::operationYieldedNoResult();
14✔
224
    }
225

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

255
        $geometryType = strtoupper($geometryType);
233✔
256
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
233✔
257
        $geometryType = preg_replace('/ .*/', '', $geometryType);
233✔
258

259
        if (! isset($geometryClasses[$geometryType])) {
233✔
260
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
261
        }
262

263
        return $geometryClasses[$geometryType];
233✔
264
    }
265

266
    #[Override]
267
    public function contains(Geometry $a, Geometry $b) : bool
268
    {
269
        return $this->queryBoolean('ST_Contains', $a, $b);
76✔
270
    }
271

272
    #[Override]
273
    public function intersects(Geometry $a, Geometry $b) : bool
274
    {
275
        return $this->queryBoolean('ST_Intersects', $a, $b);
36✔
276
    }
277

278
    #[Override]
279
    public function union(Geometry $a, Geometry $b) : Geometry
280
    {
281
        return $this->queryGeometry('ST_Union', $a, $b);
22✔
282
    }
283

284
    #[Override]
285
    public function intersection(Geometry $a, Geometry $b) : Geometry
286
    {
287
        return $this->queryGeometry('ST_Intersection', $a, $b);
12✔
288
    }
289

290
    #[Override]
291
    public function difference(Geometry $a, Geometry $b) : Geometry
292
    {
293
        return $this->queryGeometry('ST_Difference', $a, $b);
12✔
294
    }
295

296
    #[Override]
297
    public function envelope(Geometry $g) : Geometry
298
    {
299
        return $this->queryGeometry('ST_Envelope', $g);
18✔
300
    }
301

302
    #[Override]
303
    public function centroid(Geometry $g) : Point
304
    {
305
        /** @var Point */
306
        return $this->queryGeometry('ST_Centroid', $g);
45✔
307
    }
308

309
    #[Override]
310
    public function pointOnSurface(Surface|MultiSurface $g) : Point
311
    {
312
        /** @var Point */
313
        return $this->queryGeometry('ST_PointOnSurface', $g);
42✔
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);
12✔
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✔
UNCOV
363
        } catch (GeometryEngineException) {
10✔
364
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
UNCOV
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);
12✔
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);
4✔
475
    }
476

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