Passed
Pull Request — master (#120)
by Guy
05:56
created

TagField::Field()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 2
c 6
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
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
        $this->setAttribute('data-schema', json_encode($schema));
238
239
        return $schema;
240
    }
241
242
    /**
243
     * @return string
244
     */
245
    protected function getSuggestURL()
246
    {
247
        return Controller::join_links($this->Link(), 'suggest');
248
    }
249
250
    /**
251
     * @return ArrayList
252
     */
253
    protected function getOptions($onlySelected = false)
254
    {
255
        $options = ArrayList::create();
256
        $source = $this->getSourceList();
257
258
        // No source means we have no options
259
        if (!$source) {
0 ignored issues
show
introduced by
$source is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
260
            return ArrayList::create();
261
        }
262
263
        $dataClass = $source->dataClass();
264
265
        $values = $this->Value();
266
267
        // If we have no values and we only want selected options we can bail here
268
        if (empty($values) && $onlySelected) {
269
            return ArrayList::create();
270
        }
271
272
        // Convert an array of values into a datalist of options
273
        if (is_array($values) && !empty($values)) {
274
            $values = DataList::create($dataClass)
275
                ->filter($this->getTitleField(), $values);
276
        } else {
277
            $values = ArrayList::create();
278
        }
279
280
        // Prep a function to parse a dataobject into an option
281
        $addOption = function (DataObject $item) use ($options, $values) {
282
            $titleField = $this->getTitleField();
283
            $options->push(ArrayData::create([
284
                'Title' => $item->$titleField,
285
                'Value' => $item->ID,
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
    public function getValueAsOptions()
321
    {
322
        $value = $this->Value();
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
323
324
//        return $this->getOptions()
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function getAttributes()
331
    {
332
        return array_merge(
333
            parent::getAttributes(),
334
            [
335
                'name' => $this->getName() . '[]',
336
                'style' => 'width: 100%',
337
                'data-schema' => json_encode($this->getSchemaData()),
338
            ]
339
        );
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345
    public function saveInto(DataObjectInterface $record)
346
    {
347
        $name = $this->getName();
348
        $titleField = $this->getTitleField();
349
        $values = $this->Value();
350
        $relation = $record->$name();
351
        $ids = [];
352
353
        if (!$values) {
354
            $values = [];
355
        }
356
357
        if (empty($record) || empty($titleField)) {
358
            return;
359
        }
360
361
        if (!$record->hasMethod($name)) {
362
            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...
363
                sprintf("%s does not have a %s method", get_class($record), $name)
364
            );
365
        }
366
367
        foreach ($values as $key => $value) {
368
            // Get or create record
369
            $record = $this->getOrCreateTag($value);
370
            if ($record) {
371
                $ids[] = $record->ID;
372
                $values[$key] = $record->Title;
373
            }
374
        }
375
376
        $relation->setByIDList(array_filter($ids));
377
    }
378
379
    /**
380
     * Get or create tag with the given value
381
     *
382
     * @param  string $term
383
     * @return DataObject|bool
384
     */
385
    protected function getOrCreateTag($term)
386
    {
387
        // Check if existing record can be found
388
        $source = $this->getSourceList();
389
        $titleField = $this->getTitleField();
390
        $record = $source
391
            ->filter($titleField, $term)
392
            ->first();
393
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
394
            return $record;
395
        }
396
397
        // Create new instance if not yet saved
398
        if ($this->getCanCreate()) {
399
            $dataClass = $source->dataClass();
400
            $record = Injector::inst()->create($dataClass);
401
402
            if (is_array($term)) {
403
                $term = $term['Value'];
404
            }
405
406
            $record->{$titleField} = $term;
407
            $record->write();
408
            return $record;
409
        }
410
411
        return false;
412
    }
413
414
    /**
415
     * Returns a JSON string of tags, for lazy loading.
416
     *
417
     * @param  HTTPRequest $request
418
     * @return HTTPResponse
419
     */
420
    public function suggest(HTTPRequest $request)
421
    {
422
        $tags = $this->getTags($request->getVar('term'));
423
424
        $response = HTTPResponse::create();
425
        $response->addHeader('Content-Type', 'application/json');
426
        $response->setBody(json_encode(['items' => $tags]));
427
428
        return $response;
429
    }
430
431
    /**
432
     * Returns array of arrays representing tags.
433
     *
434
     * @param  string $term
435
     * @return array
436
     */
437
    protected function getTags($term)
438
    {
439
        $source = $this->getSourceList();
440
441
        $titleField = $this->getTitleField();
442
443
        $query = $source
444
            ->filter($titleField . ':PartialMatch:nocase', $term)
445
            ->sort($titleField)
446
            ->limit($this->getLazyLoadItemLimit());
447
448
        // Map into a distinct list
449
        $items = [];
450
        $titleField = $this->getTitleField();
451
        foreach ($query->map('ID', $titleField) as $id => $title) {
452
            $items[$title] = [
453
                'Title' => $title,
454
                'Value' => $id,
455
            ];
456
        }
457
458
        return array_values($items);
459
    }
460
461
    /**
462
     * DropdownField assumes value will be a scalar so we must
463
     * override validate. This only applies to Silverstripe 3.2+
464
     *
465
     * @param Validator $validator
466
     * @return bool
467
     */
468
    public function validate($validator)
469
    {
470
        return true;
471
    }
472
473
    /**
474
     * Converts the field to a readonly variant.
475
     *
476
     * @return ReadonlyTagField
477
     */
478
    public function performReadonlyTransformation()
479
    {
480
        /** @var ReadonlyTagField $copy */
481
        $copy = $this->castedCopy(ReadonlyTagField::class);
482
        $copy->setSourceList($this->getSourceList());
483
        return $copy;
484
    }
485
486
    /**
487
     * Prevent the default, which would return "tag"
488
     *
489
     * @return string
490
     */
491
    public function Type()
492
    {
493
        return '';
494
    }
495
496
    public function getSchemaStateDefaults()
497
    {
498
        $data = parent::getSchemaStateDefaults();
499
500
        // Add options to 'data'
501
        $data['lazyLoad'] = $this->getShouldLazyLoad();
502
        $data['multi'] = $this->getIsMultiple();
503
        $data['optionUrl'] = $this->getSuggestURL();
504
        $data['creatable'] = $this->getCanCreate();
505
        $options = $this->getOptions(true);
506
        $data['value'] = $options->count() ? $options->toNestedArray() : null;
507
508
        return $data;
509
    }
510
}
511
512