Passed
Push — master ( 3df3e6...fadf4c )
by Hennik
07:00
created

SpatialTrait::isColumnAllowed()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace LaravelSpatial\Eloquent;
4
5
use Exception;
6
use GeoJson\GeoJson;
7
use GeoJSON\Geometry\Geometry;
8
use geoPHP\geoPHP;
9
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
10
use Illuminate\Database\MySqlConnection;
11
use Illuminate\Database\PostgresConnection;
12
use LaravelSpatial\Exceptions\SpatialFieldsNotDefinedException;
13
use LaravelSpatial\Exceptions\SpatialParseException;
14
use LaravelSpatial\Exceptions\UnknownSpatialRelationFunction;
15
16
/**
17
 * Trait SpatialTrait.
18
 *
19
 * @property array $attributes
20
 * @method static static|EloquentBuilder distance($geometryColumn, $geometry, $distance)
21
 * @method static static|EloquentBuilder distanceValue($geometryColumn, $geometry)
22
 * @method static static|EloquentBuilder distanceExcludingSelf($geometryColumn, $geometry, $distance)
23
 * @method static static|EloquentBuilder distanceSphere($geometryColumn, $geometry, $distance)
24
 * @method static static|EloquentBuilder distanceSphereValue($geometryColumn, $geometry)
25
 * @method static static|EloquentBuilder distanceSphereExcludingSelf($geometryColumn, $geometry, $distance)
26
 * @method static static|EloquentBuilder comparison($geometryColumn, $geometry, $relationship)
27
 * @method static static|EloquentBuilder within($geometryColumn, $polygon)
28
 * @method static static|EloquentBuilder crosses($geometryColumn, $geometry)
29
 * @method static static|EloquentBuilder contains($geometryColumn, $geometry)
30
 * @method static static|EloquentBuilder disjoint($geometryColumn, $geometry)
31
 * @method static static|EloquentBuilder equals($geometryColumn, $geometry)
32
 * @method static static|EloquentBuilder intersects($geometryColumn, $geometry)
33
 * @method static static|EloquentBuilder overlaps($geometryColumn, $geometry)
34
 * @method static static|EloquentBuilder doesTouch($geometryColumn, $geometry)
35
 */
