UserIndexesController::elementQuery()   C
last analyzed

Complexity

Conditions 9
Paths 16

Size

Total Lines 64
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
dl 0
loc 64
ccs 0
cts 46
cp 0
rs 6.5449
c 0
b 0
f 0
cc 9
eloc 36
nc 16
nop 0
crap 90

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
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/organization/license
6
 * @link       https://www.flipboxfactory.com/software/organization/
7
 */
8
9
namespace flipbox\organization\controllers;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\base\ElementAction;
14
use craft\base\ElementActionInterface;
15
use craft\base\ElementInterface;
16
use craft\controllers\BaseElementsController;
17
use craft\elements\db\ElementQuery;
18
use craft\elements\db\ElementQueryInterface;
19
use craft\elements\User;
20
use craft\events\ElementActionEvent;
21
use craft\events\RegisterElementActionsEvent;
22
use craft\events\RegisterElementSourcesEvent;
23
use craft\helpers\ElementHelper;
24
use flipbox\organization\elements\actions\RemoveUsers;
25
use flipbox\organization\elements\db\User as UserQuery;
26
use yii\base\Event;
27
use yii\web\BadRequestHttpException;
28
use yii\web\ForbiddenHttpException;
29
use yii\web\Response;
30
31
/**
32
 * @author Flipbox Factory <[email protected]>
33
 * @since 1.0.0
34
 */
