Passed
Pull Request — master (#120)
by Will
02:27
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
eloc 4
dl 0
loc 7
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:client/dist/styles/bundle.css');
206
        Requirements::javascript('silverstripe/tagfield:client/dist/js/bundle.js');
207
208
        $schema = [
209
            'name' => $this->getName() . '[]',
210
            'lazyLoad' => $this->getShouldLazyLoad(),
211
            'creatable' => $this->getCanCreate(),
212
            'multi' => $this->getIsMultiple(),
213
            'value' => $this->Value(),
214
            'disabled' => $this->isDisabled() || $this->isReadonly(),
215
        ];
216
        if (!$this->getShouldLazyLoad()) {
217
            $schema['options'] = array_values($this->getOptions()->toNestedArray());
218
        } else {
219
            if ($this->Value()) {
220
                $schema['value'] = $this->getOptions(true)->toNestedArray();
0 ignored issues
show
Unused Code introduced by
The call to SilverStripe\TagField\TagField::getOptions() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

220
                $schema['value'] = $this->/** @scrutinizer ignore-call */ getOptions(true)->toNestedArray();

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
221
            }
222
            $schema['optionUrl'] = $this->getSuggestURL();
223
        }
224
        $this->setAttribute('data-schema', Convert::array2json($schema));
0 ignored issues
show
Bug introduced by
The type SilverStripe\TagField\Convert 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...
225
226
        $this->addExtraClass('ss-tag-field');
227
228
        return $this
229
            ->customise($properties)
230
            ->renderWith(self::class);
231
    }
232
233
    /**
234
     * @return string
235
     */
236
    protected function getSuggestURL()
237
    {
238
        return Controller::join_links($this->Link(), 'suggest');
239
    }
240
241
    /**
242
     * @return ArrayList
243
     */
244
    protected function getOptions()
