Test Failed
Pull Request — master (#4)
by
unknown
11:21
created

RelationWidget::withBaseUrl()   C

Complexity

Conditions 7
Paths 2

Size

Total Lines 43
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 22
nc 2
nop 0
1
<?php
2
3
namespace Charcoal\Admin\Widget;
4
5
use ArrayIterator;
6
use RuntimeException;
7
use InvalidArgumentException;
8
9
// From Pimple
10
use Pimple\Container;
11
12
// From 'bobthecow/mustache.php'
13
use Mustache_LambdaHelper as LambdaHelper;
14
15
// From 'charcoal-config'
16
use Charcoal\Config\ConfigurableInterface;
17
18
// From 'charcoal-factory'
19
use Charcoal\Factory\FactoryInterface;
20
21
// From 'charcoal-admin'
22
use Charcoal\Admin\AdminWidget;
23
use Charcoal\Admin\Ui\ObjectContainerInterface;
24
use Charcoal\Admin\Ui\ObjectContainerTrait;
25
26
// From 'charcoal-cms'
27
use Charcoal\Relation\Pivot;
28
use Charcoal\Relation\Traits\ConfigurableRelationTrait;
29
30
/**
31
 * The widget for displaying relations as Pivots.
32
 */
33
class RelationWidget extends AdminWidget implements
34
    ConfigurableInterface,
35
    ObjectContainerInterface
36
{
37
    use ConfigurableRelationTrait;
38
    use ObjectContainerTrait {
39
        ObjectContainerTrait::createOrLoadObj as createOrCloneOrLoadObj;
40
    }
41
42
    /**
43
     * The widget's title.
44
     *
45
     * @var string[]
46
     */
47
    private $title;
48
49
    /**
50
     * The Pivot target object types.
51
     *
52
     * @var array
53
     */
54
    protected $targetObjectTypes;
55
56
    /**
57
     * The Pivot grouping ident.
58
     *
59
     * @var string
60
     */
61
    protected $group;
62
63
    /**
64
     * Track the state of data merging.
65
     *
66
     * @var boolean
67
     */
68
    private $isMergingData = false;
69
70
    /**
71
     * Store the factory instance for the current class.
72
     *
73
     * @var FactoryInterface
74
     */
75
    private $widgetFactory;
76
77
    /**
78
     * Label for the relation dialog.
79
     *
80
     * @var \Charcoal\Translator\Translation|string|null
81
     */
82
    private $dialogTitle;
83
84
    /**
85
     * Store a Pivot model.
86
     *
87
     * @var ModelInterface
88
     */
89
    private $pivotObjProto;
90
91
    /**
92
     * Inject dependencies from a DI Container.
93
     *
94
     * @param Container $container A dependencies container instance.
95
     * @return void
96
     */
97
    public function setDependencies(Container $container)
98
    {
99
        parent::setDependencies($container);
100
101
        $this->setModelFactory($container['model/factory']);
102
        $this->setWidgetFactory($container['widget/factory']);
103
    }
104
105
    /**
106
     * Retrieve the widget factory.
107
     *
108
     * @throws RuntimeException If the widget factory was not previously set.
109
     * @return FactoryInterface
110
     */
111
    public function widgetFactory()
112
    {
113
        if (!isset($this->widgetFactory)) {
114
            throw new RuntimeException(
115
                sprintf('Widget Factory is not defined for "%s"', get_class($this))
116
            );
117
        }
118
119
        return $this->widgetFactory;
120
    }
121
122
    /**
123
     * Set an widget factory.
124
     *
125
     * @param FactoryInterface $factory The factory to create widgets.
126
     * @return self
127
     */
128
    protected function setWidgetFactory(FactoryInterface $factory)
129
    {
130
        $this->widgetFactory = $factory;
131
132
        return $this;
133
    }
134
135
    /**
136
     * Retrieve the widget's Pivot grouping.
137
     *
138
     * @return string
139
     */
140
    public function group()
141
    {
142
        return $this->group;
143
    }
144
145
    /**
146
     * Set the widget's Pivot grouping.
147
     *
148
     * @param string $group The object group.
149
     * @return self
150
     */
151
    public function setGroup($group)
152
    {
153
        $this->group = $group;
154
155
        return $this;
156
    }
157
158
    /**
159
     * Retrieve the widget's Pivot target object types.
160
     *
161
     * @return array
162
     */
163
    public function targetObjectTypes()
164
    {
165
        return $this->obj()->targetObjectTypes($this->group());
166
    }
167
168
    /**
169
     * Retrieve a hollow Pivot Model.
170
     *
171
     * @return ModelInterface
172
     */
173
    public function pivotObjProto()
174
    {
175
        if ($this->pivotObjProto === null) {
176
            $this->pivotObjProto = $this->modelFactory()->get(Pivot::class);
177
        }
178
179
        return $this->pivotObjProto;
180
    }
181
182
    /**
183
     * Retrieve the Pivot object type.
184
     *
185
     * @return string
186
     */
187
    public function pivotObjType()
188
    {
189
        return $this->pivotObjProto()->objType();
190
    }
191
192
    /**
193
     * Parse the target object type metadata.
194
     *
195
     * @return array
196
     */
197
    private function parsedTargetObjectTypes()
198
    {
199
        $objectTypes = $this->targetObjectTypes();
200
201
        if (!$this->isMergingData) {
202
            $objectTypes = $this->mergePresetTargetObjectTypes($objectTypes);
203
        }
204
205
        if (empty($objectTypes) || is_string($objectTypes)) {
206
            return false;
207
        }
208
209
        $out = [];
210
        foreach ($objectTypes as $type => $metadata) {
211
            $label      = '';
212
            $filters    = [];
213
            $orders     = [];
214
            $numPerPage = 0;
215
            $page       = 1;
216
            $options    = [ 'label', 'heading', 'filters', 'orders', 'num_per_page', 'page' ];
217
            $data       = isset($metadata['data']) ? $metadata['data'] : array_diff_key($metadata, $options);
218
219
            // Disable a linked model
220
            if (isset($metadata['active']) && !$metadata['active']) {
221
                continue;
222
            }
223
224
            // Useful for replacing a pre-defined object type
225
            if (isset($metadata['object_type'])) {
226
                $type = $metadata['object_type'];
227
            } else {
228
                $metadata['object_type'] = $type;
229
            }
230
231
            // Useful for linking a pre-existing object
232
            $objId = (isset($metadata['obj_id']) ? $metadata['obj_id'] : null);
233
234
            if (isset($metadata['label'])) {
235
                $label = $this->translator()->translation($metadata['label']);
236
            }
237
238
            if (isset($metadata['filters'])) {
239
                $filters = $metadata['filters'];
240
            }
241
242
            if (isset($metadata['orders'])) {
243
                $orders = $metadata['orders'];
244
            }
245
246
            if (isset($metadata['num_per_page'])) {
247
                $numPerPage = $metadata['num_per_page'];
248
            }
249
250
            if (isset($metadata['page'])) {
251
                $page = $metadata['page'];
252
            }
253
254
            $out[$type] = [
255
                'objId'      => $objId,
256
                'label'      => $label,
257
                'filters'    => $filters,
258
                'orders'     => $orders,
259
                'page'       => $page,
260
                'numPerPage' => $numPerPage,
261
                'data'       => $data
262
            ];
263
        }
264
265
        return $out;
266
    }
267
268
    /**
269
     * Formatted object types for use in templates.
270
     *
271
     * @return array
272
     */
273
    public function objectTypes()
274
    {
275
        $targetObjectTypes = $this->parsedTargetObjectTypes();
276
277
        $out = [];
278
279
        if (!$targetObjectTypes) {
280
            return $out;
281
        }
282
283
        $i = 0;
284
        foreach ($targetObjectTypes as $type => $metadata) {
285
            $i++;
286
            $label = $metadata['label'];
287
288
            $out[] = [
289
                'id'     => (isset($metadata['object_id']) ? $metadata['object_id'] : null),
290
                'ident'  => $this->createIdent($type),
291
                'label'  => $label,
292
                'val'    => $type,
293
                'active' => ($i == 1)
294
            ];
295
        }
296
297
        return $out;
298
    }
299
300
    /**
301
     * Set the widget's data.
302
     *
303
     * @param array|Traversable $data The widget data.
304
     * @return self
305
     */
306
    public function setData(array $data)
0 ignored issues
show
Coding Style introduced by
setData uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
307
    {
308
        $this->isMergingData = true;
309
        /**
310
         * @todo Kinda hacky, but works with the concept of form.
311
         *     Should work embeded in a form group or in a dashboard.
312
         */
313
        $data = array_merge($_GET, $data);
314
315
        parent::setData($data);
316
317
        /** Merge any available presets */
318
        $data = $this->mergePresets($data);
319
320
        parent::setData($data);
321
322
        $this->isMergingData = false;
323
324
        return $this;
325
    }
326
327
    /**
328
     * Set the current page listing of relations.
329
     *
330
     * @param integer $page The current page. Start at 0.
331
     * @throws InvalidArgumentException If the parameter is not numeric or < 0.
332
     * @return self
333
     */
334 View Code Duplication
    public function setPage($page)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
335
    {
336
        if (!is_numeric($page)) {
337
            throw new InvalidArgumentException(
338
                'Page number needs to be numeric.'
339
            );
340
        }
341
342
        $page = (int)$page;
343
344
        if ($page < 0) {
345
            throw new InvalidArgumentException(
346
                'Page number needs to be >= 0.'
347
            );
348
        }
349
350
        $this->page = $page;
351
352
        return $this;
353
    }
354
355
    /**
356
     * Retrieve the widget's title.
357
     *
358
     * @return Translation|string[]
359
     */
360
    public function title()
361
    {
362
        return $this->title;
363
    }
364
365
    /**
366
     * Set the widget's title.
367
     *
368
     * @param mixed $title The title for the current widget.
369
     * @return self
370
     */
371
    public function setTitle($title)
372
    {
373
        $this->title = $this->translator()->translation($title);
374
375
        return $this;
376
    }
377
378
    /**
379
     * Retrieve the title for the relation dialog.
380
     *
381
     * @return \Charcoal\Translator\Translation|string|null
382
     */
383
    public function dialogTitle()
384
    {
385
        if ($this->dialogTitle === null) {
386
            $this->setDialogTitle($this->defaultDialogTitle());
387
        }
388
389
        return $this->dialogTitle;
390
    }
391
392
    /**
393
     * Set the title for the relation dialog.
394
     *
395
     * @param  string|string[] $title The dialog title.
396
     * @return self
397
     */
398
    public function relationlogTitle($title)
399
    {
400
        $this->dialogTitle = $this->translator()->translation($title);
401
402
        return $this;
403
    }
404
405
    /**
406
     * Retrieve the default title for the relation dialog.
407
     *
408
     * @return \Charcoal\Translator\Translation|string|null
409
     */
410
    protected function defaultDialogTitle()
411
    {
412
        return $this->translator()->translation('Link an object');
413
    }
414
415
    /**
416
     * Create or load the object.
417
     *
418
     * @return ModelInterface
419
     */
420
    protected function createOrLoadObj()
421
    {
422
        $obj = $this->createOrCloneOrLoadObj();
423
424
        $obj->setData([
425
            'relation_widget' => $this
426
        ]);
427
428
        return $obj;
429
    }
430
431
    /**
432
     * Relations by object type.
433
     *
434
     * @return Collection
435
     */
436
    public function relations()
437
    {
438
        $relations = $this->obj()->pivots($this->group());
439
440
        foreach ($relations as $relation) {
441
            yield $relation;
442
        }
443
    }
444
445
    /**
446
     * Determine the number of relations.
447
     *
448
     * @return boolean
449
     */
450
    public function hasRelations()
451
    {
452
        return count(iterator_to_array($this->relations()));
453
    }
454
455
    /**
456
     * Retrieves a Closure that prepends relative URIs with the project's base URI.
457
     *
458
     * @return callable
459
     */
460
    public function withBaseUrl()
461
    {
462
        static $search;
463
464
        if ($search === null) {
465
            $attr = [ 'href', 'link', 'url', 'src' ];
466
            $uri  = [
467
                '../', './', '/', 'data', 'fax', 'file', 'ftp', 'geo',
468
                'http', 'mailto', 'sip', 'tag', 'tel', 'urn'
469
            ];
470
471
            $search = sprintf(
472
                '(?<=%1$s=["\'])(?!%2$s)(\S+)(?=["\'])',
473
                implode('=["\']|', array_map('preg_quote', $attr)),
474
                implode('|', array_map('preg_quote', $uri))
475
            );
476
        }
477
478
        /**
479
         * Prepend the project's base URI to all relative URIs in HTML attributes (e.g., src, href).
480
         *
481
         * @param  string       $text   Text to parse.
482
         * @param  LambdaHelper $helper For rendering strings in the current context.
483
         * @return string
484
         */
485
        $lambda = function ($text, LambdaHelper $helper) use ($search) {
486
            $text = $helper->render($text);
487
488
            if (preg_match('~'.$search.'~i', $text)) {
489
                $base = $helper->render('{{ baseUrl }}');
490
                return preg_replace('~'.$search.'~i', $base.'$1', $text);
491
            } elseif ($this->baseUrl instanceof \Psr\Http\Message\UriInterface) {
492
                if ($text && strpos($text, ':') === false && !in_array($text[0], [ '/', '#', '?' ])) {
493
                    return $this->baseUrl->withPath($text);
494
                }
495
            }
496
497
            return $text;
498
        };
499
        $lambda = $lambda->bindTo($this);
500
501
        return $lambda;
502
    }
503
504
    /**
505
     * Set how many relations are displayed per page.
506
     *
507
     * @param integer $num The number of results to retrieve, per page.
508
     * @throws InvalidArgumentException If the parameter is not numeric or < 0.
509
     * @return self
510
     */
511 View Code Duplication
    public function setNumPerPage($num)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
512
    {
513
        if (!is_numeric($num)) {
514
            throw new InvalidArgumentException(
515
                'Num-per-page needs to be numeric.'
516
            );
517
        }
518
519
        $num = (int)$num;
520
521
        if ($num < 0) {
522
            throw new InvalidArgumentException(
523
                'Num-per-page needs to be >= 0.'
524
            );
525
        }
526
527
        $this->numPerPage = $num;
528
529
        return $this;
530
    }
531
532
    /**
533
     * Retrieve the current widget's options as a JSON object.
534
     *
535
     * @return string A JSON string.
536
     */
537
    public function widgetOptions()
538
    {
539
        $options = [
540
            'target_object_types' => $this->targetObjectTypes(),
541
            'title'               => $this->title(),
542
            'obj_type'            => $this->obj()->objType(),
543
            'obj_id'              => $this->obj()->id(),
544
            'group'               => $this->group()
545
        ];
546
547
        return json_encode($options, true);
548
    }
549
550
    /**
551
     * Determine if the widget has an object assigned to it.
552
     *
553
     * @return boolean
554
     */
555
    public function hasObj()
556
    {
557
        return !!($this->obj()->id());
558
    }
559
560
    /**
561
     * Generate an HTML-friendly identifier.
562
     *
563
     * @param  string $string A dirty string to filter.
564
     * @return string
565
     */
566
    public function createIdent($string)
567
    {
568
        return preg_replace('~/~', '-', $string);
569
    }
570
571
    /**
572
     * Parse the given data and recursively merge presets from RelationConfig.
573
     *
574
     * @param  array $data The widget data.
575
     * @return array Returns the merged widget data.
576
     */
577
    protected function mergePresets(array $data)
578
    {
579
        if (isset($data['target_object_types'])) {
580
            $data['target_object_types'] = $this->mergePresetTargetObjectTypes($data['target_object_types']);
581
        }
582
583
        if (isset($data['preset'])) {
584
            $data = $this->mergePresetWidget($data);
585
        }
586
587
        return $data;
588
    }
589
590
    /**
591
     * Parse the given data and merge the widget preset.
592
     *
593
     * @param  array $data The widget data.
594
     * @return array Returns the merged widget data.
595
     */
596
    private function mergePresetWidget(array $data)
597
    {
598
        if (!isset($data['preset']) || !is_string($data['preset'])) {
599
            return $data;
600
        }
601
602
        $widgetIdent = $data['preset'];
603
        if ($this->hasObj()) {
604
            $widgetIdent = $this->obj()->render($widgetIdent);
605
        }
606
607
        $presetWidgets = $this->config('widgets');
608
        if (!isset($presetWidgets[$widgetIdent])) {
609
            return $data;
610
        }
611
612
        $widgetData = $presetWidgets[$widgetIdent];
613
        if (isset($widgetData['target_object_types'])) {
614
            $widgetData['target_object_types'] =
615
                $this->mergePresetTargetObjectTypes($widgetData['target_object_types']);
616
        }
617
618
        return array_replace_recursive($widgetData, $data);
619
    }
620
}
621