CollectionMapWidget::resolveDataSourceFilter()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 1
dl 0
loc 22
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Admin\Widget;
4
5
use ArrayAccess;
6
use RuntimeException;
7
use InvalidArgumentException;
8
use UnexpectedValueException;
9
10
// From Pimple
11
use Pimple\Container;
12
13
// From 'charcoal-core'
14
use Charcoal\Model\ModelInterface;
15
16
// From 'charcoal-admin'
17
use Charcoal\Admin\AdminWidget;
18
use Charcoal\Admin\Support\HttpAwareTrait;
19
use Charcoal\Admin\Ui\CollectionContainerInterface;
20
use Charcoal\Admin\Ui\CollectionContainerTrait;
21
22
/**
23
 * Displays a collection of models on a map.
24
 */
25
class CollectionMapWidget extends AdminWidget implements CollectionContainerInterface
26
{
27
    use CollectionContainerTrait;
28
    use HttpAwareTrait;
29
30
    /**
31
     * The API key for the mapping service.
32
     *
33
     * @var array
34
     */
35
    private $apiKey;
36
37
    /**
38
     * @var \Charcoal\Model\AbstractModel[] $mapObjects
39
     */
40
    private $mapObjects;
41
42
    /**
43
     * The ident of the object's property for the latitude.
44
     * @var string $latProperty
45
     */
46
    private $latProperty;
47
48
    /**
49
     * The ident of the object's property for the longitude.
50
     * @var string $latProperty
51
     */
52
    private $lonProperty;
53
54
    /**
55
     * @var string $polygonProperty
56
     */
57
    private $polygonProperty;
58
59
    /**
60
     * @var string $pathProperty
61
     */
62
    private $pathProperty;
63
64
    /**
65
     * @var string $infoboxTemplate
66
     */
67
    public $infoboxTemplate = '';
68
    /**
69
     * @param array $data The widget data.
70
     * @return TableWidget Chainable
71
     */
72
    public function setData(array $data)
73
    {
74
        parent::setData($data);
75
76
        $this->mergeDataSources($data);
77
78
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Admin\Widget\CollectionMapWidget which is incompatible with the documented return type Charcoal\Admin\Widget\TableWidget.
Loading history...
79
    }
80
81
    /**
82
     * Sets the API key for the mapping service.
83
     *
84
     * @param  string $key An API key.
85
     * @return self
86
     */
87
    public function setApiKey($key)
88
    {
89
        $this->apiKey = $key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $key of type string is incompatible with the declared type array of property $apiKey.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
90
91
        return $this;
92
    }
93
94
    /**
95
     * Retrieve API key for the mapping service.
96
     *
97
     * @return string
98
     */
99
    public function apiKey()
100
    {
101
        return $this->apiKey;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->apiKey returns the type array which is incompatible with the documented return type string.
Loading history...
102
    }
103
104
    /**
105
     * @param string $p The latitude property ident.
106
     * @return MapWidget Chainable
107
     */
108
    public function setLatProperty($p)
109
    {
110
        $this->latProperty = $p;
111
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Admin\Widget\CollectionMapWidget which is incompatible with the documented return type Charcoal\Admin\Widget\MapWidget.
Loading history...
112
    }
113
114
    /**
115
     * @return string
116
     */
117
    public function latProperty()
118
    {
119
        return $this->latProperty;
120
    }
121
122
    /**
123
     * @param string $p The longitude property ident.
124
     * @return MapWidget Chainable
125
     */
126
    public function setLonProperty($p)
127
    {
128
        $this->lonProperty = $p;
129
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Admin\Widget\CollectionMapWidget which is incompatible with the documented return type Charcoal\Admin\Widget\MapWidget.
Loading history...
130
    }
131
132
    /**
133
     * @return string
134
     */
135
    public function lonProperty()
136
    {
137
        return $this->lonProperty;
138
    }
139
140
    /**
141
     * @param string $p The polygon property ident.
142
     * @return MapWidget Chainable
143
     */
144
    public function setPolygonProperty($p)
145
    {
146
        $this->polygonProperty = $p;
147
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Admin\Widget\CollectionMapWidget which is incompatible with the documented return type Charcoal\Admin\Widget\MapWidget.
Loading history...
148
    }
149
150
    /**
151
     * @return string
152
     */
153
    public function polygonProperty()
154
    {
155
        return $this->polygonProperty;
156
    }
157
158
    /**
159
     * @param string $p The path property ident.
160
     * @return MapWidget Chainable
161
     */
162
    public function setPathProperty($p)
163
    {
164
        $this->pathProperty = $p;
165
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Charcoal\Admin\Widget\CollectionMapWidget which is incompatible with the documented return type Charcoal\Admin\Widget\MapWidget.
Loading history...
166
    }
167
168
    /**
169
     * @return string
170
     */
171
    public function pathProperty()
172
    {
173
        return $this->pathProperty;
174
    }
175
176
    /**
177
     * @param string $template The infobox template ident.
178
     * @return CollectionMapWidget Chainable
179
     */
180
    public function setInfoboxTemplate($template)
181
    {
182
        $this->infoboxTemplate = $template;
183
        return $this;
184
    }
185
186
    /**
187
     * @return string
188
     */
189
    public function infoboxTemplate()
190
    {
191
        return $this->infoboxTemplate;
192
    }
193
194
    /**
195
     * Return all the objs with geographical information
196
     *
197
     * @throws UnexpectedValueException If the object type of the colletion is missing.
198
     * @return Collection
199
     */
200
    public function mapObjects()
201
    {
202
        if ($this->mapObjects === null) {
203
            $objType = $this->objType();
204
            if (!$objType) {
205
                throw new UnexpectedValueException(sprintf(
206
                    '%1$s cannot create collection map. Object type is not defined.',
207
                    get_class($this)
208
                ));
209
            }
210
211
            $loader = $this->collectionLoader();
212
            $loader->setModel($this->proto());
213
214
            $collectionConfig = $this->collectionConfig();
215
            if (is_array($collectionConfig) && !empty($collectionConfig)) {
216
                unset($collectionConfig['properties']);
217
                $loader->setData($collectionConfig);
218
            }
219
220
            $callback = function(&$obj) {
221
                $obj->mapInfoboxTemplate = $this->infoboxTemplate();
222
223
                if ($this->latProperty() && $this->latProperty()) {
224
                    $obj->mapShowMarker = true;
225
                    $obj->mapLat = $this->getPropertyValue($obj, $this->latProperty());
226
                    $obj->mapLon = $this->getPropertyValue($obj, $this->lonProperty());
0 ignored issues
show
Bug introduced by
Accessing mapLon on the interface Charcoal\Model\ModelInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
227
228
                    if (!$obj->mapLat || !$obj->mapLon) {
0 ignored issues
show
Bug introduced by
Accessing mapLat on the interface Charcoal\Model\ModelInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
229
                        $obj = null;
230
                    }
231
                } else {
232
                    $obj->mapShowMarker = false;
233
                }
234
235
                if ($this->pathProperty()) {
236
                    $mapPath = $this->getPropertyValue($obj, $this->pathProperty());
237
                    if ($mapPath) {
238
                        $obj->mapShowPath = true;
0 ignored issues
show
Bug introduced by
Accessing mapShowPath on the interface Charcoal\Model\ModelInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
239
                        // Same type of coords.
240
                        $obj->mapPath = $this->formatPolygon($mapPath);
0 ignored issues
show
Bug introduced by
Accessing mapPath on the interface Charcoal\Model\ModelInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
241
242
                        if (!$obj->mapPath) {
243
                            $obj = null;
244
                        }
245
                    } else {
246
                        $obj->mapShowPath = false;
247
                    }
248
                }
249
250
                if ($this->polygonProperty()) {
251
                    $mapPolygon = $this->getPropertyValue($obj, $this->polygonProperty());
252
                    if ($mapPolygon) {
253
                        $obj->mapShowPolygon = true;
0 ignored issues
show
Bug introduced by
Accessing mapShowPolygon on the interface Charcoal\Model\ModelInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
254
                        $obj->mapPolygon = $this->formatPolygon($mapPolygon);
0 ignored issues
show
Bug introduced by
Accessing mapPolygon on the interface Charcoal\Model\ModelInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
255
256
                        if (!$obj->mapPolygon) {
257
                            $obj = null;
258
                        }
259
                    } else {
260
                        $obj->mapShowPolygon = false;
261
                    }
262
                }
263
            };
264
265
            $loader->setCallback($callback->bindTo($this));
266
267
            $this->mapObjects = $loader->load();
0 ignored issues
show
Documentation Bug introduced by
It seems like $loader->load() of type Charcoal\Model\ModelInterface[]&ArrayAccess is incompatible with the declared type Charcoal\Model\AbstractModel[] of property $mapObjects.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
268
        }
269
270
        foreach ($this->mapObjects as $obj) {
271
            $GLOBALS['widget_template'] = $obj->mapInfoboxTemplate;
272
            yield $obj;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $obj returns the type Generator which is incompatible with the documented return type Charcoal\Admin\Widget\Collection.
Loading history...
273
            $GLOBALS['widget_template'] = '';
274
        }
275
    }
276
277
    /**
278
     * @return boolean
279
     */
280
    public function showInfobox()
281
    {
282
        return ($this->infoboxTemplate != '');
283
    }
284
285
286
    /**
287
     * Fetch metadata from the current request.
288
     *
289
     * @return array
290
     */
291
    public function dataFromRequest()
292
    {
293
        return $this->httpRequest()->getParams($this->acceptedRequestData());
0 ignored issues
show
Bug introduced by
The method getParams() does not exist on Psr\Http\Message\RequestInterface. It seems like you code against a sub-type of Psr\Http\Message\RequestInterface such as Slim\Http\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

293
        return $this->httpRequest()->/** @scrutinizer ignore-call */ getParams($this->acceptedRequestData());
Loading history...
294
    }
295
296
    /**
297
     * Retrieve the accepted metadata from the current request.
298
     *
299
     * @return array
300
     */
301
    public function acceptedRequestData()
302
    {
303
        return [
304
            'obj_type',
305
            'obj_id',
306
            'collection_ident',
307
        ];
308
    }
309
310
    /**
311
     * Fetch metadata from the current object type.
312
     *
313
     * @return array
314
     */
315
    public function dataFromObject()
316
    {
317
        $proto         = $this->proto();
318
        $objMetadata   = $proto->metadata();
319
        $adminMetadata = (isset($objMetadata['admin']) ? $objMetadata['admin'] : null);
320
321
        if (empty($adminMetadata['lists'])) {
322
            return [];
323
        }
324
325
        $collectionIdent = $this->collectionIdent();
326
        if (!$collectionIdent) {
327
            $collectionIdent = $this->collectionIdentFallback();
328
        }
329
330
        if ($collectionIdent && $proto->view()) {
331
            $collectionIdent = $proto->render($collectionIdent);
332
        }
333
334
        if (!$collectionIdent) {
335
            return [];
336
        }
337
338
        if (isset($adminMetadata['lists'][$collectionIdent])) {
339
            $objListData = $adminMetadata['lists'][$collectionIdent];
340
        } else {
341
            $objListData = [];
342
        }
343
344
        $collectionConfig = [];
345
346
        if (isset($objListData['orders']) && isset($adminMetadata['list_orders'])) {
347
            $extraOrders = array_intersect(
348
                array_keys($adminMetadata['list_orders']),
349
                array_keys($objListData['orders'])
350
            );
351
            foreach ($extraOrders as $listIdent) {
352
                $collectionConfig['orders'][$listIdent] = array_replace_recursive(
353
                    $adminMetadata['list_orders'][$listIdent],
354
                    $objListData['orders'][$listIdent]
355
                );
356
            }
357
        }
358
359
        if (isset($objListData['filters']) && isset($adminMetadata['list_filters'])) {
360
            $extraFilters = array_intersect(
361
                array_keys($adminMetadata['list_filters']),
362
                array_keys($objListData['filters'])
363
            );
364
            foreach ($extraFilters as $listIdent) {
365
                $collectionConfig['filters'][$listIdent] = array_replace_recursive(
366
                    $adminMetadata['list_filters'][$listIdent],
367
                    $objListData['filters'][$listIdent]
368
                );
369
            }
370
        }
371
372
        if ($collectionConfig) {
373
            $this->mergeCollectionConfig($collectionConfig);
374
        }
375
376
        return $objListData;
377
    }
378
379
    /**
380
     * Inject dependencies from a DI Container.
381
     *
382
     * @param  Container $container A dependencies container instance.
383
     * @return void
384
     */
385
    protected function setDependencies(Container $container)
386
    {
387
        parent::setDependencies($container);
388
389
        // Satisfies HttpAwareTrait dependencies
390
        $this->setHttpRequest($container['request']);
391
392
        $this->setCollectionLoader($container['model/collection/loader']);
393
394
        if (isset($container['admin/config']['apis.google.map.key'])) {
395
            $this->setApiKey($container['admin/config']['apis.google.map.key']);
396
        } elseif (isset($container['config']['apis.google.map.key'])) {
397
            $this->setApiKey($container['config']['apis.google.map.key']);
398
        }
399
    }
400
401
402
    /**
403
     * @param  ModelInterface $obj The object with the latitude property.
404
     * @param  string         $key The property to retrieve.
405
     * @throws InvalidArgumentException If the data key is missing.
406
     * @return mixed
407
     */
408
    protected function getPropertyValue(ModelInterface $obj, $key)
409
    {
410
        if (!is_string($key) || $key === '') {
0 ignored issues
show
introduced by
The condition is_string($key) is always true.
Loading history...
411
            throw new InvalidArgumentException('Missing latitude property.');
412
        }
413
414
        if (isset($obj[$key])) {
415
            return $obj[$key];
416
        }
417
418
        $data     = null;
419
        $segments = explode('.', $key);
420
        if (count($segments) > 1) {
421
            $data = $obj;
422
            foreach (explode('.', $key) as $segment) {
423
                $accessible = is_array($data) || $data instanceof ArrayAccess;
424
                if ($data instanceof ArrayAccess) {
425
                    $exists = $data->offsetExists($segment);
426
                } else {
427
                    $exists = array_key_exists($segment, $data);
428
                }
429
430
                if ($accessible && $exists) {
431
                    $data = $data[$segment];
432
                } else {
433
                    return null;
434
                }
435
            }
436
        }
437
438
        return $data;
439
    }
440
441
    /**
442
     * Retrieve the default data source filters (when setting data on an entity).
443
     *
444
     * Note: Adapted from {@see \Slim\CallableResolver}.
445
     *
446
     * @link   https://github.com/slimphp/Slim/blob/3.x/Slim/CallableResolver.php
447
     * @param  mixed $toResolve A callable used when merging data.
448
     * @return callable|null
449
     */
450
    protected function resolveDataSourceFilter($toResolve)
451
    {
452
        if (is_string($toResolve)) {
453
            $model = $this->proto();
454
455
            $resolved = [ $model, $toResolve ];
456
457
            // Check for Slim callable
458
            $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
459
            if (preg_match($callablePattern, $toResolve, $matches)) {
460
                $class = $matches[1];
461
                $method = $matches[2];
462
463
                if ($class === 'parent') {
464
                    $resolved = [ $model, $class.'::'.$method ];
465
                }
466
            }
467
468
            $toResolve = $resolved;
469
        }
470
471
        return parent::resolveDataSourceFilter($toResolve);
472
    }
473
474
475
    /**
476
     * @param mixed $rawPolygon The polygon information.
477
     * @return string
478
     */
479
    private function formatPolygon($rawPolygon)
480
    {
481
        if (is_string($rawPolygon)) {
482
            $polygon = explode(' ', $rawPolygon);
483
            $ret = [];
484
            foreach ($polygon as $poly) {
485
                $coords = explode(',', $poly);
486
                if (count($coords) < 2) {
487
                    continue;
488
                }
489
                $ret[] = [(float)$coords[0], (float)$coords[1]];
490
            }
491
        } else {
492
            $ret = $rawPolygon;
493
        }
494
        return json_encode($ret, true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type integer expected by parameter $options of json_encode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

494
        return json_encode($ret, /** @scrutinizer ignore-type */ true);
Loading history...
495
    }
496
}
497