Completed
Push — develop ( b960b5...a87380 )
by Nate
01:43
created

Fields::getQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 10
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 9
nc 1
nop 2
crap 2
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/meta/license
6
 * @link       https://www.flipboxfactory.com/software/meta/
7
 */
8
9
namespace flipbox\meta\services;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\base\ElementInterface;
14
use craft\base\FieldInterface;
15
use craft\elements\db\ElementQueryInterface;
16
use craft\fields\BaseRelationField;
17
use flipbox\craft\sortable\associations\db\SortableAssociationQueryInterface;
18
use flipbox\craft\sortable\associations\records\SortableAssociationInterface;
19
use flipbox\craft\sortable\associations\services\SortableFields;
20
use flipbox\meta\db\MetaQuery;
21
use flipbox\meta\elements\Meta as MetaElement;
22
use flipbox\meta\fields\Meta;
23
use flipbox\meta\fields\Meta as MetaField;
24
use flipbox\meta\Meta as MetaPlugin;
25
use flipbox\meta\records\Meta as MetaRecord;
26
use yii\base\Exception;
27
28
/**
29
 * @author Flipbox Factory <[email protected]>
30
 * @since 1.0.0
31
 *
32
 * @method MetaQuery find()
33
 */
34
class Fields extends SortableFields
35
{
36
    /**
37
     * @inheritdoc
38
     */
39
    const SOURCE_ATTRIBUTE = MetaRecord::SOURCE_ATTRIBUTE;
40
41
    /**
42
     * @inheritdoc
43
     */
44
    const TARGET_ATTRIBUTE = MetaRecord::TARGET_ATTRIBUTE;
45
46
    /**
47
     * @inheritdoc
48
     */
49
    protected static function tableAlias(): string
50
    {
51
        return MetaRecord::tableAlias();
52
    }
53
54
    /**
55
     * @param FieldInterface $field
56
     * @throws Exception
57
     */
58
    private function ensureField(FieldInterface $field)
59
    {
60
        if (!$field instanceof MetaField) {
61
            throw new Exception(sprintf(
62
                "The field must be an instance of '%s', '%s' given.",
63
                (string)MetaField::class,
64
                (string)get_class($field)
65
            ));
66
        }
67
    }
68
69
    /**
70
     * @inheritdoc
71
     */
72
    public function getQuery(
73
        FieldInterface $field,
74
        ElementInterface $element = null
75
    ): SortableAssociationQueryInterface {
76
        /** @var MetaField $field */
77
        $this->ensureField($field);
78
79
        $query = MetaPlugin::getInstance()->getElements()->getQuery();
80
81
        $query->siteId = $this->targetSiteId($element);
82
        $query->fieldId = $field->id;
83
84
        return $query;
85
    }
86
87
88
    /*******************************************
89
     * NORMALIZE VALUE
90
     *******************************************/
91
92
    /**
93
     * @param FieldInterface $field
94
     * @param $value
95
     * @param ElementInterface|null $element
96
     * @return array
97
     */
98
    public function serializeValue(
99
        FieldInterface $field,
100
        $value,
101
        ElementInterface $element = null
102
    ): array {
103
        /** @var MetaQuery $value */
104
        $serialized = [];
105
        $new = 0;
106
107
        foreach ($value->all() as $meta) {
108
            $metaId = $meta->id ?? 'new' . ++$new;
109
            $serialized[$metaId] = [
110
                'enabled' => $meta->enabled,
111
                'fields' => $meta->getSerializedFieldValues(),
112
            ];
113
        }
114
115
        return $serialized;
116
    }
117
118
    /*******************************************
119
     * NORMALIZE VALUE
120
     *******************************************/
121
122
    /**
123
     * Accepts input data and converts it into an array of associated Meta elements
124
     *
125
     * @param FieldInterface $field
126
     * @param SortableAssociationQueryInterface $query
127
     * @param array $value
128
     * @param ElementInterface|null $element
129
     */
130
    protected function normalizeQueryInputValues(
131
        FieldInterface $field,
132
        SortableAssociationQueryInterface $query,
133
        array $value,
134
        ElementInterface $element = null
135
    ) {
136
        /** @var MetaField $field */
137
138
        $models = [];
139
        $sortOrder = 1;
140
        $prevElement = null;
141
        /** @var MetaElement|null $prevElement */
142
143
        // Get existing values
144
        $existingValues = $element === null ? [] : $this->getExistingValues($field, $value, $element);
145
        $ownerId = $element->id ?? null;
1 ignored issue
show
Bug introduced by
Accessing id on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
146
147
        foreach ($value as $metaId => $metaData) {
148
            // Is this new? (Or has it been deleted?)
149
            if (strpos($metaId, 'new') === 0 || !isset($existingValues[$metaId])) {
150
                $meta = MetaPlugin::getInstance()->getElements()->create([
151
                    'fieldId' => $field->id,
152
                    'ownerId' => $ownerId,
153
                    'siteId' => $this->targetSiteId($element)
154
                ]);
155
            } else {
156
                $meta = $existingValues[$metaId];
157
            }
158
159
            /** @var MetaElement $meta */
160
161
            $meta->enabled = (bool)$metaData['enabled'] ?? true;
162
            $meta->setOwnerId($ownerId);
163
164
            // Set the content post location on the element if we can
165
            $fieldNamespace = $element->getFieldParamNamespace();
166
167
            if ($fieldNamespace !== null) {
168
                $metaFieldNamespace = ($fieldNamespace ? $fieldNamespace . '.' : '') .
169
                    '.' . $field->handle .
170
                    '.' . $metaId .
171
                    '.fields';
172
                $meta->setFieldParamNamespace($metaFieldNamespace);
173
            }
174
175
            if (isset($metaData['fields'])) {
176
                $meta->setFieldValues($metaData['fields']);
177
            }
178
179
            $sortOrder++;
180
            $meta->sortOrder = $sortOrder;
181
182
            // Set the prev/next elements
183
            if ($prevElement) {
184
                $prevElement->setNext($meta);
185
                $meta->setPrev($prevElement);
186
            }
187
            $prevElement = $meta;
188
189
            $models[] = $meta;
190
        }
191
        $query->setCachedResult($models);
192
    }
193
194
    /**
195
     * @param array $values
196
     * @param ElementInterface $element
197
     * @return array
198
     */
199
    protected function getExistingValues(MetaField $field, array $values, ElementInterface $element): array
200
    {
201
        /** @var Element $element */
202
        if (!empty($element->id)) {
203
            $ids = [];
204
205
            foreach ($values as $metaId => $meta) {
206
                if (is_numeric($metaId) && $metaId !== 0) {
207
                    $ids[] = $metaId;
208
                }
209
            }
210
211
            if (!empty($ids)) {
212
                $oldMetaQuery = MetaPlugin::getInstance()->getElements()->getQuery();
213
                $oldMetaQuery->fieldId($field->id);
214
                $oldMetaQuery->ownerId($element->id);
215
                $oldMetaQuery->id($ids);
216
                $oldMetaQuery->limit(null);
217
                $oldMetaQuery->status(null);
218
                $oldMetaQuery->enabledForSite(false);
219
                $oldMetaQuery->siteId($element->siteId);
220
                $oldMetaQuery->indexBy('id');
221
                return $oldMetaQuery->all();
222
            }
223
        }
224
225
        return [];
226
    }
227
228
229
    /*******************************************
230
     * ELEMENT EVENTS
231
     *******************************************/
232
233
    /**
234
     * @param MetaField $field
235
     * @param ElementInterface $element
236
     * @return bool
237
     * @throws \Throwable
238
     */
239
    public function beforeElementDelete(MetaField $field, ElementInterface $element): bool
240
    {
241
        // Delete any meta elements that belong to this element(s)
242
        foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) {
243
            $query = MetaElement::find();
244
            $query->status(null);
245
            $query->enabledForSite(false);
246
            $query->fieldId($field->id);
247
            $query->siteId($siteId);
248
            $query->owner($element);
249
250
            /** @var MetaElement $meta */
251
            foreach ($query->all() as $meta) {
252
                Craft::$app->getElements()->deleteElement($meta);
253
            }
254
        }
255
256
        return true;
257
    }
