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

brick / geo / 13354628402

16 Feb 2025 11:35AM UTC coverage: 83.38% (+1.2%) from 82.19%
13354628402

push

github

BenMorel
Public readonly props in WKBGeometryHeader

1505 of 1805 relevant lines covered (83.38%)

1868.63 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\Surface;
24
use Brick\Geo\TIN;
25
use Brick\Geo\Triangle;
26

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

154
        return (bool) $result;
636✔
155
    }
156

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

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

173
        return (float) $result;
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
    private 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);
313✔
188

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

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

193
        if ($wkt !== null) {
247✔
194
            if ($this->useProxy) {
5✔
195
                $geometryClass = $this->getGeometryClass($geometryType);
5✔
196
                $reflectionClass = new \ReflectionClass($geometryClass);
5✔
197

198
                /** @var Geometry */
199
                return $reflectionClass->newLazyProxy(fn() => $geometryClass::fromText($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
                $reflectionClass = new \ReflectionClass($geometryClass);
228✔
217

218
                /** @var Geometry */
219
                return $reflectionClass->newLazyProxy(fn() => $geometryClass::fromBinary($wkb, $srid));
228✔
220
            }
221

222
            return Geometry::fromBinary($wkb, $srid);
×
223
        }
224

225
        throw GeometryEngineException::operationYieldedNoResult();
14✔
226
    }
227

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

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

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

265
        return $geometryClasses[$geometryType];
233✔
266
    }
267

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

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

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

283
    public function intersection(Geometry $a, Geometry $b) : Geometry
284
    {
285
        return $this->queryGeometry('ST_Intersection', $a, $b);
12✔
286
    }
287

288
    public function difference(Geometry $a, Geometry $b) : Geometry
289
    {
290
        return $this->queryGeometry('ST_Difference', $a, $b);
12✔
291
    }
292

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

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

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

310
    public function length(Curve|MultiCurve $g) : float
311
    {
312
        return $this->queryFloat('ST_Length', $g);
95✔
313
    }
314

315
    public function area(Surface|MultiSurface $g) : float
316
    {
317
        return $this->queryFloat('ST_Area', $g);
66✔
318
    }
319

320
    public function azimuth(Point $observer, Point $subject) : float
321
    {
322
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
12✔
323
    }
324

325
    public function boundary(Geometry $g) : Geometry
326
    {
327
        return $this->queryGeometry('ST_Boundary', $g);
36✔
328
    }
329

330
    public function isValid(Geometry $g) : bool
331
    {
332
        return $this->queryBoolean('ST_IsValid', $g);
46✔
333
    }
334

335
    public function isClosed(Geometry $g) : bool
336
    {
337
        return $this->queryBoolean('ST_IsClosed', $g);
276✔
338
    }
339

340
    public function isSimple(Geometry $g) : bool
341
    {
342
        return $this->queryBoolean('ST_IsSimple', $g);
102✔
343
    }
344

345
    public function isRing(Curve $curve) : bool
346
    {
347
        try {
348
            return $this->queryBoolean('ST_IsRing', $curve);
58✔
349
        } catch (GeometryEngineException) {
10✔
350
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
351
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
352
        }
353
    }
354

355
    public function makeValid(Geometry $g) : Geometry
356
    {
357
        return $this->queryGeometry('ST_MakeValid', $g);
12✔
358
    }
359

360
    public function equals(Geometry $a, Geometry $b) : bool
361
    {
362
        return $this->queryBoolean('ST_Equals', $a, $b);
122✔
363
    }
364

365
    public function disjoint(Geometry $a, Geometry $b) : bool
366
    {
367
        return $this->queryBoolean('ST_Disjoint', $a, $b);
36✔
368
    }
369

370
    public function touches(Geometry $a, Geometry $b) : bool
371
    {
372
        return $this->queryBoolean('ST_Touches', $a, $b);
48✔
373
    }
374

375
    public function crosses(Geometry $a, Geometry $b) : bool
376
    {
377
        return $this->queryBoolean('ST_Crosses', $a, $b);
48✔
378
    }
379

380
    public function within(Geometry $a, Geometry $b) : bool
381
    {
382
        return $this->queryBoolean('ST_Within', $a, $b);
30✔
383
    }
384

385
    public function overlaps(Geometry $a, Geometry $b) : bool
386
    {
387
        return $this->queryBoolean('ST_Overlaps', $a, $b);
12✔
388
    }
389

390
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
391
    {
392
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
24✔
393
    }
394

395
    public function locateAlong(Geometry $g, float $mValue) : Geometry
396
    {
397
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
12✔
398
    }
399

400
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
401
    {
402
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
12✔
403
    }
404

405
    public function distance(Geometry $a, Geometry $b) : float
406
    {
407
        return $this->queryFloat('ST_Distance', $a, $b);
30✔
408
    }
409

410
    public function buffer(Geometry $g, float $distance) : Geometry
411
    {
412
        return $this->queryGeometry('ST_Buffer', $g, $distance);
18✔
413
    }
414

415
    public function convexHull(Geometry $g) : Geometry
416
    {
417
        return $this->queryGeometry('ST_ConvexHull', $g);
18✔
418
    }
419

420
    public function symDifference(Geometry $a, Geometry $b) : Geometry
421
    {
422
        return $this->queryGeometry('ST_SymDifference', $a, $b);
6✔
423
    }
424

425
    public function snapToGrid(Geometry $g, float $size) : Geometry
426
    {
427
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
24✔
428
    }
429

430
    public function simplify(Geometry $g, float $tolerance) : Geometry
431
    {
432
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
12✔
433
    }
434

435
    public function maxDistance(Geometry $a, Geometry $b) : float
436
    {
437
        return $this->queryFloat('ST_MaxDistance', $a, $b);
18✔
438
    }
439

440
    public function transform(Geometry $g, int $srid) : Geometry
441
    {
442
        return $this->queryGeometry('ST_Transform', $g, $srid);
4✔
443
    }
444

445
    public function split(Geometry $g, Geometry $blade) : Geometry
446
    {
447
        return $this->queryGeometry('ST_Split', $g, $blade);
8✔
448
    }
449
}
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