36
trait SpatialTrait
37
{
38
    /*
39
     * The attributes that are spatial representations.
40
     * To use this Trait, add the following array to the model class
41
     *
42
     * @var array
43
     *
44
     * protected $spatialFields = [];
45
     */
46
47
    /**
48
     * @var array
49
     */
50
    public $geometries = [];
51
52
    /**
53
     * @var array
54
     */
55
    protected $stRelations = [
56
        'Within',
57
        'Crosses',
58
        'Contains',
59
        'Disjoint',
60
        'Equals',
61
        'Intersects',
62
        'Overlaps',
63
        'Touches',
64
    ];
65
66
    /**
67
     * Create a new Eloquent query builder for the model.
68
     *
69
     * @param  \Illuminate\Database\Query\Builder $query
70
     *
71
     * @return \LaravelSpatial\Eloquent\Builder
72
     */
73 48
    public function newEloquentBuilder($query)
74
    {
75 48
        return new Builder($query);
76
    }
77
78
    /**
79
     * @inheritDoc
80
     */
81 11
    public function setRawAttributes(array $attributes, $sync = false)
82
    {
83 11
        $spatial_fields = $this->getSpatialFields();
84
85 11
        foreach ($attributes as $attribute => &$value) {
86 11
            if (is_string($value) && in_array($attribute, $spatial_fields, true) && strlen($value) >= 15) {
87 11
                $connection = $this->getConnection();
88
89
                // MySQL adds 4 NULL bytes at the start of the binary
90 11
                if ($connection instanceof MySqlConnection && strpos($value, "\0\0\0\0") === 0) {
91 5
                    $value = substr($value, 4);
92 6
                } elseif ($connection instanceof PostgresConnection) {
93 5
                    $value = pack('H*', $value);
94
                }
95
96
                try {
97 11
                    $value = GeoJson::jsonUnserialize(json_decode(geoPHP::load($value, 'wkb')->out('json'), false));
98
                } catch (Exception $e) {
99
                    throw new SpatialParseException("Can't parse WKB {$value}: {$e->getMessage()}", $e->getCode(), $e);
100
                }
101
            }
102
        }
103
104 11
        return parent::setRawAttributes($attributes, $sync);
105
    }
106
107
    /**
108
     * @return array
109
     */
110 50
    public function getSpatialFields(): array
111
    {
112 50
        if (property_exists($this, 'spatialFields') && !empty($this->spatialFields)) {
113 47
            return $this->spatialFields;
114
        }
115
116 3
        throw new SpatialFieldsNotDefinedException(__CLASS__ . ' has to define $spatialFields');
117
    }
118
119
    /**
120
     * @param \GeoJSON\Geometry\Geometry $value
121
     *
122
     * @return string
123
     */
124 46
    protected function toWkt(Geometry $value): string
125
    {
126
        try {
127 46
            $wkt = geoPHP::load(json_decode(json_encode($value->jsonSerialize()), false), 'json')->out('wkt');
128
        } catch (Exception $e) {
129
            throw new SpatialParseException('Unable to data to geometry.', 0, $e);
130
        }
131
132 46
        return ($this->getConnection() instanceof PostgresConnection ? 'SRID=4326;' : '') . $wkt;
133
    }
134
135
    /**
136
     * @param \Illuminate\Database\Eloquent\Builder $query
137
     *
138
     * @return bool
139
     */
140 31
    protected function performInsert(EloquentBuilder $query)
141
    {
142 31
        foreach ($this->attributes as $key => $value) {
143 31
            if ($value instanceof Geometry && $this->isColumnAllowed($key)) {
144 29
                $this->geometries[$key] = $value; // Preserve the geometry objects prior to the insert
145 29
                $this->attributes[$key] = $this->getConnection()->raw("ST_GeomFromText('{$this->toWkt($value)}')");
146
            }
147
        }
148
149 29
        $insert = parent::performInsert($query);
150
151 29
        foreach ($this->geometries as $key => $value) {
152 29
            $this->attributes[$key] = $value; // Retrieve the geometry objects so they can be used in the model
153
        }
154
155 29
        return $insert; // Return the result of the parent insert
156
    }
157
158
    /**
159
     * @param $geometryColumn
160
     *
161
     * @return bool
162
     */
163 48
    public function isColumnAllowed($geometryColumn): bool
164
    {
165 48
        if (!in_array($geometryColumn, $this->getSpatialFields(), true)) {
166
            throw new SpatialFieldsNotDefinedException(sprintf('%s is not a valid spatial column.', $geometryColumn));
167
        }
168
169 46
        return true;
170
    }
171
172
    /**
173
     * @param \Illuminate\Database\Eloquent\Builder $query
174
     * @param $geometryColumn
175
     * @param $geometry
176
     * @param $distance
177
     *
178
     * @return \Illuminate\Database\Eloquent\Builder
179
     */
180 4
    public function scopeDistance($query, $geometryColumn, $geometry, $distance): EloquentBuilder
181
    {
182 4
        if ($this->isColumnAllowed($geometryColumn)) {
183 4
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
184 4
            $query->whereRaw("ST_Distance({$geometryColumn}, ST_GeomFromText(?)) <= ?", [
185 4
                $this->toWkt($geometry),
186 4
                $distance,
187
            ]);
188
        }
189
190 4
        return $query;
191
    }
192
193
    /**
194
     * @param \Illuminate\Database\Eloquent\Builder $query
195
     * @param $geometryColumn
196
     * @param $geometry
197
     * @param $distance
198
     *
199
     * @return \Illuminate\Database\Eloquent\Builder
200
     */
201 3
    public function scopeDistanceExcludingSelf($query, $geometryColumn, $geometry, $distance): EloquentBuilder
202
    {
203 3
        if ($this->isColumnAllowed($geometryColumn)) {
204 3
            $query = $this->scopeDistance($query, $geometryColumn, $geometry, $distance);
205
206 3
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
207 3
            $query->whereRaw("ST_Distance({$geometryColumn}, ST_GeomFromText(?)) != 0", [
208 3
                $this->toWkt($geometry),
209
            ]);
210
        }
211
212 3
        return $query;
213
    }
214
215
    /**
216
     * @param \Illuminate\Database\Eloquent\Builder $query
217
     * @param $geometryColumn
218
     * @param $geometry
219
     *
220
     * @return \Illuminate\Database\Eloquent\Builder
221
     */
222 4
    public function scopeDistanceValue($query, $geometryColumn, $geometry): EloquentBuilder
223
    {
224 4
        if ($this->isColumnAllowed($geometryColumn)) {
225 4
            $columns = $query->getQuery()->columns;
226
227 4
            if (!$columns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
228 3
                $query->select('*');
229
            }
230
231 4
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
232 4
            $query->selectRaw("ST_Distance({$geometryColumn}, ST_GeomFromText(?)) as distance", [
233 4
                $this->toWkt($geometry),
234
            ]);
235
        }
236
237 4
        return $query;
238
    }
239
240
    /**
241
     * @param \Illuminate\Database\Eloquent\Builder $query
242
     * @param $geometryColumn
243
     * @param $geometry
244
     * @param $distance
245
     *
246
     * @return \Illuminate\Database\Eloquent\Builder
247
     */
248 4
    public function scopeDistanceSphere($query, $geometryColumn, $geometry, $distance): EloquentBuilder
249
    {
250 4
        $distFunc = $this->getConnection() instanceof PostgresConnection ? 'ST_DistanceSphere' : 'ST_Distance_Sphere';
251
252 4
        if ($this->isColumnAllowed($geometryColumn)) {
253 4
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
254 4
            $query->whereRaw("{$distFunc}({$geometryColumn}, ST_GeomFromText(?)) <= ?", [
255 4
                $this->toWkt($geometry),
256 4
                $distance,
257
            ]);
258
        }
259
260 4
        return $query;
261
    }
262
263
    /**
264
     * @param \Illuminate\Database\Eloquent\Builder $query
265
     * @param $geometryColumn
266
     * @param $geometry
267
     * @param $distance
268
     *
269
     * @return \Illuminate\Database\Eloquent\Builder
270
     */
271 3
    public function scopeDistanceSphereExcludingSelf($query, $geometryColumn, $geometry, $distance): EloquentBuilder
272
    {
273 3
        $distFunc = $this->getConnection() instanceof PostgresConnection ? 'ST_DistanceSphere' : 'ST_Distance_Sphere';
274
275 3
        if ($this->isColumnAllowed($geometryColumn)) {
276 3
            $query = $this->scopeDistanceSphere($query, $geometryColumn, $geometry, $distance);
277
278 3
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
279 3
            $query->whereRaw("{$distFunc}({$geometryColumn}, ST_GeomFromText(?)) != 0", [
280 3
                $this->toWkt($geometry),
281
            ]);
282
        }
283
284 3
        return $query;
285
    }
286
287
    /**
288
     * @param \Illuminate\Database\Eloquent\Builder $query
289
     * @param $geometryColumn
290
     * @param $geometry
291
     *
292
     * @return \Illuminate\Database\Eloquent\Builder
293
     */
294 4
    public function scopeDistanceSphereValue($query, $geometryColumn, $geometry): EloquentBuilder
295
    {
296 4
        $distFunc = $this->getConnection() instanceof PostgresConnection ? 'ST_DistanceSphere' : 'ST_Distance_Sphere';
297
298 4
        if ($this->isColumnAllowed($geometryColumn)) {
299 4
            $columns = $query->getQuery()->columns;
300
301 4
            if (!$columns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
302 3
                $query->select('*');
303
            }
304
305 4
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
306 4
            $query->selectRaw("{$distFunc}({$geometryColumn}, ST_GeomFromText(?)) as distance", [
307 4
                $this->toWkt($geometry),
308
            ]);
309
        }
310
311 4
        return $query;
312
    }
313
314
    /**
315
     * @param \Illuminate\Database\Eloquent\Builder $query
316
     * @param $geometryColumn
317
     * @param $geometry
318
     * @param $relationship
319
     *
320
     * @return \Illuminate\Database\Eloquent\Builder
321
     */
322 9
    public function scopeComparison($query, $geometryColumn, $geometry, $relationship): EloquentBuilder
323
    {
324 9
        if ($this->isColumnAllowed($geometryColumn)) {
325 9
            $relationship = ucfirst(strtolower($relationship));
326
327 9
            if (!in_array($relationship, $this->stRelations, true)) {
328
                throw new UnknownSpatialRelationFunction($relationship);
329
            }
330
331 9
            $geometryColumn .= $this->getConnection() instanceof PostgresConnection ? '::geometry' : '';
332 9
            $query->whereRaw("ST_{$relationship}(`{$geometryColumn}`, ST_GeomFromText(?))", [
333 9
                $this->toWkt($geometry),
334
            ]);
335
        }
336
337 9
        return $query;
338
    }
339
340
    /**
341
     * @param \Illuminate\Database\Eloquent\Builder $query
342
     * @param $geometryColumn
343
     * @param $polygon
344
     *
345
     * @return \Illuminate\Database\Eloquent\Builder
346
     */
347 1
    public function scopeWithin($query, $geometryColumn, $polygon): EloquentBuilder
348
    {
349 1
        return $this->scopeComparison($query, $geometryColumn, $polygon, 'within');
350
    }
351
352
    /**
353
     * @param \Illuminate\Database\Eloquent\Builder $query
354
     * @param $geometryColumn
355
     * @param $geometry
356
     *
357
     * @return \Illuminate\Database\Eloquent\Builder
358
     */
359 1
    public function scopeCrosses($query, $geometryColumn, $geometry): EloquentBuilder
360
    {
361 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'crosses');
362
    }