35
class UserIndexesController extends BaseElementsController
36
{
37
38
    /**
39
     * @var string|null
40
     */
41
    private $elementType;
42
43
    /**
44
     * @var string|null
45
     */
46
    private $context;
47
48
    /**
49
     * @var string|null
50
     */
51
    private $sourceKey;
52
53
    /**
54
     * @var array|null
55
     */
56
    private $source;
57
58
    /**
59
     * @var array|null
60
     */
61
    private $viewState;
62
63
    /**
64
     * @var ElementQueryInterface|null
65
     */
66
    private $elementQuery;
67
68
    /**
69
     * @var ElementActionInterface[]|null
70
     */
71
    private $actions;
72
73
    /**
74
     * @inheritdoc
75
     */
76
    public function init()
77
    {
78
79
        // Register actions for our 'organization' source
80
        Event::on(
81
            User::class,
82
            User::EVENT_REGISTER_ACTIONS,
83
            function (RegisterElementActionsEvent $event) {
84
                if ($event->source == '*') {
85
                    $event->actions = [
86
                        [
87
                            'type' => RemoveUsers::class,
88
                            'organization' => $event->data['organization'] ?? null
89
                        ]
90
                    ];
91
                }
92
            },
93
            [
94
                'organization' => $this->getOrganizationIdFromRequest()
95
            ]
96
        );
97
98
        // Register actions for our 'organization' source
99
        Event::on(
100
            User::class,
101
            User::EVENT_REGISTER_SOURCES,
102
            function (RegisterElementSourcesEvent $event) {
103
                if ($event->context == 'index') {
104
                    $event->sources = [
105
                        [
106
                            'key' => '*',
107
                            'label' => Craft::t('organization', 'Organization users'),
108
                            'criteria' => ['status' => null],
109
                            'hasThumbs' => true
110
                        ]
111
                    ];
112
                }
113
            }
114
        );
115
116
        parent::init();
117
118
        $this->elementType = $this->elementType();
119
        $this->context = $this->context();
120
        $this->sourceKey = Craft::$app->getRequest()->getParam('source');
121
        $this->source = $this->source();
122
        $this->viewState = $this->viewState();
123
        $this->elementQuery = $this->elementQuery();
124
125
        if ($this->context === 'index' && $this->sourceKey !== null) {
126
            $this->actions = $this->availableActions();
127
        }
128
    }
129
130
    /**
131
     * @return mixed
132
     */
133
    private function getOrganizationIdFromRequest()
134
    {
135
        return Craft::$app->getRequest()->getParam('organization');
136
    }
137
138
    /**
139
     * THE REST IS COPIED FROM 'craft\controllers\ElementIndexesController' AS EVERYTHING IS PRETTY
140
     * MUCH PRIVATE :(
141
     */
142
143
144
    /**
145
     * Returns the element query that’s defining which elements will be returned in the current request.
146
     *
147
     * Other components can fetch this like so:
148
     *
149
     * ```php
150
     * $criteria = Craft::$app->controller->getElementQuery();
151
     * ```
152
     *
153
     * @return ElementQueryInterface
154
     */
155
    public function getElementQuery(): ElementQueryInterface
156
    {
157
        return $this->elementQuery;
158
    }
159
160
    /**
161
     * Renders and returns an element index container, plus its first batch of elements.
162
     *
163
     * @return Response
164
     */
165
    public function actionGetElements(): Response
166
    {
167
        $includeActions = ($this->context === 'index');
168
        $responseData = $this->elementResponseData(true, $includeActions);
169
170
        return $this->asJson($responseData);
171
    }
172
173
    /**
174
     * Renders and returns a subsequent batch of elements for an element index.
175
     *
176
     * @return Response
177
     */
178
    public function actionGetMoreElements(): Response
179
    {
180
        $responseData = $this->elementResponseData(false, false);
181
182
        return $this->asJson($responseData);
183
    }
184
185
    /**
186
     * Performs an action on one or more selected elements.
187
     *
188
     * @return Response
189
     * @throws BadRequestHttpException if the requested element action is not supported by the element type,
190
     * or its parameters didn’t validate
191
     */
192
    public function actionPerformAction(): Response
193
    {
194
        $this->requirePostRequest();
195
196
        $requestService = Craft::$app->getRequest();
197
        $elementsService = Craft::$app->getElements();
198
199
        $actionClass = $requestService->getRequiredBodyParam('elementAction');
200
        $elementIds = $requestService->getRequiredBodyParam('elementIds');
201
202
        // Find that action from the list of available actions for the source
203
        if (!empty($this->actions)) {
204
            /** @var ElementAction $availableAction */
205
            foreach ($this->actions as $availableAction) {
206
                if ($actionClass === get_class($availableAction)) {
207
                    $action = $availableAction;
208
                    break;
209
                }
210
            }
211
        }
212
213
        /** @noinspection UnSafeIsSetOverArrayInspection - FP */
214
        if (!isset($action)) {
215
            throw new BadRequestHttpException('Element action is not supported by the element type');
216
        }
217
218
        // Check for any params in the post data
219
        foreach ($action->settingsAttributes() as $paramName) {
220
            $paramValue = $requestService->getBodyParam($paramName);
221
222
            if ($paramValue !== null) {
223
                $action->$paramName = $paramValue;
224
            }
225
        }
226
227
        // Make sure the action validates
228
        if (!$action->validate()) {
229
            throw new BadRequestHttpException('Element action params did not validate');
230
        }
231
232
        // Perform the action
233
        /** @var ElementQuery $actionCriteria */
234
        $actionCriteria = clone $this->elementQuery;
235
        $actionCriteria->offset = 0;
236
        $actionCriteria->limit = null;
237
        $actionCriteria->orderBy = null;
238
        $actionCriteria->positionedAfter = null;
239
        $actionCriteria->positionedBefore = null;
240
        $actionCriteria->id = $elementIds;
241
242
        // Fire a 'beforePerformAction' event
243
        $event = new ElementActionEvent([
244
            'action' => $action,
245
            'criteria' => $actionCriteria
246
        ]);
247
248
        $elementsService->trigger($elementsService::EVENT_BEFORE_PERFORM_ACTION, $event);
249
250
        if ($event->isValid) {
251
            $success = $action->performAction($actionCriteria);
252
            $message = $action->getMessage();
253
254
            if ($success) {
255
                // Fire an 'afterPerformAction' event
256
                $elementsService->trigger($elementsService::EVENT_AFTER_PERFORM_ACTION, new ElementActionEvent([
257
                    'action' => $action,
258
                    'criteria' => $actionCriteria
259
                ]));
260
            }
261
        } else {
262
            $success = false;
263
            $message = $event->message;
264
        }
265
266
        // Respond
267
        $responseData = [
268
            'success' => $success,
269
            'message' => $message,
270
        ];
271
272
        if ($success) {
273
            // Send a new set of elements
274
            $responseData = array_merge($responseData, $this->elementResponseData(true, true));
275
        }
276
277
        return $this->asJson($responseData);
278
    }
279
280
    /**
281
     * Returns the source tree HTML for an element index.
282
     */
283
    public function actionGetSourceTreeHtml()
284
    {
285
        $this->requireAcceptsJson();
286
287
        $sources = Craft::$app->getElementIndexes()->getSources($this->elementType, $this->context);
288
289
        return $this->asJson([
290
            'html' => $this->getView()->renderTemplate('_elements/sources', [
291
                'sources' => $sources
292
            ])
293
        ]);
294
    }
295
296
    // Private Methods
297
    // =========================================================================
298
299
    /**
300
     * Returns the selected source info.
301
     *
302
     * @return array|null
303
     * @throws ForbiddenHttpException if the user is not permitted to access the requested source
304
     */
305
    private function source()
306
    {
307
        if ($this->sourceKey === null) {
308
            return null;
309
        }
310
311
        $source = ElementHelper::findSource($this->elementType, $this->sourceKey, $this->context);
312
313
        if ($source === null) {
314
            // That wasn't a valid source, or the user doesn't have access to it in this context
315
            throw new ForbiddenHttpException('User not permitted to access this source');
316
        }
317
318
        return $source;
319
    }
320
321
    /**
322
     * Returns the current view state.
323
     *
324
     * @return array
325
     */
326
    private function viewState(): array
327
    {
328
        $viewState = Craft::$app->getRequest()->getParam('viewState', []);
329
330
        if (empty($viewState['mode'])) {
331
            $viewState['mode'] = 'table';
332
        }
333
334
        return $viewState;
335
    }
336
337
    /**
338
     * Returns the element query based on the current params.
339
     *
340
     * @return ElementQueryInterface
341
     */
342
    private function elementQuery(): ElementQueryInterface
343
    {
344
        /** @var UserQuery $query */
345
        $query = new UserQuery(User::class);
346
347
        $request = Craft::$app->getRequest();
348
349
        // Does the source specify any criteria attributes?
350
        if (isset($this->source['criteria'])) {
351
            Craft::configure($query, $this->source['criteria']);
352
        }
353
354
        // Override with the request's params
355
        if ($criteria = $request->getBodyParam('criteria')) {
356
            Craft::configure($query, $criteria);
357
        }
358
359
        // Exclude descendants of the collapsed element IDs
360
        $collapsedElementIds = $request->getParam('collapsedElementIds');
361
362
        if ($collapsedElementIds) {
363
            // Get the actual elements
364
            $collapsedElementQuery = clone $query;
365
            /** @var Element[] $collapsedElements */
366
            $collapsedElements = $collapsedElementQuery
367
                ->id($collapsedElementIds)
368
                ->offset(0)
369
                ->limit(null)
370
                ->orderBy(['lft' => SORT_ASC])
371
                ->positionedAfter(null)
372
                ->positionedBefore(null)
373
                ->all();
374
375
            if (!empty($collapsedElements)) {
376
                $descendantIds = [];
377
378
                $descendantQuery = clone $query;
379
                $descendantQuery
380
                    ->offset(0)
381
                    ->limit(null)
382
                    ->orderBy(null)
383
                    ->positionedAfter(null)
384
                    ->positionedBefore(null);
385
386
                foreach ($collapsedElements as $element) {
387
                    // Make sure we haven't already excluded this one, because its ancestor is collapsed as well
388
                    if (in_array($element->id, $descendantIds, false)) {
389
                        continue;
390
                    }
391
392
                    $descendantQuery->descendantOf($element);
393
                    foreach ($descendantQuery->ids() as $id) {
394
                        $descendantIds[] = $id;
395
                    }
396
                }
397
398
                if (!empty($descendantIds)) {
399
                    $query->andWhere(['not', ['elements.id' => $descendantIds]]);
400
                }
401
            }
402
        }
403
404
        return $query;
405
    }
406
407
    /**
408
     * Returns the element data to be returned to the client.
409
     *
410
     * @param bool $includeContainer Whether the element container should be included in the response data
411
     * @param bool $includeActions Whether info about the available actions should be included in the response data
412
     *
413
     * @return array
414
     */
415
    private function elementResponseData(bool $includeContainer, bool $includeActions): array
416
    {
417
        $responseData = [];
418
419
        $view = $this->getView();
420
421
        // Get the action head/foot HTML before any more is added to it from the element HTML
422
        if ($includeActions) {
423
            $responseData['actions'] = $this->actionData();
424
            $responseData['actionsHeadHtml'] = $view->getHeadHtml();
425
            $responseData['actionsFootHtml'] = $view->getBodyHtml();
426
        }
427
428
        $disabledElementIds = Craft::$app->getRequest()->getParam('disabledElementIds', []);
429
        $showCheckboxes = !empty($this->actions);
430
        /** @var string|ElementInterface $elementType */
431
        $elementType = $this->elementType;
432
433
        $responseData['html'] = $elementType::indexHtml(
434
            $this->elementQuery,
435
            $disabledElementIds,
436
            $this->viewState,
437
            $this->sourceKey,
438
            $this->context,
439
            $includeContainer,
440
            $showCheckboxes
441
        );
442
443
        $responseData['headHtml'] = $view->getHeadHtml();
444
        $responseData['footHtml'] = $view->getBodyHtml();
445
446
        return $responseData;
447
    }
448
449
    /**
450
     * Returns the available actions for the current source.
451
     *
452
     * @return ElementActionInterface[]|null
453
     */
454
    private function availableActions()
455
    {
456
        if (Craft::$app->getRequest()->isMobileBrowser()) {
457
            return null;
458
        }
459
460
        /** @var string|ElementInterface $elementType */
461
        $elementType = $this->elementType;
462
        $actions = $elementType::actions($this->sourceKey);
463
464
        foreach ($actions as $i => $action) {
465
            // $action could be a string or config array
466
            if (!$action instanceof ElementActionInterface) {
467
                $actions[$i] = $action = Craft::$app->getElements()->createAction($action);
468
469
                if ($actions[$i] === null) {
470
                    unset($actions[$i]);
471
                }
472
            }
473
        }
474
475
        return array_values($actions);
476
    }
477
478
    /**
479
     * Returns the data for the available actions.
480
     *
481
     * @return array|null
482
     */
483
    private function actionData()
484
    {
485
        if (empty($this->actions)) {
486
            return null;
487
        }
488
489
        $actionData = [];
490
491
        /** @var ElementAction $action */
492
        foreach ($this->actions as $action) {
493
            $actionData[] = [
494
                'type' => get_class($action),
495
                'destructive' => $action->isDestructive(),
496
                'name' => $action->getTriggerLabel(),
497
                'trigger' => $action->getTriggerHtml(),
498
                'confirm' => $action->getConfirmationMessage(),
499
            ];
500
        }
501
502
        return $actionData;
503
    }
504
}
505