258
259
    /**
260
     * @param Meta $field
261
     * @param ElementInterface $owner
262
     * @throws \Exception
263
     * @throws \Throwable
264
     * @throws \yii\db\Exception
265
     */
266
    public function afterElementSave(MetaField $field, ElementInterface $owner)
267
    {
268
        /** @var Element $owner */
269
270
        /** @var MetaQuery $query */
271
        $query = $owner->getFieldValue($field->handle);
272
273
        // Skip if the query's site ID is different than the element's
274
        // (Indicates that the value as copied from another site for element propagation)
275
        if ($query->siteId != $owner->siteId) {
1 ignored issue
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
276
            return;
277
        }
278
279
        if (null === ($elements = $query->getCachedResult())) {
280
            $query = clone $query;
281
            $query->status = null;
282
            $query->enabledForSite = false;
283
            $elements = $query->all(); // existing meta
284
        }
285
286
        $transaction = Craft::$app->getDb()->beginTransaction();
287
        try {
288
            // If this is a preexisting element, make sure that the blocks for this field/owner respect the field's translation setting
289
            if ($query->ownerId) {
290
                $this->applyFieldTranslationSetting($query->ownerId, $query->siteId, $field);
291
            }
292
293
            // If the query is set to fetch blocks of a different owner, we're probably duplicating an element
294
            if ($query->ownerId && $query->ownerId != $owner->id) {
1 ignored issue
show
Bug introduced by
Accessing id on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
295
                $this->duplicate($field, $owner, $query, $elements);
296
            } else {
297
                $this->save($field, $owner, $elements);
298
            }
299
300
            $transaction->commit();
301
        } catch (\Exception $e) {
302
            $transaction->rollback();
303
            throw $e;
304
        }
305
306
        return;
307
    }