245
    {
246
        $options = ArrayList::create();
247
248
        $source = $this->getSourceList();
249
250
        if (!$source) {
0 ignored issues
show
introduced by
$source is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
251
            $source = ArrayList::create();
252
        }
253
254
        $dataClass = $source->dataClass();
255
256
        $values = $this->Value();
257
258
        if (!$values) {
259
            return $options;
260
        }
261
262
        if (is_array($values)) {
263
            $values = DataList::create($dataClass)
264
                ->filter($this->getTitleField(), $values);
265
        }
266
267
        $ids = $values->column($this->getTitleField());
268
269
        $titleField = $this->getTitleField();
270
271
        foreach ($source as $object) {
272
            $options->push(
273
                ArrayData::create(array(
274
                    'Title' => $object->$titleField,
275
                    'Value' => $object->ID,
276
                    'Selected' => in_array($object->$titleField, $ids),
277
                ))
278
            );
279
        }
280
281
        return $options;
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287
    public function setValue($value, $source = null)
288
    {
289
        if ($source instanceof DataObject) {
290
            $name = $this->getName();
291
292
            if ($source->hasMethod($name)) {
293
                $values = [];
294
                $titleField = $this->getTitleField();
295
296
                foreach ($source->$name() as $tag) {
297
                    $values[] = [
298
                        'Title' => $tag->$titleField,
299
                        'Value' => $tag->ID,
300
                        'Selected' => true
301
                    ];
302
                }
303
304
                return parent::setValue($values);
305
            }
306
        }
307
308
        if (!is_array($value)) {
309
            return parent::setValue($value);
310
        }
311
312
        return parent::setValue($value);
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function getAttributes()
319
    {
320
        return array_merge(
321
            parent::getAttributes(),
322
            [
323
                'name' => $this->getName() . '[]',
324
                'style'=> 'width: 100%'
325
            ]
326
        );
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     */
332
    public function saveInto(DataObjectInterface $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
345
        if (empty($record) || empty($titleField)) {
346
            return;
347
        }
348
349
        if (!$record->hasMethod($name)) {
350
            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...
351
                sprintf("%s does not have a %s method", get_class($record), $name)
352
            );
353
        }
354
355
        foreach ($values as $key => $value) {
356
            // Get or create record
357
            $record = $this->getOrCreateTag($value);
358
            if ($record) {
359
                $ids[] = $record->ID;
360
                $values[$key] = $record->Title;
361
            }
362
        }
363
364
        $relation->setByIDList(array_filter($ids));
365
    }
366
367
    /**
368
     * Get or create tag with the given value
369
     *
370
     * @param  string $term
371
     * @return DataObject
372
     */
373
    protected function getOrCreateTag($term)
374
    {
375
        // Check if existing record can be found
376
        /** @var DataList $source */
377
        $source = $this->getSourceList();
378
        $titleField = $this->getTitleField();
379
        $record = $source
380
            ->filter($titleField, $term)
381
            ->first();
382
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
383
            return $record;
384
        }
385
386
        // Create new instance if not yet saved
387
        if ($this->getCanCreate()) {
388
            $dataClass = $source->dataClass();
389
            $record = Injector::inst()->create($dataClass);
390
391
            if (is_array($term)) {
392
                $term = $term['Value'];
393
            }
394
395
            $record->{$titleField} = $term;
396
            $record->write();
397
            return $record;
398
        } else {
399
            return false;
400
        }
401
    }
402
403
    /**
404
     * Returns a JSON string of tags, for lazy loading.
405
     *
406
     * @param  HTTPRequest $request
407
     * @return HTTPResponse
408
     */
409
    public function suggest(HTTPRequest $request)
410
    {
411
        $tags = $this->getTags($request->getVar('term'));
412
413
        $response = new HTTPResponse();
414
        $response->addHeader('Content-Type', 'application/json');
415
        $response->setBody(json_encode(array('items' => $tags)));
416
417
        return $response;
418
    }
419
420
    /**
421
     * Returns array of arrays representing tags.
422
     *
423
     * @param  string $term
424
     * @return array
425
     */
426
    protected function getTags($term)
427
    {
428
        /**
429
         * @var array $source
430
         */
431
        $source = $this->getSourceList();
432
433
        $titleField = $this->getTitleField();
434
435
        $query = $source
436
            ->filter($titleField . ':PartialMatch:nocase', $term)
437
            ->sort($titleField)
438
            ->limit($this->getLazyLoadItemLimit());
439
440
        // Map into a distinct list
441
        $items = array();
442
        $titleField = $this->getTitleField();
443
        foreach ($query->map('ID', $titleField) as $id => $title) {
444
            $items[$title] = array(
445
                'Title' => $title,
446
                'Value' => $id
447
            );
448
        }
449
450
        return array_values($items);
451
    }
452
453
    /**
454
     * DropdownField assumes value will be a scalar so we must
455
     * override validate. This only applies to Silverstripe 3.2+
456
     *
457
     * @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...
458
     * @return bool
459
     */
460
    public function validate($validator)
461
    {
462
        return true;
463
    }
464
465
    /**
466
     * Converts the field to a readonly variant.
467
     *
468
     * @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...
469
     */
470
    public function performReadonlyTransformation()
471
    {
472
        $copy = $this->castedCopy(ReadonlyTagField::class);
473
        $copy->setSourceList($this->getSourceList());
474
        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...
475
    }
476
477
    public function getSchemaStateDefaults()
478
    {
479
        $data = parent::getSchemaStateDefaults();
480
481
        // Add options to 'data'
482
        $data['lazyLoad'] = $this->getShouldLazyLoad();
483
        $data['multi'] = $this->getIsMultiple();
484
        $data['optionUrl'] = $this->getSuggestURL();
485
        $data['creatable'] = $this->getCanCreate();
486
        $data['value'] = $this->Value();
487
488
489
        return $data;
490
    }
491
}
492
493