Issues (35)

src/Forms/CMSNicetiesEasyRelationshipField.php (4 issues)

1
<?php
2
3
namespace Sunnysideup\CMSNiceties\Forms;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Injector\Injector;
7
use SilverStripe\Forms\CompositeField;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\Forms\GridField\GridField;
10
use SilverStripe\Forms\GridField\GridField_ActionMenu;
11
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
12
use SilverStripe\Forms\GridField\GridFieldAddNewButton;
13
use SilverStripe\Forms\GridField\GridFieldConfig;
14
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
15
use SilverStripe\Forms\GridField\GridFieldDataColumns;
16
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
17
use SilverStripe\Forms\GridField\GridFieldDetailForm;
18
use SilverStripe\Forms\HeaderField;
19
use SilverStripe\ORM\DataList;
20
use SilverStripe\ORM\DataObject;
21
use SilverStripe\Versioned\GridFieldArchiveAction;
22
use SilverStripe\Versioned\Versioned;
23
use SilverStripe\Versioned\VersionedGridFieldDetailForm;
24
25
// use Symbiote\GridFieldExtensions\GridFieldOrderableRows;
26
// TODO: use undefinedoffset/sortablegridfield
27
28
/**
29
 * usage:
30
 *      $fields->addFieldToTab(
31
 *          'Root.RelationFoo',
32
 *          CMSNicetiesEasyRelationshipField::create($this, 'RelationFoo')
33
 *              ->setSortField('SortOrder')
34
 *              ->setLabelForField('Check this Out')
35
 *              ->setHasEditRelation(true)
36
 *              ->setHasUnlink(true)
37
 *              ->setHasDelete(true)
38
 *              ->setHasAdd(true)
39
 *              ->setHasAddExisting(true)
40
 *              ->setMaxItemsForCheckBoxSet(150)
41
 *              ->setDataColumns(['Title' => 'My Title'])
42
 *              ->setSearchFields(['Title' => 'My Title'])
43
 *              ->setSearchOutputFormat('')
44
 *      );.
45
 */