308
309
    /**
310
     * @param MetaField $field
311
     * @param ElementInterface $owner
312
     * @param MetaQuery $query
313
     * @param array $elements
314
     * @throws \Throwable
315
     * @throws \craft\errors\InvalidElementException
316
     */
317
    private function duplicate(MetaField $field, ElementInterface $owner, MetaQuery $query, array $elements)
318
    {
319
        /** @var Element $owner */
320
321
        $newQuery = clone $query;
322
        $newQuery->ownerId = $owner->id;
323
        if (!$newQuery->exists()) {
324
            // Duplicate for the new owner
325
            $elementsService = Craft::$app->getElements();
326
            foreach ($elements as $element) {
327
                $elementsService->duplicateElement($element, [
328
                    'ownerId' => $owner->id,
329
                    'ownerSiteId' => $field->localize ? $owner->siteId : null
330
                ]);
331
            }
332
        }
333
    }
334
335
    /**
336
     * @param MetaField $field
337
     * @param ElementInterface $owner
338
     * @param MetaElement[] $elements
339
     * @throws Exception
340
     * @throws \Throwable
341
     * @throws \craft\errors\ElementNotFoundException
342
     */
343
    private function save(MetaField $field, ElementInterface $owner, array $elements)
344
    {
345
        /** @var Element $owner */
346
347
        $elementIds = [];
348
349
        // Only propagate the blocks if the owner isn't being propagated
350
        $propagate = !$owner->propagating;
351
352
        /** @var MetaElement $element */
353
        foreach ($elements as $element) {
354
            $element->setOwnerId($owner->id);
355
            $element->ownerSiteId = ($field->localize ? $owner->siteId : null);
356
            $element->propagating = $owner->propagating;
357
358
            Craft::$app->getElements()->saveElement($element, false, $propagate);
359
360
            $elementIds[] = $element->id;
361
        }
362
363
        // Delete any elements that have been removed
364
        $this->deleteOld($field, $owner, $elementIds);
365
    }
366
367
    /**
368
     * @param MetaField $field
369
     * @param ElementInterface $owner
370
     * @param array $excludeIds
371
     * @throws \Throwable
372
     */
373
    private function deleteOld(MetaField $field, ElementInterface $owner, array $excludeIds)
374
    {
375
        /** @var Element $owner */
376
377
        $deleteElementsQuery = MetaElement::find()
378
            ->status(null)
379
            ->enabledForSite(false)
380
            ->ownerId($owner->id)
381
            ->fieldId($field->id)
382
            ->where(['not', ['elements.id' => $excludeIds]]);
383
384
        if ($field->localize) {
385
            $deleteElementsQuery->ownerSiteId($owner->siteId);
386
        } else {
387
            $deleteElementsQuery->siteId($owner->siteId);
388
        }
389
390
        foreach ($deleteElementsQuery->all() as $deleteElement) {
391
            Craft::$app->getElements()->deleteElement($deleteElement);
392
        }
393
    }
