Passed
Pull Request — master (#117)
by Damian
03:49
created

TagField::getSource()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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