Completed
Push — master ( bc7404...58edb1 )
by Robbie
14s queued 11s
created

TagField::getSchemaStateDefaults()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 13
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
namespace SilverStripe\TagField;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\HTTPResponse;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\Forms\MultiSelectField;
11
use SilverStripe\Forms\Validator;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\DataList;
14
use SilverStripe\ORM\DataObject;
15
use SilverStripe\ORM\DataObjectInterface;
16
use SilverStripe\View\ArrayData;
17
18
/**
19
 * Provides a tagging interface, storing links between tag DataObjects and a parent DataObject.
20
 *
21
 * @package forms
22
 * @subpackage fields
23
 */
24
class TagField extends MultiSelectField
25
{
26
    /**
27
     * @var array
28
     */
29
    private static $allowed_actions = [
30
        'suggest',
31
    ];
32
33
    /**
34
     * @var bool
35
     */
36
    protected $shouldLazyLoad = false;
37
38
    /**
39
     * @var int
40
     */
41
    protected $lazyLoadItemLimit = 10;
42
43
    /**
44
     * @var bool
45
     */
46
    protected $canCreate = true;
47
48
    /**
49
     * @var string
50
     */
51
    protected $titleField = 'Title';
52
53
    /**
54
     * @var DataList
55
     */
56
    protected $sourceList;
57
58
    /**
59
     * @var bool
60
     */
61
    protected $isMultiple = true;
62
63
    /** @skipUpgrade */
64
    protected $schemaComponent = 'TagField';
65
66
    /**
67
     * @param string $name
68
     * @param string $title
69
     * @param null|DataList $source
70
     * @param null|DataList $value
71
     * @param string $titleField
72
     */
73
    public function __construct($name, $title = '', $source = [], $value = null, $titleField = 'Title')
74
    {
75
        $this->setSourceList($source);
76
        $this->setTitleField($titleField);
77
        parent::__construct($name, $title, $source, $value);
78
79
        $this->addExtraClass('ss-tag-field');
80
    }
81
82
    /**
83
     * @return bool
84
     */
85
    public function getShouldLazyLoad()
86
    {
87
        return $this->shouldLazyLoad;
88
    }
89
90
    /**
91
     * @param bool $shouldLazyLoad
92
     *
93
     * @return static
94
     */
95
    public function setShouldLazyLoad($shouldLazyLoad)
96
    {
97
        $this->shouldLazyLoad = $shouldLazyLoad;
98
99
        return $this;
100
    }
101
102
    /**
103
     * @return int
104
     */
105
    public function getLazyLoadItemLimit()
106
    {
107
        return $this->lazyLoadItemLimit;
108
    }
109
110
    /**
111
     * @param int $lazyLoadItemLimit
112
     *
113
     * @return static
114
     */
115
    public function setLazyLoadItemLimit($lazyLoadItemLimit)
116
    {
117
        $this->lazyLoadItemLimit = $lazyLoadItemLimit;
118
119
        return $this;
120
    }
121
122
    /**
123
     * @return bool
124
     */
125
    public function getIsMultiple()
126
    {
127
        return $this->isMultiple;
128
    }
129
130
    /**
131
     * @param bool $isMultiple
132
     *
133
     * @return static
134
     */
135
    public function setIsMultiple($isMultiple)
136
    {
137
        $this->isMultiple = $isMultiple;
138
139
        return $this;
140
    }
141
142
    /**
143
     * @return bool
144
     */
145
    public function getCanCreate()
146
    {
147
        return $this->canCreate;
148
    }
149
150
    /**
151
     * @param bool $canCreate
152
     *
153
     * @return static
154
     */
155
    public function setCanCreate($canCreate)
156
    {
157
        $this->canCreate = $canCreate;
158
159
        return $this;
160
    }
161
162
    /**
163
     * @return string
164
     */
165
    public function getTitleField()
166
    {
167
        return $this->titleField;
168
    }
169
170
    /**
171
     * @param string $titleField
172
     *
173
     * @return $this
174
     */
175
    public function setTitleField($titleField)
176
    {
177
        $this->titleField = $titleField;
178
179
        return $this;
180
    }
181
182
    /**
183
     * Get the DataList source. The 4.x upgrade for SelectField::setSource starts to convert this to an array
184
     * @return DataList
185
     */
186
    public function getSourceList()
187
    {
188
        return $this->sourceList;
189
    }
190
191
    /**
192
     * Set the model class name for tags
193
     * @param  DataList $className
194
     * @return self
195
     */
196
    public function setSourceList($sourceList)
197
    {
198
        $this->sourceList = $sourceList;
199
        return $this;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function Field($properties = [])
206
    {
207
        $this->addExtraClass('entwine');
208
209
        return $this->customise($properties)->renderWith(self::class);
210
    }
211
212
    /**
213
     * Provide TagField data to the JSON schema for the frontend component
214
     *
215
     * @return array
216
     */
217
    public function getSchemaDataDefaults()
218
    {
219
        $options = $this->getOptions(true);
220
        $schema = array_merge(
221
            parent::getSchemaDataDefaults(),
222
            [
223
                'name' => $this->getName() . '[]',
224
                'lazyLoad' => $this->getShouldLazyLoad(),
225
                'creatable' => $this->getCanCreate(),
226
                'multi' => $this->getIsMultiple(),
227
                'value' => $options->count() ? $options->toNestedArray() : null,
228
                'disabled' => $this->isDisabled() || $this->isReadonly(),
229
            ]
230
        );
231
232
        if (!$this->getShouldLazyLoad()) {
233
            $schema['options'] = array_values($this->getOptions()->toNestedArray());
234
        } else {
235
            $schema['optionUrl'] = $this->getSuggestURL();
236
        }
237
238
        return $schema;
239
    }
240
241
    /**
242
     * @return string
243
     */
244
    protected function getSuggestURL()
245
    {
246
        return Controller::join_links($this->Link(), 'suggest');
247
    }
248
249
    /**
250
     * @return ArrayList
251
     */
252
    protected function getOptions($onlySelected = false)
253
    {
254
        $options = ArrayList::create();
255
        $source = $this->getSourceList();
256
257
        // No source means we have no options
258
        if (!$source) {
0 ignored issues
show
introduced by
$source is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
259
            return ArrayList::create();
260
        }
261
262
        $dataClass = $source->dataClass();
263
264
        $values = $this->Value();
265
266
        // If we have no values and we only want selected options we can bail here
267
        if (empty($values) && $onlySelected) {
268
            return ArrayList::create();
269
        }
270
271
        // Convert an array of values into a datalist of options
272
        if (is_array($values) && !empty($values)) {
273
            $values = DataList::create($dataClass)
274
                ->filter($this->getTitleField(), $values);
275
        } else {
276
            $values = ArrayList::create();
277
        }
278
279
        // Prep a function to parse a dataobject into an option
280
        $addOption = function (DataObject $item) use ($options, $values) {
281
            $titleField = $this->getTitleField();
282
            $option = $item->$titleField;
283
            $options->push(ArrayData::create([
284
                'Title' => $option,
285
                'Value' => $option,
286
                'Selected' => (bool) $values->find('ID', $item->ID)
287
            ]));
288
        };
289
290
        // Only parse the values if we only want the selected items in the values list (this is for lazy-loading)
291
        if ($onlySelected) {
292
            $values->each($addOption);
293
            return $options;
294
        }
295
296
        $source->each($addOption);
297
        return $options;
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303
    public function setValue($value, $source = null)
304
    {
305
        if ($source instanceof DataObject) {
306
            $name = $this->getName();
307
308
            if ($source->hasMethod($name)) {
309
                $value = $source->$name()->column($this->getTitleField());
310
            }
311
        }
312
313
        if (!is_array($value)) {
314
            return parent::setValue($value);
315
        }
316
317
        return parent::setValue(array_filter($value));
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323
    public function getAttributes()
324
    {
325
        return array_merge(
326
            parent::getAttributes(),
327
            [
328
                'name' => $this->getName() . '[]',
329
                'style' => 'width: 100%',
330
                'data-schema' => json_encode($this->getSchemaData()),
331
            ]
332
        );
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     */
338
    public function saveInto(DataObjectInterface $record)
339
    {
340
        $name = $this->getName();
341
        $titleField = $this->getTitleField();
342
        $values = $this->Value();
343
        $relation = $record->$name();
344
        $ids = [];
345
346
        if (!$values) {
347
            $values = [];
348
        }
349
350
        if (empty($record) || empty($titleField)) {
351
            return;
352
        }
353
354
        if (!$record->hasMethod($name)) {
355
            throw new Exception(
0 ignored issues
show
Bug introduced by
The type SilverStripe\TagField\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
356
                sprintf("%s does not have a %s method", get_class($record), $name)
357
            );
358
        }
359
360
        foreach ($values as $key => $value) {
361
            // Get or create record
362
            $record = $this->getOrCreateTag($value);
363
            if ($record) {
364
                $ids[] = $record->ID;
365
                $values[$key] = $record->Title;
366
            }
367
        }
368
369
        $relation->setByIDList(array_filter($ids));
370
    }
371
372
    /**
373
     * Get or create tag with the given value
374
     *
375
     * @param  string $term
376
     * @return DataObject|bool
377
     */
378
    protected function getOrCreateTag($term)
379
    {
380
        // Check if existing record can be found
381
        $source = $this->getSourceList();
382
        $titleField = $this->getTitleField();
383
        $record = $source
384
            ->filter($titleField, $term)
385
            ->first();
386
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
387
            return $record;
388
        }
389
390
        // Create new instance if not yet saved
391
        if ($this->getCanCreate()) {
392
            $dataClass = $source->dataClass();
393
            $record = Injector::inst()->create($dataClass);
394
395
            if (is_array($term)) {
396
                $term = $term['Value'];
397
            }
398
399
            $record->{$titleField} = $term;
400
            $record->write();
401
            if ($source instanceof SS_List) {
0 ignored issues
show
Bug introduced by
The type SilverStripe\TagField\SS_List was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
402
                $source->add($record);
403
            }
404
            return $record;
405
        }
406
407
        return false;
408
    }
409
410
    /**
411
     * Returns a JSON string of tags, for lazy loading.
412
     *
413
     * @param  HTTPRequest $request
414
     * @return HTTPResponse
415
     */
416
    public function suggest(HTTPRequest $request)
417
    {
418
        $tags = $this->getTags($request->getVar('term'));
419
420
        $response = HTTPResponse::create();
421
        $response->addHeader('Content-Type', 'application/json');
422
        $response->setBody(json_encode(['items' => $tags]));
423
424
        return $response;
425
    }
426
427
    /**
428
     * Returns array of arrays representing tags.
429
     *
430
     * @param  string $term
431
     * @return array
432
     */
433
    protected function getTags($term)
434
    {
435
        $source = $this->getSourceList();
436
437
        $titleField = $this->getTitleField();
438
439
        $query = $source
440
            ->filter($titleField . ':PartialMatch:nocase', $term)
441
            ->sort($titleField)
442
            ->limit($this->getLazyLoadItemLimit());
443
444
        // Map into a distinct list
445
        $items = [];
446
        $titleField = $this->getTitleField();
447
        foreach ($query->map('ID', $titleField) as $id => $title) {
448
            $items[$title] = [
449
                'Title' => $title,
450
                'Value' => $title,
451
            ];
452
        }
453
454
        return array_values($items);
455
    }
456
457
    /**
458
     * DropdownField assumes value will be a scalar so we must
459
     * override validate. This only applies to Silverstripe 3.2+
460
     *
461
     * @param Validator $validator
462
     * @return bool
463
     */
464
    public function validate($validator)
465
    {
466
        return true;
467
    }
468
469
    /**
470
     * Converts the field to a readonly variant.
471
     *
472
     * @return ReadonlyTagField
473
     */
474
    public function performReadonlyTransformation()
475
    {
476
        /** @var ReadonlyTagField $copy */
477
        $copy = $this->castedCopy(ReadonlyTagField::class);
478
        $copy->setSourceList($this->getSourceList());
479
        return $copy;
480
    }
481
482
    /**
483
     * Prevent the default, which would return "tag"
484
     *
485
     * @return string
486
     */
487
    public function Type()
488
    {
489
        return '';
490
    }
491
492
    public function getSchemaStateDefaults()
493
    {
494
        $data = parent::getSchemaStateDefaults();
495
496
        // Add options to 'data'
497
        $data['lazyLoad'] = $this->getShouldLazyLoad();
498
        $data['multi'] = $this->getIsMultiple();
499
        $data['optionUrl'] = $this->getSuggestURL();
500
        $data['creatable'] = $this->getCanCreate();
501
        $options = $this->getOptions(true);
502
        $data['value'] = $options->count() ? $options->toNestedArray() : null;
503
504
        return $data;
505
    }
506
}
507