Completed
Push — 1 ( 4b20af...ab6f58 )
by Guy
16s queued 12s
created

AddToCampaignHandler::addToCampaign()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 54
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 32
c 3
b 0
f 0
dl 0
loc 54
rs 8.4746
cc 7
nc 8
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CampaignAdmin;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\HTTPResponse_Exception;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Injector\Injectable;
11
use SilverStripe\Dev\Deprecation;
12
use SilverStripe\Forms\CheckboxField;
13
use SilverStripe\Forms\DropdownField;
14
use SilverStripe\Forms\HiddenField;
15
use SilverStripe\Forms\LiteralField;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\Form;
18
use SilverStripe\Forms\TextField;
19
use SilverStripe\ORM\ArrayList;
20
use SilverStripe\ORM\DataObject;
21
use SilverStripe\ORM\FieldType\DBHTMLText;
22
use SilverStripe\ORM\ValidationException;
23
use SilverStripe\ORM\ValidationResult;
24
use SilverStripe\Versioned\ChangeSet;
25
use SilverStripe\Versioned\ChangeSetItem;
26
use SilverStripe\Versioned\Versioned;
27
use SilverStripe\Core\Convert;
28
29
/**
30
 * Class AddToCampaignHandler - handle the AddToCampaign action.
31
 *
32
 * This is a class designed to be delegated to by a Form action handler method in the EditForm of a LeftAndMain
33
 * child class.
34
 *
35
 * Add To Campaign can be seen as an item action like "publish" or "rollback", but unlike those actions
36
 * it needs one additional piece of information to execute, the ChangeSet ID.
37
 *
38
 * So this handler does one of two things to respond to the action request, depending on whether the ChangeSet ID
39
 * was included in the submitted data
40
 * - If it was, perform the Add To Campaign action (as per any other action)
41
 * - If it wasn't, return a form to get the ChangeSet ID and then repeat this action submission
42
 *
43
 * To use, you'd add an action to your LeftAndMain subclass, like this:
44
 *
45
 *     function addtocampaign($data, $form) {
46
 *         $handler = AddToCampaignHandler::create($form, $data);
47
 *         return $handler->handle();
48
 *     }
49
 *
50
 *  and add an AddToCampaignHandler_FormAction to the EditForm, possibly through getCMSActions
51
 */