46
class CMSNicetiesEasyRelationshipField extends CompositeField
47
{
48
    /**
49
     * the object calling this class, aka the class where we add the fields.
50
     *
51
     * @var object
52
     */
53
    protected $callingObject;
54
55
    /**
56
     * name of the relations e.g. Members as defined in has_many or many_many.
57
     *
58
     * @var string
59
     */
60
    protected $relationName = '';
61
62
    /**
63
     * name of the class that we are linking to.
64
     *
65
     * @var string
66
     */
67
    protected $relationClassName = '';
68
69
    /**
70
     * name of the sort field used
71
     * works with:
72
     *  - UndefinedOffset\SortableGridField\Forms\GridFieldSortableRows.
73
     *
74
     * @var string
75
     */
76
    protected $sortField = '';
77
78
    /**
79
     * @var array|string
80
     */
81
    protected $checkBoxSort;
82
83
    /**
84
     * heading above field.
85
     *
86
     * @var string
87
     */
88
    protected $labelForField = '';
89
90
    /**
91
     * name for Add - e.g. My Product resulting in a button "Add My Product".
92
     *
93
     * @var string
94
     */
95
    protected $addLabel = '';
96
97
    /**
98
     * should the relationship be editable in the form?
99
     *
100
     * @var bool
101
     */
102
    protected $hasEditRelation = true;
103
104
    /**
105
     * can the link be removed?
106
     *
107
     * @var bool
108
     */
109
    protected $hasUnlink = true;
110
111
    /**
112
     * can the linked item be deleted?
113
     *
114
     * @var bool
115
     */
116
    protected $hasDelete = true;
117
118
    /**
119
     * can new items be added?
120
     *
121
     * @var bool
122
     */
123
    protected $hasAdd = true;
124
125
    /**
126
     * can existing items be linked?
127
     *
128
     * @var bool
129
     */
130
    protected $hasAddExisting = true;
131
132
    /**
133
     * @var int
134
     */
135
    protected $maxItemsForCheckBoxSet = 300;
136
137
    /**
138
     * data columns.
139
     *
140
     * @var array
141
     */
142
    protected $dataColumns = [];
143
144
    /**
145
     * data columns.
146
     *
147
     * @var array
148
     */
149
    protected $searchFields = [];
150
151
    /**
152
     * data columns.
153
     *
154
     * @var string
155
     */
156
    protected $searchOutputFormat = '';
157
158
    /**
159
     * @var null|GridFieldConfig
160
     */
161
    private $gridFieldConfig;
162
163
    /**
164
     * @var null|GridField
165
     */
166
    private $gridField;
167
168
    /**
169
     * @var null|CheckboxSetFieldWithLinks
170
     */
171
    private $checkboxSetField;
172
173
    /**
174
     * @var null|DataList
175
     */
176
    private $dataListForCheckboxSetField;
177
178
    /**
179
     * provides a generic Grid Field for Many Many relations.
180
     *
181
     * @param DataObject $callingObject Name of the Relationship - e.g. MyWidgets
182
     * @param string     $relationName  Name of the Relationship - e.g. MyWidgets
183
     *
184
     * @return array
185
     */
186
    public function __construct($callingObject, $relationName)
187
    {
188
        $this->callingObject = $callingObject;
189
        $this->relationName = $relationName;
190
191
        parent::__construct();
192
        $this->checkIfFieldsHaveBeenBuilt();
193
    }
194
195
    public function doBuild($force = false)
196
    {
197
        if ($this->listIsEmpty() || $force) {
198
            $this->getChildren();
199
        }
200
    }
201
202
    public function setSortField(string $sortField): self
203
    {
204
        $this->checkIfFieldsHaveBeenBuilt();
205
        $this->sortField = $sortField;
206
207
        return $this;
208
    }
209
210
    public function setCheckBoxSort($sortArrayOrString): self
211
    {
212
        $this->checkIfFieldsHaveBeenBuilt();
213
        $this->checkBoxSort = $sortArrayOrString;
214
215
        return $this;
216
    }
217
218
    public function setLabelForField(string $labelForField): self
219
    {
220
        $this->checkIfFieldsHaveBeenBuilt();
221
        $this->labelForField = $labelForField;
222
223
        return $this;
224
    }
225
226
    public function setAddLabel(string $addLabel): self
227
    {
228
        $this->checkIfFieldsHaveBeenBuilt();
229
        $this->addLabel = $addLabel;
230
231
        return $this;
232
    }
233
234
    public function setHasEditRelation(bool $hasEditRelation): self
235
    {
236
        $this->checkIfFieldsHaveBeenBuilt();
237
        $this->hasEditRelation = $hasEditRelation;
238
239
        return $this;
240
    }
241
242
    public function setHasUnlink(bool $hasUnlink): self
243
    {
244
        $this->checkIfFieldsHaveBeenBuilt();
245
        $this->hasUnlink = $hasUnlink;
246
247
        return $this;
248
    }
249
250
    public function setHasDelete(bool $hasDelete): self
251
    {
252
        $this->checkIfFieldsHaveBeenBuilt();
253
        $this->hasDelete = $hasDelete;
254
255
        return $this;
256
    }
257
258
    public function setHasAdd(bool $hasAdd): self
259
    {
260
        $this->checkIfFieldsHaveBeenBuilt();
261
        $this->hasAdd = $hasAdd;
262
263
        return $this;
264
    }
265
266
    public function setHasAddExisting(bool $hasAddExisting): self
267
    {
268
        $this->checkIfFieldsHaveBeenBuilt();
269
        $this->hasAddExisting = $hasAddExisting;
270
271
        return $this;
272
    }
273
274
    public function setMaxItemsForCheckBoxSet(int $maxItemsForCheckBoxSet): self
275
    {
276
        $this->checkIfFieldsHaveBeenBuilt();
277
        $this->maxItemsForCheckBoxSet = $maxItemsForCheckBoxSet;
278
279
        return $this;
280
    }
281
282
    public function setDataColumns(array $dataColumns): self
283
    {
284
        $this->checkIfFieldsHaveBeenBuilt();
285
        $this->dataColumns = $dataColumns;
286
287
        return $this;
288
    }
289
290
    public function setSearchFields(array $searchFields): self
291
    {
292
        $this->checkIfFieldsHaveBeenBuilt();
293
        $this->searchFields = $searchFields;
294
295
        return $this;
296
    }
297
298
    public function setSearchOutputFormat(string $searchOutputFormat): self
299
    {
300
        $this->searchOutputFormat = $searchOutputFormat;
301
302
        return $this;
303
    }
304
305
    public function setDataListForCheckboxSetField(DataList $list): self
306
    {
307
        $this->dataListForCheckboxSetField = $list;
308
309
        return $this;
310
    }
311
312
    public function getDetailedFields()
313
    {
314
        $this->doBuild();
315
        $type = $this->getRelationClassName();
316
        $obj = $type::create();
317
        $defaultFields = $obj->getCMSFields();
318
319
        $this->setDetailedFields($defaultFields);
320
321
        return $defaultFields;
322
    }
323
324
    public function setDetailedFields(FieldList $fields)
325
    {
326
        $this->doBuild();
327
        $this->getDetailedForm()->setFields($fields);
0 ignored issues
show
The method setFields() does not exist on SilverStripe\Versioned\V...onedGridFieldDetailForm. ( Ignorable by Annotation )

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

327
        $this->getDetailedForm()->/** @scrutinizer ignore-call */ setFields($fields);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
328
    }
329
330
    /**
331
     * @return FieldList
332
     */
333
    public function getChildren()
334
    {
335
        if ($this->listIsEmpty()) {
336
            $isVersioned = $this->isVersioned();
337
            $hasCheckboxSet = $this->hasCheckboxSet();
338
            $this->sortField = $this->getSortField();
339
            $this->relationClassName = $this->getRelationClassName();
340
341
            if ('' === $this->labelForField) {
342
                $fieldLabels = Injector::inst()->get($this->callingObject->ClassName)->fieldLabels();
343
                $this->labelForField = $fieldLabels[$this->relationName] ?? $this->relationName;
344
            }
345
346
            $safeLabel = preg_replace('#[^A-Za-z0-9 ]#', '', (string) $this->labelForField);
347
348
            $this->getGridFieldConfig = $this->getGridFieldConfig();
0 ignored issues
show
Bug Best Practice introduced by
The property getGridFieldConfig does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
349
350
            if ($hasCheckboxSet) {
351
                $this->hasUnlink = false;
352
                $this->hasAddExisting = false;
353
            }
354
355
            $this->gridField = null;
356
            if ($this->hasGridField()) {
357
                $this->getGridFieldConfig->removeComponentsByType(GridField_ActionMenu::class);
358
                $this->gridField = GridField::create(
359
                    $this->relationName . 'GridField',
360
                    $this->labelForField,
361
                    $this->callingObject->{$this->relationName}(),
362
                    $this->getGridFieldConfig
363
                );
364
365
                //we remove both - just in case the type is unknown.
366
                $this->getGridFieldConfig->removeComponentsByType(GridFieldArchiveAction::class);
367
                $this->getGridFieldConfig->removeComponentsByType(GridFieldDeleteAction::class);
368
369
                //deletes
370
                if ($this->hasDelete) {
371
                    if ($isVersioned) {
372
                        // $this->getGridFieldConfig->addComponent(new GridFieldArchiveAction());
373
                        // $this->getGridFieldConfig->addComponent(new GridFieldDeleteAction($unlink = false));
374
                    } else {
375
                        $this->getGridFieldConfig->addComponent(new GridFieldDeleteAction($unlink = false));
376
                    }
377
                } elseif ($this->hasUnlink) {
378
                    $this->getGridFieldConfig->addComponent(new GridFieldDeleteAction($unlink = true));
379
                }
380
381
                if ($this->hasAdd) {
382
                    if ('' !== $this->addLabel) {
383
                        $this->getGridFieldConfig->getComponentsByType(GridFieldAddNewButton::class)->first()->setButtonName('Add ' . $this->addLabel);
384
                    }
385
                } else {
386
                    $this->getGridFieldConfig->removeComponentsByType(GridFieldAddNewButton::class);
387
                }
388
389
                if (! $this->hasAddExisting) {
390
                    $this->getGridFieldConfig->removeComponentsByType(GridFieldAddExistingAutocompleter::class);
391
                }
392
393
                if ($hasCheckboxSet) {
394
                    $this->gridField->setTitle('Added ' . $this->labelForField);
395
                }
396
397
                if ('' !== $this->sortField) {
398
                    $classA = \UndefinedOffset\SortableGridField\Forms\GridFieldSortableRows::class;
0 ignored issues
show
The type UndefinedOffset\Sortable...s\GridFieldSortableRows 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...
399
                    if (class_exists($classA)) {
400
                        $this->getGridFieldConfig->addComponent($sorter = new $classA($this->sortField));
401
                        $sorter->setCustomRelationName($this->relationName);
402
                    }
403
                }
404
405
                if ([] !== $this->dataColumns) {
406
                    $dataColumns = $this->getGridFieldConfig->getComponentByType(GridFieldDataColumns::class);
407
                    if ($dataColumns) {
408
                        $dataColumns->setDisplayFields($this->dataColumns);
409
                    }
410
                }
411
412
                if ([] !== $this->searchFields) {
413
                    $autocompleter = $this->getGridFieldConfig->getComponentByType(GridFieldAddExistingAutocompleter::class);
414
                    if ($autocompleter) {
415
                        $autocompleter->setSearchFields($this->searchFields);
416
                    }
417
                }
418
419
                if ('' !== $this->searchOutputFormat) {
420
                    $autocompleter = $this->getGridFieldConfig->getComponentByType(GridFieldAddExistingAutocompleter::class);
421
                    if ($autocompleter) {
422
                        $autocompleter->setResultsFormat($this->searchOutputFormat);
423
                    }
424
                }
425
            }
426
427
            $this->checkboxSetField = null;
428
            if ($hasCheckboxSet) {
429
                $className = $this->relationClassName;
430
                $obj = Injector::inst()->get($className);
431
                if (null === $this->dataListForCheckboxSetField) {
432
                    $this->dataListForCheckboxSetField = $className::get();
433
                }
434
435
                if ($this->dataListForCheckboxSetField && $this->checkBoxSort) {
436
                    if (is_string($this->checkBoxSort)) {
437
                        $this->dataListForCheckboxSetField = $this->dataListForCheckboxSetField->orderBy($this->checkBoxSort);
438
                    } else {
439
                        $this->dataListForCheckboxSetField = $this->dataListForCheckboxSetField->sort($this->checkBoxSort);
440
                    }
441
                }
442
443
                if ($obj->hasMethod('getTitleForList')) {
444
                    $list = $this->dataListForCheckboxSetField->map('ID', 'getTitleForList');
445
                } else {
446
                    $list = $this->dataListForCheckboxSetField->map('ID', 'Title');
447
                }
448
449
                $this->checkboxSetField = CheckboxSetFieldWithLinks::create(
450
                    $this->relationName,
451
                    'Add / Remove',
452
                    $list
453
                )->setClassNameForLinks($this->relationClassName);
454
            }
455
456
            $fieldsArray = [
457
                HeaderField::create($safeLabel . 'Header', $this->labelForField, 1),
458
            ];
459
            if (null !== $this->gridField) {
460
                $fieldsArray[] = $this->gridField;
461
            }
462
463
            if (null !== $this->checkboxSetField) {
464
                $fieldsArray[] = $this->checkboxSetField;
465
            }
466
467
            $this->children = FieldList::create($fieldsArray);
468
            //important - as setChildren does more than just setting variable...
469
            $this->setChildren($this->children);
470
        }
471
472
        return $this->children;
473
    }
474
475
    public function getGridFieldConfig()
476
    {
477
        if (null === $this->gridFieldConfig) {
478
            $this->gridFieldConfig = GridFieldConfig_RelationEditor::create();
479
        }
480
481
        return $this->gridFieldConfig;
482
    }
483
484
    public function getGritField()
485
    {
486
        return $this->gridField;
487
    }
488
489
    public function getCheckboxSetField()
490
    {
491
        return $this->checkboxSetField;
492
    }
493
494
    protected function listIsEmpty(): bool
495
    {
496
        return ! $this->children || ($this->children instanceof FieldList && ! $this->children->exists());
497
    }
498
499
    protected function checkIfFieldsHaveBeenBuilt()
500
    {
501
        if ($this->listIsEmpty()) {
502
            //all good
503
        } else {
504
            user_error('There is an error in the sequence of your logic. The fields have already been built!');
505
        }
506
    }
507
508
    /**
509
     * @return GridFieldDetailForm|VersionedGridFieldDetailForm
510
     */
511
    protected function getDetailedForm()
512
    {
513
        $this->doBuild();
514
        $this->getGridFieldConfig = $this->getGridFieldConfig();
0 ignored issues
show
Bug Best Practice introduced by
The property getGridFieldConfig does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
515
516
        return $this->getGridFieldConfig->getComponentByType(GridFieldDetailForm::class);
517
    }
518
519
    private function getRelationClassName(): string
520
    {
521
        if ('' === $this->relationClassName) {
522
            $hasMany = Config::inst()->get($this->callingObject->ClassName, 'has_many');
523
            $manyMany = Config::inst()->get($this->callingObject->ClassName, 'many_many');
524
            $belongsManyMany = Config::inst()->get($this->callingObject->ClassName, 'belongs_many_many');
525
            foreach ([
526
                $hasMany,
527
                $manyMany,
528
                $belongsManyMany,
529
            ] as $types) {
530
                if (isset($types[$this->relationName])) {
531
                    $typeOptions = $types[$this->relationName];
532
                    $typeArray = explode('.', $typeOptions);
533
                    $this->relationClassName = $typeArray[0];
534
535
                    break;
536
                }
537
            }
538
        }
539
540
        if ($this->relationClassName && class_exists($this->relationClassName)) {
541
            return $this->relationClassName;
542
        }
543
544
        user_error('Can not find related class: ' . $this->relationClassName);
545
546
        return 'error';
547
    }
548
549
    private function isVersioned(): bool
550
    {
551
        $this->relationClassName = $this->getRelationClassName();
552
        $foreignSingleton = Injector::inst()->get($this->relationClassName);
553
554
        return (bool) $foreignSingleton->hasExtension(Versioned::class);
555
    }
556
557
    private function hasCheckboxSet(): bool
558
    {
559
        if ($this->callingObject->canEdit()) {
560
            $this->relationClassName = $this->getRelationClassName();
561
            $className = $this->relationClassName;
562
563
            return $className::get()->count() < $this->maxItemsForCheckBoxSet;
564
        }
565
566
        return false;
567
    }
568
569
    private function hasGridField(): bool
570
    {
571
        //do we need it to edit the relationship?
572
        if ($this->hasEditRelation || $this->hasDelete || $this->hasAdd || $this->sortField) {
573
            return true;
574
        }
575
576
        // do we need it because we do not have a checkboxset?
577
        //we can go without!
578
        return ! $this->hasCheckboxSet();
579
    }
580
581
    private function getSortField(): string
582
    {
583
        //todo - add undefinedoffset/sortablegridfield
584
        if ('' === $this->sortField) {
585
            $manyManyExtras = Config::inst()->get($this->callingObject->ClassName, 'many_many_extraFields');
586
            if (isset($manyManyExtras[$this->relationName])) {
587
                foreach ($manyManyExtras[$this->relationName] as $field => $tempType) {
588
                    if ('int' === strtolower((string) $tempType)) {
589
                        $this->sortField = $field;
590
                    }
591
                }
592
            }
593
        }
594
595
        return $this->sortField;
596
    }
597
}
598