Passed
Push — master ( b4f0a0...3f9085 )
by
unknown
06:23 queued 04:24
created

TagField::getSchemaDataDefaults()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 24
rs 9.7666
c 0
b 0
f 0
cc 4
nc 3
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\Injector\Injector;
9
use SilverStripe\Forms\DropdownField;
10
use SilverStripe\Forms\Validator;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\ORM\DataList;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DataObjectInterface;
15
use SilverStripe\ORM\SS_List;
16
use SilverStripe\View\ArrayData;
17
use SilverStripe\View\Requirements;
18
19
/**
20
 * Provides a tagging interface, storing links between tag DataObjects and a parent DataObject.
21
 *
22
 * @package forms
23
 * @subpackage fields
24
 */
25
class TagField extends DropdownField
26
{
27
    /**
28
     * @var array
29
     */
30
    private static $allowed_actions = [
31
        'suggest',
32
    ];
33
34
    /**
35
     * @var bool
36
     */
37
    protected $shouldLazyLoad = false;
38
39
    /**
40
     * @var int
41
     */
42
    protected $lazyLoadItemLimit = 10;
43
44
    /**
45
     * @var bool
46
     */
47
    protected $canCreate = true;
48
49
    /**
50
     * @var string
51
     */
52
    protected $titleField = 'Title';
53
54
    /**
55
     * @var DataList
56
     */
57
    protected $sourceList;
58
59
    /**
60
     * @var bool
61
     */
62
    protected $isMultiple = true;
63
64
    /**
65
     * @param string $name
66
     * @param string $title
67
     * @param null|DataList $source
68
     * @param null|DataList $value
69
     * @param string $titleField
70
     */
71
    public function __construct($name, $title = '', $source = [], $value = null, $titleField = 'Title')
72
    {
73
        $this->setSourceList($source);
74
        $this->setTitleField($titleField);
75
        parent::__construct($name, $title, $source, $value);
76
    }
77
78
    /**
79
     * @return bool
80
     */
81
    public function getShouldLazyLoad()
82
    {
83
        return $this->shouldLazyLoad;
84
    }
85
86
    /**
87
     * @param bool $shouldLazyLoad
88
     *
89
     * @return static
90
     */
91
    public function setShouldLazyLoad($shouldLazyLoad)
92
    {
93
        $this->shouldLazyLoad = $shouldLazyLoad;
94
95
        return $this;
96
    }
97
98
    /**
99
     * @return int
100
     */
101
    public function getLazyLoadItemLimit()
102
    {
103
        return $this->lazyLoadItemLimit;
104
    }
105
106
    /**
107
     * @param int $lazyLoadItemLimit
108
     *
109
     * @return static
110
     */
111
    public function setLazyLoadItemLimit($lazyLoadItemLimit)
112
    {
113
        $this->lazyLoadItemLimit = $lazyLoadItemLimit;
114
115
        return $this;
116
    }
117
118
    /**
119
     * @return bool
120
     */
121
    public function getIsMultiple()
122
    {
123
        return $this->isMultiple;
124
    }
125
126
    /**
127
     * @param bool $isMultiple
128
     *
129
     * @return static
130
     */
131
    public function setIsMultiple($isMultiple)
132
    {
133
        $this->isMultiple = $isMultiple;
134
135
        return $this;
136
    }
137
138
    /**
139
     * @return bool
140
     */
141
    public function getCanCreate()
142
    {
143
        return $this->canCreate;
144
    }
145
146
    /**
147
     * @param bool $canCreate
148
     *
149
     * @return static
150
     */
151
    public function setCanCreate($canCreate)
152
    {
153
        $this->canCreate = $canCreate;
154
155
        return $this;
156
    }
157
158
    /**
159
     * @return string
160
     */
161
    public function getTitleField()
162
    {
163
        return $this->titleField;
164
    }
165
166
    /**
167
     * @param string $titleField
168
     *
169
     * @return $this
170
     */
171
    public function setTitleField($titleField)
172
    {
173
        $this->titleField = $titleField;
174
175
        return $this;
176
    }
177
178
    /**
179
     * Get the DataList source. The 4.x upgrade for SelectField::setSource starts to convert this to an array
180
     * @return DataList
181
     */
182
    public function getSourceList()
183
    {
184
        return $this->sourceList;
185
    }
186
187
    /**
188
     * Set the model class name for tags
189
     * @param  DataList $className
190
     * @return self
191
     */
192
    public function setSourceList($sourceList)
193
    {
194
        $this->sourceList = $sourceList;
195
        return $this;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function Field($properties = [])
202
    {
203
        $this->addExtraClass('ss-tag-field');
204
205
        return $this->customise($properties)->renderWith(self::class);
206
    }
207
208
    /**
209
     * Provide TagField data to the JSON schema for the frontend component
210
     *
211
     * @return array
212
     */
213
    public function getSchemaDataDefaults()
214
    {
215
        $schema = array_merge(
216
            parent::getSchemaDataDefaults(),
217
            [
218
                'name' => $this->getName() . '[]',
219
                'lazyLoad' => $this->getShouldLazyLoad(),
220
                'creatable' => $this->getCanCreate(),
221
                'multi' => $this->getIsMultiple(),
222
                'value' => $this->Value(),
223
                'disabled' => $this->isDisabled() || $this->isReadonly(),
224
            ]
225
        );
226
227
        if (!$this->getShouldLazyLoad()) {
228
            $schema['options'] = array_values($this->getOptions()->toNestedArray());
229
        } else {
230
            if ($this->Value()) {
231
                $schema['value'] = $this->getOptions(true)->toNestedArray();
232
            }
233
            $schema['optionUrl'] = $this->getSuggestURL();
234
        }
235
236
        return $schema;
237
    }
238
239
    /**
240
     * When not used in a React form factory context, this adds the schema data to SilverStripe template
241
     * rendered attributes lists
242
     *
243
     * @return array
244
     */
245
    public function getAttributes()
246
    {
247
        $attributes = parent::getAttributes();
248
        $attributes['data-schema'] = json_encode($this->getSchemaData());
249
        return $attributes;
250
    }
251
252
    /**
253
     * @return string
254
     */
255
    protected function getSuggestURL()
256
    {
257
        return Controller::join_links($this->Link(), 'suggest');
258
    }
259
260
    /**
261
     * @param bool $onlySelected Only return options that are selected
262
     * @return ArrayList
263
     */
264
    protected function getOptions($onlySelected = false)
265
    {
266
        $source = $this->getSourceList();
267
268
        if (!$source) {
0 ignored issues
show
introduced by
$source is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
269
            $source = ArrayList::create();
270
        }
271
272
        $dataClass = $source->dataClass();
273
        $titleField = $this->getTitleField();
274
        $values = $this->Value();
275
276
        if ($values) {
277
            if (is_array($values)) {
278
                $values = DataList::create($dataClass)->filter($titleField, $values);
279
            }
280
        }
281
        if ($onlySelected) {
282
            $source = $values;
283
        }
284
285
        return $source instanceof DataList ? $this->formatOptions($source) : ArrayList::create();
286
    }
287
288
    /**
289
     * @param DataList $source
290
     * @return ArrayList
291
     */
292
    protected function formatOptions(DataList $source)
293
    {
294
        $options = ArrayList::create();
295
        $titleField = $this->getTitleField();
296
297
        foreach ($source as $object) {
298
            $options->push(
299
                ArrayData::create([
300
                    'Title' => $object->$titleField,
301
                    'Value' => $object->Title,
302
                ])
303
            );
304
        }
305
306
        return $options;
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     */
312
    public function setValue($value, $source = null)
313
    {
314
        if ($source instanceof DataObject) {
315
            $name = $this->getName();
316
317
            if ($source->hasMethod($name)) {
318
                $value = $source->$name()->column($this->getTitleField());
319
            }
320
        } elseif ($value instanceof SS_List) {
321
            $value = $value->column($this->getTitleField());
322
        }
323
324
        if (!is_array($value)) {
325
            return parent::setValue($value);
326
        }
327
328
        return parent::setValue(array_filter($value));
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334
    public function saveInto(DataObjectInterface $record)
335
    {
336
        parent::saveInto($record);
337
338
        $name = $this->getName();
339
        $titleField = $this->getTitleField();
340
        $values = $this->Value();
341
        $relation = $record->$name();
342
        $ids = [];
343
344
        if (!$values) {
345
            $values = [];
346
        }
347
        if (empty($record) || empty($titleField)) {
348
            return;
349
        }
350
351
        if (!$record->hasMethod($name)) {
352
            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...
353
                sprintf("%s does not have a %s method", get_class($record), $name)
354
            );
355
        }
356
357
        foreach ($values as $key => $value) {
358
            // Get or create record
359
            $record = $this->getOrCreateTag($value);
360
            if ($record) {
361
                $ids[] = $record->ID;
362
                $values[$key] = $record->Title;
363
            }
364
        }
365
366
        $relation->setByIDList(array_filter($ids));
367
    }
368
369
    /**
370
     * Get or create tag with the given value
371
     *
372
     * @param  string $term
373
     * @return DataObject|bool
374
     */
375
    protected function getOrCreateTag($term)
376
    {
377
        // Check if existing record can be found
378
        $source = $this->getSourceList();
379
        $titleField = $this->getTitleField();
380
        $record = $source
381
            ->filter($titleField, $term)
382
            ->first();
383
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
384
            return $record;
385
        }
386
387
        // Create new instance if not yet saved
388
        if ($this->getCanCreate()) {
389
            $dataClass = $source->dataClass();
390
            $record = Injector::inst()->create($dataClass);
391
            $record->{$titleField} = $term;
392
            $record->write();
393
            return $record;
394
        }
395
396
        return false;
397
    }
398
399
    /**
400
     * Returns a JSON string of tags, for lazy loading.
401
     *
402
     * @param  HTTPRequest $request
403
     * @return HTTPResponse
404
     */
405
    public function suggest(HTTPRequest $request)
406
    {
407
        $tags = $this->getTags($request->getVar('term'));
408
409
        $response = HTTPResponse::create();
410
        $response->addHeader('Content-Type', 'application/json');
411
        $response->setBody(json_encode(['items' => $tags]));
412
413
        return $response;
414
    }
415
416
    /**
417
     * Returns array of arrays representing tags.
418
     *
419
     * @param  string $term
420
     * @return array
421
     */
422
    protected function getTags($term)
423
    {
424
        $source = $this->getSourceList();
425
426
        $titleField = $this->getTitleField();
427
428
        $query = $source
429
            ->filter($titleField . ':PartialMatch:nocase', $term)
430
            ->sort($titleField)
431
            ->limit($this->getLazyLoadItemLimit());
432
433
        // Map into a distinct list
434
        $items = [];
435
        $titleField = $this->getTitleField();
436
        foreach ($query->map('ID', $titleField) as $id => $title) {
437
            $items[$title] = [
438
                'id' => $title,
439
                'text' => $title,
440
            ];
441
        }
442
443
        return array_values($items);
444
    }
445
446
    /**
447
     * DropdownField assumes value will be a scalar so we must
448
     * override validate. This only applies to Silverstripe 3.2+
449
     *
450
     * @param Validator $validator
451
     * @return bool
452
     */
453
    public function validate($validator)
454
    {
455
        return true;
456
    }
457
458
    /**
459
     * Converts the field to a readonly variant.
460
     *
461
     * @return ReadonlyTagField
462
     */
463
    public function performReadonlyTransformation()
464
    {
465
        /** @var ReadonlyTagField $copy */
466
        $copy = $this->castedCopy(ReadonlyTagField::class);
467
        $copy->setSourceList($this->getSourceList());
468
        return $copy;
469
    }
470
471
    /**
472
     * Prevent the default, which would return "tag"
473
     *
474
     * @return string
475
     */
476
    public function Type()
477
    {
478
        return '';
479
    }
480
}
481