363
364
    /**
365
     * @param \Illuminate\Database\Eloquent\Builder $query
366
     * @param $geometryColumn
367
     * @param $geometry
368
     *
369
     * @return \Illuminate\Database\Eloquent\Builder
370
     */
371 1
    public function scopeContains($query, $geometryColumn, $geometry): EloquentBuilder
372
    {
373 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'contains');
374
    }
375
376
    /**
377
     * @param \Illuminate\Database\Eloquent\Builder $query
378
     * @param $geometryColumn
379
     * @param $geometry
380
     *
381
     * @return \Illuminate\Database\Eloquent\Builder
382
     */
383 1
    public function scopeDisjoint($query, $geometryColumn, $geometry): EloquentBuilder
384
    {
385 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'disjoint');
386
    }
387
388
    /**
389
     * @param \Illuminate\Database\Eloquent\Builder $query
390
     * @param $geometryColumn
391
     * @param $geometry
392
     *
393
     * @return \Illuminate\Database\Eloquent\Builder
394
     */
395 1
    public function scopeEquals($query, $geometryColumn, $geometry): EloquentBuilder
396
    {
397 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'equals');
398
    }
399
400
    /**
401
     * @param \Illuminate\Database\Eloquent\Builder $query
402
     * @param $geometryColumn
403
     * @param $geometry
404
     *
405
     * @return \Illuminate\Database\Eloquent\Builder
406
     */
407 1
    public function scopeIntersects($query, $geometryColumn, $geometry): EloquentBuilder
408
    {
409 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'intersects');
410
    }
411
412
    /**
413
     * @param \Illuminate\Database\Eloquent\Builder $query
414
     * @param $geometryColumn
415
     * @param $geometry
416
     *
417
     * @return \Illuminate\Database\Eloquent\Builder
418
     */
419 1
    public function scopeOverlaps($query, $geometryColumn, $geometry): EloquentBuilder
420
    {
421 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'overlaps');
422
    }
423
424
    /**
425
     * @param \Illuminate\Database\Eloquent\Builder $query
426
     * @param $geometryColumn
427
     * @param $geometry
428
     *
429
     * @return \Illuminate\Database\Eloquent\Builder
430
     */
431 1
    public function scopeDoesTouch($query, $geometryColumn, $geometry): EloquentBuilder
432
    {
433 1
        return $this->scopeComparison($query, $geometryColumn, $geometry, 'touches');
434
    }
435
}
436