52
class AddToCampaignHandler
53
{
54
    use Injectable;
55
56
    /**
57
     * Parent controller for this form
58
     *
59
     * @var Controller
60
     */
61
    protected $controller;
62
63
    /**
64
     * The submitted form data
65
     *
66
     * @var array
67
     */
68
    protected $data;
69
70
    /**
71
     * Form name to use
72
     *
73
     * @var string
74
     */
75
    protected $name;
76
77
    /**
78
     * AddToCampaignHandler constructor.
79
     *
80
     * @param Controller $controller Controller for this form
81
     * @param array|DataObject $data The data submitted as part of that form
82
     * @param string $name Form name
83
     */
84
    public function __construct($controller = null, $data = [], $name = 'AddToCampaignForm')
85
    {
86
        $this->controller = $controller;
87
        if ($data instanceof DataObject) {
88
            $data = $data->toMap();
89
        }
90
        $this->data = $data;
91
        $this->name = $name;
92
    }
93
94
    /**
95
     * Perform the action. Either returns a Form or performs the action, as per the class doc
96
     *
97
     * @return DBHTMLText|HTTPResponse
98
     */
99
    public function handle()
100
    {
101
        Deprecation::notice('5.0', 'handle() will be removed. Use addToCampaign or Form directly');
102
        $object = $this->getObject($this->data['ID'], $this->data['ClassName']);
103
104
        if (empty($this->data['Campaign'])) {
105
            return $this->Form($object)->forTemplate();
106
        } else {
107
            return $this->addToCampaign($object, $this->data['Campaign']);
108
        }
109
    }
110
111
    /**
112
     * Get what ChangeSets are available for an item to be added to by this user
113
     *
114
     * @return ArrayList|ChangeSet[]
115
     */
116
    protected function getAvailableChangeSets()
117
    {
118
        return ChangeSet::get()
119
            ->filter([
120
                'State' => ChangeSet::STATE_OPEN,
121
                'IsInferred' => 0
122
            ])
123
            ->filterByCallback(function ($item) {
124
                /** @var ChangeSet $item */
125
                return $item->canView();
126
            });
127
    }
128
129
    /**
130
     * Get changesets that a given object is already in
131
     *
132
     * @param DataObject
133
     * @return ArrayList[ChangeSet]
0 ignored issues
show
Documentation Bug introduced by
The doc comment ArrayList[ChangeSet] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
134
     */
135
    protected function getInChangeSets($object)
136
    {
137
        $inChangeSetIDs = array_unique(ChangeSetItem::get_for_object($object)->column('ChangeSetID') ?? []);
138
        if ($inChangeSetIDs > 0) {
139
            $changeSets = $this->getAvailableChangeSets()->filter([
140
                'ID' => $inChangeSetIDs,
141
                'State' => ChangeSet::STATE_OPEN
142
            ]);
143
        } else {
144
            $changeSets = new ArrayList();
145
        }
146
147
        return $changeSets;
148
    }
149
150
    /**
151
     * Safely get a DataObject from a client-supplied ID and ClassName, checking: argument
152
     * validity; existence; and canView permissions.
153
     *
154
     * @param int $id The ID of the DataObject
155
     * @param string $class The Class of the DataObject
156
     * @return DataObject The referenced DataObject
157
     * @throws HTTPResponse_Exception
158
     */
159
    protected function getObject($id, $class)
160
    {
161
        $id = (int)$id;
162
        $class = ClassInfo::class_name($class);
163
164
        if (!$class
165
            || !is_subclass_of($class, DataObject::class)
166
            || !DataObject::has_extension($class, Versioned::class)
167
        ) {
168
            $this->controller->httpError(400, _t(
169
                __CLASS__ . '.ErrorGeneral',
170
                'We apologise, but there was an error'
171
            ));
172
            return null;
173
        }
174
175
        $object = DataObject::get($class)->byID($id);
176
177
        if (!$object) {
178
            $this->controller->httpError(404, _t(
179
                __CLASS__ . '.ErrorNotFound',
180
                'That {Type} couldn\'t be found',
181
                '',
182
                ['Type' => $class]
183
            ));
184
            return null;
185
        }
186
187
        if (!$object->canView()) {
188
            $this->controller->httpError(403, _t(
189
                __CLASS__ . '.ErrorItemPermissionDenied',
190
                'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign',
191
                '',
192
                ['ObjectTitle' => $object->Title]
193
            ));
194
            return null;
195
        }
196
197
        return $object;
198
    }
199
200
    /**
201
     * Builds a Form that mirrors the parent editForm, but with an extra field to collect the ChangeSet ID
202
     *
203
     * @param DataObject $object The object we're going to be adding to whichever ChangeSet is chosen
204
     * @return Form
205
     */
206
    public function Form($object)
207
    {
208
        $inChangeSets = $this->getInChangeSets($object);
209
        $inChangeSetIDs = $inChangeSets->column('ID');
210
211
        // Get changesets that can be added to
212
        $candidateChangeSets = $this->getAvailableChangeSets();
213
        if ($inChangeSetIDs) {
214
            $candidateChangeSets = $candidateChangeSets->exclude('ID', $inChangeSetIDs);
215
        }
216
217
        $canCreate = ChangeSet::singleton()->canCreate();
218
        $message = $this->getFormAlert($inChangeSets, $candidateChangeSets, $canCreate);
219
        $fields = new FieldList(array_filter([
220
            $message ? LiteralField::create("AlertMessages", $message) : null,
221
            HiddenField::create('ID', null, $object->ID),
222
            HiddenField::create('ClassName', null, $object->baseClass())
223
        ]));
224
225
        // Add fields based on available options
226
        $showSelect = $candidateChangeSets->count() > 0;
227
        if ($showSelect) {
228
            $campaignDropdown = DropdownField::create(
229
                'Campaign',
230
                _t(__CLASS__ . '.AddToCampaignAvailableLabel', 'Available campaigns'),
231
                $candidateChangeSets
232
            )
233
                ->setEmptyString(_t(__CLASS__ . '.AddToCampaignFormFieldLabel', 'Select a Campaign'))
234
                ->addExtraClass('noborder')
235
                ->addExtraClass('no-chosen');
236
            $fields->push($campaignDropdown);
237
238
            // Show visibilty toggle of other create field
239
            if ($canCreate) {
240
                $addCampaignSelect = CheckboxField::create('AddNewSelect', _t(
241
                    __CLASS__ . '.ADD_TO_A_NEW_CAMPAIGN',
242
                    'Add to a new campaign'
243
                ))
244
                    ->setAttribute('data-shows', 'NewTitle')
245
                    ->setSchemaData(['data' => ['shows' => 'NewTitle']]);
246
                $fields->push($addCampaignSelect);
247
            }
248
        }
249
        if ($canCreate) {
250
            $placeholder = _t(__CLASS__ . '.CREATE_NEW_PLACEHOLDER', 'Enter campaign name');
251
            $createBox = TextField::create(
252
                'NewTitle',
253
                _t(__CLASS__ . '.CREATE_NEW', 'Create a new campaign')
254
            )
255
                ->setAttribute('placeholder', $placeholder)
256
                ->setSchemaData(['attributes' => ['placeholder' => $placeholder]]);
257
            $fields->push($createBox);
258
        }
259
260
        $actions = FieldList::create();
261
        if ($canCreate || $showSelect) {
262
            $actions->push(
263
                AddToCampaignHandler_FormAction::create()
264
                    ->setTitle(_t(__CLASS__ . '.AddToCampaignAddAction', 'Add'))
265
                    ->addExtraClass('add-to-campaign__action')
266
            );
267
        }
268
269
        $form = Form::create(
270
            $this->controller,
271
            $this->name,
272
            $fields,
273
            $actions,
274
            AddToCampaignValidator::create()
275
        );
276
277
        $form->setHTMLID('Form_EditForm_AddToCampaign');
278
        $form->addExtraClass('form--no-dividers add-to-campaign__form');
279
280
        return $form;
281
    }
282
283
    /**
284
     * Performs the actual action of adding the object to the ChangeSet, once the ChangeSet ID is known
285
     *
286
     * @param DataObject $object The object to add to the ChangeSet
287
     * @param array|int $data Post data for this campaign form, or the ID of the campaign to add to
288
     * @return HTTPResponse
289
     * @throws ValidationException
290
     */
291
    public function addToCampaign($object, $data)
292
    {
293
        // Extract $campaignID from $data
294
        $campaignID = $this->getOrCreateCampaign($data);
295
296
        /** @var ChangeSet $changeSet */
297
        $changeSet = ChangeSet::get()->byID($campaignID);
298
299
        if (!$changeSet) {
0 ignored issues
show
introduced by
$changeSet is of type SilverStripe\Versioned\ChangeSet, thus it always evaluated to true.
Loading history...
300
            throw new ValidationException(_t(
301
                __CLASS__ . '.ErrorNotFound',
302
                'That {Type} couldn\'t be found',
303
                ['Type' => 'Campaign']
304
            ));
305
        }
306
307
        if (!$changeSet->canEdit()) {
308
            throw new ValidationException(_t(
309
                __CLASS__ . '.ErrorCampaignPermissionDenied',
310
                'It seems you don\'t have the necessary permissions to add {ObjectTitle} to {CampaignTitle}',
311
                [
312
                    'ObjectTitle' => $object->Title,
313
                    'CampaignTitle' => $changeSet->Title
314
                ]
315
            ));
316
        }
317
318
        $changeSet->addObject($object);
319
320
        $childObjects = $object->findRelatedObjects('cascade_add_to_campaign');
321
        if ($childObjects) {
0 ignored issues
show
introduced by
$childObjects is of type SilverStripe\ORM\ArrayList, thus it always evaluated to true.
Loading history...
322
            foreach ($childObjects as $childObject) {
323
                $changeSet->addObject($childObject);
324
            }
325
        }
326
327
        $request = $this->controller->getRequest();
328
        $message = _t(
329
            __CLASS__ . '.Success',
330
            'Successfully added <strong>{ObjectTitle}</strong> to <strong>{CampaignTitle}</strong>',
331
            [
332
                'ObjectTitle' => Convert::raw2xml($object->Title),
333
                'CampaignTitle' => Convert::raw2xml($changeSet->Title)
334
            ]
335
        );
336
        if ($request->getHeader('X-Formschema-Request')) {
337
            return $message;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $message returns the type string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
338
        } elseif (Director::is_ajax()) {
339
            $response = new HTTPResponse($message, 200);
340
341
            $response->addHeader('Content-Type', 'text/html; charset=utf-8');
342
            return $response;
343
        } else {
344
            return $this->controller->redirectBack();
345
        }
346
    }
347
348
    /**
349
     * Get descriptive alert to display at the top of the form
350
     *
351
     * @param ArrayList $inChangeSets List of changesets this item exists in
352
     * @param ArrayList $candidateChangeSets List of changesets this item could be added to
353
     * @param bool $canCreate
354
     * @return string
355
     */
356
    protected function getFormAlert($inChangeSets, $candidateChangeSets, $canCreate)
357
    {
358
        // In a subset of changesets
359
        if ($inChangeSets->count() > 0 && $candidateChangeSets->count() > 0) {
360
            return sprintf(
361
                '<div class="alert alert-info"><strong>%s</strong><br/>%s</div>',
362
                _t(
363
                    __CLASS__ . '.AddToCampaignInChangsetLabel',
364
                    'Heads up, this item is already in campaign(s):'
365
                ),
366
                Convert::raw2xml(implode(', ', $inChangeSets->column('Name')))
367
            );
368
        }
369
370
        // In all changesets
371
        if ($inChangeSets->count() > 0) {
372
            return sprintf(
373
                '<div class="alert alert-info"><strong>%s</strong><br/>%s</div>',
374
                _t(
375
                    __CLASS__ . '.AddToCampaignInChangsetLabelAll',
376
                    'Heads up, this item is already in ALL campaign(s):'
377
                ),
378
                Convert::raw2xml(implode(', ', $inChangeSets->column('Name')))
379
            );
380
        }
381
382
        // Create only
383
        if ($candidateChangeSets->count() === 0 && $canCreate) {
384
            return sprintf(
385
                '<div class="alert alert-info">%s</div>',
386
                _t(
387
                    __CLASS__ . '.NO_CAMPAIGNS',
388
                    "You currently don't have any campaigns. "
389
                    . "You can edit campaign details later in the Campaigns section."
390
                )
391
            );
392
        }
393
394
        // Can't select or create
395
        if ($candidateChangeSets->count() === 0 && !$canCreate) {
396
            return sprintf(
397
                '<div class="alert alert-warning">%s</div>',
398
                _t(
399
                    __CLASS__ . '.NO_CREATE',
400
                    "Oh no! You currently don't have any campaigns created. "
401
                    . "Your current login does not have privileges to create campaigns. "
402
                    . "Campaigns can only be created by users with Campaigns section rights."
403
                )
404
            );
405
        }
406
        return null;
407
    }
408
409
    /**
410
     * Find or build campaign from posted data
411
     *
412
     * @param array|int $data
413
     * @return int
414
     * @throws ValidationException
415
     */
416
    protected function getOrCreateCampaign($data)
417
    {
418
        // Create new campaign if selected
419
        if (is_array($data) && !empty($data['AddNewSelect']) // Explicitly click "Add to a new campaign"
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_array($data) && ! em... IssetNode && IssetNode, Probably Intended Meaning: is_array($data) && (! em...IssetNode && IssetNode)
Loading history...
420
            || (is_array($data) && !isset($data['Campaign']) && isset($data['NewTitle'])) // This is the only option
421
        ) {
422
            // Permission
423
            if (!ChangeSet::singleton()->canCreate()) {
424
                throw $this->validationResult(
425
                    _t(__CLASS__ . '.CREATE_DENIED', 'You do not have permission to create campaigns')
426
                );
427
            }
428
429
            // Check title is valid
430
            $title = $data['NewTitle'];
431
            if (empty($title)) {
432
                throw $this->validationResult(
433
                    _t(__CLASS__ . '.MISSING_TITLE', 'Campaign name is required'),
434
                    'NewTitle'
435
                );
436
            }
437
438
            // Prevent duplicates
439
            $hasExistingName = Changeset::get()
440
                    ->filter('Name:nocase', $title)
441
                    ->count() > 0;
442
443
            if ($hasExistingName) {
444
                throw $this->validationResult(
445
                    _t(
446
                        'SilverStripe\\CampaignAdmin\\CampaignAdmin.ERROR_DUPLICATE_NAME',
447
                        'Name "{Name}" already exists',
448
                        ['Name' => $title]
449
                    ),
450
                    'NewTitle'
451
                );
452
            }
453
454
            // Create and return
455
            $campaign = ChangeSet::create();
456
            $campaign->Name = $title;
457
            $campaign->write();
458
            return $campaign->ID;
459
        }
460
461
        // Get selected campaign ID
462
        $campaignID = null;
463
        if (is_array($data) && !empty($data['Campaign'])) {
464
            $campaignID = $data['Campaign'];
465
        } elseif (is_numeric($data)) {
466
            $campaignID = (int)$data;
467
        }
468
        if (empty($campaignID)) {
469
            throw $this->validationResult(_t(__CLASS__ . '.NONE_SELECTED', 'No campaign selected'));
470
        }
471
        return $campaignID;
472
    }
473
474
    /**
475
     * Raise validation error
476
     *
477
     * @param string $message
478
     * @param string $field
479
     * @return ValidationException
480
     */
481
    protected function validationResult($message, $field = null)
482
    {
483
        $error = ValidationResult::create()
484
            ->addFieldError($field, $message);
485
        return new ValidationException($error);
486
    }
487
}
488