Passed
Pull Request — master (#113)
by
unknown
02:41
created

TagField::getAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
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\Convert;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\Forms\DropdownField;
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 = array(
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 = array())
202
    {
203
        Requirements::css('silverstripe/tagfield:client/dist/styles/bundle.css');
204
        Requirements::javascript('silverstripe/tagfield:client/dist/js/bundle.js');
205
206
        $schema = [
207
            'name' => $this->getName() . '[]',
208
            'lazyLoad' => $this->getShouldLazyLoad(),
209
            'creatable' => $this->getCanCreate(),
210
            'multi' => $this->getIsMultiple(),
211
            'value' => $this->Value(),
212
            'disabled' => $this->isDisabled() || $this->isReadonly(),
213
        ];
214
        if (!$this->getShouldLazyLoad()) {
215
            $schema['options'] = array_values($this->getOptions()->toNestedArray());
216
        } else {
217
            if ($this->Value()) {
218
                $schema['value'] = $this->getOptions(true)->toNestedArray();
219
            }
220
            $schema['optionUrl'] = $this->getSuggestURL();
221
        }
222
        $this->setAttribute('data-schema', Convert::array2json($schema));
223
224
        $this->addExtraClass('ss-tag-field');
225
226
        return $this
227
            ->customise($properties)
228
            ->renderWith(self::class);
229
    }
230
231
    /**
232
     * @return string
233
     */
234
    protected function getSuggestURL()
235
    {
236
        return Controller::join_links($this->Link(), 'suggest');
237
    }
238
239
    /**
240
     * @param bool $onlySelected Only return options that are selected
241
     * @return ArrayList
242
     */
243
    protected function getOptions($onlySelected = false)
244
    {
245
        $source = $this->getSourceList();
246
247
        if (!$source) {
0 ignored issues
show
introduced by
$source is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
248
            $source = ArrayList::create();
249
        }
250
251
        $dataClass = $source->dataClass();
252
        $titleField = $this->getTitleField();
253
        $values = $this->Value();
254
255
        if ($values) {
256
            if (is_array($values)) {
257
                $values = DataList::create($dataClass)->filter($titleField, $values);
258
            }
259
        }
260
        if ($onlySelected) {
261
            $source = $values;
262
        }
263
264
        return $source instanceof DataList ? $this->formatOptions($source) : ArrayList::create();
265
    }
266
267
    /**
268
     * @param DataList $source
269
     * @return ArrayList
270
     */
271
    protected function formatOptions(DataList $source)
272
    {
273
        $options = ArrayList::create();
274
        $titleField = $this->getTitleField();
275
276
        foreach ($source as $object) {
277
            $options->push(
278
                ArrayData::create(array(
279
                    'Title' => $object->$titleField,
280
                    'Value' => $object->Title,
281
                ))
282
            );
283
        }
284
285
        return $options;
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function setValue($value, $source = null)
292
    {
293
        if ($source instanceof DataObject) {
294
            $name = $this->getName();
295
296
            if ($source->hasMethod($name)) {
297
                $value = $source->$name()->column($this->getTitleField());
298
            }
299
        } elseif ($value instanceof SS_List) {
300
            $value = $value->column($this->getTitleField());
301
        }
302
303
        if (!is_array($value)) {
304
            return parent::setValue($value);
305
        }
306
307
        return parent::setValue(array_filter($value));
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     */
313
    public function saveInto(DataObjectInterface $record)
314
    {
315
        parent::saveInto($record);
316
317
        $name = $this->getName();
318
        $titleField = $this->getTitleField();
319
        $source = $this->getSource();
0 ignored issues
show
Unused Code introduced by
The assignment to $source is dead and can be removed.
Loading history...
320
        $values = $this->Value();
321
        $relation = $record->$name();
322
        $ids = array();
323
324
        if (!$values) {
325
            $values = array();
326
        }
327
        if (empty($record) || empty($titleField)) {
328
            return;
329
        }
330
331
        if (!$record->hasMethod($name)) {
332
            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...
333
                sprintf("%s does not have a %s method", get_class($record), $name)
334
            );
335
        }
336
337
        foreach ($values as $key => $value) {
338
            // Get or create record
339
            $record = $this->getOrCreateTag($value);
340
            if ($record) {
341
                $ids[] = $record->ID;
342
                $values[$key] = $record->Title;
343
            }
344
        }
345
346
        $relation->setByIDList(array_filter($ids));
347
    }
348
349
    /**
350
     * Get or create tag with the given value
351
     *
352
     * @param  string $term
353
     * @return DataObject
354
     */
355
    protected function getOrCreateTag($term)
356
    {
357
        // Check if existing record can be found
358
        /** @var DataList $source */
359
        $source = $this->getSourceList();
360
        $titleField = $this->getTitleField();
361
        $record = $source
362
            ->filter($titleField, $term)
363
            ->first();
364
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
365
            return $record;
366
        }
367
368
        // Create new instance if not yet saved
369
        if ($this->getCanCreate()) {
370
            $dataClass = $source->dataClass();
371
            $record = Injector::inst()->create($dataClass);
372
            $record->{$titleField} = $term;
373
            $record->write();
374
            return $record;
375
        } else {
376
            return false;
377
        }
378
    }
379
380
    /**
381
     * Returns a JSON string of tags, for lazy loading.
382
     *
383
     * @param  HTTPRequest $request
384
     * @return HTTPResponse
385
     */
386
    public function suggest(HTTPRequest $request)
387
    {
388
        $tags = $this->getTags($request->getVar('term'));
389
390
        $response = new HTTPResponse();
391
        $response->addHeader('Content-Type', 'application/json');
392
        $response->setBody(json_encode(array('items' => $tags)));
393
394
        return $response;
395
    }
396
397
    /**
398
     * Returns array of arrays representing tags.
399
     *
400
     * @param  string $term
401
     * @return array
402
     */
403
    protected function getTags($term)
404
    {
405
        /**
406
         * @var array $source
407
         */
408
        $source = $this->getSourceList();
409
410
        $titleField = $this->getTitleField();
411
412
        $query = $source
413
            ->filter($titleField . ':PartialMatch:nocase', $term)
414
            ->sort($titleField)
415
            ->limit($this->getLazyLoadItemLimit());
416
417
        // Map into a distinct list
418
        $items = array();
419
        $titleField = $this->getTitleField();
420
        foreach ($query->map('ID', $titleField) as $id => $title) {
421
            $items[$title] = array(
422
                'id' => $title,
423
                'text' => $title
424
            );
425
        }
426
427
        return array_values($items);
428
    }
429
430
    /**
431
     * DropdownField assumes value will be a scalar so we must
432
     * override validate. This only applies to Silverstripe 3.2+
433
     *
434
     * @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...
435
     * @return bool
436
     */
437
    public function validate($validator)
438
    {
439
        return true;
440
    }
441
442
    /**
443
     * Converts the field to a readonly variant.
444
     *
445
     * @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...
446
     */
447
    public function performReadonlyTransformation()
448
    {
449
        $copy = $this->castedCopy(ReadonlyTagField::class);
450
        $copy->setSourceList($this->getSourceList());
451
        return $copy;
452
    }
453
}
454