Passed
Pull Request — master (#120)
by Will
05:22
created

TagField::getSchemaStateDefaults()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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