Completed
Pull Request — master (#106)
by
unknown
03:26
created

TagField::validate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
c 0
b 0
f 0
rs 10
cc 1
eloc 1
nc 1
nop 1
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\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 DropdownField
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
    /**
64
     * @param string $name
65
     * @param string $title
66
     * @param null|DataList $source
67
     * @param null|DataList $value
68
     */
69
    public function __construct($name, $title = '', $source = array(), $value = null)
70
    {
71
        $this->setSourceList($source);
72
        parent::__construct($name, $title, $source, $value);
73
    }
74
75
    /**
76
     * @return bool
77
     */
78
    public function getShouldLazyLoad()
79
    {
80
        return $this->shouldLazyLoad;
81
    }
82
83
    /**
84
     * @param bool $shouldLazyLoad
85
     *
86
     * @return static
87
     */
88
    public function setShouldLazyLoad($shouldLazyLoad)
89
    {
90
        $this->shouldLazyLoad = $shouldLazyLoad;
91
92
        return $this;
93
    }
94
95
    /**
96
     * @return int
97
     */
98
    public function getLazyLoadItemLimit()
99
    {
100
        return $this->lazyLoadItemLimit;
101
    }
102
103
    /**
104
     * @param int $lazyLoadItemLimit
105
     *
106
     * @return static
107
     */
108
    public function setLazyLoadItemLimit($lazyLoadItemLimit)
109
    {
110
        $this->lazyLoadItemLimit = $lazyLoadItemLimit;
111
112
        return $this;
113
    }
114
115
    /**
116
     * @return bool
117
     */
118
    public function getIsMultiple()
119
    {
120
        return $this->isMultiple;
121
    }
122
123
    /**
124
     * @param bool $isMultiple
125
     *
126
     * @return static
127
     */
128
    public function setIsMultiple($isMultiple)
129
    {
130
        $this->isMultiple = $isMultiple;
131
132
        return $this;
133
    }
134
135
    /**
136
     * @return bool
137
     */
138
    public function getCanCreate()
139
    {
140
        return $this->canCreate;
141
    }
142
143
    /**
144
     * @param bool $canCreate
145
     *
146
     * @return static
147
     */
148
    public function setCanCreate($canCreate)
149
    {
150
        $this->canCreate = $canCreate;
151
152
        return $this;
153
    }
154
155
    /**
156
     * @return string
157
     */
158
    public function getTitleField()
159
    {
160
        return $this->titleField;
161
    }
162
163
    /**
164
     * @param string $titleField
165
     *
166
     * @return $this
167
     */
168
    public function setTitleField($titleField)
169
    {
170
        $this->titleField = $titleField;
171
172
        return $this;
173
    }
174
175
    /**
176
     * Get the DataList source. The 4.x upgrade for SelectField::setSource starts to convert this to an array
177
     * @return DataList
178
     */
179
    public function getSourceList()
180
    {
181
        return $this->sourceList;
182
    }
183
184
    /**
185
     * Set the model class name for tags
186
     * @param  DataList $className
187
     * @return self
188
     */
189
    public function setSourceList($sourceList)
190
    {
191
        $this->sourceList = $sourceList;
192
        return $this;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function Field($properties = array())
199
    {
200
        Requirements::css('silverstripe/tagfield:css/select2.min.css');
201
        Requirements::css('silverstripe/tagfield:css/TagField.css');
202
203
        Requirements::javascript('silverstripe/tagfield:js/select2.js');
204
        Requirements::javascript('silverstripe/tagfield:js/TagField.js');
205
206
        $this->addExtraClass('ss-tag-field');
207
208
        if ($this->getIsMultiple()) {
209
            $this->setAttribute('multiple', 'multiple');
210
        }
211
212
        if ($this->shouldLazyLoad) {
213
            $this->setAttribute('data-ss-tag-field-suggest-url', $this->getSuggestURL());
214
        } else {
215
            $properties = array_merge($properties, array(
216
                'Options' => $this->getOptions()
217
            ));
218
        }
219
220
        $this->setAttribute('data-can-create', (int) $this->getCanCreate());
221
222
        return $this
223
            ->customise($properties)
224
            ->renderWith(self::class);
225
    }
226
227
    /**
228
     * @return string
229
     */
230
    protected function getSuggestURL()
231
    {
232
        return Controller::join_links($this->Link(), 'suggest');
233
    }
234
235
    /**
236
     * @return ArrayList
237
     */
238
    protected function getOptions()
239
    {
240
        $options = ArrayList::create();
241
242
        $source = $this->getSourceList();
243
244
        if (!$source) {
0 ignored issues
show
introduced by
The condition ! $source can never be false.
Loading history...
245
            $source = ArrayList::create();
246
        }
247
248
        $dataClass = $source->dataClass();
249
250
        $values = $this->Value();
251
252
        if (!$values) {
253
            return $options;
254
        }
255
256
        if (is_array($values)) {
257
            $values = DataList::create($dataClass)->filter('Title', $values);
258
        }
259
260
        $ids = $values->column('Title');
261
262
        $titleField = $this->getTitleField();
263
264
        foreach ($source as $object) {
265
            $options->push(
266
                ArrayData::create(array(
267
                'Title' => $object->$titleField,
268
                'Value' => $object->Title,
269
                'Selected' => in_array($object->Title, $ids),
270
                ))
271
            );
272
        }
273
274
        return $options;
275
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280
    public function setValue($value, $source = null)
281
    {
282
        if ($source instanceof DataObject) {
283
            $name = $this->getName();
284
285
            if ($source->hasMethod($name)) {
286
                $value = $source->$name()->column('Title');
287
            }
288
        } elseif ($value instanceof SS_List) {
289
            $value = $value->column('Title');
290
        }
291
292
        if (!is_array($value)) {
293
            return parent::setValue($value);
294
        }
295
296
        return parent::setValue(array_filter($value));
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     */
302
    public function getAttributes()
303
    {
304
        return array_merge(
305
            parent::getAttributes(),
306
            array('name' => $this->getName() . '[]')
307
        );
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) {
0 ignored issues
show
introduced by
The condition $record can never be true.
Loading history...
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
The condition $record can never be 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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\ORM\DataObject.
Loading history...
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;
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...
452
    }
453
}
454