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

brick / geo / 13349853684

16 Feb 2025 12:13AM UTC coverage: 80.033% (+32.7%) from 47.314%
13349853684

push

github

BenMorel
Use PHP 8.4 lazy proxies

27 of 48 new or added lines in 2 files covered. (56.25%)

55 existing lines in 4 files now uncovered.

1443 of 1803 relevant lines covered (80.03%)

499.41 hits per line

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

88.68
/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_GeomFromWKB(), together with placeholders.
58
     *
59
     * This method may be overridden if necessary.
60
     */
61
    protected function getGeomFromWKBSyntax(): string
62
    {
63
        return 'ST_GeomFromWKB(?, ?)';
201✔
64
    }
65

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

76
    /**
77
     * Builds and executes a SQL query for a GIS function.
78
     *
79
     * @psalm-param array<Geometry|string|float|int> $parameters
80
     * @psalm-return list<mixed>
81
     *
82
     * @param string $function        The SQL GIS function to execute.
83
     * @param array  $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 array A numeric result array.
87
     *
88
     * @throws GeometryEngineException
89
     */
90
    private function query(string $function, array $parameters, bool $returnsGeometry) : array
91
    {
92
        $queryParameters = [];
201✔
93
        $queryValues = [];
201✔
94

95
        foreach ($parameters as $parameter) {
201✔
96
            if ($parameter instanceof Geometry) {
201✔
97
                $queryParameters[] = $this->getGeomFromWKBSyntax();
201✔
98
                $queryValues[] = new GeometryParameter($parameter, true);
201✔
99
            } else {
100
                $queryParameters[] = $this->getParameterPlaceholder($parameter);
17✔
101
                $queryValues[] = $parameter;
17✔
102
            }
103
        }
104

105
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
201✔
106

107
        if ($returnsGeometry) {
201✔
108
            $query = sprintf('
57✔
109
                SELECT
110
                    CASE WHEN ST_IsEmpty(g) THEN ST_AsText(g) ELSE NULL END,
111
                    CASE WHEN ST_IsEmpty(g) THEN NULL ELSE ST_AsBinary(g) END,
112
                    ST_GeometryType(g),
113
                    ST_SRID(g)
114
                FROM (%s AS g) AS q
115
            ', $query);
57✔
116
        }
117

118
        return $this->executeQuery($query, $queryValues);
201✔
119
    }
120

121
    /**
122
     * Queries a GIS function returning a boolean value.
123
     *
124
     * @param string                       $function   The SQL GIS function to execute.
125
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
126
     *
127
     * @throws GeometryEngineException
128
     */
129
    private function queryBoolean(string $function, Geometry|string|float|int ...$parameters) : bool
130
    {
131
        [$result] = $this->query($function, $parameters, false);
130✔
132

133
        // SQLite3 returns -1 when calling a boolean GIS function on a NULL result,
134
        // MariaDB returns -1 when an unsupported operation is performed on a Z/M geometry.
135
        if ($result === null || $result === -1 || $result === '-1') {
130✔
136
            throw GeometryEngineException::operationYieldedNoResult();
4✔
137
        }
138

139
        return (bool) $result;
126✔
140
    }
141

142
    /**
143
     * Queries a GIS function returning a floating point value.
144
     *
145
     * @param string                       $function   The SQL GIS function to execute.
146
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
147
     *
148
     * @throws GeometryEngineException
149
     */
150
    private function queryFloat(string $function, Geometry|string|float|int ...$parameters) : float
151
    {
152
        [$result] = $this->query($function, $parameters, false);
40✔
153

154
        if ($result === null) {
40✔
155
            throw GeometryEngineException::operationYieldedNoResult();
8✔
156
        }
157

158
        return (float) $result;
32✔
159
    }
160

161
    /**
162
     * Queries a GIS function returning a Geometry object.
163
     *
164
     * @param string                       $function   The SQL GIS function to execute.
165
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
166
     *
167
     * @throws GeometryEngineException
168
     */
169
    private function queryGeometry(string $function, Geometry|string|float|int ...$parameters) : Geometry
170
    {
171
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
172
        $result = $this->query($function, $parameters, true);
57✔
173

174
        [$wkt, $wkb, $geometryType, $srid] = $result;
57✔
175

176
        $srid = (int) $srid;
57✔
177

178
        if ($wkt !== null) {
57✔
179
            if ($this->useProxy) {
×
NEW
180
                $geometryClass = $this->getGeometryClass($geometryType);
×
NEW
181
                $reflectionClass = new \ReflectionClass($geometryClass);
×
182

NEW
183
                return $reflectionClass->newLazyProxy(fn() => $geometryClass::fromText($wkt, $srid));
×
184
            }
185

186
            return Geometry::fromText($wkt, $srid);
×
187
        }
188

189
        if ($wkb !== null) {
57✔
190
            if (is_resource($wkb)) {
53✔
191
                $wkb = stream_get_contents($wkb);
×
192
            }
193

194
            if ($this->useProxy) {
53✔
195
                $geometryClass = $this->getGeometryClass($geometryType);
53✔
196
                $reflectionClass = new \ReflectionClass($geometryClass);
53✔
197

198
                return $reflectionClass->newLazyProxy(fn() => $geometryClass::fromBinary($wkb, $srid));
53✔
199
            }
200

201
            return Geometry::fromBinary($wkb, $srid);
×
202
        }
203

204
        throw GeometryEngineException::operationYieldedNoResult();
4✔
205
    }
206

207
    /**
208
     * @return class-string<Geometry>
209
     *
210
     * @throws GeometryEngineException
211
     */
212
    private function getGeometryClass(string $geometryType) : string
213
    {
214
        $geometryClasses = [
53✔
215
            'CIRCULARSTRING'     => CircularString::class,
53✔
216
            'COMPOUNDCURVE'      => CompoundCurve::class,
53✔
217
            'CURVE'              => Curve::class,
53✔
218
            'CURVEPOLYGON'       => CurvePolygon::class,
53✔
219
            'GEOMCOLLECTION'     => GeometryCollection::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
53✔
220
            'GEOMETRY'           => Geometry::class,
53✔
221
            'GEOMETRYCOLLECTION' => GeometryCollection::class,
53✔
222
            'LINESTRING'         => LineString::class,
53✔
223
            'MULTICURVE'         => MultiCurve::class,
53✔
224
            'MULTILINESTRING'    => MultiLineString::class,
53✔
225
            'MULTIPOINT'         => MultiPoint::class,
53✔
226
            'MULTIPOLYGON'       => MultiPolygon::class,
53✔
227
            'MULTISURFACE'       => MultiSurface::class,
53✔
228
            'POINT'              => Point::class,
53✔
229
            'POLYGON'            => Polygon::class,
53✔
230
            'POLYHEDRALSURFACE'  => PolyhedralSurface::class,
53✔
231
            'SURFACE'            => Surface::class,
53✔
232
            'TIN'                => TIN::class,
53✔
233
            'TRIANGLE'           => Triangle::class
53✔
234
        ];
53✔
235

236
        $geometryType = strtoupper($geometryType);
53✔
237
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
53✔
238
        $geometryType = preg_replace('/ .*/', '', $geometryType);
53✔
239

240
        if (! isset($geometryClasses[$geometryType])) {
53✔
241
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
242
        }
243

244
        return $geometryClasses[$geometryType];
53✔
245
    }
246

247
    public function contains(Geometry $a, Geometry $b) : bool
248
    {
249
        return $this->queryBoolean('ST_Contains', $a, $b);
15✔
250
    }
251

252
    public function intersects(Geometry $a, Geometry $b) : bool
253
    {
254
        return $this->queryBoolean('ST_Intersects', $a, $b);
6✔
255
    }
256

257
    public function union(Geometry $a, Geometry $b) : Geometry
258
    {
259
        return $this->queryGeometry('ST_Union', $a, $b);
2✔
260
    }
261

262
    public function intersection(Geometry $a, Geometry $b) : Geometry
263
    {
264
        return $this->queryGeometry('ST_Intersection', $a, $b);
2✔
265
    }
266

267
    public function difference(Geometry $a, Geometry $b) : Geometry
268
    {
269
        return $this->queryGeometry('ST_Difference', $a, $b);
2✔
270
    }
271

272
    public function envelope(Geometry $g) : Geometry
273
    {
274
        return $this->queryGeometry('ST_Envelope', $g);
3✔
275
    }
276

277
    public function centroid(Geometry $g) : Point
278
    {
279
        /** @var Point */
280
        return $this->queryGeometry('ST_Centroid', $g);
8✔
281
    }
282

283
    public function pointOnSurface(Surface|MultiSurface $g) : Point
284
    {
285
        /** @var Point */
286
        return $this->queryGeometry('ST_PointOnSurface', $g);
7✔
287
    }
288

289
    public function length(Curve|MultiCurve $g) : float
290
    {
291
        return $this->queryFloat('ST_Length', $g);
15✔
292
    }
293

294
    public function area(Surface|MultiSurface $g) : float
295
    {
296
        return $this->queryFloat('ST_Area', $g);
11✔
297
    }
298

299
    public function azimuth(Point $observer, Point $subject) : float
300
    {
301
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
6✔
302
    }
303

304
    public function boundary(Geometry $g) : Geometry
305
    {
306
        return $this->queryGeometry('ST_Boundary', $g);
6✔
307
    }
308

309
    public function isValid(Geometry $g) : bool
310
    {
311
        return $this->queryBoolean('ST_IsValid', $g);
10✔
312
    }
313

314
    public function isClosed(Geometry $g) : bool
315
    {
316
        return $this->queryBoolean('ST_IsClosed', $g);
31✔
317
    }
318

319
    public function isSimple(Geometry $g) : bool
320
    {
321
        return $this->queryBoolean('ST_IsSimple', $g);
16✔
322
    }
323

324
    public function isRing(Curve $curve) : bool
325
    {
326
        try {
327
            return $this->queryBoolean('ST_IsRing', $curve);
13✔
328
        } catch (GeometryEngineException) {
×
329
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
330
            return $this->isClosed($curve) && $this->isSimple($curve);
×
331
        }
332
    }
333

334
    public function makeValid(Geometry $g) : Geometry
335
    {
336
        return $this->queryGeometry('ST_MakeValid', $g);
6✔
337
    }
338

339
    public function equals(Geometry $a, Geometry $b) : bool
340
    {
341
        return $this->queryBoolean('ST_Equals', $a, $b);
21✔
342
    }
343

344
    public function disjoint(Geometry $a, Geometry $b) : bool
345
    {
346
        return $this->queryBoolean('ST_Disjoint', $a, $b);
6✔
347
    }
348

349
    public function touches(Geometry $a, Geometry $b) : bool
350
    {
351
        return $this->queryBoolean('ST_Touches', $a, $b);
8✔
352
    }
353

354
    public function crosses(Geometry $a, Geometry $b) : bool
355
    {
356
        return $this->queryBoolean('ST_Crosses', $a, $b);
8✔
357
    }
358

359
    public function within(Geometry $a, Geometry $b) : bool
360
    {
361
        return $this->queryBoolean('ST_Within', $a, $b);
5✔
362
    }
363

364
    public function overlaps(Geometry $a, Geometry $b) : bool
365
    {
366
        return $this->queryBoolean('ST_Overlaps', $a, $b);
2✔
367
    }
368

369
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
370
    {
371
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
4✔
372
    }
373

374
    public function locateAlong(Geometry $g, float $mValue) : Geometry
375
    {
376
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
2✔
377
    }
378

379
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
380
    {
381
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
2✔
382
    }
383

384
    public function distance(Geometry $a, Geometry $b) : float
385
    {
386
        return $this->queryFloat('ST_Distance', $a, $b);
5✔
387
    }
388

389
    public function buffer(Geometry $g, float $distance) : Geometry
390
    {
391
        return $this->queryGeometry('ST_Buffer', $g, $distance);
3✔
392
    }
393

394
    public function convexHull(Geometry $g) : Geometry
395
    {
396
        return $this->queryGeometry('ST_ConvexHull', $g);
3✔
397
    }
398

399
    public function symDifference(Geometry $a, Geometry $b) : Geometry
400
    {
401
        return $this->queryGeometry('ST_SymDifference', $a, $b);
1✔
402
    }
403

404
    public function snapToGrid(Geometry $g, float $size) : Geometry
405
    {
406
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
4✔
407
    }
408

409
    public function simplify(Geometry $g, float $tolerance) : Geometry
410
    {
411
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
2✔
412
    }
413

414
    public function maxDistance(Geometry $a, Geometry $b) : float
415
    {
416
        return $this->queryFloat('ST_MaxDistance', $a, $b);
3✔
417
    }
418

419
    public function transform(Geometry $g, int $srid) : Geometry
420
    {
421
        return $this->queryGeometry('ST_Transform', $g, $srid);
×
422
    }
423

424
    public function split(Geometry $g, Geometry $blade) : Geometry
425
    {
426
        return $this->queryGeometry('ST_Split', $g, $blade);
4✔
427
    }
428
}
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