394
395
    /**
396
     * Applies the field's translation setting to a set of blocks.
397
     *
398
     * @param int $ownerId
399
     * @param int $ownerSiteId
400
     * @param Meta $field
401
     * @throws Exception
402
     * @throws \Throwable
403
     * @throws \craft\errors\ElementNotFoundException
404
     */
405
    private function applyFieldTranslationSetting(int $ownerId, int $ownerSiteId, MetaField $field)
406
    {
407
        // If the field is translatable, see if there are any global blocks that should be localized
408
        if ($field->localize) {
409
            $elementQuery = MetaElement::find()
0 ignored issues
show
Bug introduced by
The method ownerSiteId() does not exist on craft\elements\db\ElementQuery. Did you maybe mean siteId()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
410
                ->fieldId($field->id)
411
                ->ownerId($ownerId)
412
                ->status(null)
413
                ->enabledForSite(false)
414
                ->limit(null)
415
                ->siteId($ownerSiteId)
416
                ->ownerSiteId(':empty:');
417
418
            $elements = $elementQuery->all();
419
420
            if (!empty($elements)) {
421
                // Find any relational fields on these blocks
422
                $relationFields = [];
423
424
                /** @var MetaElement $element */
425
                foreach ($elements as $element) {
426
                    foreach ($element->getFieldLayout()->getFields() as $field) {
427
                        if ($field instanceof BaseRelationField) {
428
                            $relationFields[] = $field->handle;
429
                        }
430
                    }
431
                    break;
432
                }
433
434
                // Prefetch the blocks in all the other sites, in case they have
435
                // any localized content
436
                $otherSiteBlocks = [];
437
                $allSiteIds = Craft::$app->getSites()->getAllSiteIds();
438
                foreach ($allSiteIds as $siteId) {
439
                    if ($siteId != $ownerSiteId) {
440
                        /** @var MetaElement[] $siteElements */
441
                        $siteElements = $otherSiteBlocks[$siteId] = $elementQuery->siteId($siteId)->all();
442
443
                        // Hard-set the relation IDs
444
                        foreach ($siteElements as $element) {
445
                            foreach ($relationFields as $handle) {
446
                                /** @var ElementQueryInterface $relationQuery */
447
                                $relationQuery = $element->getFieldValue($handle);
448
                                $element->setFieldValue($handle, $relationQuery->ids());
449
                            }
450
                        }
451
                    }
452
                }
453
454
                // Explicitly assign the current site's blocks to the current site
455
                foreach ($elements as $element) {
456
                    $element->ownerSiteId = $ownerSiteId;
457
                    Craft::$app->getElements()->saveElement($element, false);
458
                }
459
460
                // Now save the other sites' blocks as new site-specific blocks
461
                foreach ($otherSiteBlocks as $siteId => $siteElements) {
462
                    foreach ($siteElements as $element) {
463
                        $element->id = null;
464
                        $element->contentId = null;
465
                        $element->siteId = (int)$siteId;
466
                        $element->ownerSiteId = (int)$siteId;
467
                        Craft::$app->getElements()->saveElement($element, false);
468
                    }
469
                }
470
            }
471
        } else {
472
473
            // Otherwise, see if the field has any localized blocks that should be deleted
474
            foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) {
475
                if ($siteId != $ownerSiteId) {
476
                    $elements = MetaElement::find()
0 ignored issues
show
Bug introduced by
The method ownerSiteId() does not exist on craft\elements\db\ElementQuery. Did you maybe mean siteId()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
477
                        ->fieldId($field->id)
478
                        ->ownerId($ownerId)
479
                        ->status(null)
480
                        ->enabledForSite(false)
481
                        ->limit(null)
482
                        ->siteId($siteId)
483
                        ->ownerSiteId($siteId)
484
                        ->all();
485
486
                    foreach ($elements as $element) {
487
                        Craft::$app->getElements()->deleteElement($element);
488
                    }
489
                }
490
            }
491
        }
492
    }
493
494
    /**
495
     * @inheritdoc
496
     */
497
    protected function normalizeQueryInputValue(
498
        FieldInterface $field,
499
        $value,
500
        int &$sortOrder,
501
        ElementInterface $element = null
502
    ): SortableAssociationInterface {
503
504
        throw new Exception(__METHOD__ . ' is not implemented');
505
    }
506
}
507